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 17package android.support.v4.view; 18 19import android.content.Context; 20import android.os.Handler; 21import android.os.Handler.Callback; 22import android.os.Looper; 23import android.os.Message; 24import android.support.annotation.LayoutRes; 25import android.support.annotation.NonNull; 26import android.support.annotation.Nullable; 27import android.support.annotation.UiThread; 28import android.support.v4.util.Pools.SynchronizedPool; 29import android.util.AttributeSet; 30import android.util.Log; 31import android.view.LayoutInflater; 32import android.view.View; 33import android.view.ViewGroup; 34 35import 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 */ 67public final class AsyncLayoutInflater { 68 private static final String TAG = "AsyncLayoutInflater"; 69 70 private LayoutInflater mInflater; 71 private Handler mHandler; 72 private 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 public 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 121 private static class BasicInflater extends LayoutInflater { 122 private static final String[] sClassPrefixList = { 123 "android.widget.", 124 "android.webkit.", 125 "android.app." 126 }; 127 128 public BasicInflater(Context context) { 129 super(context); 130 } 131 132 @Override 133 public LayoutInflater cloneInContext(Context newContext) { 134 return new BasicInflater(newContext); 135 } 136 137 @Override 138 protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { 139 for (String prefix : sClassPrefixList) { 140 try { 141 View view = createView(name, prefix, attrs); 142 if (view != null) { 143 return view; 144 } 145 } catch (ClassNotFoundException e) { 146 // In this case we want to let the base class take a crack 147 // at it. 148 } 149 } 150 151 return super.onCreateView(name, attrs); 152 } 153 } 154 155 private static class InflateThread extends Thread { 156 private static final InflateThread sInstance; 157 static { 158 sInstance = new InflateThread(); 159 sInstance.start(); 160 } 161 162 public static InflateThread getInstance() { 163 return sInstance; 164 } 165 166 private ArrayBlockingQueue<InflateRequest> mQueue 167 = new ArrayBlockingQueue<>(10); 168 private SynchronizedPool<InflateRequest> mRequestPool 169 = new SynchronizedPool<>(10); 170 171 @Override 172 public void run() { 173 while (true) { 174 InflateRequest request; 175 try { 176 request = mQueue.take(); 177 } catch (InterruptedException ex) { 178 // Odd, just continue 179 Log.w(TAG, ex); 180 continue; 181 } 182 183 try { 184 request.view = request.inflater.mInflater.inflate( 185 request.resid, request.parent, false); 186 } catch (RuntimeException ex) { 187 // Probably a Looper failure, retry on the UI thread 188 Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI thread", 189 ex); 190 } 191 Message.obtain(request.inflater.mHandler, 0, request) 192 .sendToTarget(); 193 } 194 } 195 196 public InflateRequest obtainRequest() { 197 InflateRequest obj = mRequestPool.acquire(); 198 if (obj == null) { 199 obj = new InflateRequest(); 200 } 201 return obj; 202 } 203 204 public void releaseRequest(InflateRequest obj) { 205 obj.callback = null; 206 obj.inflater = null; 207 obj.parent = null; 208 obj.resid = 0; 209 obj.view = null; 210 mRequestPool.release(obj); 211 } 212 213 public void enqueue(InflateRequest request) { 214 try { 215 mQueue.put(request); 216 } catch (InterruptedException e) { 217 throw new RuntimeException( 218 "Failed to enqueue async inflate request", e); 219 } 220 } 221 } 222} 223