转载请以链接形式标明出处: 本文出自:103style的博客
《Android开发艺术探索》 学习记录
可以带着以下问题来看本文:
- View的坐标系和坐标,平移等动画改变的是什么属性?
- View有哪些事件?
- 如果获取系统可识别的最短滑动距离?
- 如果计算滑动的速度?
- 单击、双击、长按等事件的监听?
- 弹性滑动的实现?
目录
- View 与 ViewGroup
- View 的位置参数
- MotionEvent 和 TouchSlop
- VelocityTracker
- GestureDetector
- Scroller
View与ViewGroup
public class View extends
Object
implementsDrawable.Callback
,KeyEvent.Callback
,AccessibilityEventSource
java.lang.Object
↳android.view.View
Known direct subclassesAnalogClock
,ImageView
,KeyboardView
,MediaRouteButton
,ProgressBar
,Space
,SurfaceView
,TextView
,TextureView
,ViewGroup
,ViewStub
. Known indirect subclassesAbsListView
,AbsSeekBar
,AbsSpinner
,AbsoluteLayout
,AutoCompleteTextView
,Button
,CalendarView
,CheckBox
,CheckedTextView
,Chronometer
,and 57 others..
public abstract class
ViewGroup
extendsView
implementsViewParent
,ViewManager
java.lang.Object
↳android.view.View
↳android.view.ViewGroup
Known direct subclassesAbsoluteLayout
,AdapterView<T extends Adapter>
,FragmentBreadCrumbs
,FrameLayout
,GridLayout
,LinearLayout
,RelativeLayout
,SlidingDrawer
,Toolbar
,TvView
. Known indirect subclassesAbsListView
,AbsSpinner
,CalendarView
,DatePicker
,ExpandableListView
,Gallery
,GridView
,HorizontalScrollView
,ImageSwitcher
,and 26 others
.
通过上面的官方介绍,我们可以看到,View 是我们平常看到的视图上所有元素的父类,按钮Button
、文本TextView
、图片ImageView
等。
ViewGroup 也是 View 的子类,ViewGroup 相当与 View 的容器,可以包含很多的 View.
View的位置参数
View的坐标系如下图:
左上角为原点O(0,0),X、Y轴分别向右向下递增。
图中 View 和 ViewGroup 的位置由其四个顶点决定,以View为例,分别对应四个属性:Left
、Top
、Right
、Bottom
.
所以 Width = Right - Left
, Height = Bottom - Top
.
在 Android 3.0
开始,View又增加了 x
、y
、translationX
、translationY
四个参数。
x
、y
即为上图中的A点,分别对应A点在View坐标系中的X、Y轴上的坐标。
translationX
、translationY
则为相对于父容器ViewGroup的偏移量,默认为 0
。
他们的关系为: x = left + tranlastionX
、y = top + tranlastionY
.
需要注意的是:在平移过程中,top 和 left 表示的是原始左上角的位置信息,是不变的,发生改变的是 x、y、translationX、translationY。
下面我们来测试看看:
<!-- activity_main.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:padding="8dp"
android:text="Hello World!" />
</LinearLayout>
//MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView tv = findViewById(R.id.tv);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.e(TAG, "tv.getLeft() = " + tv.getLeft());
Log.e(TAG, "tv.getTop() = " + tv.getTop());
Log.e(TAG, "tv.getRight() = " + tv.getRight());
Log.e(TAG, "tv.getBottom() = " + tv.getBottom());
Log.e(TAG, "tv.getWidth() = " + tv.getWidth());
Log.e(TAG, "tv.getHeight() = " + tv.getHeight());
Log.e(TAG, "tv.getX() = " + tv.getX());
Log.e(TAG, "tv.getY() = " + tv.getY());
Log.e(TAG, "tv.getTranslationX() = " + tv.getTranslationX());
Log.e(TAG, "tv.getTranslationY() = " + tv.getTranslationY());
}
});
}
}
点击按钮,打印日志如下:
MainActivity: tv.getLeft() = 21
MainActivity: tv.getTop() = 21
MainActivity: tv.getRight() = 263
MainActivity: tv.getBottom() = 114
MainActivity: tv.getWidth() = 242
MainActivity: tv.getHeight() = 93
MainActivity: tv.getX() = 21.0
MainActivity: tv.getY() = 21.0
MainActivity: tv.getTranslationX() = 0.0
MainActivity: tv.getTranslationY() = 0.0
我们可以看到 left、top、right、bottom 是整形的, 而 x、y、translationX、translationY 是浮点型的。
MotionEvent 和 TouchSlop
MotionEvent 即为我们点击屏幕所产生的一些列事件,主要有以下几个:
ACTION_DOWN
:手指刚接触屏幕。ACTION_MOVE
:手指在屏幕上滑动。ACTION_UP
:手指离开屏幕的一瞬间。ACTION_CANCEL
:消耗了DOWN事件却没有消耗UP事件,再次触发DOWN时,会先触发CANCEL事件。
一般依次点击屏幕操作,会产生一些列事件:DOWN → 0个或多个 MOVE → UP。
通过MotionEvent 我们可以知道事件发生的 x , y 坐标, 可以通过系统提供的 getX()/getY()
和 getRawX()/getRawY()
获取。
getX()/getY()
是对于当前View左上角的坐标.
getRawX()/getRawY()
则是对于屏幕左上点的坐标.
TouchSlop 则是系统所能识别的最短的滑动距离,
这个距离可以通过 ViewConfiguration.get(getContext()).getScaledTouchSlop()
获得。
在 Genymotion上的 Google pixel
9.0系统 420dpi
的模拟器上得到的值如下:
MainActivity: getScaledTouchSlop = 21
VelocityTracker
VelocityTracker 是用来记录手指滑动过程中的速度的,包括水平方向和数值方向。 可以通过如下方式来获取当前事件的滑动速度:
tv.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);
float vX = velocityTracker.getXVelocity();
float vY = velocityTracker.getYVelocity();
Log.e(TAG, "vX = " + vX + ", vY = " + vY);
velocityTracker.clear();
velocityTracker.recycle();
break;
}
return true;
}
});
MainActivity: vX = 542.164, vY = 271.18683
MainActivity: vX = 2257.9578, vY = 291.47467
MainActivity: vX = 2237.9333, vY = 379.69537
MainActivity: vX = 1676.5919, vY = 697.79443
MainActivity: vX = 1672.0844, vY = 288.5999
MainActivity: vX = 645.7418, vY = 322.51065
MainActivity: vX = 810.2783, vY = 270.19778
当然最后,在不用的时候记得调用以下代码重置并回收掉 VelocityTracker:
velocityTracker.clear();
velocityTracker.recycle();
GestureDetector
GestureDetector 即手势检测,用于辅助我们捕获用户的 单击、双击、滑动、长按等行为。
使用也很简单,只需要创建一个下面来看个示例。
在构造函数中创建 通过 gestureDetector = new GestureDetector(context, this)
创建 GestureDetector,
然后实现 GestureDetector.OnGestureListener
和 GestureDetector.OnDoubleTapListener
接口,
然后在 onTouchEvent
中 返回 gestureDetector.onTouchEvent(event)
:
public class TestGestureDetector extends View implements GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener {
private static final String TAG = "TestGestureDetector";
GestureDetector gestureDetector;
public TestGestureDetector(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
gestureDetector = new GestureDetector(context, this);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
@Override
public boolean onDown(MotionEvent e) {
Log.e(TAG, "onDown: action = " + e.getAction());
return false;
}
@Override
public void onShowPress(MotionEvent e) {
Log.e(TAG, "onShowPress:");
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
Log.e(TAG, "onSingleTapUp: " + e.getAction());
return false;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
Log.e(TAG, "onScroll: e1.action = " + e1.getAction() + ", e2.action = " + e2.getAction());
return false;
}
@Override
public void onLongPress(MotionEvent e) {
Log.e(TAG, "onLongPress: action = " + e.getAction());
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
Log.e(TAG, "onFling: e1.action = " + e1.getAction() + ", e2.action = " + e2.getAction());
return false;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.e(TAG, "onSingleTapConfirmed: action = " + e.getAction());
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.e(TAG, "onDoubleTap: action = " + e.getAction());
return false;
}
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
Log.e(TAG, "onDoubleTapEvent: action = " + e.getAction());
return false;
}
}
然后在布局中让它占满屏幕。
tips: action = 0 为 DOWN 事件 action = 1 为 UP 事件 action = 2 为 MOVE 事件
运行程序,我们执行一次单击,一次长按单击,然后双击一次,发下打印日志如下:
//第一次单击
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onLongPress: action = 0
//第一次长按单击
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onLongPress: action = 0
//第一次双击
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onLongPress: action = 0
通过上面的日志信息我们可以知道 :
一次 单击 和 长按单击 操作会触发 onDown
、onShowPress
、onLongPress
三个回调。
双击 操作则会依次触发 onDown
、onShowPress
、onDown
、onShowPress
、onLongPress
五次回调。
显示单击出现 onLongPress 是不合理的,我们可以通过 gestureDetector.setIsLongpressEnabled(false)
禁用掉,而且我们也没有监听到 单机和双击等其他回调,这是为什么呢?
这是因为我们 没有消耗掉 DOWN 事件,这涉及到事件分发相关的知识了,这里先不说,后面会写文章单独讲解。那怎么消耗掉 DOWN 事件呢?很简单,只要在 onDown
中返回 true
。
修改上述代码如下,只贴出修改的部分,
public class TestGestureDetector extends View implements GestureDetector.OnGestureListener,
GestureDetector.OnDoubleTapListener {
...
public TestGestureDetector(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
gestureDetector = new GestureDetector(context, this);
gestureDetector.setIsLongpressEnabled(false);
}
@Override
public boolean onDown(MotionEvent e) {
Log.e(TAG, "onDown: action = " + e.getAction());
return true;
}
...
}
运行程序,在执行一次单击,一次长按单击和一次双击,日志如下:
//第一次单击
TestGestureDetector: onDown: action = 0
TestGestureDetector: onSingleTapUp: 1
TestGestureDetector: onSingleTapConfirmed: action = 0
//第一次长按单击
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onSingleTapUp: 1
TestGestureDetector: onSingleTapConfirmed: action = 1
//第一次双击
TestGestureDetector: onDown: action = 0
TestGestureDetector: onSingleTapUp: 1
TestGestureDetector: onDoubleTap: action = 0
TestGestureDetector: onDoubleTapEvent: action = 0
TestGestureDetector: onDown: action = 0
TestGestureDetector: onDoubleTapEvent: action = 1
我们可以看到现在一次单击则会触发onDown
、onSingleTapUp
、onSingleTapConfirmed
这三个回调。
一次长按单击则会触发onDown
、onShowPress
、onSingleTapUp
、onSingleTapConfirmed
这四个回调。
一次双击则会一次触发onDown
、onSingleTapUp
、onDoubleTap
、onDoubleTapEvent
、onDown
、onDoubleTapEvent
这六个回调。
而我们在屏幕上快速滑动时,则会触发 onDown
、onShowPress
、onScroll
、onScroll
、onFling
这五个回调,onShowPress 取决于你在按下和开始滑动之前的时间间隔,短的话就不会有, 是否有 onFling 取决于滑动的距离和速度
。
TestGestureDetector: onDown: action = 0
TestGestureDetector: onShowPress:
TestGestureDetector: onScroll: e1.action = 0, e2.action = 2
TestGestureDetector: onScroll: e1.action = 0, e2.action = 2
TestGestureDetector: onFling: e1.action = 0, e2.action = 1
下面我们来统一介绍下这些回调具体的含义把:
方法名 | 描述 | 所属接口 |
---|---|---|
onDown |
触摸View的瞬间,由一个 DOWN 触发 | OnGestureListener |
onShowPress |
触摸View未松开或者滑动时触发 | OnGestureListener |
onSingleTapUp |
触摸后松开,在onDown的基础上加了个 UP 事件, 属于单击行为 |
OnGestureListener |
onScroll |
按下并拖动,由一个 DOWN 和 多个 MOVE 组成, 属于拖动行为 |
OnGestureListener |
onLongPress |
长按事件 | OnGestureListener |
onFling |
快速滑动后松开,需要滑动一定的距离 | OnGestureListener |
onSingleTapConfirmed |
严格的单击行为,onSingleTapUp之后只能是onSingleTapConfirmed 或 onDoubleTap 中 的一个 | OnDoubleTapListener |
onDoubleTap |
双击行为,和 onSingleTapConfirmed 不共存 | OnDoubleTapListener |
onDoubleTapEvent |
表示双击行为的发生, 一次双击行为会触发多次onDoubleTapEvent |
OnDoubleTapListener |
Scroller
Scroller 用于实现View的弹性滑动,当我们使用View的 scrollTo
、scrollBy
方法进行滑动时,滑动时瞬间完成的,没有过渡效果使得用户体验不好,这个时候就可以使用 Scroler 来解决这一用户体验差的问题。
Scroller本身无法让View弹性滑动,需要配合View的 computeScroll
方法。
那如果使用Scroller呢? 它的典型代码是固定的,如下所示。 至于为什么能够实现,我们下篇文章介绍 View的滑动 的时候再具体分析。
public class TestScroller extends TextView {
private static final String TAG = "TestScroller";
Scroller mScroller;
public TestScroller(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
public void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int scrollY = getScrollY();
int deltaX = destX - scrollX;
int deltaY = destY - scrollY;
mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000);
invalidate();
}
}
//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.lxk.viewdemo.TestScroller
android:id="@+id/tv"
android:layout_width="320dp"
android:layout_height="320dp"
android:layout_margin="8dp"
android:background="@color/colorAccent"
android:gravity="center"
android:padding="8dp"
android:text="Hello World!" />
</LinearLayout>
//MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TestScroller scroller = findViewById(R.id.tv);
scroller.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scroller.smoothScrollTo(200, 200);
}
});
}
运行看看,可以看到点击之后,内容在 1s
内往左上方各平移了 200px
。
如果觉得不错的话,请帮忙点个赞呗。
以上
扫描下面的二维码,关注我的公众号 Android1024, 点关注,不迷路。
`