重写ViewGroup并借助ViewDragHelper实现各种拖拽交互效果(二)
2018-01-30 21:44
489 查看
本文是《重写ViewGroup并借助ViewDragHelper实现各种拖拽交互效果(一)》http://blog.csdn.net/lin_dianwei/article/details/79166466 的延续,针对(一)中存在的问题继续优化。
一、前面SlideUpLayout控件在使用时,如果包含有ListView或RecyclerView等列表的时候,可能存在这样的问题,直接看图:
其中xml文件如下:
二、原因经分析是事件分派拦截存在着问题,还记得上一篇文章中,事件的拦截是完全交给ViewDragHelper处理的,如下:
三、此处穿插科普下事件分派拦截的原理:(此处是Copy过来的,放这里帮助理解)
dispatchTouchEvent
用于touch事件的分发;通俗点说,就是决定当前这个touch事件应该交给谁来处理(是当前View还是父View)
当触摸事件发生时 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法会从根元素依次往下传递直到最内层子元素或在中间某一元素中事件被拦截或者消费.
如果 return true,事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递;这样该View的onTouchEvent事件也不会得到响应. 如果return false,会将事件返回给父 View 的 onTouchEvent 进行消费。 如果 return super.dispatchTouchEvent(ev),事件会分发给当前 View 的 onInterceptTouchEvent 方法去进行处理。
onInterceptTouchEvent
用于touch事件的拦截;通俗点说,就是决定刚刚 dispatchTouchEvent 抛给我的touch事件应该交给谁来处理(是当前View还是子View)注意跟dispatchTouchEvent的区别
如果 return true,则将事件进行拦截,并将拦截到的事件交由该 View 的 onTouchEvent 进行处理; 如果 return false,则将事件向子View传递,再由子View的 dispatchTouchEvent来对这个事件处理; 如果 return super.onInterceptTouchEvent(ev),事件会被拦截,并将事件交由该 View 的 onTouchEvent 进行处理。
onTouchEvent
用于touch事件的处理;通俗点说,就是决定刚刚 onInterceptTouchEvent 抛给我的touch事件进行处理。
如果return false,那么这个事件会从该 View 向父View传递,父 View 的 onTouchEvent 来接收,而且如果父View也是return false,那事件也会向上传递由onTouchEvent接收处理. 如果retrun true, 则会接收并消费该事件。 如果retrun super.onTouchEvent(ev) 和返回 false 时相同。
四、有了思路,那么解决方法就是在拦截事件之前,先判断下ListView是否需要消费事件,即是否还能继续往下滚动,即是否已经到顶部了。所以优化后的SlideUpLayout如下:
五、最终效果图:完美
一、前面SlideUpLayout控件在使用时,如果包含有ListView或RecyclerView等列表的时候,可能存在这样的问题,直接看图:
其中xml文件如下:
<com.dway.testwork.viewdrag.SlideUpLayout android:id="@+id/slide_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:visibility="visible"> <FrameLayout android:id="@+id/slide_layout_up" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#1f00ffff"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="up"/> </FrameLayout> <FrameLayout android:id="@+id/slide_layout_down" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#1fffff00"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="down"/> <ListView android:id="@+id/slide_layout_down_list" android:layout_width="match_parent" android:layout_height="match_parent"/> </FrameLayout> <FrameLayout android:id="@+id/slide_layout_slide" android:layout_width="match_parent" android:layout_height="100dp" android:clickable="true" android:background="#1fff00ff"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:text="slide"/> </FrameLayout> </com.dway.testwork.viewdrag.SlideUpLayout>
二、原因经分析是事件分派拦截存在着问题,还记得上一篇文章中,事件的拦截是完全交给ViewDragHelper处理的,如下:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { // 把事件处理交给ViewDragHelper return mHelper.shouldInterceptTouchEvent(ev); }因为把拦截交给了ViewDragHelper,那么包含的ListView等就无法消费事件,从而无法正常滚动。所以解决方法就是在这个方法中先对ListView进行判断看是否需要消费,需要消费事件则把事件交给ListView处理。
三、此处穿插科普下事件分派拦截的原理:(此处是Copy过来的,放这里帮助理解)
dispatchTouchEvent
用于touch事件的分发;通俗点说,就是决定当前这个touch事件应该交给谁来处理(是当前View还是父View)
当触摸事件发生时 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法会从根元素依次往下传递直到最内层子元素或在中间某一元素中事件被拦截或者消费.
如果 return true,事件会分发给当前 View 并由 dispatchTouchEvent 方法进行消费,同时事件会停止向下传递;这样该View的onTouchEvent事件也不会得到响应. 如果return false,会将事件返回给父 View 的 onTouchEvent 进行消费。 如果 return super.dispatchTouchEvent(ev),事件会分发给当前 View 的 onInterceptTouchEvent 方法去进行处理。
onInterceptTouchEvent
用于touch事件的拦截;通俗点说,就是决定刚刚 dispatchTouchEvent 抛给我的touch事件应该交给谁来处理(是当前View还是子View)注意跟dispatchTouchEvent的区别
如果 return true,则将事件进行拦截,并将拦截到的事件交由该 View 的 onTouchEvent 进行处理; 如果 return false,则将事件向子View传递,再由子View的 dispatchTouchEvent来对这个事件处理; 如果 return super.onInterceptTouchEvent(ev),事件会被拦截,并将事件交由该 View 的 onTouchEvent 进行处理。
onTouchEvent
用于touch事件的处理;通俗点说,就是决定刚刚 onInterceptTouchEvent 抛给我的touch事件进行处理。
如果return false,那么这个事件会从该 View 向父View传递,父 View 的 onTouchEvent 来接收,而且如果父View也是return false,那事件也会向上传递由onTouchEvent接收处理. 如果retrun true, 则会接收并消费该事件。 如果retrun super.onTouchEvent(ev) 和返回 false 时相同。
四、有了思路,那么解决方法就是在拦截事件之前,先判断下ListView是否需要消费事件,即是否还能继续往下滚动,即是否已经到顶部了。所以优化后的SlideUpLayout如下:
package com.dway.testwork.viewdrag; import android.content.Context; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; /** * 可上下切换,类似纵向的ViewPager,并且上划时向下弹出菜单效果 * Created by dway on 2018/1/23. */ public class SlideUpLayout extends ViewGroup { private View mUpView; private View mDownView; private View mSlideView; //private RecyclerView mListView; private ViewDragHelper mHelper; //上下滑的程度,0表示在upView,1表示在downView private float mSlidePercent = 0; private boolean mInLayout = false; private boolean mFirstLayout = true; public SlideUpLayout(Context context, AttributeSet attrs) { super(context, attrs); mHelper = ViewDragHelper.create(this, 1.0f, new ViewDragHelper.Callback() { @Override public int clampViewPositionVertical(View child, int top, int dy) { if(child == mUpView){ return Math.max(- mUpView.getMeasuredHeight() + mSlideView.getMeasuredHeight(), Math.min(top, 0)); }else if(child == mDownView){ return Math.max(mSlideView.getMeasuredHeight(), Math.min(top, mUpView.getMeasuredHeight())); }else if(child == mSlideView){ return Math.max(- mSlideView.getMeasuredHeight(), Math.min(top, 0)); } return 0; } @Override public boolean tryCaptureView(View child, int pointerId) { return child == mUpView || child == mDownView; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { if(releasedChild == mUpView){ int upViewHeight = mUpView.getMeasuredHeight(); int slideViewHeight = mSlideView.getMeasuredHeight(); float offset = (upViewHeight + releasedChild.getTop() - slideViewHeight) * 1.0f / (upViewHeight - slideViewHeight); mHelper.settleCapturedViewAt(releasedChild.getLeft(), yvel > 0 || yvel == 0 && offset > 0.5f ? 0 : -upViewHeight + slideViewHeight); invalidate(); }else if(releasedChild == mDownView){ int downViewHeight = mDownView.getMeasuredHeight(); int slideViewHeight = mSlideView.getMeasuredHeight(); float offset = (releasedChild.getTop() - slideViewHeight) * 1.0f / downViewHeight; mHelper.settleCapturedViewAt(releasedChild.getLeft(), yvel > 0 || yvel == 0 && offset > 0.5f ? mUpView.getMeasuredHeight() : slideViewHeight); invalidate(); } } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { if(changedView == mUpView){ mDownView.setTop(top + mUpView.getMeasuredHeight()); mSlidePercent = (float) (-top) / mDownView.getMeasuredHeight(); if(mSlidePercent > 0.9f){ mSlideView.setTop(-mSlideView.getMeasuredHeight() + (int)((mSlidePercent - 0.9f)/(1-0.9) * mSlideView.getMeasuredHeight())); }else{ mSlideView.setTop(-mSlideView.getMeasuredHeight()); } requestLayout(); if(mOnSlideListener != null){ mOnSlideListener.onSlide(mSlidePercent); } }else if(changedView == mDownView){ mUpView.setTop(top - mUpView.getMeasuredHeight()); mSlidePercent = (float) (mUpView.getMeasuredHeight() - top) / mDownView.getMeasuredHeight(); if(mSlidePercent > 0.9f){ mSlideView.setTop(-mSlideView.getMeasuredHeight() + (int)((mSlidePercent - 0.9f)/(1-0.9) * mSlideView.getMeasuredHeight())); }else{ mSlideView.setTop(-mSlideView.getMeasuredHeight()); } requestLayout(); if(mOnSlideListener != null){ mOnSlideListener.onSlide(mSlidePercent); } } } @Override public int getViewVerticalDragRange(View child) { return child == mUpView ? mUpView.getMeasuredHeight() - mSlideView.getMeasuredHeight() : child == mDownView ? mDownView.getMeasuredHeight() : child == mSlideView ? mSlideView.getMeasuredHeight() : 0; } }); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(widthSize, heightSize); mUpView = getChildAt(0); mDownView = getChildAt(1); mSlideView = getChildAt(2); //mListView = mDownView.findViewById(R.id.rv_select_course); //up MarginLayoutParams lp = (MarginLayoutParams) mUpView.getLayoutParams(); int widthSpec = MeasureSpec.makeMeasureSpec( widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); int heightSpec = MeasureSpec.makeMeasureSpec( heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); mUpView.measure(widthSpec, heightSpec); //slide lp = (MarginLayoutParams) mSlideView.getLayoutParams(); widthSpec = getChildMeasureSpec(widthMeasureSpec, lp.leftMargin + lp.rightMargin, lp.width); heightSpec = getChildMeasureSpec(heightMeasureSpec, lp.topMargin + lp.bottomMargin, lp.height); mSlideView.measure(widthSpec, heightSpec); //down lp = (MarginLayoutParams) mDownView.getLayoutParams(); widthSpec = MeasureSpec.makeMeasureSpec( widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); heightSpec = MeasureSpec.makeMeasureSpec( heightSize - lp.topMargin - lp.bottomMargin - mSlideView.getMeasuredHeight(), MeasureSpec.EXACTLY); mDownView.measure(widthSpec, heightSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { mInLayout = true; MarginLayoutParams lp = (MarginLayoutParams) mUpView.getLayoutParams(); int upTop; if(mFirstLayout){ upTop = lp.topMargin; }else{ upTop = mUpView.getTop(); } mUpView.layout(lp.leftMargin, upTop, lp.leftMargin + mUpView.getMeasuredWidth(), upTop + mUpView.getMeasuredHeight()); lp = (MarginLayoutParams) mDownView.getLayoutParams(); int downTop; if(mFirstLayout){ downTop = mUpView.getMeasuredHeight() + lp.topMargin; }else{ downTop = mDownView.getTop(); } mDownView.layout(lp.leftMargin, downTop, lp.leftMargin + mDownView.getMeasuredWidth(), downTop + mDownView.getMeasuredHeight()); lp = (MarginLayoutParams) mSlideView.getLayoutParams(); int slideTop; if(mFirstLayout){ slideTop = - mSlideView.getMeasuredHeight() + lp.topMargin; }else{ slideTop = mSlideView.getTop(); } mSlideView.layout(lp.leftMargin, slideTop, lp.leftMargin + mSlideView.getMeasuredWidth(), slideTop + mSlideView.getMeasuredHeight()); mInLayout = false; mFirstLayout = false; } @Override public void requestLayout() { if(!mInLayout) { super.requestLayout(); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mFirstLayout = true; } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mFirstLayout = true; } @Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { //return mHelper.shouldInterceptTouchEvent(ev); //方法一:还需要根据坐标是否是mListView的范围,进行控制。该方式更直接,理论上效率会更高 /*if(isInViewArea(mListView, ev.getRawX(), ev.getRawY()) && mListView.canScrollVertically(-1)){ //listview可以滚动则交给子view决定 return false; }else{ //已经滚动到顶部,则交给ViewDragHelper处理 return mHelper.shouldInterceptTouchEvent(ev); }*/ //方法二:把以上的处理改为更一般的情况,递归获取每个子view进行判断处理 if(hasViewCanScrollUp(mDownView, ev.getRawX(), ev.getRawY())){ return false; }else{ return mHelper.shouldInterceptTouchEvent(ev); } } @Override public boolean onTouchEvent(MotionEvent event) { mHelper.processTouchEvent(event); return true; } @Override public void computeScroll() { if (mHelper.continueSettling(true)) { invalidate(); } } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); } /** * 判断指定点所在的view是否未到顶可以继续上划,如果view为ViewGroup的话还得递归往子view判断进去 */ private boolean hasViewCanScrollUp(View view, float x, float y){ if(view instanceof ViewGroup){ ViewGroup viewGroup = (ViewGroup) view; for(int i=0; i<viewGroup.getChildCount(); i++){ View child = viewGroup.getChildAt(i); if(hasViewCanScrollUp(child, x, y)){ return true; } } return isInViewArea(view, x, y) && view.canScrollVertically(-1); }else{ return isInViewArea(view, x, y) && view.canScrollVertically(-1); } } /** * 判断坐标是否在view区域内 */ private boolean isInViewArea(View view, float x, float y){ int[] local = new int[2]; view.getLocationOnScreen(local); return x > local[0] && x < local[0]+view.getMeasuredWidth() && y > local[1] && y < local[1]+view.getMeasuredHeight(); } public float getSlidePercent(){ return mSlidePercent; } public boolean isSlideUp(){ return floatCompare(mSlidePercent, 0); } public boolean isSlideDown(){ return floatCompare(mSlidePercent, 1); } public void slideToDown(){ mHelper.smoothSlideViewTo(mDownView, mDownView.getLeft(), mSlideView.getMeasuredHeight()); invalidate(); } public void slideToUp(){ mHelper.smoothSlideViewTo(mUpView, mUpView.getLeft(), 0); invalidate(); } private OnSlideListener mOnSlideListener = null; /** * 外部可设置监听slide的位置回调 */ public void setOnSlideListener(OnSlideListener listener) { mOnSlideListener = listener; } public interface OnSlideListener{ /** * slide回调 * @param percent 取值区间[0, 1],0代表滑到最顶部,1代表滑到最底部 */ void onSlide(float percent); } private boolean floatCompare(float f1, float f2){ return Math.abs(f1 - f2) < Float.MIN_VALUE; } }注释都在代码中,有两种方法,一种比较直接,直接判断ListView。第二种是把方法一般化,递归判断每一个子view。注意其中view位置的判断和view是否能滚动的判断方法。另外代码也添加了滚动的监听。
五、最终效果图:完美
相关文章推荐
- 重写ViewGroup并借助ViewDragHelper实现各种拖拽交互效果(一)
- Android——RecyclerView——使用ItemDragHelper来实现酷炫拖拽效果
- ViewDragHelper 自定义ViewGroup实现QQ5.0侧滑效果
- Android使用ViewDragHelper实现QQ6.X最新版本侧滑界面效果实例代码
- Android开发:使用ViewDragHelper实现抽屉拉伸效果
- 彷QQ侧滑菜单动画实现效果—ViewDragHelper
- Android 中通过ViewDragHelper实现ListView的Item的侧拉划出效果
- ViewDragHelper实现QQ侧滑效果
- 使用ItemTouchHelper和RecyclerView实现拖拽移动效果
- android 用ViewDragHelper实现IOSAssistiveTouch小圆点(或者其他拖拉效果)
- 通过ViewDragHelper实现ListView的Item的侧拉划出效果
- Android ViewDragHelper实现 侧滑删除效果
- 利用ViewDragHelper实现3d效果的View
- RecyclerView进阶:使用ItemTouchHelper实现拖拽和侧滑删除效果
- 使用ViewDragHelper 实现吸边效果
- android 利用ViewDragHelper实现childView可随手指移动的自定义ViewGroup
- Android开发之使用ViewDragHelper实现侧边栏滑动的效果
- 使用ViewDragHelper实现的DragLayout开门效果
- Android开发:使用ViewDragHelper实现抽屉拉伸效果
- Android Scroll滑动效果精炼详解第(二)课:ViewDragHelper和简单实现QQ首页滑动效果