Android 各种坐标彻底明了

前言

 

本篇文章的重点是分析由Layout摆放位置引起的坐标相关知识分析。

 

通过本篇文章,你将了解到:

1、View坐标基础
2、常用的View获取坐标方法

 

1

View坐标基础

可以看出与传统的坐标系有所不同的是,屏幕的左上角作为坐标原点,X轴向右为正,Y轴向下为正。

 

上图分为四个层次,由内到外依次为:

 

  • 第一层为触摸点MotionEvent。

  • 第二层为View,作为第三层的子布局。

  • 第三层为Window/RootView(根View的尺寸与Window尺寸一致),作为第二层的父布局,也是作为整个ViewTree的根布局。

  • 第四层为屏幕。

 

触摸点(MotionEvent)坐标

 

上图红色部分为触摸点坐标相关的。

 

  • getX()、getY()获取的坐标是相对于其所在的View,相对于View的左上角。

     

  • getRawX()、getRawY()获取的坐标是相对于整个屏幕的,相对于屏幕的左上角。

 

对于同一个坐标点(getRawX()/getRawY()相同),在不同的View里,getX()、getY()可能不同:

黑色箭头是触摸点在View1里的getX()/getY()。


蓝色箭头是触摸点在View2里的getX()/getY()。

 

View 坐标

 

在Layout阶段会计算:mLeft、mTop、mRight、mBottom的值,也就是确定了该View的四个顶点距离父布局的左上角的偏移。这些值可正可负,以X轴为例,如果View的顶点在父布局左上角的右侧,即为正,否则为负。

 

这四个值用来确定View在父布局内的摆放位置。


接下来引入两个经典问题:


1、这四个值由什么决定的?


我们知道Measure过程计算了View长、宽,以LinearLayout为例,看看其如何摆放子布局的。

上图是纵向的LinearLayout,其内部有两个子布局:View1、View2。


LinearLayout布局过程如下:

 

1、检测到LinearLayout设置了纵向mPaddingTop,此时View1.mTop = mPaddingTop。


2、View1的底部距离父布局为:mBottom=mTop+View1.height。


3、View2设置了margin(距离View1),View2的顶部距离父布局为:mTop=View1.mbottom+margin。

 

由上可知:

 

View的四个顶点的值是相对于其直接父布局的左上角来计算的,用来指示该View在其布局内的位置。


会受到父布局设置的padding,View 设置的margin、gravity、View本身尺寸等影响。也就是说当我们设置这些值时候,最终反馈到四个顶点的值上。

 

获取与设置四个顶点的值方法:

 

//获取

    {
        getLeft();
        getTop();
        getRight();
        getBottom();
    }

//设置
    {
        layout(left, top, right, bottom);
    }

 

既然知道了四个顶点的值会影响View绘制的范围,也就是Canvas绘制范围,那么引出下面问题:


2、还有什么能够影响Canvas绘制范围

想想在不改变View四个顶点值的情况下,如何让View1向下移动。


你可能会说:设置View margin、设置ViewGroup padding等。上面有提到过这些值最终都是反馈到View的四个顶点的值上,该答案不符合题意。

 

 

换个角度想:分别从View和ViewGroup考虑。


从View的角度想:

 

#View.java

    public void setTranslationY(float translationY) {...}
    public void setTranslationX(float translationX) {...}

    public void setX(float x){...};
    public void setY(float y){...};

 

从ViewGroup角度想:

 

#View.java

    public void scrollTo(int x, int y) {...}
    public void scrollBy(int x, int y) {...}
    public void setScrollX(int value) {...}
    public void setScrollY(int value){...}

 

以纵轴(Y)的移动为例,分别看看以上方法是如何工作的。


先看View角度的:

 

setTranslationY(xx)

 

#View.java

    public void setTranslationY(float translationY) {
        //translationY 为正,往下移动,为负往上移动
        //设置的值与当前值不一样则认为是有效设置
        if (translationY != getTranslationY()) {
            invalidateViewProperty(true, false);
            //记录到renderNode里
            mRenderNode.setTranslationY(translationY);
            //触发invalidate->三大流程
            invalidateViewProperty(false, true);

            invalidateParentIfNeededAndWasQuickRejected();
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
    }

 

看到这可能比较疑惑,虽然是设置到了RenderNode里,什么时候拿出这个值以及什么地方使用呢?

 

1、对于支持硬件加速来说,RenderNode变化了,相应的Canvas绘制范围也会变化。


2、对于不支持硬件加速来说,将会在View.draw(x1,x2,x3)方法里获取matrix,从而移动Canvas。

 

 

#View.java

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ...
        //设置了setTranslationY()后,实际上就是给Canvas设置了偏移
        //因此View的matrix不再是单位矩阵
        final boolean childHasIdentityMatrix = hasIdentityMatrix();
        if (!childHasIdentityMatrix && !drawingWithRenderNode) {
            canvas.translate(-transX, -transY);
            //matrix操作,getMatrix() 会影响canvas坐标
            canvas.concat(getMatrix());
            canvas.translate(transX, transY);
        }
        ...
    }

 

可以看出View.setTranslationY()最终是移动了Canvas的坐标(平移),最终使得View移动了。

 

setY(xx)

 

先看View.getY(xx)

 

#View.java

    public float getY() {
        return mTop + getTranslationY();
    }

 

明显的,getY()获取的就是mTop顶点的值+translationY的值,因此:

 

#View.java

    public void setY(float y) {
        setTranslationY(y - mTop);
    }

 

实际上也就是设置了translationY的值。

 

再看ViewGroup角度的:

 

scrollTo(xx)

 

#View.java

    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();
            }
        }
    }

 

来看看mScrollX、mScrollY 什么时候使用的:


一、对于开启了硬件加速的View来说:


在该方法里启用:

 

#View.java

    public RenderNode updateDisplayListIfDirty() {
        ...
        try {
            if (layerType == LAYER_TYPE_SOFTWARE) {
                ...
                //软件绘制缓存
            } else {
                computeScroll();

                //mScrollX,mScrollY 在此处使用
                //将Canvas进行平移,注意此处是取反
                canvas.translate(-mScrollX, -mScrollY);
                mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;

                ...
                //分发Draw事件
            }
        } finally {
            //结束录制
            renderNode.endRecording();
            setDisplayListProperties(renderNode);
        }
        ...
    }

 

二、对于关闭了硬件加速的View来说:

 

#View.java

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ...
        if (!drawingWithRenderNode) {
            computeScroll();
            //记录值
            sx = mScrollX;
            sy = mScrollY;
        }

        final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
        final boolean offsetForScroll = cache == null && !drawingWithRenderNode;
        ...
        if (offsetForScroll) {
            //走软件绘制分支
            //平移Canvas,此处取反了
            canvas.translate(mLeft - sx, mTop - sy);
        } else {
            ...
        }
        ...
    }

 

结合上述对mScrollX,mScrollY引用,我们知道这两个值的设置最终影响到了Canvas坐标,并且进行了取负。


而我们知道Canvas.translate(float dx, float dy),对于纵向来说,当dy>0,Canvas向下平移,产生的效果是View向下移动了。


当mScrollY>0时,因为取反的缘故,因此Canvas向上平移,产生的效果是View向上移动了。

 

scrollBy(xx), setScrollY(xx) 内部实际上就是调用了scrollTo(xx),此处不再分析。

这就是为什么明明设置scroll值为正却向相反方向移动的原因

 

 

以上分别从View、ViewGroup角度分析了如何移动View。


它们的异同点:

 

1、都是通过平移Canvas坐标达到移动的效果
2、都是没有改变View四个顶点的坐标值
3、只是View.setTranslationY(100) 使得View沿着纵轴向下移动。而ViewGroup. scrollTo(0, 100),使得其子布局沿着纵轴向上移动

 

以上分析我们知道要移动View,本质上就是对其Canvas进行操作,而Canvas没有提供外部直接操作的方法,因此通过曲线救国,总结出移动View的方法:

 

1、改变View四个顶点的值
2、设置translationX(xx)、translationY(xx)
3、设置Scroll(待移动View的父布局)
4、View动画

 

2

常用的View获取坐标方法

 

了解了View坐标基础,再来看看由此引出的其它属性,如:


获取View的可见区域,获取View在屏幕上的位置等。


涉及到方法如下:

 

public void getLocationInWindow(@Size(2) int[] outLocation){...}

public void getLocationOnScreen(@Size(2) int[] outLocation){...}

public final boolean getGlobalVisibleRect(Rect r){...}
public final boolean getLocalVisibleRect(Rect r){...}

public void getHitRect(Rect outRect){...}
public void getDrawingRect(Rect outRect){...}

public void getWindowVisibleDisplayFrame(Rect outRect);

 

如上图所示,绿色为ViewGroup1,它作为Window的RootView,此时的Window尺寸大小与RootView大小一致。


蓝色为ViewGroup2,作为ViewGroup1子布局,同时作为ViewGroup2的父布局。


红色+白色为View,作为ViewGroup2的子布局。

 

分两种情况说明:

 

a、子布局可以超过父布局展示

 

设置View不被父布局clip,如上图所示,为简单起见,只以水平方向为例分析。
前提条件

 

View 长宽:

width:600
height:200

View 四个顶点:
mLeft = -200
mTop = 200
mRight = 400
mBottom = 400;

 

结合上图来看,View不仅超出了父布局,也超出了Window,白色部分为超过Window的区域,是看不到的。


分别来看看各个方法获取的坐标值:

 

getLocationInWindow

 

View距离Window左上角坐标,因为View超出了Window,因此获取的坐标为

 

[x,y]=[-100,400]

 

getLocationOnScreen

 

View距离屏幕左上角的坐标,在getLocationInWindow 基础上加上Window 的偏移。

 

[x,y]=[-100, 400] + [200, 100] = [100, 500]

 

getGlobalVisibleRect

 

View的可见部分在Window里的区域,View的真实区域:白色 + 红色 部分,只是白色部分超出了Window,不会展示,可见区域是红色部分。

 

rect=[0, 400, 500, 600]

 

注意:此处是相对于Window左上角计算的区域,而非屏幕。网上很多文章分析是针对Activity的Window,由于此时Window大小与屏幕尺寸一致,因此会误认为getGlobalVisibleRect是相对屏幕左上角计算的。

 

关于Window/RootView尺寸如何测量请移步:

Android Window 如何确定大小/onMeasure()多次执行原因

https://www.jianshu.com/p/6e45f42da304

 

getLocalVisibleRect

 

View可见部分相对于自身的区域,也就是说自身的哪些区域可见。在getGlobalVisibleRect基础上,不断查找。

 

rect=[100, 0, 600, 200]

 

getHitRect

 

获取View有效的点击区域,以四个顶点为基础,考虑matrix,得出结果如下:

 

rect=[-200,200,400, 400]

 

getDrawingRect

 

获取View的绘制区域,以四个顶点为基础,考虑scroll值,得出结果如下:

 

rect=[0,0,600,200]

 

getWindowVisibleDisplayFrame

 

Window 的可见区域,一般用来计算导航栏、状态栏、键盘高度:


具体可移步:

Android 软键盘一招搞定(原理篇)

https://www.jianshu.com/p/5093f9fd57c8

 

再来看另外一个情况:

 

b、子布局不可以超过父布局展示

 

当子布局被父布局clip时(默认状态),效果图如下:

 

如上图,白色+红色部分为View的区域,只是白色部分由于超过了其父布局:ViewGroup2,因此不会展示。

 

与 a 场景相比,显然是View的可见部分发生变化,因此我们重点关注:


getGlobalVisibleRect 与getLocalVisibleRect 的变化:

 

getGlobalVisibleRect

 

可以看出,红色部分为可见区域,那么该区域相对Window左上角的距离为:


rect=[100,400,500,600]

 

getLocalVisibleRect

 

红色部分在View自身里的区域


rect=[200,0,600,200]

 

关于View可见与可视区域

 

想要隐藏一个View,通过设置View.setVisibility(VISIBLE);


判断一个View是否隐藏:getVisibility() == VISIBLE


显然这个判断不是那么的完善,试想一下:隐藏与显示仅仅只是View的状态而已,如果其父布局状态为GONE,此时View状态为VISIBLE,判断出来View是可见的,但是实际上却是看不到。


解决方法是:从View开始,不断向上寻找父布局,查找其状态是否是VISIBLE,若不是则认为该View不可见。当然,SDK里已经提供该方法。

 

#View.java

public boolean isShown() {...}

 

再想另一个问题,isShown()判断的仅仅是状态是否可见。当该View超出了Window、或者屏幕,纵然isShown()==true,对用户来说依然是不可见的,此时就需要使用getGlobalVisibleRect(xx)判断了。

 

3

小结

 

以上方法源码都比较简单,都是以四个顶点为基础,有些方法里会考虑matrix变化(如setTranslationX()、setTranslationY() 导致变化)、scroll值等的影响。


通过上面的图示再结合源码对比,相信大家对上面的方法不再有疑惑。

 

本文基于Android 10.0

 


 

最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!

转自:https://www.jianshu.com/p/071e04108f4d

 

 

 

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值