SizeAdaptiveLayout.java revision 3a59d6e26dbec61ede7d6f87d966698e27c91d78
1/*
2 * Copyright (C) 2012 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 com.android.internal.widget;
18
19import com.android.internal.R;
20
21import android.animation.Animator;
22import android.animation.Animator.AnimatorListener;
23import android.animation.AnimatorSet;
24import android.animation.ObjectAnimator;
25import android.content.Context;
26import android.content.res.TypedArray;
27import android.graphics.Color;
28import android.graphics.drawable.ColorDrawable;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.view.View;
32import android.view.ViewDebug;
33import android.view.ViewGroup;
34import android.widget.RemoteViews.RemoteView;
35
36/**
37 * A layout that switches between its children based on the requested layout height.
38 * Each child specifies its minimum and maximum valid height.  Results are undefined
39 * if children specify overlapping ranges.  A child may specify the maximum height
40 * as 'unbounded' to indicate that it is willing to be displayed arbitrarily tall.
41 *
42 * <p>
43 * See {@link SizeAdaptiveLayout.LayoutParams} for a full description of the
44 * layout parameters used by SizeAdaptiveLayout.
45 */
46@RemoteView
47public class SizeAdaptiveLayout extends ViewGroup {
48
49    private static final String TAG = "SizeAdaptiveLayout";
50    private static final boolean DEBUG = false;
51    private static final long CROSSFADE_TIME = 250;
52
53    // TypedArray indices
54    private static final int MIN_VALID_HEIGHT =
55            R.styleable.SizeAdaptiveLayout_Layout_layout_minHeight;
56    private static final int MAX_VALID_HEIGHT =
57            R.styleable.SizeAdaptiveLayout_Layout_layout_maxHeight;
58
59    // view state
60    private View mActiveChild;
61    private View mLastActive;
62
63    // animation state
64    private AnimatorSet mTransitionAnimation;
65    private AnimatorListener mAnimatorListener;
66    private ObjectAnimator mFadePanel;
67    private ObjectAnimator mFadeView;
68    private int mCanceledAnimationCount;
69    private View mEnteringView;
70    private View mLeavingView;
71    // View used to hide larger views under smaller ones to create a uniform crossfade
72    private View mModestyPanel;
73    private int mModestyPanelTop;
74
75    public SizeAdaptiveLayout(Context context) {
76        super(context);
77        initialize();
78    }
79
80    public SizeAdaptiveLayout(Context context, AttributeSet attrs) {
81        super(context, attrs);
82        initialize();
83    }
84
85    public SizeAdaptiveLayout(Context context, AttributeSet attrs, int defStyle) {
86        super(context, attrs, defStyle);
87        initialize();
88    }
89
90    private void initialize() {
91        mModestyPanel = new View(getContext());
92        // If the SizeAdaptiveLayout has a solid background, use it as a transition hint.
93        if (getBackground() instanceof ColorDrawable) {
94            mModestyPanel.setBackgroundDrawable(getBackground());
95        } else {
96            mModestyPanel.setBackgroundColor(Color.BLACK);
97        }
98        SizeAdaptiveLayout.LayoutParams layout =
99                new SizeAdaptiveLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
100                                                    ViewGroup.LayoutParams.MATCH_PARENT);
101        mModestyPanel.setLayoutParams(layout);
102        addView(mModestyPanel);
103        mFadePanel = ObjectAnimator.ofFloat(mModestyPanel, "alpha", 0f);
104        mFadeView = ObjectAnimator.ofFloat(null, "alpha", 0f);
105        mAnimatorListener = new BringToFrontOnEnd();
106        mTransitionAnimation = new AnimatorSet();
107        mTransitionAnimation.play(mFadeView).with(mFadePanel);
108        mTransitionAnimation.setDuration(CROSSFADE_TIME);
109        mTransitionAnimation.addListener(mAnimatorListener);
110    }
111
112    /**
113     * Visible for testing
114     * @hide
115     */
116    public Animator getTransitionAnimation() {
117        return mTransitionAnimation;
118    }
119
120    /**
121     * Visible for testing
122     * @hide
123     */
124    public View getModestyPanel() {
125        return mModestyPanel;
126    }
127
128    @Override
129    public void onAttachedToWindow() {
130        mLastActive = null;
131        // make sure all views start off invisible.
132        for (int i = 0; i < getChildCount(); i++) {
133            getChildAt(i).setVisibility(View.GONE);
134        }
135    }
136
137    @Override
138    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
139        if (DEBUG) Log.d(TAG, this + " measure spec: " +
140                         MeasureSpec.toString(heightMeasureSpec));
141        View model = selectActiveChild(heightMeasureSpec);
142        SizeAdaptiveLayout.LayoutParams lp =
143          (SizeAdaptiveLayout.LayoutParams) model.getLayoutParams();
144        if (DEBUG) Log.d(TAG, "active min: " + lp.minHeight + " max: " + lp.maxHeight);
145        measureChild(model, widthMeasureSpec, heightMeasureSpec);
146        int childHeight = model.getMeasuredHeight();
147        int childWidth = model.getMeasuredHeight();
148        int childState = combineMeasuredStates(0, model.getMeasuredState());
149        if (DEBUG) Log.d(TAG, "measured child at: " + childHeight);
150        int resolvedWidth = resolveSizeAndState(childWidth, widthMeasureSpec, childState);
151        int resolvedheight = resolveSizeAndState(childHeight, heightMeasureSpec, childState);
152        setMeasuredDimension(resolvedWidth, resolvedheight);
153        if (DEBUG) Log.d(TAG, "resolved to: " + resolvedheight);
154    }
155
156    //TODO extend to width and height
157    private View selectActiveChild(int heightMeasureSpec) {
158        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
159        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
160
161        View unboundedView = null;
162        View tallestView = null;
163        int tallestViewSize = 0;
164        View smallestView = null;
165        int smallestViewSize = Integer.MAX_VALUE;
166        for (int i = 0; i < getChildCount(); i++) {
167            View child = getChildAt(i);
168            if (child != mModestyPanel) {
169                SizeAdaptiveLayout.LayoutParams lp =
170                    (SizeAdaptiveLayout.LayoutParams) child.getLayoutParams();
171                if (DEBUG) Log.d(TAG, "looking at " + i +
172                                 " with min: " + lp.minHeight +
173                                 " max: " +  lp.maxHeight);
174                if (lp.maxHeight == SizeAdaptiveLayout.LayoutParams.UNBOUNDED &&
175                    unboundedView == null) {
176                    unboundedView = child;
177                }
178                if (lp.maxHeight > tallestViewSize) {
179                    tallestViewSize = lp.maxHeight;
180                    tallestView = child;
181                }
182                if (lp.minHeight < smallestViewSize) {
183                    smallestViewSize = lp.minHeight;
184                    smallestView = child;
185                }
186                if (heightMode != MeasureSpec.UNSPECIFIED &&
187                    heightSize >= lp.minHeight && heightSize <= lp.maxHeight) {
188                    if (DEBUG) Log.d(TAG, "  found exact match, finishing early");
189                    return child;
190                }
191            }
192        }
193        if (unboundedView != null) {
194            tallestView = unboundedView;
195        }
196        if (heightMode == MeasureSpec.UNSPECIFIED) {
197            return tallestView;
198        }
199        if (heightSize > tallestViewSize) {
200            return tallestView;
201        }
202        return smallestView;
203    }
204
205    @Override
206    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
207        if (DEBUG) Log.d(TAG, this + " onlayout height: " + (bottom - top));
208        mLastActive = mActiveChild;
209        int measureSpec = View.MeasureSpec.makeMeasureSpec(bottom - top,
210                                                           View.MeasureSpec.EXACTLY);
211        mActiveChild = selectActiveChild(measureSpec);
212        mActiveChild.setVisibility(View.VISIBLE);
213
214        if (mLastActive != mActiveChild && mLastActive != null) {
215            if (DEBUG) Log.d(TAG, this + " changed children from: " + mLastActive +
216                    " to: " + mActiveChild);
217
218            mEnteringView = mActiveChild;
219            mLeavingView = mLastActive;
220
221            mEnteringView.setAlpha(1f);
222
223            mModestyPanel.setAlpha(1f);
224            mModestyPanel.bringToFront();
225            mModestyPanelTop = mLeavingView.getHeight();
226            mModestyPanel.setVisibility(View.VISIBLE);
227            // TODO: mModestyPanel background should be compatible with mLeavingView
228
229            mLeavingView.bringToFront();
230
231            if (mTransitionAnimation.isRunning()) {
232                mTransitionAnimation.cancel();
233            }
234            mFadeView.setTarget(mLeavingView);
235            mFadeView.setFloatValues(0f);
236            mFadePanel.setFloatValues(0f);
237            mTransitionAnimation.setupStartValues();
238            mTransitionAnimation.start();
239        }
240        final int childWidth = mActiveChild.getMeasuredWidth();
241        final int childHeight = mActiveChild.getMeasuredHeight();
242        // TODO investigate setting LAYER_TYPE_HARDWARE on mLastActive
243        mActiveChild.layout(0, 0, 0 + childWidth, 0 + childHeight);
244
245        if (DEBUG) Log.d(TAG, "got modesty offset of " + mModestyPanelTop);
246        mModestyPanel.layout(0, mModestyPanelTop, 0 + childWidth, mModestyPanelTop + childHeight);
247    }
248
249    @Override
250    public LayoutParams generateLayoutParams(AttributeSet attrs) {
251        if (DEBUG) Log.d(TAG, "generate layout from attrs");
252        return new SizeAdaptiveLayout.LayoutParams(getContext(), attrs);
253    }
254
255    @Override
256    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
257        if (DEBUG) Log.d(TAG, "generate default layout from viewgroup");
258        return new SizeAdaptiveLayout.LayoutParams(p);
259    }
260
261    @Override
262    protected LayoutParams generateDefaultLayoutParams() {
263        if (DEBUG) Log.d(TAG, "generate default layout from null");
264        return new SizeAdaptiveLayout.LayoutParams();
265    }
266
267    @Override
268    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
269        return p instanceof SizeAdaptiveLayout.LayoutParams;
270    }
271
272    /**
273     * Per-child layout information associated with ViewSizeAdaptiveLayout.
274     *
275     * TODO extend to width and height
276     *
277     * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_minHeight
278     * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_maxHeight
279     */
280    public static class LayoutParams extends ViewGroup.LayoutParams {
281
282        /**
283         * Indicates the minimum valid height for the child.
284         */
285        @ViewDebug.ExportedProperty(category = "layout")
286        public int minHeight;
287
288        /**
289         * Indicates the maximum valid height for the child.
290         */
291        @ViewDebug.ExportedProperty(category = "layout")
292        public int maxHeight;
293
294        /**
295         * Constant value for maxHeight that indicates there is not maximum height.
296         */
297        public static final int UNBOUNDED = -1;
298
299        /**
300         * {@inheritDoc}
301         */
302        public LayoutParams(Context c, AttributeSet attrs) {
303            super(c, attrs);
304            if (DEBUG) {
305                Log.d(TAG, "construct layout from attrs");
306                for (int i = 0; i < attrs.getAttributeCount(); i++) {
307                    Log.d(TAG, " " + attrs.getAttributeName(i) + " = " +
308                          attrs.getAttributeValue(i));
309                }
310            }
311            TypedArray a =
312                    c.obtainStyledAttributes(attrs,
313                            R.styleable.SizeAdaptiveLayout_Layout);
314
315            minHeight = a.getDimensionPixelSize(MIN_VALID_HEIGHT, 0);
316            if (DEBUG) Log.d(TAG, "got minHeight of: " + minHeight);
317
318            try {
319                maxHeight = a.getLayoutDimension(MAX_VALID_HEIGHT, UNBOUNDED);
320                if (DEBUG) Log.d(TAG, "got maxHeight of: " + maxHeight);
321            } catch (Exception e) {
322                if (DEBUG) Log.d(TAG, "caught exception looking for maxValidHeight " + e);
323            }
324
325            a.recycle();
326        }
327
328        /**
329         * Creates a new set of layout parameters with the specified width, height
330         * and valid height bounds.
331         *
332         * @param width the width, either {@link #MATCH_PARENT},
333         *        {@link #WRAP_CONTENT} or a fixed size in pixels
334         * @param height the height, either {@link #MATCH_PARENT},
335         *        {@link #WRAP_CONTENT} or a fixed size in pixels
336         * @param minHeight the minimum height of this child
337         * @param maxHeight the maximum height of this child
338         *        or {@link #UNBOUNDED} if the child can grow forever
339         */
340        public LayoutParams(int width, int height, int minHeight, int maxHeight) {
341            super(width, height);
342            this.minHeight = minHeight;
343            this.maxHeight = maxHeight;
344        }
345
346        /**
347         * {@inheritDoc}
348         */
349        public LayoutParams(int width, int height) {
350            this(width, height, UNBOUNDED, UNBOUNDED);
351        }
352
353        /**
354         * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}.
355         */
356        public LayoutParams() {
357            this(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
358        }
359
360        /**
361         * {@inheritDoc}
362         */
363        public LayoutParams(ViewGroup.LayoutParams p) {
364            super(p);
365            minHeight = UNBOUNDED;
366            maxHeight = UNBOUNDED;
367        }
368
369        public String debug(String output) {
370            return output + "SizeAdaptiveLayout.LayoutParams={" +
371                    ", max=" + maxHeight +
372                    ", max=" + minHeight + "}";
373        }
374    }
375
376    class BringToFrontOnEnd implements AnimatorListener {
377        @Override
378            public void onAnimationEnd(Animator animation) {
379            if (mCanceledAnimationCount == 0) {
380                mLeavingView.setVisibility(View.GONE);
381                mModestyPanel.setVisibility(View.GONE);
382                mEnteringView.bringToFront();
383                mEnteringView = null;
384                mLeavingView = null;
385            } else {
386                mCanceledAnimationCount--;
387            }
388        }
389
390        @Override
391            public void onAnimationCancel(Animator animation) {
392            mCanceledAnimationCount++;
393        }
394
395        @Override
396            public void onAnimationRepeat(Animator animation) {
397            if (DEBUG) Log.d(TAG, "fade animation repeated: should never happen.");
398            assert(false);
399        }
400
401        @Override
402            public void onAnimationStart(Animator animation) {
403        }
404    }
405}
406