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 || heightSize > tallestViewSize) { 229 return tallestView; 230 } else { 231 return smallestView; 232 } 233 } 234 235 @Override 236 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 237 if (DEBUG) Log.d(TAG, this + " onlayout height: " + (bottom - top)); 238 mLastActive = mActiveChild; 239 int measureSpec = View.MeasureSpec.makeMeasureSpec(bottom - top, 240 View.MeasureSpec.EXACTLY); 241 mActiveChild = selectActiveChild(measureSpec); 242 mActiveChild.setVisibility(View.VISIBLE); 243 244 if (mLastActive != mActiveChild && mLastActive != null) { 245 if (DEBUG) Log.d(TAG, this + " changed children from: " + mLastActive + 246 " to: " + mActiveChild); 247 248 mEnteringView = mActiveChild; 249 mLeavingView = mLastActive; 250 251 mEnteringView.setAlpha(1f); 252 253 mModestyPanel.setAlpha(1f); 254 mModestyPanel.bringToFront(); 255 mModestyPanelTop = mLeavingView.getHeight(); 256 mModestyPanel.setVisibility(View.VISIBLE); 257 // TODO: mModestyPanel background should be compatible with mLeavingView 258 259 mLeavingView.bringToFront(); 260 261 if (mTransitionAnimation.isRunning()) { 262 mTransitionAnimation.cancel(); 263 } 264 mFadeView.setTarget(mLeavingView); 265 mFadeView.setFloatValues(0f); 266 mFadePanel.setFloatValues(0f); 267 mTransitionAnimation.setupStartValues(); 268 mTransitionAnimation.start(); 269 } 270 final int childWidth = mActiveChild.getMeasuredWidth(); 271 final int childHeight = mActiveChild.getMeasuredHeight(); 272 // TODO investigate setting LAYER_TYPE_HARDWARE on mLastActive 273 mActiveChild.layout(0, 0, childWidth, childHeight); 274 275 if (DEBUG) Log.d(TAG, "got modesty offset of " + mModestyPanelTop); 276 mModestyPanel.layout(0, mModestyPanelTop, childWidth, mModestyPanelTop + childHeight); 277 } 278 279 @Override 280 public LayoutParams generateLayoutParams(AttributeSet attrs) { 281 if (DEBUG) Log.d(TAG, "generate layout from attrs"); 282 return new SizeAdaptiveLayout.LayoutParams(getContext(), attrs); 283 } 284 285 @Override 286 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 287 if (DEBUG) Log.d(TAG, "generate default layout from viewgroup"); 288 return new SizeAdaptiveLayout.LayoutParams(p); 289 } 290 291 @Override 292 protected LayoutParams generateDefaultLayoutParams() { 293 if (DEBUG) Log.d(TAG, "generate default layout from null"); 294 return new SizeAdaptiveLayout.LayoutParams(); 295 } 296 297 @Override 298 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 299 return p instanceof SizeAdaptiveLayout.LayoutParams; 300 } 301 302 /** 303 * Per-child layout information associated with ViewSizeAdaptiveLayout. 304 * 305 * TODO extend to width and height 306 * 307 * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_minHeight 308 * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_maxHeight 309 */ 310 public static class LayoutParams extends ViewGroup.LayoutParams { 311 312 /** 313 * Indicates the minimum valid height for the child. 314 */ 315 @ViewDebug.ExportedProperty(category = "layout") 316 public int minHeight; 317 318 /** 319 * Indicates the maximum valid height for the child. 320 */ 321 @ViewDebug.ExportedProperty(category = "layout") 322 public int maxHeight; 323 324 /** 325 * Constant value for maxHeight that indicates there is not maximum height. 326 */ 327 public static final int UNBOUNDED = -1; 328 329 /** 330 * {@inheritDoc} 331 */ 332 public LayoutParams(Context c, AttributeSet attrs) { 333 super(c, attrs); 334 if (DEBUG) { 335 Log.d(TAG, "construct layout from attrs"); 336 for (int i = 0; i < attrs.getAttributeCount(); i++) { 337 Log.d(TAG, " " + attrs.getAttributeName(i) + " = " + 338 attrs.getAttributeValue(i)); 339 } 340 } 341 TypedArray a = 342 c.obtainStyledAttributes(attrs, 343 R.styleable.SizeAdaptiveLayout_Layout); 344 345 minHeight = a.getDimensionPixelSize(MIN_VALID_HEIGHT, 0); 346 if (DEBUG) Log.d(TAG, "got minHeight of: " + minHeight); 347 348 try { 349 maxHeight = a.getLayoutDimension(MAX_VALID_HEIGHT, UNBOUNDED); 350 if (DEBUG) Log.d(TAG, "got maxHeight of: " + maxHeight); 351 } catch (Exception e) { 352 if (DEBUG) Log.d(TAG, "caught exception looking for maxValidHeight " + e); 353 } 354 355 a.recycle(); 356 } 357 358 /** 359 * Creates a new set of layout parameters with the specified width, height 360 * and valid height bounds. 361 * 362 * @param width the width, either {@link #MATCH_PARENT}, 363 * {@link #WRAP_CONTENT} or a fixed size in pixels 364 * @param height the height, either {@link #MATCH_PARENT}, 365 * {@link #WRAP_CONTENT} or a fixed size in pixels 366 * @param minHeight the minimum height of this child 367 * @param maxHeight the maximum height of this child 368 * or {@link #UNBOUNDED} if the child can grow forever 369 */ 370 public LayoutParams(int width, int height, int minHeight, int maxHeight) { 371 super(width, height); 372 this.minHeight = minHeight; 373 this.maxHeight = maxHeight; 374 } 375 376 /** 377 * {@inheritDoc} 378 */ 379 public LayoutParams(int width, int height) { 380 this(width, height, UNBOUNDED, UNBOUNDED); 381 } 382 383 /** 384 * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}. 385 */ 386 public LayoutParams() { 387 this(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 388 } 389 390 /** 391 * {@inheritDoc} 392 */ 393 public LayoutParams(ViewGroup.LayoutParams p) { 394 super(p); 395 minHeight = UNBOUNDED; 396 maxHeight = UNBOUNDED; 397 } 398 399 public String debug(String output) { 400 return output + "SizeAdaptiveLayout.LayoutParams={" + 401 ", max=" + maxHeight + 402 ", max=" + minHeight + "}"; 403 } 404 } 405 406 class BringToFrontOnEnd implements AnimatorListener { 407 @Override 408 public void onAnimationEnd(Animator animation) { 409 if (mCanceledAnimationCount == 0) { 410 mLeavingView.setVisibility(View.GONE); 411 mModestyPanel.setVisibility(View.GONE); 412 mEnteringView.bringToFront(); 413 mEnteringView = null; 414 mLeavingView = null; 415 } else { 416 mCanceledAnimationCount--; 417 } 418 } 419 420 @Override 421 public void onAnimationCancel(Animator animation) { 422 mCanceledAnimationCount++; 423 } 424 425 @Override 426 public void onAnimationRepeat(Animator animation) { 427 if (DEBUG) Log.d(TAG, "fade animation repeated: should never happen."); 428 assert(false); 429 } 430 431 @Override 432 public void onAnimationStart(Animator animation) { 433 } 434 } 435 } 436