欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

View的滑动效果

程序员文章站 2022-05-11 14:05:43
...

1滑动原理

滑动一个view的原理就是通过不断的改变view的坐标来实现这个效果,基本思路:要实现view的滑动,必须监听用户的触摸事件,并根据事件传入的坐标,动态且不断的改变view的坐标,从而实现view跟随用户触摸的滑动而滑动.
1.1 坐标系
在android中有两种坐标系:android坐标系(屏幕坐标系)和view坐标系(视图坐标系).android坐标系的定义是
移动设备一般将屏幕的左上角定义为android坐标系原点,向右为X轴正方向,向下为Y轴正方向,如下图:

View的滑动效果

1.2 视图坐标系
视图坐标系描述了子视图和父布局的位置关系,定义是以父视图左上角为坐标原点,以原点向右为X轴正方向,向下为Y轴正方向,如下图:
View的滑动效果

1.3 触控事件
在onTouchEvent(MotionEvent event)方法中通过event.getAction()来获取事件类型,从而对不同的事件进行处理。
对于获取坐标值的,android提供了两类方法来处理,如下图

View的滑动效果

  • View自身坐标
    通过如下方法可以获得View到其父控件(ViewGroup)的距离:
    getTop():获取View自身顶边到其父布局顶边的距离
    getLeft():获取View自身左边到其父布局左边的距离
    getRight():获取View自身右边到其父布局左边的距离
    getBottom():获取View自身底边到其父布局顶边的距离

  • MotionEvent提供的方法
    我们看上图那个深蓝色的点,假设就是我们触摸的点:
    getX():获取点击事件距离控件左边的距离,即视图坐标
    getY():获取点击事件距离控件顶边的距离,即视图坐标
    getRawX():获取点击事件距离整个屏幕左边距离,即绝对坐标
    getRawY():获取点击事件距离整个屏幕顶边的的距离,即绝对坐标
    另外:
    View获取自身宽高
    getHeight():获取View自身高度
    getWidth():获取View自身宽度

七种滑动方法

1 Layout
在view进行绘制时会调用onLayout()来设置显示的位置,因此我们就可以通过修改view的left,top,right,bottom等属性来控制view的坐标。代码如下


public class MoveDemo extends TextView {
    private int lastX ,lastY ,offsetX ,offsetY;
    private Scroller scroller;
    public MoveDemo(Context context) {
        super(context);
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                offsetX = x-lastX;
                offsetY = y-lastY;
//                方式一 layout方式 

layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
                break;
        }
        return true;
    }
    //或者使用android绝对坐标
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int)event.getRawX();//使用绝对坐标
        int y = (int)event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                Log.i("niuniu"," ACTION_DOWN  lastX "+ lastX + " lastY " + lastY);
                break;
            case MotionEvent.ACTION_MOVE:
                offsetX = x-lastX;
                offsetY = y-lastY;
                Log.i("niuniu"," ACTION_MOVE  offsetY "+ offsetY + " offsetX " + offsetX);
                layout(getLeft()+offsetX,getTop()+offsetY,getRight()+offsetX,getBottom()+offsetY);
                  //重新设置初始坐标,也就是将移动后的坐标设置为初始坐标,供下次移动计算偏移量
                lastX = x;
                lastY = y;
                break;
        }
        return true;
    }
}

2offsetLeftAndRight()和offsetTopAndBottom()
这两个方法相当于系统提供的一个对左右,上下移动的API的封装。计算出偏移量后,使用如下代码就可以完成view的重新布局,效果和使用onLayout()方法一样。

//              方式二 offsetXXXXX方式 设置其新坐标,并重新布局
//              同时对left和right进行偏移
                offsetLeftAndRight(offsetX);   
//              同时对top和bottom进行偏移
                offsetTopAndBottom(offsetY);

3 LayoutParams
LayoutParams属性保存了一个view的布局参数,因此可以在程序中通过改变LayoutParams来动态改变一个布局的位置参数,最终实现改变view位置的效果.

//                方式三 LayoutParams 修改View布局的位置参数,从而改变view的位置
//                MoveDemo的父布局是类型是LinearLayout,因此这里的布局参数是根据父布局的类型而                LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft()+offsetX;
                layoutParams.topMargin =getTop()+offsetY;
                setLayoutParams(layoutParams);
/**注意:得根据view所在父布局的类型来设置不同的类型,如果当前view所在父布局为LinearLayout那么就可以使用LinearLayout.LayoutParams类似的如果在RelativeLayout中就要使用RelativeLayout.LayoutParams。当然这一切的前提你的view还需要有个父布局。
*/

通过改变LayoutParams来改变一个View的位置时,通常改变的是这个view的Margin属性.所以除了使用布局LayoutParams之外,还可以使用ViewGroup.MarginLayoutParams来实现这个功能,而且使用 ViewGroup.MarginLayoutParams 不需要考虑父布局的类型,更加方便

                 ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
                layoutParams.leftMargin = getLeft()+offsetX;
                layoutParams.topMargin =getTop()+offsetY;
                setLayoutParams(layoutParams);

4 scrollTo和scrollBy
使用scrollTo和scrollBy进行移动操作时,被移动的是view的内容.也就是说如果是一个ViewGroup调用该方法,那么被移动的是所有的子view;如果是View例如TextView那么被移动的就是它的文本,如果是ImageView那么被移动的就是它的drawable对象。
下面用一组图来说明其移动的参数
View的滑动效果View的滑动效果   View的滑动效果

上图中中间矩形相当于用户看到的手机屏幕,即为可视区域,后面的content相当于是一个大的画布,在可视区域,以可视区域为坐标系,button的坐标(20,10),当调用scrollBy(20,10)方法的时,就可以认为是矩形的可视区域以content画布为坐标系以x轴正方向平移20,以y轴的正方向平移10,因此移动之后的矩形可视区域的位置右图所示。

所以,相对content来说,ViewGroup是移动了(20,10),相对ViewGroup来说,button是移动了(-20,-10),这就是因为参考系不同,而产生不同的视图效果。

由此分析可知,如果设置scrollBy(20,10),content是向坐标轴的负方向移动,被移动是viewGroup ;如果设置为scrollBy(-20,-10),content是像坐标轴正方向移动,被移动是viewGroup .

结论 对于我们用户来说,是要将可视区域的view移动,就需要设置参数为负数。

/**
方式四 scrollBy(相对坐标时使用) 
*/
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                offsetX = x-lastX;
                offsetY = y-lastY;
                ((View)getParent()).scrollBy(-offsetX,-offsetY);
                break;
         }
         return true;
    }

/**
方式四 scrollTo(绝对坐标时使用)
*/
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int)event.getRawX();
        int y = (int)event.getRawY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                Log.i("niuniu"," ACTION_DOWN  lastX "+ lastX + " lastY " + lastY);
                break;
            case MotionEvent.ACTION_MOVE:
                offsetX = x-lastX;
                offsetY = y-lastY;
                Log.i("niuniu"," ACTION_MOVE  offsetY "+ offsetY + " offsetX " + offsetX);
                ((View)getParent()).scrollTo(-x,-y);
                 //重新设置初始坐标,也就是将移动后的坐标设置为初始坐标,供下次移动计算偏移量
                lastX = x;
                lastY = y;
                break;
                         }
         return true;
    }

5 Scoller
scroller类的原理与scrollTo和scrollBy方法来实现view的移动原理基本类似,代码如下:

public class MoveDemo extends TextView {
    private Scroller scroller;
    public MoveDemo(Context context) {
        super(context);
    }
    public MoveDemo(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //第一步 初始化Scroller,创建一个Scroller对象
        scroller = new Scroller(context);
    }

    //第二步,重写computeScroll()方法,实现模拟滑动
    /**
    computeScroll()方法是使用Scroller类的核心,系统在绘制view的时候会在draw()方法中调用该方法而这个方法实际上就是使用的scrollTo方法。在结合scroller对象,帮助获取到当前的滚动值,我们就可以通过不断的瞬间移动一个小的距离来实现整体上的平滑的移动效果,
    */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(scroller.getCurrX(),scroller.getCurrY());
            Log.i("niuniu"," computeScroll  scroller.getCurrX() "+ scroller.getCurrX() + " scroller.getCurrY() " + scroller.getCurrY());
            // 通过重绘来不断调用computeScroll
            invalidate();
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                View viewGroup = ((View)getParent());
                //第三步,启动模拟过程,移动偏移量的参数,与ScrollBy的情况一致。
scroller.startScroll(viewGroup.getScrollX(),viewGroup.getScrollY(),-500,-300);
                Log.i("niuniu"," ACTION_UP  viewGroup.getScrollX() "+ viewGroup.getScrollX() + " scroller.getFinalX() " + scroller.getFinalX() + " " + scroller.getStartX());
                invalidate();
                break;
        }
        return true;
    }
}

Scroller这个类提供了computeScrollOffset()方法来判断是否完成了整个滚动,同时也提供了getCurrX(),getCurrY()方法获得当前的滑动坐标。而且只能在computeScroller方法中获取模拟过程中的scrollX和scrollY的坐标,但是computeScroller不能自动调用,只能通过invalidate()—>onDraw()—>computeScroller()来间接的调用computeScroller方法,从而循环获取当前的scrollX,scrollY的值,实现了滑动效果。在当模拟过程结束后,scroller.computeScrollOffset方法会返回false从而结束循环。
5 属性动画
6 ViewDragHelper

public class ViewDragHelperDemo  extends LinearLayout {
    private ViewDragHelper viewDragHelper;
    private ViewDragHelper.Callback callback = null;
    private int mWidth;
    private TextView mMenu,mMain;
    public ViewDragHelperDemo(@NonNull Context context) {
        super(context);
    }

    public ViewDragHelperDemo(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        /**
        第四步 处理回调callback 
        */
        callback = new ViewDragHelper.Callback() {
            @Override
            public boolean tryCaptureView(View child, int pointerId) {
//            通过这个回调接口的该方法,我们可以确定那个子view可以被移动
//            如果当前触摸的child是mMenu时开始检测
                return mMenu == child;
            }
/**
clampViewPositionHorizontal(View child, int left, int dx)和clampViewPositionVertical(View child, int top, int dy)这两个方法分别对应水平和垂直方向上的滑动
            child是待移动的子view
            left/top代表水平或者垂直上child移动的距离
            dy代表比较前一次的增量
            默认返回值是0即不发生滑动
*/
            @Override
            public int clampViewPositionHorizontal(View child, int left, int dx) {
                return 0;
            }

            @Override
            public int clampViewPositionVertical(View child, int top, int dy) {
                return top;
            }

//        拖动结束后调用
            @Override
            public void onViewReleased(View releasedChild, float xvel, float yvel) {
                super.onViewReleased(releasedChild, xvel, yvel);
                Log.i("niuniu"," top " + mMenu.getTop());
                if (mMenu.getTop()<500){
                //相当于scoller的startScoll方法,启动后,要主动重新绘图
                    viewDragHelper.smoothSlideViewTo(mMenu,0,0);
                    ViewCompat.postInvalidateOnAnimation(ViewDragHelperDemo.this);
                } else {
                    viewDragHelper.smoothSlideViewTo(mMenu,0,620);
                    ViewCompat.postInvalidateOnAnimation(ViewDragHelperDemo.this);
                }
            }
            //当触摸到这个mMenu时调用
            @Override
            public void onViewCaptured(View capturedChild, int activePointerId) {
                super.onViewCaptured(capturedChild, activePointerId);
                Log.i("niuniu"," onViewCaptured");
                mMenu.setBackgroundColor(Color.GRAY);
            }
          //当mMenu滑动时会有不同的状态
            @Override
            public void onViewDragStateChanged(int state) {
                super.onViewDragStateChanged(state);
                if (state == ViewDragHelper.STATE_IDLE){
                    Log.i("niuniu"," state STATE_IDLE" + state);
                    mMenu.setText("滑动结束,回到初始位置");
                } else if (state == ViewDragHelper.STATE_DRAGGING){
                    Log.i("niuniu"," state STATE_DRAGGING" + state);
                    mMenu.setText("正在滑动");
                } else if (state == ViewDragHelper.STATE_SETTLING) {
                    Log.i("niuniu"," state STATE_SETTLING" + state);
                    mMenu.setText("设置滑动");
                }
            }
       //当mMenu滑动时坐标会变化,该方法调用
            @Override
            public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
                super.onViewPositionChanged(changedView, left, top, dx, dy);
                Log.i("niuniu"," onViewPositionChanged");
            }
        };
        //第一步 初始化ViewDragHelper  
        /**
        它的第一个参数就是要监听的View,也就是parentView(本例子中是LinearLayout),第二个参数是一个CallBack回调,这个回调就是整个ViewDragHelper的逻辑核心。
        */
        viewDragHelper = ViewDragHelper.create(this,callback);
    }

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

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMain = (TextView) getChildAt(0);
        mMenu = (TextView)getChildAt(1);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = mMenu.getMeasuredWidth();
    }
/** 
第二步 事件拦截 
由于我们要监听与屏幕的交互,因此要重写事件拦截方法,将事件传递给ViewDragHelper进行处理
*/
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return viewDragHelper.shouldInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        viewDragHelper.processTouchEvent(event);
        return true;
    }
/**
第三步 处理 computeScroll 
这里我们依然重写computeScroll方法,因为ViewDragHelper内部也是通过Scroller这个类来实现的,可以参考这么来写。
*/
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (viewDragHelper.continueSettling(true)){
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
}


//main_activity.xml
    <com.example.nft.myapplication.ViewDragHelperDemo
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/Drag"
        android:orientation="vertical">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#345ff2"
            android:textStyle="bold"
            android:textSize="30dp"
            android:background="#f4f332"
            android:text="主页"/>
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#3ff452"
            android:textStyle="bold"
            android:background="#fd2143"
            android:textSize="30dp"
            android:text="菜单"/>
    </com.example.nft.myapplication.ViewDragHelperDemo>