從未見過如此美麗動人的CardView

故事還得從看到那張動圖說起。

像往常一樣,休息時間我都會打開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增加背景切換速度,還有就是背景放大的比例也需要根據實際需求做調整。

下面給出代碼,

github

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容