/* * Copyright 2012 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.apps.iosched.ui.widget; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.util.AttributeSet; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import static com.google.android.apps.iosched.util.LogUtils.LOGW; import static com.google.android.apps.iosched.util.LogUtils.makeLogTag; /** * A layout that supports the Show/Hide pattern for portrait tablet layouts. See <a * href="http://developer.android.com/design/patterns/multi-pane-layouts.html#orientation">Android * Design > Patterns > Multi-pane Layouts & gt; Compound Views and Orientation Changes</a> for * more details on this pattern. This layout should normally be used in association with the Up * button. Specifically, show the master pane using {@link #showMaster(boolean, int)} when the Up * button is pressed. If the master pane is visible, defer to normal Up behavior. * * <p>TODO: swiping should be more tactile and actually follow the user's finger. * * <p>Requires API level 11 */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class ShowHideMasterLayout extends ViewGroup implements Animator.AnimatorListener { private static final String TAG = makeLogTag(ShowHideMasterLayout.class); /** * A flag for {@link #showMaster(boolean, int)} indicating that the change in visiblity should * not be animated. */ public static final int FLAG_IMMEDIATE = 0x1; private boolean mFirstShow = true; private boolean mMasterVisible = true; private View mMasterView; private View mDetailView; private OnMasterVisibilityChangedListener mOnMasterVisibilityChangedListener; private GestureDetector mGestureDetector; private boolean mFlingToExposeMaster; private boolean mIsAnimating; private Runnable mShowMasterCompleteRunnable; // The last measured master width, including its margins. private int mTranslateAmount; public interface OnMasterVisibilityChangedListener { public void onMasterVisibilityChanged(boolean visible); } public ShowHideMasterLayout(Context context) { super(context); init(); } public ShowHideMasterLayout(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ShowHideMasterLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } private void init() { mGestureDetector = new GestureDetector(getContext(), mGestureListener); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); // Measure once to find the maximum child size. int maxHeight = 0; int maxWidth = 0; int childState = 0; for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState, child.getMeasuredState()); } // Account for padding too maxWidth += getPaddingLeft() + getPaddingRight(); maxHeight += getPaddingLeft() + getPaddingRight(); // Check against our minimum height and width maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); // Set our own measured size setMeasuredDimension( resolveSizeAndState(maxWidth, widthMeasureSpec, childState), resolveSizeAndState(maxHeight, heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT)); // Measure children for them to set their measured dimensions for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() == GONE) { continue; } final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int childWidthMeasureSpec; int childHeightMeasureSpec; if (lp.width == LayoutParams.MATCH_PARENT) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin, lp.width); } if (lp.height == LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - getPaddingTop() - getPaddingBottom() - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin, lp.height); } child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { updateChildReferences(); if (mMasterView == null || mDetailView == null) { LOGW(TAG, "Master or detail is missing (need 2 children), can't layout."); return; } int masterWidth = mMasterView.getMeasuredWidth(); MarginLayoutParams masterLp = (MarginLayoutParams) mMasterView.getLayoutParams(); MarginLayoutParams detailLp = (MarginLayoutParams) mDetailView.getLayoutParams(); mTranslateAmount = masterWidth + masterLp.leftMargin + masterLp.rightMargin; mMasterView.layout( l + masterLp.leftMargin, t + masterLp.topMargin, l + masterLp.leftMargin + masterWidth, b - masterLp.bottomMargin); mDetailView.layout( l + detailLp.leftMargin + mTranslateAmount, t + detailLp.topMargin, r - detailLp.rightMargin + mTranslateAmount, b - detailLp.bottomMargin); // Update translationX values if (!mIsAnimating) { final float translationX = mMasterVisible ? 0 : -mTranslateAmount; mMasterView.setTranslationX(translationX); mDetailView.setTranslationX(translationX); } } private void updateChildReferences() { int childCount = getChildCount(); mMasterView = (childCount > 0) ? getChildAt(0) : null; mDetailView = (childCount > 1) ? getChildAt(1) : null; } /** * Allow or disallow the user to flick right on the detail pane to expose the master pane. * @param enabled Whether or not to enable this interaction. */ public void setFlingToExposeMasterEnabled(boolean enabled) { mFlingToExposeMaster = enabled; } /** * Request the given listener be notified when the master pane is shown or hidden. * * @param listener The listener to notify when the master pane is shown or hidden. */ public void setOnMasterVisibilityChangedListener(OnMasterVisibilityChangedListener listener) { mOnMasterVisibilityChangedListener = listener; } /** * Returns whether or not the master pane is visible. * * @return True if the master pane is visible. */ public boolean isMasterVisible() { return mMasterVisible; } /** * Calls {@link #showMaster(boolean, int, Runnable)} with a null runnable. */ public void showMaster(boolean show, int flags) { showMaster(show, flags, null); } /** * Shows or hides the master pane. * * @param show Whether or not to show the master pane. * @param flags {@link #FLAG_IMMEDIATE} to show/hide immediately, or 0 to animate. * @param completeRunnable An optional runnable to run when any animations related to this are * complete. */ public void showMaster(boolean show, int flags, Runnable completeRunnable) { if (!mFirstShow && mMasterVisible == show) { return; } mShowMasterCompleteRunnable = completeRunnable; mFirstShow = false; mMasterVisible = show; if (mOnMasterVisibilityChangedListener != null) { mOnMasterVisibilityChangedListener.onMasterVisibilityChanged(show); } updateChildReferences(); if (mMasterView == null || mDetailView == null) { LOGW(TAG, "Master or detail is missing (need 2 children), can't change translation."); return; } final float translationX = show ? 0 : -mTranslateAmount; if ((flags & FLAG_IMMEDIATE) != 0) { mMasterView.setTranslationX(translationX); mDetailView.setTranslationX(translationX); if (mShowMasterCompleteRunnable != null) { mShowMasterCompleteRunnable.run(); mShowMasterCompleteRunnable = null; } } else { final long duration = getResources().getInteger(android.R.integer.config_shortAnimTime); // Animate if we have Honeycomb APIs, don't animate otherwise mIsAnimating = true; AnimatorSet animatorSet = new AnimatorSet(); mMasterView.setLayerType(LAYER_TYPE_HARDWARE, null); mDetailView.setLayerType(LAYER_TYPE_HARDWARE, null); animatorSet .play(ObjectAnimator .ofFloat(mMasterView, "translationX", translationX) .setDuration(duration)) .with(ObjectAnimator .ofFloat(mDetailView, "translationX", translationX) .setDuration(duration)); animatorSet.addListener(this); animatorSet.start(); // For API level 12+, use this instead: // mMasterView.animate().translationX().setDuration(duration); // mDetailView.animate().translationX(show ? masterWidth : 0).setDuration(duration); } } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { // Really bad hack... we really shouldn't do this. //super.requestDisallowInterceptTouchEvent(disallowIntercept); } @Override public boolean onInterceptTouchEvent(MotionEvent event) { if (mFlingToExposeMaster && !mMasterVisible) { mGestureDetector.onTouchEvent(event); } if (event.getAction() == MotionEvent.ACTION_DOWN && mMasterView != null && mMasterVisible) { // If the master is visible, touching in the detail area should hide the master. if (event.getX() > mTranslateAmount) { return true; } } return super.onInterceptTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { if (mFlingToExposeMaster && !mMasterVisible && mGestureDetector.onTouchEvent(event)) { return true; } if (event.getAction() == MotionEvent.ACTION_DOWN && mMasterView != null && mMasterVisible) { // If the master is visible, touching in the detail area should hide the master. if (event.getX() > mTranslateAmount) { showMaster(false, 0); return true; } } return super.onTouchEvent(event); } @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { mIsAnimating = false; mMasterView.setLayerType(LAYER_TYPE_NONE, null); mDetailView.setLayerType(LAYER_TYPE_NONE, null); requestLayout(); if (mShowMasterCompleteRunnable != null) { mShowMasterCompleteRunnable.run(); mShowMasterCompleteRunnable = null; } } @Override public void onAnimationCancel(Animator animator) { mIsAnimating = false; mMasterView.setLayerType(LAYER_TYPE_NONE, null); mDetailView.setLayerType(LAYER_TYPE_NONE, null); requestLayout(); if (mShowMasterCompleteRunnable != null) { mShowMasterCompleteRunnable.run(); mShowMasterCompleteRunnable = null; } } @Override public void onAnimationRepeat(Animator animator) { } private final GestureDetector.OnGestureListener mGestureListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { ViewConfiguration viewConfig = ViewConfiguration.get(getContext()); float absVelocityX = Math.abs(velocityX); float absVelocityY = Math.abs(velocityY); if (mFlingToExposeMaster && !mMasterVisible && velocityX > 0 && absVelocityX >= absVelocityY // Fling at least as hard in X as in Y && absVelocityX > viewConfig.getScaledMinimumFlingVelocity() && absVelocityX < viewConfig.getScaledMaximumFlingVelocity()) { showMaster(true, 0); return true; } return super.onFling(e1, e2, velocityX, velocityY); } }; }