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