转载请以链接形式标明出处: 本文出自:103style的博客
《Android开发艺术探索》 学习记录
可以带着以下问题来看本文:
public class View extends
ObjectimplementsDrawable.Callback,KeyEvent.Callback,AccessibilityEventSourcejava.lang.Object↳android.view.ViewKnown 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
ViewGroupextendsViewimplementsViewParent,ViewManagerjava.lang.Object↳android.view.View↳android.view.ViewGroupKnown 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的坐标系如下图:

左上角为原点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 即为我们点击屏幕所产生的一些列事件,主要有以下几个:
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 = 21VelocityTracker 是用来记录手指滑动过程中的速度的,包括水平方向和数值方向。 可以通过如下方式来获取当前事件的滑动速度:
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 = 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 用于实现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, 点关注,不迷路。
`