【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,這篇文章就寫到這裡,下期我們再見,回家看西遊記去了