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