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 的滑动,其实我们还可以通过动画来实现,这里先不总结,有兴趣的小伙伴可以去找一些资料。
谢谢观看。。。