blob: ba113a337c4803fced5e84d6226c9cbffce2aa12 [file] [log] [blame]
Chris Wren3a59d6e2012-03-30 18:28:14 -04001/*
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
Chris Wren54dfa5d2012-04-24 09:50:03 -040019import java.lang.Math;
20
Chris Wren3a59d6e2012-03-30 18:28:14 -040021import 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;
Chris Wrend84e5932012-04-19 17:58:03 -040031import android.graphics.drawable.Drawable;
32import android.graphics.drawable.StateListDrawable;
Chris Wren3a59d6e2012-03-30 18:28:14 -040033import android.util.AttributeSet;
34import android.util.Log;
Chris Wrend84e5932012-04-19 17:58:03 -040035import android.util.StateSet;
Chris Wren3a59d6e2012-03-30 18:28:14 -040036import 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;
Chris Wrened5cc0e2012-06-21 11:25:58 -040056 private static final boolean REPORT_BAD_BOUNDS = true;
Chris Wren3a59d6e2012-03-30 18:28:14 -040057 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.
Chris Wrend84e5932012-04-19 17:58:03 -040099 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);
Chris Wren3a59d6e2012-03-30 18:28:14 -0400107 } 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);
Chris Wren54dfa5d2012-04-24 09:50:03 -0400163 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
Chris Wrened5cc0e2012-06-21 11:25:58 -0400179 if (REPORT_BAD_BOUNDS && heightIn != height) {
Chris Wren54dfa5d2012-04-24 09:50:03 -0400180 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;
Chris Wren3a59d6e2012-03-30 18:28:14 -0400186 }
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 }
Fabrice Di Meglio8afcd142012-07-27 18:27:11 -0700228 if (heightMode == MeasureSpec.UNSPECIFIED || heightSize > tallestViewSize) {
Chris Wren3a59d6e2012-03-30 18:28:14 -0400229 return tallestView;
Fabrice Di Meglio8afcd142012-07-27 18:27:11 -0700230 } else {
231 return smallestView;
Chris Wren3a59d6e2012-03-30 18:28:14 -0400232 }
Chris Wren3a59d6e2012-03-30 18:28:14 -0400233 }
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
Fabrice Di Meglio8afcd142012-07-27 18:27:11 -0700273 mActiveChild.layout(0, 0, childWidth, childHeight);
Chris Wren3a59d6e2012-03-30 18:28:14 -0400274
275 if (DEBUG) Log.d(TAG, "got modesty offset of " + mModestyPanelTop);
Fabrice Di Meglio8afcd142012-07-27 18:27:11 -0700276 mModestyPanel.layout(0, mModestyPanelTop, childWidth, mModestyPanelTop + childHeight);
Chris Wren3a59d6e2012-03-30 18:28:14 -0400277 }
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}