Home | History | Annotate | Download | only in view
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.support.v4.view;
     18 
     19 import android.content.Context;
     20 import android.os.Handler;
     21 import android.os.Handler.Callback;
     22 import android.os.Looper;
     23 import android.os.Message;
     24 import android.support.annotation.LayoutRes;
     25 import android.support.annotation.NonNull;
     26 import android.support.annotation.Nullable;
     27 import android.support.annotation.UiThread;
     28 import android.support.v4.util.Pools.SynchronizedPool;
     29 import android.util.AttributeSet;
     30 import android.util.Log;
     31 import android.view.LayoutInflater;
     32 import android.view.View;
     33 import android.view.ViewGroup;
     34 
     35 import java.util.concurrent.ArrayBlockingQueue;
     36 
     37 /**
     38  * <p>Helper class for inflating layouts asynchronously. To use, construct
     39  * an instance of {@link AsyncLayoutInflater} on the UI thread and call
     40  * {@link #inflate(int, ViewGroup, OnInflateFinishedListener)}. The
     41  * {@link OnInflateFinishedListener} will be invoked on the UI thread
     42  * when the inflate request has completed.
     43  *
     44  * <p>This is intended for parts of the UI that are created lazily or in
     45  * response to user interactions. This allows the UI thread to continue
     46  * to be responsive & animate while the relatively heavy inflate
     47  * is being performed.
     48  *
     49  * <p>For a layout to be inflated asynchronously it needs to have a parent
     50  * whose {@link ViewGroup#generateLayoutParams(AttributeSet)} is thread-safe
     51  * and all the Views being constructed as part of inflation must not create
     52  * any {@link Handler}s or otherwise call {@link Looper#myLooper()}. If the
     53  * layout that is trying to be inflated cannot be constructed
     54  * asynchronously for whatever reason, {@link AsyncLayoutInflater} will
     55  * automatically fall back to inflating on the UI thread.
     56  *
     57  * <p>NOTE that the inflated View hierarchy is NOT added to the parent. It is
     58  * equivalent to calling {@link LayoutInflater#inflate(int, ViewGroup, boolean)}
     59  * with attachToRoot set to false. Callers will likely want to call
     60  * {@link ViewGroup#addView(View)} in the {@link OnInflateFinishedListener}
     61  * callback at a minimum.
     62  *
     63  * <p>This inflater does not support setting a {@link LayoutInflater.Factory}
     64  * nor {@link LayoutInflater.Factory2}. Similarly it does not support inflating
     65  * layouts that contain fragments.
     66  */
     67 public final class AsyncLayoutInflater {
     68     private static final String TAG = "AsyncLayoutInflater";
     69 
     70     LayoutInflater mInflater;
     71     Handler mHandler;
     72     InflateThread mInflateThread;
     73 
     74     public AsyncLayoutInflater(@NonNull Context context) {
     75         mInflater = new BasicInflater(context);
     76         mHandler = new Handler(mHandlerCallback);
     77         mInflateThread = InflateThread.getInstance();
     78     }
     79 
     80     @UiThread
     81     public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
     82             @NonNull OnInflateFinishedListener callback) {
     83         if (callback == null) {
     84             throw new NullPointerException("callback argument may not be null!");
     85         }
     86         InflateRequest request = mInflateThread.obtainRequest();
     87         request.inflater = this;
     88         request.resid = resid;
     89         request.parent = parent;
     90         request.callback = callback;
     91         mInflateThread.enqueue(request);
     92     }
     93 
     94     private Callback mHandlerCallback = new Callback() {
     95         @Override
     96         public boolean handleMessage(Message msg) {
     97             InflateRequest request = (InflateRequest) msg.obj;
     98             if (request.view == null) {
     99                 request.view = mInflater.inflate(
    100                         request.resid, request.parent, false);
    101             }
    102             request.callback.onInflateFinished(
    103                     request.view, request.resid, request.parent);
    104             mInflateThread.releaseRequest(request);
    105             return true;
    106         }
    107     };
    108 
    109     public interface OnInflateFinishedListener {
    110         void onInflateFinished(View view, int resid, ViewGroup parent);
    111     }
    112 
    113     private static class InflateRequest {
    114         AsyncLayoutInflater inflater;
    115         ViewGroup parent;
    116         int resid;
    117         View view;
    118         OnInflateFinishedListener callback;
    119 
    120         InflateRequest() {
    121         }
    122     }
    123 
    124     private static class BasicInflater extends LayoutInflater {
    125         private static final String[] sClassPrefixList = {
    126             "android.widget.",
    127             "android.webkit.",
    128             "android.app."
    129         };
    130 
    131         BasicInflater(Context context) {
    132             super(context);
    133         }
    134 
    135         @Override
    136         public LayoutInflater cloneInContext(Context newContext) {
    137             return new BasicInflater(newContext);
    138         }
    139 
    140         @Override
    141         protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
    142             for (String prefix : sClassPrefixList) {
    143                 try {
    144                     View view = createView(name, prefix, attrs);
    145                     if (view != null) {
    146                         return view;
    147                     }
    148                 } catch (ClassNotFoundException e) {
    149                     // In this case we want to let the base class take a crack
    150                     // at it.
    151                 }
    152             }
    153 
    154             return super.onCreateView(name, attrs);
    155         }
    156     }
    157 
    158     private static class InflateThread extends Thread {
    159         private static final InflateThread sInstance;
    160         static {
    161             sInstance = new InflateThread();
    162             sInstance.start();
    163         }
    164 
    165         public static InflateThread getInstance() {
    166             return sInstance;
    167         }
    168 
    169         private ArrayBlockingQueue<InflateRequest> mQueue = new ArrayBlockingQueue<>(10);
    170         private SynchronizedPool<InflateRequest> mRequestPool = new SynchronizedPool<>(10);
    171 
    172         // Extracted to its own method to ensure locals have a constrained liveness
    173         // scope by the GC. This is needed to avoid keeping previous request references
    174         // alive for an indeterminate amount of time, see b/33158143 for details
    175         public void runInner() {
    176             InflateRequest request;
    177             try {
    178                 request = mQueue.take();
    179             } catch (InterruptedException ex) {
    180                 // Odd, just continue
    181                 Log.w(TAG, ex);
    182                 return;
    183             }
    184 
    185             try {
    186                 request.view = request.inflater.mInflater.inflate(
    187                         request.resid, request.parent, false);
    188             } catch (RuntimeException ex) {
    189                 // Probably a Looper failure, retry on the UI thread
    190                 Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
    191                         + " thread", ex);
    192             }
    193             Message.obtain(request.inflater.mHandler, 0, request)
    194                     .sendToTarget();
    195         }
    196 
    197         @Override
    198         public void run() {
    199             while (true) {
    200                 runInner();
    201             }
    202         }
    203 
    204         public InflateRequest obtainRequest() {
    205             InflateRequest obj = mRequestPool.acquire();
    206             if (obj == null) {
    207                 obj = new InflateRequest();
    208             }
    209             return obj;
    210         }
    211 
    212         public void releaseRequest(InflateRequest obj) {
    213             obj.callback = null;
    214             obj.inflater = null;
    215             obj.parent = null;
    216             obj.resid = 0;
    217             obj.view = null;
    218             mRequestPool.release(obj);
    219         }
    220 
    221         public void enqueue(InflateRequest request) {
    222             try {
    223                 mQueue.put(request);
    224             } catch (InterruptedException e) {
    225                 throw new RuntimeException(
    226                         "Failed to enqueue async inflate request", e);
    227             }
    228         }
    229     }
    230 }
    231