View的滑动实现方式
转载请以链接形式标明出处:
本文出自:103style的博客
《Android开发艺术探索》 学习记录
base on Android-29
可以带着以下问题来看本文:
- scrollTo 和 scrollBy 改变是 View 的什么属性?
- 补间动画和属性动画的使用?
- 如何改变 View 的LayoutParams ?
- Scroller实现平滑滑动的原理?
目录
- scrollTo 和 scrollBy
- 使用动画
- 改变布局参数
- 弹性滑动Scroller
- 问题的解答
scrollTo 和 scrollBy
我们先来看看这两个方法的源码:
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();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
我们可以看到 scrollBy
也是调用了 scrollTo
方法。
首先我们先看下 mScrollX 和 mScrollY 的示例图:
通过上图,我们可以很明显的看出:
当View的内容往左往上时,mScrollX 和 mScrollY 为正。
当View的内容往右往下时,mScrollX 和 mScrollY 为负。
也就是说在View的坐标系中, mScrollX、mScrollY 分别为View的边缘减去对应内容边缘的大小。
并且 scrollTo 和 scrollBy 改变的是其内容的位置,而不是其在布局中的位置!
我们来看个示例:
//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/bt_anim"
android:layout_width="160dp"
android:layout_height="160dp"
android:text="Anim"
android:textAllCaps="false" />
</LinearLayout>
//MainActivity.java
public class MainActivity extends AppCompatActivity {
private Button btAnim;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btAnim = findViewById(R.id.bt_anim);
btAnim.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
btAnim.scrollTo(100, 100);
}
});
}
}
点击可以明显看到,内容往左上方移动了。
我们把
btAnim.scrollTo(100, 100);
改成 btAnim.scrollTo(-100, -100);
看看,可以看到内容往右下方移动了。scrollTo(-100, -100)
使用动画
动画这块我们后面会单独具体介绍,这里先简单介绍下怎么使用动画来实现滑动。
还记得我们在 View的基础知识介绍 中说到的View的位置参数中的 translationX
、translationY
吗?动画实现滑动就是改变这个属性的值。
下面通过补间动画和属性动画来实现View的滑动:
// res/anim/translation.xml
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="400"
android:toYDelta="400" />
这个补间动画的意思是 将 View 从 (0,0) 在 2s 内移动到 (400,400)。
属性动画的用法则为:
ObjectAnimator.ofFloat(view, "translationX", 0, 400).setDuration(2000).start();
意思是 将 view 的 translationX 属性在两秒内从 0 移动到 400.
//MainActivity.java
public class MainActivity extends AppCompatActivity {
private Button btAnim;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btAnim = findViewById(R.id.bt_anim);
btAnim.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Animation animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.translation);
btAnim.startAnimation(animation);
}
});
btAnim.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
ObjectAnimator
.ofFloat(btAnim, "translationX", 0, 400)
.setDuration(2000)
.start();
return true;
}
});
}
}
上面代码中我们通过监听 btAnim
的点击事件来触发 补间动画,监听长按事件来触发 属性动画。
这里我们需要注意的是 补间动画 实现的平移 实际上只是对View的影像做操作,并不会真正改变View的位置参数。
如果我们添加 android:fillAfter="true"
的话,当动画结束后,则会停在最后的位置。
此时你会发现一个问题,当我们再次点击View时,并不会触发动画效果,但是点击之前的位置则会触发。
我们修改例子来看看。
// res/anim/translation.xml
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="2000"
android:fillAfter="true"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="400"
android:toYDelta="400" />
//MainActivity.java
public class MainActivity extends AppCompatActivity {
private Button btAnim;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btAnim = findViewById(R.id.bt_anim);
btAnim.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
show();
Animation animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.translation);
btAnim.startAnimation(animation);
}
});
btAnim.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
show();
ObjectAnimator
.ofFloat(btAnim, "translationX", 0, 400)
.setDuration(2000)
.start();
return true;
}
});
}
private void show(){
Toast.makeText(this,"start anim", Toast.LENGTH_SHORT).show();
}
}
补间动画
属性动画
我们可以看到 补间动画 完了之后,只有点击之前所在位置才能触发点击事件, 而 属性动画 则只有点到View所在的位置才会触发长按事件。
是什么原因我们后面在将动画的时候再介绍吧。
改变布局参数
改变布局参数很简单,就是改变其 LayoutParams,我们可以通过 View.getLayoutParams()
来获取这个布局参数,然后修改属性, 再通过 setLayoutParams()
或者 requestLayout()
来重新布局。
具体我们来下面这个示例:
//MainActivity.java
public class MainActivity extends AppCompatActivity {
private Button btAnim;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btAnim = findViewById(R.id.bt_anim);
btAnim.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
changeLayoutParams();
}
});
}
private void changeLayoutParams() {
LinearLayout.LayoutParams llp = (LinearLayout.LayoutParams) btAnim.getLayoutParams();
llp.width += 100;
llp.height += 100;
llp.setMarginStart(llp.getMarginStart() + 50);
llp.topMargin += 50;
btAnim.setLayoutParams(llp);
// btAnim.requestLayout();
}
}
我们通过每次点击使其 宽高 增加 100px
, 左边距 和 上边距 增加 50px
,效果图如下:
这里我们先来总结下上面三种实现滑动的方法:
- scollTo/scollBy : 操作简单,只能移动View的内容。
- 动画:操作简单,主要用于没有交互的View 和 复杂的动画效果
- 改变布局参数:操作稍微复杂,适用于有交互的View.
通过效果图,我们可以很明显的看到 scollTo/scollBy 和 改变布局参数 这两种实现滑动的方法 效果比较生硬,用户体验不太好。 动画 方式实现的效果则会体验好很多。
下面我们来介绍通过 Scroller 来实先动画那样用户体验相对较好的 的滑动效果。
弹性滑动Scroller
我们在 View的基础知识介绍 中有介绍 Scroller 的用法,再重新回顾下:
- 创建一个Scroller;
- 重写
view
的computeScroll
方法; - 然后通过
mScroller.startScroll()
来实现滑动。
//TestScroller.java
public class TestScroller extends TextView {
Scroller mScroller;
public TestScroller(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
public void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int scrollY = getScrollY();
int deltaX = destX - scrollX;
int deltaY = destY - scrollY;
mScroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000);
invalidate();
}
}
//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.lxk.viewdemo.TestScroller
android:id="@+id/tv"
android:layout_width="320dp"
android:layout_height="320dp"
android:layout_margin="8dp"
android:background="@color/colorAccent"
android:gravity="center"
android:padding="8dp"
android:text="Hello World!" />
</LinearLayout>
//MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TestScroller scroller = findViewById(R.id.tv);
scroller.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
scroller.smoothScrollTo(200, 200);
}
});
}
运行,可以看到点击之后,内容在 1s
内往左上方各平移了 200px
, 并且改变的也是View的内容。
首先我们来看看 smoothScrollTo
中调用的 Scroller
的 startScroll
方法,我们可以看到它其实只是保存我们传入的参数。
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;//滑动的事件
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;//滑动的起点
mStartY = startY;//滑动的起点
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;//滑动的距离
mDeltaY = dy;//滑动的距离
mDurationReciprocal = 1.0f / (float) mDuration;
}
然后通过在 smoothScrollTo
调用 invalidate()
方法,通过 invalidate()
触发重绘,来调用 computeScroll
方法,
然后通过Scroller.computeScrollOffset()
判断状态,
满足则通过 mScroller.getCurrX()
和 mScroller.getCurrY()
获取当前的位置,
然后通过 scrollTo
实现滑动,
然后通过 postInvalidate()
来继续触发重绘。
我们来看看 Scroller
的 computeScrollOffset()
方法:
//Scroller.java
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
//通过插值器来计算对应时间对应的值
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
...
}
} else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
我们可以看到里面通过插值器来计算对应时间对应的 mCurrX
、mCurrY
:
mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
然后通过重绘来达到平滑滑动的效果。
插值器 属于 动画那块的内容,我们在将动画的时候在具体介绍,暂时当它是一个数学函数就可以了。
至此,我们大致知道了 Scroller实现滑动的原理为:
我们通过 Scroller 的 startScroll()
来设置要滑动的位置,
然后通过 invalidate()
触发重绘 来调用 View 的 computeScroll()
方法,
然后在 Scroller 的computeScrollOffset()
中 通过插值器计算 这个滑动时间中 每个时间点对应的 目标距离,
然后再通过 scrollTo()
滑动这个时间点对应的距离,
然后继续重绘到对应时间点来实现滑动。
所以实际上 Scroller
本身并不能实现View的滑动,他需要配合View的 computeScroll()
方法才能达到平滑滑动的效果。
问题解答
-
scrollTo 和 scrollBy 改变是 View 的什么属性?
A:mScrollX 和 mScrollY,内容往上往左滑 这两个值为正, 反之为负。 -
补间动画和属性动画的使用?
A: 补间动画: 通过AnimationUtils.loadAnimation(context, R.anim.translation)
来获取补间动画,然后通过view.startAnimation(animation)
来执行动画,补间动画进改变View的影像,并不改变其实际位置,所以点击事件只有点击原位置才会响应。
属性动画:通过ObjectAnimator.ofFloat(view, 对应属性, 起始值, 结束值).setDuration(时间).start()
来实现。 -
如何改变 View 的LayoutParams ?
A:通过View.getLayoutParams()
获取LayoutParams,然后修改宽高、边距等,再通过setLayoutParams()
或者requestLayout()
来重新布局。 -
Scroller实现平滑滑动的原理?
A:问题解答标题 上面就有,就不再赘述了。
如果觉得不错的话,请帮忙点个赞呗。
以上
扫描下面的二维码,关注我的公众号 103Tech, 点关注,不迷路。
`