diff options
author | Chris Wren <cwren@android.com> | 2012-03-30 18:28:14 -0400 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2012-04-17 09:06:21 -0700 |
commit | 3a59d6e26dbec61ede7d6f87d966698e27c91d78 (patch) | |
tree | 1991beeb07ff593a230d994d6712d8a9867bfe6f /core/java/com | |
parent | 8334679be1a9defb9d52217542ec0a1389c9fcae (diff) | |
download | frameworks_base-3a59d6e26dbec61ede7d6f87d966698e27c91d78.zip frameworks_base-3a59d6e26dbec61ede7d6f87d966698e27c91d78.tar.gz frameworks_base-3a59d6e26dbec61ede7d6f87d966698e27c91d78.tar.bz2 |
A layout that switches between its children based on the requested layout height.
Change-Id: I5a4e5892fbed7cab2470e458a38accbbcb05ae51
Diffstat (limited to 'core/java/com')
-rw-r--r-- | core/java/com/android/internal/widget/SizeAdaptiveLayout.java | 405 |
1 files changed, 405 insertions, 0 deletions
diff --git a/core/java/com/android/internal/widget/SizeAdaptiveLayout.java b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java new file mode 100644 index 0000000..adfd3dc --- /dev/null +++ b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2012 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 com.android.internal.widget; + +import com.android.internal.R; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.widget.RemoteViews.RemoteView; + +/** + * A layout that switches between its children based on the requested layout height. + * Each child specifies its minimum and maximum valid height. Results are undefined + * if children specify overlapping ranges. A child may specify the maximum height + * as 'unbounded' to indicate that it is willing to be displayed arbitrarily tall. + * + * <p> + * See {@link SizeAdaptiveLayout.LayoutParams} for a full description of the + * layout parameters used by SizeAdaptiveLayout. + */ +@RemoteView +public class SizeAdaptiveLayout extends ViewGroup { + + private static final String TAG = "SizeAdaptiveLayout"; + private static final boolean DEBUG = false; + private static final long CROSSFADE_TIME = 250; + + // TypedArray indices + private static final int MIN_VALID_HEIGHT = + R.styleable.SizeAdaptiveLayout_Layout_layout_minHeight; + private static final int MAX_VALID_HEIGHT = + R.styleable.SizeAdaptiveLayout_Layout_layout_maxHeight; + + // view state + private View mActiveChild; + private View mLastActive; + + // animation state + private AnimatorSet mTransitionAnimation; + private AnimatorListener mAnimatorListener; + private ObjectAnimator mFadePanel; + private ObjectAnimator mFadeView; + private int mCanceledAnimationCount; + private View mEnteringView; + private View mLeavingView; + // View used to hide larger views under smaller ones to create a uniform crossfade + private View mModestyPanel; + private int mModestyPanelTop; + + public SizeAdaptiveLayout(Context context) { + super(context); + initialize(); + } + + public SizeAdaptiveLayout(Context context, AttributeSet attrs) { + super(context, attrs); + initialize(); + } + + public SizeAdaptiveLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + initialize(); + } + + private void initialize() { + mModestyPanel = new View(getContext()); + // If the SizeAdaptiveLayout has a solid background, use it as a transition hint. + if (getBackground() instanceof ColorDrawable) { + mModestyPanel.setBackgroundDrawable(getBackground()); + } else { + mModestyPanel.setBackgroundColor(Color.BLACK); + } + SizeAdaptiveLayout.LayoutParams layout = + new SizeAdaptiveLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mModestyPanel.setLayoutParams(layout); + addView(mModestyPanel); + mFadePanel = ObjectAnimator.ofFloat(mModestyPanel, "alpha", 0f); + mFadeView = ObjectAnimator.ofFloat(null, "alpha", 0f); + mAnimatorListener = new BringToFrontOnEnd(); + mTransitionAnimation = new AnimatorSet(); + mTransitionAnimation.play(mFadeView).with(mFadePanel); + mTransitionAnimation.setDuration(CROSSFADE_TIME); + mTransitionAnimation.addListener(mAnimatorListener); + } + + /** + * Visible for testing + * @hide + */ + public Animator getTransitionAnimation() { + return mTransitionAnimation; + } + + /** + * Visible for testing + * @hide + */ + public View getModestyPanel() { + return mModestyPanel; + } + + @Override + public void onAttachedToWindow() { + mLastActive = null; + // make sure all views start off invisible. + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).setVisibility(View.GONE); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (DEBUG) Log.d(TAG, this + " measure spec: " + + MeasureSpec.toString(heightMeasureSpec)); + View model = selectActiveChild(heightMeasureSpec); + SizeAdaptiveLayout.LayoutParams lp = + (SizeAdaptiveLayout.LayoutParams) model.getLayoutParams(); + if (DEBUG) Log.d(TAG, "active min: " + lp.minHeight + " max: " + lp.maxHeight); + measureChild(model, widthMeasureSpec, heightMeasureSpec); + int childHeight = model.getMeasuredHeight(); + int childWidth = model.getMeasuredHeight(); + int childState = combineMeasuredStates(0, model.getMeasuredState()); + if (DEBUG) Log.d(TAG, "measured child at: " + childHeight); + int resolvedWidth = resolveSizeAndState(childWidth, widthMeasureSpec, childState); + int resolvedheight = resolveSizeAndState(childHeight, heightMeasureSpec, childState); + setMeasuredDimension(resolvedWidth, resolvedheight); + if (DEBUG) Log.d(TAG, "resolved to: " + resolvedheight); + } + + //TODO extend to width and height + private View selectActiveChild(int heightMeasureSpec) { + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + View unboundedView = null; + View tallestView = null; + int tallestViewSize = 0; + View smallestView = null; + int smallestViewSize = Integer.MAX_VALUE; + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (child != mModestyPanel) { + SizeAdaptiveLayout.LayoutParams lp = + (SizeAdaptiveLayout.LayoutParams) child.getLayoutParams(); + if (DEBUG) Log.d(TAG, "looking at " + i + + " with min: " + lp.minHeight + + " max: " + lp.maxHeight); + if (lp.maxHeight == SizeAdaptiveLayout.LayoutParams.UNBOUNDED && + unboundedView == null) { + unboundedView = child; + } + if (lp.maxHeight > tallestViewSize) { + tallestViewSize = lp.maxHeight; + tallestView = child; + } + if (lp.minHeight < smallestViewSize) { + smallestViewSize = lp.minHeight; + smallestView = child; + } + if (heightMode != MeasureSpec.UNSPECIFIED && + heightSize >= lp.minHeight && heightSize <= lp.maxHeight) { + if (DEBUG) Log.d(TAG, " found exact match, finishing early"); + return child; + } + } + } + if (unboundedView != null) { + tallestView = unboundedView; + } + if (heightMode == MeasureSpec.UNSPECIFIED) { + return tallestView; + } + if (heightSize > tallestViewSize) { + return tallestView; + } + return smallestView; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + if (DEBUG) Log.d(TAG, this + " onlayout height: " + (bottom - top)); + mLastActive = mActiveChild; + int measureSpec = View.MeasureSpec.makeMeasureSpec(bottom - top, + View.MeasureSpec.EXACTLY); + mActiveChild = selectActiveChild(measureSpec); + mActiveChild.setVisibility(View.VISIBLE); + + if (mLastActive != mActiveChild && mLastActive != null) { + if (DEBUG) Log.d(TAG, this + " changed children from: " + mLastActive + + " to: " + mActiveChild); + + mEnteringView = mActiveChild; + mLeavingView = mLastActive; + + mEnteringView.setAlpha(1f); + + mModestyPanel.setAlpha(1f); + mModestyPanel.bringToFront(); + mModestyPanelTop = mLeavingView.getHeight(); + mModestyPanel.setVisibility(View.VISIBLE); + // TODO: mModestyPanel background should be compatible with mLeavingView + + mLeavingView.bringToFront(); + + if (mTransitionAnimation.isRunning()) { + mTransitionAnimation.cancel(); + } + mFadeView.setTarget(mLeavingView); + mFadeView.setFloatValues(0f); + mFadePanel.setFloatValues(0f); + mTransitionAnimation.setupStartValues(); + mTransitionAnimation.start(); + } + final int childWidth = mActiveChild.getMeasuredWidth(); + final int childHeight = mActiveChild.getMeasuredHeight(); + // TODO investigate setting LAYER_TYPE_HARDWARE on mLastActive + mActiveChild.layout(0, 0, 0 + childWidth, 0 + childHeight); + + if (DEBUG) Log.d(TAG, "got modesty offset of " + mModestyPanelTop); + mModestyPanel.layout(0, mModestyPanelTop, 0 + childWidth, mModestyPanelTop + childHeight); + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + if (DEBUG) Log.d(TAG, "generate layout from attrs"); + return new SizeAdaptiveLayout.LayoutParams(getContext(), attrs); + } + + @Override + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + if (DEBUG) Log.d(TAG, "generate default layout from viewgroup"); + return new SizeAdaptiveLayout.LayoutParams(p); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + if (DEBUG) Log.d(TAG, "generate default layout from null"); + return new SizeAdaptiveLayout.LayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof SizeAdaptiveLayout.LayoutParams; + } + + /** + * Per-child layout information associated with ViewSizeAdaptiveLayout. + * + * TODO extend to width and height + * + * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_minHeight + * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_maxHeight + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + + /** + * Indicates the minimum valid height for the child. + */ + @ViewDebug.ExportedProperty(category = "layout") + public int minHeight; + + /** + * Indicates the maximum valid height for the child. + */ + @ViewDebug.ExportedProperty(category = "layout") + public int maxHeight; + + /** + * Constant value for maxHeight that indicates there is not maximum height. + */ + public static final int UNBOUNDED = -1; + + /** + * {@inheritDoc} + */ + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + if (DEBUG) { + Log.d(TAG, "construct layout from attrs"); + for (int i = 0; i < attrs.getAttributeCount(); i++) { + Log.d(TAG, " " + attrs.getAttributeName(i) + " = " + + attrs.getAttributeValue(i)); + } + } + TypedArray a = + c.obtainStyledAttributes(attrs, + R.styleable.SizeAdaptiveLayout_Layout); + + minHeight = a.getDimensionPixelSize(MIN_VALID_HEIGHT, 0); + if (DEBUG) Log.d(TAG, "got minHeight of: " + minHeight); + + try { + maxHeight = a.getLayoutDimension(MAX_VALID_HEIGHT, UNBOUNDED); + if (DEBUG) Log.d(TAG, "got maxHeight of: " + maxHeight); + } catch (Exception e) { + if (DEBUG) Log.d(TAG, "caught exception looking for maxValidHeight " + e); + } + + a.recycle(); + } + + /** + * Creates a new set of layout parameters with the specified width, height + * and valid height bounds. + * + * @param width the width, either {@link #MATCH_PARENT}, + * {@link #WRAP_CONTENT} or a fixed size in pixels + * @param height the height, either {@link #MATCH_PARENT}, + * {@link #WRAP_CONTENT} or a fixed size in pixels + * @param minHeight the minimum height of this child + * @param maxHeight the maximum height of this child + * or {@link #UNBOUNDED} if the child can grow forever + */ + public LayoutParams(int width, int height, int minHeight, int maxHeight) { + super(width, height); + this.minHeight = minHeight; + this.maxHeight = maxHeight; + } + + /** + * {@inheritDoc} + */ + public LayoutParams(int width, int height) { + this(width, height, UNBOUNDED, UNBOUNDED); + } + + /** + * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}. + */ + public LayoutParams() { + this(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + } + + /** + * {@inheritDoc} + */ + public LayoutParams(ViewGroup.LayoutParams p) { + super(p); + minHeight = UNBOUNDED; + maxHeight = UNBOUNDED; + } + + public String debug(String output) { + return output + "SizeAdaptiveLayout.LayoutParams={" + + ", max=" + maxHeight + + ", max=" + minHeight + "}"; + } + } + + class BringToFrontOnEnd implements AnimatorListener { + @Override + public void onAnimationEnd(Animator animation) { + if (mCanceledAnimationCount == 0) { + mLeavingView.setVisibility(View.GONE); + mModestyPanel.setVisibility(View.GONE); + mEnteringView.bringToFront(); + mEnteringView = null; + mLeavingView = null; + } else { + mCanceledAnimationCount--; + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceledAnimationCount++; + } + + @Override + public void onAnimationRepeat(Animator animation) { + if (DEBUG) Log.d(TAG, "fade animation repeated: should never happen."); + assert(false); + } + + @Override + public void onAnimationStart(Animator animation) { + } + } +} |