package deadline.scalelayout; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.widget.FrameLayout; import java.util.ArrayList; /** * @author deadline * @time 2016/10/12. * */ public class ScaleLayout extends FrameLayout{ private static final String TAG = ScaleLayout.class.getSimpleName(); /** * 可以缩小到的最小比例 */ private static final float DEFAULT_MIN_SCALE = 0.7f; private static final int DEFAULT_DURATION = 1000; public static final int STATE_OPEN = 0; public static final int STATE_CLOSE = 1; private boolean mSuggestScaleEnable; /** * 设置是否启用上滑缩小功能 */ private boolean mSlideScaleEnable; /** * 现在有这么几种情况, 默认第二种 * 1. 只上滑放大下滑缩小 false * 2. 只上滑缩小下滑放大 true */ private boolean mSlideUpOrDownEnable; /** * topView位移的距离,默认是topView的高度 */ private int mTopViewMoveDistance; /** * bottomView位移的距离,默认是bottomView的高度 */ private int mBottomViewMoveDistance; /** * 默认状态关闭 */ private int mState = STATE_CLOSE; protected View mTopView, mBottomView, mCenterView; /** * 可滑动到的最小scale */ private float mMinScale = DEFAULT_MIN_SCALE; /** * 当前的缩放比例 */ private float mCurrentScale = 1f; /** * touchSlop */ private int mTouchSlop = 5; /** * 根据down up之间滑动的距离计算缩放比例 */ private float mSlopLength = 0; private float downY; private float mInitialMotionX, mInitialMotionY; /** * 用来ACTION_UP 之后处理变大(scale = 1f) * 或变小(scale = mMinScale)的动画 */ ValueAnimator animator; private OnGetCanScaleListener mCanScaleListener; /** * scale变化的监听器 */ private ArrayList<OnScaleChangedListener> mScaleListenerList; /** * 状态变化的监听器 */ private ArrayList<OnStateChangedListener> mStateListenerList; public ScaleLayout(Context context) { this(context, null); } public ScaleLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ScaleLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ScaleLayout, 0, 0); mState = a.getInteger(R.styleable.ScaleLayout_state, STATE_CLOSE); mSlideScaleEnable = a.getBoolean(R.styleable.ScaleLayout_slideScaleEnable, true); mSlideUpOrDownEnable = a.getBoolean(R.styleable.ScaleLayout_slideUpOrDownEnable, true); mSuggestScaleEnable = a.getBoolean(R.styleable.ScaleLayout_suggestScaleEnable, false); a.recycle(); setupScaleLayout(); } private void setupScaleLayout() { setWillNotDraw(false); mScaleListenerList = new ArrayList<>(); mStateListenerList = new ArrayList<>(); } /** * 设置最小scale * {@link #DEFAULT_MIN_SCALE} * @param minScale */ public void setMinScale(float minScale){ if(minScale > 0f && minScale < 1f){ if(mMinScale != minScale){ if(isOpen()){ if(animator != null){ animator.cancel(); animator = null; } animator = getAnimator(mMinScale, minScale); animator.start(); } mMinScale = minScale; } } } public float getMinScale(){ return mMinScale; } public float getCurrentScale(){ return mCurrentScale; } public void setSuggestScaleEnable(boolean enable){ if(mSuggestScaleEnable != enable){ mSuggestScaleEnable = enable; requestLayout(); } } /** * 设置的scale不得当的话,有可能topView / bottomView被覆盖 * 通过设置{@link #setSuggestScaleEnable(boolean)}启用 * @return */ private float getSuggestScale(){ int height = 0; if(mTopView != null){ height += mTopView.getMeasuredHeight(); } if(mBottomView != null){ height += mBottomView.getMeasuredHeight(); } return 1 - height * 1f / (getMeasuredHeight() - getPaddingTop() - getPaddingBottom()); } /** * 设置是否启用滑动缩小功能 * @param enable */ public void setSlideScaleEnable(boolean enable){ this.mSlideScaleEnable = enable; } /** * 现在有这么几种情况, 默认第二种, 两者都可以的话,感觉好奇怪, * 比如一直下滑会由大变小后又变大,操作感觉不是很好 * 1. 只上滑放大下滑缩小 false * 2. 只上滑缩小下滑放大 true */ public void setSlideUpOrDownEnable(boolean enable){ this.mSlideUpOrDownEnable = enable; } /** * add OnScaleChangedListener * @param listener */ public void addOnScaleChangedListener(OnScaleChangedListener listener){ if(listener != null){ mScaleListenerList.add(listener); } } /** * add OnStateChangedListener * @param listener */ public void addOnStateChangedListener(OnStateChangedListener listener){ if(listener != null){ mStateListenerList.add(listener); } } public void setOnGetCanScaleListener(OnGetCanScaleListener listener){ mCanScaleListener = listener; } /** * {@link #setState(int state, boolean animationEnable)} * @param state */ public void setState(int state){ setState(state, true); } /** * 设置状态变化 * @param state open or close * @param animationEnable change state with or without animation */ public void setState(final int state, boolean animationEnable) { if(!animationEnable) { if(state == STATE_CLOSE){ mSlopLength = 0; mCurrentScale = 1; }else{ if(mSlideUpOrDownEnable) { mSlopLength = -getMeasuredHeight() * (1 - mMinScale) * 1.25f; }else{ mSlopLength = getMeasuredHeight() * (1 - mMinScale) * 1.25f; } mCurrentScale = mMinScale; } doSetScale(); mState = state; }else{ if(animator != null){ animator.cancel(); animator = null; } if(state == STATE_CLOSE && mCurrentScale != 1){ mSlopLength = 0; animator = getAnimator(mCurrentScale, 1f); }else if(state == STATE_OPEN && mCurrentScale != mMinScale){ if(mSlideUpOrDownEnable) { mSlopLength = -getMeasuredHeight() * (1 - mMinScale) * 1.25f; }else{ mSlopLength = getMeasuredHeight() * (1 - mMinScale) * 1.25f; } animator = getAnimator(mCurrentScale, mMinScale); } if(animator != null) { animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mState = state; } }); animator.start(); } } } /** * 获取当前状态开启或者关闭 * @return */ public boolean isOpen(){ return mState == STATE_OPEN; } /** * @param from scale * @param to scale * @return */ private ValueAnimator getAnimator(float from, float to){ ValueAnimator animator = ValueAnimator.ofFloat(from, to); animator.setDuration((long)(DEFAULT_DURATION * Math.abs(to - from))); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float scale = (Float) animation.getAnimatedValue(); if(mCurrentScale != scale){ mCurrentScale = scale; doSetScale(); } } }); return animator; } /** * 1.触发监听事件 * 2.计算scale的pivotX, pivotY(因为topView 和bottomView 的高度可能不一样,所以不能固定设置在中心点) * 3.设置 mCenterView的scale * 4.设置topView and BottomView 的动画(渐变和位移) */ private void doSetScale() { int scaleListenerCount = mScaleListenerList.size(); OnScaleChangedListener mScaleChangedListener; for (int i = 0; i < scaleListenerCount; i++) { mScaleChangedListener = mScaleListenerList.get(i); if(mScaleChangedListener != null){ mScaleChangedListener.onScaleChanged(mCurrentScale); } } if(mCurrentScale == mMinScale || mCurrentScale == 1f){ int stateListenerCount = mStateListenerList.size(); OnStateChangedListener mStateChangedListener; for (int i = 0; i < stateListenerCount; i++) { mStateChangedListener = mStateListenerList.get(i); if(mStateChangedListener != null){ mStateChangedListener.onStateChanged(mCurrentScale == mMinScale); } } } doSetCenterView(mCurrentScale); doSetTopAndBottomView(mCurrentScale); } /** * 当scale发生变化时,centerView设置scale * @param scale */ public void doSetCenterView(float scale){ mCenterView.setPivotX(getCenterViewPivotX()); mCenterView.setPivotY(getCenterViewPivotY()); mCenterView.setScaleX(scale); mCenterView.setScaleY(scale); } public float getCenterViewPivotX(){ return (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) / 2f; } public float getCenterViewPivotY(){ float pivotY = 0; if(mTopView == null && mBottomView != null) { pivotY = 0; }else if(mBottomView == null && mTopView != null){ pivotY = getMeasuredHeight() - getPaddingBottom() - getPaddingTop(); }else if(mTopView == null && mBottomView == null){ pivotY = (getMeasuredHeight() - getPaddingTop() - getPaddingBottom()) / 2f; }else{ int totalDistance = mTopViewMoveDistance + mBottomViewMoveDistance; int temp = getMeasuredHeight() - getPaddingBottom() - getPaddingTop(); if(totalDistance != 0) { pivotY = temp * mTopViewMoveDistance / totalDistance; } } if(BuildConfig.DEBUG) { Log.d(TAG, "pivotY : " + pivotY); } return pivotY; } /** * 当scale发生变化时,topView bottomView设置渐变和位移 * @param scale */ public void doSetTopAndBottomView(float scale){ //这里把mMinScale(0.7f) ~ 1 区间的值映射到 0 ~ 1 float value = (scale - mMinScale) / (1 - mMinScale); float alpha = 1 - value; int top = 0; if(mTopView != null){ top = getPaddingTop() + (int)(mTopViewMoveDistance * value); mTopView.setAlpha(alpha); mTopView.setTop(top); mTopView.setBottom(top + mTopView.getMeasuredHeight()); } if(mBottomView != null){ top = getMeasuredHeight() - getPaddingBottom() -mBottomViewMoveDistance - (int)(mBottomViewMoveDistance * value); mBottomView.setAlpha(alpha); mBottomView.setTop(top); mBottomView.setBottom(top + mBottomView.getMeasuredHeight()); } } /** * xml解析完的回调,检测如果子view的数量少于1个抛出异常 * 获取并设置top center bottom view */ @Override protected void onFinishInflate() { super.onFinishInflate(); int childCount = getChildCount(); if(childCount < 1){ throw new IllegalStateException("ScaleLayout should have one direct child at least !"); } mTopView = findViewById(R.id.scaleLayout_top); mBottomView = findViewById(R.id.scaleLayout_bottom); mCenterView = findViewById(R.id.scaleLayout_center); // if centerView does not exist // it make no sense if(mCenterView == null){ throw new IllegalStateException("ScaleLayout should have one direct child at least !"); } LayoutParams lp = (FrameLayout.LayoutParams)mCenterView.getLayoutParams(); lp.gravity &= Gravity.CENTER; mCenterView.setLayoutParams(lp); //hide topView and bottomView //set the topView on the top of ScaleLayout if(mTopView != null){ lp = (FrameLayout.LayoutParams)mTopView.getLayoutParams(); lp.gravity &= Gravity.TOP; mTopView.setLayoutParams(lp); mTopView.setAlpha(0); } //set the bottomView on the bottom of ScaleLayout if(mBottomView != null){ lp = (FrameLayout.LayoutParams)mBottomView.getLayoutParams(); lp.gravity &= Gravity.BOTTOM; mBottomView.setLayoutParams(lp); mBottomView.setAlpha(0); } setState(mState, false); } /** * 使得centerView 大小等同ScaleLayout的大小 * 如果不想这样处理,也可以在触摸事件中使用TouchDelegate * @param widthMeasureSpec * @param heightMeasureSpec */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int widthSize = MeasureSpec.getSize(widthMeasureSpec); final int heightSize = MeasureSpec.getSize(heightMeasureSpec); int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); int layoutWidth = widthSize - getPaddingLeft() - getPaddingRight(); int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(layoutWidth, MeasureSpec.EXACTLY); int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.EXACTLY); mCenterView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if(mBottomView != null){ mBottomViewMoveDistance = mBottomView.getMeasuredHeight(); } if(mTopView != null){ mTopViewMoveDistance = mTopView.getMeasuredHeight(); } if(mSuggestScaleEnable){ setMinScale(getSuggestScale()); } } /** * 所有的down事件都不拦截,因此接下来的move, up事件, * 都会先执行onInterceptTouchEvent的(move, up) * 继而分发给子view的dispatchTouchEvent(move, up), * 然后在onInterceptTouchEvent(move)事件中判断是否满足滑动条件 * 满足就拦截,拦截了之后move up事件就会都分发给自身的OnTouchEvent, * 否则如上继续分发给子View * @param ev * @return */ @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false; switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: onTouchEvent(ev); mInitialMotionX = ev.getX(); mInitialMotionY = ev.getY(); break; case MotionEvent.ACTION_MOVE: final float deltaX = Math.abs(ev.getX() - mInitialMotionX); final float deltaY = Math.abs(ev.getY() - mInitialMotionY); if(mCanScaleListener != null && !mCanScaleListener.onGetCanScale(ev.getX() - mInitialMotionX > 0)){ intercept = false; }else { intercept = deltaY > deltaX && deltaY > mTouchSlop; } break; } return intercept; } /** * 该方法中实现了 * 上滑缩小下滑放大功能 * 也可设置为 上滑放大下滑缩小 * @param ev * @return */ @Override public boolean onTouchEvent(MotionEvent ev) { if (!isEnabled() || !mSlideScaleEnable) { return super.onTouchEvent(ev); } switch (ev.getActionMasked()) { case MotionEvent.ACTION_DOWN: downY = ev.getY(); return true; case MotionEvent.ACTION_MOVE: if(mCanScaleListener != null && !mCanScaleListener.onGetCanScale(ev.getY() - downY > 0)){ return super.onTouchEvent(ev); } if (Math.abs(ev.getY() - downY) > mTouchSlop) { mSlopLength += (ev.getY() - downY); float scale; if (mSlideUpOrDownEnable) { scale = 1 + (0.8f * mSlopLength / getMeasuredHeight()); } else { scale = 1 - (0.8f * mSlopLength / getMeasuredHeight()); } scale = Math.min(scale, 1f); mCurrentScale = Math.max(mMinScale, scale); doSetScale(); downY = ev.getY(); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (mCurrentScale > mMinScale && mCurrentScale < 1f) { float half = (1 - mMinScale) / 2; if (mCurrentScale >= mMinScale + half) { setState(STATE_CLOSE, true); } else { setState(STATE_OPEN, true); } } break; } return super.onTouchEvent(ev); } /** * 存储当前状态 * @return */ @Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable("superState", super.onSaveInstanceState()); bundle.putSerializable(TAG, mState); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if(state instanceof Bundle) { Bundle bundle = (Bundle) state; mState = (int) bundle.getSerializable(TAG); state = bundle.getParcelable("superState"); setState(mState, true); } super.onRestoreInstanceState(state); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if(animator != null){ animator.cancel(); animator = null; } } /** * 当centerView 的scale变化的时候,通过这个 * 接口外部的View可以做一些同步的事情, * 比如,你有一个其他的view要根据centerView的变化而变化 */ public interface OnScaleChangedListener{ void onScaleChanged(float currentScale); } /** * state == false 当完全关闭(scale == 1f) * state == true 或当完全开启的时候(scale = mMinScale) */ public interface OnStateChangedListener{ void onStateChanged(boolean state); } /** * 返回是否可以scale,主要为了适配部分有滑动冲突的view * 如TouchImageView, 甚至webView等 * isScrollSown = true 代表向下, * isScrollSown = false 代表向上 */ public interface OnGetCanScaleListener{ boolean onGetCanScale(boolean isScrollSown); } }