之前写的两篇关于自定义View:
http://www.jianshu.com/p/32d7d1ab985c 模仿饿了么加载效果(五八同城,UC也都有这个效果)
http://www.jianshu.com/p/e180aa9f293b 模仿小米的进度控件
先来看看效果图,这个gif弄得蛋疼,加快了播放速度,降低了清晰度:
scale_gif1.gif
GIF_20161019_230600.gif
github地址:https://github.com/niniloveyou/ScaleLayout
下面会从以下几个方面分析如何实现这个效果:
1.初始化完成后做了什么
2.onMeasure onLayout
3.触摸事件的处理
4.对外提供方法和接口
.
.
.
.
首先讲讲大概的思路:
就是我们要有三个View 分别为TopView CenterView bottomView 这很好理解,故名思义就是把这三个子View分别放在ViewGroup的上中下。
OnMeasure()中把CenterView的大小设置为等同于自身的大小
onLayout() 获取topview bottomView的高度,根据高度设置当centerView缩小时topView/BottomView位移距离
onInterceptTouchEvent() 只处理滑动冲突部分。
onTouchEvent()中才是真正滑动缩小或放大实现的部分。
1.初始化完成后做了什么
我们先贴代码,后面紧跟着解释:
@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); }
大家都知道onFinishInflate方法是View在XML中解析完成的回调,因此可以在里面做一些检查以及初始化的工作。 从代码不难看出,我首先就是检查了ScaleLayout的子View数量, 少于一个就直接抛出异常了,因为如果没有一个子View, 咱们自定义的这个ScaleLayout就没什么意义了, 其次 是我指定了 上中下三个子View的id, 这么做是因为ScaleLayout是个ViewGroup,可能不止三个,但是多了我们又没法判断,哪一个是topView, 哪个是centerView, 有可能会乱掉。
后面又对CenterView做了判空, 以及对三个View的位置做了些设置。
2.onMeasure onLayout
/** * 使得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()); } }
很简单,只说一点:mBottomViewMoveDistance, mTopViewMoveDistance 分别为bottomView, topView动画时位移的距离。
3.触摸事件的处理
重点来了这个也是核心部分了。
onInterceptTouchEvent
@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;
所有的down事件都不拦截,因此接下来的move, up事件,
都会先执行onInterceptTouchEvent的(move, up)
继而分发给子view的dispatchTouchEvent(move, up),
因此在onInterceptTouchEvent(move)事件中我们可以判断是否满足滑动条件,满足就拦截,拦截了之后move up事件就会都分发给自身的OnTouchEvent, 否则如上继续分发给子View.
intercept = deltaY > deltaX && deltaY > mTouchSlop;
即Y位移的距离大于X方向 ,并且Y方向位移的距离大于TouchSlop,则认为这是有效滑动。
/** * 返回是否可以scale,主要为了适配部分有滑动冲突的view * 如TouchImageView, 甚至webView等 * isScrollSown = true 代表向下, * isScrollSown = false 代表向上 */ public interface OnGetCanScaleListener{ boolean onGetCanScale(boolean isScrollSown); }
if(mCanScaleListener != null && !mCanScaleListener.onGetCanScale(ev.getX() - mInitialMotionX > 0)){ intercept = false; }
这下明白了吧,我是做了个接口,要不要拦截由你说了算,也算我偷懒了。
OnTouchEvent
/** * 该方法中实现了 * 上滑缩小下滑放大功能 * 也可设置为 上滑放大下滑缩小 * @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); }
这部分,首先是move的时候用mSlopLength计算滑动的距离向下滑就加正值,向上划值就减小,不断根据这个值计算当前的Scale. 应该缩放的比例,然后根据这个值计算topView bottomView 的透明度,位移距离,等等, 当UP的时候,根据当前的Scale决定是应该放大到原宽高还是缩小,以动画的形式。
···
/**
* 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); }
我把监听事件也贴上:
/** * 当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); }
4.对外提供方法和接口
关于接口,代码我都无耻的贴上去了。
下面说说提供的几个简单的外部方法:
/** * 设置最小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; }
代码贴完了。
如果感觉还行,到我的github star一下吧。 谢谢!
https://github.com/niniloveyou/ScaleLayout