1. 程式人生 > >【Android開源專案解析】背景有波浪效果的TextView——從Titanic專案學習BitmapShader的使用

【Android開源專案解析】背景有波浪效果的TextView——從Titanic專案學習BitmapShader的使用

Hello,好久沒寫文章了,有木有想我呀~
正式工作已經過去一個月了,發現在青島實習和在北京工作,感覺完全不一樣呢~
現在每天晚上回到住的地方,都累的想睡覺…所以也沒心情寫太多文章和大家分享了,不過我會盡快調整狀態,重振雄風的!(哪裡起來怪怪的…)

專案介紹

這篇文章,會介紹一個開源專案,叫做Titanic,是的,中文名就叫“泰坦尼克”…

要實現的效果是下面這樣滴

我的想法

如果你是一個程式設計師,那麼在你第一眼看到這個效果的時候,你可能會想,如果我要實現類似的效果,應該怎麼做呢?

我不知道你們會有什麼樣的思路,我第一眼看到的時候,我想起了PorterDuffXfermode,前面是一張文字的圖片,後面一張波浪的圖片,然後設定相交模式,從而出現這種文字和圖片的交叉效果,不斷的改變後面圖片位置,實現動態的效果。

當然,我只是想了一想,然後就抱著這個想法看原始碼去了,看完原始碼才發現,作者的實現思路更加的NB~通過BitmapShader實現了這種效果,下面,我會參照原始碼,給你介紹一下實現思路,非常簡單,不要眨眼哦~

實現思路

Titanic這個專案非常的簡單,只有兩個類,分別是Titanic和TitanicTextView,那麼如何使用呢?非常簡單:

TitanicTextView tv = (TitanicTextView) findViewById(R.id.my_text_view);
        tv.setTypeface(Typefaces.get(this, "Satisfy-Regular.ttf"
)); new Titanic().start(tv);

如果你不想設定特殊字型,那麼中間的程式碼刪除也可以,兩行程式碼搞定,是不是非常easy~

那麼Titanic這個類是幹嘛的呢?

為了方便理解和觀看,我將不重要程式碼省略了,你可以對照原始碼觀看
我們呼叫了Titanic的start()之後,就執行了下面的程式碼了:

public void start(final TitanicTextView textView) {

        final Runnable animate = new Runnable() {
            @Override
public void run() { textView.setSinking(true); // horizontal animation. 200 = wave.png width ObjectAnimator maskXAnimator = ObjectAnimator.ofFloat(textView, "maskX", 0, 200); maskXAnimator.setRepeatCount(ValueAnimator.INFINITE); maskXAnimator.setDuration(1000); maskXAnimator.setStartDelay(0); int h = textView.getHeight(); // vertical animation // maskY = 0 -> wave vertically centered // repeat mode REVERSE to go back and forth ObjectAnimator maskYAnimator = ObjectAnimator.ofFloat(textView, "maskY", h/2, - h/2); maskYAnimator.setRepeatCount(ValueAnimator.INFINITE); maskYAnimator.setRepeatMode(ValueAnimator.REVERSE); maskYAnimator.setDuration(10000); maskYAnimator.setStartDelay(0); // now play both animations together animatorSet = new AnimatorSet(); animatorSet.playTogether(maskXAnimator, maskYAnimator); animatorSet.setInterpolator(new LinearInterpolator()); animatorSet.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { textView.setSinking(false); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { textView.postInvalidate(); } else { textView.postInvalidateOnAnimation(); } animatorSet = null; } 省略... }); if (animatorListener != null) { animatorSet.addListener(animatorListener); } animatorSet.start(); } if (!textView.isSetUp()) { textView.setAnimationSetupCallback(new TitanicTextView.AnimationSetupCallback() { @Override public void onSetupAnimation(final TitanicTextView target) { animate.run(); } }); } else { animate.run(); } };

這段程式碼是不是非常簡單?但是有點奇怪啊,裡面為什麼宣告Runnable物件呢?而且這裡沒開執行緒啊,而是直接呼叫的run(),好詭異的程式碼!

其實這個不需要多想,作者只不過是為了程式碼複用,所用把需要的程式碼放在run()方法,方便呼叫,但是這個程式碼風格我並不喜歡,因為這會讓其他閱讀程式碼的人產生誤會,比如我就為了這一塊程式碼,想了半個多小時他為什麼這樣寫…

我們看不慣,就可以改成下面的這種方式:

public void start(final TitanicTextView textView) {

        if (!textView.isSetUp()) {
            textView.setAnimationSetupCallback(new TitanicTextView.AnimationSetupCallback() {
                @Override
                public void onSetupAnimation(final TitanicTextView target) {
                    run(textView);
                }
            });
        } else {
            run(textView);
        }
    }

    private void run(final TitanicTextView textView) {
        textView.setSinking(true);

        ObjectAnimator maskXAnimator = ObjectAnimator.ofFloat(textView, "maskX", 0, 200);
        maskXAnimator.setRepeatCount(ValueAnimator.INFINITE);
        maskXAnimator.setDuration(1000);
        maskXAnimator.setStartDelay(0);

        int h = textView.getHeight();

        ObjectAnimator maskYAnimator = ObjectAnimator.ofFloat(textView, "maskY", h / 2, -h / 2);
        maskYAnimator.setRepeatCount(ValueAnimator.INFINITE);
        maskYAnimator.setRepeatMode(ValueAnimator.REVERSE);
        maskYAnimator.setDuration(10000);
        maskYAnimator.setStartDelay(0);

        // now play both animations together
        animatorSet = new AnimatorSet();
        animatorSet.playTogether(maskXAnimator, maskYAnimator);
        animatorSet.setInterpolator(new LinearInterpolator());
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                textView.setSinking(false);

                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
                    textView.postInvalidate();
                } else {
                    textView.postInvalidateOnAnimation();
                }

                animatorSet = null;
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        animatorSet.start();
    }

都是為了程式碼複用,這種程式碼風格我更喜歡一些。
ok,不扯淡,我們看看在start()裡面到底做了些什麼!

textView.setSinking(true);

        ObjectAnimator maskXAnimator = ObjectAnimator.ofFloat(textView, "maskX", 0, 200);
        maskXAnimator.setRepeatCount(ValueAnimator.INFINITE);
        maskXAnimator.setDuration(1000);
        maskXAnimator.setStartDelay(0);

        int h = textView.getHeight();

        ObjectAnimator maskYAnimator = ObjectAnimator.ofFloat(textView, "maskY", h / 2, -h / 2);
        maskYAnimator.setRepeatCount(ValueAnimator.INFINITE);
        maskYAnimator.setRepeatMode(ValueAnimator.REVERSE);
        maskYAnimator.setDuration(10000);
        maskYAnimator.setStartDelay(0);

這段程式碼也很好理解,設定了個標誌位,表示我們要開始下沉了哈!然後使用兩個ObjectAnimator來進行動畫,這個動畫是控制幹嘛的呢?

看裡面的引數,maskX和maskY,作者的註釋寫的很清楚,maskX的範圍是0-200,200是圖片的高度,什麼圖片?當然是後面波浪效果的圖片啦,見下圖(啥?沒有圖片!?白色的看不出來…右鍵儲存看吧):

注意哦,這張圖片的上半部分是透明的,下面是白色的,寬高為200*300。

maskY的變化範圍是h/2到-h/2,h則是TextView的高度,也就是上下浮動高度為h,那麼這個maskX和maskY到底是幹嘛的,這兩個屬性控制的是什麼呢?

首先,我們知道,如果要使用ObjectAnimator實現這種效果,那麼我們的控制元件裡面必須有這個屬性的getter和setter才行,所以這兩個屬性肯定是自定義的,我們去看TitanicTextView的程式碼驗證一下:

 public float getMaskX() {
        return maskX;
    }

    public void setMaskX(float maskX) {
        this.maskX = maskX;
        invalidate();
    }

    public float getMaskY() {
        return maskY;
    }

    public void setMaskY(float maskY) {
        this.maskY = maskY;
        invalidate();
    }

果然,我們看到,作者自己定義了這兩個成員變數,那麼改變這兩個值到底有什麼作用呢?我們找一下程式碼裡面有什麼地方用到了這兩個值:

 @Override
    protected void onDraw(Canvas canvas) {

        if (sinking && shader != null) {
            if (getPaint().getShader() == null) {
                getPaint().setShader(shader);
            }
            shaderMatrix.setTranslate(maskX, maskY + offsetY);
            shader.setLocalMatrix(shaderMatrix);
        } else {
            getPaint().setShader(null);
        }

        super.onDraw(canvas);
    }

耶,在onDraw()裡面,在super.onDraw()之前,用到了這兩個引數,這裡面有幾個變數,我們還沒有介紹

  • sinking 用於判斷是否開始浮動動畫
  • shader 這是一個BitmapShader,這是今天我們的男主角
  • shaderMatrix 這是一個變換矩陣,在這裡主要進行了setTranslate操作,引數正是我們的maskX和maskY還有offsetY

這個offsetY是啥子?程式碼裡找找

private void createShader() {
         if (wave == null) {
            wave = getResources().getDrawable(R.drawable.wave);
        }

        int waveW = wave.getIntrinsicWidth();
        int waveH = wave.getIntrinsicHeight();

        Bitmap b = Bitmap.createBitmap(waveW, waveH, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);

        c.drawColor(getCurrentTextColor());

        wave.setBounds(0, 0, waveW, waveH);
        wave.draw(c);

        shader = new BitmapShader(b, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
        getPaint().setShader(shader);
        offsetY = (getHeight() - waveH) / 2;
    }

在最後計算了offsetY,TextView的高度減去waveH,然後除以二,這得到的其實就是wave或者說b的y座標,在這個基礎上,再進行以下操作,就可以實現上下左右移動的動畫效果

shaderMatrix.setTranslate(maskX, maskY + offsetY);
shader.setLocalMatrix(shaderMatrix);

那麼這個shader到底是幹嘛的?為啥是我們的男主角?
我們在上面的程式碼裡面,可以看到不僅對offsetY進行了初始化,而且也對shader進行了初始化,裡面傳入了三個引數

new BitmapShader(b, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);

第一個引數b是設定Shader要使用的Bitmap物件,第二個引數設定x軸上影象的擴充套件方式,第三個引數則是y軸上的擴充套件方式,這個引數共有以下三種

  • CLAMP :如果渲染器超出原始邊界範圍,會複製範圍內邊緣染色
  • REPEAT :橫向和縱向的重複渲染器圖片,平鋪
  • MIRROR:映象方式平鋪。

那麼上面的寫法我們就很明白了,作者在x軸上覆制重複,在y軸上渲染邊緣顏色,那麼,wave得到的Shader就是下面這個樣子()

畫工咋樣…^_^

還要注意一點,就是建立BitmapShader時,第一個引數的得來

        int waveW = wave.getIntrinsicWidth();
        int waveH = wave.getIntrinsicHeight();

        Bitmap b = Bitmap.createBitmap(waveW, waveH, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);

        c.drawColor(getCurrentTextColor());

        wave.setBounds(0, 0, waveW, waveH);
        wave.draw(c);

        shader = new BitmapShader(b, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);

首先根據wave的Drawable物件的長寬,構建一個一樣大的Bitmap物件,然後去建立了個Canvas物件,然後畫了一個和當前文字顏色的背景,這時候,我們Shader上面的透明顏色,就變成了我們的文字顏色,然後給Drawable設定Bounds之後,draw了一下,這樣,我們的Bitmap物件,就是一個和wave的Drawable大小相同,下半部分是白色波浪,上半部分是文字顏色背景的一張圖片啦,這個時候在建立一個BitmapShader,就可以得到我們想要的了,然後

 getPaint().setShader(shader);

設定給paint,這樣用這支筆寫出來的字,就是以我們建立好的BitmapShader為背景的字了~

波浪效果呢?

開啟動畫之後,改變maskX和maskY,然後用矩陣移動BitmapShader的位置,字型的背景顏色就變化咯,而我們看到的效果就是波浪效果~

還記得maskY的變化範圍嗎?

h/2 到 -h/2

這樣就能夠從最下面沒有波浪,一直增長到白色波浪沾滿整個高度,然後就出現這個效果啦~

如果這篇文章你看明白了,那麼另外一個類似的專案,你應該就很容易知道是怎麼實現的了吧~

ok,這篇文章就寫到這裡,下期我們再見,回家看西遊記去了