故事還得從看到那張動圖說起。
像往常一樣,休息時間我都會打開uplabs瀏覽一下國外大佬們的UI設計。
有個設計十分吸引眼球,就是下圖。
仔細看每張圖片,在加載出來的時候背景都會有一個偏移的動效,簡約而不簡單。
這個能實現嗎?如果公司UI團隊給了這么一個效果圖,你該咋辦?
思路
首先說說思路,既然要做,顯得有個載體吧,可能很多同學一下子就想到了ImageView
這個東西。但現在是設么年代了?Material Design的呀,所以再用ImageView是不是有點low了。所以自然想到就應該是CardView
嘛。
但CardView有個蛋疼的設定,不能設置背景圖片,不知道小伙伴們發現了沒有?
stackoverflow上的答案過于簡單粗暴,不是我的菜。
既然不用這種方法,那我們只能使用我們的神器onDraw
了,從根本上解決問題。
代碼
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if ((tobePaint!=null&&!tobePaint.isRecycled())) {
canvas.drawBitmap(tobePaint, backgroundSubRec, backgroundRec, paint);
}
}
onDraw方法很簡單,就是當有可繪制背景的時候就去繪制。
這里有四個變量要關注一下:
- tobePaint: 需要被繪制的Bitmap對象。
- backgroundSubRec:Rect對象,表示Bitmap中需要被繪制的區域。后續就是通過改變這個變量來達到動畫效果。
- backgroundRec:Rect對象,表示繪制區域的大小,大小同CardView的大小。
最開始我們自定義的這個視圖與普通的CardView沒有差異,當調用完public void enableActivation(Bitmap activationBg, String key)
這個方法后,背景就被繪制上去了,如下圖。
來看看代碼
public void enableActivation(Bitmap activationBg, String key) {
currentKey = key;
isActivation = false;
init(activationBg,key);
}
具體看init這個方法:
private void init(final Bitmap originBitmap,final String key) {
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
Executors.newSingleThreadExecutor().execute(new Runnable() {
@Override
public void run() {
w = getWidth();
h = getHeight();
int scaledW = (int) (w * bgScale);
int scaledH = (int) (h * bgScale);
double preSH = 1.0 * originBitmap.getHeight() / scaledH;
double preSW = 1.0 * originBitmap.getWidth() / scaledW;
float smallPreS = (float) Math.min(preSH, preSW);
Matrix matrix = new Matrix();
float s = 1 / smallPreS;
matrix.postScale(s, s);
if(sIsEnableCache){
background = sCache.get(key);
if(background == null){
background = Bitmap.createBitmap(originBitmap, 0, 0, originBitmap.getWidth(), originBitmap.getHeight(), matrix, true);
sCache.put(key,background);
}
}else {
background = Bitmap.createBitmap(originBitmap, 0, 0, originBitmap.getWidth(), originBitmap.getHeight(), matrix, true);
}
defaultLeft = (background.getWidth() - w) / 2;
defaultTop = (background.getHeight() - h) / 2;
backgroundRec = new Rect(0, 0, w, h);
backgroundSubRec = new Rect(defaultLeft, defaultTop, w + defaultLeft, h + defaultTop);
currentPosition = POSITION_CENTER;
tobePaint = background;
Log.d("scott"," key = " + key + " current key = " + currentKey);
if(key.equals(currentKey)){
handler.post(new Runnable() {
@Override
public void run() {
invalidate();
isActivation = true;
}
});
}
}
});
return true;
}
});
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
invalidate();
}
這個方法做了這么幾件事:
- 確定CardView的長寬。
- 根據CardView的實際大小對傳入的Bitmap進行適當放大,為后續動畫做準備。
- 確定backgroundRec,backgroundSubRec這兩個對象的值。
- 給tobePaint對象賦值。
- 調用invalidate()繪制背景。
為了方便理解,我畫了如下這張圖。
下面是動畫部分,這部分。
先來說說原理,上面繪制的圖像是靠backgroundSubRec對tobePaint進行截取而來的,一開始backgroundSubRec截取的是放大后tobePaint的中間部分,其大小和CardView一致,接著通過不斷的改變backgroundSubRec的值,讓其慢慢向右移動。來截取tobePaint的右邊部分。
下面是代碼:
public void postRight() {
if (!isActivation) {
handler.postDelayed(new Runnable() {
@Override
public void run() {
postRight();
}
}, 1000 / 60);
return;
}
if (currentPosition == POSITION_INVAL || currentPosition == POSITION_RIGHT) {
Log.d("scott", "current position is already right");
return;
}
currentPosition = POSITION_INVAL;
final int delta = defaultLeft * 2 - backgroundSubRec.left;
int tempStep = delta / animationDuration;
if (tempStep == 0) tempStep = 1;
final int step = tempStep;
handler.postDelayed(new Runnable() {
@Override
public void run() {
if(!isActivation) return;
invalidate();
if (backgroundSubRec.left < defaultLeft * 2) {
backgroundSubRec.left += step;
backgroundSubRec.right += step;
handler.postDelayed(this, fps);
} else {
currentPosition = POSITION_RIGHT;
}
}
}, fps);
}
最后是效果圖。
最后
雖然上面講的比較簡單,其實在這過程中有一些細節還是需要注意的,比如bitmap的格式最好使用RGB_565來減少內存占用,使用LruCahce來緩存Bitmap增加背景切換速度,還有就是背景放大的比例也需要根據實際需求做調整。
下面給出代碼,