Android字型漸變效果實戰!

語言: CN / TW / HK

 BATcoder技術 群,讓一部分人先進大廠

大家好,我是劉望舒,騰訊最具價值專家,著有三本業內知名暢銷書,連續五年蟬聯電子工業出版社年度優秀作者, 百度百科收錄的資深技術專家。

前華為面試官、獨角獸公司技術總監。

想要 加入  BATcoder技術群,公號回覆 BAT  即可。

作者:android超級兵

https://blog.csdn.net/weixin_44819566

先來看看完成的效果:

簡單解釋,在滑動的過程中,漸變文字會隨著ViewPager的滑動而變化!!

繪製文字與BaseLine思考

先來看看最初版程式碼:

public class   GradualChangeTv   extends   AppCompatTextView   {

public Paint mPaint =  new Paint();

public final String text =  "android 超級兵" ;

public GradualChangeTv (Context context) {

this (context,  null );

}

public GradualChangeTv (Context context, AttributeSet attrs) {

this (context, attrs,  0 );

}

public GradualChangeTv (Context context, AttributeSet attrs,  int  defStyleAttr) {

super (context, attrs, defStyleAttr);

mPaint.setColor(Color.RED);

//抗鋸齒

mPaint.setAntiAlias( true );

}

@Override

protected void onDraw (Canvas canvas) {

super .onDraw(canvas);

/*

* 繪製文字

* 引數一: 繪製文字

* 引數二: x軸開始位置

* 引數三: y 軸開始位置

* 引數四: 畫筆

canvas.drawText(text,  00 , mPaint);

}

}

就是簡單的繪製了一行字。

疑問

為什麼這裡要繼承自AppCompatTextView而不是View?

答:偷個懶而已,因為不用在我來測量View,直接用父類的就行

來看看效果順便也看看佈局:

出現問題

文字並沒有顯示。

答:因為文字座標系和螢幕座標系不一樣,文字座標系是從BaseLine線開始計算的。

先來回顧一下螢幕的座標系:

再來看看文字的座標系。

(圖片來自於網路)

再來思考一下文字是為什麼不顯示的:

虛線為BaseLine

如果此時我把字型放大到100,看一看我能不能看到文字。

再一次證明了文字是從BaseLine線開始繪製。

文字居中

可以用兩條輔助線,水平線與垂直線。然後在來看文字是否居中。

程式碼

:warning: 底部會給出完整程式碼。這裡看思路即可,不用複製程式碼。

@Override

protected void onDraw (Canvas canvas) {

super .onDraw(canvas);

//獲取當前控制元件的寬高

int viewWidth = getWidth() /  2 ;

int viewHeight = getHeight() /  2 ;

/*

* 繪製文字

* 引數一: 繪製文字

* 引數二: x軸開始位置

* 引數三: y 軸開始位置

* 引數四: 畫筆

canvas.drawText(text, viewWidth, viewHeight, mPaint);

//繪製居中線

drawCenterLine(canvas, viewWidth, viewHeight);

}

private void drawCenterLine (Canvas canvas,  int  viewWidth,  int  viewHeight) {

//垂直線

canvas.drawLine(viewWidth,  0 , viewWidth, getHeight(), mPaint);

//水平線

canvas.drawLine( 0 ,viewHeight,getWidth(),viewHeight,mPaint);

}

效果圖

可以看出,還是上面說的那個問題,文字繪製是基於baseLine線來繪製的。

文字居中思路:

  • 通過mPaint.measureText(text) 獲取文字寬

  • 通過mPaint.descent() + mPaint.ascent(); 獲取文字高

  • 然後控制元件各取一半,讓控制元件減去即可

這裡的descent和ascent可以參考上面文字繪製圖。

相關程式碼

@Override

protected void onDraw (Canvas canvas) {

super .onDraw(canvas);

//文字寬度

float textWidth = mPaint.measureText(text);

//文字高度

float textHeight = mPaint.descent() + mPaint.ascent();

//獲取當前控制元件的寬高

int viewWidth = getWidth() /  2 ;

int viewHeight = getHeight() /  2 ;

canvas.drawText(text, viewWidth - textWidth /  2 , viewHeight - textHeight /  2 , mPaint);

//繪製居中線

drawCenterLine(canvas, viewWidth, viewHeight);

}

效果圖

裁剪

@Override

protected void onDraw (Canvas canvas) {

super .onDraw(canvas);

//文字寬度

float textWidth = mPaint.measureText(text);

//文字高度

float textHeight = mPaint.descent() + mPaint.ascent();

//獲取當前控制元件的寬高的一半

int viewWidth = getWidth() /  2 ;

int viewHeight = getHeight() /  2 ;

//裁剪

drawClip(canvas, viewWidth, viewHeight, textWidth, textHeight);

//繪製居中線

drawCenterLine(canvas, viewWidth, viewHeight);

}

private void drawClip (Canvas canvas,  int  viewWidth,  int  viewHeight,  float  textWidth,  float  textHeight) {

mPaint.setColor(Color.BLACK);

canvas.save();

//繪製文字X軸的位置

float left = viewWidth - textWidth /  2 ;

//繪製文字Y軸的位置

float right = viewHeight - textHeight /  2 ;

//裁剪

canvas.clipRect(( int ) left,  0 , ( int ) left +  300 , getHeight());

/*

* 繪製文字

* 引數一: 繪製文字

* 引數二: x軸開始位置

* 引數三: y 軸開始位置

* 引數四: 畫筆

canvas.drawText(text, left, right, mPaint);

canvas.restore();

}

裁剪(clipRect)引數分析:

  • 引數一: 從文字開始位置繪製

  • 引數二: 頂部裁剪為0

  • 引數三: 裁剪寬度

  • 引數四: 繪製高度

canvas的save()和restore()方法可以理解為將當前繪製的東西當作一個新的圖層!

來看看效果圖:

程式碼註釋很清晰,就不過多解釋了。

從左到右漸變文字

眾所周知,在android中是不能夠將文字繪製一般的。

思路分析:

  • 繪製兩層(兩層顏色不同),兩層疊加起來

  • 然後通過裁剪將上面一層給裁剪掉

在來看看現在程式碼是什麼樣子的:

//用來記錄當前進度 【0-1】

float progress =  0.3f ;

@Override

protected void onDraw (Canvas canvas) {

super .onDraw(canvas);

//文字寬度

float textWidth = mPaint.measureText(text);

//文字高度

float textHeight = mPaint.descent() + mPaint.ascent();

//獲取當前控制元件的寬高的一半

int viewWidth = getWidth() /  2 ;

int viewHeight = getHeight() /  2 ;

//繪製底層

drawBottom(canvas, viewWidth, viewHeight, textWidth, textHeight);

//繪製上層【顏色漸變的】

drawUp(canvas, viewWidth, viewHeight, textWidth, textHeight);

//繪製居中線

drawCenterLine(canvas, viewWidth, viewHeight);

}

//繪製上層【漸變的】

private void drawUp (Canvas canvas,  int  viewWidth,  int  viewHeight,  float  textWidth,  float  textHeight) {

mPaint.setColor(Color.BLACK);

canvas.save();

//繪製文字X軸的位置

float left = viewWidth - textWidth /  2 ;

//繪製文字Y軸的位置

float right = viewHeight - textHeight /  2 ;

//裁剪

canvas.clipRect(( int ) left,  0 , ( int ) left + textWidth * progress, getHeight());

/*

* 繪製文字

* 引數一: 繪製文字

* 引數二: x軸開始位置

* 引數三: y 軸開始位置

* 引數四: 畫筆

canvas.drawText(text, left, right, mPaint);

canvas.restore();

}

//繪製下層 不動的

private void drawBottom (Canvas canvas,  int  viewWidth,  int  viewHeight,  float  textWidth,  float  textHeight) {

mPaint.setColor(Color.RED);   //文字顏色

canvas.save();

//文字開始位置

float left = viewWidth - textWidth /  2 ;

/*

* 繪製文字

* 引數一: 繪製文字

* 引數二: x軸開始位置

* 引數三: y 軸開始位置

* 引數四: 畫筆

canvas.drawText(text, left, viewHeight - textHeight /  2 , mPaint);

canvas.restore();

}

這裡重點解釋一下上層[需要裁剪的]引數:

//裁剪

canvas.clipRect(( int ) left,  0

int

) left + textWidth * progress, getHeight());

  • textWidth需要繪製文字的寬度

  • viewWidth控制元件寬度的一半

  • 文字開始的位置:left = viewWidth - textWidth / 2

  • 文字需要裁剪的位置:文字的寬度 * progress

通過手勢滑動來控制。這段程式碼並沒有實質性作用,只是來看看效果。

@SuppressLint ( "ClickableViewAccessibility" )

@Override

public boolean onTouchEvent (MotionEvent event) {

if (event.getAction() == MotionEvent.ACTION_MOVE) {

progress = event.getX() / getWidth();

invalidate();

}

return true ;

}

效果圖

從右到左漸變文字

思路和從左到右繪製是一樣的直接看關鍵程式碼:

private void drawRightToLeft (Canvas canvas,  int  viewWidth,  int  viewHeight,  float  textWidth,  float  textHeight) {

mPaint.setColor(Color.GREEN);

/*

* 這裡 left和right能夠在此抽取出來,不過這樣寫很易懂,有需求自己弄吧!!!

canvas.save();

//繪製文字X軸的位置 【文字開始的位置】

float textX = viewWidth - textWidth /  2 ;

//繪製文字Y軸的位置

float textY = viewHeight - textHeight /  2 ;

//文字結束的位置

float end = viewWidth + mPaint.measureText(text) /  2 ;

canvas.clipRect(end,  0 , textX + textWidth * ( 1 - progress), getHeight());

canvas.drawText(text, textX, textY, mPaint);

canvas.restore();

}

@SuppressLint ( "ClickableViewAccessibility" )

@Override

public boolean onTouchEvent (MotionEvent event) {

if (event.getAction() == MotionEvent.ACTION_MOVE) {

if (type == GradualChangeTextView.GRADUAL_CHANGE_RIGHT) {

//從右到左滑動

progress =  1 - event.getX() / getWidth();

else if (type == GradualChangeTextView.GRADUAL_CHANGE_LEFT) {

//從左到右滑動

progress = event.getX() / getWidth();

}

invalidate();

}

return true ;

}

效果圖

最後在新增兩個按鈕來完全測試一下程式碼有沒有問題。

完完全全沒有問題!

最終實現效果(漸變滑動)

先來看看佈局:

佈局簡單的很,就是文字和ViewPager。大致看看ViewPager程式碼:

//text1 .. text4 是控制元件id

val textList = listOf(text1, text2, text3, text4)

val list = listOf(HomeFragment(), MyFragment(), TestFragment(), SettingFragment())

val viewPagerAdapter = ViewPagerAdapter(supportFragmentManager, list)

viewPager.adapter = viewPagerAdapter

//預設選擇第一頁

viewPager.currentItem =  1

//預設選中

1f

這段程式碼只要學過就懂,不細說了。重中之重來了:

viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {

override fun  onPageScrolled

(

position: Int,

positionOffset: Float,

positionOffsetPixels: Int,

{

if (positionOffset >  0 ) {

val left = textList[position]

val right = textList[position +  1 ]

//從右到左滑動

left.setSlidingPosition(GradualChangeTextView.GRADUAL_CHANGE_RIGHT)

//從左到右滑動

right.setSlidingPosition(GradualChangeTextView.GRADUAL_CHANGE_LEFT)

//當前頁面取反[從右到左]

left.percent =  1 - positionOffset

//下一個頁面正常[從左到右]

right.percent = positionOffset

}

}

override fun  onPageSelected (position: Int) { }

override fun  onPageScrollStateChanged (state: Int) {

//當 ViewPage結束的時候,重新設定一下狀態 [不設定的話滑動太快,會導致'殘影']

textList.forEach {

if (it.tag == textList[viewPager.currentItem].tag) {

it.percent =  1f

else {

it.percent =  0f

}

}

}

})

來看看效果

過度繪製極限優化

什麼是過度繪製,參考文件:

https://www.jianshu.com/p/2cc6d5842986

重點總結

  1. 原色 – 沒有被過度繪製 – 這部分的畫素點只在螢幕上繪製了一次。

  2. 藍色 – 1次過度繪製– 這部分的畫素點只在螢幕上繪製了兩次。

  3. 綠色 – 2次過度繪製 – 這部分的畫素點只在螢幕上繪製了三次。

  4. 粉色 – 3次過度繪製 – 這部分的畫素點只在螢幕上繪製了四次。

  5. 紅色 – 4次過度繪製 – 這部分的畫素點只在螢幕上繪製了五次。

先來看看沒有優化的效果:

可以看到,在繪製的過程中,因為是兩層,那麼就繪製了2次。

優化思路

當黑色[上層]從左到右滑動的時候,紅色[下層]跟隨著從左到右裁剪。來看看下層繪製的程式碼:

//繪製下層 不動的

private void drawBottom (Canvas canvas,  int  viewWidth,  int  viewHeight,  float  textWidth,  float  textHeight) {

mPaint.setColor(Color.RED);

canvas.save();

//繪製文字X軸的位置 [文字開始的位置]

float textX = viewWidth - textWidth /  2 ;

//繪製文字Y軸的位置

float textY = viewHeight - textHeight /  2 ;

//跟隨者上層裁剪

canvas.clipRect(( int ) textX + textWidth * progress,  0 , textWidth + viewWidth, getHeight());

/*

* 繪製文字

* 引數一: 繪製文字

* 引數二: x軸開始位置

* 引數三: y 軸開始位置

* 引數四: 畫筆

canvas.drawText(text, textX, textY, mPaint);

canvas.restore();

}

效果圖

完整程式碼地址如下所示:

https://gitee.com/lanyangyangzzz/android_ui