Android自定義控件:NestedScrolling實(shí)現(xiàn)仿魅族flyme6應(yīng)用市場應(yīng)用詳情彈出式layout

在前一篇博文中已經(jīng)實(shí)現(xiàn)過一個仿魅族flyme6應(yīng)用市場應(yīng)用詳情彈出式layout: Android自定義控件:從零開始實(shí)現(xiàn)魅族flyme6應(yīng)用市場應(yīng)用詳情彈出式layout琼娘,主要是通過viewDragHelper來實(shí)現(xiàn)渠欺,大部分效果算是實(shí)現(xiàn)了肥惭,但是在最后還是有一些bug。
趁著這段時間工作比較輕松一點(diǎn)迈螟,這次再通過NestedScrolling來實(shí)現(xiàn)一次這個自定義控件弄息,對比前面的實(shí)現(xiàn)方法,通過NestedScrolling實(shí)現(xiàn)起來會簡單許多。
老規(guī)矩狸涌,先看看最終要實(shí)現(xiàn)的效果圖:

最終效果圖

NestedScrolling

NestedScrolling是個啥玩意呢切省?這是Google官方從5.0后引入的滑動嵌套解決方案。
看效果圖看的出來帕胆,這次我們要實(shí)現(xiàn)的效果的難點(diǎn)就在嵌套滑動朝捆,因?yàn)槭种阜诺絪crollview中,然后實(shí)際滾動的是卻外部的ViewGroup懒豹,在ViewGroup滾動到頂部的時候呢芙盘,內(nèi)部的Scrollview又繼續(xù)滾動。按照傳統(tǒng)的View事件攔截和處理方式脸秽,那首先要保證ViewGroup攔截事件儒老,否則事件會被內(nèi)部的scrollview消費(fèi)掉。但是如果攔截了记餐,當(dāng)ViewGroup滾動到頂部的時候又如何讓scrollview又持續(xù)滑動呢驮樊?按照傳統(tǒng)的方式,一次事件攔截就是一次性處理的事情片酝,ViewGroup如果攔截了這次滑動事件囚衔,那么scrollview肯定是沒法繼續(xù)處理這次滑動事件的。
我們上篇博文是通過事件攔截和分發(fā)人為的在ViewGroup中更動態(tài)的修改scrollView的滑動雕沿,從視覺上實(shí)現(xiàn)一次滑動事件ViewGroup和子view嵌套的滾動效果练湿。實(shí)際上從本質(zhì)上來講,還是ViewGroup攔截和消費(fèi)了事件审轮,第一次ViewGroup中的事件并沒有到子view中去處理肥哎。

那么NestedScrolling如何實(shí)現(xiàn)嵌套滑動呢?
NestedScrollingParent內(nèi)部實(shí)現(xiàn)了NestedScrollingChild接口的子View會優(yōu)先獲得事件處理權(quán)疾渣,然后滑動的時候篡诽,會先將dx、dy傳入給NestedScrollingParent榴捡,NestedScrollingParent可以決定是否對其進(jìn)行消耗霞捡,也就是說NestedScrollingParent可以消費(fèi)部分dx、dy薄疚,余下的未消費(fèi)完的dx碧信、dy交還給子view去消費(fèi)。

這樣看實(shí)際上要實(shí)現(xiàn)本次的效果就很簡單了街夭,話不多說砰碴,貼代碼。

先讓我們的自定義ScrollView實(shí)現(xiàn)NestedScrollingChild接口板丽,并且將NestedScrolling相關(guān)的處理全部交給ScrollingChildHelper處理呈枉。

public class MyScrollView extends ScrollView implements NestedScrollingChild{
    private boolean isScrollToTop = true;
    private boolean isScrollToBottom = false;
    private OnScrollLimitListener mOnScrollLimitListener;

    private NestedScrollingChildHelper mScrollingChildHelper;

    public MyScrollView(Context context) {
        this(context, null);
    }

    public MyScrollView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return getScrollingChildHelper().isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return getScrollingChildHelper().startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        getScrollingChildHelper().stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return getScrollingChildHelper().hasNestedScrollingParent();
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
    }

    private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mScrollingChildHelper == null) {
            mScrollingChildHelper = new NestedScrollingChildHelper(this);
            setNestedScrollingEnabled(true);
        }
        return mScrollingChildHelper;
    }

    /**
     * 設(shè)置ScrollView滑動到邊界監(jiān)聽
     *
     * @param onScrollLimitListener ScrollView滑動到邊界監(jiān)聽
     */
    public void setOnScrollLimitListener(OnScrollLimitListener onScrollLimitListener) {
        mOnScrollLimitListener = onScrollLimitListener;
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        if (getScrollY() == 0) {//滑動到頂部
            isScrollToTop = true;
            isScrollToBottom = false;
            isScrollToBottom = false;
        } else if (getScrollY() + getHeight() - getPaddingTop() - getPaddingBottom() ==
                getChildAt(0).getHeight()) {
            // 小心踩坑: 這里不能是 >=
            // 小心踩坑:這里最容易忽視的就是ScrollView上下的padding 
            isScrollToTop = false;
            isScrollToBottom = true;
        } else {
            isScrollToTop = false;
            isScrollToBottom = false;
        }
        notifyScrollChangedListeners();
    }

    /**
     * 回調(diào)
     */
    private void notifyScrollChangedListeners() {
        if (isScrollToTop) {
            if (mOnScrollLimitListener != null) {
                mOnScrollLimitListener.onScrollTop();
            }
        } else if (isScrollToBottom) {
            if (mOnScrollLimitListener != null) {
                mOnScrollLimitListener.onScrollBottom();
            }
        } else {
            if (mOnScrollLimitListener != null) {
                mOnScrollLimitListener.onScrollOther();
            }
        }
    }

    /**
     * scrollview滑動到邊界監(jiān)聽接口
     */
    public interface OnScrollLimitListener {
        /**
         * 滑動到頂部
         */
        void onScrollTop();

        /**
         * 滑動到頂部和底部之間的位置(既不是頂部也不是底部)
         */
        void onScrollOther();

        /**
         * 滑動到底部
         */
        void onScrollBottom();
    }
}

然后是我們的PopupLayout趁尼,上一篇博文是通過自定義FrameLayout的方式實(shí)現(xiàn)的,這次由于是通過NestedScrolling實(shí)現(xiàn)猖辫,所以一次滑動事件其實(shí)是針對整個ViewGroup的酥泞,所以本次采取自定義LinearLayout的方式去實(shí)現(xiàn)。

在這里我們重點(diǎn)看下面幾個方法啃憎,首先是onMeasure方法芝囤。因?yàn)槌跏紶顟B(tài)下ContentView是在界面之外的,所以要確定ContentView的高度辛萍。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.e("tag", "onMeasure");
        ViewGroup.LayoutParams params = contentView.getLayoutParams();
        params.height = darkView.getMeasuredHeight() - mOrginY;
        setMeasuredDimension(getMeasuredWidth(), contentView.getMeasuredHeight() + darkView
                .getMeasuredHeight());
    }

接下來看看重寫的NestedScrollingParent幾個方法悯姊。

@Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        Log.e(TAG, "onStartNestedScroll");
        return true;
    }

    @Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        Log.e(TAG, "onNestedScrollAccepted");
    }

    @Override
    public void onStopNestedScroll(View target) {
        Log.e(TAG, "onStopNestedScroll");
        if (mDarkViewHeight - mOrginY - getScrollY() > mDragRange) {//向下拖拽,超出拖拽限定距離
            dismiss();
        } else if (mDarkViewHeight - mOrginY - getScrollY() > 0) {//向下拖拽贩毕,但是沒有超出拖拽限定距離
            springback();
        }
    }

    @Override
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int
            dyUnconsumed) {
        Log.e(TAG, "onNestedScroll");
    }

    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        boolean patchDown = dy < 0 && mIsScrollInTop;//下滑
        boolean patchUp = dy > 0 && getScrollY() < (mDarkViewHeight - UIUtils.getStatusBarHeight
                (target));//上滑

        if (patchDown || patchUp) {
            scrollBy(0, dy);
            consumed[1] = dy;
        }
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return true;
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        //不做攔截 可以傳遞給子View
        return false;
    }

    @Override
    public int getNestedScrollAxes() {
        Log.e(TAG, "getNestedScrollAxes");
        return 0;
    }

onNestedPreScroll中悯许,我們判斷,如果是上滑且contentView未滑動到頂部辉阶,則消耗掉dy先壕,即consumed[1]=dy。如果是下滑且內(nèi)部scrollview已經(jīng)滑動到頂谆甜,則消耗掉dy垃僚,即consumed[1]=dy,消耗掉的意思店印,就是自己去執(zhí)行scrollBy,實(shí)際上就是滑動PopupLayout本身倒慧。

onStopNestedScroll中按摘,我們判斷向下滑動的距離,來確定是dismiss PopupLayout還是回彈到初始位置纫谅。

最后由于需要更新TitleBar的狀態(tài)炫贤,所以重寫了scrollTo方法,在scrollTo方法中更新TitleBar的狀態(tài)付秕。

    @Override
    public void scrollTo(int x, int y) {
        if (y >= mDarkViewHeight - UIUtils.getStatusBarHeight(this)) {
            y = mDarkViewHeight - UIUtils.getStatusBarHeight(this);
            darkView.setBackgroundColor(Color.WHITE);//拖動到頂部時darkview背景設(shè)置白色
            titleBar.setBackImageResource(R.mipmap.back);
        } else {
            darkView.setBackgroundResource(R.color.dark);//沒有拖動到頂部時darkview背景設(shè)置暗色
            titleBar.setBackImageResource(R.mipmap.close);
        }

        if (y != getScrollY()) {
            super.scrollTo(x, y);
        }
    }

本次的要點(diǎn)基本就這么多兰珍,總的來說相較上一篇博文各種絞盡腦汁想著事件處理,這次通過NestedScrolling就重寫幾個方法询吴,然后根據(jù)自己的實(shí)際需求做一些判斷掠河,實(shí)現(xiàn)起來還是很簡單的。

最后附上源碼鏈接:https://github.com/Horrarndoo/PopupLayoutNew

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猛计,一起剝皮案震驚了整個濱河市唠摹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌奉瘤,老刑警劉巖勾拉,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡藕赞,警方通過查閱死者的電腦和手機(jī)成肘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斧蜕,“玉大人双霍,你說我怎么就攤上這事〕图ぃ” “怎么了店煞?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長风钻。 經(jīng)常有香客問我顷蟀,道長,這世上最難降的妖魔是什么骡技? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任鸣个,我火速辦了婚禮,結(jié)果婚禮上布朦,老公的妹妹穿的比我還像新娘囤萤。我一直安慰自己,他們只是感情好是趴,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布涛舍。 她就那樣靜靜地躺著,像睡著了一般唆途。 火紅的嫁衣襯著肌膚如雪富雅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天肛搬,我揣著相機(jī)與錄音没佑,去河邊找鬼。 笑死温赔,一個胖子當(dāng)著我的面吹牛蛤奢,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播陶贼,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼啤贩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了拜秧?” 一聲冷哼從身側(cè)響起瓜晤,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎腹纳,沒想到半個月后痢掠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體驱犹,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年足画,在試婚紗的時候發(fā)現(xiàn)自己被綠了雄驹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡淹辞,死狀恐怖医舆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情象缀,我是刑警寧澤蔬将,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站央星,受9級特大地震影響霞怀,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜莉给,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一毙石、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧颓遏,春花似錦徐矩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至曼玩,卻和暖如春鳞骤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背演训。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工弟孟, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贝咙,地道東北人样悟。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像庭猩,于是被迫代替她去往敵國和親窟她。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評論 2 359

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