安卓进阶之光---View体系与自定义体系---上

一级目录

二级目录

三级目录

Android的View体系都基于View和ViewGroup两个大类,同时ViewGroup又是View的子类。其结构设计基于“组合模式”,ViewGroup是容器,View是叶子节点,这就意味着ViewGroup中既可以包含View,又可以包含ViewGroup,但是反之则不行,这就保证依赖关系的安全。

1.View与ViewGroup

请添加图片描述
view的树状结构图

2.坐标系

Android 系统中有两种坐标系,分别为Android坐标系和View坐标系。

2.1Android坐标系

在这里插入图片描述

2.2View坐标系

在这里插入图片描述
View坐标系图
1.View获取自身的宽和高
getHeigh() 获取view的高度
和 getWidth()获取view的宽度
两个方法,用的还是图中的结论

public final int getHeight(){
	return mBottom - mTop;
}
public final int getWidth(){
	return mRight;
}

2.view获取自身的坐标
getTop(): 获取View自身顶边到其父布局顶边的距离。
getLeft():获取View自身左边到其父布局顶边的距离。
getRight():获取View自身右边到其父布局顶边的距离。
getBottom():获取View自身底边到其父布局顶边的距离。

3.MotionEvent提取的方法
getX(): 获取点击事件距离控件左边的距离,即视图坐标。
getY(): 获取点击事件距离控件顶边的距离,即视图坐标。
getRawX(): 获取点击事件距离整个屏幕左边的距离,即绝对坐标。
getRawY(): 获取点击事件距离整个屏幕顶边的距离,即绝对坐标。

3.View的滑动

原理: 当点击事件传到View时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改View的坐标。以下是6种滑动方法:

3.1layout的方法

原理 :在绘制view的时候会调用onLayout方法来设置显示的位置
应用:

  1. 在onTouchEvent方法中获取触摸点的坐标。

  2. 在ACTION_MOVE事件中计算偏移量,再调用layout方法重新设置这个自定义View的位置。

  3. 在每次移动时都会调用layout方法对屏幕重新布局,从而达到移动View的效果。

public class CustomView extends androidx.appcompat.widget.AppCompatButton {
    
    

    public static final String TAG = "CustomView";

    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    
    
        super(context, attrs, defStyleAttr);
    }

    public CustomView(Context context, @Nullable AttributeSet attrs) {
    
    
        super(context, attrs);
    }

    public CustomView(Context context) {
    
    
        super(context);
    }

    private int lastY;

    private int lastX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    
    
        //获取手指触摸点的横坐标和纵坐标
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()){
    
    
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "onTouchEvent: ACTION_DOWN222");
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "onTouchEvent: ACTION_MOVE1111111");
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                //调用layout方法来重新放置它的位置
                layout(getLeft()+offsetX,getTop()+offsetY,
                        getRight()+offsetX, getBottom()+offsetY);
                break;
            case  MotionEvent.ACTION_UP:
                Log.d(TAG, "onTouchEvent: ACTION_UP333");
                //这里就可以证明这个layout方法是把bottom本身的属性一起变更的,不是只是动画变更了
                break;
        }
        return true;
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.example.jinjie_viewtest01.CustomView
        android:id="@+id/buttom"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:background="@color/black"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

3.2动画

使用:在res目录中新建anim文件夹并创建translate.xml.最后使用loadAnimation(this,R.anim.translate)

缺点:View动画并不能改变View的位置参数。

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
<!--    android:fillAfter="true" 是动画完成之后是否保持当前状态,默认为false-->
    <translate
        android:duration="2000"
        android:fromXDelta="0"
        android:repeatCount="3"
        android:toXDelta="300"/>
<!--    android:duration="2000" 这是动画的持续时间
        android:repeatCount="3" 动画的重复次数
        android:fromXDelta="0"  android:toXDelta="300" 沿着X轴向右平移300像素-->
</set>
//这是动画的添加,表示向右移动了300像素,但是这个buttom的点击事件还是在原地
customView.setAnimation(AnimationUtils.loadAnimation(this, R.anim.translate));

3.2.1scrollTo 与 scrollBy

scrollTo(x,y): 表示移动到一个具体的坐标点

scrollBy(dx,dy): 表示移动的增量为dx,dy

3.2.2Scroller

Scroller本身是不能实现View的滑动的,它需要与View的computeScroll方法配合才能实现弹性滑动的效果。
系统在绘制View的的时候在draw方法中调用该方法,在这个方法中,我们调用父类的scrollTo方法并通过Scroller来不断获取当前的滑动值。每滑动一小段距离,我们就调用invalidate方法不断的进行重绘。重绘又会调用computeScroll方法,这样就连贯起来了。
后面会讲解源码分析
https://juejin.cn/post/6997254031735259172#heading-2

4.属性动画

5.源码解析Scroller

6.View的事件分发机制

6.1源码解析Activity的构成

在Activity中都有onCreate方法用于进行初始化设置,还记得其中的setContentView(R.layout.activity_main);嘛?这就是我们设置xml布局的地方,也是我们分析的入口。

6.1.1Activity 的 setContentView 方法

进入setContentView

@Override
public void setContentView(@LayoutRes int layoutResID) {
    
    
    getDelegate().setContentView(layoutResID);
}

再进到getDelegate()下的setContentView,会发现是一个抽象方法,这么快就断了线索吗?一回想,代理这个词,那就去找具体实现这个方法的对象吧。
其实在抽象方法的注释上也表明了要去哪里寻找:

/**
 * Should be called instead of {@link Activity#setContentView(int)}}
 */
public abstract void setContentView(@LayoutRes int resId);

由于新建的Activity最终都是继承于Acivity类的,所以我们在Activty类中找到 setContentView 方法实现:

// Activity的setContentView方法
public void setContentView(@LayoutRes int layoutResID) {
    
    
    // 获取Window,设置布局 --> 分析2.从哪里获取的Window,怎么样设置的布局
    getWindow().setContentView(layoutResID);
    // 初始化ActionBar
    initWindowDecorActionBar();
}

// 在Activity中有三个重载函数用于在不同的情况下创建布局:
// setContentView(@LayoutRes int layoutResID)
// setContentView(View view)
// setContentView(View view, ViewGroup.LayoutParams params)

可以看到在Activity的setContentView方法中有两步操作,从字面上的意思来看:

获取Window,设置布局
初始化ActionBar

这里就有两个疑问了:

获取了什么Window?
初始化的ActionBar是哪里的,Decor是啥?

6.1.2从哪里获取的Window,怎么样设置的布局

通过代码追踪我们发现getWindow方法返回了一个mWindow对象:

public Window getWindow() {
    
    
    return mWindow;
}

那么这个mWindow实例是从哪创建的呢?通过搜索我们发现这是一个PhoneWindow:

// mWindow是一个PhoneWindow的实例对象
mWindow = new PhoneWindow(this, window, activityConfigCallback);

这样我们就知道是通过PhoneWindow来进行布局的设置的,那么我们进到PhoneWindow当中,搜索一下setContentView方法:

// PhoneWindow中的setContentView方法,省略无关的代码
@Override
public void setContentView(int layoutResID) {
    
    
    // 判断内容父布局是否为空,如果为空则创建DecorView --> 分析3.创建DecorView的过程
    if (mContentParent == null) {
    
    
        installDecor();
    } 
    // 将布局添加到内容父布局当中去 --> 展示过程
    mLayoutInflater.inflate(layoutResID, mContentParent);
    // 此外通过其它重载方法进行布局的设置会调用
    // mContentParent.addView(view, params);方法
    // 原理是一样的,只不过所获的参数不同需要通过不同的方式进行展示
    // 因为最终都会通过addView(view, params)方法进行添加,展示过程将以这个方法作为入口
    
    ···
}

6.1.3创建DecorView的过程

进入到installDecor方法中,来看一下DecorView这个东西具体是怎么被创建出来的。

// 省略无关代码,只关注DecorView的创建过程
private void installDecor() {
    
    
    // 判断DecorView是否为空,为空则创建DecorView
    if (mDecor == null) {
    
    
        // 创建DecorView
        mDecor = generateDecor(-1);
        mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
        mDecor.setIsRootNamespace(true);
        if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
    
    
            mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
        }
    } 
    // 不为空,则将DecorView依附到PhoneWindow上
    else {
    
    
        mDecor.setWindow(this);
    }
    
    // 如果内容父布局为空,则根据DecorView生成布局
    if (mContentParent == null) {
    
    
        // 获取内容布局
        mContentParent = generateLayout(mDecor);

        ···
    }
}

这里有两个方法generateDecor和generateLayout分别用来创建和生成DecorView,在这其中具体做了什么呢?
generateDecor方法,主要获取了DecorView所需要的相关参数之后,进行DecorView的创建,并返回DecorView实例


protected DecorView generateDecor(int featureId) {
    
    
    Context context;
    if (mUseDecorContext) {
    
    
        Context applicationContext = getContext().getApplicationContext();
        if (applicationContext == null) {
    
    
            context = getContext();
        } else {
    
    
            context = new DecorContext(applicationContext, getContext());
            if (mTheme != -1) {
    
    
                context.setTheme(mTheme);
            }
        }
    } else {
    
    
        context = getContext();
    }
    return new DecorView(context, featureId, this, getAttributes());
}

generateLayout方法,主要实现了DecorView中布局的创建

protected ViewGroup generateLayout(DecorView decor) {
    
    
    ···
    // 布局资源
    int layoutResource;
    // 一般情况下会获得screen_simple布局
    layoutResource = R.layout.screen_simple;
    // 并加载到DecorView中
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

    // 获取内容布局并返回
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    
    ···
    return contentParent;
}

至此,我们就获得了DecorView,那么疑问一个接着一个:DecorView中的布局是怎么样的呢?

我们找到R.layout.screen_simple所对应的文件来看看其中的布局到底是怎么样的?

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

在这里插入图片描述

6.2源码解析View的事件分发机制!

6.2.1事件分发的事件是什么?

在这里插入图片描述

6.2.2事件的一系列事是什么?

在这里插入图片描述

6.2.3事件分发的本质是什么?

就是把目前的点击事件(MotionEvent)传递,传递,传递…,最后进行消费的过程
事件是在那些对象中进行传递的?
一个点击事件的发生,事件先传递到activity中,再传到viewgroup,最终再传到view中去
在这里插入图片描述
在这里插入图片描述

6.2.4事件分发中的几个重要方法

在这里插入图片描述

6.2.5View的事件分发机制

在这里插入图片描述

  1. 首先是要说明的是,上图的分析仅仅只限于对ACTION_DOWN的分析
  2. U形图从上到下可以分为三层,分别是Activity层,ViewGroup层,和View层
  3. 事件的开始分发是从右上角的大红色的箭头开始传输的
  4. 线上的false/ture/super 分别表示的是箭头起点函数的返回值:return false/return ture/return supperxxx(调用父类的返回)
  5. 箭头指向消费,表示该事件就到此为止,不会再往下传或往上一级传
  6. 如果事件在分发的过程中一直不被消费,那么整个分发的过程看起来就是一个U形的结构
  7. 图中只是说明了activity中只有一个ViewGroup的情况,实际操作中可能会有View ViewGroup多层嵌套的情况,原理也是这个原理,大同小异。
总结一下同种分发的几条规律:
  1. 对于activity来说dispatchTouchEvent中无论是返回ture/false都会将事件消费,即不会再往下面传播,只有return super的时候才会传到 ViewGroup()
  2. 对于dispatchTouchEvent和onTouchEvent来说return false会把该事件交给父容器来处理
  3. 对于onInterceptTouchEvent来说返回false,顾名思义,该viewGroup将不会拦截该事件,这个事件就可以继续向下传播了,如果onInterceptTouchEvent返回ture就表明该ViewGroup将会拦截该事件,事件将会交给该组件的onTouchEvent来处理,事件也就不会再往下面的组件分发了
  4. 对于onTouchEvent来说对于传入他的事件,如果返回ture就代表该事件已经被消费,则流程就终止,如果返回false,那么就代表该事件将不会由他处理,将会将给父容器的onTouchEvent来处理。
  5. 每个ViewGroup每次在做分发的时候,问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理)如果要自己处理那就在onInterceptTouchEvent方法中 return true就会交给自己的onTouchEvent的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super.onInterceptTouchEvent()和return false是一样的,是不会拦截的,事件会继续往子View的dispatchTouchEvent传递。
  6. 看下ViewGroup 的dispatchTouchEvent,之前说的return true是终结传递。return false 是回溯到父View的onTouchEvent,然后ViewGroup怎样通过dispatchTouchEvent方法能把事件分发到自己的onTouchEvent处理呢,return true和false 都不行,那么只能通过Interceptor把事件拦截下来给自己的onTouchEvent,所以ViewGroup dispatchTouchEvent方法的super默认实现就是去调用onInterceptTouchEvent
  7. 那么对于View的dispatchTouchEvent return super.dispatchTouchEvent()的时候呢事件会传到哪里呢,很遗憾View没有拦截器。但是同样的道理return true是终结。return false 是回溯会父类的onTouchEvent,怎样把事件分发给自己的onTouchEvent处理呢,那只能return
    super.dispatchTouchEvent,View类的dispatchTouchEvent()方法默认实现就是能帮你调用View自己的onTouchEvent方法的。

6.2.6点击事件的分发传递

举个例子,在金庸的《倚天屠龙记》中,武当派实力强劲,按照身份和实力区分,分别是武当掌门张三丰、武当七侠、武当弟子。这时突然有一个敌人来犯,这个消息首先会汇报给武当掌门张三丰。张三丰当然不会亲自出马,因此他就将应战的任务交给武当七侠之一的宋远桥(onInterceptTouchEvent()返回false);宋远桥威名远扬,也不会应战,因此他就把应战的任务交给武当弟子宋青书(onInterceptTouchEvent()返回 false);武当弟子宋青书没有手下,他只能自己应战。在这里我们把武当掌门张三丰比作顶层 ViewGroup,将武当七侠之一的宋远桥比作中层ViewGroup,武当弟子宋青书比作底层View。那么事件的传递流程就是:武当掌门张三丰(顶层ViewGroup)→武当七侠宋远桥(中层ViewGroup)→武当弟子宋青书(底层View)。因此得出结论,事件由上而下传递返回值的规则如下:为true,则拦截,不继续向下传递;为false,则不拦截,继续向下传递。

接下来讲解点击事件由下而上的传递。当点击事件传给底层的 View 时,如果其onTouchEvent()方法返回true,则事件由底层的View消耗并处理;如果返回false则表示该View不做处理,则传递给父View的onTouchEvent()处理;如果父View的onTouchEvent()仍旧返回false,则继续传递给该父View的父View处理,如此反复下去。

再返回上面武侠的例子。武当弟子宋青书发现来犯的敌人是混元霹雳手成昆,他打不过成昆(onTouchEvent()返回 false),于是就跑去找宋远桥,宋远桥来了,发现自己也打不过成昆(onTouchEvent()返回 false),就去找武当掌门张三丰,张三丰用太极拳很轻松地打败了成昆(onTouchEvent()返回true)。因此得出结论,事件由下而上传递返回值的规则如下:为true,则处理了,不继续向上传递;为false,则不处理,继续向上传递。

猜你喜欢

转载自blog.csdn.net/qq_65337539/article/details/130206479