赞
踩
随手拖动
展开闭合
动态更改文字
全屏拖动,也可以限定位置
响应点击事件
可通过xml配置颜色和内部样式
1.在工程根目录的build.gradle中添加依赖
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
并在工程目录的build.gradle中添加依赖(最新版本可查看TrackView)
implementation 'com.github.XiaogegeChen:TrackView:2.0'
2.在xml中配置属性
<com.xiaoegeg.arttest.TrackView
android:id="@+id/track_view"
android:layout_marginTop="50dp"
android:layout_width="50dp"
android:layout_height="100dp" />
3.展开、闭合、动态更改文字、监听点击。
final TrackView trackView = findViewById (R.id.track_view); trackView.setOnClickListener (new View.OnClickListener () { @Override public void onClick(View v) { Toast.makeText (MainActivity.this, "点击了拖动按钮", Toast.LENGTH_SHORT).show (); } }); findViewById (R.id.open).setOnClickListener (new View.OnClickListener () { @Override public void onClick(View v) { // 展开 trackView.open (); } }); findViewById (R.id.close).setOnClickListener (new View.OnClickListener () { @Override public void onClick(View v) { // 闭合 trackView.close (); } }); findViewById (R.id.change).setOnClickListener (new View.OnClickListener () { @Override public void onClick(View v) { // 动态更改文字 trackView.setText ("num:" + num); num++; } });
可以根据需要配置相应的属性
app:inner_text
文字,可动态更改
app:inner_text_color
文字颜色
app:inner_text_size
文字尺寸,单位sp
app:inner_distance
是两个箭头之间的间距
app:inner_length
是每个箭头的边长
app:inner_stroke_width
是两个箭头的线条宽
app:blank_bottom
是底部留白的高度
app:blank_left
是左侧留白的高度
app:blank_right
是右侧留白的高度
app:blank_top
是顶部留白的高度
app:inner_content_color
是圆形内部的填充色
app:inner_stroke_color
是两个箭头的线条颜色
app:out_stroke_color
是外圆线条的颜色
app:out_stroke_width
是外圆线条的线宽
至此, 就可以实现演示的功能了。有兴趣可以接着分析一下实现方法。
主要是通过重写onTouchEvent()
方法,下面按照功能分步完成onTouchEvent()
方法。
在自定义view中,如果需要实现随手拖动功能,可以从Android的事件分发机制入手。 手指从接触屏幕到离开屏幕是一个事件序列,这个序列一定是从MotionEvent.ACTION_DOWN
开始,到MotionEvent.ACTION_UP
结束,如果是滑动,中间会有一系列的MotionEvent.ACTION_MOVE
事件,如果是点击或者长按事件,则不会有MotionEvent.ACTION_MOVE
事件。因此可以在MotionEvent.ACTION_MOVE
事件发生时候拿到手指点击位置的坐标,并将view移动到这个位置,即可实现随手拖动。
所以,通过重写onTouchEvent()
方法,在event为MotionEvent.ACTION_MOVE
时候移动view,就可以实现随手拖动,并返回true来截获并消费触摸事件序列,不再继续传递。伪代码就可以这样写:
@Override public boolean onTouchEvent(MotionEvent event) { // 获得触摸点的绝对坐标 int x = (int) event.getRawX (); int y = (int) event.getRawY (); switch(event.getAction ()){ case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_UP: break; case MotionEvent.ACTION_MOVE: int dx; int dy; // 拿到位置差 dx = x - mLastX; dy = y - mLastY; // 移动view setTranslationX (getTranslationX () + dx); setTranslationY (getTranslationY () + dy); break; } // 更新位置 mLastX = x; mLastY = y; return true; }
如果按照上面的代码,可以实现随手拖动,但是不能响应点击事件。这时注意这个警告:
意思是说我们在调用onTouchEvent()
时要考虑在它的内部调用performClick()
方法,因为view的点击事件其实是在onTouchEvent()
调用的,如果我们在重写onTouchEvent()
时没有调用performClick()
,就会导致点击事件无法响应,从源码中也能印证这一点
这个方法在MotionEvent.ACTION_UP
,就是一个事件序列结束时候调用。因此要响应点击事件,需要在onTouchEvent()
中调用performClick()
方法。MotionEvent.ACTION_UP
代表一个事件序列的结束,因此需要在这时调用performClick()
方法如下:
@Override public boolean onTouchEvent(MotionEvent event) { // 获得触摸点的绝对坐标 int x = (int) event.getRawX (); int y = (int) event.getRawY (); switch(event.getAction ()){ case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_UP: performClick (); break; case MotionEvent.ACTION_MOVE: int dx; int dy; // 拿到位置差 dx = x - mLastX; dy = y - mLastY; // 移动view setTranslationX (getTranslationX () + dx); setTranslationY (getTranslationY () + dy); break; } // 更新位置 mLastX = x; mLastY = y; return true; }
但是这样会出现滑动与点击冲突,滑动结束的MotionEvent.ACTION_UP
同样会触发点击事件。
点击时间与滑动事件的区别在于,点击事件的事件序列中无MotionEvent.ACTION_MOVE
事件,因此可以通过这个差异来进行区分(这里增加了一种情况,如果触摸着这个view超过500ms没有离开,将其判定为用户在犹豫,因此判定为取消,不判定为点击)。如下:
@Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getRawX (); int y = (int) event.getRawY (); switch(event.getAction ()){ case MotionEvent.ACTION_DOWN: // 重置这个开始的时间 mDownTime = System.currentTimeMillis (); break; case MotionEvent.ACTION_UP: // 重置这个结束的时间 mUpTime = System.currentTimeMillis (); // 设置当前的模式 if(mMode != Mode.MOVE){ if(mUpTime - mDownTime >= CANCEL_INTERVAL_DEFAULT){ mMode = Mode.CANCEL; }else{ mMode = Mode.CLICK; } } // 根据当前的模式设置是否调用点击事件 if(mMode == Mode.CLICK){ performClick (); } // 这个事件序列结束,重置当前的模式 mMode = Mode.NONE; break; case MotionEvent.ACTION_MOVE: // 只要触发了ACTION_MOVE就设置为move模式 mMode = Mode.MOVE; int dx; int dy; dx = x - mLastX; dy = y - mLastY; setTranslationX (getTranslationX () + dx); setTranslationY (getTranslationY () + dy); break; } // 更新位置 mLastX = x; mLastY = y; return true; } /** * 该控件的三种模式,只要触发了ACTION_MOVE就是MOVE模式 * 没有触发ACTION_MOVE但是从ACTION_DOWN开始超过了500ms就是CANCEL模式 * 未超过就是CLICK模式 */ private enum Mode{ // 取消,不执行任何逻辑 CANCEL, // 点击,执行点击事件 CLICK, // 移动模式,随手移动 MOVE, // 无模式,就是复位后的状态 NONE }
记录事件序列开始,就是MotionEvent.ACTION_DOWN
的时间和事件序列结束,就是MotionEvent.ACTION_UP
的时间。如果有出现MotionEvent.ACTION_MOVE
,直接判定MOVE
模式,如果时间差超过500ms并且没MotionEvent.ACTION_MOVE
,判定为CANCEL
模式,剩下的就是CLICK
模式了。判定完模式之后,就可以根据模式来决定是否调用performClick()
以响应点击事件了。
由于是全屏滑动,如果不设置限定,会出现view飞出视野的情况。
因此,在执行view的移动前预先判断一下不加限制将会到达的位置,如果位置在限定范围之外,就调整移动的距离即可,如下:
@Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getRawX (); int y = (int) event.getRawY (); switch(event.getAction ()){ case MotionEvent.ACTION_DOWN: // 重置这个开始的时间 mDownTime = System.currentTimeMillis (); // 拿到拖动点相对view的位置 mDisX = (int) event.getX (); mDisY = (int) event.getY (); break; case MotionEvent.ACTION_UP: // 重置这个结束的时间 mUpTime = System.currentTimeMillis (); // 设置当前的模式 if(mMode != Mode.MOVE){ if(mUpTime - mDownTime >= CANCEL_INTERVAL_DEFAULT){ mMode = Mode.CANCEL; }else{ mMode = Mode.CLICK; } } // 根据当前的模式设置是否调用点击事件 if(mMode == Mode.CLICK){ performClick (); } // 这个事件序列结束,重置当前的模式 mMode = Mode.NONE; break; case MotionEvent.ACTION_MOVE: // 只要触发了ACTION_MOVE就设置为move模式 mMode = Mode.MOVE; int dx; int dy; // 预测量的边距 int preXLeft = x - mDisX; int preXRight = mScreenWidthInPixel - (x + Math.min (mOutWidth, mOutHeight) - mDisX); int preYUp = y - mDisY; int preYDown = mScreenHeightInPixel - (y + Math.min (mOutWidth, mOutHeight) - mDisY); // 处理X坐标 if(preXLeft <= mBlankLeft){ // 超出左边界 dx = x - mLastX + mBlankLeft - preXLeft; x = x + mBlankLeft - preXLeft; }else if(preXRight <= mBlankRight){ // 超出右边界 dx = x - mLastX - (mBlankRight - preXRight); x = x - (mBlankRight - preXRight); }else{ // 正常 dx = x - mLastX; } // 处理Y坐标 if (preYUp <= mBlankTop) { // 超出上边界 dy = y - mLastY + mBlankTop - preYUp; y = y + mBlankTop - preYUp; }else if(preYDown <= mBlankBottom){ // 超出下边界 dy = y - mLastY - (mBlankBottom - preYDown); y = y - (mBlankBottom - preYDown); }else { // 正常 dy = y - mLastY; } setTranslationX (getTranslationX () + dx); setTranslationY (getTranslationY () + dy); break; } // 更新位置 mLastX = x; mLastY = y; return true; }
画个图来辅助理解
拿左边界来说。存在一个临界点,上一次未到达边界,下一次将会到达边界,因此预先计算一下,如果是这种情况,将多出的长度减掉即可。
知乎的悬浮按钮可以自动靠边,不然影响阅读。
在事件序列结束时,即MotionEvent.ACTION_UP
中判断当前view处于屏幕的左半部还是右半部,然后直接移动到边上即可。
@Override public boolean onTouchEvent(MotionEvent event) { int x = (int) event.getRawX (); int y = (int) event.getRawY (); switch(event.getAction ()){ case MotionEvent.ACTION_DOWN: // 重置这个开始的时间 mDownTime = System.currentTimeMillis (); mDisX = (int) event.getX (); mDisY = (int) event.getY (); break; case MotionEvent.ACTION_UP: // 重置这个结束的时间 mUpTime = System.currentTimeMillis (); // 设置当前的模式 if(mMode != Mode.MOVE){ if(mUpTime - mDownTime >= CANCEL_INTERVAL_DEFAULT){ mMode = Mode.CANCEL; }else{ mMode = Mode.CLICK; } } // 根据当前的模式设置是否调用点击事件 if(mMode == Mode.CLICK){ performClick (); } // 回到侧面 if(event.getRawX () < mScreenWidthInPixel / 2){ // 回到最左侧 setTranslationX (getTranslationX () + (-1 * (x - mDisX - mBlankLeft))); x = x - (x - mDisX - mBlankLeft); }else{ // 回到最右侧 setTranslationX (getTranslationX () + ((mScreenWidthInPixel - mBlankRight) - (Math.min (mOutWidth, mOutHeight) - mDisX + x))); x = x + (mScreenWidthInPixel - mBlankRight) - (Math.min (mOutWidth, mOutHeight) - mDisX + x); } // 这个事件序列结束,重置当前的模式 mMode = Mode.NONE; break; case MotionEvent.ACTION_MOVE: // 只要触发了ACTION_MOVE就设置为move模式 mMode = Mode.MOVE; int dx; int dy; // 预测量的边距 int preXLeft = x - mDisX; int preXRight = mScreenWidthInPixel - (x + Math.min (mOutWidth, mOutHeight) - mDisX); int preYUp = y - mDisY; int preYDown = mScreenHeightInPixel - (y + Math.min (mOutWidth, mOutHeight) - mDisY); // 处理X坐标 if(preXLeft <= mBlankLeft){ // 超出左边界 dx = x - mLastX + mBlankLeft - preXLeft; x = x + mBlankLeft - preXLeft; }else if(preXRight <= mBlankRight){ // 超出右边界 dx = x - mLastX - (mBlankRight - preXRight); x = x - (mBlankRight - preXRight); }else{ // 正常 dx = x - mLastX; } // 处理Y坐标 if (preYUp <= mBlankTop) { // 超出上边界 dy = y - mLastY + mBlankTop - preYUp; y = y + mBlankTop - preYUp; }else if(preYDown <= mBlankBottom){ // 超出下边界 dy = y - mLastY - (mBlankBottom - preYDown); y = y - (mBlankBottom - preYDown); }else { // 正常 dy = y - mLastY; } setTranslationX (getTranslationX () + dx); setTranslationY (getTranslationY () + dy); break; } // 更新位置 mLastX = x; mLastY = y; return true; }
至此,完整的onTouchEvent()
方法就完成了,其它的就是一些自定义view常用的方法了,完整代码可以参考TrackView.java。
项目托管在GitHub上,欢迎提出issue,共同探讨,共同进步。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。