View的基础知识介绍

656 阅读8分钟

转载请以链接形式标明出处: 本文出自:103style的博客

《Android开发艺术探索》 学习记录


可以带着以下问题来看本文:

  • View的坐标系和坐标,平移等动画改变的是什么属性?
  • View有哪些事件?
  • 如果获取系统可识别的最短滑动距离?
  • 如果计算滑动的速度?
  • 单击、双击、长按等事件的监听?
  • 弹性滑动的实现?

目录

  • View 与 ViewGroup
  • View 的位置参数
  • MotionEvent 和 TouchSlop
  • VelocityTracker
  • GestureDetector
  • Scroller

View与ViewGroup

View

public class View extends Object implements Drawable.Callback,KeyEvent.Callback,AccessibilityEventSource java.lang.Object    ↳ android.view.View Known direct subclasses AnalogClock , ImageView,KeyboardViewMediaRouteButtonProgressBar , Space , SurfaceView , TextViewTextureViewViewGroup,ViewStub. Known indirect subclasses AbsListViewAbsSeekBarAbsSpinner , AbsoluteLayoutAutoCompleteTextViewButtonCalendarViewCheckBoxCheckedTextViewChronometer, and 57 others..

ViewGroup

public abstract class ViewGroup extends View implements ViewParent, ViewManager java.lang.Objectandroid.view.Viewandroid.view.ViewGroup Known direct subclasses AbsoluteLayout, AdapterView<T extends Adapter>, FragmentBreadCrumbs, FrameLayout, GridLayout, LinearLayout, RelativeLayout, SlidingDrawer, Toolbar, TvView. Known indirect subclasses AbsListView, AbsSpinner, CalendarView, DatePicker, ExpandableListView, Gallery, GridView, HorizontalScrollView,ImageSwitcher, and 26 others.

通过上面的官方介绍,我们可以看到,View 是我们平常看到的视图上所有元素的父类,按钮Button、文本TextView、图片ImageView 等。 ViewGroup 也是 View 的子类,ViewGroup 相当与 View 的容器,可以包含很多的 View.


View的位置参数

View的坐标系如下图:

View坐标系

左上角为原点O(0,0),X、Y轴分别向右向下递增。 图中 View 和 ViewGroup 的位置由其四个顶点决定,以View为例,分别对应四个属性:LeftTopRightBottom. 所以 Width = Right - Left, Height = Bottom - Top.

Android 3.0 开始,View又增加了 xytranslationXtranslationY 四个参数。 xy 即为上图中的A点,分别对应A点在View坐标系中的X、Y轴上的坐标。 translationXtranslationY则为相对于父容器ViewGroup的偏移量,默认为 0。 他们的关系为: x = left + tranlastionXy = 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.OnGestureListenerGestureDetector.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 = 0DOWN 事件 action = 1UP 事件 action = 2MOVE 事件

运行程序,我们执行一次单击,一次长按单击,然后双击一次,发下打印日志如下:

//第一次单击
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

通过上面的日志信息我们可以知道 : 一次 单击长按单击 操作会触发 onDownonShowPressonLongPress三个回调。 双击 操作则会依次触发 onDownonShowPressonDownonShowPressonLongPress 五次回调。

显示单击出现 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

我们可以看到现在一次单击则会触发onDownonSingleTapUponSingleTapConfirmed 这三个回调。 一次长按单击则会触发onDownonShowPressonSingleTapUponSingleTapConfirmed 这四个回调。 一次双击则会一次触发onDownonSingleTapUponDoubleTaponDoubleTapEventonDownonDoubleTapEvent 这六个回调。

而我们在屏幕上快速滑动时,则会触发 onDownonShowPressonScrollonScrollonFling这五个回调,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的 scrollToscrollBy 方法进行滑动时,滑动时瞬间完成的,没有过渡效果使得用户体验不好,这个时候就可以使用 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

Scroll


如果觉得不错的话,请帮忙点个赞呗。

以上


扫描下面的二维码,关注我的公众号 Android1024, 点关注,不迷路。

Android1024

`