专栏首页指点的专栏Android 中 View 的滑动

Android 中 View 的滑动

Android View控件的滑动是 Android 的一个重要内容。在 View 需要变换位置时,为其添加适当的滑动效果,获得更好的用户体验,下面来看一下怎样去实现 View 的滑动:

1、scrollBy / ScrollTo 方法: View 控件提供的两个方法,来看一下官方文档给出的说明:

两个方法都会使得 View 重绘,不同的是:

scrollBy 方法是将 View 基于当前位置分别向水平移动 x 绝对值的距离(x 为正,向右移动,否则向左),向竖直方向移动 y 绝对值的距离(y 为正,向下移动,否则向上移动) scrollTo 方法将 View 基于父容器左上角分别向水平移动 x 绝对值的距离(x 为正,向右移动,否则向左),向竖直方向移动 y 绝对值的距离(y 为正,向下移动,否则向上移动)

下面通过一个小例子来理解两个方法,新建一个 Android 工程: activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main_activity_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.company.zhidian.viewscroll.MainActivity">

    <Button
        android:id="@+id/scrollToButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="scrollTo" />
    <Button
        android:id="@+id/scrollByButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="scrollBy" />

</LinearLayout>

MainActivity.java:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Layout;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    private ViewGroup layout = null;
    private Button scrollToButton = null;
    private Button scrollByButton = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        layout = (ViewGroup) findViewById(R.id.main_activity_layout);
        scrollByButton = (Button) findViewById(R.id.scrollByButton);
        scrollByButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                layout.scrollBy(-30, -30);
            }
        });
        scrollToButton = (Button) findViewById(R.id.scrollToButton);
        scrollToButton.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                layout.scrollTo(-30, -30);
            }
        });
    }
}

两个按钮,分别对应不同的点击事件来调用 scrollBy 方法和 scrollTo 方法,这里为什么要调用布局的 scrollTo 方法和 scrollBy 方法呢?这个问题先放一下,后面就会知道,我们先来看看结果:

Ok, 和上文将的能对上,下面我们改一下代码: 先是 activty_main.xml:

<Button
    android:id="@+id/scrollByButton"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:gravity="start"
    android:text="scrollBy" />

把 scrollBy 的那个按钮改了一下,下面是 MainActivity.java:

scrollByButton.scrollBy(-30, -30);

MainActivity.java 只需要把 scrollBy 按钮的点击事件改成它本身调用 scrollBy 方法就行了,来看看结果:

到这里,我想小伙伴们应该能明白为什么上面要调用 layout.scrollTo 方法和 layout.scrollBy 方法了: scrollTo 方法和 scrollBy 方法移动的是 view 里面的内容(子控件或者是显示的内容),并且移动的方向和方法的参数正负是相反的(也可以借助参考物来理解(父容器移动,子控件不移动,相对父容器来说,子控件移动的方向是与其相反))。

Ok,下面来看一下那两个方法的升级版:Scroller 类。在上面的滑动中,效果是瞬间完成的,在 APP 中,这种效果会给人一种非常突兀的感觉。Scroller 类正是为了给 View 的滑动添加动画效果产生的。一般来说,使用 Scroller 类要有下面三个步骤: 1、初始化 Scroller 类的对象:Scroller scroller = new Scroller(context) 2、重写要滑动的 View 的 computeScroll() 方法: 3、调用 startScroll(int startX, int startY, int dx, int dy)方法开始 View 的滑动,参数分别为开始的位置和横纵方向滑动的位移,这个方法还有一个重载版本,多了一个参数用于控制滑动时间

下面我们仍然以上面的那个例子做些改变来看一下 Scroller 类的用法: 因为要处理触摸事件,因此我们新建一个类继承 LinearLayout 类并重写部分方法来实现我们的需求: MyLinearLayout.java:

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.Scroller;

public class MyLinearLayout extends LinearLayout {

    private Scroller scroller = null;
    private int lastX = 0;
    private int lastY = 0;

    private void init(Context context) {
        scroller = new Scroller(context);
    }

    public MyLinearLayout(Context context) {
        super(context);
        init(context);
    }

    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    // 重写父类的 computeScroll 方法来判断滑动是否完成,进行对应处理
    @Override
    public void computeScroll() {
        super.computeScroll();
        // 如果滑动完成,返回 false,否则返回 true
        if(scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            invalidate(); // 通过不断调用 invalidate 方法来调用 computeScroll 方法
        }
    }

    // 重写父类的触摸事件处理方法
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            // 手指按下:
            case MotionEvent.ACTION_DOWN:
                Log.e("onTouchEvent", "DOWN");
                scroller.startScroll(lastX, lastY, lastX-x, lastY-y);
                /*
                 * 调用这个方法重绘并且调用 computeScroll 方法,
                 * 因为只有通过这个方法才能间接地调用 computeScroll 方法,实现滑动
                 */
                invalidate();
                break;
            // 手指移动
            case MotionEvent.ACTION_MOVE:
                Log.e("onTouchEvent", "MOVE");
                scrollTo(-x, -y);
                invalidate();
                break;
            // 手指松开:
            case MotionEvent.ACTION_UP:
                Log.e("onTouchEvent", "UP");
                scroller.startScroll(getScrollX(), getScrollY(), -getScrollX(), -getScrollY());
                invalidate();
                break;
        }
        return true;
    }

}

对于 activity_main.xml,我们则需要使用我们自定义的布局:

<?xml version="1.0" encoding="utf-8"?>
<com.company.zhidian.viewscroll.MyLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main_activity_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <Button
        android:id="@+id/scrollToButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="scrollTo" />
    <Button
        android:id="@+id/scrollByButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="scrollBy" />

</com.company.zhidian.viewscroll.MyLinearLayout>

MainActivity.java改为初始状态下的代码就行了,因为我们的关键代码在 MyLinearLayout 中实现了:

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.Layout;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }
}

来看看结果:

成功的实现了滑动效果!

不知道小伙伴们发现没有,在这里实现的滑动都是对当前控件的全部的子 View 进行滑动,这样在一定程度上限制了滑动的灵活性。那么我们如何处理单个 View 的滑动呢?轮到我们的 ViewDragHelper 类出场了,通过 ViewDragHelper 我们可以灵活地对不同的 View 施加不同的滑动效果,下面我们来看一下怎么使用这个功能强大的类:

1、初始化 ViewDragHelper 对象:通常使用 ViewDragHelper.create(ViewGroup v, ViewDragHelper.Callback c); 方法来初始化一个新的 ViewDragHelper 对象 2、拦截触摸事件,传递给 ViewDragHelper 对象处理:重写要检测的 ViewGroup 的 onInterceptTouchEvent 方法来拦截触摸事件并且将触摸事件传递给 ViewDragHelper 对象处理 3、处理 computeScroll 方法:ViewDragHelper 内部还是通过 Scroller 来实现滑动的,所以需要实现 computeScroll 方法 4、处理 ViewDragHelper.Callback:这个回调是整个滑动的核心,我们要在这个接口中根据我们自己的逻辑来实现不同的方法并进行处理

Ok,让我们对上面的工程的 MyLinearLayout.java 进行修改:

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.LinearLayout;

public class MyLinearLayout extends LinearLayout {

    private ViewDragHelper viewDragHelper = null;
    private View childViews[] = null;

    private void init() {
        /*
         * 通过 ViewDragHelper 类的静态方法创建对象,第一个参数是要监听的 ViewGroup,
         * 第二个参数是处理监听事件的回调接口,在里面可以对触摸事件的不同处理状态进行对应操作
         */
        viewDragHelper = ViewDragHelper.create(this, callback);
    }

    public MyLinearLayout(Context context) {
        super(context);
        init();
    }

    public MyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    // 这个方法在布局文件加载完成的时候回调,在这里获取子 View
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        childViews = new View[this.getChildCount()];
        for(int i = 0; i < childViews.length; i++) { // 获取布局的子 View 对象
            childViews[i] = this.getChildAt(i);
        }
    }

    // ViewDragHelper 同样需要重写 computeScroll 方法,因为其内部也是通过这个类来实现滑动的
    @Override
    public void computeScroll() {
        super.computeScroll();
        // 如果滑动完成,返回 false,否则返回 true
        if(viewDragHelper.continueSettling(true)) {
            ViewCompat.postInvalidateOnAnimation(this);
        }
    }

    // 拦截触摸事件,将事件传递给 viewDragHelper 处理
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        viewDragHelper.shouldInterceptTouchEvent(event);
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 将触摸事件传递给 viewDragHelper 处理
        viewDragHelper.processTouchEvent(event);
        return true;
    }

    // viewDragHelper 处理触摸事件的回调
    private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {
        /*
         * 这个方法可以在创建 ViewDragHelper 对象时,指定被监听的 ViewGroup 中哪个子 View 可以被移动,
         * 如果返回 true,那么继续监测当前触摸事件,否则不检测
         */
        @Override
        public boolean tryCaptureView(View child, int pointerId) {
            return child == childViews[0]; // 如果触摸的是是第一个子 View 则继续监测触摸事件
        }

        /*
         * 水平方向上的滑动处理方法,第一个参数为滑动的子 View,第二个参数是水平方向上移动的距离,
         * 第三个参数为水平方向上较上一次的增量,通常只需要返回 left 就行了,如果不重写这个方法,
         * 那么水平方向上是不会滑动的,因为父类的该方法返回值为 0,下同。
         */
        @Override
        public int clampViewPositionHorizontal(View child, int left, int dx) {
            return left;
        }

        // 竖直方向上的滑动
        @Override
        public int clampViewPositionVertical(View child, int top, int dy) {
            return top;
        }
    };
}

来看看结果:

这个效果正是我们想要的,可以灵活并且有选择性的移动子 View。我们可以发现,真正的处理滑动的逻辑都是在 callback 这个回调中完成的,这个接口中给我们提供的方法还有很多, 足够应付一般的开发需求,有兴趣的小伙伴可以去试试。

除了上面介绍的 3 种实现 View 的滑动,其实我们还可以通过动画来实现,这里先不总结,有兴趣的小伙伴可以去找一些资料。

如果博客中有什么不正确的地方,还请多多指点。如果觉得我写的不错,请点个赞支持我吧。

谢谢观看。。。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android自定义View

    前几天在郭霖大神的博客上看了自定义View的知识,感觉受益良多,大神毕竟大神。在此总结一下关于Android 自定义View的用法:

    指点
  • Android文件读写和使用SharedPreferences储存数据

    程序的终归目的还是操作数据来达到实现一些特定功能,在Android中,我们可以通过操作文件或者使用SharedPreferences还有数据库来保存一些数据。首...

    指点
  • Android中的权限问题

    在Android程序中,在执行形如访问网络、读取联系人时都要声明权限,在 Android 系统版本小于6.0时,所有的权限只需要在AndroidManifest...

    指点
  • 碎片的简单用法

    Dream城堡
  • [android] 手机卫士自定义组合控件

    调用GridView对象的setOnItemClickListenner()方法,参数:OnItemClickListenner对象

    陶士涵
  • StackView实现卡片堆叠如此简单

    上一期学习了AdapterViewFilpper的使用,你已经掌握了吗?本期开始学习同系列的StackView控件的使用方法。 一、认识StackView ...

    分享达人秀
  • android galley实现画廊效果

    今天在做一个软件界面时用到了ImageSwitcher和Gallery控件,在看API时,感觉上面的例子讲的不是很具体,效率并不高。在这里我就以一个图片浏览功能...

    xiangzhihong
  • Android仿淘宝购物车,玩转电商购物车

    用户2032165
  • 手把手教你写《雷神》游戏(二)

    构造函数里面默认menubg是正在播放的。 然后设置ProgressBar,TextView为隐藏 然后设置几个button的click事件 组后设...

    提莫队长
  • Android TextView 属性大全

    Android 中我们知道有一个使用频率非常高的控件,它就是 TextView,但是它的属性特别多,今天我们就来探究下,它都有哪些属性。

    IT大飞说

扫码关注云+社区

领取腾讯云代金券