本文出處: 炎之鎧csdn博客:http://blog.csdn.net/totond 炎之鎧郵箱:yanzhikai_yjk@qq.com 本項目Github地址:https://github.com/totond/TextPathView 本文原創,轉載請註明本出處! 本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈git
此博客主要是介紹TextPathView的實現原理,而TextPathView的使用能夠參考README,效果如圖:github
下面寫的實現TextPathView思路介紹主要有兩部分:一部分是文字路徑的實現,包括文字路徑的獲取、同步繪畫和異步繪畫;一部分是畫筆特效,包括各類畫筆特效的實現思路。canvas
文字路徑的實現是核心部分,主要的工做就是把輸入的文字轉化爲Path,而後繪畫出來。繪畫分爲兩種繪畫:bash
一種是同步繪畫,也就是至關於只有一支「畫筆」,按順序來每一個筆畫來繪畫出文字Path。以下面: 微信
一種是異步繪畫,也就是至關於多支「畫筆」,每一個筆畫(閉合的路徑)有一支,來一塊兒繪畫出文字Path。以下面: dom
這二者的區別大概就像一個線程同步繪畫和多個異步繪畫同樣,固然實際實現是都是在主線程裏面繪畫的,具體實現能夠看下面介紹。異步
獲取文字路徑用到的是Paint的一個方法getTextPath(String text, int start, int end,float x, float y, Path path)
,這個方法能夠獲取到一整個String的Path(包括全部閉合Path),而後設置在一個PathMeasure類裏面,方便後面繪畫的時候截取路徑。如SyncTextPathView裏面的:ide
//初始化文字路徑
@Override
protected void initTextPath(){
//...
mTextPaint.getTextPath(mText, 0, mText.length(), 0, mTextPaint.getTextSize(), mFontPath);
mPathMeasure.setPath(mFontPath, false);
mLengthSum = mPathMeasure.getLength();
//獲取全部路徑的總長度
while (mPathMeasure.nextContour()) {
mLengthSum += mPathMeasure.getLength();
}
}
複製代碼
每次設定輸入的String值的時候都會調用initTextPath()
來初始化文字路徑。post
PathMeasure是Path的一個輔助類,能夠實現截取Path,獲取Path上點的座標,正切值等等,具體使用網上不少介紹。動畫
同步繪畫,也就是按順序繪畫每一個筆畫(至於筆畫的順序是誰先誰後,就要看Paint.getTextPath()
方法的實現了,這不是重點),這種刻畫在SyncTextPathView實現。 這種繪畫方法不復雜,就是根據輸入的比例來決定文字路徑的顯示比例就好了,想是這樣想,具體實現仍是要經過代碼的,這裏先給出一些全局屬性的介紹:
//文字裝載路徑、文字繪畫路徑、畫筆特效路徑
protected Path mFontPath = new Path(), mDst = new Path(), mPaintPath = new Path();
//屬性動畫
protected ValueAnimator mAnimator;
//動畫進度值
protected float mAnimatorValue = 0;
//繪畫部分長度
protected float mStop = 0;
//是否展現畫筆
protected boolean showPainter = false, canShowPainter = false;
//當前繪畫位置
protected float[] mCurPos = new float[2];
複製代碼
根據以前init時候獲取的總長度mLengthSum和比例progress,來求取將要繪畫的文字路徑部分的長度mStop,而後用一個while循環使得mPathMeasure定位到最後一段Path片斷,在這期間把循環的到片斷都加入到要繪畫的目標路徑mDst,而後最後在按照剩下的長度截取最後一段Path片斷:
/**
* 繪畫文字路徑的方法
* @param progress 繪畫進度,0-1
*/
@Override
public void drawPath(float progress) {
if (!isProgressValid(progress)){
return;
}
mAnimatorValue = progress;
mStop = mLengthSum * progress;
//重置路徑
mPathMeasure.setPath(mFontPath, false);
mDst.reset();
mPaintPath.reset();
//根據進度獲取路徑
while (mStop > mPathMeasure.getLength()) {
mStop = mStop - mPathMeasure.getLength();
mPathMeasure.getSegment(0, mPathMeasure.getLength(), mDst, true);
if (!mPathMeasure.nextContour()) {
break;
}
}
mPathMeasure.getSegment(0, mStop, mDst, true);
//繪畫畫筆特效
if (canShowPainter) {
mPathMeasure.getPosTan(mStop, mCurPos, null);
drawPaintPath(mCurPos[0], mCurPos[1], mPaintPath);
}
//繪畫路徑
postInvalidate();
}
複製代碼
在最後調用的onDraw():
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//...
//畫筆特效繪製
if (canShowPainter) {
canvas.drawPath(mPaintPath, mPaint);
}
//文字路徑繪製
canvas.drawPath(mDst, mDrawPaint);
}
複製代碼
這樣子就能夠畫出progress相對應比例的文字路徑了。
異步繪畫,也就是至關於多支「畫筆」,每一個筆畫(閉合的路徑)有一支,來一塊兒繪畫出文字Path。,這種刻畫在AsyncTextPathView實現。 這種繪畫方法也不是很複雜,就是根據比例來決定文字路徑裏面每個筆畫(閉合的路徑)的顯示比例就好了。 具體就是使用while循環遍歷全部筆畫(閉合的路徑)Path,循環裏面根據progress比例算出截取的長度mStop,而後加入到mDst中,最後繪畫出來。這裏給出drawPath()
代碼就好了:
/**
* 繪畫文字路徑的方法
* @param progress 繪畫進度,0-1
*/
@Override
public void drawPath(float progress){
if (!isProgressValid(progress)){
return;
}
mAnimatorValue = progress;
//重置路徑
mPathMeasure.setPath(mFontPath,false);
mDst.reset();
mPaintPath.reset();
//根據進度獲取路徑
while (mPathMeasure.nextContour()) {
mLength = mPathMeasure.getLength();
mStop = mLength * mAnimatorValue;
mPathMeasure.getSegment(0, mStop, mDst, true);
//繪畫畫筆特效
if (canShowPainter) {
mPathMeasure.getPosTan(mStop, mCurPos, null);
drawPaintPath(mCurPos[0],mCurPos[1],mPaintPath);
}
}
//繪畫路徑
postInvalidate();
}
複製代碼
這樣就能以每一個筆畫做爲一個個體,按比例顯示文字路徑了。
畫筆特效就是以當前繪畫終點爲基準,增長一點Path,來使整個動畫看起來更加好看的操做。以下面的火花特效:
具體的原理就是利用PathMeasurel類的getPosTan(float distance, float pos[], float tan[])
方法,在每次繪畫文字路徑的時候調用drawPaintPath()
來繪畫附近的mPaintPath,而後在ondraw()
畫出來就行了:
/**
* 繪畫文字路徑的方法
* @param progress 繪畫進度,0-1
*/
@Override
public void drawPath(float progress) {
//...
//繪畫畫筆特效
if (canShowPainter) {
mPathMeasure.getPosTan(mStop, mCurPos, null);
drawPaintPath(mCurPos[0], mCurPos[1], mPaintPath);
}
//繪畫路徑
postInvalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//...
//畫筆特效繪製
if (canShowPainter) {
canvas.drawPath(mPaintPath, mPaint);
}
//文字路徑繪製
canvas.drawPath(mDst, mDrawPaint);
}
複製代碼
而drawPaintPath()
方法的實現是這樣的(以SyncTextPathView爲例):
//畫筆特效
private SyncTextPainter mPainter;
private void drawPaintPath(float x, float y, Path paintPath) {
if (mPainter != null) {
mPainter.onDrawPaintPath(x, y, paintPath);
}
}
複製代碼
這裏的畫筆特效Painter就是一個接口,可讓使用者自定義的,由於繪畫的原理不同,Painter也分兩種:
public interface SyncTextPainter extends TextPainter {
//開始動畫的時候執行
void onStartAnimation();
/**
* 繪畫畫筆特效時候執行
* @param x 當前繪畫點x座標
* @param y 當前繪畫點y座標
* @param paintPath 畫筆Path對象,在這裏畫出想要的畫筆特效
*/
@Override
void onDrawPaintPath(float x, float y, Path paintPath);
}
public interface AsyncTextPainter extends TextPainter{
/**
* 繪畫畫筆特效時候執行
* @param x 當前繪畫點x座標
* @param y 當前繪畫點y座標
* @param paintPath 畫筆Path對象,在這裏畫出想要的畫筆特效
*/
@Override
void onDrawPaintPath(float x, float y, Path paintPath);
}
複製代碼
TextPainter就不用說了,是父接口。而後使用者是經過set方法來傳入TextPainter
//設置畫筆特效
public void setTextPainter(SyncTextPainter listener) {
this.mPainter = listener;
}
複製代碼
以上就是畫筆特效的原理,使用者經過重寫TextPainter接口來繪畫附加特效。
TextPathView暫時實現了3種自帶的畫筆特效能夠選擇:
//箭頭畫筆特效,根據傳入的當前點與上一個點之間的速度方向,來調整箭頭方向
public class ArrowPainter implements SyncTextPathView.SyncTextPainter{}
//一支筆的畫筆特效,就是在繪畫點旁邊畫多一支筆
public class PenPainter implements SyncTextPathView.SyncTextPainter,AsyncTextPathView.AsyncTextPainter {}
//火花特效,根據箭頭引伸變化而來,根據當前點與上一個點算出的速度方向來控制火花的方向
public class FireworksPainter implements SyncTextPathView.SyncTextPainter{}
複製代碼
下面介紹箭頭和火花,筆太簡單了不用說,直接看代碼就能夠懂。而後這二者都用到了一個計算速度的類:
/**
* author : yany
* e-mail : yanzhikai_yjk@qq.com
* time : 2018/02/08
* desc : 計算傳入的當前點與上一個點之間的速度
*/
public class VelocityCalculator {
private float mLastX = 0;
private float mLastY = 0;
private long mLastTime = 0;
private boolean first = true;
private float mVelocityX = 0;
private float mVelocityY = 0;
//重置
public void reset(){
mLastX = 0;
mLastY = 0;
mLastTime = 0;
first = true;
}
//計算速度
public void calculate(float x, float y){
long time = System.currentTimeMillis();
if (!first){
//由於只須要方向,不須要具體速度值,因此默認deltaTime = 1,提升效率
// float deltaTime = time - mLastTime;
// mVelocityX = (x - mLastX) / deltaTime;
// mVelocityY = (y - mLastY) / deltaTime;
mVelocityX = x - mLastX;
mVelocityY = y - mLastY;
}else {
first = false;
}
mLastX = x;
mLastY = y;
mLastTime = time;
}
public float getVelocityX() {
return mVelocityX;
}
public float getVelocityY() {
return mVelocityY;
}
}
複製代碼
因此這個Path就應該是:在前進速度的反方向,以當前繪畫點爲起點,以必定夾角畫出兩條直線:
因此咱們能夠轉化爲幾何數學問題:已知箭頭長別爲r,夾角爲a,還有當前點座標(x,y),還有它的速度夾角angle,求出箭頭兩個末端的座標(字寫的難看,不要在乎這些細節啦O(∩_∩)O):
上面這個簡單的高中數學問題竟然搞了半天,具體是由於我一開始沒有使用Android的View座標系來畫,一直用傳統的數學座標系來畫,因此算出來每次都有誤差,意識到這個問題以後就簡單了。
根據上面的推導過程咱們能夠得出箭頭兩個末端的座標,而後就是用代碼表達出來了:
/**
* author : yany
* e-mail : yanzhikai_yjk@qq.com
* time : 2018/02/09
* desc : 箭頭畫筆特效,根據傳入的當前點與上一個點之間的速度方向,來調整箭頭方向
*/
public class ArrowPainter implements SyncTextPathView.SyncTextPainter {
private VelocityCalculator mVelocityCalculator = new VelocityCalculator();
//箭頭長度
private float radius = 60;
//箭頭夾角
private double angle = Math.PI / 8;
//...
@Override
public void onDrawPaintPath(float x, float y, Path paintPath) {
mVelocityCalculator.calculate(x, y);
double angleV = Math.atan2(mVelocityCalculator.getVelocityY(), mVelocityCalculator.getVelocityX());
double delta = angleV - angle;
double sum = angleV + angle;
double rr = radius / (2 * Math.cos(angle));
float x1 = (float) (rr * Math.cos(sum));
float y1 = (float) (rr * Math.sin(sum));
float x2 = (float) (rr * Math.cos(delta));
float y2 = (float) (rr * Math.sin(delta));
paintPath.moveTo(x, y);
paintPath.lineTo(x - x1, y - y1);
paintPath.moveTo(x, y);
paintPath.lineTo(x - x2, y - y2);
}
@Override
public void onStartAnimation() {
mVelocityCalculator.reset();
}
}
//一些set方法...
複製代碼
/**
* author : yany
* e-mail : yanzhikai_yjk@qq.com
* time : 2018/02/11
* desc : 火花特效,根據箭頭引伸變化而來,根據當前點與上一個點算出的速度方向來控制火花的方向
*/
public class FireworksPainter implements SyncTextPathView.SyncTextPainter {
private VelocityCalculator mVelocityCalculator = new VelocityCalculator();
private Random random = new Random();
//箭頭長度
private float radius = 100;
//箭頭夾角
private double angle = Math.PI / 8;
//同時存在箭頭數
private static final int arrowCount = 6;
//最大線段切斷數
private static final int cutCount = 9;
public FireworksPainter(){
}
public FireworksPainter(int radius,double angle){
this.radius = radius;
this.angle = angle;
}
@Override
public void onDrawPaintPath(float x, float y, Path paintPath) {
mVelocityCalculator.calculate(x, y);
for (int i = 0; i < arrowCount; i++) {
double angleV = Math.atan2(mVelocityCalculator.getVelocityY(), mVelocityCalculator.getVelocityX());
double rAngle = (angle * random.nextDouble());
double delta = angleV - rAngle;
double sum = angleV + rAngle;
double rr = radius * random.nextDouble() / (2 * Math.cos(rAngle));
float x1 = (float) (rr * Math.cos(sum));
float y1 = (float) (rr * Math.sin(sum));
float x2 = (float) (rr * Math.cos(delta));
float y2 = (float) (rr * Math.sin(delta));
splitPath(x, y, x - x1, y - y1, paintPath, random.nextInt(cutCount) + 2);
splitPath(x, y, x - x2, y - y2, paintPath, random.nextInt(cutCount) + 2);
}
}
@Override
public void onStartAnimation() {
mVelocityCalculator.reset();
}
//分解Path爲虛線
//注意count要大於0
private void splitPath(float startX, float startY, float endX, float endY, Path path, int count) {
float deltaX = (endX - startX) / count;
float deltaY = (endY - startY) / count;
for (int i = 0; i < count; i++) {
if (i % 3 == 0) {
path.moveTo(startX, startY);
path.lineTo(startX + deltaX, startY + deltaY);
}
startX += deltaX;
startY += deltaY;
}
}
}
複製代碼
上面介紹的都是局部的細節實現,可是TextPathView做爲一個自定義View,是須要封裝一個總體的工做流程的,這樣才能讓使用者方便地使用,下降耦合性。
看過README的都知道,TextPathView並不提供給用戶直接使用,而是讓用戶來使用它的子類SyncTextPathView和AsyncTextPathView來實現同步繪畫和異步繪畫的功能。而父類TextPathView則是負責寫一些給子類複用的代碼。具體代碼就不貼了,能夠直接看Github。
SyncTextPathView和AsyncTextPathView的工做過程是差很少的,這裏以SyncTextPathView爲例,介紹它從建立到使用完動畫的過程。
init()
方法:protected void init() {
//初始化畫筆
initPaint();
//初始化文字路徑
initTextPath();
//是否自動播放動畫
if (mAutoStart) {
startAnimation(0,1);
}
//是否一開始就顯示出完整的文字路徑
if (mShowInStart){
drawPath(1);
}
}
protected void initPaint(){
mTextPaint = new Paint();
mTextPaint.setTextSize(mTextSize);
mDrawPaint = new Paint();
mDrawPaint.setAntiAlias(true);
mDrawPaint.setColor(mTextStrokeColor);
mDrawPaint.setStrokeWidth(mTextStrokeWidth);
mDrawPaint.setStyle(Paint.Style.STROKE);
if (mTextInCenter){
mDrawPaint.setTextAlign(Paint.Align.CENTER);
}
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setColor(mPaintStrokeColor);
mPaint.setStrokeWidth(mPaintStrokeWidth);
mPaint.setStyle(Paint.Style.STROKE);
}
//省略對initTextPath()和drawPath()方法的代碼,由於前面已經有...
複製代碼
/**
* 重寫onMeasure方法使得WRAP_CONTENT生效
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int hSpeSize = MeasureSpec.getSize(heightMeasureSpec);
// int hSpeMode = MeasureSpec.getMode(heightMeasureSpec);
int wSpeSize = MeasureSpec.getSize(widthMeasureSpec);
// int wSpeMode = MeasureSpec.getMode(widthMeasureSpec);
int width = wSpeSize;
int height = hSpeSize;
mTextWidth = TextUtil.getTextWidth(mTextPaint,mText);
mTextHeight = mTextPaint.getFontSpacing() + 1;
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT){
width = (int) mTextWidth;
}
if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT){
height = (int) mTextHeight;
}
setMeasuredDimension(width,height);
}
複製代碼
startAnimation()
開始繪製文字路徑動畫:/**
* 開始繪製文字路徑動畫
* @param start 路徑比例,範圍0-1
* @param end 路徑比例,範圍0-1
*/
public void startAnimation(float start, float end) {
if (!isProgressValid(start) || !isProgressValid(end)){
return;
}
if (mAnimator != null) {
mAnimator.cancel();
}
initAnimator(start, end);
initTextPath();
canShowPainter = showPainter;
mAnimator.start();
if (mPainter != null) {
mPainter.onStartAnimation();
}
}
複製代碼
以上就是SyncTextPathView的一個簡單的工做流程,註釋應該都寫的挺清楚的了,裏面還有一些細節,若是想了解能夠查看源碼。
showFillColorText()
方法來設置直接顯示填充好顏色了的所有文字。showPainterActually
屬性,設置全部時候是否顯示畫筆效果,因爲動畫繪畫完畢應該將畫筆特效消失,因此每次執行完動畫都會自動將它設置爲false。所以它用處就是在不使用自帶Animator的時候顯示畫筆特效。終於完成了TextPathView的原理介紹,TextPathView我目前想到的應用場景就是作一些簡單的開場動畫或者進度顯示。它是我元旦後在工做外抽空寫的,最近幾個月工做很忙,生活上遇到了不少的事情,可是仍是要堅持作一些本身喜歡的事情,TextPathView會繼續維護下去和開發新的東西,但願你們喜歡的話給個star,有意見和建議的提個issue,多多指教。
最後再貼上地址:https://github.com/totond/TextPathView