1 /*
2  * Copyright (C) 2008 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.view;
18 
19 import android.annotation.IdRes;
20 import android.annotation.LayoutRes;
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.util.AttributeSet;
25 import android.widget.RemoteViews.RemoteView;
26 
27 import com.android.internal.R;
28 
29 import java.lang.ref.WeakReference;
30 
31 /**
32  * A ViewStub is an invisible, zero-sized View that can be used to lazily inflate
33  * layout resources at runtime.
34  *
35  * When a ViewStub is made visible, or when {@link #inflate()}  is invoked, the layout resource
36  * is inflated. The ViewStub then replaces itself in its parent with the inflated View or Views.
37  * Therefore, the ViewStub exists in the view hierarchy until {@link #setVisibility(int)} or
38  * {@link #inflate()} is invoked.
39  *
40  * The inflated View is added to the ViewStub's parent with the ViewStub's layout
41  * parameters. Similarly, you can define/override the inflate View's id by using the
42  * ViewStub's inflatedId property. For instance:
43  *
44  * <pre>
45  *     &lt;ViewStub android:id="@+id/stub"
46  *               android:inflatedId="@+id/subTree"
47  *               android:layout="@layout/mySubTree"
48  *               android:layout_width="120dip"
49  *               android:layout_height="40dip" /&gt;
50  * </pre>
51  *
52  * The ViewStub thus defined can be found using the id "stub." After inflation of
53  * the layout resource "mySubTree," the ViewStub is removed from its parent. The
54  * View created by inflating the layout resource "mySubTree" can be found using the
55  * id "subTree," specified by the inflatedId property. The inflated View is finally
56  * assigned a width of 120dip and a height of 40dip.
57  *
58  * The preferred way to perform the inflation of the layout resource is the following:
59  *
60  * <pre>
61  *     ViewStub stub = findViewById(R.id.stub);
62  *     View inflated = stub.inflate();
63  * </pre>
64  *
65  * When {@link #inflate()} is invoked, the ViewStub is replaced by the inflated View
66  * and the inflated View is returned. This lets applications get a reference to the
67  * inflated View without executing an extra findViewById().
68  *
69  * @attr ref android.R.styleable#ViewStub_inflatedId
70  * @attr ref android.R.styleable#ViewStub_layout
71  */
72 @RemoteView
73 public final class ViewStub extends View {
74     private int mInflatedId;
75     private int mLayoutResource;
76 
77     private WeakReference<View> mInflatedViewRef;
78 
79     private LayoutInflater mInflater;
80     private OnInflateListener mInflateListener;
81 
ViewStub(Context context)82     public ViewStub(Context context) {
83         this(context, 0);
84     }
85 
86     /**
87      * Creates a new ViewStub with the specified layout resource.
88      *
89      * @param context The application's environment.
90      * @param layoutResource The reference to a layout resource that will be inflated.
91      */
ViewStub(Context context, @LayoutRes int layoutResource)92     public ViewStub(Context context, @LayoutRes int layoutResource) {
93         this(context, null);
94 
95         mLayoutResource = layoutResource;
96     }
97 
ViewStub(Context context, AttributeSet attrs)98     public ViewStub(Context context, AttributeSet attrs) {
99         this(context, attrs, 0);
100     }
101 
ViewStub(Context context, AttributeSet attrs, int defStyleAttr)102     public ViewStub(Context context, AttributeSet attrs, int defStyleAttr) {
103         this(context, attrs, defStyleAttr, 0);
104     }
105 
ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)106     public ViewStub(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
107         super(context);
108 
109         final TypedArray a = context.obtainStyledAttributes(attrs,
110                 R.styleable.ViewStub, defStyleAttr, defStyleRes);
111         mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
112         mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
113         mID = a.getResourceId(R.styleable.ViewStub_id, NO_ID);
114         a.recycle();
115 
116         setVisibility(GONE);
117         setWillNotDraw(true);
118     }
119 
120     /**
121      * Returns the id taken by the inflated view. If the inflated id is
122      * {@link View#NO_ID}, the inflated view keeps its original id.
123      *
124      * @return A positive integer used to identify the inflated view or
125      *         {@link #NO_ID} if the inflated view should keep its id.
126      *
127      * @see #setInflatedId(int)
128      * @attr ref android.R.styleable#ViewStub_inflatedId
129      */
130     @IdRes
getInflatedId()131     public int getInflatedId() {
132         return mInflatedId;
133     }
134 
135     /**
136      * Defines the id taken by the inflated view. If the inflated id is
137      * {@link View#NO_ID}, the inflated view keeps its original id.
138      *
139      * @param inflatedId A positive integer used to identify the inflated view or
140      *                   {@link #NO_ID} if the inflated view should keep its id.
141      *
142      * @see #getInflatedId()
143      * @attr ref android.R.styleable#ViewStub_inflatedId
144      */
145     @android.view.RemotableViewMethod(asyncImpl = "setInflatedIdAsync")
setInflatedId(@dRes int inflatedId)146     public void setInflatedId(@IdRes int inflatedId) {
147         mInflatedId = inflatedId;
148     }
149 
150     /** @hide **/
setInflatedIdAsync(@dRes int inflatedId)151     public Runnable setInflatedIdAsync(@IdRes int inflatedId) {
152         mInflatedId = inflatedId;
153         return null;
154     }
155 
156     /**
157      * Returns the layout resource that will be used by {@link #setVisibility(int)} or
158      * {@link #inflate()} to replace this StubbedView
159      * in its parent by another view.
160      *
161      * @return The layout resource identifier used to inflate the new View.
162      *
163      * @see #setLayoutResource(int)
164      * @see #setVisibility(int)
165      * @see #inflate()
166      * @attr ref android.R.styleable#ViewStub_layout
167      */
168     @LayoutRes
getLayoutResource()169     public int getLayoutResource() {
170         return mLayoutResource;
171     }
172 
173     /**
174      * Specifies the layout resource to inflate when this StubbedView becomes visible or invisible
175      * or when {@link #inflate()} is invoked. The View created by inflating the layout resource is
176      * used to replace this StubbedView in its parent.
177      *
178      * @param layoutResource A valid layout resource identifier (different from 0.)
179      *
180      * @see #getLayoutResource()
181      * @see #setVisibility(int)
182      * @see #inflate()
183      * @attr ref android.R.styleable#ViewStub_layout
184      */
185     @android.view.RemotableViewMethod(asyncImpl = "setLayoutResourceAsync")
setLayoutResource(@ayoutRes int layoutResource)186     public void setLayoutResource(@LayoutRes int layoutResource) {
187         mLayoutResource = layoutResource;
188     }
189 
190     /** @hide **/
setLayoutResourceAsync(@ayoutRes int layoutResource)191     public Runnable setLayoutResourceAsync(@LayoutRes int layoutResource) {
192         mLayoutResource = layoutResource;
193         return null;
194     }
195 
196     /**
197      * Set {@link LayoutInflater} to use in {@link #inflate()}, or {@code null}
198      * to use the default.
199      */
setLayoutInflater(LayoutInflater inflater)200     public void setLayoutInflater(LayoutInflater inflater) {
201         mInflater = inflater;
202     }
203 
204     /**
205      * Get current {@link LayoutInflater} used in {@link #inflate()}.
206      */
getLayoutInflater()207     public LayoutInflater getLayoutInflater() {
208         return mInflater;
209     }
210 
211     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)212     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
213         setMeasuredDimension(0, 0);
214     }
215 
216     @Override
draw(Canvas canvas)217     public void draw(Canvas canvas) {
218     }
219 
220     @Override
dispatchDraw(Canvas canvas)221     protected void dispatchDraw(Canvas canvas) {
222     }
223 
224     /**
225      * When visibility is set to {@link #VISIBLE} or {@link #INVISIBLE},
226      * {@link #inflate()} is invoked and this StubbedView is replaced in its parent
227      * by the inflated layout resource. After that calls to this function are passed
228      * through to the inflated view.
229      *
230      * @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
231      *
232      * @see #inflate()
233      */
234     @Override
235     @android.view.RemotableViewMethod(asyncImpl = "setVisibilityAsync")
setVisibility(int visibility)236     public void setVisibility(int visibility) {
237         if (mInflatedViewRef != null) {
238             View view = mInflatedViewRef.get();
239             if (view != null) {
240                 view.setVisibility(visibility);
241             } else {
242                 throw new IllegalStateException("setVisibility called on un-referenced view");
243             }
244         } else {
245             super.setVisibility(visibility);
246             if (visibility == VISIBLE || visibility == INVISIBLE) {
247                 inflate();
248             }
249         }
250     }
251 
252     /** @hide **/
setVisibilityAsync(int visibility)253     public Runnable setVisibilityAsync(int visibility) {
254         if (visibility == VISIBLE || visibility == INVISIBLE) {
255             ViewGroup parent = (ViewGroup) getParent();
256             return new ViewReplaceRunnable(inflateViewNoAdd(parent));
257         } else {
258             return null;
259         }
260     }
261 
inflateViewNoAdd(ViewGroup parent)262     private View inflateViewNoAdd(ViewGroup parent) {
263         final LayoutInflater factory;
264         if (mInflater != null) {
265             factory = mInflater;
266         } else {
267             factory = LayoutInflater.from(mContext);
268         }
269         final View view = factory.inflate(mLayoutResource, parent, false);
270 
271         if (mInflatedId != NO_ID) {
272             view.setId(mInflatedId);
273         }
274         return view;
275     }
276 
replaceSelfWithView(View view, ViewGroup parent)277     private void replaceSelfWithView(View view, ViewGroup parent) {
278         final int index = parent.indexOfChild(this);
279         parent.removeViewInLayout(this);
280 
281         final ViewGroup.LayoutParams layoutParams = getLayoutParams();
282         if (layoutParams != null) {
283             parent.addView(view, index, layoutParams);
284         } else {
285             parent.addView(view, index);
286         }
287     }
288 
289     /**
290      * Inflates the layout resource identified by {@link #getLayoutResource()}
291      * and replaces this StubbedView in its parent by the inflated layout resource.
292      *
293      * @return The inflated layout resource.
294      *
295      */
inflate()296     public View inflate() {
297         final ViewParent viewParent = getParent();
298 
299         if (viewParent != null && viewParent instanceof ViewGroup) {
300             if (mLayoutResource != 0) {
301                 final ViewGroup parent = (ViewGroup) viewParent;
302                 final View view = inflateViewNoAdd(parent);
303                 replaceSelfWithView(view, parent);
304 
305                 mInflatedViewRef = new WeakReference<>(view);
306                 if (mInflateListener != null) {
307                     mInflateListener.onInflate(this, view);
308                 }
309 
310                 return view;
311             } else {
312                 throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
313             }
314         } else {
315             throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
316         }
317     }
318 
319     /**
320      * Specifies the inflate listener to be notified after this ViewStub successfully
321      * inflated its layout resource.
322      *
323      * @param inflateListener The OnInflateListener to notify of successful inflation.
324      *
325      * @see android.view.ViewStub.OnInflateListener
326      */
setOnInflateListener(OnInflateListener inflateListener)327     public void setOnInflateListener(OnInflateListener inflateListener) {
328         mInflateListener = inflateListener;
329     }
330 
331     /**
332      * Listener used to receive a notification after a ViewStub has successfully
333      * inflated its layout resource.
334      *
335      * @see android.view.ViewStub#setOnInflateListener(android.view.ViewStub.OnInflateListener)
336      */
337     public static interface OnInflateListener {
338         /**
339          * Invoked after a ViewStub successfully inflated its layout resource.
340          * This method is invoked after the inflated view was added to the
341          * hierarchy but before the layout pass.
342          *
343          * @param stub The ViewStub that initiated the inflation.
344          * @param inflated The inflated View.
345          */
onInflate(ViewStub stub, View inflated)346         void onInflate(ViewStub stub, View inflated);
347     }
348 
349     /** @hide **/
350     public class ViewReplaceRunnable implements Runnable {
351         public final View view;
352 
ViewReplaceRunnable(View view)353         ViewReplaceRunnable(View view) {
354             this.view = view;
355         }
356 
357         @Override
run()358         public void run() {
359             replaceSelfWithView(view, (ViewGroup) getParent());
360         }
361     }
362 }
363