package com.dodola.flip; import android.animation.Animator; import android.animation.Animator.AnimatorListener; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.content.Context; import android.os.Build; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.view.ScaleGestureDetectorCompat; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector.OnScaleGestureListener; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.AccelerateDecelerateInterpolator; import android.view.animation.Interpolator; import android.widget.FrameLayout; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; public class FoldLayout extends FrameLayout implements OnScaleGestureListener { public static final String FRAGMENT_DETAIL_VIEW_TAG = "detail"; private boolean animatingToMain; List<WeakReference<ValueAnimator>> animatorWeakHashMap; private boolean closeDown; @Nullable private DetailAnimViewGroup detail; @Nullable private View detailWrapper; private float downY; private int foldCenter; FragmentManager fragmentManager; private float initialPercentY; private Interpolator interpolatorFling; private Interpolator interpolatorTap; private boolean isTouchReservedForPinch; private float latestScaleDistance; private long latestScaleDuration; @Nullable DetailAnimViewGroup main; @Nullable private View mainWrapper; private ScaleGestureDetector pinchDetector; private boolean startedSwipeToClose; VelocityTracker velocityTracker; private ViewConfiguration viewConfig; public static final float DEFAULT_BACKOFF_MULT = 1.0f; public FoldLayout(Context context) { super(context); this.animatorWeakHashMap = new ArrayList(); this.foldCenter = -1; } public FoldLayout(Context context, AttributeSet attrs) { super(context, attrs); this.animatorWeakHashMap = new ArrayList(); this.foldCenter = -1; } public FoldLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); this.animatorWeakHashMap = new ArrayList(); this.foldCenter = -1; } @TargetApi(Build.VERSION_CODES.KITKAT) protected void onAttachedToWindow() { super.onAttachedToWindow(); this.velocityTracker = VelocityTracker.obtain(); this.interpolatorFling = new FoldInterpolator(); this.interpolatorTap = new AccelerateDecelerateInterpolator(); this.pinchDetector = new ScaleGestureDetector(getContext(), this); ScaleGestureDetectorCompat.setQuickScaleEnabled(pinchDetector, false); this.viewConfig = ViewConfiguration.get(getContext()); } protected void onDetachedFromWindow() { super.onDetachedFromWindow(); this.velocityTracker.recycle(); } public void setFragmentManager(FragmentManager fragmentManager) { this.fragmentManager = fragmentManager; } public void addView(View child) { super.addView(child); ViewGroup fragmentFrame = (ViewGroup) child; if (this.main == null) { if (child instanceof DetailAnimViewGroup) { this.main = (DetailAnimViewGroup) child; } else { this.main = (DetailAnimViewGroup) fragmentFrame.getChildAt(0); } this.main.setInitialFoldCenter(this.foldCenter); this.mainWrapper = child; if (this.detailWrapper != null) { this.detailWrapper.bringToFront(); this.main.setTopFoldFactor(DEFAULT_BACKOFF_MULT); this.main.setBottomFoldFactor(DEFAULT_BACKOFF_MULT); return; } return; } if (this.detailWrapper != null) { removeView(this.detailWrapper); } if (child instanceof DetailAnimViewGroup) { this.detail = (DetailAnimViewGroup) child; } else { this.detail = (DetailAnimViewGroup) fragmentFrame.getChildAt(0); } this.detail.setTopFoldFactor(DEFAULT_BACKOFF_MULT); this.detail.setBottomFoldFactor(DEFAULT_BACKOFF_MULT); this.detail.setInitialFoldCenter(this.foldCenter); this.detailWrapper = child; this.main.setInitialFoldCenter(this.foldCenter); animateToDetail(DEFAULT_BACKOFF_MULT, 0.0f, DetailAnimViewGroup.ANIMATION_DURATION, this.interpolatorTap); } public void setFoldCenter(int foldCenter) { this.foldCenter = foldCenter; } public void removeView(View view) { super.removeView(view); if (view == this.detailWrapper) { this.detail = null; this.detailWrapper = null; } else if (view == this.mainWrapper) { this.main = null; this.mainWrapper = null; } else { } } public boolean onInterceptTouchEvent(MotionEvent ev) { boolean handled = false; if (this.detail != null) { this.pinchDetector.onTouchEvent(ev); if (this.pinchDetector.isInProgress()) { handled = true; } else { handled = this.isTouchReservedForPinch || handleTouch(ev); } } if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) { this.isTouchReservedForPinch = false; } return handled; } public boolean onTouchEvent(MotionEvent ev) { boolean handled; if (this.startedSwipeToClose) { handled = handleTouch(ev); } else { handled = this.pinchDetector.onTouchEvent(ev); } if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) { this.isTouchReservedForPinch = false; } return handled; } private boolean handleTouch(MotionEvent ev) { if (this.detail == null) { return false; } if (ev.getAction() == MotionEvent.ACTION_DOWN) { this.downY = ev.getY(); } if (!this.startedSwipeToClose) { boolean scrolledTouchSlop = Math.abs(ev.getY() - this.downY) > ((float) this.viewConfig.getScaledTouchSlop()); boolean canScroll = canScroll(this.detail, false, (int) (ev.getY() - this.downY), (int) ev.getX(), (int) ev.getY()); boolean currentlyFolding = this.detail.hasFold(); if (currentlyFolding || (scrolledTouchSlop && !canScroll)) { this.velocityTracker.clear(); this.velocityTracker.addMovement(ev); if (currentlyFolding) { this.closeDown = ev.getY() < ((float) this.foldCenter); } else { this.closeDown = this.downY < ev.getY(); } this.downY = ev.getY(); this.startedSwipeToClose = true; this.initialPercentY = this.detail.getTopFoldFactor(); clearAnimations(); requestDisallowInterceptTouchEvent(true); } } if (!this.startedSwipeToClose) { return false; } if (ev.getAction() == MotionEvent.ACTION_MOVE) { this.velocityTracker.addMovement(ev); float totalDistanceY = ev.getY() - this.downY; if (!this.closeDown) { totalDistanceY = -totalDistanceY; } float percentY = ((totalDistanceY / ((float) getHeight())) * 2.0f) + this.initialPercentY; this.detail.setTopFoldFactor(percentY); this.detail.setBottomFoldFactor(percentY); this.main.setTopFoldFactor(DEFAULT_BACKOFF_MULT - percentY); this.main.setBottomFoldFactor(DEFAULT_BACKOFF_MULT - percentY); } if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) { this.velocityTracker.computeCurrentVelocity(1); this.startedSwipeToClose = false; handleFling(this.velocityTracker.getYVelocity()); requestDisallowInterceptTouchEvent(false); } return true; } private void handleFling(float speed) { float absoluteSpeed = Math.abs(speed); if (((double) absoluteSpeed) > 0.5d) { float startFoldFactor = this.main.getTopFoldFactor() - ((210.0f * speed) / ((float) getHeight())); boolean animateToMain = (this.closeDown && speed > 0.0f) || (!this.closeDown && speed < 0.0f); if (animateToMain) { animateToMain(DEFAULT_BACKOFF_MULT - startFoldFactor, startFoldFactor, (int) Math.max(135.0f, (((float) ((int) Math.max(0.0f, (((float) getHeight()) * startFoldFactor) / 2.0f))) / absoluteSpeed) * FoldInterpolator.initialSpeedFactor), this.interpolatorFling); } else { animateToDetail(DEFAULT_BACKOFF_MULT - startFoldFactor, startFoldFactor, (int) ViewUtil.clamp((float) ((int) ((((float) ((int) Math.max(0.0f, (this.detail.getTopFoldFactor() * ((float) getHeight())) / 2.0f))) / absoluteSpeed) * FoldInterpolator.initialSpeedFactor)), 135.0f, 810.0f), this.interpolatorFling); } } else if (((double) this.detail.getTopFoldFactor()) < 0.5d) { animateToDetail(this.detail.getTopFoldFactor(), this.main.getTopFoldFactor(), 270, this.interpolatorTap); } else { animateToMain(this.detail.getTopFoldFactor(), this.main.getTopFoldFactor(), 270, this.interpolatorTap); } } protected boolean canScroll(View v, boolean checkV, int dy, int x, int y) { if (v instanceof ViewGroup) { ViewGroup group = (ViewGroup) v; int scrollX = v.getScrollX(); int scrollY = v.getScrollY(); for (int i = group.getChildCount() - 1; i >= 0; i--) { View child = group.getChildAt(i); if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop() && y + scrollY < child.getBottom()) { if (canScroll(child, true, dy, (x + scrollX) - child.getLeft(), (y + scrollY) - child.getTop())) { return true; } } } } return checkV && ViewCompat.canScrollVertically(v, -dy); } public void animateToDetail(float detailStart, float mainStart, int duration, Interpolator interpolator) { this.animatingToMain = false; ObjectAnimator topAngleAnimator = ObjectAnimator.ofFloat(this.detail, "topFoldFactor", new float[]{detailStart, 0.0f}).setDuration((long) duration); topAngleAnimator.setInterpolator(interpolator); this.animatorWeakHashMap.add(new WeakReference(topAngleAnimator)); topAngleAnimator.start(); ObjectAnimator bottomAngleAnimator = ObjectAnimator.ofFloat(this.detail, "bottomFoldFactor", new float[]{detailStart, 0.0f}).setDuration((long) duration); bottomAngleAnimator.setInterpolator(interpolator); this.animatorWeakHashMap.add(new WeakReference(bottomAngleAnimator)); bottomAngleAnimator.start(); ObjectAnimator topAngleMain = ObjectAnimator.ofFloat(this.main, "topFoldFactor", new float[]{mainStart, DEFAULT_BACKOFF_MULT}).setDuration((long) duration); topAngleMain.setInterpolator(interpolator); this.animatorWeakHashMap.add(new WeakReference(topAngleMain)); topAngleMain.start(); ObjectAnimator bottomAngleMain = ObjectAnimator.ofFloat(this.main, "bottomFoldFactor", new float[]{mainStart, DEFAULT_BACKOFF_MULT}).setDuration((long) duration); bottomAngleMain.setInterpolator(interpolator); this.animatorWeakHashMap.add(new WeakReference(bottomAngleMain)); bottomAngleMain.start(); } public void animateToMain(float detailStart, float mainStart, int duration, Interpolator interpolator) { this.animatingToMain = true; ObjectAnimator topAngleDetail = ObjectAnimator.ofFloat(this.detail, "topFoldFactor", new float[]{detailStart, DEFAULT_BACKOFF_MULT}).setDuration((long) duration); topAngleDetail.setInterpolator(interpolator); this.animatorWeakHashMap.add(new WeakReference(topAngleDetail)); topAngleDetail.start(); ObjectAnimator bottomAngleDetail = ObjectAnimator.ofFloat(this.detail, "bottomFoldFactor", new float[]{detailStart, DEFAULT_BACKOFF_MULT}).setDuration((long) duration); bottomAngleDetail.setInterpolator(interpolator); this.animatorWeakHashMap.add(new WeakReference(bottomAngleDetail)); bottomAngleDetail.start(); ObjectAnimator topAngleMain = ObjectAnimator.ofFloat(this.main, "topFoldFactor", new float[]{mainStart, 0.0f}).setDuration((long) duration); topAngleMain.setInterpolator(interpolator); this.animatorWeakHashMap.add(new WeakReference(topAngleMain)); topAngleMain.start(); ValueAnimator anim = ObjectAnimator.ofFloat(this.main, "bottomFoldFactor", new float[]{mainStart, 0.0f}).setDuration((long) duration); anim.setInterpolator(interpolator); anim.addListener(new AnimatorListener() { public boolean cancelled; public void onAnimationStart(Animator animation) { } public void onAnimationEnd(Animator animation) { if (!this.cancelled) { final Fragment detailFragment = FoldLayout.this.fragmentManager.findFragmentByTag(FRAGMENT_DETAIL_VIEW_TAG); if (detailFragment != null) { FoldLayout.this.post(new Runnable() { public void run() { FoldLayout.this.fragmentManager.beginTransaction().remove(detailFragment).commitAllowingStateLoss(); } }); } FoldLayout.this.animatingToMain = false; } } public void onAnimationCancel(Animator animation) { this.cancelled = true; } public void onAnimationRepeat(Animator animation) { } }); this.animatorWeakHashMap.add(new WeakReference(anim)); anim.start(); } void clearAnimations() { for (WeakReference<ValueAnimator> reference : this.animatorWeakHashMap) { ValueAnimator anim = reference.get(); if (anim != null) { anim.cancel(); } } this.animatorWeakHashMap.clear(); } public boolean onBackPressed() { if (this.detail == null || this.animatingToMain) { return false; } clearAnimations(); animateToMain(this.detail.getTopFoldFactor(), this.main.getTopFoldFactor(), DetailAnimViewGroup.ANIMATION_DURATION, this.interpolatorTap); return true; } public boolean onScale(ScaleGestureDetector detector) { if (this.detail == null) { return false; } this.latestScaleDistance = detector.getScaleFactor() - DEFAULT_BACKOFF_MULT; this.latestScaleDuration = detector.getTimeDelta(); float fold = this.main.getTopFoldFactor() + this.latestScaleDistance; this.main.setTopFoldFactor(fold); this.main.setBottomFoldFactor(fold); this.detail.setTopFoldFactor(DEFAULT_BACKOFF_MULT - fold); this.detail.setBottomFoldFactor(DEFAULT_BACKOFF_MULT - fold); return true; } public boolean onScaleBegin(ScaleGestureDetector detector) { if (this.detail == null) { return false; } clearAnimations(); this.startedSwipeToClose = false; this.initialPercentY = this.main.getTopFoldFactor(); this.isTouchReservedForPinch = true; requestDisallowInterceptTouchEvent(true); return true; } public void onScaleEnd(ScaleGestureDetector detector) { if (this.detail != null) { float pixelSpeed = ((-this.latestScaleDistance) * ((float) getHeight())) / ((float) this.latestScaleDuration); this.closeDown = true; handleFling(pixelSpeed); requestDisallowInterceptTouchEvent(false); } } protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (this.foldCenter < 0) { this.foldCenter = ((bottom - top) / 2) + 1; if (this.main != null) { this.main.setInitialFoldCenter(this.foldCenter); } if (this.detail != null) { this.detail.setInitialFoldCenter(this.foldCenter); } } } class FoldInterpolator implements Interpolator { public static final float initialSpeedFactor = 1.5f; public float getInterpolation(float input) { return (float) Math.sin((((double) input) * Math.PI) / 2.0d); } } }