View虽不属于Android四代组件,但应用程度却非常非常广泛。在Android客户端,君所见之处皆是View。我们看到的Button、ImageView、TextView等等可视化的控件都是View,ViewGroup是View的子类因此它也是View。但是现在我们把View和ViewGroup当成两个类来看待,ViewGroup可以容纳View和ViewGroup,但View不可以再容纳其他View或ViewGroup,这种容纳的关系可以一直延伸仿佛一棵大树,从内而外有了父子关系,因此有个概念叫做ViewTree。
这篇文章一起总结一下View的基础知识:View的位置坐标、View宽高、View移动scrollTo和ScrollBy、手势追踪、显示隐藏、点击事件(包含单击事件、长按事件、双击事件)、ViewTreeObserver。
如果ViewA内部是ViewB,那么ViewA就被称为ViewB的父容器或父View。
1、View的位置坐标
位置坐标是View四个顶点相对于父容器的位置,因此View的坐标其实是相对坐标。
2、View宽高
View类中有mLeft-左、mTop-上、mRight-右、mBottom-下四个属性,分别对应左上角的横坐标、左上角的纵坐标、右下角的横坐标、右下角的纵坐标,利用View左上角有右下角两个顶点相对于父容器来计算顶点坐标到父容器的。View的宽高就是根据这四个属性计算出来的,公式如下:
Widht = mRight- mLeft;
Height = mBottom- mTop
这四个属性以及宽高都可以通过View类的get方法直接获取:以获取mLeft和高度为例看源码:
java
/**
* Return the height of your view.
*
* @return The height of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getHeight() {
return mBottom - mTop;
}
/**
* Top position of this view relative to its parent.
*
* @return The top of this view, in pixels.
*/
@ViewDebug.CapturedViewProperty
public final int getTop() {
return mTop;
}
注意这里返回的像素PX哈,如果使用dp为单位需要自行转化。
3、View移动scrollTo和ScrollBy
scrollTo和scrollBy是View类内部的两个负责实现滑动的方法,两者的区别scrollBy是基于当前View自身位置的滑动,scrollTo是基于传递参数的绝对滑动。scrollBy内部调用了scrollTo只是累加了之前已经滑动的距离。先上源码:
java
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
if (mScrollX != x || mScrollY != y) {
int oldX = mScrollX;
int oldY = mScrollY;
mScrollX = x;
mScrollY = y;
invalidateParentCaches();
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
if (!awakenScrollBars()) {
postInvalidateOnAnimation();
}
}
}
/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
总结:
两个方法都是传入2个参数x和y,scrollTo中会把x、y分别赋值给mScrollX 和mScrollY ,scrollBy方法是在mScrollX 和mScrollY基础上累加了X、Y以后再调用scrollTo方法进行移动。
这里有两个概念,View和View内部的内容,scrollTo和scrollBy只能移动View内部的内容但不能移动View分毫。
mScrollX等于View左上角横坐标-去view内部内容的左上角横坐标;mScrollY等于View右下角纵坐标-view内部内容的右下角纵坐标。所以参数有正负之分,x参数为正说明内容向左移动x为负数向右移动,y参数为正说明内容向上移动y为负数向下移动
举个例子:
textView2.scrollTo(20,0);
移动到相对于父view向左移动20PX,不累加,调用多少次也就是固定到这个位置不再移动了
textView2.scrollBy(-20, 0);
通过源码可知:移动到相对于父view向右移动20PX,内部调用的scrollTo移动都是先用当前位置加上要移动的距离,逐渐累加的,移动次数越多走的越远。
4、显示隐藏
编码过程中经常会遇到View的显示隐藏,但是这个很简单只需要调用setVisibility方法并传入对应的int参数。看一下源码:
java
*
* @param visibility One of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
* @attr ref android.R.styleable#View_visibility
*/
@RemotableViewMethod
public void setVisibility(@Visibility int visibility) {
setFlags(visibility, VISIBILITY_MASK);
}
四种int的flag, VISIBLE 、 INVISIBLE 、 GONE 、 VISIBILITY_MASK:
public static final int VISIBLE = 0x00000000; 设置View可见,正常显示
public static final int INVISIBLE = 0x00000004; 设置View不可见,不显示但会正常保留它的位置给它。
public static final int GONE = 0x00000008; 设置View不可见,不显示并且保留它的位置,给其他view占用。
5、手势追踪(包含单击事件、长按事件、双击事件)
GestureDetector用于辅助检测用户的单机、滑动、长按、双击等事件。
GestureDetector是一个类,它内部其中包含了两个接口OnGestureListener
和OnDoubleTapListener是分别可以监听单击、长按和双击事件。
有一点需要注意GestureDetector的构造方法形参只有OnGestureListener没有OnDoubleTapListener,因此我们如果要接收双击事件需要先通过OnGestureListener创建GestureDetector实例,然后再调用setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener)方法。
第一步 在布局文件写个控件,这里选用TextView,然后给他设置clickable和onTouchListener;所在Activity要先实现View.OnTouchListener并重写onTouch方法
java
<TextView
android:id="@+id/gestureAct_content"
android:layout_width="300dp"
android:layout_height="400dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:background="@mipmap/ic_launcher"/>
第二步 创建GestureDetector实例并设置双击事件setOnDoubleTapListener
使用不复杂,直接上代码,通过代码注释和打印日志看一下:
java
package com.example.testdemo.activity;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
import com.example.testdemo.R;
import com.example.testdemo.base.BaseActivity;
public class GestureDetectorActivity extends BaseActivity implements View.OnTouchListener{
private TextView contentTv;
private GestureDetectorActivity mThis;
private GestureDetector mGestureDetector;
@Override
public void initView() {
super.initView();
setContentView(R.layout.activity_gesture);
mThis = this;
mGestureDetector = new GestureDetector(this, new SingleGestureListener());
contentTv = findViewById(R.id.gestureAct_content);
contentTv.setOnTouchListener(this);
contentTv.setFocusable(true);
contentTv.setClickable(true);
contentTv.setLongClickable(true);
mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
// 单击事件。用来判定该次点击是SingleTap而不是DoubleTap,如果连续点击两次就是DoubleTap手势,只有单击时才执行,是一个ACTION_DOWN事件
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.e("SingleGestureListener", "onSingleTapConfirmed ACTION= "+MotionEvent.actionToString(e.getAction()));
return false;
}
//被系统认定为双击事件时执行,是个DOWN事件
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.e("SingleGestureListener", "onDoubleTap ACTION= "+MotionEvent.actionToString(e.getAction()));
return false;
}
//第二次点击屏幕被系统认定时双击以后到双击操作完成,双指触发onDoubleTap以后,包含ACTION_DOWN、ACTION_MOVE、ACTION_UP事件
@Override
public boolean onDoubleTapEvent(MotionEvent e) {
Log.e("SingleGestureListener", "onDoubleTapEvent ACTION= "+MotionEvent.actionToString(e.getAction()));
return false;
}
});
}
@Override
public boolean onTouch(View v, MotionEvent event) {
return mGestureDetector.onTouchEvent(event);
}
private class SingleGestureListener implements GestureDetector.OnGestureListener{
// 点击屏幕时ACTION_DOWN事件触发,只要点一定执行
public boolean onDown(MotionEvent e) {
Log.e("SingleGestureListener", "onDown ACTION= "+MotionEvent.actionToString(e.getAction()));
return false;
}
/*
* 一般来说点击屏幕onDown的那个ACTION_DOWN事件也会过来。但有两种情况例外,点了以后不松手和点了以后直接滑动不会执行该事件;点了不松手会执行onLongPress,点了以后进行滑动会执行onScroll。
*/
public void onShowPress(MotionEvent e) {
Log.e("SingleGestureListener", "onShowPress ACTION= "+MotionEvent.actionToString(e.getAction()));
}
// 用户点击屏幕并马上松开,由一个1个MotionEvent ACTION_UP触发,就是一个标准点击事件,同上,点了以后不松手和点了以后直接滑动不会执行该事件
public boolean onSingleTapUp(MotionEvent e) {
Log.e("SingleGestureListener", "onSingleTapUp ACTION= "+MotionEvent.actionToString(e.getAction()));
return true;
}
// 点击屏幕后滑动,e1是 ACTION_DOWN触发, e2是ACTION_MOVE
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
Log.e("SingleGestureListener", "onScroll:"+(e2.getX()-e1.getX()) +" "+distanceX+" e1_ACTION= "+MotionEvent.actionToString(e1.getAction())+" e2_ACTION= "+MotionEvent.actionToString(e2.getAction()));
return true;
}
// 长按屏幕触发,ACTION_DOWN事件
public void onLongPress(MotionEvent e) {
Log.e("SingleGestureListener", "onLongPress ACTION= "+MotionEvent.actionToString(e.getAction()));
}
// 用户按下触摸屏、快速移动后松开,e1是 ACTION_DOWN, e2s是个ACTION_UP触发
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
float velocityY) {
Log.e("SingleGestureListener", "onFling"+" e1_ACTION= "+MotionEvent.actionToString(e1.getAction())+" e2_ACTION= "+MotionEvent.actionToString(e2.getAction()));
return true;
}
};
}
日志打印:
单击:点击屏幕后马上抬起
2024-07-13 21:23:09.356 19222-19222/com.example.testdemo E/SingleGestureListener: onDown ACTION= ACTION_DOWN
2024-07-13 21:23:09.404 19222-19222/com.example.testdemo E/SingleGestureListener: onSingleTapUp ACTION= ACTION_UP
2024-07-13 21:23:09.657 19222-19222/com.example.testdemo E/SingleGestureListener: onSingleTapConfirmed ACTION= ACTION_DOWN
长按屏幕:长按以后执行,并且松开后也不会再执行其他的回调了,因为他是一个 ACTION_UP 事件
2024-07-13 21:23:17.696 19222-19222/com.example.testdemo E/SingleGestureListener: onDown ACTION= ACTION_DOWN
2024-07-13 21:23:17.788 19222-19222/com.example.testdemo E/SingleGestureListener: onShowPress ACTION= ACTION_DOWN
2024-07-13 21:23:18.089 19222-19222/com.example.testdemo E/SingleGestureListener: onLongPress ACTION= ACTION_DOWN
滑动屏幕
2024-07-13 21:35:08.821 21772-21772/com.example.testdemo E/SingleGestureListener: onDown ACTION= ACTION_DOWN
2024-07-13 21:35:08.866 21772-21772/com.example.testdemo E/SingleGestureListener: onScroll:7.7454224 -7.7454224 e1_ACTION= ACTION_DOWN e2_ACTION= ACTION_MOVE
2024-07-13 21:35:08.916 21772-21772/com.example.testdemo E/SingleGestureListener: onScroll:36.8945 -29.149078 e1_ACTION= ACTION_DOWN e2_ACTION= ACTION_MOVE
2024-07-13 21:35:08.966 21772-21772/com.example.testdemo E/SingleGestureListener: onScroll:60.22644 -23.33194 e1_ACTION= ACTION_DOWN e2_ACTION= ACTION_MOVE
2024-07-13 21:35:08.999 21772-21772/com.example.testdemo E/SingleGestureListener: onScroll:28.0 32.22644 e1_ACTION= ACTION_DOWN e2_ACTION= ACTION_MOVE
2024-07-13 21:35:09.027 21772-21772/com.example.testdemo E/SingleGestureListener: onFling e1_ACTION= ACTION_DOWN e2_ACTION= ACTION_UP
双击屏幕:
2024-07-13 21:35:53.532 21772-21772/com.example.testdemo E/SingleGestureListener: onDown ACTION= ACTION_DOWN
2024-07-13 21:35:53.601 21772-21772/com.example.testdemo E/SingleGestureListener: onSingleTapUp ACTION= ACTION_UP
2024-07-13 21:35:53.706 21772-21772/com.example.testdemo E/SingleGestureListener: onDoubleTap ACTION= ACTION_DOWN
2024-07-13 21:35:53.706 21772-21772/com.example.testdemo E/SingleGestureListener: onDoubleTapEvent ACTION= ACTION_DOWN
2024-07-13 21:35:53.706 21772-21772/com.example.testdemo E/SingleGestureListener: onDown ACTION= ACTION_DOWN
2024-07-13 21:35:53.770 21772-21772/com.example.testdemo E/SingleGestureListener: onDoubleTapEvent ACTION= ACTION_MOVE
2024-07-13 21:35:53.770 21772-21772/com.example.testdemo E/SingleGestureListener: onDoubleTapEvent ACTION= ACTION_UP
6、ViewTreeObserver
如果我们想在onCreate生命周期获取一个View的宽高该怎么呢?
直接getWidth()和getHeight()方法会有问题吗?没大问题,但是获取到的值都是0,为啥呢?因为测试还没完成measure绘制。因此需要注册一个监听器等view绘制完成以后再取获取宽高:
java
ViewTreeObserver viewTreeObserver = contentTv.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Log.e("initView: -1", " width="+contentTv.getWidth()+" height= "+contentTv.getHeight());
// 在这里注销监听器
// viewTreeObserver.removeOnGlobalLayoutListener(this); //This ViewTreeObserver is not alive, call getViewTreeObserver() again
contentTv.getViewTreeObserver().removeOnGlobalLayoutListener(this::onGlobalLayout);
}
});
Log.e("initView: -2", " width="+contentTv.getWidth()+" height= "+contentTv.getHeight());
注意:
1、首先看到直接打印的initView: -2只想时间早于initView: -1,其次initView: -2执行了多次,因为没有调用removeOnGlobalLayoutListener注销监听器onGlobalLayout就会被回调多次:
2024-07-13 22:37:53.641 4857-4857/com.example.testdemo E/initView: -1: width=900 height= 1200
2024-07-13 22:37:53.705 4857-4857/com.example.testdemo E/initView: -1: width=900 height= 1200
2024-07-13 22:37:55.430 5654-5654/com.example.testdemo E/initView: -2: width=0 height= 0
2024-07-13 22:37:55.599 5654-5654/com.example.testdemo E/initView: -1: width=900 height= 1200
2、其次如果viewTreeObserver.removeOnGlobalLayoutListener(this); 这样注销会引发crash闪退:This ViewTreeObserver is not alive, call getViewTreeObserver() again。所以,哟啊重新获取getViewTreeObserver对象,contentTv.getViewTreeObserver().removeOnGlobalLayoutListener(this::onGlobalLayout);就可以成功获取宽高并且不会执行多次,日志如下:
2024-07-13 22:48:05.698 9659-9659/com.example.testdemo E/initView: -2: width=0 height= 0
2024-07-13 22:48:05.868 9659-9659/com.example.testdemo E/initView: -1: width=900 height= 1200
才疏学浅,如有错误,欢迎指正,多谢。