展开说说:Android之View基础知识解析

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

才疏学浅,如有错误,欢迎指正,多谢。

相关推荐
周全全10 分钟前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
- 羊羊不超越 -1 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
wk灬丨1 小时前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
千雅爸爸1 小时前
Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
android
晨曦_子画2 小时前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
孤客网络科技工作室2 小时前
AJAX 全面教程:从基础到高级
android·ajax·okhttp
Mr Lee_4 小时前
android 配置鼠标右键快捷对apk进行反编译
android
顾北川_野4 小时前
Android CALL关于电话音频和紧急电话设置和获取
android·音视频
&岁月不待人&4 小时前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin