预备知识

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


    Android中实现滑动效果_第1张图片 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区域常用的位置和距离。先看下面这幅图:
    Android中实现滑动效果_第2张图片
    通过上图我们可以很直观的给出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 ()方法之后才能使用。如下图:


Android中实现滑动效果_第3张图片

下面我们就给出上面这幅图涉及的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滑动侧边栏的效果,初始时显示内容界面,当用户手指滑动超过一定距离时,内容界面侧滑显示菜单界面,整个过程下图所示:


Android中实现滑动效果_第4张图片 初始状态
Android中实现滑动效果_第5张图片 侧滑展开菜单界面

实现代码如下所示:

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中初始可见的内容。一般情况下两者的大小一致,本文为了显示方便,将虚线框画小了一点。图中的黄色区域的位置始终不变,发生偏移的是初始可见的内容。):


    Android中实现滑动效果_第6张图片

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


    Android中实现滑动效果_第7张图片
    scrollTo(100, 100)的效果图如下:
    Android中实现滑动效果_第8张图片

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


    Android中实现滑动效果_第9张图片

通过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 View坐标getLeft, getRight, getTop, getBottom
  2. android中求区域内两个坐标之间的距离的实现
  3. Android VideoView设置静音,Android 设置VideoView静音,Android
  4. ListView去掉分割线的几种方法
  5. SDK Platform Tools component is missing! Please use the SDK
  6. Android Market google play store帐号注册方法流程 及发布应用
  7. Android 实现全屏显示的几种方法整理
  8. Android中TextView中内容不换行的解决方法

随机推荐

  1. mysql5.7.23版本安装教程及配置方法
  2. mysql中的锁机制深入讲解
  3. innodb如何巧妙的实现事务隔离级别详解
  4. Mysql 8.0安装及重置密码问题
  5. centos7通过yum安装mysql的方法
  6. CentOS 7 下使用yum安装MySQL5.7.20 最简
  7. linux CentOS 7.4下 mysql5.7.20 密码改
  8. mysql全文模糊搜索MATCH AGAINST方法示例
  9. 深入讲解MySQL Innodb索引的原理
  10. 解压版MYSQL安装及遇到的错误及解决方法