同样,先上效果图如下:
效果图中,抛物线的动画即是由SurfaceView实现的。底部栏中的文字翻转详情相关帖子:
[Android] 文字翻转动画的实现
需求:
1.实现抛物线动画
1.1 设计物理模型,能够根据时间变量计算出某个时刻图片的X/Y坐标。
1.2 将图片高频率(相比于UI线程的缓慢而言)刷新到界面中。这儿需要实现将脏界面清屏及刷新操作。
2.文字翻转动画(已解决,见上面的帖子链接)
下面来逐一解决所提出的问题。
-----------------------------------------------------------------------------
分隔线内容与Android无关,请慎读,勿拍砖。谢啦
1.1 设计物理模型,如果大家还记得初中物理时,这并不难。自己写的草稿图见下:
可以有:图片要从高度为H的位置下落,并且第一次与X轴碰撞时会出现能量损失,至原来的N%。并且我们需要图片的最终落点离起始位置在X轴上的位移为L,默认存在重力加速度g。
详细的物理分析见上图啦,下面只说代码中如何实现,相关代码在PhysicalTool.java。
第一次下落过程所耗时t1与高度height会有如下关系:
t1 = Math.sqrt(2 * height * 1.0d / GRAVITY);
t2 = Math.sqrt((1 - WASTAGE) * 2 * height * 1.0d / GRAVITY);
velocity = width * 1.0d / (t1 + 2 * t2);
double used = (System.currentTimeMillis() - startTime) * 1.0d / 1000; x = velocity * used; if (0 <= used && used < t1) { y = height - 0.5d * GRAVITY * used * used; } else if (t1 <= used && used < (t1 + t2)) { double tmp = t1 + t2 - used; y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp; } else if ((t1 + t2) <= used && used < (t1 + 2 * t2)) { double tmp = used - t1 - t2; y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp; }Android无关内容结束了。
surfaceCreated()//通知Surface已被创建,可以在此处启动动画线程 surfaceChanged()//通知Surface已改变 surfaceDestroyed()//通知Surface已被销毁,可以在此处终止动画线程
//于SurfaceView类中,该类实现SurfaceHolder.Callback接口,如本例中的ParabolaView SurfaceHolder holder = getHolder(); holder.addCallback(this);
canvas = holder.lockCanvas(); … … … … … … … … canvas.drawBitmap(bitmap, x, y, paint); holder.unlockCanvasAndPost(canvas);
本例中,需要清除屏幕脏区域,出于简便的做法,是将整个SurfaceView背景重复地设置为透明,代码为:
canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR);
惯例,Java代码如下,XML请自行实现
本文由Sodino所有,转载请注明出处:http://blog.csdn.net/sodino/article/details/7704084
ActSurfaceView.java package lab.sodino.surfaceview; import lab.sodino.surfaceview.RotateAnimation.InterpolatedTimeListener; import android.app.Activity; import android.graphics.BitmapFactory; import android.os.Bundle; import android.os.Handler; import android.os.Handler.Callback; import android.os.Message; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; public class ActSurfaceView extends Activity implements OnClickListener, ParabolaView.ParabolaListener, Callback, InterpolatedTimeListener { public static final int REFRESH_TEXTVIEW = 1; private Button btnStartAnimation; /** 动画界面。 */ private ParabolaView parabolaView; /** 购物车处显示购物数量的TextView。 */ private TextView txtNumber; /** 购物车中的数量。 */ private int number; private Handler handler; /** TextNumber是否允许显示最新的数字。 */ private boolean enableRefresh; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); handler = new Handler(this); number = 0; btnStartAnimation = (Button) findViewById(R.id.btnStartAnim); btnStartAnimation.setOnClickListener(this); parabolaView = (ParabolaView) findViewById(R.id.surfaceView); parabolaView.setParabolaListener(this); txtNumber = (TextView) findViewById(R.id.txtNumber); } public void onClick(View v) { if (v == btnStartAnimation) { LogOut.out(this, "isShowMovie:" + parabolaView.isShowMovie()); if (parabolaView.isShowMovie() == false) { number++; enableRefresh = true; parabolaView.setIcon(BitmapFactory.decodeResource(getResources(), R.drawable.icon)); // 设置起始Y轴高度和终止X轴位移 parabolaView.setParams(200, ((ViewGroup) txtNumber.getParent()).getLeft()); parabolaView.showMovie(); } } } public void onParabolaStart(ParabolaView view) { } public void onParabolaEnd(ParabolaView view) { handler.sendEmptyMessage(REFRESH_TEXTVIEW); } public boolean handleMessage(Message msg) { switch (msg.what) { case REFRESH_TEXTVIEW: if (txtNumber.getVisibility() != View.VISIBLE) { txtNumber.setVisibility(View.VISIBLE); } RotateAnimation anim = new RotateAnimation(txtNumber.getWidth() >> 1, txtNumber.getHeight() >> 1, RotateAnimation.ROTATE_INCREASE); anim.setInterpolatedTimeListener(this); txtNumber.startAnimation(anim); break; } return false; } @Override public void interpolatedTime(float interpolatedTime) { // 监听到翻转进度过半时,更新txtNumber显示内容。 if (enableRefresh && interpolatedTime > 0.5f) { txtNumber.setText(Integer.toString(number)); // Log.d("ANDROID_LAB", "setNumber:" + number); enableRefresh = false; } } }
DrawThread.java package lab.sodino.surfaceview; import android.view.SurfaceView; /** * @author Sodino E-mail:[email protected] * @version Time:2012-6-18 上午03:14:31 */ public class DrawThread extends Thread { private SurfaceView surfaceView; private boolean running; public DrawThread(SurfaceView surfaceView) { this.surfaceView = surfaceView; } public void run() { if (surfaceView == null) { return; } if (surfaceView instanceof ParabolaView) { ((ParabolaView) surfaceView).handleThread(); } } public void setRunning(boolean b) { running = b; } public boolean isRunning() { return running; } }
ParabolaView.java package lab.sodino.surfaceview; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PixelFormat; import android.util.AttributeSet; import android.view.SurfaceHolder; import android.view.SurfaceView; /** * @author Sodino E-mail:[email protected] * @version Time:2012-6-18 上午02:52:33 */ public class ParabolaView extends SurfaceView implements SurfaceHolder.Callback { /** 每30ms刷一帧。 */ private static final long SLEEP_DURATION = 10l; private SurfaceHolder holder; /** 动画图标。 */ private Bitmap bitmap; private DrawThread thread; private PhysicalTool physicalTool; private ParabolaView.ParabolaListener listener; /** 默认未创建,相当于Destory。 */ private boolean surfaceDestoryed = true; public ParabolaView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } public ParabolaView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public ParabolaView(Context context) { super(context); init(); } private void init() { holder = getHolder(); holder.addCallback(this); holder.setFormat(PixelFormat.TRANSPARENT); setZOrderOnTop(true); // setZOrderOnTop(false); physicalTool = new PhysicalTool(); } @Override public void surfaceCreated(SurfaceHolder holder) { surfaceDestoryed = false; } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { LogOut.out(this, "surfaceDestroyed"); surfaceDestoryed = true; physicalTool.cancel(); } public void handleThread() { Canvas canvas = null; Paint pTmp = new Paint(); pTmp.setAntiAlias(true); pTmp.setColor(Color.RED); Paint paint = new Paint(); // 设置抗锯齿 paint.setAntiAlias(true); paint.setColor(Color.CYAN); physicalTool.start(); LogOut.out(this, "doing:" + physicalTool.doing()); if (listener != null) { listener.onParabolaStart(this); } while (physicalTool.doing()) { try { physicalTool.compute(); canvas = holder.lockCanvas(); // 设置画布的背景为透明。 canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR); // 绘上新图区域 float x = (float) physicalTool.getX(); // float y = (float) physicalTool.getY(); float y = (float) physicalTool.getMirrorY(getHeight(), bitmap.getHeight()); // LogOut.out(this, "x:" + x + " y:" + y); canvas.drawRect(x, y, x + bitmap.getWidth(), y + bitmap.getHeight(), pTmp); canvas.drawBitmap(bitmap, x, y, paint); holder.unlockCanvasAndPost(canvas); Thread.sleep(SLEEP_DURATION); } catch (Exception e) { e.printStackTrace(); } } // 清除屏幕内容 // 直接按"Home"回桌面,SurfaceView被销毁了,lockCanvas返回为null。 if (surfaceDestoryed == false) { canvas = holder.lockCanvas(); canvas.drawColor(Color.TRANSPARENT, android.graphics.PorterDuff.Mode.CLEAR); holder.unlockCanvasAndPost(canvas); } thread.setRunning(false); if (listener != null) { listener.onParabolaEnd(this); } } public void showMovie() { if (thread == null) { thread = new DrawThread(this); } else if (thread.getState() == Thread.State.TERMINATED) { thread.setRunning(false); thread = new DrawThread(this); } LogOut.out(this, "thread.getState:" + thread.getState()); if (thread.getState() == Thread.State.NEW) { thread.start(); } } /** 正在播放动画时,返回true;否则返回false。 */ public boolean isShowMovie() { return physicalTool.doing(); } public void setIcon(Bitmap bit) { bitmap = bit; } public void setParams(int height, int width) { physicalTool.setParams(height, width); } /** 设置抛物线的动画监听器。 */ public void setParabolaListener(ParabolaView.ParabolaListener listener) { this.listener = listener; } static interface ParabolaListener { public void onParabolaStart(ParabolaView view); public void onParabolaEnd(ParabolaView view); } }
PhysicalTool.java package lab.sodino.surfaceview; /** * @author Sodino E-mail:[email protected] * @version Time:2012-6-18 上午06:07:16 */ public class PhysicalTool { /** 重力加速度值。 */ private static final float GRAVITY = 400.78033f; /** 与X轴碰撞后,重力势能损失掉的百分比。 */ private static final float WASTAGE = 0.3f; /** 起始下降高度。 */ private int height; /** 起始点到终点的X轴位移。 */ private int width; /** 水平位移速度。 */ private double velocity; /** X Y坐标。 */ private double x, y; /** 动画开始时间。 */ private long startTime; /** 首阶段下载的时间。 单位:毫秒。 */ private double t1; /** 第二阶段上升与下载的时间。 单位:毫秒。 */ private double t2; /** 动画正在进行时值为true,反之为false。 */ private boolean doing; public void start() { startTime = System.currentTimeMillis(); doing = true; } /** 设置起始下落的高度及水平初速度;并以此计算小球下落的第一阶段及第二阶段上升耗时。 */ public void setParams(int h, int w) { height = h; width = w; t1 = Math.sqrt(2 * height * 1.0d / GRAVITY); t2 = Math.sqrt((1 - WASTAGE) * 2 * height * 1.0d / GRAVITY); velocity = width * 1.0d / (t1 + 2 * t2); LogOut.out(this, "t1=" + t1 + " t2=" + t2); } /** 根据当前时间计算小球的X/Y坐标。 */ public void compute() { double used = (System.currentTimeMillis() - startTime) * 1.0d / 1000; x = velocity * used; if (0 <= used && used < t1) { y = height - 0.5d * GRAVITY * used * used; } else if (t1 <= used && used < (t1 + t2)) { double tmp = t1 + t2 - used; y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp; } else if ((t1 + t2) <= used && used < (t1 + 2 * t2)) { double tmp = used - t1 - t2; y = (1 - WASTAGE) * height - 0.5d * GRAVITY * tmp * tmp; } else { LogOut.out(this, "used:" + used + " set doing false"); x = velocity * (t1 + 2 * t2); y = 0; doing = false; } } public double getX() { return x; } public double getY() { return y; } /** 反转Y轴正方向。适应手机的真实坐标系。 */ public double getMirrorY(int parentHeight, int bitHeight) { int half = parentHeight >> 1; double tmp = half + (half - y); tmp -= bitHeight; return tmp; } public boolean doing() { return doing; } public void cancel() { doing = false; } }
RotateAnimation.java package lab.sodino.surfaceview; import android.graphics.Camera; import android.graphics.Matrix; import android.view.animation.Animation; import android.view.animation.Transformation; /** * @author Sodino E-mail:[email protected] * @version Time:2012-6-27 上午07:32:00 */ public class RotateAnimation extends Animation { /** 值为true时可明确查看动画的旋转方向。 */ public static final boolean DEBUG = false; /** 沿Y轴正方向看,数值减1时动画逆时针旋转。 */ public static final boolean ROTATE_DECREASE = true; /** 沿Y轴正方向看,数值减1时动画顺时针旋转。 */ public static final boolean ROTATE_INCREASE = false; /** Z轴上最大深度。 */ public static final float DEPTH_Z = 310.0f; /** 动画显示时长。 */ public static final long DURATION = 800l; /** 图片翻转类型。 */ private final boolean type; private final float centerX; private final float centerY; private Camera camera; /** 用于监听动画进度。当值过半时需更新txtNumber的内容。 */ private InterpolatedTimeListener listener; public RotateAnimation(float cX, float cY, boolean type) { centerX = cX; centerY = cY; this.type = type; setDuration(DURATION); } public void initialize(int width, int height, int parentWidth, int parentHeight) { // 在构造函数之后、getTransformation()之前调用本方法。 super.initialize(width, height, parentWidth, parentHeight); camera = new Camera(); } public void setInterpolatedTimeListener(InterpolatedTimeListener listener) { this.listener = listener; } protected void applyTransformation(float interpolatedTime, Transformation transformation) { // interpolatedTime:动画进度值,范围为[0.0f,10.f] if (listener != null) { listener.interpolatedTime(interpolatedTime); } float from = 0.0f, to = 0.0f; if (type == ROTATE_DECREASE) { from = 0.0f; to = 180.0f; } else if (type == ROTATE_INCREASE) { from = 360.0f; to = 180.0f; } float degree = from + (to - from) * interpolatedTime; boolean overHalf = (interpolatedTime > 0.5f); if (overHalf) { // 翻转过半的情况下,为保证数字仍为可读的文字而非镜面效果的文字,需翻转180度。 degree = degree - 180; } // float depth = 0.0f; float depth = (0.5f - Math.abs(interpolatedTime - 0.5f)) * DEPTH_Z; final Matrix matrix = transformation.getMatrix(); camera.save(); camera.translate(0.0f, 0.0f, depth); camera.rotateY(degree); camera.getMatrix(matrix); camera.restore(); if (DEBUG) { if (overHalf) { matrix.preTranslate(-centerX * 2, -centerY); matrix.postTranslate(centerX * 2, centerY); } } else { matrix.preTranslate(-centerX, -centerY); matrix.postTranslate(centerX, centerY); } } /** 动画进度监听器。 */ public static interface InterpolatedTimeListener { public void interpolatedTime(float interpolatedTime); } }