Android触摸事件传递机制实践——可拖动、大小切换的SizeSwitchView
来源:互联网 发布:淘宝的被骗了怎么办 编辑:程序博客网 时间:2024/06/14 03:27
出处:
炎之铠邮箱:yanzhikai_yjk@qq.com
博客地址:http://blog.csdn.net/totond
本文原创,转载请注明本出处!
本项目GitHub地址:https://github.com/totond/SizeSwitchViewDemo
欢迎 Star or Fork!
前言
对于Android的触摸事件传递机制,网上有很多讲解,有结合源码的,有图文结合的,其中不乏一些讲解清晰明了的文章,看完之后都能有所收获。然而,理论终究是要应用在实践上的,最近工作的时候,做出了一个可拖动,可以大小切换,大形态嵌套着ViewGroup的SizeSwitchView,其中涉及了比较复杂的触摸事件处理,实践完之后我感觉对事件传递机制熟悉了很多,在这里做出记录,并分享给大家。
介绍
这个需求是做一个方向键,然后这个方向键有5个按键,整体比较大,可能会挡着其他的内容,然后就要求支持拖动和大小切换:
由于SizeSwitchView的可扩展性不高(大形态的ViewGroup可以是多种多样的),要修改的话改动比较大,功能也不是很全面(只支持父ViewGroup为RelativeLayout),所以我把它定位为demo的方式分享出去,就不把它封装并上传到Jcenter了。
实现
1.结构分析
SizeSwitchView本质上是一个ViewGroup,里面包含两个互斥居中的控件(一个显示,另外一个就不显示),一个是小形态的控件,就只是一个ImageView,另一个是大形态的控件,是一个ViewGroup,里面包含着5个ImageView,也就是下面的BigDirectionKey:
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" > <yanzhikai.sizeswitchview.BigDirectionKey android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" android:id="@+id/big_dk"/> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" android:id="@+id/small_dk" android:clickable="true" android:scaleType="fitCenter" android:src="@drawable/direction" android:background="@drawable/background_button" /></RelativeLayout>
2.状态切换
关于大小形态的互相切换,主要注意的有两个点:宽高和位置的变化、动画的处理。
宽高和位置的变化
由于大形态和小形态的宽高不同,所以SizeSwitchView就要根据形态来变化大小,这里使用修改LayoutParams的方式来修改大小和位置:
public void setMode(boolean isSmallMode){ this.isSmallMode = isSmallMode; Log.d(TAG, "setMode: "); //设置大小形态的宽高和位置 if (isSmallMode){ LayoutParams smallParams = (LayoutParams) getLayoutParams(); smallParams.width = mSmallWidth; smallParams.height = mSmallHeight; smallParams.leftMargin += (getWidth() - mSmallWidth)/2; smallParams.bottomMargin += (getHeight() - mSmallHeight)/2; setLayoutParams(smallParams); }else { LayoutParams bigParams = (LayoutParams) getLayoutParams(); bigParams.width = mBigWidth; bigParams.height = mBigHeight; bigParams.leftMargin -= (mBigWidth - mSmallWidth)/2; bigParams.bottomMargin -= (mBigHeight - mSmallHeight)/2; setLayoutParams(bigParams);// requestLayout(); } setKeysVisibility(); isDraggable = true; }
通过getLayoutParams()
获取LayoutParams来改动SizeSwitchView的宽高和位置,然后根据大小的宽高变化量来调整位置,使大小形态的中心点保持在同一个点上,这样就让人看起来是在中心点缩放变化一样。
这个LayoutParams的类型取决于父ViewGroup,所以这里就限定了父ViewGroup是要使用RelativeLayout(使用到Margin属性)。
动画的处理
这个切换的动画实际上就是一个旋转缩小透明度减少的动画,加上反向旋转放大透明度增加的动画,这两个组合起来(没错,就是在模仿宇智波带土的神威)。。。
旋转缩小透明度减少动画shrink.xml:
<?xml version="1.0" encoding="utf-8"?><set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="400"> <rotate android:fromDegrees="0" android:toDegrees="360" android:pivotX="50%" android:pivotY="50%" /> <scale android:fromXScale="1" android:fromYScale="1" android:pivotX="50%" android:pivotY="50%" android:toXScale="0" android:toYScale="0" /> <alpha android:fromAlpha="1" android:toAlpha="0.3" /></set>
反向旋转放大透明度增加动画:
<?xml version="1.0" encoding="utf-8"?><set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="400"> <rotate android:fromDegrees="0" android:toDegrees="-360" android:pivotX="50%" android:pivotY="50%" /> <scale android:fromXScale="0" android:fromYScale="0" android:pivotX="50%" android:pivotY="50%" android:toXScale="1" android:toYScale="1" /> <alpha android:fromAlpha="0.3" android:toAlpha="1" /></set>
下面是动画的设置:
//初始化动画 private void initAnim(){ smallShrinkAnimation = AnimationUtils.loadAnimation(mContext,R.anim.shrink); bigLargenAnimation = AnimationUtils.loadAnimation(mContext,R.anim.largen); smallShrinkAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { setKeysClickable(false); isDraggable = false; } @Override public void onAnimationEnd(Animation animation) { setMode(false); startAnimation(bigLargenAnimation); } @Override public void onAnimationRepeat(Animation animation) { } }); bigLargenAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { setKeysClickable(true); isDraggable = true; setSmallKeyClick(); checkBoundary(); } @Override public void onAnimationRepeat(Animation animation) { } }); bigShrinkAnimation = AnimationUtils.loadAnimation(mContext,R.anim.shrink); smallLargenAnimation = AnimationUtils.loadAnimation(mContext,R.anim.largen); bigShrinkAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { setKeysClickable(false); isDraggable = false; } @Override public void onAnimationEnd(Animation animation) { setMode(true); startAnimation(smallLargenAnimation); setSmallKeyClick(); } @Override public void onAnimationRepeat(Animation animation) { } }); smallLargenAnimation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { setKeysClickable(true); isDraggable = true; checkBoundary(); } @Override public void onAnimationRepeat(Animation animation) { } }); }
上面主要是4个动画(变大变小各两个)监听器的实现,思路就是:动画开始的时候不能点击,不能拖动;等到缩小动画完毕之后瞬间切换大小状态,再进行放大动画;动画都结束后就是恢复可以点击和可拖动状态,还有进行一次边界检测,看变换后的View是否越出了父View的边界,越出了的话就移动越出的位移,这个checkBoundary()
方法的实现在后面讲。
3.触摸事件处理
由于SizeSwitchView需要支持拖动,需要实现拦截触摸事件,但是它也是一个父ViewGroup,还需要把点击事件(DOWN和UP事件)传递到子ViewGroup里面的View。了解过Android触摸事件传递机制的都知道,如果父View拦截了DOWN事件之后,后面的事件就不会传递到它的子View了。
所以我在SizeSwitchView的拦截思路是这样的:
其实也不复杂,就是让父ViewGroup只有在拖动达到一定距离的时候才拦截MOVE事件,DOWN事件就传递给子View处理,但是这样有一个问题:在父ViewGroupOnTouchEvent()
方法是没有DOWN事件的,不能在这里获取DOWN事件的坐标,MotionEvent.getX()
和MotionEvent.getY()
方法获取的只是点击事件相对于当前View的零点的位置,而不是在屏幕上的XY坐标,所以光靠MOVE事件的位置数据是无法准确计算SizeSwitchView的移动的(每次都要平移到View的零点才能正常拖动),如下面的效果:
所以就直接在onInterceptTouchEvent()
里面获取DOWN事件的坐标,用全局变量保存着,用MOVE事件的坐标减去它,才能正确计算出它的位移,从而进行准确移动:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: //记录DOWN事件的点击位置,因为不拦截DOWN事件,移动的时候需要这个起点坐标来计算距离。 lastX = ev.getX(); lastY = ev.getY(); break; case MotionEvent.ACTION_MOVE: //拖动距离超过最小拖动量才会被拖动 if (Math.abs(ev.getX() - lastX) > clickOffset && Math.abs(ev.getY() - lastY )> clickOffset){ if (canDrag && isDraggable && ev.getAction() == MotionEvent.ACTION_MOVE){ return true; } } break; } return false; } @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP){ //抬手就进行一次边界检测 checkBoundary(); }else if (event.getAction() == MotionEvent.ACTION_MOVE){ //进行移动操作 if (canDrag && isDraggable) { int offX = (int) (event.getX() - lastX); int offY = (int) (event.getY() - lastY); LayoutParams params = (LayoutParams) getLayoutParams(); params.leftMargin = params.leftMargin + offX; params.rightMargin = params.rightMargin - offX; params.topMargin += offY; params.bottomMargin -= offY; setLayoutParams(params); return true; } return false; } return super.onTouchEvent(event); }
然后就是边界检测了,具体思路很简单,就是计算SizeSwitchView当前位置是不是超出它的父ViewGroup的范围,如果超过的话就要移回来:
//检测View是否跑出边界,如果是则移回来 private void checkBoundary(){ Log.d("checkBoundary", "checkBoundary: "); ViewGroup parent = (ViewGroup) getParent(); boolean isOut = false; int moveX = 0; int moveY = 0; if (getLeft() < 0){ moveX = getLeft(); isOut = true; } if (getTop() < 0){ moveY = getTop(); isOut = true; } if (getRight() > parent.getWidth()){ moveX = (getRight() - parent.getWidth()); isOut = true; } if (getBottom() > parent.getHeight()){ moveY = (getBottom() - parent.getHeight()); isOut = true; } //有出界才进行LayoutParams的设置,节省性能 if (isOut) { LayoutParams params = (LayoutParams) getLayoutParams(); params.setMargins(params.leftMargin - moveX, params.topMargin - moveY, params.rightMargin + moveX, params.bottomMargin + moveY); setLayoutParams(params); } }
这样的话拖动的功能就处理好了,点击事件也传进去子ViewGroup了,但是怎样把子ViewGroup的点击实现实现接口传出来呢?这就和我上一篇的YMenuView的设计差不多了:由于在大形态的BigDirectionKey有5个子View,所以就自己实现一个带index索引的接口OnKeyClickListener去实现点击,外层调用的话只需要实现这个OnKeyClickListener然后传入进来就行了。
private OnKeyClickListener mOnKeyClickListener; //初始化 private void initKeys(){ okKey = new ImageView(mContext); upKey = new ImageView(mContext); downKey = new ImageView(mContext); leftKey = new ImageView(mContext); rightKey = new ImageView(mContext); okKey.setImageResource(R.drawable.background_ok); upKey.setImageResource(R.drawable.background_up); downKey.setImageResource(R.drawable.background_down); leftKey.setImageResource(R.drawable.background_left); rightKey.setImageResource(R.drawable.background_right); okKey.setClickable(true); upKey.setClickable(true); downKey.setClickable(true); leftKey.setClickable(true); rightKey.setClickable(true); okKey.setScaleType(ImageView.ScaleType.FIT_XY); upKey.setScaleType(ImageView.ScaleType.FIT_XY); downKey.setScaleType(ImageView.ScaleType.FIT_XY); leftKey.setScaleType(ImageView.ScaleType.FIT_XY); rightKey.setScaleType(ImageView.ScaleType.FIT_XY); okKey.setId(generateViewId()); upKey.setId(generateViewId()); downKey.setId(generateViewId()); leftKey.setId(generateViewId()); rightKey.setId(generateViewId()); addView(okKey); addView(upKey); addView(downKey); addView(leftKey); addView(rightKey); setBackgroundResource(R.drawable.button_shape); //设置点击监听器 for (int i = 0; i < getChildCount(); i++){ getChildAt(i).setOnClickListener(new MyOnClickListener(i)); } } //重写一个带索引的OnClickListener,索引用于标识5个子View private class MyOnClickListener implements OnClickListener { private int index; public MyOnClickListener(int index) { this.index = index; } @Override public void onClick(View v) { if (mOnKeyClickListener != null) { mOnKeyClickListener.onKeyClick(index); } } } //暴露给外部的点击接口 public interface OnKeyClickListener{ public void onKeyClick(int index); }
这样子,在Activity里面只需要实现BigDirectionKey.OnKeyClickListener接口然后重写里面的方法就可以处理点击事件了:
@Override public void onKeyClick(int index) { switch (index){ case 0: mSizeSwitchView.toSmallMode(); break; case 1: makeToast("1"); break; case 2: makeToast("2"); break; case 3: makeToast("3"); break; case 4: makeToast("4"); break; } }
总结
这样就介绍完了SizeSwitchView大体实现思路了,总体来说就是点击、切换、拖动。其实难点并不多,但是实现的时间还是不短的,就是实现的时候遇到搞不定的功能会各种各种的尝试,最后才得到解决方法,而且思路还是很乱,经过总结原理之后发现很多可以改善的地方,小改的地方我都优化了,可以大改的地方,如换一种移动的方式(使用layout()
方法的方式,可以大大减少Measure的次数,让拖动更平滑),这个改动比较大,留到后面。
后话
这个SizeSwitchView和上一篇的YMenuView都是属于我做的一个项目,目前项目处于总结阶段,在实现功能中学到很多东西,做完过段时间将这些东西整理一遍,又改进了一遍,自己理解得又更深了。
- Android触摸事件传递机制实践——可拖动、大小切换的SizeSwitchView
- Android 触摸事件传递机制
- Android 触摸事件传递机制
- Android 触摸事件传递机制
- Android 触摸事件传递机制
- Android 触摸事件传递机制
- Android 触摸事件传递机制
- android 触摸事件传递机制
- android触摸事件传递机制
- Android 触摸事件传递机制
- Android触摸事件传递机制
- Android触摸事件传递机制
- Android 触摸事件传递机制
- Android 触摸事件传递机制
- 触摸事件的传递机制
- 触摸事件的传递机制
- Android 5.1触摸事件的传递机制深度剖析(上)
- 彻底掌握Android的Touch触摸事件传递机制
- QML中调用摄像头
- Kafka巨坑:org.apache.kafka.common.errors.TimeoutException: Failed to update metadata after 60000 ms.
- OSG的setViewMatrixAsLookAt解析
- python数据处理中的一些实际问题
- Python基础整理操作积累
- Android触摸事件传递机制实践——可拖动、大小切换的SizeSwitchView
- java.lang.IllegalStateException: Fragment already active
- 圆桌问题
- 9.java内存泄漏(2)
- 理解ORM和数据持久化
- nginx:[emerg]unknown directive "ssl"
- 关于Serializable的用法,为什么要实现Serializable?对象为什么要序列化
- 搜索引擎学习(一):搜索引擎学习
- spring的@Transactional注解详细用法