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

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

最終效果圖

NestedScrolling

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

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

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

先讓我們的自定義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滑動(dòng)到邊界監(jiān)聽(tīng)
     *
     * @param onScrollLimitListener ScrollView滑動(dòng)到邊界監(jiān)聽(tīng)
     */
    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) {//滑動(dòng)到頂部
            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滑動(dòng)到邊界監(jiān)聽(tīng)接口
     */
    public interface OnScrollLimitListener {
        /**
         * 滑動(dòng)到頂部
         */
        void onScrollTop();

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

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

然后是我們的PopupLayout,上一篇博文是通過(guò)自定義FrameLayout的方式實(shí)現(xiàn)的,這次由于是通過(guò)NestedScrolling實(shí)現(xiàn),所以一次滑動(dòng)事件其實(shí)是針對(duì)整個(gè)ViewGroup的,所以本次采取自定義LinearLayout的方式去實(shí)現(xiàn)。

在這里我們重點(diǎn)看下面幾個(gè)方法,首先是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());
    }

接下來(lái)看看重寫(xiě)的NestedScrollingParent幾個(gè)方法。

@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) {//向下拖拽,但是沒(méi)有超出拖拽限定距離
            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未滑動(dòng)到頂部,則消耗掉dy,即consumed[1]=dy。如果是下滑且內(nèi)部scrollview已經(jīng)滑動(dòng)到頂,則消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去執(zhí)行scrollBy,實(shí)際上就是滑動(dòng)PopupLayout本身。

onStopNestedScroll中,我們判斷向下滑動(dòng)的距離,來(lái)確定是dismiss PopupLayout還是回彈到初始位置。

最后由于需要更新TitleBar的狀態(tài),所以重寫(xiě)了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);//拖動(dòng)到頂部時(shí)darkview背景設(shè)置白色
            titleBar.setBackImageResource(R.mipmap.back);
        } else {
            darkView.setBackgroundResource(R.color.dark);//沒(méi)有拖動(dòng)到頂部時(shí)darkview背景設(shè)置暗色
            titleBar.setBackImageResource(R.mipmap.close);
        }

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

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

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

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

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

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