Android初級進階之自定義果凍視圖(BouncingJellyView)(一)

前言

上一個周沒有寫博客,是我太懶,無法堅持。在上一個周,除去工作的任務(wù)(迭代版本,修復(fù)BUG)之外,我一直在模仿一個UI效果。我呢,算是一個米粉,我前面的博客,有一些效果就是來自MIUI。在MIUI中,很多的列表都具有彈性和粘性,個人覺得這個效果不錯,于是就模仿了一下。

本來開始之初是為了將這個效果封裝成為一個單獨的UI控件,結(jié)果寫著寫著就發(fā)現(xiàn)這樣是不合理的,于是就放在一旁等待解決方案,先看看實際的效果吧。(前面兩個是我項目中的實際效果)

訂單詳情效果
設(shè)置列表效果
demo->RecyclerView效果

怎么樣,看上去效果還是挺可以的吧,不得不說,MIUI在一些小細節(jié)上面做得非常不錯,很多效果都值得我們深入的進行學(xué)習(xí)。

注意

  1. 本博客最主要的是為了向大家展示一種解決思路,文章中的類表效果用到實際項目中還是有些許問題。
  2. 為了方便起見,本文中使用的動畫效果來自于JakeWharton大神的NineOldAndroids的支持庫,個人非常不建議新手直接就來使用開源庫,最起碼應(yīng)該熟悉一些基礎(chǔ)。

分析

剛開始的時候,我一直在網(wǎng)上找類似的效果,一直是沒有找到。直到我學(xué)習(xí)完屬性動畫之后才發(fā)現(xiàn),其實這個效果實現(xiàn)起來是非常的簡單。

  1. 整個效果看上去分為上拉和下來,上拉和下拉的時候進行縮放。

     1. 下拉:
         將View的中心點移到(width/2,0)中進行Scale縮放
     2. 上拉
         將View的中心點移到(width/2,height)中進行Scale縮放
    
  2. 松手之后會有一個回彈效果,使用ValueAnimator來進行散發(fā)scale值,采用OvershootInterpolator差值器就能達到這樣的效果。

編碼

1. 選擇繼承

自定義View又幾種方式:

  1. 繼承自View實現(xiàn)效果。
  2. 繼承原生控件進行拓展。
  3. 組合控件。

很明顯,效果圖中都是包含了子控件的,可以選擇繼承ViewGroup來實現(xiàn),但是我根本不關(guān)心子控件的一些測量和layout,所以需要繼承已經(jīng)實現(xiàn)的ViewGroup。最后我選定的是使用ScrollView,原因是為了兼容滾動,并且需要監(jiān)聽是否已經(jīng)滾動到了底部。

2.準備工作

  1. 創(chuàng)建項目
  2. 引用開源庫 compile 'com.nineoldandroids:library:2.4.0'
  3. 創(chuàng)建自定義控件類繼承ScrollView,實現(xiàn)三個構(gòu)造方法,并且在xml中引用

3. 自定義屬性

首先思考我們需要哪一些屬性,比方說手指抬起后回彈的速度,回彈的效果方式(其實就是不同的差值器),能夠進行果凍縮放的方式,只能是頂部、底部或者不限制。

在value文件夾中創(chuàng)建attr.xml

    
    <attr name="BouncingDuration" format="integer" />
    <attr name="BouncingInterpolator" format="enum">
        <enum name="OvershootInterpolator" value="1" />
        <enum name="BounceInterpolator" value="2" />
        <enum name="LinearInterpolator" value="3" />
        <enum name="AccelerateDecelerateInterpolator" value="4" />
    </attr>
    <attr name="BouncingType" format="enum">
        <enum name="none" value="0" />
        <enum name="top" value="1" />
        <enum name="bottom" value="2" />
        <enum name="both" value="3" />
    </attr>

    <declare-styleable name="BouncingJellyScrollView">
        <attr name="BouncingDuration" />
        <attr name="BouncingInterpolator" />
        <attr name="BouncingType" />
    </declare-styleable>

將attr獨立出來的原因是我還有幾個控件需要使用相同的一些屬性。

5. 初始化

在構(gòu)造方法中初始化一些常量值和屬性。

其它的一些工具類方法


    public class BouncingType {
        public static final int NONE = 0;
        public static final int TOP = 1;
        public static final int BOTTOM = 2;
        public static final int BOTH = 3;
    }

    public class BouncingInterpolatorType {

        public static final int OVERSHOOT_INTERPOLATOR = 1;
        public static final int BOUNCE_INTERPOLATOR = 2;
        public static final int LINEAR_INTERPOLATOR = 3;
        public static final int ACCELERATE_DECELERATE_INTERPOLATOR = 4;
    
        /**
         * 獲取彈跳類型
         *
         * @return
         */
        public static TimeInterpolator getTimeInterpolator(int type) {
            TimeInterpolator mTimeInterpolator = null;
            switch (type) {
                case OVERSHOOT_INTERPOLATOR:
                    mTimeInterpolator = new OvershootInterpolator();
                    break;
                case BOUNCE_INTERPOLATOR:
                    mTimeInterpolator = new BounceInterpolator();
                    break;
                case LINEAR_INTERPOLATOR:
                    mTimeInterpolator = new LinearInterpolator();
                    break;
                case ACCELERATE_DECELERATE_INTERPOLATOR:
                    mTimeInterpolator = new AccelerateDecelerateInterpolator();
                    break;
            }
            return mTimeInterpolator;
        }
    }

初始化屬性

    
    /**
     * @param attrs
     */
    private void initAttr(AttributeSet attrs) {
        if (attrs != null) {
            TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.BouncingJellyScrollView);
            //差值器
            mTimeInterpolator = BouncingInterpolatorType.getTimeInterpolator(typedArray.getInteger(
                    R.styleable.BouncingJellyScrollView_BouncingInterpolator
                    , BouncingInterpolatorType.OVERSHOOT_INTERPOLATOR));
            //回彈速度
            mBouncingDuration = typedArray.getInteger(R.styleable.BouncingJellyScrollView_BouncingDuration, mBouncingDuration);
            //果凍類型
            mBouncingType = typedArray.getInt(R.styleable.BouncingJellyScrollView_BouncingType, BouncingType.BOTH);
            typedArray.recycle();
            //獲取是差值  整個屏幕的三倍大小
            bouncingOffset=ScreenUtils.getScreenHeight(getContext()) * 3;
        }
    }

onSizeChanged中驗證模式

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    //判斷可滾動的內(nèi)容是不是小于整個屏幕的高度,以防底部進行所動
    int contentHeight = getChildAt(0).getHeight();
    if (contentHeight > 0 && contentHeight <= ScreenUtils.getScreenHeight(getContext())) {
        mBouncingType = BouncingType.TOP;
    }
}

4. 開始編碼

因為我們繼承的是ViewGroup,子View還需要攔截事件,所以我們需要重寫dispatchTouchEvent方法,并且在其中攔截事件分發(fā)和做縮放效果。

先實現(xiàn)在頂部進行滑動的時候隨著手指移動而進行縮放


    /**
     * 從頂部開始滑動
     */
    public void bouncingTo() {
        //設(shè)置X坐標點
        ViewHelper.setPivotX(this, getWidth() / 2);
        //設(shè)置Y坐標點
        ViewHelper.setPivotY(this, 0);
        //進行縮放
        ViewHelper.setScaleY(this, 1.0f + offsetScale);      
    }
    
     /**
     * 從頂部開始滑動
     */
    public void bouncingBottom() {
        //設(shè)置X坐標點
        ViewHelper.setPivotX(this, getWidth() / 2);
        //設(shè)置Y坐標點
        ViewHelper.setPivotY(this, getHeight());
        ViewHelper.setScaleY(this, 1.0f + offsetScale);
    }

在ACTION_DOWN記錄按下的坐標,用于計算縮放值和進行回彈。因為ACTION_DOWN事件必定會傳遞到子view的,所以不能直接返回true。

//移動坐標
dowY = (int) event.getRawY();
//按下坐標 用于計算縮放值
dowY2 = (int) event.getRawY();

在ACTION_MOVE中進行事件分發(fā)和縮放。

  1. 實現(xiàn)頂部滑動縮放,主要原理是判斷當(dāng)前是不是滾動到了頂部,獲取手指移動的方向和距離。

    moveX = (int) event.getRawX();
    moveY = (int) event.getRawY();
    //dy值 判斷方向
    int dy = moveY - dowY;
    dowY = moveY;
    //頂部
    if (dy > 0 && getScrollY() == 0) {
    //判斷果凍的類型
    if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
    //獲取現(xiàn)在坐標與按下坐標的差值
    int abs = moveY - dowY2;
    //計算縮放值
    offsetScale = (Math.abs(abs) / bouncingOffset);
    if (offsetScale > 0.3f) {
    offsetScale = 0.3f;
    }
    isTop = true;
    bouncingTo();
    return true;
    }
    }

實現(xiàn)第一步效果如下:

頂部下拉縮放1
  1. 回拉恢復(fù)

效果是出來了,從頂部下拉的時候慢慢的縮放了,但是如果在下拉一定距離后上拉會是怎么樣的呢?應(yīng)該是慢慢的縮回去,然后再進行滾動。 需要在頂部if后面再加上判斷

 if (getScrollY() == 0 && dy < 0 && offsetScale > 0) {//為頂部 并且dy為下拉 并且縮放值大于0
    if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
        //獲取現(xiàn)在坐標與按下坐標的差值
        int abs = moveY - dowY2;
        //計算縮放值
        offsetScale = (Math.abs(abs) / bouncingOffset);
        if (offsetScale > 0.3f) {
            offsetScale = 0.3f;
        }
        if (abs <= 0) {
            offsetScale = 0;
            dowY2 = moveY;
        }
        isTop = true;
        bouncingTo();
        return true;
    }
}

效果如下:

頂部下拉縮放回拉恢復(fù)
  1. 手指抬起進行回彈

前面兩步完成了整個拉取的過程,現(xiàn)在只要加上手機抬起的時候進行回彈就可以了。整個回彈過程是有一個時間段,并且還有一個效果。采用ValueAnimator來散發(fā)offsetScale值來不斷的改變縮放值就能達到效果。

ACTION_UP代碼

 if (mBouncingType != BouncingType.NONE) {
        if (offsetScale > 0) {
            backBouncing(offsetScale, 0);
            return true;
        }
    }

/**
 * 進行回彈
 *
 * @param from
 * @param to
 */
private void backBouncing(final float from, final float to) {
    //初始化
    if (animator != null && animator.isRunning()) {
        animator.cancel();
        animator = null;
        offsetScale = 0;
        bouncingTo();
    }
    if (mTimeInterpolator == null) {
        mTimeInterpolator = new OvershootInterpolator();
    }
    //散發(fā)值
    animator = ValueAnimator.ofFloat(from, to).setDuration(mBouncingDuration);
    animator.setInterpolator(mTimeInterpolator);//差值器
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //獲取動畫階段的值
            offsetScale = (float) animation.getAnimatedValue();
            if (isTop) {//回彈到頂部
                bouncingTo();
            } else {//回彈到底部
                bouncingBottom();
            }
        }
    });
    animator.start();
}

效果如下:

頂部下拉縮放抬起回彈

其實到這里,整個果凍視圖就已經(jīng)算是完成了,至于底部滑動,縮放都是一樣的,只是方向,值相反而已。判斷是否已經(jīng)滾動到了底部,判斷方向等。以下附上dispatchTouchEvent的代碼,代碼量有些冗余,只是為了每個部分的清晰而已。

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //移動坐標
            dowY = (int) event.getRawY();
            //按下坐標 用于計算縮放值
            dowY2 = (int) event.getRawY();
            break;
        case MotionEvent.ACTION_MOVE:
            if (mBouncingType != BouncingType.NONE) {
                moveX = (int) event.getRawX();
                moveY = (int) event.getRawY();
                //dy值 判斷方向
                int dy = moveY - dowY;
                dowY = moveY;
                //頂部
                if (dy > 0 && getScrollY() == 0) {
                    //判斷果凍的類型
                    if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
                        //獲取現(xiàn)在坐標與按下坐標的差值
                        int abs = moveY - dowY2;
                        //計算縮放值
                        offsetScale = (Math.abs(abs) / bouncingOffset);
                        if (offsetScale > 0.3f) {
                            offsetScale = 0.3f;
                        }
                        isTop = true;
                        bouncingTo();
                        return true;
                    }
                } else if (getScrollY() == 0 && dy < 0 && offsetScale > 0) {//為頂部 并且dy為下拉 并且縮放值大于0
                    if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
                        //獲取現(xiàn)在坐標與按下坐標的差值
                        int abs = moveY - dowY2;
                        //計算縮放值
                        offsetScale = (Math.abs(abs) / bouncingOffset);
                        if (offsetScale > 0.3f) {
                            offsetScale = 0.3f;
                        }
                        if (abs <= 0) {
                            offsetScale = 0;
                            dowY2 = moveY;
                        }
                        isTop = true;
                        bouncingTo();
                        return true;
                    }
                }

                //底部
                if (dy < 0 && getScrollY() + getHeight() >= computeVerticalScrollRange()) {//滾動到底部
                    if (mBouncingType == BouncingType.BOTTOM || mBouncingType == BouncingType.BOTH) {
                        int abs = moveY - dowY2;
                        offsetScale = (Math.abs(abs) / bouncingOffset);
                        if (offsetScale > 0.3f) {
                            offsetScale = 0.3f;
                        }
                        isTop = false;
                        bouncingBottom();
                    }
                } else if (dy > 0 && getScrollY() + getHeight() >= computeVerticalScrollRange() && offsetScale > 0) {
                    if (mBouncingType == BouncingType.BOTTOM || mBouncingType == BouncingType.BOTH) {
                        int abs = moveY - dowY2;
                        offsetScale = (Math.abs(abs) / bouncingOffset);
                        if (offsetScale > 0.3f) {
                            offsetScale = 0.3f;
                        }
                        if (abs >= 0) {
                            offsetScale = 0;
                            dowY2 = moveY;
                        }
                        isTop = false;
                        bouncingBottom();
                        return true;
                    }
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            if (mBouncingType != BouncingType.NONE) {
                if (offsetScale > 0) {
                    backBouncing(offsetScale, 0);
                    return true;
                }
            }
            break;
    }
    return super.dispatchTouchEvent(event);
}

來一個整體完成的效果圖:

頂部下拉縮放完整

其它View

  1. RecyclerView,ListVIew實現(xiàn)的原理都是一樣的,判斷是否在頂部,滑動方向等,再進行縮放即可。另外我實現(xiàn)了一個RecycerView的demo,代碼和上面的基本上一致。

最后

源碼地址

最后,這暫時是一個最基本的,等有時間我會繼續(xù)完成這個自定義View的。如果可能,希望大家能夠給我一個star。

微信公眾號

AndroidRookie
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容