/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.support.v4.view; import android.content.Context; import android.os.Handler; import android.os.Handler.Callback; import android.os.Looper; import android.os.Message; import android.support.annotation.LayoutRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import android.support.v4.util.Pools.SynchronizedPool; import android.util.AttributeSet; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import java.util.concurrent.ArrayBlockingQueue; /** *

Helper class for inflating layouts asynchronously. To use, construct * an instance of {@link AsyncLayoutInflater} on the UI thread and call * {@link #inflate(int, ViewGroup, OnInflateFinishedListener)}. The * {@link OnInflateFinishedListener} will be invoked on the UI thread * when the inflate request has completed. * *

This is intended for parts of the UI that are created lazily or in * response to user interactions. This allows the UI thread to continue * to be responsive & animate while the relatively heavy inflate * is being performed. * *

For a layout to be inflated asynchronously it needs to have a parent * whose {@link ViewGroup#generateLayoutParams(AttributeSet)} is thread-safe * and all the Views being constructed as part of inflation must not create * any {@link Handler}s or otherwise call {@link Looper#myLooper()}. If the * layout that is trying to be inflated cannot be constructed * asynchronously for whatever reason, {@link AsyncLayoutInflater} will * automatically fall back to inflating on the UI thread. * *

NOTE that the inflated View hierarchy is NOT added to the parent. It is * equivalent to calling {@link LayoutInflater#inflate(int, ViewGroup, boolean)} * with attachToRoot set to false. Callers will likely want to call * {@link ViewGroup#addView(View)} in the {@link OnInflateFinishedListener} * callback at a minimum. * *

This inflater does not support setting a {@link LayoutInflater.Factory} * nor {@link LayoutInflater.Factory2}. Similarly it does not support inflating * layouts that contain fragments. */ public final class AsyncLayoutInflater { private static final String TAG = "AsyncLayoutInflater"; private LayoutInflater mInflater; private Handler mHandler; private InflateThread mInflateThread; public AsyncLayoutInflater(@NonNull Context context) { mInflater = new BasicInflater(context); mHandler = new Handler(mHandlerCallback); mInflateThread = InflateThread.getInstance(); } @UiThread public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent, @NonNull OnInflateFinishedListener callback) { if (callback == null) { throw new NullPointerException("callback argument may not be null!"); } InflateRequest request = mInflateThread.obtainRequest(); request.inflater = this; request.resid = resid; request.parent = parent; request.callback = callback; mInflateThread.enqueue(request); } private Callback mHandlerCallback = new Callback() { @Override public boolean handleMessage(Message msg) { InflateRequest request = (InflateRequest) msg.obj; if (request.view == null) { request.view = mInflater.inflate( request.resid, request.parent, false); } request.callback.onInflateFinished( request.view, request.resid, request.parent); mInflateThread.releaseRequest(request); return true; } }; public interface OnInflateFinishedListener { public void onInflateFinished(View view, int resid, ViewGroup parent); } private static class InflateRequest { AsyncLayoutInflater inflater; ViewGroup parent; int resid; View view; OnInflateFinishedListener callback; } private static class BasicInflater extends LayoutInflater { private static final String[] sClassPrefixList = { "android.widget.", "android.webkit.", "android.app." }; public BasicInflater(Context context) { super(context); } @Override public LayoutInflater cloneInContext(Context newContext) { return new BasicInflater(newContext); } @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { for (String prefix : sClassPrefixList) { try { View view = createView(name, prefix, attrs); if (view != null) { return view; } } catch (ClassNotFoundException e) { // In this case we want to let the base class take a crack // at it. } } return super.onCreateView(name, attrs); } } private static class InflateThread extends Thread { private static final InflateThread sInstance; static { sInstance = new InflateThread(); sInstance.start(); } public static InflateThread getInstance() { return sInstance; } private ArrayBlockingQueue mQueue = new ArrayBlockingQueue<>(10); private SynchronizedPool mRequestPool = new SynchronizedPool<>(10); @Override public void run() { while (true) { InflateRequest request; try { request = mQueue.take(); } catch (InterruptedException ex) { // Odd, just continue Log.w(TAG, ex); continue; } try { request.view = request.inflater.mInflater.inflate( request.resid, request.parent, false); } catch (RuntimeException ex) { // Probably a Looper failure, retry on the UI thread Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI thread", ex); } Message.obtain(request.inflater.mHandler, 0, request) .sendToTarget(); } } public InflateRequest obtainRequest() { InflateRequest obj = mRequestPool.acquire(); if (obj == null) { obj = new InflateRequest(); } return obj; } public void releaseRequest(InflateRequest obj) { obj.callback = null; obj.inflater = null; obj.parent = null; obj.resid = 0; obj.view = null; mRequestPool.release(obj); } public void enqueue(InflateRequest request) { try { mQueue.put(request); } catch (InterruptedException e) { throw new RuntimeException( "Failed to enqueue async inflate request", e); } } } }