展开说说: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

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

相关推荐
__water8 分钟前
RHA《Unity兼容AndroidStudio打Apk包》
android·unity·jdk·游戏引擎·sdk·打包·androidstudio
一起搞IT吧2 小时前
相机Camera日志实例分析之五:相机Camx【萌拍闪光灯后置拍照】单帧流程日志详解
android·图像处理·数码相机
浩浩乎@3 小时前
【openGLES】安卓端EGL的使用
android
Kotlin上海用户组4 小时前
Koin vs. Hilt——最流行的 Android DI 框架全方位对比
android·架构·kotlin
zzq19965 小时前
Android framework 开发者模式下,如何修改动画过度模式
android
木叶丸5 小时前
Flutter 生命周期完全指南
android·flutter·ios
阿幸软件杂货间5 小时前
阿幸课堂随机点名
android·开发语言·javascript
没有了遇见5 小时前
Android 渐变色整理之功能实现<二>文字,背景,边框,进度条等
android
没有了遇见6 小时前
Android RecycleView 条目进入和滑出屏幕的渐变阴影效果
android
站在巨人肩膀上的码农7 小时前
去掉长按遥控器power键后提示关机、飞行模式的弹窗
android·安卓·rk·关机弹窗·power键·长按·飞行模式弹窗