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