预备知识

  1. Android屏幕区域划分
    我们先看一副图来了解一下Android屏幕的区域划分,如下:


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

特别注意:上面这些方法最好在Activity的onWindowFocusChanged ()方法或者之后调运,因为只有这时候才是真正的显示OK。

  1. Android坐标系、View坐标系、位置的获取、距离的获取和View宽度的获取
    在Android中,将屏幕最左上角的顶点作为Android坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向。
    在Android中,将View的左上角顶点作为View坐标系的原点,从这个点向右是X轴正方向,从这个点向下是Y轴正方向。
    下面我们就来看看在上面两种坐标系下位置的获取、距离的获取和View宽度的获取 的方法。
    1中我们分析了Android屏幕的划分,可以发现我们平时开发的重点其实都在关注View布局区域,那么下面我们就来细说一下View区域常用的位置和距离。先看下面这幅图:

    通过上图我们可以很直观的给出View一些坐标相关的方法解释,不过必须要明确的是上面这些方法必须要在layout之后才有效,如下:
View的静态坐标方法 解释
getLeft() 返回View自身左边到父布局左边的距离(返回值是mLeft)
getTop() 返回View自身顶边到父布局顶边的距离(返回值是mTop)
getRight() 返回View自身右边到父布局左边的距离(返回值是mRight)
getBottom() 返回View自身底边到父布局顶边的距离(返回值是mBottom)
getX() 返回值为getLeft()+getTranslationX(),当setTranslationX()时getLeft()不变,getX()变。
getY() 返回值为getTop()+getTranslationY(),当setTranslationY()时getTop()不变,getY()变。

同时也可以看见上图中给出了手指触摸屏幕时MotionEvent提供的一些方法解释,如下:

MotionEvent坐标方法 解释
getX() 当前触摸事件距离当前View左边的距离
getY() 当前触摸事件距离当前View顶边的距离
getRawX() 当前触摸事件距离整个屏幕左边的距离
getRawY() 当前触摸事件距离整个屏幕顶边的距离

下面我们来看看几个和上面方法紧密相关的获取View宽高的View方法。如下:

View宽高方法 解释
getWidth() layout后有效,返回值是mRight-mLeft,一般会参考measure的宽度(measure可能没用),但不是必须的。
getHeight() layout后有效,返回值是mBottom-mTop,一般会参考measure的高度(measure可能没用),但不是必须的。
getMeasuredWidth() 返回measure过程得到的mMeasuredWidth值,供layout参考,或许没用。
getMeasuredHeight() 返回measure过程得到的mMeasuredHeight值,供layout参考,或许没用。

上面解释了自定义View时各种获取宽高的一些方法,下面我们再来看看获取View可见区域和顶点坐标的一些方法,不过这些方法需要在Activity的onWindowFocusChanged ()方法之后才能使用。如下图:



下面我们就给出上面这幅图涉及的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左上角的距离。

通过layout方法实现滑动

我们知道,在View进行绘制时,会调用onLayout方法来设置显示的位置。同样,可以通过修改View的mLeft, mTop, mRight, mBottom四个属性来控制View的位置。实现代码如下所示:

@Overridepublic boolean onTouchEvent(MotionEvent event) {    // TODO Auto-generated method stub    int x = (int) event.getX();    int y = (int) event.getY();    switch (event.getAction()) {    case MotionEvent.ACTION_DOWN:        // 记录触摸点坐标        android.util.Log.i("chenyang", "onTouchEvent ACTION_DOWN x = " + x + ",  y = " + y);        mLastX = x;        mLastY = y;        break;    case MotionEvent.ACTION_MOVE:        // 计算偏移量        android.util.Log.i("chenyang", "onTouchEvent ACTION_HOVER_MOVE x = " + x + ",  y = " + y);        int offsetX = x - mLastX;        int offsetY = y - mLastY;        // 在当前mLeft, mTop, mRight, mBottom的基础上加上偏移量        layout(getLeft() + offsetX, getTop() + offsetY, getRight()                + offsetX, getBottom() + offsetY);        break;    default:        break;    }    return true;}

通过offsetLeftAndRight()与offsetTopAndBottom实现滑动

这两个方法相当于系统提供了一个对左右、上下移动的API的封装。与上面一样,也是通过修改View的mLeft, mTop, mRight, mBottom四个属性来控制View的位置,实现代码如下:

@Overridepublic boolean onTouchEvent(MotionEvent event) {    // TODO Auto-generated method stub    int x = (int) event.getX();    int y = (int) event.getY();    switch (event.getAction()) {    case MotionEvent.ACTION_DOWN:        // 记录触摸点坐标        android.util.Log.i("chenyang", "onTouchEvent ACTION_DOWN x = " + x + ",  y = " + y);        mLastX = x;        mLastY = y;        break;    case MotionEvent.ACTION_MOVE:        // 计算偏移量        android.util.Log.i("chenyang", "onTouchEvent ACTION_HOVER_MOVE x = " + x + ",  y = " + y);        int offsetX = x - mLastX;        int offsetY = y - mLastY;        offsetLeftAndRight(offsetX);        offsetTopAndBottom(offsetY);        break;    default:        break;    }    return true;}

通过LayoutParams实现滑动

LayoutParams保存了一个View的布局参数,因此可以在程序中,通过改变LayoutParams来动态地修改一个View的布局参数,从而达到改变View位置的效果。我们可以很方便的在程序中使用getLayoutParams()来获取一个View的LayoutParams(注意必须在layout之后才可以获取到)。实现代码如下:

@Overridepublic boolean onTouchEvent(MotionEvent event) {    // TODO Auto-generated method stub    int x = (int) event.getX();    int y = (int) event.getY();    switch (event.getAction()) {    case MotionEvent.ACTION_DOWN:        // 记录触摸点坐标        android.util.Log.i("chenyang", "onTouchEvent ACTION_DOWN x = " + x + ",  y = " + y);        mLastX = x;        mLastY = y;        break;    case MotionEvent.ACTION_MOVE:        // 计算偏移量        android.util.Log.i("chenyang", "onTouchEvent ACTION_HOVER_MOVE x = " + x + ",  y = " + y);        int offsetX = x - mLastX;        int offsetY = y - mLastY;        ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();        layoutParams.leftMargin = getLeft() + offsetX;        layoutParams.topMargin = getTop() + offsetY;        setLayoutParams(layoutParams);        break;    default:        break;    }    return true;}

通过ViewDragHelper实现滑动

Google在其support库中为我们提供了DrawerLayout和SlidingPaneLayout两个布局来帮助开发者实现侧边栏滑动的效果。这两个新的布局大大方便了我们创建自己的滑动布局界面。然而,这两个功能强大的布局背后隐藏着一个鲜为人知却功能强大的类---ViewDragHelper。通过ViewDragHelper基本可以实现各种不同的滑动、拖放需求,因此此方法也是各种滑动解决方案中的终极绝招。

ViewDragHelper虽然功能强大,但其使用方法也是最复杂的。下面通过一个实例,来演示一下如何使用ViewDragHelper创建一个滑动布局,在这个例子中,准备实现类似QQ滑动侧边栏的效果,初始时显示内容界面,当用户手指滑动超过一定距离时,内容界面侧滑显示菜单界面,整个过程下图所示:


初始状态
侧滑展开菜单界面

实现代码如下所示:

package com.cytmxk.test.scroll;import android.content.Context;import android.support.v4.view.ViewCompat;import android.support.v4.widget.ViewDragHelper;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.view.View;import android.widget.FrameLayout;/** * Created by chenyang on 16/6/26. */public class DragViewGroup extends FrameLayout {    private static final String TAG = DragViewGroup.class.getCanonicalName();    private ViewDragHelper mViewDragHelper = null;    private View mMenuView = null;    private View mMainView = null;    private int mMenuWidth;    public DragViewGroup(Context context) {        super(context);        initView();    }    public DragViewGroup(Context context, AttributeSet attrs) {        super(context, attrs);        initView();    }    public DragViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        initView();    }    private void initView() {        //初始化ViewDragHelper,第一个参数是要监听的View,通常需要是一个ViewGroup,        //即parentView;第二个参数是一个Callback回调,后面会做解释。        mViewDragHelper = ViewDragHelper.create(this, callback);    }    //获取菜单布局的宽度,之后可以根据菜单布局(mMenuView)的宽度处理滑动后的效果。    @Override    protected void onSizeChanged(int w, int h, int oldw, int oldh) {        super.onSizeChanged(w, h, oldw, oldh);        mMenuWidth = mMenuView.getMeasuredWidth();        Log.d(TAG, "onSizeChanged mMenuWidth = " + mMenuWidth);    }    //初始化菜单布局(mMenuView)和主布局(mMainView)    @Override    protected void onFinishInflate() {        super.onFinishInflate();        mMenuView = getChildAt(0);        mMainView = getChildAt(1);    }    @Override    public boolean onInterceptTouchEvent(MotionEvent ev) {        //将触摸事件传递给ViewDragHelper,此操作必不可少        return mViewDragHelper.shouldInterceptTouchEvent(ev);    }    @Override    public boolean onTouchEvent(MotionEvent event) {        super.onTouchEvent(event);        //将触摸事件传递给ViewDragHelper,此操作必不可少        mViewDragHelper.processTouchEvent(event);        return true;    }    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {        // 何时开始检测触摸事件,通过这个方法,我们可以指定在创建ViewDragHelper时,       //参数parentView中的哪一个View可以被移动。        @Override        public boolean tryCaptureView(View child, int pointerId) {            //如果当前触摸的child是mMainView时开始检测,并且只有mMainView可以被移动            return mMainView == child;        }        // 触摸到View后回调        @Override        public void onViewCaptured(View capturedChild,                                   int activePointerId) {            super.onViewCaptured(capturedChild, activePointerId);        }        // 当拖拽状态改变,比如idle,dragging        @Override        public void onViewDragStateChanged(int state) {            super.onViewDragStateChanged(state);        }        // 当位置改变的时候调用,常用与滑动时更改scale等        @Override        public void onViewPositionChanged(View changedView,                                          int left, int top, int dx, int dy) {            super.onViewPositionChanged(changedView, left, top, dx, dy);        }        // 处理水平滑动        @Override        public int clampViewPositionHorizontal(View child, int left, int dx) {            return left;        }        // 处理垂直滑动        @Override        public int clampViewPositionVertical(View child, int top, int dy) {            return 0;        }        // 拖动结束后调用        @Override        public void onViewReleased(View releasedChild, float xvel, float yvel) {            super.onViewReleased(releasedChild, xvel, yvel);            Log.d(TAG, "onViewReleased mMainView.getLeft() = " + mMainView.getLeft() + ", mMenuWidth = " + mMenuWidth);            //手指抬起后缓慢移动到指定位置            if (mMainView.getLeft() < mMenuWidth) {                //关闭菜单                //相当于Scroller的startScroll方法                mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);            } else {                //打开菜单                mViewDragHelper.smoothSlideViewTo(mMainView, mMenuWidth, 0);                ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);            }        }    };    //由于ViewDragHelper内部是利用Scroller实现滑动的,所以利用computeScroll方法实现平滑滑动    @Override    public void computeScroll() {        super.computeScroll();        if (mViewDragHelper.continueSettling(true)) {            ViewCompat.postInvalidateOnAnimation(this);        }    }}

通过scrollTo和scrollBy实现滑动

  1. 在一个View中,系统提供了scrollTo、scrollBy两种方式来改变一个View中初始可见内容的位置。这两个方法的区别非常好理解,与英文中To和By的区别类似,scrollTo(x, y)表示让View中初始可见内容的在水平方向偏移到点(- x, - y)(x大于零表示向左偏移,否者向右偏移; y大于零表示向上偏移,否者向右偏移),scrollBy(dx, dy)表示让View中初始可见内容的在水平方向偏移dx(dx大于零表示向左偏移,否者向右偏移),在垂直方向偏移dy(dy大于零表示向上偏移,否者向右偏移)如下是这两个方法的代码实现:
    /**     * The offset, in pixels, by which the content of this view is scrolled     * horizontally.     * {@hide}     */    @ViewDebug.ExportedProperty(category = "scrolling")    protected int mScrollX;    /**     * The offset, in pixels, by which the content of this view is scrolled     * vertically.     * {@hide}     */    @ViewDebug.ExportedProperty(category = "scrolling")    protected int mScrollY;    /**     * 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);    }

有上面的代码可以得知mScrollX,mScrollY是用来保存View初始可见内容的偏移量。理解了mScrollX和mScrollY的用法,就不难理解getScrollX() 和getScrollY()。这两个函数的源码如下所示:

    /**     * Return the scrolled left position of this view. This is the left edge of     * the displayed part of your view. You do not need to draw any pixels     * farther left, since those are outside of the frame of your view on     * screen.     *     * @return The left edge of the displayed part of your view, in pixels.     */    public final int getScrollX() {        return mScrollX;    }    /**     * Return the scrolled top position of this view. This is the top edge of     * the displayed part of your view. You do not need to draw any pixels above     * it, since those are outside of the frame of your view on screen.     *     * @return The top edge of the displayed part of your view, in pixels.     */    public final int getScrollY() {        return mScrollY;    }
  1. 举例说明,如下图所示(注意,图中黄色矩形区域表示的是View,绿色虚线矩形为View中初始可见的内容。一般情况下两者的大小一致,本文为了显示方便,将虚线框画小了一点。图中的黄色区域的位置始终不变,发生偏移的是初始可见的内容。):



    scrollTo(0, 100)的效果如下图所示:



    scrollTo(100, 100)的效果图如下:

    若函数中参数为负值,则子View的移动方向将相反:


通过Scroller实现滑动

上面举例中通过scrollTo偏移View的初始可见内容是在瞬间完成的,这样的效果会让人感觉非常突兀。Google也想到了这一点,所以提供了Scroller类来模拟平滑滑动的效果。
Scroller类提供了startScroll方法来初始化一个模拟平滑滑动的过程,然后调用invalidate()方法,这个方法会导致View重绘,系统在绘制View的时候会在draw方法中调用computeScroll方法来实现模拟滑动,在computeScroll方法中通过调用Scroller的computeScrollOffset方法判断是否完成了整个滑动,同时Scroller也提供了getCurrX、getCurrY来获取当前滑动过程中 View初始可见内容 即将的偏移量,然后利用srcollTo方法实现偏移即可,然后执行invalidate方法实现循环调用computeScroll方法直到滑动结束。

  1. Scroller中相关API简介如下:
mScroller.getCurrX() //获取mScroller当前水平方向滑动过程中的位置  mScroller.getCurrY() //获取mScroller当前竖直方向滑动过程中的位置  mScroller.getFinalX() //获取mScroller最终停止滑动的水平位置  mScroller.getFinalY() //获取mScroller最终停止滑动的竖直位置  mScroller.setFinalX(int newX) //设置mScroller最终停留的水平位置,没有动画效果,直接跳到目标位置  mScroller.setFinalY(int newY) //设置mScroller最终停留的竖直位置,没有动画效果,直接跳到目标位置  mScroller.startScroll(int startX, int startY, int dx, int dy) //使用默认完成时间250ms  mScroller.startScroll(int startX, int startY, int dx, int dy, int duration)  //开始滑动,startX, startY为 View初始可见内容 开始滑动的位置(即mScrollX,mScrollY的值),dx,dy分别为水平方向和垂直方向的偏移量(dx大于零表示向左偏移,否者向右偏移;dy大于零表示向上偏移,否者向下偏移), duration为完成滚动的时间 。mScroller.computeScrollOffset() //返回值为boolean,true说明滑动尚未完成,false说明滑动已经完成。这是一个很重要的方法,通常放在View.computeScroll()中,用来判断是否滑动是否结束。

2 举例如下:

public void moveToDest(int index) {    /*     * 对 index 进行判断 ,确保 是在合理的范围     * 即  index >=0  && index <=getChildCount()-1     */    //确保 index>=0    index = index >= 0 ? index : 0;    //确保 currIndex<=getChildCount()-1    currIndex = index <= getChildCount() - 1 ? index : getChildCount() - 1;    if (null != mOnPagerChangeListener) {        mOnPagerChangeListener.OnPagerChange(currIndex);    }    myScroller.startScroll(getScrollX(), 0, currIndex * getWidth() - getScrollX(), 0, 500);    invalidate();}@Overridepublic void computeScroll() {    super.computeScroll();    if (myScroller.computeScrollOffset()) {        scrollTo(myScroller.getCurrX(), 0);        invalidate();    }}

参考文档

  1. Android应用坐标系统全面详解
  2. Android群英传

更多相关文章

  1. Android(安卓)activity 详解一:activity的生命周期
  2. 简单明了的分析Android触摸事件,看完再也不纠结了
  3. 初学者---Android(安卓)Fragment之间数据传递的三种方式
  4. Android的线程
  5. Webview实现Android和JS通信
  6. afinal - afinal 是一个android的 orm 和 ioc 框架。通过afinal
  7. Android(安卓)Launcher 分析
  8. ANDROID自定义SCROLLVIEW
  9. 【Android(安卓)- 进阶】之事件分发机制

随机推荐

  1. Android shape使用
  2. [原创]让android支持RTSP(live555分析)
  3. android layout物业介绍
  4. Android数据加密之Aes加密
  5. [置顶] 我的Android进阶之旅------>Andro
  6. 【Android(安卓)界面效果9】9patch图片
  7. Android单元测试初探Instrumentation
  8. EditText的常用属性和实例
  9. Android(安卓)UI开发第二十五篇――分享
  10. TextView常用属性