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