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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个自定义Preference布局的示例代码,包含一个Button按钮并实现了点击事件: 首先,在res/layout目录下创建一个名为custom_preference.xml的布局文件,代码如下: ```xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="?android:attr/listPreferredItemHeight" android:gravity="center_vertical" android:paddingEnd="?android:attr/scrollbarSize"> <!-- Preference icon --> <ImageView android:id="@+android:id/icon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="10dp" android:layout_gravity="center_vertical" android:contentDescription="@null" /> <!-- Preference title and summary --> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+android:id/title" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceLargeInverse" android:textColor="?android:attr/textColorPrimaryInverse" android:singleLine="true" android:ellipsize="end" android:textStyle="bold" android:paddingTop="5dp" /> <TextView android:id="@+android:id/summary" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?android:attr/textColorSecondaryInverse" android:maxLines="3" android:ellipsize="end" android:paddingBottom="5dp" /> </LinearLayout> <!-- Custom button --> <Button android:id="@+id/custom_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Custom Button" android:layout_marginEnd="10dp" android:onClick="onButtonClicked" /> </LinearLayout> ``` 其中,我们添加了一个Button按钮,设置了其ID为custom_button,并在其中添加了一个onClick属性,指向一个名为onButtonClicked的方法。 接着,在我们的Preference类中,重写onBindViewHolder方法,以及实现onButtonClicked方法,完整代码如下: ```java public class CustomPreference extends Preference { public CustomPreference(Context context, AttributeSet attrs) { super(context, attrs); setLayoutResource(R.layout.custom_preference); } @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); // Get the custom button view Button button = (Button) holder.findViewById(R.id.custom_button); // Set the click listener for the button button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Do something when the button is clicked Toast.makeText(getContext(), "Custom button clicked", Toast.LENGTH_SHORT).show(); } }); } // Custom button click handler public void onButtonClicked(View view) { // Do something when the button is clicked Toast.makeText(getContext(), "Custom button clicked", Toast.LENGTH_SHORT).show(); } } ``` 在该类中,我们重写了onBindViewHolder方法,通过ViewHolder获取了我们自定义布局中的Button,并设置了其点击事件。同时,我们还实现了一个名为onButtonClicked的方法,用于处理Button的点击事件。 最后,在我们的PreferenceActivity或PreferenceFragment中,添加我们自定义的Preference: ```xml <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceCategory android:title="Custom Preference Category"> <com.example.myapplication.CustomPreference android:key="custom_preference" android:title="Custom Preference" android:summary="This is a custom preference with a button" /> </PreferenceCategory> </PreferenceScreen> ``` 这样就完成了一个自定义Preference布局,并添加了一个Button按钮并实现了点击事件的示例。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值