美文网首页最近需要做的
Android触摸滑动全解(三)——View坐标体系详解

Android触摸滑动全解(三)——View坐标体系详解

作者: TokyoZ | 来源:发表于2018-08-26 21:35 被阅读77次

    Android触摸滑动全解(三)——View坐标体系详解

    当我们触摸屏幕上的View时,有时候想要获取此时View的一些属性状态,比如说在屏幕的坐标,或者相对于父布局的坐标,或者View的宽高等,但是由于View有很多属性,我们很苦恼不知道应该去选择哪个方法去调用,今天,我们就梳理一下View的坐标体系。

    一、屏幕区域划分

    Android系统的屏幕区域划分如图:


    Android屏幕区域划分

    获取上述区域宽高的方法

    获取屏幕区域的宽高等尺寸获取:

    DisplayMetrics metrics = new DisplayMetrics();
    getWindowManager().getDefaultDisplay().getMetrics(metrics);
    int widthPixels = metrics.widthPixels;
    int heightPixels = metrics.heightPixels;
    

    应用程序App区域宽高等尺寸获取:

    Rect rect = new Rect();
    getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
    

    获取状态栏高度:

    Rect rect= new Rect();
    getWindow().getDecorView().getWindowVisibleDisplayFrame(rect);
    int statusBarHeight = rectangle.top;
    

    View布局区域宽高等尺寸获取:

    Rect rect = new Rect();  
    getWindow().findViewById(Window.ID_ANDROID_CONTENT).getDrawingRect(rect);
    

    二、View坐标轴

    我们平时的开发工作中,一般都是在APP的区域,因此我们比较关系的是APP部分的坐标体系。
    Android系统和我们平时接触的坐标轴不一样,它是以屏幕左上角为原点,向右为X正方向,向下为Y轴正方向,因此屏幕左上角坐标为(0,0)。


    View坐标体系

    1、View的尺寸和相对于父布局的位置

    1.1 View的位置(相对于父布局)

    1.1.1 View的初始位置(在XML中布局时的位置)

    View中有四个属性:

     protected int mLeft;
     protected int mRight;
     protected int mTop;
     protected int mBottom; 
    

    其值由layout过程的四个参数(l,t,r,b)确定。这四个参数的设置一般会参考measure过程中测量出来的值。View的四个属性值表示layout过程中确定的基本位置。含义如下图所示,坐标系是父View的视图坐标:


    View的位置

    并且有四个方法获取它们:

    • view.getLeft():View左侧到父View左侧的距离。
    • view.getRight():View右侧到父View左侧的距离。
    • view.getTop():View上侧到父View上侧的距离。
    • view.getBottom():View下侧到父View上侧的距离。

    可通过两个方法改变它们的值:

    • view.offsetLeftAndRight(int offset):改变mLeftmRight的值,offset为正View整体位置向右偏移,为负则向左偏移。
    • view.offsetTopAndBottom(int offset):改变mTopmBottom的值,offset为正View整体位置向下偏移,为负则向上偏移。
    1.1.2 获取移动后的偏移量

    View中还有两个方法可以设置View的偏移量,这两个方法可以改变当前View的位置:

    • view.setTranslationX(int offset)offset为正View整体位置向右偏移,为负则向左偏移。
    • view.setTranslationY(int offset)offset为正View整体位置向下偏移,为负则向上偏移。

    相应的获取偏移量:

    • view.getTranslationX():获取View在X轴方向的偏移量。
    • view.getTranslationY():获取View在Y轴方向的偏移量。
    1.1.3 获取View当前的位置

    View中还有两个方法可以获取View当前的位置:

    • view.getX():获取View在X轴方向的当前位置,返回值为getLeft()+getTranslationX(),当setTranslationX()getLeft()不变,getX()变。
    • view.getY():获取View在Y轴方向当前位置,返回值为getTop()+getTranslationY(),当setTranslationY()getTop()不变,getY()变。

    同样的,也可以通过setX()setY()来改变getXgetY()的值,它们相当于设置setTranslationX(int offset)setTranslationY(int offset)

    1.1.4 三种获取位置方法总结(以X轴举例)
    • view.getLeft():布局后View相对于父布局的原始距离。
    • view.getTranslationX():布局后如果View有移动,那么可通过此方法获取View移动后的偏移量。
    • view.getX():布局后如果View没有移动,那么此方法获取的值等同于getLeft(),如果View有移动,此方法获取的值等同于getLeft()+getTranslationX()

    1.2 View的尺寸

    View的尺寸也就是View的宽高,获取方法有两种:

    1.2.1 获取宽高:
    public final int getWidth() {
        return mRight - mLeft;
    }
    
    public final int getHeight() {
        return mBottom - mTop;
    }
    

    从源码中,我们可以看到,实际上View获取宽高时也就是用mRight减去mLeftmBottom减去mTop(所以说mRightmLeftmBottommTop的值永远不会改变)。

    1.2.2 获取测量的宽高:
    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }
    
    public final int getMeasuredHeight() {
        return mMeasuredHeight & MEASURED_SIZE_MASK;
    }
    

    这里mMeasuredWidth & MEASURED_SIZE_MASK表示的是测量阶段结束之后,View真实的值。而且这个值会在measure()调用了setMeasuredDimensionRaw()函数之后会被设置。所以getMeasuredWidth()的值是measure()阶段结束之后得到的view的原始的值。

    1.2.3 1和2中的两种方法比较和区别
    • 我们知道,measure()方法是在layout()方法之前调用的,因此,mMeasuredWidthmMeasuredHeight值在measure()后就被赋值,而getWidth()getHeight()的值需要在layout()之后才能得到。

    • 由1得知,getMeasuredWidth()获取的是view原始的大小,也就是这个view在XML文件中配置或者是代码中设置的大小。getWidth()获取的是这个view最终显示的大小,这个大小有可能等于原始的大小也有可能不等于原始大小。

    1.2.4 Activity中无法获取View宽高的解决办法

    Activity在onCreate()onStart()onResume()时无法获取View的宽高,解决的办法一般有如下四种:

    • onWindowFocusChanged() :

    在Activity或者View的onWindowFocusChanged()中获取,其中hasFocus表示当前窗口(Activity或者View)是否获取窗口,true表示获取:

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        L.i("onWindowFocusChanged : v_view1.getWidth():" + v_view1.getWidth()
                + "  v_view1.getHeight():" + v_view1.getHeight());
    }
    
    • view.post(runnable):

    通过post可以将一个runnable投递到消息队列的尾部,然后等待UI线程Looper调用此runnable的时候,view也已经初始化好了。

        v_view1.post(new Runnable() {
            @Override
            public void run() {
                L.i("post(Runnable) : v_view1.getWidth():" + v_view1.getWidth()
                        + "  v_view1.getHeight():" + v_view1.getHeight());
            }
        });
    
    • ViewTreeObserver:

    使用ViewTreeObserver的众多回调可以完成这个功能,比如使用OnGlobalLayoutListener这个接口,当view树的状态发生改变或者view树内部的view的可见性发生改变时,onGlobalLayout方法将被回调,因此这是获取view的宽高一个很好的时机。需要注意的是,伴随着view树的状态改变等,onGlobalLayout会被调用多次。

        v_view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                L.i("ViewTreeObserver : v_view1.getWidth():" + v_view1.getWidth()
                        + "  v_view1.getHeight():" + v_view1.getHeight());
            }
        });
    
    • view.measure(int widthMeasureSpec, int heightMeasureSpec)

    通过手动对view进行measure来得到view的宽/高,这种情况比较复杂,这里要分情况处理,根据view的layoutparams来分:
    MATCH_PARENT:直接放弃,无法measure出具体的宽/高。原因很简单,根据view的measure过程,构造此种MeasureSpec需要知道parentSize,即父容器的剩余空间,而这个时候我们无法知道parentSize的大小,所以理论上不可能测量处view的大小。
    WRAP_CONTENT

        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30)-1, View.MeasureSpec.AT_MOST);
        v_view1.measure(widthMeasureSpec, heightMeasureSpec);
    

    具体数值(比如宽高都是100dp/px):

        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
        v_view1.measure(widthMeasureSpec, heightMeasureSpec);
    

    2、View的相对屏幕的坐标

    下面我们再来看看关于View获取屏幕中位置的一些方法,不过这些方法需要在Activity的onWindowFocusChanged ()方法之后才能使用。


    View在屏幕中的坐标

    如图所示,View1(绿色)在屏幕中的左上角和右下角坐标分别是(30,100)(440,200),View2(紫色)在屏幕中的左上角和右下角坐标分别是(30,250)(440,800),其中View2可见位置的右下角坐标是(440,720)

    下面我们就给出上面这幅图涉及的View的一些坐标方法的结果(结果采用使用方法返回的实际坐标,不依赖上面实际绝对坐标转换,上面绝对坐标只是为了说明例子中的位置而已),如下:

    View的方法 View1的结果 View2的结果 结论描述
    getLocalVisibleRect() (0, 0, 410, 100) (0, 0, 410, 470) 获取View自身可见的坐标区域,坐标以自己的左上角为原点(0,0),另一点为可见区域右下角相对自己(0,0)点的坐标,其实View2当前height为550,可见height为470。
    getGlobalVisibleRect() (30, 100, 440, 200) (30, 250, 440, 720) 获取View在屏幕绝对坐标系中的可视区域,坐标以屏幕左上角为原点(0,0),另一个点为可见区域右下角相对屏幕原点(0,0)点的坐标。
    getLocationOnScreen() (30, 100) (30, 250) 坐标是相对整个屏幕而言,Y坐标为View左上角到屏幕顶部的距离。
    getLocationInWindow() (30, 100) (30, 250) 如果为普通Activity则Y坐标为View左上角到屏幕顶部(此时Window与屏幕一样大);如果为对话框式的Activity则Y坐标为当前Dialog模式Activity的标题栏顶部到View左上角的距离。

    三、View移动自身或者内容的方法

    3.1 改变自身的位置

    改变自身的位置在方法前面其实已经介绍过了,就是下面几种:

    • view.offsetLeftAndRight(int offset):水平方向挪动View,offset为正则x轴正向移动,移动的是整个View,getLeft()会变的。

    • view.offsetTopAndBottom(int offset):垂直方向挪动View,offset为正则Y轴向下移动,移动的是整个View,getTop()会变的。

    • +view.setTranslationX(int offset):水平方向挪动View,offset为正则x轴正向移动,移动的是整个View,getLeft()不会改变。

    • view.setTranslationY(int offset):水平方向挪动View,offset为正则Y轴向下移动,移动的是整个View,getTop()不会改变。

    • view.layout(int left, int top, int right, int bottom):重新布局View在父布局中的位置,此方法会改变getLeft()等方法的值。

    • LayoutParams:通过设置View的margin值来改变自身的位置:

        LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) mtv.getLayoutParams();
        layoutParams.leftMargin = 20;
        layoutParams.bottomMargin = 20;
        mtv.setLayoutParams(layoutParams);
      
    • 动画:通过设置View的margin值来改变自身的位置:

    3.2 自身内容的滚动

    滚动相关的方法只是改变View中内容的位置,而整体View在屏幕中的位置不会移动!

    • view.scrollTo(int x, int y)将View中内容(不是整个View)滑动到相应的位置,参考坐标原点为ParentView左上角,x,y表示滑动到的左上角坐标,为正则向xy轴反方向移动,反之同理。

    • view.scrollBy(int x, int y)将View中内容(不是整个View)相对滑动x,y的距离,为正则向xy轴反方向移动,反之同理。

    • view.setScrollX(int value):实质为scrollTo(int x, int y),只是改变X轴方向的内容。

    • view.setScrollY(int value):实质为scrollTo(int x, int y),只是改变Y轴方向的内容。

    • getScrollX()/getScrollY():获取当前滑动位置偏移量。

    • Scroller:通过Scroller类也可以实现View的滑动,并且Scroller效果看起来更加顺滑自然,此类我们会在后续介绍。

    scrollTo()和scrollBy()方法特别注意:如果你给一个ViewGroup调用scrollTo()方法滚动的是ViewGroup里面的内容,如果想滚动一个ViewGroup则再给他嵌套一个外层,滚动外层即可。

    四、总结

    1. 我们知道了Android中屏幕各区域的划分以及获取屏幕各区域的方法。
    2. 我们知道了View在父布局位置的布局方式以及获取位置的方法。
    3. 我们知道了View得到宽高的两种方法的异同点。
    4. 我们知道了View在屏幕中的位置以及得到屏幕中位置坐标的方法。
    5. 我们知道了改变View自身位置和内容的方法。

    参考资料

    Android应用坐标系统全面详解

    相关文章

      网友评论

        本文标题:Android触摸滑动全解(三)——View坐标体系详解

        本文链接:https://www.haomeiwen.com/subject/velaiftx.html