Android自定义控件:NestedScrolling实现仿魅族flyme6应用市场应用详情弹出式layout

在前一篇博文中已经实现过一个仿魅族flyme6应用市场应用详情弹出式layout: Android自定义控件:从零开始实现魅族flyme6应用市场应用详情弹出式layout,主要是通过viewDragHelper来实现,大部分效果算是实现了,但是在最后还是有一些bug。
趁着这段时间工作比较轻松一点,这次再通过NestedScrolling来实现一次这个自定义控件,对比前面的实现方法,通过NestedScrolling实现起来会简单许多。
老规矩,先看看最终要实现的效果图:

最终效果图

NestedScrolling

NestedScrolling是个啥玩意呢?这是Google官方从5.0后引入的滑动嵌套解决方案。
看效果图看的出来,这次我们要实现的效果的难点就在嵌套滑动,因为手指放到scrollview中,然后实际滚动的是却外部的ViewGroup,在ViewGroup滚动到顶部的时候呢,内部的Scrollview又继续滚动。按照传统的View事件拦截和处理方式,那首先要保证ViewGroup拦截事件,否则事件会被内部的scrollview消费掉。但是如果拦截了,当ViewGroup滚动到顶部的时候又如何让scrollview又持续滑动呢?按照传统的方式,一次事件拦截就是一次性处理的事情,ViewGroup如果拦截了这次滑动事件,那么scrollview肯定是没法继续处理这次滑动事件的。
我们上篇博文是通过事件拦截和分发人为的在ViewGroup中更动态的修改scrollView的滑动,从视觉上实现一次滑动事件ViewGroup和子view嵌套的滚动效果。实际上从本质上来讲,还是ViewGroup拦截和消费了事件,第一次ViewGroup中的事件并没有到子view中去处理。

那么NestedScrolling如何实现嵌套滑动呢?
NestedScrollingParent内部实现了NestedScrollingChild接口的子View会优先获得事件处理权,然后滑动的时候,会先将dx、dy传入给NestedScrollingParent,NestedScrollingParent可以决定是否对其进行消耗,也就是说NestedScrollingParent可以消费部分dx、dy,余下的未消费完的dx、dy交还给子view去消费。

这样看实际上要实现本次的效果就很简单了,话不多说,贴代码。

先让我们的自定义ScrollView实现NestedScrollingChild接口,并且将NestedScrolling相关的处理全部交给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;
    }

    /**
     * 设置ScrollView滑动到边界监听
     *
     * @param onScrollLimitListener ScrollView滑动到边界监听
     */
    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();
    }

    /**
     * 回调
     */
    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滑动到边界监听接口
     */
    public interface OnScrollLimitListener {
        /**
         * 滑动到顶部
         */
        void onScrollTop();

        /**
         * 滑动到顶部和底部之间的位置(既不是顶部也不是底部)
         */
        void onScrollOther();

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

然后是我们的PopupLayout,上一篇博文是通过自定义FrameLayout的方式实现的,这次由于是通过NestedScrolling实现,所以一次滑动事件其实是针对整个ViewGroup的,所以本次采取自定义LinearLayout的方式去实现。

在这里我们重点看下面几个方法,首先是onMeasure方法。因为初始状态下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。如果是下滑且内部scrollview已经滑动到顶,则消耗掉dy,即consumed[1]=dy,消耗掉的意思,就是自己去执行scrollBy,实际上就是滑动PopupLayout本身。

onStopNestedScroll中,我们判断向下滑动的距离,来确定是dismiss PopupLayout还是回弹到初始位置。

最后由于需要更新TitleBar的状态,所以重写了scrollTo方法,在scrollTo方法中更新TitleBar的状态。

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

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

本次的要点基本就这么多,总的来说相较上一篇博文各种绞尽脑汁想着事件处理,这次通过NestedScrolling就重写几个方法,然后根据自己的实际需求做一些判断,实现起来还是很简单的。

最后附上源码链接:https://github.com/Horrarndoo/PopupLayoutNew

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容