教你一步一步實現圖示無縫變形切換
我的CSDN同步釋出:教你一步一步實現圖示無縫變形切換
轉載請註明出處:【huachao1001的簡書:http://www.jianshu.com/users/0a7e42698e4b/latest_articles】
*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出
本來這篇文章幾天前就應該寫好併發布出來的,由於要寫小論文,被導師劈頭蓋臉的罵了幾天,一直在搞論文,耽誤了博文的編寫。今天終於把小論文給投出去了,終於可以好好寫部落格了!
在上一篇文章《酷炫的Activity切換動畫,打造更好的使用者體驗 》中,我們感受到了過渡切換動畫帶來的不一樣的使用者體驗。如何你還意猶未盡,那麼今天我們再體驗一把圖示切換動畫。之前看過Material Design的圖示切換,如下圖:
感覺效果挺好的,但是發現很多實現是通過多個圖片切換產生的動畫效果。如果想要定製屬於自己的切換效果顯然得要去製作很多張圖片,導致apk變大不說,這得需要一定的flash功底啊,於是我就想是否可以通過屬性動畫,根據起始path資料和最終的path資料產生動畫效果。先來個我們的最終效果圖,讓你更有動力往下看(PS:以下gif是放慢了的動畫,另外gif丟幀導致不流暢,各位不要覺得很卡哈):
在API 21後,系統內建了AnimatedVectorDrawable ,它能將兩個Path以動畫方式切換。可是,畢竟不相容5.0之前的版本,這個類還是過幾年再用吧~。既然不用AnimatedVectorDrawable 類,我們就自己寫一個唄~。
1 讀取SVG path並顯示
SVG繪製路徑的命令雖然不多,如下(參考【W3School中SVG path教程】):
M : 相當於moveTo 兩個參數列示移動終點位置的x,y
L :相當於lineto 兩個參數列示x ,y
H :相當於水平的Line to,需要一個參數列示lineto的x座標,y座標則是當前繪製點的座標
V :相當於垂直的line to需要一個參數列示lineto的y座標
C :curveto(相當於cubicTo,需要6個引數,分別表示第1、2控制點座標以及結束點的座標
S :4個引數,表示平滑的使用3階貝塞爾曲線,另一個控制點座標被省略,需要我們去計算
Q :二階貝塞爾曲線,4個引數,分別表示控制點和結束點座標
T :平滑使用二階貝塞爾曲線,只有2個參數列示結束點,控制點需要我們計算
A :繪製弧線,引數比較複雜,有7個引數
Z :相當於close path,無引數
其中S、T、A幾個命令較複雜,本文先不去實現這幾個命令,感興趣的童鞋可以自己去實現。首先,一個Path是由多個Path組成,由於需要實現動畫效果,也就是Path裡面的資料我們需要動態變化,我們把各個Path“片段”封裝到一個物件中。一個“片段”對應一個svg path的命令,因為引數最多是3個點(Point),我們只需封裝3個Point物件:
class FragmentPath {
//記錄當前path片段的命令
public PathType pathType;
// 資料佔用長度,同樣是Line to,V、H與L後面攜帶的資料長度不同,這裡需要記錄
public int dataLen;
public Point p1;
public Point p2;
public Point p3;
}
其中,PathType是列舉型別,列舉型別無需加V、H命令,因為V、H在最終繪製的時候還是要轉為Line To,dataLen引數用於記錄當前的命令所佔的字串長度。PathType列舉型別如下:
enum PathType {
MOVE, LINE_TO, CURVE_TO, QUAD_TO, CLOSE
}
對SVG path的操作太多,我們把這些操作單獨封裝到一個SVGUtil
中,並將其設定為單例模式:
package com.hc.transformicon;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.graphics.Path;
import android.graphics.Point;
import android.util.Log;
public class SVGUtil {
private static volatile SVGUtil svgUtil;
private Set<String> svgCommandSet;
private String[] command = { "M", "L", "H", "V", "C", "S", "Q", "T", "A",
"Z" };
private SVGUtil() {
svgCommandSet = new HashSet<String>();
for (String cmd : command) {
svgCommandSet.add(cmd);
}
}
public static SVGUtil getInstance() {
if (svgUtil == null) {
synchronized (SVGUtil.class) {
if (svgUtil == null) {
svgUtil = new SVGUtil();
}
}
}
return svgUtil;
}
static class FragmentPath {
//記錄當前path片段的命令
public PathType pathType;
// 資料佔用長度,同樣是Line to,V、H與L後面攜帶的資料長度不同,這裡需要記錄
public int dataLen;
public Point p1;
public Point p2;
public Point p3;
}
static enum PathType {
MOVE, LINE_TO, CURVE_TO, QUAD_TO, ARC, CLOSE
}
}
由於SVG path中的資料可能寫的格式不同,比如使用M命令,有些人會寫成:M 100 100
而有些人會寫成M 100,100
這還算好的了,因為看起來比較“規矩”,以空格或逗號分隔字串就可以提取資料。有些人可能會寫成M100,100
,也就是在命令字母兩邊沒有加空格,這就讓你沒辦法提取資料了。另外還有就是使用者不小心多加了幾個空格,或者多加了幾個逗號,這讓你讀取也會帶來很多麻煩。還有就是使用者還可能把M
寫成小寫的m
,在SVG中大小寫的含義是不同的,但是我們不是去實現標準的SVG顯示,我們可以去忽略大小寫,我們只是借鑑一下SVG的命令,順帶學習一下SVG而已。說了那麼多,就是為了引入一個話題:需要對使用者原始資料進行預處理,在SVGUtil類中新增如下函式:
// 提取SVG資料
public ArrayList<String> extractSvgData(String svgData) {
//以下為了將命令字母兩邊新增空格
//儲存已經替換過的字母
Set<String> hasReplaceSet = new HashSet<String>();
//正規表示式,用於匹配path裡面的字母
Pattern pattern = Pattern.compile("[a-zA-Z]");
Matcher matcher = pattern.matcher(svgData);
//遍歷匹配正規表示式的字串
while (matcher.find()) {
//s為匹配的字串
String s = matcher.group();
//如果該字串沒有替換,則在改字串兩邊加空格
if (!hasReplaceSet.contains(s)) {
svgData = svgData.replace(s, " " + s + " ");
hasReplaceSet.add(s);
}
}
//---end--命令字母兩邊新增字母結束---
//將","替換為" ",並強制轉為大寫字母
svgData = svgData.replace(",", " ").trim().toUpperCase();
//以" "為分割符分割字串
String[] ss = svgData.split(" ");
//將最終分割成的字串陣列轉為List
ArrayList<String> data = new ArrayList<String>();
for (String s : ss) {
//只有當前的字串不是空格,才將該字串加入到List中
//相當於實現了自動刪除多餘的空格
if (s != null && !"".equals(s)) {
data.add(s);
}
}
return data;
}
對原始資料做了預處理後,開始真正的將資料轉換為Path物件了,在SVGUtil類中新增如下函式:
//根據ArrayList儲存的資料,將path資料轉為Android中的Path物件
//widthFactor,寬度放縮倍數
//heightFactor,高度放縮倍數
public Path parsePath(ArrayList<String> svgDataList, float widthFactor,
float heightFactor) {
//new一個需要返回的Path物件
Path path = new Path();
//解析字串偏移位置
int startIndex = 0;
//上一次繪製的終點,預設為左上角
Point lastPoint = new Point(0, 0);
//提取下一條FragmentPath物件
FragmentPath fp = nextFrag(svgDataList, startIndex, lastPoint);
//如果下一條FragmentPath不為null,則迴圈
while (fp != null) {
//根據命令型別,執行Path的不同方法,主要,所有的座標需要乘以放縮倍數
switch (fp.pathType) {
case MOVE: {
path.moveTo(fp.p1.x * widthFactor, fp.p1.y * heightFactor);
lastPoint = fp.p1;
break;
}
case LINE_TO: {
path.lineTo(fp.p1.x * widthFactor, fp.p1.y * heightFactor);
lastPoint = fp.p1;
break;
}
case CURVE_TO: {
path.cubicTo(fp.p1.x * widthFactor, fp.p1.y * heightFactor,
fp.p2.x * widthFactor, fp.p2.y * heightFactor, fp.p3.x
* widthFactor, fp.p3.y * heightFactor);
lastPoint = fp.p3;
break;
}
case QUAD_TO: {
path.quadTo(fp.p1.x * widthFactor, fp.p1.y * heightFactor,
fp.p2.x * widthFactor, fp.p2.y * heightFactor);
lastPoint = fp.p2;
break;
}
case CLOSE: {
path.close();
}
default:
break;
}
//設定下一條Path的偏移量,以便提取下一條命令
startIndex = startIndex + fp.dataLen + 1;
fp = nextFrag(svgDataList, startIndex, lastPoint);
}
return path;
}
我們看到,引數中有寬高的放縮倍數。為什麼需要放縮倍數呢?我們知道,SVG是向量圖,放縮後圖片清晰度是無影響的,因此我們這裡需要加放縮倍數。另外我們注意到還有個nextFrag函式,用於提取下一條命令,並封裝為FragmentPath物件,在SVGUtil類中新增如下函式:
//根據偏移量,解析下一條命令,並將命令封裝為FragmentPath物件
private FragmentPath nextFrag(ArrayList<String> svgData, int startIndex,
Point lastPoint) {
if (svgData == null)
return null;
int svgDataSize = svgData.size();
if (startIndex >= svgDataSize)
return null;
// 當前的path片段下標範圍[startIndex,i)
int i = startIndex + 1;
//儲存該命令的長度(指資料長度,不包括命令字母)
int length = 0;
FragmentPath fp = new FragmentPath();
//計算命令的長度
while (i < svgDataSize) {
if (svgCommandSet.contains(svgData.get(i)))
break;
i++;
length++;
}
//資料長度儲存到FragmentPath物件中
fp.dataLen = length;
// 根據資料的長度,把各個資料封裝到Point物件,並儲存到FragmentPath中
switch (length) {
case 0: {
Log.d("", svgData.get(startIndex) + " none data");
break;
}
case 1: {//如果資料只有一個,那麼可能是H或V命令,我們需要根據上一次的終端推算x或y座標
int d = (int) Float.parseFloat(svgData.get(startIndex + 1));
if (svgData.get(startIndex).equals("H")) {
fp.p1 = new Point(d, lastPoint.y);
} else {// "V"
fp.p1 = new Point(lastPoint.x, d);
}
break;
}
case 2: {//兩個資料,只有一個Point物件(x,y)
int x = (int) Float.parseFloat(svgData.get(startIndex + 1));
int y = (int) Float.parseFloat(svgData.get(startIndex + 2));
fp.p1 = new Point(x, y);
break;
}
case 4: {//4個資料,則封裝到兩個Point物件中
int x1 = (int) Float.parseFloat(svgData.get(startIndex + 1));
int y1 = (int) Float.parseFloat(svgData.get(startIndex + 2));
int x2 = (int) Float.parseFloat(svgData.get(startIndex + 3));
int y2 = (int) Float.parseFloat(svgData.get(startIndex + 4));
fp.p1 = new Point(x1, y1);
fp.p2 = new Point(x2, y2);
break;
}
case 6: {//6個資料,封裝到3個Point物件中
int x1 = (int) Float.parseFloat(svgData.get(startIndex + 1));
int y1 = (int) Float.parseFloat(svgData.get(startIndex + 2));
int x2 = (int) Float.parseFloat(svgData.get(startIndex + 3));
int y2 = (int) Float.parseFloat(svgData.get(startIndex + 4));
int x3 = (int) Float.parseFloat(svgData.get(startIndex + 5));
int y3 = (int) Float.parseFloat(svgData.get(startIndex + 6));
fp.p1 = new Point(x1, y1);
fp.p2 = new Point(x2, y2);
fp.p3 = new Point(x3, y3);
break;
}
default:
break;
}
// 設定當前路徑片段的繪製型別
switch (svgData.get(startIndex)) {
case "M": {
fp.pathType = PathType.MOVE;
break;
}
case "H":
case "V":
case "L": {
fp.pathType = PathType.LINE_TO;
break;
}
case "C": {
fp.pathType = PathType.CURVE_TO;
break;
}
case "Q": {
fp.pathType = PathType.QUAD_TO;
break;
}
case "Z": {
fp.pathType = PathType.CLOSE;
break;
}
}
return fp;
}
接下來就是自定義View了,由於接下來我們需要實現動畫效果,因此我們就將自定義的View繼承SurfaceView:
package com.hc.transformicon;
import java.util.ArrayList;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Bitmap.Config;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
/**
* Created by HuaChao on 2016/6/17.
*/
public class SVGPathView extends SurfaceView implements SurfaceHolder.Callback {
// 動畫起始Path資料
private ArrayList<String> svgStartDataList;
// 動畫結束時的Path資料
private ArrayList<String> svgEndDataList;
private SurfaceHolder surfaceHolder;
// 用於SurfaceView顯示的物件
private Bitmap mBitmap;
private Canvas mCanvas;
private Paint mPaint;
// view的寬高
private int mWidth;
private int mHeight;
// SVG path裡面的資料中參考的寬高
private int mViewWidth;
private int mViewHeight;
// 繪製線條的寬度
private int mPaintWidth;
// 用於等比放縮
private float widthFactor;
private float heightFactor;
private int mPaintColor;
public SVGPathView(Context context) {
super(context);
init();
}
public SVGPathView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs,
R.styleable.SVGPathView);
// 讀取佈局檔案設定的起始Path資料和結束Path資料
String svgStartPath = ta
.getString(R.styleable.SVGPathView_svg_start_path);
String svgEndPath = ta.getString(R.styleable.SVGPathView_svg_end_path);
// 如果二者有一個沒有設定,就將沒有設定的那個設定為已經設定的資料
if (svgStartPath == null && svgEndPath != null) {
svgStartPath = svgEndPath;
} else if (svgStartPath != null && svgEndPath == null) {
svgEndPath = svgStartPath;
}
// 讀取佈局檔案的配置
mViewWidth = ta.getInteger(R.styleable.SVGPathView_svg_view_width, -1);
mViewHeight = ta
.getInteger(R.styleable.SVGPathView_svg_view_height, -1);
mPaintWidth = ta.getInteger(R.styleable.SVGPathView_svg_paint_width, 5);
mPaintColor = ta.getColor(R.styleable.SVGPathView_svg_color,
Color.BLACK);
// 將原始資料做預處理
svgStartDataList = SVGUtil.getInstance().extractSvgData(svgStartPath);
svgEndDataList = SVGUtil.getInstance().extractSvgData(svgEndPath);
ta.recycle();
init();
}
// 初始化
private void init() {
surfaceHolder = getHolder();
surfaceHolder.addCallback(this);
mPaint = new Paint();
mPaint.setStrokeJoin(Join.ROUND);
mPaint.setStrokeCap(Cap.ROUND);
mPaint.setColor(mPaintColor);
}
// 開始繪製
public void drawPath() {
clearCanvas();
mPaint.setStyle(Style.STROKE);
mPaint.setColor(mPaintColor);
Path path = SVGUtil.getInstance().parsePath(svgStartDataList,
widthFactor, heightFactor);
mCanvas.drawPath(path, mPaint);
Canvas canvas = surfaceHolder.lockCanvas();
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
surfaceHolder.unlockCanvasAndPost(canvas);
}
// 清屏
private void clearCanvas() {
mPaint.setColor(Color.WHITE);
mPaint.setStyle(Style.FILL);
mCanvas.drawRect(0, 0, mWidth, mHeight, mPaint);
}
// 呼叫invalidate時,把Bitmap物件繪製到View中
@Override
public void invalidate() {
super.invalidate();
Canvas canvas = surfaceHolder.lockCanvas();
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
surfaceHolder.unlockCanvasAndPost(canvas);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
// 儲存當前的View寬高
mWidth = width;
mHeight = height;
// 如果沒有設定Path的參考寬高,預設設定為View的寬高
if (mViewWidth <= 0) {
mViewWidth = width;
}
if (mViewHeight <= 0) {
mViewHeight = height;
}
// 計算放縮倍數
widthFactor = 1.f * width / mViewWidth;
heightFactor = 1.f * height / mViewHeight;
// 建立Bitmap物件,用於繪製到螢幕中
mBitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
mCanvas = new Canvas(mBitmap);
// 將畫筆繪製線條的寬度設定為經過放縮後的寬度
mPaint.setStrokeWidth(mPaintWidth * widthFactor);
// 清屏
clearCanvas();
// 將清屏結果繪製到螢幕
invalidate();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
最後,再看看我們的佈局檔案以及自定義的佈局屬性:
styles.xml新增如下:
<declare-styleable name="SVGPathView">
<attr name="svg_start_path" format="reference" />
<attr name="svg_end_path" format="reference" />
<attr name="svg_paint_width" format="integer" />
<attr name="svg_view_width" format="integer" />
<attr name="svg_view_height" format="integer" />
<attr name="svg_color" format="color" />
</declare-styleable>
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res/com.hc.transformicon"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.hc.transformicon.SVGPathView
android:id="@+id/svgPathView"
android:layout_width="100dp"
android:layout_height="100dp"
app:svg_color="#00ff00"
app:svg_paint_width="18"
app:svg_start_path="@string/svg_back"
app:svg_view_height="100"
app:svg_view_width="100" />
</RelativeLayout>
佈局檔案中可以看到,我們設定的path裡面的資料,參考的寬高是100,看看我們的path是怎麼寫的:
<string name="svg_back">M 50 14 L 90 50 M 10 50 H 90 M 50 86 L 90 50</string>
最終會有一個箭頭顯示處理,無論我們的SVGPathView寬高如何,都會等比放縮。先看看最後顯示的圖吧~
2 兩個Path以動畫方式變形
為了避免每次都通過解析字串的方式來生成Path物件,我們需要把ArrayList<String>
轉為ArrayList<FragmentPath>
即儲存已經解析過的命令,減少重複解析。修改SVGPathView
類中的svgStartDataList
和svgEndDataList
:
// 動畫起始Path資料
private ArrayList<FragmentPath> svgStartDataList;
// 動畫結束時的Path資料
private ArrayList<FragmentPath> svgEndDataList;
並在建構函式中,修改svgStartDataList
和svgEndDataList
物件建立方式:
SVGUtil svgUtil = SVGUtil.getInstance();
// 將原始資料做預處理
ArrayList<String> svgStartStrList = svgUtil.extractSvgData(svgStartPath);
ArrayList<String> svgEndStrList = svgUtil.extractSvgData(svgEndPath);
// 將經過預處理後的path資料,轉為FragmentPath列表
svgStartDataList = svgUtil.strListToFragList(svgStartStrList);
svgEndDataList = svgUtil.strListToFragList(svgEndStrList);
SVGUtil
中新增strListToFragList
函式:
// 將path字串列表轉為封裝成FramentPath片段的列表
public ArrayList<FragmentPath> strListToFragList(ArrayList<String> svgDataList) {
ArrayList<FragmentPath> fragmentPaths = new ArrayList<SVGUtil.FragmentPath>();
int startIndex = 0;
Point lastPoint = new Point(0, 0);
FragmentPath fp = nextFrag(svgDataList, startIndex, lastPoint);
while (fp != null) {
fragmentPaths.add(fp);
switch (fp.pathType) {
case MOVE:
case LINE_TO: {
lastPoint = fp.p1;
break;
}
case CURVE_TO: {
lastPoint = fp.p3;
break;
}
case QUAD_TO: {
lastPoint = fp.p2;
break;
}
default:
break;
}
startIndex = startIndex + fp.dataLen + 1;
fp = nextFrag(svgDataList, startIndex, lastPoint);
}
return fragmentPaths;
}
SVGPathView
類中的drawPath
函式也需要修改,因為我們是通過屬性動畫動態生成Path了,而不是當初直接解析原始資料生成Path,將drawPath修改如下:
public void drawPath(Path path) {
clearCanvas();
mPaint.setStyle(Style.STROKE);
mPaint.setColor(mPaintColor);
mCanvas.drawPath(path, mPaint);
Canvas canvas = surfaceHolder.lockCanvas();
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
surfaceHolder.unlockCanvasAndPost(canvas);
}
在SVGPathView
類中新加一個函式startTransform
,用於開啟動畫,作為開始執行的入口函式:
public void startTransform() {
if (!isAnim) {
isAnim = true;
ValueAnimator va = ValueAnimator.ofFloat(0, 1f);
va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatorFactor = (float) animation.getAnimatedValue();
Path path = SVGUtil.getInstance().parseFragList(
svgStartDataList, svgEndDataList, widthFactor,
heightFactor, animatorFactor);
drawPath(path);
}
});
va.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
isAnim = false;
}
@Override
public void onAnimationCancel(Animator animation) {
isAnim = false;
}
});
va.setDuration(1000).start();
}
}
// 開始繪製
public void drawPath(Path path) {
clearCanvas();
mPaint.setStyle(Style.STROKE);
mPaint.setColor(mPaintColor);
mCanvas.drawPath(path, mPaint);
Canvas canvas = surfaceHolder.lockCanvas();
canvas.drawBitmap(mBitmap, 0, 0, mPaint);
surfaceHolder.unlockCanvasAndPost(canvas);
}
可以看到,真正的核心函式是SVGUtil
的parseFragList函式
,這個函式是根據起始的Path資料和終止的Path資料,以及動畫變化時刻的資料,生成新的Path,這個函式也不復雜:
public Path parseFragList(ArrayList<FragmentPath> svgStartDataList,
ArrayList<FragmentPath> svgEndDataList, float widthFactor,
float heightFactor, float animatorFactor) {
Path path = new Path();
for (int i = 0; i < svgStartDataList.size(); i++) {
FragmentPath startFp = svgStartDataList.get(i);
FragmentPath endFp = svgEndDataList.get(i);
//計算出當前的3個點的位置
int x1 = 0;
int y1 = 0;
int x2 = 0;
int y2 = 0;
int x3 = 0;
int y3 = 0;
if (startFp.p1 != null) {
x1 = (int) (startFp.p1.x + (endFp.p1.x - startFp.p1.x)
* animatorFactor);
y1 = (int) (startFp.p1.y + (endFp.p1.y - startFp.p1.y)
* animatorFactor);
}
if (startFp.p2 != null) {
x2 = (int) (startFp.p2.x + (endFp.p2.x - startFp.p2.x)
* animatorFactor);
y2 = (int) (startFp.p2.y + (endFp.p2.y - startFp.p2.y)
* animatorFactor);
}
if (startFp.p3 != null) {
x3 = (int) (startFp.p3.x + (endFp.p3.x - startFp.p3.x)
* animatorFactor);
y3 = (int) (startFp.p3.y + (endFp.p3.y - startFp.p3.y)
* animatorFactor);
}
switch (startFp.pathType) {
case MOVE: {
path.moveTo(x1 * widthFactor, y1 * heightFactor);
break;
}
case LINE_TO: {
path.lineTo(x1 * widthFactor, y1 * heightFactor);
break;
}
case CURVE_TO: {
path.cubicTo(x1 * widthFactor, y1 * heightFactor, x2
* widthFactor, y2 * heightFactor, x3 * widthFactor, y3
* heightFactor);
break;
}
case QUAD_TO: {
path.quadTo(x1 * widthFactor, y1 * heightFactor, x2
* widthFactor, y2 * heightFactor);
break;
}
case CLOSE: {
path.close();
}
default:
break;
}
}
return path;
}
好啦,看看動畫吧~
我們再加上旋轉動畫一起執行,讓切換效果更自然一點,先設定rotateDegree屬性,並在
onAnimationUpdate
函式中新增rotateDegree = animatorFactor * 360;
注意,需要在drawPath函式執行之前新增。將drawPath中的
mCanvas.drawPath(path, mPaint);
改為
mCanvas.save();
mCanvas.rotate(rotateDegree, mWidth / 2, mHeight / 2);
mCanvas.drawPath(path, mPaint);
看看效果吧~
動畫設定時間為1秒,加上Gif丟幀的原因,所以上面效果看起似乎有點不流暢
最後,請注意,兩個變形的Path資料中,對應的命令格式一定要一模一樣,否則會出錯!!!!
比如,要實現如下效果
path資料則必須寫成:
<string name="svg_add"> M 10,50 H 90 M 50 10 V 90 </string>
<string name="svg_remove">M 10,50 H 90 M 10 50 H 90</string>
雖然減號可以通過如下就可以畫出來
<string name="svg_remove">M 10,50 H 90 </string>
但是,我們需要加號中後半段資料的最終變形位置,因此不可以省去後面的。
最後獻上原始碼:http://download.csdn.net/download/huachao1001/9554503
相關文章
- 鵝廠優文|主播pk,如何實現無縫切換?
- 一步一步教你實現iOS音訊頻譜動畫(一)iOS音訊動畫
- 一步一步教你實現iOS音訊頻譜動畫(二)iOS音訊動畫
- 教你一步一步用c語言實現sift演算法C語言演算法
- Android自定義View教你一步一步實現即刻點贊效果AndroidView
- Flutter Web網站之最簡方式實現暗黑主題無縫切換FlutterWeb網站
- Vue雙向繫結原理,教你一步一步實現雙向繫結Vue
- Android自定義View教你一步一步實現薄荷健康滑動捲尺AndroidView
- 一步一步教你 https 抓包HTTP
- Android 一步一步教你使用ViewDragHelperAndroidView
- JavaScript圖片裁剪的無變形實現方法JavaScript
- 一步一步實現一個PromisePromise
- 一步一步實現手寫PromisePromise
- 左右無縫輪播圖的實現
- netty無縫切換rabbitmq、activemq、rocketmq實現聊天室單聊、群聊功能NettyMQ
- 一步一步,實現自己的ButterKnife(二)
- 一步一步實現單身狗雨
- 一步一步,實現自己的ButterKnife(一)
- 一步一步帶你實現virtualdom(一)
- 成品直播原始碼,輪播圖無縫切換以及自動懸停原始碼
- 一步步教你用 WebVR 實現虛擬現實遊戲WebVR遊戲
- jquery實現的具有漸變效果的圖片切換jQuery
- Android技術分享| 自定義ViewGroup實現直播間大小屏無縫切換AndroidView
- 雙buffer實現無鎖切換
- 一步一步實現現代前端單元測試前端
- 一步一步教你寫kubernetes sidecarIDE
- 一步一步教你認識 Python 閉包Python
- 一步一步實現Vue資料繫結Vue
- promise原理—一步一步實現一個promisePromise
- 一步一步實現 .NET 8 部署到 DockerDocker
- Android教你一步一步從學習貝塞爾曲線到實現波浪進度條Android
- Windows環境下無縫切換Listener log檔案Windows
- 一步一步教你封裝最新版的Dio封裝
- 一步一步教你如何用Python做詞雲Python
- JQuery實現圖片輪播無縫滾動jQuery
- activity切換無動畫效果的實現動畫
- Midjourney:一步一步教你如何使用 AI 繪畫 MJAI
- 一步一步教你如何搭建自己的視訊聚合站