package info.guardianproject.securereaderinterface.views; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Region.Op; import android.util.Log; import android.view.GestureDetector; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.Animation.AnimationListener; import android.view.animation.Transformation; import android.widget.FrameLayout; import info.guardianproject.securereaderinterface.App; import info.guardianproject.securereaderinterface.uiutil.AnimationHelpers; import info.guardianproject.securereaderinterface.R; public class ExpandingFrameLayout extends FrameLayout { public static final int DEFAULT_EXPAND_DURATION = 500; public static final int DEFAULT_COLLAPSE_DURATION = 500; public interface ExpansionListener { void onExpanded(); void onCollapsed(); } public interface SwipeListener { void onSwipeUp(); void onSwipeDown(); } View mContentView; int mCollapsedClip; int mCollapsedTop; int mCollapsedHeight; int mCurrentClip = 0; int mCurrentTop = 0; int mCurrentHeight = 0; private boolean mHasExpanded; private ExpansionListener mExpansionListener; // Swipe listener // private GestureDetector mGestureDetector; private SwipeListener mSwipeListener; // private boolean isTakingSnap; private boolean mUseBitmap = false; private Bitmap mBitmap; public ExpandingFrameLayout(Context context, View content) { super(context); mContentView = content; FrameLayout.LayoutParams lays = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT, Gravity.LEFT | Gravity.TOP); lays.setMargins(0, 0, 0, 0); mContentView.setLayoutParams(lays); setBackgroundColor(0xffffffff); addView(mContentView); mSwipeListener = null; } private final Runnable mExpandRunnable = new Runnable() { @Override public void run() { expand(); } }; private final Runnable mExpandNowRunnable = new Runnable() { @Override public void run() { expand(0); } }; @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { if (!mHasExpanded) { mHasExpanded = true; post(mExpandRunnable); } else { post(mExpandNowRunnable); } } } @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); this.post(new Runnable() { @Override public void run() { // On orientation change we stop the clipping and go full screen mCurrentTop = 0; mCurrentHeight = getHeight(); invalidate(); } }); } @Override public void draw(Canvas canvas) { if (isTakingSnap) return; int bottomMargin = getHeight() - mCurrentTop - mCurrentHeight; if (bottomMargin != 0 || mCurrentTop != 0) { if (mBitmap != null && mUseBitmap) { Rect rectSource; Rect rectDest; if ((mCurrentTop + mCurrentClip) > 0) { // Top part int visibleHeight = Math.max(0, mCurrentTop + mCurrentClip); rectSource = new Rect(0, Math.max(0, Math.max(0, mCollapsedTop + mCollapsedClip) - visibleHeight), getWidth(), Math.max(0, mCollapsedTop + mCollapsedClip)); rectDest = new Rect(0, 0, getWidth(), visibleHeight); canvas.drawBitmap(mBitmap, rectSource, rectDest, null); } if (bottomMargin > 0) { // Bottom part rectSource = new Rect(0, mCollapsedTop + mCollapsedHeight, getWidth(), mCollapsedTop + mCollapsedHeight + bottomMargin); rectDest = new Rect(0, getHeight() - bottomMargin, getWidth(), getHeight()); canvas.drawBitmap(mBitmap, rectSource, rectDest, null); } } canvas.translate(0, mCurrentTop); canvas.clipRect(new Rect(0, mCurrentClip, getWidth(), mCurrentHeight), Op.REPLACE); } super.draw(canvas); } /** * Set the starting (collapsed) size of the view. * * @param clip * Top clip margin of the view (if it is hidden by other * components). * @param top * Top coordinate of view (in parent coordinates). * @param height * Height of view. */ public void setCollapsedSize(int clip, int top, int height) { mCollapsedClip = clip; mCollapsedTop = top; mCollapsedHeight = height; } public void setExpansionListener(ExpansionListener expansionListener) { mExpansionListener = expansionListener; } private void setSize(int clip, int top, int height) { mCurrentClip = clip; mCurrentTop = top; mCurrentHeight = height; invalidate(); } private void takeSnapshot() { View parent = (View) getParent(); if (parent != null) { ViewGroup.MarginLayoutParams params = (MarginLayoutParams) this.getLayoutParams(); isTakingSnap = true; parent.setDrawingCacheEnabled(true); Bitmap bmp = parent.getDrawingCache(); isTakingSnap = false; try { mBitmap = Bitmap.createBitmap(bmp, params.leftMargin, params.topMargin, bmp.getWidth() - params.rightMargin - params.leftMargin, bmp.getHeight() - params.bottomMargin - params.topMargin).copy(bmp.getConfig(), false); } catch(Exception e) { Log.e("ExpandingFrameLayout", e.toString()); } parent.setDrawingCacheEnabled(false); } } /** * Expand the view from the collapsed size (need to call * {@link #setCollapsedSize(int, int, int)} first) using the default * duration. */ public void expand() { expand(DEFAULT_EXPAND_DURATION); } /** * Expand the view from the collapsed size (need to call * {@link #setCollapsedSize(int, int, int)} first) using the given duration. * * @param duration * Duration in milliseconds. */ public void expand(int duration) { takeSnapshot(); mUseBitmap = true; ViewGroup.MarginLayoutParams params = (MarginLayoutParams) this.getLayoutParams(); int toHeight = ((View) getParent()).getHeight() - params.topMargin - params.bottomMargin; final ExpandAnim anim = new ExpandAnim(mCollapsedClip, 0, mCollapsedTop, 0, mCollapsedHeight, toHeight); anim.setDuration(duration); anim.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { mUseBitmap = false; mBitmap = null; if (mExpansionListener != null) mExpansionListener.onExpanded(); // Show hint screen? if (!App.getSettings().hasShownSwipeUpHint()) { postDelayed(new Runnable() { @Override public void run() { showSwipeHint(); App.getSettings().setHasShownSwipeUpHint(true); } }, 3000); } } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationStart(Animation animation) { } }); this.startAnimation(anim); } /** * Collapse the view to the collapsed size (need to call * {@link #setCollapsedSize(int, int, int)} first) using the default * duration. */ public void collapse() { collapse(DEFAULT_COLLAPSE_DURATION); } /** * Collapse the view to the collapsed size (need to call * {@link #setCollapsedSize(int, int, int)} first) using the given duration. * * @param duration * Duration in milliseconds. */ public void collapse(int duration) { ViewGroup.MarginLayoutParams params = (MarginLayoutParams) this.getLayoutParams(); int toHeight = ((View) getParent()).getHeight() - params.topMargin - params.bottomMargin; // TODO - remove old snapshot and take new one here to save memory? takeSnapshot(); mUseBitmap = true; final ExpandAnim anim = new ExpandAnim(0, mCollapsedClip, 0, mCollapsedTop, toHeight, mCollapsedHeight); anim.setDuration(duration); anim.setAnimationListener(new AnimationListener() { @Override public void onAnimationEnd(Animation animation) { mUseBitmap = false; // mBitmap = null; if (mExpansionListener != null) mExpansionListener.onCollapsed(); } @Override public void onAnimationRepeat(Animation animation) { } @Override public void onAnimationStart(Animation animation) { } }); this.startAnimation(anim); } public class ExpandAnim extends Animation { int fromClip; int toClip; int fromTop; int toTop; int fromHeight; int toHeight; public ExpandAnim(int fromClip, int toClip, int fromTop, int toTop, int fromHeight, int toHeight) { this.fromClip = fromClip; this.toClip = toClip; this.fromTop = fromTop; this.toTop = toTop; this.fromHeight = fromHeight; this.toHeight = toHeight; } @Override protected void applyTransformation(float interpolatedTime, Transformation t) { int newClip; int newTop; int newHeight; newClip = (int) (fromClip + ((toClip - fromClip) * interpolatedTime)); newTop = (int) (fromTop + ((toTop - fromTop) * interpolatedTime)); newHeight = (int) (fromHeight + ((toHeight - fromHeight) * interpolatedTime)); setSize(newClip, newTop, newHeight); } @Override public void initialize(int width, int height, int parentWidth, int parentHeight) { super.initialize(width, height, parentWidth, parentHeight); } @Override public boolean willChangeBounds() { return true; } } /* * Add a listener for swipe events (up and down) */ public void setSwipeListener(SwipeListener listener) { mSwipeListener = listener; if (mSwipeListener != null) createGestureDetector(); else mGestureDetector = null; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { // If the event is within our clipped area we should not react to it! // int[] location = new int[2]; this.getLocationOnScreen(location); if ((location[1] + mCurrentTop) > ev.getY()) return false; return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (mGestureDetector != null) { if (mGestureDetector.onTouchEvent(ev)) return true; } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { if (mGestureDetector != null) { if (mGestureDetector.onTouchEvent(event)) return true; } return super.onTouchEvent(event); } public void createGestureDetector() { if (mGestureDetector == null) { mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { private static final int SWIPE_MIN_DISTANCE = 40; private static final int SWIPE_MAX_OFF_PATH = 20; @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { try { if (Math.abs(e1.getX() - e2.getX()) > SWIPE_MAX_OFF_PATH) return false; // bottom to up swipe if (e1.getY() - e2.getY() > SWIPE_MIN_DISTANCE) { if (mSwipeListener != null) mSwipeListener.onSwipeUp(); return true; } else if (e2.getY() - e1.getY() > SWIPE_MIN_DISTANCE) { if (mSwipeListener != null) mSwipeListener.onSwipeDown(); return true; } } catch (Exception e) { // nothing } return false; } }); } } private final Runnable hideActionBarRunnable = new Runnable() { @Override public void run() { hideActionBar(); } }; public void showActionBar(int actionBarHeight) { removeCallbacks(hideActionBarRunnable); final ExpandAnim anim = new ExpandAnim(0, 0, mCurrentTop, actionBarHeight, getHeight(), getHeight()); anim.setDuration(300); this.startAnimation(anim); this.postDelayed(hideActionBarRunnable, 5000); } public void hideActionBar() { final ExpandAnim anim = new ExpandAnim(0, 0, mCurrentTop, 0, getHeight(), getHeight()); anim.setDuration(300); this.startAnimation(anim); } private void showSwipeHint() { LayoutInflater inflater = LayoutInflater.from(getContext()); final View view = inflater.inflate(R.layout.story_item_swipe_hint, this, false); AnimationHelpers.fadeOut(view, 0, 0, false); addView(view); AnimationHelpers.fadeIn(view, 500, 3000, true); } }