前言:
- 本文翻译于 sonymobile 的一系列教程,指导 Android 开发人员如何用一个手指控制图片的缩放,接第三篇。这一篇在上一篇的基础上,增加了速滑和跳跃的动力效果。关于动力效果,需要参考之前的翻译《[翻译] 制作你自己的 3D ListView 第三部分(最后部分)(Making your own 3D list – Part 3 (final part))》点击查看。这样使得控件更加生动。由于速滑和跳跃的动力效果需要使用到数学和物理知识,这一篇的代码比较难读,我们还是关注他们的实现思路,在熟悉思路以后,有功夫再研究计算过程。同样,本文的结尾会有代码下载链接。
原文:
Welcome to the fourth and final part of the Android tutorial on how to make your own zoom control like the one used in Sony Ericsson X10 Mini in the Camera and Album applications. Click [here] to go to the prevoius part of this tutorial. As usual the source code is included, see below. Don’t forgett to download ‘Sony Ericsson Tutorials’ from Android market to see demos of this and other tutorials in action.
译文:
欢迎来到第四也是最后一部分,这系列Android课程是关于如何制作你自己的缩放控制的,就像Sony Ericsson X10 Mini 手机的相机和相册应用中使用的一样,点击这里查看这个课程的前一部分。和平常一样包含了源代码,见下面的链接(译注:链接没有了,我在文章结尾重新上传源代码了)。不要忘了从Android市场下载Sony Ericsson Tutorials查看这个和其他课程的演示。
原文:
In this part we’ll focus on introducing dynamic behavior to our zoom such as fling and bounce by animating the zoom state. Dynamic behavior adds a lot in terms of looks, feedback and usability.
译文:
在这一部分我们重点给我们的缩放引入动力行为,例如通过缩放状态的动画(zoom state)实现速滑和跳跃,动力行为使得在感觉,反馈和可用性方面提升很多。
原文:
Dynamics
To implement dynamic behavior we’re going to subclass the Dynamics class introduced in [the final part of the list tutorial]. Make sure to read through that tutorial if you want to know more about the Dynamics base class.
译文:
为了实现动力行为我们将要继承列表课程的最后一部分介绍的Dynamics类,如果你想要知道更多关于Dynamics基类,确保通读那个课程。
原文:
The Dynamics class is useful for applying dynamic behavior to a value, the class itself holds a position and a velocity and functionality for setting min and max positions. When subclassing Dynamics we must implement the onUpdate(int) method that is responsible for updating the state. This gives us control over the dynamic behavior and in our Dynamics sub-class we’ll implement basic friction and spring physics to handle fling and edge bounce. If you want to know more about spring physics then this is a nice place to start.
译文:
Dynamics 类对于给一个数值应用动力行为是非常有用的,这个类自己保存了一个位置和一个速度,以及一些设置最小和最大位置的方法。要扩展Dynamics类我们必须实现一个onUpdate(int)方法,它负责更新状态。这让我们控制动力行为,在我们自己的Dynamics 子类我们要实现基本的摩擦和弹簧的物理属性来处理速滑和边缘跳动。如果你想要了解更多关于弹簧物理属性,那么这里 是一个很好的开端。
原文:
Let start by defining the necessary attributes for our dynamic behavior. The friction factor will be used to decelerate the fling animation when we are inside the pan limits, and the stiffness and damping factor will be used to simulate a spring pulling the zoom window back to the content if we are outside of the pan limits. To simplify setting up the physics we let the user specify a damping ratio which we then internally recalculate to the damping factor we use in our calculations. Damping ratio is easy to use, basically a value of less than 1 makes the animation overshoot while a value of 1 or more doesn’t. Here you can learn more about damping.
译文:
让我们开始给动力行为定义必要的属性,当我们在平放限制(pan limits)范围里面时,摩擦因子(friction factor )会被用来给速滑动画减速。如果我们在平放限制的外面,刚度(stiffness )和阻尼(damping )因子(译注:弹簧的两个属性)要被用来模拟一个弹簧把缩放窗体拉回到内容区域。为了简便设立物理属性我们让用户指定一个阻尼比例,然后用它在内部重新计算我们的运算中所要使用的阻尼系数。阻尼比例非常容易用,基本上是,一个小于1的值会使动画超过范围,等于或者大于1的值不会。这里你可以学到更多关于阻尼的知识.
public class SpringDynamics extends Dynamics {
private float mFriction;
private float mStiffness;
private float mDamping;
public void setFriction(float friction) {
mFriction = friction;
}
public void setSpring(float stiffness, float dampingRatio) {
mStiffness = stiffness;
mDamping = dampingRatio * 2 * (float)Math.sqrt(stiffness);
}
原文:
Next we’ll implement the onUpdate() method which is responsible for updating the position and velocity of the Dynamics object. The input parameter, dt, is the number of milliseconds passed since the method was previously called. (By the way: dt stands for delta time. Delta is commonly used in mathematics and denotes change and thus: dt = change in time.)
译文:
接下来我们要实现onUpdate()方法,由它负责更新Dynamics 类的对象的位置和速度。输入参数dt,是相距上一次调用这个方法度过的毫秒数值。(顺便说一下,dt表示 delta time, delta 在数学中是普遍使用,表示变化例如: dt = 时间变化)
原文:
To implement friction and spring physics we need to numerically solve the equations of motion. To do this we’ll use Euler integration. Although it has low accuracy and is prone to instability it works fine in a simple case like this. Here you can read more about numerical integration.
译文:
为了实现摩擦和弹簧物理属性,我们需要数值求解运动方程。为了做这个,我们要使用欧拉积分,尽管它的精度低并且有不稳定倾向,但它在一个像这样简单的场景中工作得很好。这儿你可以了解更多关于数值积分.
@Override
protected void onUpdate(int dt) {
final float fdt = dt / 1000f;
final float a = calculateAcceleration();
mPosition += mVelocity * fdt + .5f * a * fdt * fdt;
mVelocity += a * fdt;
}
(译注:上面的代码意思是,毫秒时间除以1000变成秒,然后计算了一个加速度a,然后计算位移,位移公式是: s = vt + at^2 / 2, 然后更新速度,因为经过一段匀加速直线运动,速度会发生变化,速度公式是: v = v0 + at)
原文:
Our algorithm requires us to be able to calculate the acceleration at the current position which we’ll do by using spring physics if we’re outside of the pan limits. If we are inside of the pan limits we’ll calculate the acceleration caused by friction as the product of the friction coefficient and the current velocity.
译文:
我们的算法要求我们能够计算当前位置的加速度,如果我们在平放限制的外部,我们将要通过使用弹簧物理属性做这件事,如果我们在平放限制的内部,我们将计算摩擦力引起的加速度,使用摩擦系数和当前速度的乘积。
private float calculateAcceleration() {
float acceleration;
final float distanceFromSnap = getDistanceFromSnap();
if (distanceFromSnap != 0) {
acceleration = distanceFromSnap * mStiffness - mDamping * mVelocity;
} else {
acceleration = -mFriction * mVelocity;
}
return acceleration;
}
(译注:上面的代码是加速度的具体计算过程,如果平放已经超出界限,使用弹簧的相关公式计算加速度,如果平放尚在界内,使用摩擦力公式计算加速度,这个和前面一样,需要理解他们的物理公式,我能力有限,已先昏昏,如果有读者看到这里,望自昭昭。)
原文:
A dynamic zoom control
Once we have the SpringDynamics implementation down we can improve our zoom control. As you might remember from the previous tutorials the zoom control is responsible for imposing limits and other similar logic. Let’s introduce our new SpringDynamics class to the zoom control by creating one of them for each dimension and setting fitting friction and spring coefficients.
译文:
一个动力缩放控制
我们已经有了SpringDynamics (译注:这是一个类,从名字看,就是弹簧动力效果)实现,我们可以提升我们的缩放控制了。你也许还记得,在前面的课程中缩放控制(zoom control)是负责实施限制和其他类似的逻辑。让我们给缩放控制引入新的SpringDynamics 类,给每一个纬度创建一个实例,并设置合适的摩擦和弹簧系数。
private final SpringDynamics mPanDynamicsX = new SpringDynamics();
private final SpringDynamics mPanDynamicsY = new SpringDynamics();
public DynamicZoomControl() {
mPanDynamicsX.setFriction(2f);
mPanDynamicsY.setFriction(2f);
mPanDynamicsX.setSpring(50f, 1f);
mPanDynamicsY.setSpring(50f, 1f);
}
原文:
Next, as we no longer will have hard limits on the pan but instead a spring pulling behavior when we are outside the pan limits, we need to modify the pan method:
译文:
接下来,我们不再对平放使用硬性限制,代替的是通过一个弹簧拉拽行为,如果我们在平放限制外部,我们需要修改pan方法。
private static final float PAN_OUTSIDE_SNAP_FACTOR = .4f;
private float mPanMinX;
private float mPanMaxX;
private float mPanMinY;
private float mPanMaxY;
public void pan(float dx, float dy) {
final float aspectQuotient = mAspectQuotient.get();
dx /= mState.getZoomX(aspectQuotient);
dy /= mState.getZoomY(aspectQuotient);
if (mState.getPanX() > mPanMaxX && dx > 0 || mState.getPanX() < mPanMinX && dx < 0) {
dx *= PAN_OUTSIDE_SNAP_FACTOR;
}
if (mState.getPanY() > mPanMaxY && dy > 0 || mState.getPanY() < mPanMinY && dy < 0) {
dy *= PAN_OUTSIDE_SNAP_FACTOR;
}
final float newPanX = mState.getPanX() + dx;
final float newPanY = mState.getPanY() + dy;
mState.setPanX(newPanX);
mState.setPanY(newPanY);
mState.notifyObservers();
}
原文:
We will no longer limit the pan by clamping it inside bounds when the user is panning around. Instead we’ll impose a penalty factor when panning outside the bounds giving the user clear visible feedback and a sense of struggling. This also means we’ll no longer need the limitPan() method we used in the previous tutorials. However, we still need to calculate the pan limits so we can do the pull back animation. We’ll replace the limitPan() method with a new method, updatePanLimits(), that calculates the pan limits depending on the zoom level, and then call that method every time the zoom level changes:
译文:
当用户正在向四周拖放的时候,我们不再通过固定上下界的方式限制平放。替代办法是我们使用一个补偿因子,当平放到边界外部时,给用户一个清晰可见的反馈和一个挣扎的感觉。这也意味着我们不再需要前面课程使用的limitPan()方法。然而,我们仍然需要计算平放限制,这样才可以做回拉动画,我们用一个新方法替换 limitPan() 方法,updatePanLimits(),这个方法根据缩放级别计算平放限制,然后每一次缩放级别变化都调用这个方法。
private void updatePanLimits() {
final float aspectQuotient = mAspectQuotient.get();
final float zoomX = mState.getZoomX(aspectQuotient);
final float zoomY = mState.getZoomY(aspectQuotient);
mPanMinX = .5f - getMaxPanDelta(zoomX);
mPanMaxX = .5f + getMaxPanDelta(zoomX);
mPanMinY = .5f - getMaxPanDelta(zoomY);
mPanMaxY = .5f + getMaxPanDelta(zoomY);
}
原文:
Next we’ll add functionality for starting a fling which will be called when the user lifts his or her finger from the screen after panning.
译文:
下一步我们将添加开始速滑的方法,它会在平放以后,用户手指离开屏幕的时候调用。
private final Handler mHandler = new Handler();
public void startFling(float vx, float vy) {
final float aspectQuotient = mAspectQuotient.get();
final long now = SystemClock.uptimeMillis();
mPanDynamicsX.setState(mState.getPanX(), vx / mState.getZoomX(aspectQuotient), now);
mPanDynamicsY.setState(mState.getPanY(), vy / mState.getZoomY(aspectQuotient), now);
mPanDynamicsX.setMinPosition(mPanMinX);
mPanDynamicsX.setMaxPosition(mPanMaxX);
mPanDynamicsY.setMinPosition(mPanMinY);
mPanDynamicsY.setMaxPosition(mPanMaxY);
mHandler.post(mUpdateRunnable);
}
原文:
In the startFling() method we prepare the dynamics objects for taking over control of the pan, setting to it the current pan and velocity values as well as the current time. We also set the min and max values of the dynamics object to the current pan limits. Finally we post a runnable that is responsible for updating and animating the zoom state.
译文:
在startFling()方法中,我们准备dynamics 对象来接管平放控制,给他设置当前的平放值和速度值以及当前时间。我们还给当前平放限制设置了dynamics 对象的最小和最大值。最后我们post 一个runnable ,由它负责更新和动画缩放状态。
private static final float REST_VELOCITY_TOLERANCE = 0.004f;
private static final float REST_POSITION_TOLERANCE = 0.01f;
private static final int FPS = 50;
private final Runnable mUpdateRunnable = new Runnable() {
public void run() {
final long startTime = SystemClock.uptimeMillis();
mPanDynamicsX.update(startTime);
mPanDynamicsY.update(startTime);
final boolean isAtRest = mPanDynamicsX.isAtRest(REST_VELOCITY_TOLERANCE, REST_POSITION_TOLERANCE) && mPanDynamicsY.isAtRest(REST_VELOCITY_TOLERANCE, REST_POSITION_TOLERANCE);
mState.setPanX(mPanDynamicsX.getPosition());
mState.setPanY(mPanDynamicsY.getPosition());
if (!isAtRest) {
final long stopTime = SystemClock.uptimeMillis();
mHandler.postDelayed(mUpdateRunnable, 1000 / FPS - (stopTime - startTime));
}
mState.notifyObservers();
}
};
原文:
In the runnable that is posted for animating the pan values we update the dynamics objects and then we set the updated pan values to the ZoomState. Then we check if the animation should go on or by checking if the the dynamics objects are both at rest. If the dynamics objects are not at rest we post this runnable again using a delay calculated to try to meet a chosen FPS. Finally we notify observers of the ZoomState object that it’s been changed, ultimately causing the zoom view to invalidate.
译文:
在posted 的做平放值动画的runnable 中,我们更新dynamics 对象,然后设置更新过的平放值给ZoomState,然后我们检查动画是否应该继续,通过判断两个dynamics 是否是空闲状态。如果动画对象不再空闲状态,我们使用一个尝试匹配已选帧率而计算出来的延迟再一次post这个runnable。最后我们通知ZoomState 的观察者对象已经发生变化,最后使缩放View 失效(译注:使得ondraw重新调用)。
原文:
Lastly our dynamic zoom control will need a method for stopping the fling that will be used when the user puts his or her finger down on the screen again after making a fling. Since our fling animation uses a runnable posted on a handler we simply remove the runnable from the handler, thus no longer triggering the automatic zoom state updates and effectively stopping the animation.
译文:
最后我们的动力缩放控制需要一个方法来停止速滑,这个会在开始速滑以后,用户再一次按下屏幕时调用。由于我们的速滑动画使用的是一个handler post的runnable ,我们简单地从handler删除这个runnable。这样就不会触发缩放状态的自动更新,从而有效地停止动画。
public void stopFling() {
mHandler.removeCallbacks(mUpdateRunnable);
}
原文:
Utilizing our new tools
Alright, so we have implemented dynamic behavior to our zoom control, now lets start using the new methods we created so we can start flinging and bouncing around! To do this we will build on our OnTouchListener implementation and make sure the onTouch method calls our new methods in all the correct places.
译文:
使用我们的新工具
很好,我们已经实现了缩放控制的动力行为,现在让我们开始使用我们创建的新方法,这样我们可以开始速滑和四周跳跃!为了实现这个,我们基于OnTouchListener 实现,确保onTouch 方法在所有合适的地方调用我们的新方法。
原文:
The startFling method we declared above takes two velocity parameters, one for each dimension, and luckily the Android platform has just the right tools for calculating the velocity of a fling motion. The onTouch method is will use VelocityTracker for tracking the velocity of motion events by simply adding the events to the tracker object:
译文:
我们上面申明的startFling 方法接受2个速度参数,分别对应每个维度,幸运的是Android平台有合适的工具来计算速滑运动的速度,onTouch 方法要使用VelocityTracker 来追踪运动事件的速度,简单地添加事件给tracker 对象。
private VelocityTracker mVelocityTracker;
public boolean onTouch(View v, MotionEvent event) {
final int action = event.getAction();
final float x = event.getX();
final float y = event.getY();
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
原文:
The handling of down events will be largely unchanged with the exception of the call to the stopFling() method on the zoom control, stopping any current animation when the user puts his or her finger back on the screen:
译文:
down事件的处理基本上没有改动,例外的地方是调用了缩放控制的stopFling()方法,当用户把手指放回屏幕的时候停止当前的任何动画。
switch (action) {
case MotionEvent.ACTION_DOWN:
mZoomControl.stopFling();
v.postDelayed(mLongPressRunnable, mLongPressTimeout);
mDownX = x;
mDownY = y;
mX = x;
mY = y;
break;
原文:
The handling of move events is unchanged since the previous part of the tutorial so lets skip ahead to the handling of up events. Here we will use the velocity tracker for calculating the velocity of the fling event if we were panning around. If we were not panning but instead perhaps zooming or simply in an undefined state, for example if the user has done a quick tap on the screen, we’ll start a fling without velocity. Even if we don’t give the fling any initial velocity it is important that we start it since the pan values might be outside of the limits which makes the springs physics accelerate the pan values, pulling them back within limits.
译文:
move事件的处理自前一部分的课程以后没有改变,所以我们直接前进到up 事件的处理,这里当我们在执行平放的时候,我们要使用速度追踪器( velocity tracker)来计算速滑事件的速度。如果我们没有平放,或许是在一个缩放或者甚至没有定义的状态,例如用户快速地点击了一下屏幕,我们开始一个没有速度的速滑。尽管我们没有给速滑任何初速度,但开始这个速滑非常重要,由于平放值可能在界限范围之外,它会使得弹簧的物理属性加速平放值,把他们拉回界限之内。
(译注:这一段后面的部分是说,如果是up事件,不在pan状态,则开始一个初速度为0的速滑,让弹簧计算加速度,把它拉回界限。)
case MotionEvent.ACTION_UP:
if (mMode == Mode.PAN) {
mVelocityTracker.computeCurrentVelocity(1000, mScaledMaximumFlingVelocity);
mZoomControl.startFling(-mVelocityTracker.getXVelocity() / v.getWidth(), -mVelocityTracker.getYVelocity() / v.getHeight());
} else {
mZoomControl.startFling(0, 0);
}
mVelocityTracker.recycle();
mVelocityTracker = null;
v.removeCallbacks(mLongPressRunnable);
mMode = Mode.UNDEFINED;
break;
default:
mVelocityTracker.recycle();
mVelocityTracker = null;
v.removeCallbacks(mLongPressRunnable);
mMode = Mode.UNDEFINED;
break;
}
return true;
}
原文:
Note that the fling uses the negative of the calculated velocities similar to how the negative of the dx and dy values are used in the part of the switch statement that handles move events.
译文:
注意速滑使用了计算得到速度的负值,这和switch语句中处理移动事件部分使用的dx,dy值相似。
原文:
And with that, we’re done! We now have a dynamic zoom with a nicely separated software architecture, and while this is where this tutorial series will end I hope it can be the start of experiments and projects for some of you. In this tutorial series we’ve limited ourselves to zooming in images, but the code can be used to zoom in any application. To do this you need to write your own View subclass and implement the drawing in this view to honor the values you get from ZoomState.
译文:
有这些东西,我们实现了。我们现在有一个动力感缩放,伴随一个分离很好的软件架构,这是这个课程结束的地方,然而我希望它能成为你们一些人的实验和项目的开端,在这个课程中我们限制自己缩放图片,但这个代码可以在任何应用中用来缩放。为了这样做,你需要编写你自己的View子类,并且在这个view中使用你从ZoomState取得的值来实现绘制。
原文:
Please feel free to share your zoom experiences, and don’t hesitate to ask any questions.
Good luck!
译文:
请愉快地分享你的缩放体验,并且毫不犹豫地问任何问题。
好运!
代码下载