package info.guardianproject.securereaderinterface.views; import info.guardianproject.securereaderinterface.App; import info.guardianproject.securereaderinterface.uiutil.AnimationHelpers; import info.guardianproject.securereaderinterface.uiutil.UIHelpers; import info.guardianproject.securereaderinterface.R; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Region.Op; import android.graphics.Shader; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewConfigurationCompat; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.ViewTreeObserver.OnGlobalLayoutListener; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.BounceInterpolator; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.Transformation; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; import com.actionbarsherlock.app.ActionBar; public class LeftSideMenu { public static final String LOGTAG = "LeftSideMenu"; public interface LeftSideMenuListener { void onMenuCreated(View parent, View menuRoot, View menu); void onBeforeShow(); void onHide(); } private static final int ANIMATION_SLIDE_DURATION = 600; // Duration of the // slide // animation in // ms private static final float ANIMATION_DECELERATION = 2f; private static final float MENU_SPEED = 0.5f; // 0 for REVEAL, 1 for SLIDE // and in between for cool // effects private static final float RUBBERBAND_LIMIT = 30.0f; private static final boolean USE_SHADOW = true; private final Activity mActivity; private final ActionBar mActionBar; private final int mResIdMenuLayout; private final FrameLayout mParent; private final View mContent; private boolean mMenuShown = false; private int mMenuWidth; private int mStatusBarHeight = 0; private LeftMenuRootLayout mRoot; private View mMenuView; private boolean mIsAnimating; private boolean mIsDragEnabled; private float mShowPercentage; private LeftSideMenuListener mListener; private int positionMenu; private int positionContent; private boolean mIsInteractive; private boolean mDisplayMenuIndicator; public LeftSideMenu(Activity activity, ActionBar actionBar, int resIdMenuLayout) { this.mActivity = activity; this.mActionBar = actionBar; this.mResIdMenuLayout = resIdMenuLayout; mIsInteractive = false; mIsDragEnabled = true; mIsAnimating = false; mShowPercentage = 0; mDisplayMenuIndicator = true; mParent = (FrameLayout) mActivity.getWindow().getDecorView(); mContent = mParent.getChildAt(0); final ViewTreeObserver vto = mParent.getViewTreeObserver(); vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { try { // remove the listener... or we'll be doing this a lot. ViewTreeObserver observer = vto; if (!observer.isAlive()) observer = mContent.getViewTreeObserver(); observer.removeGlobalOnLayoutListener(this); } catch (Exception ex) { } createMenuView(); if (mListener != null) mListener.onMenuCreated(mParent, mRoot, mMenuView); } }); } public void checkMenuCreated() { if (mParent != null && mRoot != null && mMenuView != null && mListener != null) mListener.onMenuCreated(mParent, mRoot, mMenuView); } public View getMenuView() { return mMenuView; } public boolean isDragEnabled() { return mIsDragEnabled; } public void setDragEnabled(boolean enabled) { mIsDragEnabled = enabled; } public void setListener(LeftSideMenuListener listener) { this.mListener = listener; } public void show() { this.show(true, true); } public void toggle() { if (mMenuShown) hide(); else show(); } public int getMenuWidth() { createMenuView(); return mMenuWidth; } private View createMenuView() { if (mMenuView == null) { // get the height of the status bar if (mStatusBarHeight == 0) { mStatusBarHeight = UIHelpers.getStatusBarHeight(mActivity); } // Create the new root container that will hold both our menu and // the actual content // (as well as the hidden touch intercept view and the "drag bar") // mRoot = new LeftMenuRootLayout(mActivity); FrameLayout.LayoutParams laysRoot = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.LEFT | Gravity.TOP); laysRoot.setMargins(0, mStatusBarHeight, 0, 0); mRoot.setLayoutParams(laysRoot); LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mMenuView = inflater.inflate(mResIdMenuLayout, mRoot, false); mMenuView.setTag(this); FrameLayout.LayoutParams lays = (LayoutParams) mMenuView.getLayoutParams(); lays.setMargins(0, 0, 0, 0); mMenuView.setLayoutParams(lays); mMenuView.setVisibility(View.GONE); mMenuWidth = lays.width; mRoot.addView(mMenuView); // Then move the content over to our new root // mParent.removeView(mContent); // Set content layout params // FrameLayout.LayoutParams lpContent = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.LEFT | Gravity.TOP); lpContent.setMargins(0, -mStatusBarHeight, 0, 0); mContent.setLayoutParams(lpContent); mRoot.addView(mContent); mParent.addView(mRoot); } return mMenuView; } public boolean showMenuHintIfNotShown() { if (mMenuView != null) { if (!App.getSettings().hasShownMenuHint()) { mRoot.postDelayed(new Runnable() { @Override public void run() { showBounce(0f); App.getSettings().setHasShownMenuHint(true); } }, 3000); return true; } } return false; } public void show(boolean animate, boolean callOnBeforeShow) { if (mIsAnimating) return; mIsAnimating = true; if (mListener != null && callOnBeforeShow) mListener.onBeforeShow(); final MenuShowAnimation anim = new MenuShowAnimation(100.0f); anim.setInterpolator(new DecelerateInterpolator(ANIMATION_DECELERATION)); anim.setDuration(ANIMATION_SLIDE_DURATION); anim.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { mRoot.post(new Runnable() { @Override public void run() { enableHardwareLayering(false); setInteractive(true); } }); mIsAnimating = false; mMenuShown = true; } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationStart(Animation animation) { } }); mMenuView.startAnimation(anim); } public void hide() { if (mIsAnimating) return; mIsAnimating = true; if (mMenuView != null) { setInteractive(false); enableHardwareLayering(true); final MenuShowAnimation anim = new MenuShowAnimation(0.0f); anim.setInterpolator(new DecelerateInterpolator(ANIMATION_DECELERATION)); anim.setDuration(ANIMATION_SLIDE_DURATION); anim.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { mIsAnimating = false; mMenuShown = false; if (mListener != null) mListener.onHide(); mRoot.post(new Runnable() { @Override public void run() { enableHardwareLayering(false); } }); } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationStart(Animation animation) { } }); mMenuView.startAnimation(anim); } } public void showBounce(final float fractionVisible) { if (mIsAnimating) return; mIsAnimating = true; if (mListener != null) mListener.onBeforeShow(); setInteractive(false); enableHardwareLayering(true); final MenuShowAnimation anim = new MenuShowAnimation(40.0f); MenuBounceInterpolator ip = new MenuBounceInterpolator(fractionVisible); anim.setInterpolator(ip); anim.setDuration(4 * ANIMATION_SLIDE_DURATION); anim.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { mIsAnimating = false; mMenuShown = false; } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationStart(Animation animation) { } }); mMenuView.startAnimation(anim); } public class MenuBounceInterpolator implements Interpolator { private final Interpolator mDecelerateInterpolator = new DecelerateInterpolator(); private final Interpolator mBounceInterpolator = new BounceInterpolator(); private final float mFractionVisible; public MenuBounceInterpolator(float fractionVisible) { mFractionVisible = fractionVisible; } @Override public float getInterpolation(float t) { // The purpose of this is pretty simple : // - mDecelerateInterpolator for t < 0.33 // - mBounceInterpolator for t >= 0.33 if (t < 0.33) { return mDecelerateInterpolator.getInterpolation(t / 0.33f); } else { return 1f - (1.0f - mFractionVisible) * mBounceInterpolator.getInterpolation((t - 0.33f) / 0.67f); } } } private boolean getInteractive() { return mIsInteractive; } private void setInteractive(boolean interactive) { if (mIsInteractive != interactive) { mIsInteractive = interactive; if (mIsInteractive) { AnimationHelpers.setTranslationX(mContent, positionContent); AnimationHelpers.setTranslationX(mMenuView, positionMenu); } else { AnimationHelpers.setTranslationX(mContent, 0); AnimationHelpers.setTranslationX(mMenuView, 0); } mRoot.invalidate(); } } @SuppressLint("NewApi") private void enableHardwareLayering(boolean enable) { if (Build.VERSION.SDK_INT >= 11) { int layerType = enable ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE; if (layerType != mMenuView.getLayerType()) { mMenuView.setLayerType(layerType, null); mContent.setLayerType(layerType, null); } } else if (mMenuView.isDrawingCacheEnabled() != enable) { mMenuView.setDrawingCacheEnabled(enable); mContent.setDrawingCacheEnabled(enable); } } public boolean isOpen() { return mMenuShown; } public float getShowPercentage() { return mShowPercentage; } @SuppressLint("NewApi") public void setShowPercentage(float percent) { if (percent > 100.0f) percent = 100.0f; else if (percent < 0.0f) percent = 0.0f; mShowPercentage = percent; createMenuView(); int contentPercentage = (int) ((mMenuWidth * percent) / 100.0f); int menuPercentage = -(int) ((MENU_SPEED * mMenuWidth * (100.0f - percent)) / 100.0f); if (mIsInteractive) { AnimationHelpers.setTranslationX(mContent, contentPercentage); AnimationHelpers.setTranslationX(mMenuView, menuPercentage); } else { positionContent = contentPercentage; positionMenu = menuPercentage; } ViewCompat.postInvalidateOnAnimation(mRoot); if (percent == 0) { mMenuView.setVisibility(View.GONE); } else { mMenuView.setVisibility(View.VISIBLE); } } public class MenuShowAnimation extends Animation { float fromShowPercentage; float toShowPercentage; public MenuShowAnimation(float toShowPercentage) { this.fromShowPercentage = LeftSideMenu.this.getShowPercentage(); this.toShowPercentage = toShowPercentage; } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { float newPercentage = fromShowPercentage + ((toShowPercentage - fromShowPercentage) * interpolatedTime); LeftSideMenu.this.setShowPercentage(newPercentage); } @Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); } @Override public boolean willChangeBounds() { return true; } } private class LeftMenuRootLayout extends FrameLayout { private final Paint mShadowPaint; private int mMenuIndicatorY; private int mMenuIndicatorInset; private BitmapDrawable mMenuIndicatorDrawable; private boolean mClickedOutside; private int mTranslationAtStartOfDrag; private final int mTouchSlop; private boolean mIsBeingDragged; private boolean mIsUnableToDrag; private int mActivePointerId; private float mInitialMotionX; private float mInitialMotionY; private float mLastMotionX; private float mLastMotionY; public LeftMenuRootLayout(Context context) { super(context); mShadowPaint = new Paint(); mShadowPaint.setAntiAlias(true); mShadowPaint.setDither(true); mShadowPaint.setStyle(Paint.Style.FILL); mShadowPaint.setShader(new LinearGradient(0f, 0f, 30f, 0f, new int[] { 0x00000000, 0x60000000, 0xff000000 }, new float[] { 0f, 0.8f, 1f }, Shader.TileMode.CLAMP)); updateMenuIcon(); mIsBeingDragged = false; final ViewConfiguration configuration = ViewConfiguration.get(context); mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); } public void updateMenuIcon() { // Get information about menu icon to display (if any) // TypedArray styled = getContext().obtainStyledAttributes(new int[] { com.actionbarsherlock.R.attr.actionBarSize, R.attr.actionBarMenuIcon, R.attr.actionBarMenuIconInset } ); int actionBarHeight = styled.getDimensionPixelOffset(0, UIHelpers.dpToPx(54, getContext())); Drawable actionBarMenuIcon = styled.getDrawable(1); mMenuIndicatorInset = styled.getDimensionPixelOffset(2, UIHelpers.dpToPx(6, getContext())); styled.recycle(); if (actionBarMenuIcon != null && actionBarMenuIcon instanceof BitmapDrawable) mMenuIndicatorDrawable = (BitmapDrawable) actionBarMenuIcon; else mMenuIndicatorDrawable = null; mMenuIndicatorY = actionBarHeight / 2; } @SuppressLint("NewApi") @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean ret; canvas.save(); if (!getInteractive()) { if (child == mContent) { canvas.translate(positionContent, 0); } else if (child == mMenuView) { canvas.translate(positionMenu, 0); canvas.clipRect(0, mMenuView.getTop(), positionContent - positionMenu, mMenuView.getBottom(), Op.REPLACE); } } ret = super.drawChild(canvas, child, drawingTime); // Draw shadow? if (child == mMenuView && USE_SHADOW) { int offset = positionContent - positionMenu; if (offset > 0) { canvas.translate(offset - 30, 0); canvas.drawRect(0, mMenuView.getTop(), 30f, mMenuView.getBottom(), mShadowPaint); } } canvas.restore(); // Draw menu indicator icon? if (child == mContent && mMenuIndicatorDrawable != null && mDisplayMenuIndicator && mIsDragEnabled) { canvas.save(); canvas.translate(positionContent, 0); int xTo = - (int)((mMenuIndicatorInset) * getShowPercentage() / 100f); Bitmap bmp = mMenuIndicatorDrawable.getBitmap(); canvas.clipRect(0, 0, xTo + bmp.getWidth(), mContent.getBottom(), Op.REPLACE); canvas.drawBitmap(bmp, xTo, mMenuIndicatorY - (bmp.getHeight() / 2), null); canvas.restore(); } return ret; } private boolean isWithinDragHandle(float x, float y) { Rect draggerRect = new Rect(); if (mContent.getGlobalVisibleRect(draggerRect)) { draggerRect.left -= UIHelpers.dpToPx(30, getContext()); draggerRect.right = draggerRect.left + UIHelpers.dpToPx(40, getContext()); draggerRect.top += mActionBar.getHeight(); if (draggerRect.contains((int) x, (int) y)) { return true; } } return false; } private boolean isWithinMenu(float x, float y) { Rect menuRect = new Rect(); if (mMenuView.getGlobalVisibleRect(menuRect)) { // Adjust rect to menu width! menuRect.right = menuRect.left + mMenuWidth; if (menuRect.contains((int) x, (int) y)) { return true; } } return false; } protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { if (v.getVisibility() != View.VISIBLE) return false; if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int scrollX = v.getScrollX(); final int scrollY = v.getScrollY(); final int count = group.getChildCount(); // Count backwards - let topmost views consume scroll distance // first. for (int i = count - 1; i >= 0; i--) { final View child = group.getChildAt(i); if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && canScroll(child, true, dx, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) { return true; } } } return checkV && UIHelpers.canScrollHorizontally(v, -dx); } private void onSecondaryPointerUp(MotionEvent ev) { final int pointerIndex = MotionEventCompat.getActionIndex(ev); final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); if (pointerId == mActivePointerId) { // This was our active pointer going up. Choose a new // active pointer and adjust accordingly. final int newPointerIndex = pointerIndex == 0 ? 1 : 0; mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); } } private void startDrag() { if (!mIsDragEnabled || mIsBeingDragged) return; mIsBeingDragged = true; if (mListener != null) mListener.onBeforeShow(); mTranslationAtStartOfDrag = AnimationHelpers.getTranslationX(mContent); setInteractive(false); enableHardwareLayering(true); } private void endDrag() { mIsBeingDragged = false; mIsUnableToDrag = false; // Open or close, based on how far we pulled it out float rubberbandPercentage = LeftSideMenu.this.isOpen() ? (100.0f - RUBBERBAND_LIMIT) : RUBBERBAND_LIMIT; if (LeftSideMenu.this.getShowPercentage() > rubberbandPercentage) LeftSideMenu.this.show(true, false); else LeftSideMenu.this.hide(); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * This method JUST determines whether we want to intercept the * motion. If we return true, onMotionEvent will be called and we do * the actual scrolling there. */ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; // Always take care of the touch gesture being complete. if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // Release the drag. mIsBeingDragged = false; mIsUnableToDrag = false; mActivePointerId = -1; return false; } // Nothing more to do here if we have decided whether or not we // are dragging. if (action != MotionEvent.ACTION_DOWN) { if (mIsBeingDragged) { return true; } if (mIsUnableToDrag) { return false; } } switch (action) { case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have * caught it. Check whether the user has moved far enough from * his original down touch. */ /* * Locally do absolute value. mLastMotionY is set to the y value * of the down event. */ final int activePointerId = mActivePointerId; if (activePointerId == -1) { // If we don't have a valid id, the touch down wasn't on // content. break; } final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); final float x = MotionEventCompat.getX(ev, pointerIndex); final float dx = x - mLastMotionX; final float xDiff = Math.abs(dx); final float y = MotionEventCompat.getY(ev, pointerIndex); final float yDiff = Math.abs(y - mInitialMotionY); if (dx != 0 && canScroll(this, false, (int) dx, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it // be handled there. mLastMotionX = x; mLastMotionY = y; mIsUnableToDrag = true; return false; } if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; startDrag(); } else if (yDiff > mTouchSlop) { // The finger has moved enough in the vertical // direction to be counted as a drag... abort // any attempt to drag horizontally, to work correctly // with children that have scrolling containers. mIsUnableToDrag = true; } if (mIsBeingDragged) { // Scroll to follow the motion event performDrag(x); } break; } case MotionEvent.ACTION_DOWN: { /* * Remember location of down touch. ACTION_DOWN always refers to * pointer index 0. */ mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); mIsUnableToDrag = false; mIsBeingDragged = false; // If we clicked in the drag handle (and drag enabled), // intercept the event! if (isWithinDragHandle(mLastMotionX, mLastMotionY)) startDrag(); else if ((LeftSideMenu.this.isOpen() || LeftSideMenu.this.mIsAnimating) && !isWithinMenu(mLastMotionX, mLastMotionY)) { mClickedOutside = true; } break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } /* * The only time we want to intercept motion events is if we are in * the drag mode or if the menu is open and we clicked outside the * menu content. */ return mIsBeingDragged || mClickedOutside; } @Override public boolean onTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { // Don't handle edge touches immediately -- they may actually // belong to one of our // descendants. return false; } final int action = ev.getAction(); // Of this is a click outside the menu area, consume the event if (mClickedOutside) { if ((action & MotionEventCompat.ACTION_MASK) == MotionEvent.ACTION_UP) { mClickedOutside = false; LeftSideMenu.this.hide(); } return true; } switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { // Remember where the motion event started mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = MotionEventCompat.getPointerId(ev, 0); startDrag(); break; } case MotionEvent.ACTION_MOVE: if (!mIsBeingDragged) { final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, pointerIndex); final float xDiff = Math.abs(x - mLastMotionX); final float y = MotionEventCompat.getY(ev, pointerIndex); final float yDiff = Math.abs(y - mLastMotionY); if (xDiff > mTouchSlop && xDiff > yDiff) { mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; startDrag(); } } // Not else! Note that mIsBeingDragged can be set above. if (mIsBeingDragged) { // Scroll to follow the motion event final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); final float x = MotionEventCompat.getX(ev, activePointerIndex); performDrag(x); } break; case MotionEvent.ACTION_UP: if (mIsBeingDragged) { mActivePointerId = -1; endDrag(); } break; case MotionEvent.ACTION_CANCEL: if (mIsBeingDragged) { mActivePointerId = -1; endDrag(); } break; case MotionEventCompat.ACTION_POINTER_DOWN: { final int index = MotionEventCompat.getActionIndex(ev); final float x = MotionEventCompat.getX(ev, index); mLastMotionX = x; mActivePointerId = MotionEventCompat.getPointerId(ev, index); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId)); break; } return true; } private boolean performDrag(float x) { mLastMotionX = x; setShowPercentage(100.0f * ((x - this.mInitialMotionX + mTranslationAtStartOfDrag) / getMenuWidth())); return true; } } public void onConfigurationChanged() { if (mRoot != null) mRoot.updateMenuIcon(); } public void setDisplayMenuIndicator(boolean displayMenuIndicator) { mDisplayMenuIndicator = displayMenuIndicator; } }