Android中动画分为3种:
- Tween Animation(补间动画):通过对场景的对象不断做图像变换(平移、缩放、旋转)产生动画效果,即是一种渐变动画。
- Frame Animation(逐帧动画):顺序播放事先做好的图像,是一种画面转换动画。
- Property Animation(属性动画):通过动态地改变对象的属性从而达到动画效果,属性动画为 API 11 新特性。
Tween Animation
根据不同的动画效果,补间动画分为 4 种动画:
- 平移动画(Translate)画面位置移动动画效果
- 缩放动画(scale)渐变尺寸伸缩动画效果
- 旋转动画(rotate)画面旋转动画效果
- 透明度动画(alpha) 渐变透明度动画效果
这四种动画实现方式都是通过 Animation 类和 AnimationUtils 配合实现。
具体使用
方法1.通过xml实现
- 步骤1:在 res/anim 的文件夹里创建动画效果 rotate.xml 文件
- 步骤2:根据不同动画效果的语法设置不同动画参数,从而实现动画效果。
rotate.xml
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromDegrees="0" // 动画开始时 视图的旋转角度(正数 = 顺时针,负数 = 逆时针)
android:toDegrees="270" // 动画结束时 视图的旋转角度(正数 = 顺时针,负数 = 逆时针)
android:pivotX="50%" // 旋转轴点的x坐标
android:pivotY="0" // 旋转轴点的y坐标
/>
- 步骤3:在 Java 代码中创建 Animation 对象并播放动画
MainActivity.java
public class MainActivity extends AppCompatActivity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = findViewById(R.id.button);
Animation rotateAnimation = AnimationUtils.loadAnimation(this, R.anim.rotate);
mButton.startAnimation(rotateAnimation);
}
}
方法2:代码设置
Button mButton = (Button) findViewById(R.id.Button);
Animation rotateAnimation = new RotateAnimation(0,270,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
rotateAnimation.setDuration(3000);
mButton.startAnimation(rotateAnimation);
Frame Animation
Frame Animation 是顺序播放事先做好的图像,跟电影类似。不同于animation package,Android SDK 提供了另外一个类 AnimationDrawable 来定义使用 Frame Animation。
具体使用
- 首先将动画资源(即每张图片资源)放到 drawable文件夹里
-
方式1: XML 实现
step1:在 res/drawable-hdpi 的文件夹里创建动画效果 frame.xml 文件:
frame.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list
xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true"
>
<item android:drawable="@drawable/p1" android:duration="1000"></item>
<item android:drawable="@drawable/p2" android:duration="1000"></item>
<item android:drawable="@drawable/p3" android:duration="1000"></item>
<item android:drawable="@drawable/p4" android:duration="1000"></item>
<item android:drawable="@drawable/p5" android:duration="1000"></item>
<item android:drawable="@drawable/p6" android:duration="1000"></item>
</animation-list>
step2:在Java中实现
FrameActivity.java
public class FrameActivity extends AppCompatActivity {
private Button btn_startFrame,btn_stopFrame;
private ImageView iv;
private AnimationDrawable animationDrawable;
iv = (ImageView) findViewById(R.id.iv);
btn_startFrame = (Button) findViewById(R.id.btn_startFrame);
btn_stopFrame = (Button) findViewById(R.id.btn_stopFrame);
<-- 开始动画 -->
btn_startFrame.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
iv.setImageResource(R.drawable.frame);
// 1. 设置动画
animationDrawable = (AnimationDrawable) iv.getDrawable();
// 2. 获取动画对象
animationDrawable.start();
// 3. 启动动画
}
});
//停止动画
btn_stopFrame.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
iv.setImageResource(R.drawable.frame);
// 1. 设置动画
animationDrawable = (AnimationDrawable) iv.getDrawable();
// 2. 获取动画对象
animationDrawable.stop();
// 3. 暂停动画
}
});
}
}
- 方式2:在代码中实现
<-- 直接从drawable文件夹获取动画资源(图片) -->
animationDrawable = new AnimationDrawable();
for (int i = 0; i <= 25; i++) {
int id = getResources().getIdentifier("a" + i, "drawable", getPackageName());
Drawable drawable = getResources().getDrawable(id);
animationDrawable.addFrame(drawable, 100);
}
<-- 开始动画 -->
btn_startFrame.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
animationDrawable.setOneShot(true);
iv.setImageDrawable(animationDrawable);
// 获取资源对象
animationDrawable.stop();
// 特别注意:在动画start()之前要先stop(),不然在第一次动画之后会停在最后一帧,这样动画就只会触发一次
animationDrawable.start();
// 启动动画
}
});
<-- 停止动画 -->
btn_stopFrame.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
animationDrawable.setOneShot(true);
iv.setImageDrawable(animationDrawable);
animationDrawable.stop();
}
});
参考:Android 逐帧动画:关于 逐帧动画 的使用都在这里了!
Property Animation
原理
属性动画要求动画作用的对象提供该属性的 get 和 set 方法,属性动画根据你传递的该熟悉的初始值和最终值,以动画的效果多次去调用 set 方法,每次传递给 set 方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,你对 object 的属性 xxx 做动画,如果想让动画生效,要同时满足两个条件:
1. object 必须要提供 setXxx 方法,如果动画的时候没有传递初始值,那么还要提供 getXxx 方法,因为系统要去拿 xxx 属性的初始值(如果这条不满足,程序直接 Crash)
2. object 的 setXxx 对属性 xxx 所做的改变必须能够通过某种方法反映出来,比如会带来 UI 的改变啥的(如果这条不满足,动画无效果但不会 Crash)
以上条件缺一不可。
具体使用
1.ValueAnimator 类
ValueAnimator类中有3个重要方法:
- ValueAnimator.ofInt(int values)作用:将初始值以整型数值的形式过渡到结束值
- ValueAnimator.ofFloat(float values)作用:将初始值以浮点型数值的形式过渡到结束值
- ValueAnimator.ofObject(int values) 作用:将初始值以对象的形式过渡到结束值
实例:按钮的宽度从 150px 放大到 500px。
public class MainActivity extends AppCompatActivity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = findViewById(R.id.button);
ValueAnimator valueAnimator = ValueAnimator.ofInt(mButton.getLayoutParams().width, 500);
valueAnimator.setDuration(2000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
int currentValue = (Integer) animator.getAnimatedValue();
mButton.getLayoutParams().width = currentValue;
mButton.requestLayout();
}
});
valueAnimator.start();
}
}
效果图:
效果图
2.ObjectAnimator类
旋转
public class MainActivity extends AppCompatActivity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = findViewById(R.id.button);
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);
animator.setDuration(5000);
animator.start();
}
}
效果图
旋转
参考:Android 属性动画:这是一篇很详细的 属性动画 总结&攻略
手势检测(GestureDetector)
在开发 Android 手机应用过程中,可能需要对一些手势作出响应,如:单击、双击、长按、滑动、缩放等。这些都是很常用的手势。
实例:轨迹球
FailingBall.java
package com.scarf.demo007.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import com.scarf.demo007.R;
/**
* Created on 2019/2/12 15:09
* 轨迹球
* @author Scarf Gong
*/
public class FailingBall extends View {
private int mWidth; // 宽度
private int mHeight; // 高度
private float mStartX = 0; // 小方块开始位置X
private float mStartY = 0; // 小方块开始位置Y
private float mEdgeLength = 200; // 边长
private RectF mRect = new RectF(mStartX, mStartY, mStartX + mEdgeLength, mStartY + mEdgeLength);
private float mFixedX = 0; // 修正距离X
private float mFixedY = 0; // 修正距离Y
private Paint mPaint;
private GestureDetector mGestureDetector;
private boolean mCanFail = false; // 是否可以拖动
private float mSpeedX = 0;
private float mSpeedY = 0;
private Boolean mXFixed = false;
private Boolean mYFixed = false;
private Bitmap mBitmap;
public FailingBall(Context context) {
this(context,null);
}
public FailingBall(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public FailingBall(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mGestureDetector = new GestureDetector(context, mSimpleOnGestureListener);
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setAntiAlias(true);
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ball);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
mStartX = (w - mEdgeLength) / 2;
mStartY = (h - mEdgeLength) / 2;
refreshRectByCurrentPoint();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawOval(mRect, mPaint);
canvas.drawBitmap(mBitmap, new Rect(0, 0, mBitmap.getWidth(), mBitmap.getHeight()),
mRect, mPaint);
}
// 每 100 ms 更新一次
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
mStartX = mStartX + mSpeedX / 30;
mStartY = mStartY + mSpeedY / 30;
//mSpeedX = mSpeedX > 0 ? mSpeedX - 10 : mSpeedX + 10;
//mSpeedY = mSpeedY > 0 ? mSpeedY - 10 : mSpeedY + 10;
mSpeedX *= 0.97;
mSpeedY *= 0.97;
if (Math.abs(mSpeedX) < 10) {
mSpeedX = 0;
}
if (Math.abs(mSpeedY) < 10) {
mSpeedY = 0;
}
if (refreshRectByCurrentPoint()) {
// 转向
if (mXFixed) {
mSpeedX = -mSpeedX;
}
if (mYFixed) {
mSpeedY = -mSpeedY;
}
}
invalidate();
if (mSpeedX == 0 && mSpeedY == 0) {
mHandler.removeCallbacks(this);
return;
}
mHandler.postDelayed(this, 33);
}
};
private GestureDetector.SimpleOnGestureListener mSimpleOnGestureListener = new
GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float
velocityY) {
Log.e("Failing", velocityX + " : " + velocityY);
if (!mCanFail) return false;
mSpeedX = velocityX;
mSpeedY = velocityY;
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, 0);
return super.onFling(e1, e2, velocityX, velocityY);
}
};
@Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
if (contains(event.getX(), event.getY())) {
mCanFail = true;
mFixedX = event.getX() - mStartX;
mFixedY = event.getY() - mStartY;
mSpeedX = 0;
mSpeedY = 0;
} else {
mCanFail = false;
}
break;
case MotionEvent.ACTION_MOVE:
if (!mCanFail) {
break;
}
mStartX = event.getX() - mFixedX;
mStartY = event.getY() - mFixedY;
if (refreshRectByCurrentPoint()) {
mFixedX = event.getX() - mStartX;
mFixedY = event.getY() - mStartY;
}
invalidate();
break;
}
return true;
}
private Boolean contains(float x, float y) {
float radius = mEdgeLength / 2;
float centerX = mRect.left + radius;
float centerY = mRect.top + radius;
return Math.sqrt(Math.pow(x - centerX, 2) + Math.pow(y - centerY, 2)) <= radius;
}
/**
* 刷新方块位置
*
* @return true 表示修正过位置, false 表示没有修正过位置
*/
private Boolean refreshRectByCurrentPoint() {
Boolean fixed = false;
mXFixed = false;
mYFixed = false;
// 修正坐标
if (mStartX < 0) {
mStartX = 0;
fixed = true;
mXFixed = true;
}
if (mStartY < 0) {
mStartY = 0;
fixed = true;
mYFixed = true;
}
if (mStartX + mEdgeLength > mWidth) {
mStartX = mWidth - mEdgeLength;
fixed = true;
mXFixed = true;
}
if (mStartY + mEdgeLength > mHeight) {
mStartY = mHeight - mEdgeLength;
fixed = true;
mYFixed = true;
}
mRect.left = mStartX;
mRect.top = mStartY;
mRect.right = mStartX + mEdgeLength;
mRect.bottom = mStartY + mEdgeLength;
return fixed;
}
}
布局文件:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.scarf.demo007.widget.FailingBall
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
效果图
轨迹球
缩放手势检测(ScaleGestureDecetor)
缩放手势对于大部分 Android 工程师来说,需要用到的机会比较少,它最常见于以下的一些应用场景中,例如:图片浏览,图片编辑(贴图效果)、网页缩放、地图、文本阅读(通过缩放手势调整文字大小)等。
代码:
/**
* Created on 2019/2/12 15:30
*
* @author Scarf Gong
*/
public class GestureDemoView extends View {
GestureDetector mGestureDetector;
ScaleGestureDetector mScaleGestureDetector;
// 画布当前的 Matrix, 用于获取当前画布的一些状态信息,例如缩放大小,平移距离等
private Matrix mCanvasMatrix = new Matrix();
// 将用户触摸的坐标转换为画布上坐标所需的 Matrix, 以便找到正确的缩放中心位置
private Matrix mInvertMatrix = new Matrix();
// 所有用户触发的缩放、平移等操作都通过下面的 Matrix 直接作用于画布上,
// 将系统计算的一些初始缩放平移信息与用户操作的信息进行隔离,让操作更加直观
private Matrix mUserMatrix = new Matrix();
private Bitmap mBitmap;
// 基础的缩放和平移信息,该信息与用户的手势操作无关
private float mBaseScale;
private float mBaseTranslateX;
private float mBaseTranslateY;
private Paint mPaint;
public GestureDemoView(Context context) {
super(context);
}
public GestureDemoView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test);
initGesture(context);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mBitmap.getWidth() * 1.0f / mBitmap.getHeight() > w * 1.0f / h) {
mBaseScale = w * 1.0f / mBitmap.getWidth();
mBaseTranslateX = 0;
mBaseTranslateY = (h - mBitmap.getHeight() * mBaseScale) / 2;
} else {
mBaseScale = h * 1.0f / mBitmap.getHeight() * 1.0f;
mBaseTranslateX = (w - mBitmap.getWidth() * mBaseScale) / 2;
mBaseTranslateY = 0;
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(6);
canvas.translate(mBaseTranslateX, mBaseTranslateY);
canvas.scale(mBaseScale, mBaseScale);
canvas.save();
canvas.concat(mUserMatrix);
mCanvasMatrix = canvas.getMatrix();
mCanvasMatrix.invert(mInvertMatrix);
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
canvas.restore();
}
//--- 手势处理 ----------------------------------------------------------------------------------
private void initGesture(Context context) {
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
float scale = getMatrixValue(MSCALE_X, mCanvasMatrix);
mUserMatrix.preTranslate(-distanceX / scale, -distanceY / scale);
//fixTranslate(); // 在用户滚动时不进行修正,保证用户滚动时也有响应, 在用户抬起手指后进行修正
invalidate();
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
if (!mUserMatrix.isIdentity()) {
mUserMatrix.reset();
} else {
float[] points = mapPoint(e.getX(), e.getY(), mInvertMatrix);
mUserMatrix.postScale(MAX_SCALE, MAX_SCALE, points[0], points[1]);
}
fixTranslate();
invalidate();
return true;
}
});
mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
float fx = detector.getFocusX();
float fy = detector.getFocusY();
float[] points = mapPoint(fx, fy, mInvertMatrix);
scaleFactor = getRealScaleFactor(scaleFactor);
mUserMatrix.preScale(scaleFactor, scaleFactor, points[0], points[1]);
fixTranslate();
invalidate();
return true;
}
});
}
// 修正缩放
private void fixTranslate() {
// 对 Matrix 进行预计算,并根据计算结果进行修正
Matrix viewMatrix = getMatrix(); // 获取当前控件的Matrix
viewMatrix.preTranslate(mBaseTranslateX, mBaseTranslateY);
viewMatrix.preScale(mBaseScale, mBaseScale);
viewMatrix.preConcat(mUserMatrix);
Matrix invert = new Matrix();
viewMatrix.invert(invert);
Rect rect = new Rect();
getGlobalVisibleRect(rect);
float userScale = getMatrixValue(MSCALE_X, mUserMatrix);
float scale = getMatrixValue(MSCALE_X, viewMatrix);
float[] center = mapPoint(mBitmap.getWidth() / 2.0f, mBitmap.getHeight() / 2.0f, viewMatrix);
float distanceX = center[0] - getWidth() / 2.0f;
float distanceY = center[1] - getHeight() / 2.0f;
float[] wh = mapVectors(mBitmap.getWidth(), mBitmap.getHeight(), viewMatrix);
if (userScale <= 1.0f) {
mUserMatrix.preTranslate(-distanceX / scale, -distanceY / scale);
} else {
float[] lefttop = mapPoint(0, 0, viewMatrix);
float[] rightbottom = mapPoint(mBitmap.getWidth(), mBitmap.getHeight(), viewMatrix);
// 如果宽度小于总宽度,则水平居中
if (wh[0] < getWidth()) {
mUserMatrix.preTranslate(distanceX / scale, 0);
} else {
if (lefttop[0] > 0) {
mUserMatrix.preTranslate(-lefttop[0] / scale, 0);
} else if (rightbottom[0] < getWidth()) {
mUserMatrix.preTranslate((getWidth() - rightbottom[0]) / scale, 0);
}
}
// 如果高度小于总高度,则垂直居中
if (wh[1] < getHeight()) {
mUserMatrix.preTranslate(0, -distanceY / scale);
} else {
if (lefttop[1] > 0) {
mUserMatrix.preTranslate(0, -lefttop[1] / scale);
} else if (rightbottom[1] < getHeight()) {
mUserMatrix.preTranslate(0, (getHeight() - rightbottom[1]) / scale);
}
}
}
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
mScaleGestureDetector.onTouchEvent(event);
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
fixTranslate();
}
return true;
}
//--- Tools ------------------------------------------------------------------------------------
//--- 将坐标转换为画布坐标 ---
private float[] mapPoint(float x, float y, Matrix matrix) {
float[] temp = new float[2];
temp[0] = x;
temp[1] = y;
matrix.mapPoints(temp);
return temp;
}
private float[] mapVectors(float x, float y, Matrix matrix) {
float[] temp = new float[2];
temp[0] = x;
temp[1] = y;
matrix.mapVectors(temp);
return temp;
}
//--- 获取 Matrix 中的属性 ---
private float[] matrixValues = new float[9];
private static final int MSCALE_X = 0, MSKEW_X = 1, MTRANS_X = 2;
private static final int MSKEW_Y = 3, MSCALE_Y = 4, MTRANS_Y = 5;
private static final int MPERSP_0 = 6, MPERSP_1 = 7, MPERSP_2 = 8;
@IntDef({MSCALE_X, MSKEW_X, MTRANS_X, MSKEW_Y, MSCALE_Y, MTRANS_Y, MPERSP_0, MPERSP_1, MPERSP_2})
@Retention(RetentionPolicy.SOURCE)
private @interface MatrixName {}
private float getMatrixValue(@MatrixName int name, Matrix matrix) {
matrix.getValues(matrixValues);
return matrixValues[name];
}
//--- 限制缩放比例 ---
private static final float MAX_SCALE = 4.0f; //最大缩放比例
private static final float MIN_SCALE = 0.5f; // 最小缩放比例
private float getRealScaleFactor(float currentScaleFactor) {
float realScale = 1.0f;
float userScale = getMatrixValue(MSCALE_X, mUserMatrix); // 用户当前的缩放比例
float theoryScale = userScale * currentScaleFactor; // 理论缩放数值
// 如果用户在执行放大操作并且理论缩放数据大于4.0
if (currentScaleFactor > 1.0f && theoryScale > MAX_SCALE) {
realScale = MAX_SCALE / userScale;
} else if (currentScaleFactor < 1.0f && theoryScale < MIN_SCALE) {
realScale = MIN_SCALE / userScale;
} else {
realScale = currentScaleFactor;
}
return realScale;
}
}
网友评论