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