2017年4月24日 星期一

Android: 會呼吸跳動的聚焦效果

前言

上一篇藉由step的方式,來達到圖形自動增減形成呼吸的跳動效果,這次稍微把程式碼調整加上動畫效果,練習做成一個會呼吸跳動的聚焦效果,效果如下。


以下我歸納為幾個實作步驟:
  1. 初始化
  2. 紀錄HighlightView的相關資料
  3. 動態添加一層可點擊的FrameLayout
  4. 製作HighlightView
  5. 加入進場和退場動畫特效

初始化

初始化分為兩部分,一個是初始欲聚焦視圖的資源,我們可以從Activity傳入所需的配置,像是欲聚焦視圖、聚焦的形狀和疊加上去的背景顏色,程式碼如下。

private void initMemberVariables(Activity activity, View focusedOnView, int highlightShape, int backgroundColor) {
        this.activity = activity;
        this.focusedOnView = focusedOnView;
        this.highlightShape = (highlightShape == 0 ? SHAPE_CIRCLE : SHAPE_RECT);
        this.backgroundColor = backgroundColor != 0 ? this.activity.getResources().getColor(backgroundColor) : this.activity.getResources().getColor(R.color.default_color_CC000000);
}

第二則是儲存手機的寬高,並且取得其中心點的XY值,程式碼如下。

private void initScreenResources() {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        this.activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);

        int deviceWidth = displayMetrics.widthPixels;
        int deviceHeight = displayMetrics.heightPixels;

        this.centerX = deviceWidth / 2;
        this.centerY = deviceHeight / 2;
}

紀錄HighlightView的相關資料

初始化完成後,我們可以記錄一些關於聚焦視圖的資料,像是聚焦視圖的XY值、寬度、高度、繪製圖形的半徑,程式碼如下。
  • 聚焦視圖的XY值可以用getLocationInWindow()方法取得,如此一來,我們就能快速得知聚焦圖形需要繪製在什麼位置,程式碼如下。
int[] viewPoints = new int[2];
focusedOnView.getLocationInWindow(viewPoints);

  • 聚焦視圖的寬高,可以直接利用getWidth()與getHeight()兩個方法取得,程式碼如下。

  • this.focusedOnViewWidth = focusedOnView.getWidth();
    this.focusedOnViewHeight = focusedOnView.getHeight();
    


  • 繪製圖形的半徑要比原本聚焦的視圖來得大一些,所以可以使用聚焦視圖對角線的一半作為半徑,當然你也可以用聚焦視圖的一半,再往外加一點也行。。

  • this.focusedOnViewRadius = (int) (Math.hypot(this.focusedOnViewWidth, this.focusedOnViewHeight) / 2);
    

    動態添加一層可點擊的FrameLayout

    因為我們需要在原本的Acitivity上疊加一層,所以這邊我們動態加入一層FrameLayout,程式碼如下。

    this.highlightContainer = new FrameLayout(this.activity);
    this.highlightContainer.setTag(HIGHLIGHT_CONTAINER_TAG);
    this.highlightContainer.setClickable(true);
    this.highlightContainer.setOnClickListener(this);
    this.highlightContainer.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    this.mainViewGroup.addView(this.highlightContainer);
    

    製作HighlightView

    我們要客製一個聚焦的視圖,除了基本的繪圓和繪方外,還要用到上篇文章提到的方式,才能讓聚焦視圖產生呼吸跳動的效果,程式碼如下。

    @Override
    protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
    
            if (this.bitmap == null) {
                this.bitmap = Bitmap.createBitmap(this.getWidth(), this.getHeight(), Bitmap.Config.ARGB_8888);
                this.bitmap.eraseColor(this.backgroundColor);
            }
    
            canvas.drawBitmap(this.bitmap, 0, 0, this.backgroundPaint);
    
            if (this.renderHelper.isFocus()) {
                this.animCounter = this.animCounter + this.step;
    
                if (this.renderHelper.getHighlightShape() == ViewManager.SHAPE_CIRCLE) {
                    this.drawCircle(canvas);
                } else {
                    this.drawRoundedRect(canvas);
                }
    
                if (this.animCounter == ANIM_COUNTER_MAX) {
                    this.step = -1;
                } else if (this.animCounter == 0) {
                    this.step = 1;
                }
            }
    
            this.postInvalidate();
    }
    

    加入進場和退場動畫特效

    我們要讓Framlayout產生正確的動畫,所以需要取得正確的寬高,所以須透過getViewTreeObserver()方法,取得正確的數值,才能順利完成動畫效果;而退場效果因為已經繪製完成,所以無需透過該方法取得,可以直接利用getWidth()和getHeight()兩個方法,程式碼如下。
    private void enterAnimation() {
            final int revealRadius = (int) Math.hypot(highlightContainer.getWidth(), highlightContainer.getHeight());
            int startRadius = 0;
    
            if (focusedOnView != null) {
                startRadius = focusedOnView.getWidth() / 2;
            }
    
            Animator enterAnimator = ViewAnimationUtils.createCircularReveal(highlightContainer, centerX, centerY, startRadius, revealRadius);
            enterAnimator.setDuration(1000);
            enterAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
            enterAnimator.start();
    
            this.highlightContainer.getViewTreeObserver().addOnPreDrawListener(
                    new ViewTreeObserver.OnPreDrawListener() {
                        @Override
                        public boolean onPreDraw() {
                            highlightContainer.getViewTreeObserver().removeOnPreDrawListener(this);
    
                            final int revealRadius = (int) Math.hypot(highlightContainer.getWidth(), highlightContainer.getHeight());
                            int startRadius = 0;
    
                            if (focusedOnView != null) {
                                startRadius = focusedOnView.getWidth() / 2;
                            }
    
                            Animator enterAnimator = ViewAnimationUtils.createCircularReveal(highlightContainer, centerX, centerY, startRadius, revealRadius);
                            enterAnimator.setDuration(1000);
                            enterAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
                            enterAnimator.start();
    
                            return false;
                        }
                    });
    }
    

    private void exitAnimation() {
            final int revealRadius = (int) Math.hypot(highlightContainer.getWidth(), highlightContainer.getHeight());
    
            Animator exitAnimator = ViewAnimationUtils.createCircularReveal(highlightContainer, centerX, centerY, revealRadius, 0f);
            exitAnimator.setDuration(300);
            exitAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
            exitAnimator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    mainViewGroup.removeView(highlightContainer);
                }
            });
            exitAnimator.start();
    }
    

    Source Code