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

前言

上一個(gè)周沒(méi)有寫博客凑兰,是我太懶雳窟,無(wú)法堅(jiān)持负间。在上一個(gè)周苛秕,除去工作的任務(wù)(迭代版本唯笙,修復(fù)BUG)之外螟蒸,我一直在模仿一個(gè)UI效果。我呢崩掘,算是一個(gè)米粉七嫌,我前面的博客,有一些效果就是來(lái)自MIUI苞慢。在MIUI中诵原,很多的列表都具有彈性和粘性,個(gè)人覺(jué)得這個(gè)效果不錯(cuò),于是就模仿了一下绍赛。

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

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

怎么樣,看上去效果還是挺可以的吧褪测,不得不說(shuō)猴誊,MIUI在一些小細(xì)節(jié)上面做得非常不錯(cuò),很多效果都值得我們深入的進(jìn)行學(xué)習(xí)侮措。

注意

  1. 本博客最主要的是為了向大家展示一種解決思路懈叹,文章中的類表效果用到實(shí)際項(xiàng)目中還是有些許問(wèn)題。
  2. 為了方便起見(jiàn)分扎,本文中使用的動(dòng)畫效果來(lái)自于JakeWharton大神的NineOldAndroids的支持庫(kù)澄成,個(gè)人非常不建議新手直接就來(lái)使用開源庫(kù),最起碼應(yīng)該熟悉一些基礎(chǔ)畏吓。

分析

剛開始的時(shí)候墨状,我一直在網(wǎng)上找類似的效果,一直是沒(méi)有找到菲饼。直到我學(xué)習(xí)完屬性動(dòng)畫之后才發(fā)現(xiàn)肾砂,其實(shí)這個(gè)效果實(shí)現(xiàn)起來(lái)是非常的簡(jiǎn)單。

  1. 整個(gè)效果看上去分為上拉和下來(lái)宏悦,上拉和下拉的時(shí)候進(jìn)行縮放镐确。

     1. 下拉:
         將View的中心點(diǎn)移到(width/2,0)中進(jìn)行Scale縮放
     2. 上拉
         將View的中心點(diǎn)移到(width/2,height)中進(jìn)行Scale縮放
    
  2. 松手之后會(huì)有一個(gè)回彈效果,使用ValueAnimator來(lái)進(jìn)行散發(fā)scale值饼煞,采用OvershootInterpolator差值器就能達(dá)到這樣的效果源葫。

編碼

1. 選擇繼承

自定義View又幾種方式:

  1. 繼承自View實(shí)現(xiàn)效果。
  2. 繼承原生控件進(jìn)行拓展砖瞧。
  3. 組合控件息堂。

很明顯,效果圖中都是包含了子控件的块促,可以選擇繼承ViewGroup來(lái)實(shí)現(xiàn)荣堰,但是我根本不關(guān)心子控件的一些測(cè)量和layout,所以需要繼承已經(jīng)實(shí)現(xiàn)的ViewGroup竭翠。最后我選定的是使用ScrollView持隧,原因是為了兼容滾動(dòng),并且需要監(jiān)聽(tīng)是否已經(jīng)滾動(dòng)到了底部逃片。

2.準(zhǔn)備工作

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

3. 自定義屬性

首先思考我們需要哪一些屬性,比方說(shuō)手指抬起后回彈的速度呀狼,回彈的效果方式(其實(shí)就是不同的差值器)裂允,能夠進(jìn)行果凍縮放的方式,只能是頂部哥艇、底部或者不限制绝编。

在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獨(dú)立出來(lái)的原因是我還有幾個(gè)控件需要使用相同的一些屬性。

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();
            //獲取是差值  整個(gè)屏幕的三倍大小
            bouncingOffset=ScreenUtils.getScreenHeight(getContext()) * 3;
        }
    }

onSizeChanged中驗(yàn)證模式

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

4. 開始編碼

因?yàn)槲覀兝^承的是ViewGroup,子View還需要攔截事件祖乳,所以我們需要重寫dispatchTouchEvent方法逗堵,并且在其中攔截事件分發(fā)和做縮放效果。

先實(shí)現(xiàn)在頂部進(jìn)行滑動(dòng)的時(shí)候隨著手指移動(dòng)而進(jìn)行縮放


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

在ACTION_DOWN記錄按下的坐標(biāo)眷昆,用于計(jì)算縮放值和進(jìn)行回彈蜒秤。因?yàn)锳CTION_DOWN事件必定會(huì)傳遞到子view的,所以不能直接返回true亚斋。

//移動(dòng)坐標(biāo)
dowY = (int) event.getRawY();
//按下坐標(biāo) 用于計(jì)算縮放值
dowY2 = (int) event.getRawY();

在ACTION_MOVE中進(jìn)行事件分發(fā)和縮放作媚。

  1. 實(shí)現(xiàn)頂部滑動(dòng)縮放,主要原理是判斷當(dāng)前是不是滾動(dòng)到了頂部帅刊,獲取手指移動(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)在坐標(biāo)與按下坐標(biāo)的差值
    int abs = moveY - dowY2;
    //計(jì)算縮放值
    offsetScale = (Math.abs(abs) / bouncingOffset);
    if (offsetScale > 0.3f) {
    offsetScale = 0.3f;
    }
    isTop = true;
    bouncingTo();
    return true;
    }
    }

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

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

效果是出來(lái)了,從頂部下拉的時(shí)候慢慢的縮放了赖瞒,但是如果在下拉一定距離后上拉會(huì)是怎么樣的呢女揭?應(yīng)該是慢慢的縮回去,然后再進(jìn)行滾動(dòng)冒黑。 需要在頂部if后面再加上判斷

 if (getScrollY() == 0 && dy < 0 && offsetScale > 0) {//為頂部 并且dy為下拉 并且縮放值大于0
    if (mBouncingType == BouncingType.TOP || mBouncingType == BouncingType.BOTH) {
        //獲取現(xiàn)在坐標(biāo)與按下坐標(biāo)的差值
        int abs = moveY - dowY2;
        //計(jì)算縮放值
        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. 手指抬起進(jìn)行回彈

前面兩步完成了整個(gè)拉取的過(guò)程,現(xiàn)在只要加上手機(jī)抬起的時(shí)候進(jìn)行回彈就可以了勤哗。整個(gè)回彈過(guò)程是有一個(gè)時(shí)間段抡爹,并且還有一個(gè)效果。采用ValueAnimator來(lái)散發(fā)offsetScale值來(lái)不斷的改變縮放值就能達(dá)到效果芒划。

ACTION_UP代碼

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

/**
 * 進(jìn)行回彈
 *
 * @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) {
            //獲取動(dòng)畫階段的值
            offsetScale = (float) animation.getAnimatedValue();
            if (isTop) {//回彈到頂部
                bouncingTo();
            } else {//回彈到底部
                bouncingBottom();
            }
        }
    });
    animator.start();
}

效果如下:

頂部下拉縮放抬起回彈

其實(shí)到這里冬竟,整個(gè)果凍視圖就已經(jīng)算是完成了,至于底部滑動(dòng)民逼,縮放都是一樣的泵殴,只是方向,值相反而已拼苍。判斷是否已經(jīng)滾動(dòng)到了底部笑诅,判斷方向等。以下附上dispatchTouchEvent的代碼,代碼量有些冗余吆你,只是為了每個(gè)部分的清晰而已弦叶。

@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //移動(dòng)坐標(biāo)
            dowY = (int) event.getRawY();
            //按下坐標(biāo) 用于計(jì)算縮放值
            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)在坐標(biāo)與按下坐標(biāo)的差值
                        int abs = moveY - dowY2;
                        //計(jì)算縮放值
                        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)在坐標(biāo)與按下坐標(biāo)的差值
                        int abs = moveY - dowY2;
                        //計(jì)算縮放值
                        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()) {//滾動(dòng)到底部
                    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);
}

來(lái)一個(gè)整體完成的效果圖:

頂部下拉縮放完整

其它View

  1. RecyclerView,ListVIew實(shí)現(xiàn)的原理都是一樣的妇多,判斷是否在頂部伤哺,滑動(dòng)方向等,再進(jìn)行縮放即可者祖。另外我實(shí)現(xiàn)了一個(gè)RecycerView的demo立莉,代碼和上面的基本上一致。

最后

源碼地址

最后七问,這暫時(shí)是一個(gè)最基本的蜓耻,等有時(shí)間我會(huì)繼續(xù)完成這個(gè)自定義View的。如果可能烂瘫,希望大家能夠給我一個(gè)star媒熊。

微信公眾號(hào)

AndroidRookie
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市坟比,隨后出現(xiàn)的幾起案子芦鳍,更是在濱河造成了極大的恐慌,老刑警劉巖葛账,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柠衅,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡籍琳,警方通過(guò)查閱死者的電腦和手機(jī)菲宴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)趋急,“玉大人喝峦,你說(shuō)我怎么就攤上這事∥卮铮” “怎么了谣蠢?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)查近。 經(jīng)常有香客問(wèn)我眉踱,道長(zhǎng),這世上最難降的妖魔是什么霜威? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任谈喳,我火速辦了婚禮,結(jié)果婚禮上戈泼,老公的妹妹穿的比我還像新娘婿禽。我一直安慰自己赏僧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布谈宛。 她就那樣靜靜地躺著次哈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吆录。 梳的紋絲不亂的頭發(fā)上窑滞,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音恢筝,去河邊找鬼哀卫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛撬槽,可吹牛的內(nèi)容都是我干的此改。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼侄柔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼共啃!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起暂题,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤移剪,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后薪者,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纵苛,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年言津,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了攻人。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡悬槽,死狀恐怖怀吻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情初婆,我是刑警寧澤蓬坡,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站烟逊,受9級(jí)特大地震影響渣窜,放射性物質(zhì)發(fā)生泄漏铺根。R本人自食惡果不足惜宪躯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望位迂。 院中可真熱鬧访雪,春花似錦详瑞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至精置,卻和暖如春计寇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背脂倦。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工番宁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赖阻。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓蝶押,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親火欧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子棋电,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容