2 * Copyright 2012 Google Inc. Licensed under the Apache License, Version 2.0
3 * (the "License"); you may not use this file except in compliance with the
4 * License. You may obtain a copy of the License at
5 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law
6 * or agreed to in writing, software distributed under the License is
7 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
8 * KIND, either express or implied. See the License for the specific language
9 * governing permissions and limitations under the License.
12 package com.cyanogenmod.eleven.widgets;
14 import android.animation.Animator;
15 import android.animation.AnimatorSet;
16 import android.animation.ObjectAnimator;
17 import android.annotation.TargetApi;
18 import android.content.Context;
19 import android.os.Build;
20 import android.util.AttributeSet;
21 import android.view.GestureDetector;
22 import android.view.MotionEvent;
23 import android.view.View;
24 import android.view.ViewConfiguration;
25 import android.view.ViewGroup;
28 * A layout that supports the Show/Hide pattern for portrait tablet layouts. See
30 * "http://developer.android.com/design/patterns/multi-pane-layouts.html#orientation"
31 * >Android Design > Patterns > Multi-pane Layouts & gt; Compound Views
32 * and Orientation Changes</a> for more details on this pattern. This layout
33 * should normally be used in association with the Up button. Specifically, show
34 * the master pane using {@link #showMaster(boolean, int)} when the Up button is
35 * pressed. If the master pane is visible, defer to normal Up behavior.
37 * TODO: swiping should be more tactile and actually follow the user's finger.
39 * Requires API level 11
41 @TargetApi(Build.VERSION_CODES.HONEYCOMB)
42 public class ShowHideMasterLayout extends ViewGroup implements Animator.AnimatorListener {
45 * A flag for {@link #showMaster(boolean, int)} indicating that the change
46 * in visiblity should not be animated.
48 public final static int FLAG_IMMEDIATE = 0x1;
50 private View sMasterView;
52 private View mDetailView;
54 private OnMasterVisibilityChangedListener mOnMasterVisibilityChangedListener;
56 private GestureDetector mGestureDetector;
58 private Runnable mShowMasterCompleteRunnable;
60 private boolean mFirstShow = true;
62 private boolean mMasterVisible = true;
64 private boolean mFlingToExposeMaster;
66 private boolean mIsAnimating;
68 /* The last measured master width, including its margins */
69 private int mTranslateAmount;
71 public interface OnMasterVisibilityChangedListener {
72 public void onMasterVisibilityChanged(boolean visible);
75 public ShowHideMasterLayout(final Context context) {
80 public ShowHideMasterLayout(final Context context, final AttributeSet attrs) {
81 super(context, attrs);
85 public ShowHideMasterLayout(final Context context, final AttributeSet attrs, final int defStyle) {
86 super(context, attrs, defStyle);
91 mGestureDetector = new GestureDetector(getContext(), mGestureListener);
95 public LayoutParams generateLayoutParams(final AttributeSet attrs) {
96 return new MarginLayoutParams(getContext(), attrs);
100 protected LayoutParams generateLayoutParams(final LayoutParams p) {
101 return new MarginLayoutParams(p);
105 protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
106 final int mCount = getChildCount();
108 /* Measure once to find the maximum child size */
113 for (int i = 0; i < mCount; i++) {
114 final View mChild = getChildAt(i);
115 if (mChild.getVisibility() == GONE) {
119 measureChildWithMargins(mChild, widthMeasureSpec, 0, heightMeasureSpec, 0);
120 final MarginLayoutParams mLayoutParams = (MarginLayoutParams)mChild.getLayoutParams();
121 sMaxWidth = Math.max(sMaxWidth, mChild.getMeasuredWidth() + mLayoutParams.leftMargin
122 + mLayoutParams.rightMargin);
123 sMaxHeight = Math.max(sMaxHeight, mChild.getMeasuredHeight() + mLayoutParams.topMargin
124 + mLayoutParams.bottomMargin);
125 mChildState = combineMeasuredStates(mChildState, mChild.getMeasuredState());
128 /* Account for padding too */
129 sMaxWidth += getPaddingLeft() + getPaddingRight();
130 sMaxHeight += getPaddingLeft() + getPaddingRight();
132 /* Check against our minimum height and width */
133 sMaxHeight = Math.max(sMaxHeight, getSuggestedMinimumHeight());
134 sMaxWidth = Math.max(sMaxWidth, getSuggestedMinimumWidth());
136 /* Set our own measured size */
137 setMeasuredDimension(
138 resolveSizeAndState(sMaxWidth, widthMeasureSpec, mChildState),
139 resolveSizeAndState(sMaxHeight, heightMeasureSpec,
140 mChildState << MEASURED_HEIGHT_STATE_SHIFT));
142 /* Measure children for them to set their measured dimensions */
143 for (int i = 0; i < mCount; i++) {
144 final View child = getChildAt(i);
145 if (child.getVisibility() == GONE) {
149 final MarginLayoutParams mLayoutParams = (MarginLayoutParams)child.getLayoutParams();
151 int mChildWidthMeasureSpec;
152 int mChildHeightMeasureSpec;
154 if (mLayoutParams.width == LayoutParams.MATCH_PARENT) {
155 mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth()
156 - getPaddingLeft() - getPaddingRight() - mLayoutParams.leftMargin
157 - mLayoutParams.rightMargin, MeasureSpec.EXACTLY);
159 mChildWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft()
160 + getPaddingRight() + mLayoutParams.leftMargin + mLayoutParams.rightMargin,
161 mLayoutParams.width);
164 if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
165 mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight()
166 - getPaddingTop() - getPaddingBottom() - mLayoutParams.topMargin
167 - mLayoutParams.bottomMargin, MeasureSpec.EXACTLY);
169 mChildHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
170 getPaddingTop() + getPaddingBottom() + mLayoutParams.topMargin
171 + mLayoutParams.bottomMargin, mLayoutParams.height);
174 child.measure(mChildWidthMeasureSpec, mChildHeightMeasureSpec);
179 protected void onLayout(final boolean changed, final int l, final int t, final int r,
181 updateChildReferences();
183 if (sMasterView == null || mDetailView == null) {
187 final int sMasterWidth = sMasterView.getMeasuredWidth();
188 final MarginLayoutParams sMasterLp = (MarginLayoutParams)sMasterView.getLayoutParams();
189 final MarginLayoutParams mDetailLp = (MarginLayoutParams)mDetailView.getLayoutParams();
191 mTranslateAmount = sMasterWidth + sMasterLp.leftMargin + sMasterLp.rightMargin;
193 sMasterView.layout(l + sMasterLp.leftMargin, t + sMasterLp.topMargin, l
194 + sMasterLp.leftMargin + sMasterWidth, b - sMasterLp.bottomMargin);
196 mDetailView.layout(l + mDetailLp.leftMargin + mTranslateAmount, t + mDetailLp.topMargin, r
197 - mDetailLp.rightMargin + mTranslateAmount, b - mDetailLp.bottomMargin);
199 /* Update translationX values */
201 final float mTranslationX = mMasterVisible ? 0 : -mTranslateAmount;
202 sMasterView.setTranslationX(mTranslationX);
203 mDetailView.setTranslationX(mTranslationX);
207 private void updateChildReferences() {
208 final int mChildCount = getChildCount();
209 sMasterView = mChildCount > 0 ? getChildAt(0) : null;
210 mDetailView = mChildCount > 1 ? getChildAt(1) : null;
214 * Allow or disallow the user to flick right on the detail pane to expose
217 * @param enabled Whether or not to enable this interaction.
219 public void setFlingToExposeMasterEnabled(final boolean enabled) {
220 mFlingToExposeMaster = enabled;
224 * Request the given listener be notified when the master pane is shown or
227 * @param listener The listener to notify when the master pane is shown or
230 public void setOnMasterVisibilityChangedListener(
231 final OnMasterVisibilityChangedListener listener) {
232 mOnMasterVisibilityChangedListener = listener;
236 * Returns whether or not the master pane is visible.
238 * @return True if the master pane is visible.
240 public boolean isMasterVisible() {
241 return mMasterVisible;
245 * Calls {@link #showMaster(boolean, int, Runnable)} with a null runnable.
247 public void showMaster(final boolean show, final int flags) {
248 showMaster(show, flags, null);
252 * Shows or hides the master pane.
254 * @param show Whether or not to show the master pane.
255 * @param flags {@link #FLAG_IMMEDIATE} to show/hide immediately, or 0 to
257 * @param completeRunnable An optional runnable to run when any animations
258 * related to this are complete.
260 public void showMaster(final boolean show, final int flags, final Runnable completeRunnable) {
261 if (!mFirstShow && mMasterVisible == show) {
265 mShowMasterCompleteRunnable = completeRunnable;
268 mMasterVisible = show;
269 if (mOnMasterVisibilityChangedListener != null) {
270 mOnMasterVisibilityChangedListener.onMasterVisibilityChanged(show);
273 updateChildReferences();
275 if (sMasterView == null || mDetailView == null) {
279 final float mTranslationX = show ? 0 : -mTranslateAmount;
281 if ((flags & FLAG_IMMEDIATE) != 0) {
282 sMasterView.setTranslationX(mTranslationX);
283 mDetailView.setTranslationX(mTranslationX);
284 if (mShowMasterCompleteRunnable != null) {
285 mShowMasterCompleteRunnable.run();
286 mShowMasterCompleteRunnable = null;
289 final long mDuration = getResources()
290 .getInteger(android.R.integer.config_shortAnimTime);
292 /* Animate if we have Honeycomb APIs, don't animate otherwise */
294 final AnimatorSet mAnimatorSet = new AnimatorSet();
295 sMasterView.setLayerType(LAYER_TYPE_HARDWARE, null);
296 mDetailView.setLayerType(LAYER_TYPE_HARDWARE, null);
298 ObjectAnimator.ofFloat(sMasterView, "translationX", mTranslationX).setDuration(
300 ObjectAnimator.ofFloat(mDetailView, "translationX", mTranslationX).setDuration(
302 mAnimatorSet.addListener(this);
303 mAnimatorSet.start();
308 public void requestDisallowInterceptTouchEvent(final boolean disallowIntercept) {
309 // Really bad hack... we really shouldn't do this.
310 // super.requestDisallowInterceptTouchEvent(disallowIntercept);
314 public boolean onInterceptTouchEvent(final MotionEvent event) {
315 if (mFlingToExposeMaster && !mMasterVisible) {
316 mGestureDetector.onTouchEvent(event);
319 if (event.getAction() == MotionEvent.ACTION_DOWN && sMasterView != null && mMasterVisible) {
320 if (event.getX() > mTranslateAmount) {
324 return super.onInterceptTouchEvent(event);
328 public boolean onTouchEvent(final MotionEvent event) {
329 if (mFlingToExposeMaster && !mMasterVisible && mGestureDetector.onTouchEvent(event)) {
333 if (event.getAction() == MotionEvent.ACTION_DOWN && sMasterView != null && mMasterVisible) {
334 if (event.getX() > mTranslateAmount) {
335 showMaster(false, 0);
339 return super.onTouchEvent(event);
343 public void onAnimationEnd(final Animator animator) {
344 mIsAnimating = false;
345 sMasterView.setLayerType(LAYER_TYPE_NONE, null);
346 mDetailView.setLayerType(LAYER_TYPE_NONE, null);
348 if (mShowMasterCompleteRunnable != null) {
349 mShowMasterCompleteRunnable.run();
350 mShowMasterCompleteRunnable = null;
355 public void onAnimationCancel(final Animator animator) {
356 mIsAnimating = false;
357 sMasterView.setLayerType(LAYER_TYPE_NONE, null);
358 mDetailView.setLayerType(LAYER_TYPE_NONE, null);
360 if (mShowMasterCompleteRunnable != null) {
361 mShowMasterCompleteRunnable.run();
362 mShowMasterCompleteRunnable = null;
366 private final GestureDetector.OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() {
368 public boolean onDown(final MotionEvent e) {
373 public boolean onFling(final MotionEvent e1, final MotionEvent e2, final float velocityX,
374 final float velocityY) {
375 final ViewConfiguration mViewConfig = ViewConfiguration.get(getContext());
376 final float mAbsVelocityX = Math.abs(velocityX);
377 final float mAbsVelocityY = Math.abs(velocityY);
378 if (mFlingToExposeMaster && !mMasterVisible && velocityX > 0
379 && mAbsVelocityX >= mAbsVelocityY
380 && mAbsVelocityX > mViewConfig.getScaledMinimumFlingVelocity()
381 && mAbsVelocityX < mViewConfig.getScaledMaximumFlingVelocity()) {
385 return super.onFling(e1, e2, velocityX, velocityY);
390 public void onAnimationStart(final Animator animator) {
395 public void onAnimationRepeat(final Animator animator) {