原文在这里->Making your own 3D list – Part 3 (final part)
Adding dynamics
为了让我们的listview更加灵活,我们把物理效果的实现委托给一个专门负责滑动物理特性的类,让使用者设置自己想要的效果。很容易想到不同的使用方式会有不同的滑动特性,所以这样做以后我们可以轻松的替换它,还可以在其他view上重用这些物理效果。
为了能使用不同的物理效果,我们首先要有一个供listview使用的统一接口,下面是一个抽象类,使用者去继承实现:
public abstract class Dynamics {
private static final int MAX_TIMESTEP = 50;
protected float mPosition;
protected float mVelocity;
protected float 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);
}
很简单,包含了位置和速度,还存储了上一次更新的时间来计算时间差。有一系列的set方法来初始化状态值,update()方法计算时间差,然后调用抽象方法onUpdate(),就是这个方法具体实现位置和速度的更新,依赖于子类的实现。
我们在这个应用里只使用一个简单的物理特性:
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;
}
}
上面的实现使用了摩擦力模型来减小速度,位置使用标准的欧拉积分来通过速度得出,速度的单位是像素/秒,所以要除以1000。
Moving without touching
对于滑动和回弹有一个共同点就是我们需要一种机制让列表在离开手势操作的情况下任然能够移动,现在,移动列表的唯一时刻就是我们收到手势事件时,也就是说,只有当用户触摸屏幕时才会有反应。我们需要一种方式来让列表脱离手指后继续移动。
一种方便的方法是post一个Runnable,当我们想产生动态效果时就运行runnable,在run方法里更新列表位置,然后检测是否需要下一帧的效果,如果需要,就继续post,下面是一个使用物理特性的Runnable:
mDynamicsRunnable = new Runnable() {
private static final float VELOCITY_TOLERANCE = 0.5f;
private static final float POSITION_TOLERANCE = 0.4F;
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);
}
}
};
为什么使用AnimationUtils.currentAnimationTimeMillis()?像这样的过渡动画不建议使用System.currentTImeMills(),因为会随时被用户或其他应用程序改变,而currentAnimationTimeMillis()不会这样。
scrollList方法和之前的一样,改变mListTop然后requestLayout,onLayout()方法就会被调用然后绘制子view。
Flinging
现在我们有了Dynamics抽象类和它的一个实现SimpleDynamics,知道怎么用Runnable来应用它,就剩下控制列表了。我们需要在用户滑开屏幕时开始过渡,需要首先给dynamics对象设置速度和位置,然后post runnable,在endTouch()里:
if (mDynamics != null) {
mDynamics.setState(mListTop, velocity,
AnimationUtils.currentAnimationTimeMillis());
post(mDynamicsRunnable);
}
我们也要修改endTouch()方法接受一个float型的速度变量,获取手势速度使用VelocityTracker,要使用这个首先要obtain一个实例,我们把它添加在startTouch()方法里:
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(event);
然后在每次move事件里将反馈给velocityTracker,然后,我们修改up事件的处理:
case MotionEvent.ACTION_UP:
float velocity = 0;
if (mCurrentTouchState == TOUCH_STATE_CLICK) {
clickChildAt((int) event.getX(), (int) event.getY());
} else if (mCurrentTouchState == TOUCH_STATE_SCROLL) {
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND);
velocity = mVelocityTracker.getYVelocity();
}
endTouch(velocity);
break;
现在就剩下一件事了。当用户再次触摸屏幕,我们需要停下过渡动画,所以,在startTouch()里添加removeCallbacks()来讲dynamics的Runnable移除。
Accepting our limits
现在还有一个问题是很容易将列表滚出边界,所以我们需要一些限制条件,让列表有一些弹性效果。
首先我们来给Dynamics类添加一些最大和最小值限制。
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;
}
我们为min和max positions添加了一些设置函数和一个返回阈值距离的函数,这样即使速度为0,如果不在限制范围内过渡也不会停。
下一步就是修改SimpleDynamic类了,实际上只需要在onUpdate()开头添加下面这行:
mVelocity += getDistanceToLimit() * mSnapToFactor;
如果列表滑出了限制范围(getDistanceToLimit()返回了除0之外的任何数),我们会根据这个距离修改速度,这样列表就会像个橡皮筋一样,具体效果取决于friction和snapToFactor这两个参数,当然你可以选择更好的算法。
现在我们还需要设置最大和最小值,因为我们将列表第一项的top作为列表的位置参数,列表向上滑动,所以0是最大值,最小值是一个负数。如果当前列表最后一项可见了,那现在的mListTop就是最小值,scrollList()是改变列表位置的唯一入口,所以我们在这里添加代码:
if (mLastSnapPos == Integer.MIN_VALUE
&& mLastItemPosition == mAdapter.getCount() - 1
&& getChildBottom(getChildAt(getChildCount() - 1)) < getHeight()) {
mLastSnapPos = mListTop;
mDynamics.setMinPosition(mLastSnapPos);
}
Snapping
到目前为止列表项的滚动角度依赖于列表的位置,所有项都面对屏幕的时候是最佳位置,下面我们就来做些校准。
我们可以使用同一个dynamics类,然而,还需要每次重设snap点,因为有可能我们滚动到了下一个对齐位置,下面的setSnapPoint在scrollList()中调用。
private void setSnapPoint() {
final int rotation = mListRotation % 90;
int snapPosition = 0;
if (rotation < 45) {
snapPosition = (-(mListRotation - rotation) * getHeight())
/ DEGREES_PER_SCREEN;
} else {
snapPosition = (-(mListRotation + 90 - rotation) * getHeight())
/ DEGREES_PER_SCREEN;
}
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);
}
最终效果图如下,模拟器比较慢
代码在这里->http://download.csdn.net/detail/xu_fu/7056297