Android 漸變的‘TabLayout’ , (含免費( java / kotlin) Demo)

語言: CN / TW / HK

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家發佈

@TOC

先來看看完成的效果:


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

繪製文字與BaseLine思考

先來看看最初版代碼:

```java 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, 0, 0, mPaint);
}

} ``` 就是簡單的繪製了一行字

疑問: - 為什麼這裏要繼承自AppCompatTextView 而不是View

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

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

出現問題: 文字並沒有顯示 答:因為文字座標系和屏幕座標系不一樣,文字座標系是從BaseLine線開始計算的

先來回顧一下屏幕的座標系


在來看看文字的座標系


(圖片來自於網絡)

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

  • 虛線為BaseLine

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


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

文字居中

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

代碼:

⚠️ : 底部會給出完整代碼.這裏看思路即可,不用複製代碼 ```java @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可以參考上面文字繪製圖

相關代碼:

```java @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);
}

``` 效果圖:


裁剪

```java @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(),和canvas.restore();方法
可以理解為:將當前繪製的東西當作一個新的圖層!

來看看效果圖:

代碼註釋很清晰;就不過多解釋了

從左到右漸變文字

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

思路分析: - 繪製兩層(兩層顏色不同),兩層疊加起來 - 然後通過裁剪將上面一層給裁剪掉

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

```java //用來記錄當前進度 【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();
}

這裏重點解釋一下上層[需要裁剪的]參數:java //裁剪 canvas.clipRect((int) left, 0, (int) left + textWidth * progress, getHeight()); ``` - textWidth 需要繪製文字的寬度 - viewWidth 控件寬度的一半 - 文字開始的位置:left = viewWidth - textWidth / 2; - 文字需要裁剪的位置: 文字的寬度 * progress


通過手勢滑動來控制:

這段代碼並沒有實質性作用,只是來看看效果:

java @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_MOVE) { progress = event.getX() / getWidth(); invalidate(); } return true; } 效果圖:


從右到左漸變文字

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

```java 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代碼:

```kotlin //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

    //默認選中
    textList[viewPager.currentItem].percent = 1f

``` 這段代碼,只要學過就懂,不細説了!

重中之重來了:

```java 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
                }
            }
        }
    })

``` 來看看效果:


過度繪製極限優化

什麼是過度繪製:

參考文檔

重點總結: - 1.原色 – 沒有被過度繪製 – 這部分的像素點只在屏幕上繪製了一次。 - 2.藍色 – 1次過度繪製– 這部分的像素點只在屏幕上繪製了兩次。 - 3.綠色 – 2次過度繪製 – 這部分的像素點只在屏幕上繪製了三次。 - 4.粉色 – 3次過度繪製 – 這部分的像素點只在屏幕上繪製了四次。 - 5.紅色 – 4次過度繪製 – 這部分的像素點只在屏幕上繪製了五次。

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

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

優化思路: 當黑色[上層]從左到右滑動的時候,紅色[下層]跟隨着從左到右裁剪

來看看下層繪製的代碼:

```java //繪製下層 不動的 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();
}

``` 效果圖:

完整代碼

原創不易,您的點贊就是對我最大的支持!