前言:
- 本文翻译于 sonymobile 的一系列教程,指导 Android 开发人员如何制作3D风格的ListView,共三部分。这是第三部分。接上篇, 在上一节的基础上,这一节主要实现的是一些列表滑动时的行为,例如如何速滑,回弹,跳跃。本文结尾可以看到效果图。可以在文章结尾找到代码下载链接,下载代码运行就能看到完整的效果,文章结尾的代码是最终的成果,代码不多,就几个文件。
正文:
原文:
Introduction
This is the third and final article in the series of how to make your own list view. Right now we have a basic working list with some nice graphics. Click here to go to the previus part of this tutorial. In this article we will add some behavior to our list and add the fling and bounce/snap effects. Fling support is in my view mandatory for any list where you navigate by touch. As a user I wouldn’t expect that the list simply stops when I lift my finger from the touch screen. If I give the list a velocity, I expect it to continue scrolling for a while, and gradually slow down until it comes to a halt. Fortunately, supporting fling is no big deal. In fact it’s very simple. Below is the source code for this part of the tutorial ready to be set up in e.g. Eclipse. And as usual: Don’t forget to download the ‘Sony Ericsson Tutorials’ app from Android market where all sample apps for this and other tutorials are collected.
译文:
介绍
这是第三部分也是《怎样制作你自己的ListView》这一系列的最后一篇文章了。现在我们有一个基本上可以工作并且绘制得很好的ListView。点击这儿可以链接到这个课程的前一部分。在这篇文章我们将会给列表添加一些行为,并添加一些速滑,跳跃和回弹效果。在我看来,支持速滑,对任何通过触摸操作的列表来说是强制性的。作为一个用户我不希望当我在触摸屏幕时抬起手指,列表就简单停下来。如果我给列表一个速度,我希望它继续滑动一会儿,然后逐渐慢下来直到它停住。幸运的是,支持速滑没有多大工作量,实际上非常简单,下面是这节课的这一部分准备好的代码,例如在eclipse中建立起来的。并且和往常一样,不要忘了从Android 市场下载‘Sony Ericsson Tutorials’ 应用,所有这节课和其他课程的示例应用都收集在那里(译注,我找不到APP了,示例代码在文章结尾)。
原文:
Adding dynamics
To make our list more customizable we are going to delegate the dynamics of the flinging to another class and letting the application set the specific dynamic class it wants to our list. It’s quite easy to imagine that different usages of a list could use different dynamics so doing like this will let us easily change the dynamics as well as reusing them for other views.
译文:
添加动力效果
为了使我们的列表更加可定制,我们将委派速滑动力效果到另外一个类,让应用给我们的列表设置它想要的特定动力类。很容易想象不同用途的列表应当使用不同的动力效果,这样做会使我们容易替换动力效果,同样也容易在其他view中复用。
原文:
For the list to be able to handle different dynamics we need of course to have a common interface that the list can use. Below is an abstract class that an application can extend and implement.
译文:
对列表来说能够处理不同的动力,我们当然需要有一个列表可以使用的通用接口。下面是应用可以扩展和实现一个抽象类。
public abstract class Dynamics {
private static final int MAX_TIMESTEP = 50;
protected float mPosition;
protected float mVelocity;
protected long mLastTime = 0;
public void setState(final float position, final float velocity, final long now) {
mVelocity = velocity;
mPosition = position;
mLastTime = now;
}
public float getPosition() {
return mPosition;
}
public float getVelocity() {
return mVelocity;
}
public boolean isAtRest(final float velocityTolerance) {
return Math.abs(mVelocity) < velocityTolerance
}
public void update(final long now) {
int dt = (int)(now - mLastTime);
if (dt > MAX_TIMESTEP) {
dt = MAX_TIMESTEP;
}
onUpdate(dt);
mLastTime = now;
}
abstract protected void onUpdate(int dt);
}
原文:
It’s pretty simple. It has a position and a velocity. It also stores the last time it was updated to be able to calculate the time between to updates. There’s a set method that the list will use to initialize the dynamic each time we start an animation, get methods for position and velocity, a method to find out if the dynamics is at rest and finally a function to update position and velocity based on a new time. The update method computes the delta time, then it calls the protected method onUpdate(). It is this function that actually update the position and velocity and it’s up to the extending class to implement this.
译文:
它非常简单,有一个position (位置)和一个velocity(速度)。它还保存了最后一次更新的时间,用来计算更新的时间间隔。有一个set方法,列表在每次我们开始动画时,可以用它来初始化动力。position 和 velocity 的get方法,一个找出动力是否静止的方法,最后一个方法是根据新的时间更新position 和 velocity 的。update 方法计算时间差值,然后调用protected 的 onUpdate() 方法,这个方法实际修改position 和 velocity, 它取决于扩展类的实现。
原文:
For this application we will use a pretty simple dynamics.
译文:
对这个应用我们一个非常好的示例动力。
class SimpleDynamics extends Dynamics {
private float mFrictionFactor;
public SimpleDynamics(final float frictionFactor) {
mFrictionFactor = frictionFactor;
}
@Override
protected void onUpdate(final int dt) {
mPosition += mVelocity * dt / 1000;
mVelocity *= mFrictionFactor;
}
}
原文:
This is an implementation that models friction as something that decreases the velocity by a fixed number of percent each frame. The position is calculated from the velocity by using standard Euler integration. I didn’t mention it before but the unit for the velocity is pixels per second, that’s the reason for the divide by 1000. This is about as simple as it gets, but it works very well in my opinion.
译文:
这是一个模拟摩擦力的实现,就像每一帧有东西按一个固定的百分比数字减慢速度。位置通过标准的欧拉方法 Euler integration根据速度计算出来。我在前面没有提到它,但是速度的单位是每秒钟的像素数量。那就是除以1000的原因。这段讲的是简单取得摩擦力,但在我看来它工作得很好。
原文:
Moving without touching
One thing that’s common for both fling and bounce/snap is that we need a mechanism to move the list without relying on touch. Right now, the only time we move the list is when we receive touch move events, in other words, only when the user is touching the screen. What we need is a way to continuously update the position for as long as we want that is separated from touch events.
译文:
没有触摸的移动
对于速滑(fling)和跳跃(bounce)/ 回弹(snap)来说很常见的一件事是我们需要一个不依赖触摸的移动列表机制。现在,我们移动列表的时机只是在我们接收到了触摸和移动事件(touch move ),只有用户在触摸屏幕的时候。我们需要的办法是:离开触摸事件以后,持续更新位置直到我们想要的。
原文:
A convenient way to do that is to use a Runnable. Whenever we want to start an animation we post the runnable, and in the run method we update the position of the list. Then we check if we need another frame of the animation. If we do, the runnable post itself to the view. Below is a Runnable that uses the dynamics class.
译文:
一个渐变的办法是使用一个 Runnable,无论何时我们想开始一个动画,我们post 这个 runnable(译注:Android标准用法,没法翻译),在run方法我们更新列表的位置。然后我们检查这个动画是否需要另外一帧,如果需要,runnable把它自己post到这个View。下面是一个使用动力类的Runnable。
mDynamicsRunnable = new Runnable() {
public void run() {
// set the start position
mListTopStart = getChildTop(getChildAt(0)) - mListTopOffset;
// calculate the new position
mDynamics.update(AnimationUtils.currentAnimationTimeMillis());
// update the list position
scrollList((int)mDynamics.getPosition() - mListTopStart);
// if we are not at rest...
if (!mDynamics.isAtRest(VELOCITY_TOLERANCE)) {
// ...schedule a new frame
postDelayed(this, 16);
}
}
};
原文:
Why use AnimationUtils.currentAnimationTimeMillis()? Well, for animation purposes like this it’s a bad idea to use System.currentTimeMillis() since it can be changed at any time by the user or any other application. CurrentAnimationTimeMillis() instead uses SystemClock.uptimeMillis() which won’t change unexpectedly (and if we want to we could just as well used that one directly).
译文:
为什么使用AnimationUtils.currentAnimationTimeMillis(),为了动画目的使用 System.currentTimeMillis()是一个不好的主意,由于它可以被用户或者其他任何应用在任何时刻修改。CurrentAnimationTimeMillis() 使用SystemClock.uptimeMillis() 这个时间,它不会被意外地修改(如果我们愿意,我们可以直接使用那一个(译注: 即可以直接使用SystemClock.uptimeMillis()))。
原文:
The scrollList() method is the same as before (roughly unchanged since the first part) but it might be a good idea to go through what will happen. scrollList() will change the mListTop variable and then request a layout. Then our onLayout() method will be called and the child views will be positioned correctly and any new views will added and views that scroll outside will be removed. Then onLayout() invalidates the list. This triggers a draw call and the list is redrawn.
译文:
scrollList() 方法和前面是一样的(大体上从第一节课以后就没有改变),但是它可能是完成即将发生的事情的一个很好的想法。 scrollList()会改变mListTop 变量,然后请求布局(layout),然后我们的 onLayout() 方法会被调用,子view会被正确的放置,任何新的view会被添加,滑出的view会被移除,然后onLayout() 使列表无效(invalidates ),这个会触发一个绘制的调用,列表会被重绘。
原文:
Flinging
OK, now we have our abstract Dynamics class, our specific implementation of it, SimpleDynamics, and we know how to apply it using the Runnable. Now we just need to handle it in the list. We need to start the fling whenever the user lets go of the screen. Starting the fling means we need to set the velocity and position to the dynamics object and then post the runnable. In endTouch():
译文:
速滑
现在我们有抽象的Dynamics 类。我们对于它的具体实现是SimpleDynamics,并且我们知道如何把它用到Runnable上。现在我们仅仅需要在列表中处理它。我们需要在用户让屏幕运动的时候速滑。开始速滑意味着我们需要给动力对象设置速度和位置,并且post那个runnable。在endTouch()方法中:
if (mDynamics != null) {
mDynamics.setState(mListTop, velocity, AnimationUtils.currentAnimationTimeMillis());
post(mDynamicsRunnable);
}
原文:
We also modify the endTouch() method to take the velocity as a float. To get the velocity of the touch gesture we use the VelocityTracker. To use it we first need to obtain one. In the startTouch() method, we add the following.
译文:
我们还修改了 endTouch() 方法把 velocity (译注:速度)看做是一个浮点型数据。为了得到触摸手势的速度,我们用了VelocityTracker(译注:速度追踪器),用它之前我们首先需要获取一个,在 startTouch() 方法中我们添加了如下代码:
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(event);
原文:
Then we also need to make sure we recycle it when we don’t need it. In the endTouch() method we add this:
译文:
然后我们还需要确保在不需要它的时候回收了,在endTouch() 方法中我们添加如下:
mVelocityTracker.recycle();
mVelocityTracker = null;
原文:
For each move event we feed the velocity tracker the motion event. Then, we modify our handling of UP events to look like this.
译文:
对于每一个移动事件,我们给把动作事件喂给速度追踪器(velocity tracker ),然后我们修改up事件处理如下面的样子:
case MotionEvent.ACTION_UP:
float velocity = 0;
if (mTouchState == TOUCH_STATE_CLICK) {
clickChildAt((int)event.getX(), (int)event.getY());
} else if (mTouchState == TOUCH_STATE_SCROLL) {
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND);
velocity = mVelocityTracker.getYVelocity();
}
endTouch(velocity);
break;
default:
endTouch(0);
break;
原文:
What’s new here is the else statement handling up events in case of a scroll. We feed the final up event to the velocity tracker and then we calculate the velocity (in pixels per second) and get the Y-part of the velocity (we are not interested in the X-part of the velocity).
译文:
这里新增的是处理滑动情形下up事件的else语句,我们把最终的up事件喂给速度追踪器(velocity tracker)然后我们计算速度(每秒钟的像素数量)并且得到Y方向的速度(我们对X方向的速度不感兴趣)。
原文:
Now it’s only one thing left until we have a working fling. Whenever the user starts touching the list again, we need to stop any fling animation we have. So, in startTouch() we add a call to removeCallbacks() to remove the dynamics Runnable if it’s posted.
译文:
现在只剩下一件事,直到我们处理速滑。无论何时当用户开始再次触摸列表,我们需要停止任何速滑动画。因此,在startTouch() 方法中我们添加了一个removeCallbacks()的调用,来删除它post过的动力的Runnable 。
原文:
Accepting our limits
If we try it out now, it works nice, but an old problem that we’ve ignored until now becomes even more obvious. It is very easy to scroll and fling the list so all the items disappears. What we need is some limits to how we can scroll the list. However, instead of looking at the limits as absolute, we’re going to think of them as a bit “bendable”.
译文:
接受我们的限制
如果我们现在试验它,它工作得挺好,但是我们曾经忽略过的一个老问题现在变得更加明显,列表很容易滑动或者速滑以至于所有的条目都消失。如何滑动列表,我们需要一些限制,然而,不是绝对地寻找界限,我们把它们(译注:ListView的item)想象成有点可变弯曲(“bendable”)。
原文:
First let’s add some max and min limits to our Dynamics class.
译文:
首先我们给Dynamics 类添加一些最大和最小限制。
protected float mMaxPosition = Float.MAX_VALUE;
protected float mMinPosition = -Float.MAX_VALUE;
public boolean isAtRest(final float velocityTolerance, final float positionTolerance) {
final boolean standingStill = Math.abs(mVelocity) < velocityTolerance;
final boolean withinLimits = mPosition - positionTolerance mMinPosition;
return standingStill && withinLimits;
}
public void setMaxPosition(final float maxPosition) {
mMaxPosition = maxPosition;
}
public void setMinPosition(final float minPosition) {
mMinPosition = minPosition;
}
protected float getDistanceToLimit() {
float distanceToLimit = 0;
if (mPosition > mMaxPosition) {
distanceToLimit = mMaxPosition - mPosition;
} else if (mPosition < mMinPosition) {
distanceToLimit = mMinPosition - mPosition;
}
return distanceToLimit;
}
原文:
We’ve added set functions for the min and max positions and a method that returns how much outside the limits we are. We’ve also modified the isAtRest() method. Even if the velocity is 0, if we are not within our limits then we are not at rest.
The next step is to handle the limits in our SimpleDynamic class. And actually, the only thing (almost) we are going to do is to add this line at the top of onUpdate()
译文:
我们给最小位置和最大位置添加了set方法,另一个方法返回界外的限制,我们还修改了isAtRest()方法,甚至如果速度是0,如果我们不再边界范围内,我们不休息(译注:rest,可以理解成后来的RecyclerView.SCROLL_STATE_IDLE)。
下一步是在SimpleDynamic 类中处理这些limits。并且实际上,我们要做的唯一(几乎)一件事是在onUpdate()顶部增加这样一行代码。
mVelocity += getDistanceToLimit() * mSnapToFactor;
原文:
If we are outside of our limits (if getDistnaceToLimit() returns anything other than 0) we modify the velocity based on the distance to the limit. This will behave as a spring, how bouncy the spring is depends on the friction and the snap to factor. This is also about as simple as you can make this, and there are lots of more advanced algorithms that you can use instead if you want to. I like the simplicity of it and depending on the use, the behavior will often be quite nice. If you switch to modifying the position instead of the velocity you will get something that exponentially decreases the distance to the limit, looking like a critically damped spring (without any bounce).
译文:
如果我们在界限的外面(如果getDistnaceToLimit()返回任何非0数)(译注:此处是笔误,应该是getDistanceToLimit()),我们基于距离边界的间隔修改速度。我们表现得像弹簧一样,弹簧的弹性是多少取决于摩擦力和回弹的因子。实现这个也非常简单, 并且有很多先进的算法可以替换。我喜欢简单实用的,并且通常表现也挺好。如果你把修改位置变换成修改速度,你会得到一些边界距离指数下降的问题,看起来像是严重抑制的弹簧(没有任何弹性)。
原文:
Now, we also need to set the max and min positions in the list. Since we defined the position of the list as the position of the first item the position of the list will be 0 when we start. If we scroll up the position is going to be negative so the list position will be between 0 and some negative value which will depend on how many items we have and the height of them. However, we know that 0 is our maximum position so we can set that directly.
译文:
现在我们还需要设置列表的最大和最小位置,由于我们是根据列表中第一个条目的位置定义列表的位置(position),所以列表的位置一开始是0。如果我们往上滑,position会变成负数,所以列表的位置会在0和一些负数之间,取决于条目的数量和它们各自的高度(译注:应该是滑出去的条目数量及它们的高度总和,不过从效果看,这个列表向上可以滑动到所有的item都不可见),尽管如此,我们知道0是我们最大的position,所以可以直接设置它。
原文:
As for the minimum position we can set that first when we know where it is. If our last visible item position is equal to the item count minus one we know we are showing the last item. If also the bottom of that item is less than the height of the view then we have scrolled passed the limit and can set the current position as the minimum limit. scrollList() is the only place where we change the list position so we can add the code there.
译文:
由于我们可设置的最小位置(position )是在我们第一次知道它的所在。如果我们最后可见的条目位置等于条目数量减去1,我们就知道我们显示了最后一个条目(译注:假如有20个item,最后一个可见的item是19,说明列表已经展示到最后了)。再如果,这个条目的底(bottom)比view的高度小,那么我们已经滑过界限,可以设置当前position 作为最小界限,scrollList()是我们修改列表位置的唯一地方,我们可以在那儿添加代码。
if (mLastSnapPos == Integer.MIN_VALUE && mLastItemPosition == mAdapter.getCount() - 1
&& getChildBottom(getChildAt(getChildCount() - 1)) < getHeight()) {
// then save the last snap position and snap to it
mLastSnapPos = mListTop;
mDynamics.setMinPosition(mLastSnapPos);
}
原文:
Snapping
Due to the fact that the items of the list rotate based on the position of the list, some positions are, in a way, better than others. The positions where all items are facing towards the screen are the positions that gives the best view of the list. Let’s recognize this fact by snapping to these positions, that is, when the user lets go of the screen we animate the list to the closest position where the items are rotated so they face the screen.
译文:
回弹
一个事实是:由于列表中条目的旋转是根据它们在列表中的位置(译注:计算的),一些位置在某种程度上,比其他位置好。所有条目都面朝屏幕的位置给了列表最好的视野,让我们通过回弹到这些位置承认这个事实,那就是,当用户让屏幕移动,我们推动列表到使它的条目旋转到面朝屏幕的位置。
原文:
We can use the same dynamics class for the snapping by simply setting both max and min positions to the snap position. However, we need to re-set this snap point every time we scroll since we might have scrolled so that another snap position is closer. The below method setSnapPoint() is called from scrollList().
译文:
我们可以使用相同的动力类来回弹,通过简单的设置最大和最小位置给回弹位置,然而,我们需要在每次滑动的时候重新设置这个回弹点,由于我们已经滑动,所以另一个回弹点离得更近。在scrollList()方法中调用下面的方法setSnapPoint()。
private void setSnapPoint() {
final int rotation = mListRotation % 90;
int snapPosition = 0;
// set snap position that corresponds to closest 90 degree rotation
if (rotation < 45) {
snapPosition = (-(mListRotation - rotation) * getHeight()) / DEGREES_PER_SCREEN;
} else {
snapPosition = (-(mListRotation + 90 - rotation) * getHeight())
/ DEGREES_PER_SCREEN;
}
// if we haven't set mLastSnapPos before and...
// the last item is added as a child and..
// it's bottom edge is visible
if (mLastSnapPos == Integer.MIN_VALUE && mLastItemPosition == mAdapter.getCount() - 1
&& getChildBottom(getChildAt(getChildCount() - 1)) 0) {
snapPosition = 0;
} else if (snapPosition < mLastSnapPos) {
snapPosition = mLastSnapPos;
}
mDynamics.setMaxPosition(snapPosition);
mDynamics.setMinPosition(snapPosition);
}
原文:
The method starts with checking the current rotation and determining what 90 degree rotation is closest and then converts this rotation to a list position. The second part is similar to the code snippet before this. It checks if this is the last snap position and if it is, saves the value. Then we make sure the snap position is at most 0 and not less than the last snap position and set the snap position as both the max and min position of the dynamics.
译文:
这个方法开始检查了当前的旋转角度,决定90度旋转是最接近的,并把这个角度转换成列表的位置。第二部分和这之前的代码片段相似,它检查者是不是最后回弹的位置,如果是,保存值,然后我们确保回弹位置最大是0并且不比最后一次回弹的位置小,设置回弹位置作为最大和最小的位置给dynamics。
原文:
This is not the end
Now we have reached the end of this series of tutorials. We now have a list that 1) supports basic features (we can scroll it and click and long press on items), 2) looks nice (even a bit over the top if you ask me) and 3) has a nice behavior (we can fling it and it can both bounce at the limits or snap to certain positions).
译文:
这不是结尾(译注:这是这系列课程的结尾,第三课的结尾)
现在我们已经到达了这系列课程的结尾,我们现在有一个列表:1)支持基本的功能(我们可以滑动它,点击它和长按它),2)看起来很好(我认为有点点夸大了)和 3)有一个很好的表现(我们可以猛推它,它既可以在边界跳跃,或者回弹到特定的位置)
原文:
But, hopefully, it’s not the end of this list code. There are a lot of things that can be improved or tweaked and many features are still waiting to be implemented. For example, the list as it is now does not support different types of items, scrollbars, or dividers. The list also does not register an observer on the adapter so it is not aware of changes to the content. When it comes to the graphical look of the list it can of course be modified into more or less an infinite number of designs. And why not add animations when items are clicked or long pressed?
译文:
但是,希望这不是这个列表代码的终点。还有很多事情可以提升或者调整。很多特征仍然在等待实现。例如,列表现在还不支持不同类型的条目,滑动条,或者分割线。列表还不能在Adapter上注册一个观察者,所以它不能感知内容的变化。谈到它生动的外观,它当然或多或少可以被修改成一个无限数量的设计。为啥不在条目点击和长按的时候添加动画。
原文:
From here on I leave it to you. This list is a base to start from and, depending on your needs, you will probably need to modify, tweak and develop the list in different directions in order to use it for your application, prototype or what ever it might be. If you use and modify it, we would really appreciate if you let us know. Mail us with a link to an blog post, make a comment here, or even better, record a YouTube video like this one. We would love to see what you can come up with.
译文:
在这儿我把它留给你们,这个列表是一个开始的基础,而且,根据你的需要,你可能在不同的方向需要修改,调整,和开发这个列表,使它可以运用到你的应用,原型或其他可以存在的地方。如果你使用并修改它,如果你让我们知道,我们会非常感激。给我们发邮件,附上微博的转发连接,在这儿写评论,或者甚至更好,录一个像这样的YouTube 视频 。我们非常喜欢看到你用它发生了什么。
-
评注:
这一系列课程共三节,到这里就结束了,先在下面贴出最终的效果图,是一个翻滚的ListView。
也可以通过学习这系列课程了解自定义View以及ListView的内部实现。
effect.gif
代码下载