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