Android实现滑动的七种方式

释放双眼,带上耳机,听听看~!

1. layout方法

根据用户手指滑动的位置(ACTION_MOVE),记录每次小段的偏移量(offset),通过layout不停对view进行重新布局,完成view的移动效果。

a). 使用视图坐标系:getX(),getY()

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 这里需要转换为int值,保证layout方法参数
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
                layout(getLeft() + offsetX, getTop() + offsetY,
                        getRight() + offsetX, getBottom() + offsetY);
                break;
        }
        // 返回false不能达到滑动的目标
        return true;
    }
}

由于每次layout后,触摸点相对于父控件的位置不变,因此滑动期间不需要更新mLastX和mLastY的值。

b). 使用Android坐标系:getRawX(),getRawY()

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 这里需要转换为int值,保证layout方法参数
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
                layout(getLeft() + offsetX, getTop() + offsetY,
                        getRight() + offsetX, getBottom() + offsetY);
                // 需要更新mLastX与mLastY的值
                mLastX = x;
                mLastY = y;
                break;
        }
        // 返回false不能达到滑动的目标
        return true;
    }

需要注意的是,每次move更新界面后都要更新上一次的坐标值,否则下一次布局时会加上控件到屏幕边缘的距离。

2. offsetLeftAndRight()、offsetTopAndBottom()方法:和第一种方法没啥区别

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 这里需要转换为int值,保证layout方法参数
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
 
                offsetLeftAndRight(offsetX);
                offsetTopAndBottom(offsetY);
 
                break;
        }
        // 返回false不能达到滑动的目标
        return true;
    }

3. layoutParams: 利用设置边距完成view的移动

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 这里需要转换为int值,保证layout方法参数
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                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;
        }
        // 返回false不能达到滑动的目标
        return true;
    }

注意:使用ViewGroup必须保证该控件有一个父布局,否则不能使用

4. scrollBy、scrollTo:瞬间移动

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 这里需要转换为int值,保证layout方法参数
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
 
                // scrollBy和scrollTo的移动方向为屏幕移动方向,与控件移动方向相反
                // scrollBy移动的是content,不是view
                ((View) getParent()).scrollBy(-offsetX, -offsetY);
 
                break;
        }
        // 返回false不能达到滑动的目标
        return true;
    }

注意:scroll方法移动的不是控件本身,而是其内容。举例来说,View为ViewGroup时,移动的是其全部子控件;View为TextView时,移动的是其文字内容。

5. Scroller:实现平滑移动,在移动模块的同时增加松手后回弹至初始位置的功能。

public class Rect extends View {
    private int mLastX;
    private int mLastY;
    private Scroller mScroller;
 
    public Rect(Context context, AttributeSet attrs) {
        super(context, attrs);
        mScroller = new Scroller(context);
    }
 
    @Override
    public void computeScroll() {
        super.computeScroll();
        // 如果Scroller还在计算中,则另其移动
        if (mScroller.computeScrollOffset()) {
            ((View)getParent()).scrollTo(mScroller.getCurrX(),
                    mScroller.getCurrY());
            invalidate();
        }
    }
 
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 这里需要转换为int值,保证layout方法参数
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - mLastX;
                int offsetY = y - mLastY;
 
                // scrollBy和scrollTo的移动方向为屏幕移动方向,与控件移动方向相反
                // scrollBy移动的是content,不是view
                ((View) getParent()).scrollBy(-offsetX, -offsetY);
                break;
            case MotionEvent.ACTION_UP:
 
                View viewGroup = (View) getParent();
                // 这里的起始坐标为content的起始坐标,而不是view的起始坐标
                mScroller.startScroll(viewGroup.getScrollX(), viewGroup.getScrollY(),
                        -viewGroup.getScrollX(), -viewGroup.getScrollY());
                invalidate();
        }
        // 返回false不能达到滑动的目标
        return true;
    }
}

步骤分为:初始化Scroller对象——复写computeScroll函数——调用Scroller对象的startScroll函数开启滑动过程

6. 属性动画

后期待添加

7. ViewDragHelper

xml文件:

<?xml version="1.0" encoding="utf-8"?>
<com.example.tianshuhe.learningcomponent.DragFrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.example.tianshuhe.learningcomponent.MainActivity">
    <View
        android:id="@+id/menu_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/black"/>
 
    <View
        android:id="@+id/main_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/holo_red_light"/>
 
</com.example.tianshuhe.learningcomponent.DragFrameLayout>

自定义的DragFrameLayout文件:

package com.example.tianshuhe.learningcomponent;
 
import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
 
/**
 * Created by tianshuhe on 17/8/22.
 */
 
public class DragFrameLayout extends FrameLayout {
    private ViewDragHelper mViewDragHelper;
    private ViewDragHelper.Callback mCallback;
    private View mMainView, mMenuView;
 
    public DragFrameLayout (Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }
 
    private void initView() {
        mCallback = new ViewDragHelper.Callback() {
            @Override
            // 只拦截主界面的滑动事件
            public boolean tryCaptureView(View child, int pointerId) {
                return mMainView == child;
            }
 
            // 设置水平滑动事件
            @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);
                // 根据滑动的大小,设置是否显示全部的菜单
                // 这种实现方式实际上是一开始menu被main阻挡,后期main被拿开
                if (mMainView.getLeft() < 500) {
                    mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
                    ViewCompat.postInvalidateOnAnimation(DragFrameLayout.this);
                } else {
                    mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
                    ViewCompat.postInvalidateOnAnimation(DragFrameLayout.this);
                }
            }
        };
 
        mViewDragHelper = ViewDragHelper.create(this, mCallback);
    }
 
    // 拦截点击事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mViewDragHelper.processTouchEvent(event);
        return true;
    }
 
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 这里一开始main和menu使用自定义的滑动view,则返回true,否则不能实现效果
        return mViewDragHelper.shouldInterceptTouchEvent(ev);
    }
 
    @Override
    public void computeScroll() {
        if (mViewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }
 
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mMainView = findViewById(R.id.main_view);
        mMenuView = findViewById(R.id.menu_view);
    }
}

一个奇怪的现象:onInterceptTouchEvent中,如果本身的控件带有滑动效果,这种方式返回后并不会对其事件进行拦截,这种情况待后续分析。

人已赞赏
Android文章

安卓弹性滑动的几种实现方式

2019-10-12 9:27:31

Android文章

android 生命周期

2019-10-12 12:44:05

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索