美文网首页android杂谈
android自定义滚动选择器(三)

android自定义滚动选择器(三)

作者: 寒潇2018 | 来源:发表于2019-01-24 12:51 被阅读213次

    本篇文章将会阐述ScrollPickerAdapter及默认的item视图DefaultItemViewProvider的具体实现,ScrollPickerAdapter的设计在文章android自定义滚动选择器(一)
    已经详细阐述过,这里照例直接从代码的角度进行阐述。

    如果来不及阅读文章,或者想直接获取源码,见git:android自定义滚动选择器

    ScrollPickerAdapter解析

    根据前面分析,ScrollPickerAdapter首先要继承RecyclerView.Adapter并实现IPickerViewOperation接口,这里我们就从这两个方面进行分析。

    继承RecyclerView.Adapter必须要复写其中的抽象方法,这个是无法避免的,只不过我们要明确在每个方法中应该做哪些事情,如下所示:

        @NonNull
        @Override
        public ScrollPickerAdapterHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            if (mViewProvider == null) {
                mViewProvider = new DefaultItemViewProvider();
            }
            return new ScrollPickerAdapterHolder(LayoutInflater.from(mContext).inflate(mViewProvider.resLayout(), parent, false));
        }
    
        @Override
        public void onBindViewHolder(@NonNull ScrollPickerAdapterHolder holder, int position) {
            mViewProvider.onBindView(holder.itemView, mDataList.get(position));
        }
    
        @Override
        public int getItemCount() {
            return mDataList.size();
        }
    

    我们分开来阐述下每个方法完成的功能:

    1. onCreateViewHolder方法,这个方法的目的显然需要返回一个viewHolder,这里我们直接返回了ScrollPickerAdapterHolder,ScrollPickerAdapterHolder的具体定义如下:
        static class ScrollPickerAdapterHolder extends RecyclerView.ViewHolder {
            private View itemView;
    
            private ScrollPickerAdapterHolder(@NonNull View view) {
                super(view);
                itemView = view;
            }
        }
    

    ScrollPickerAdapterHolder接收一个View视图,这个就是我们的item视图,所以我们需要在构造ScrollPickerAdapterHolder的时候传入item视图,那么这个item视图该如何提供?

    按照常规方法,可以直接在onCreateViewHolder方法中,通过LayoutInflater inflate具体的视图,但是这么做显然无法满足我们的需求,即无法满足用户可以自定义的需求,那么如果要满足用户自定义的需求该怎么办?

    答案是我们将视图的构造入口暴露给用户即可,因此,这里我们提供一个视图提供接口,如下所示:

    public interface IViewProvider<T> {
    //提供layout布局id
        @LayoutRes
        int resLayout();
    //对应于adapter中的onBindView方法
        void onBindView(@NonNull View view, @Nullable T itemData);
    //选择滚动器滚动的时候,通知外界视图更新的接口
        void updateView(@NonNull View itemView, boolean isSelected);
    }
    

    通过提供IViewProvider接口,我们就能够满足用户自定义的需求。但是,我们同样需要提供一个默认item视图实现,当用户不需要自定义的时候,可以使用默认的item视图,所以在onCreateViewHolder中,我们做了一下判断:

    //当用户没有提供view provider的时候,使用默认item视图提供者
            if (mViewProvider == null) {
                mViewProvider = new DefaultItemViewProvider();
            }
    

    对于DefaultItemViewProvider的实现,会在下面进行分析。

    1. onBindViewHolder方法,其实现代码如下所示:
        @Override
        public void onBindViewHolder(@NonNull ScrollPickerAdapterHolder holder, int position) {
            mViewProvider.onBindView(holder.itemView, mDataList.get(position));
        }
    

    onBindViewHolder本身的功能就是完成holder视图和数据的绑定,这里因为我们允许用户自定义item视图,所以就直接委托给view provider进行实现。

    至此,关于adapter自身的一些方法就阐述完了,下面来看一下IPickerViewOperation相应的方法,在ScrollPickerAdapter中的实现,如下所示:

        @Override
        public int getSelectedItemOffset() {
            return mSelectedItemOffset;
        }
    
        @Override
        public int getVisibleItemNumber() {
            return mVisibleItemNum;
        }
    
        @Override
        public int getLineColor() {
            return mLineColor;
        }
        @Override
        public void updateView(View itemView, boolean isSelected) {
            mViewProvider.updateView(itemView, isSelected);
            adaptiveItemViewSize(itemView);
            itemView.setOnClickListener(isSelected ? new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mOnItemClickListener != null) {
                        mOnItemClickListener.onSelectedItemClicked(v);
                    }
                }
            } : null);
            if (isSelected && mOnScrollListener != null) {
                mOnScrollListener.onScrolled(itemView);
            }
        }
    

    除了updateView,其他的方法都是数据提供功能,这些都是由外界塞进来的,所以这里主要关注下updateView。

    再次阐述下updateView方法的作用,updateView就是在ScrollPickerView滚动的时候,及时通知外界。updateView包括两个入参:一个是当前的item视图,一个是标识当前item视图是否被选中。外界拿到这两个参数后可以根据自己的需求来定制化,比如将选中的item视图文字变大、变色等。

    在ScrollPickerAdapter中的updateView中,我们主要完成以下几个工作:

    1. 调用用户提供的view provider的updateView,即通知对方视图要更新。
    2. 对于滚动选择器中的item视图,提供了一个默认的点击事件,这个事件只有在item视图被选中的时候才有。其实用户完全可以在自己的view provider中,通过updateView来完成业务逻辑处理,这里只是通过adapter对外暴露一个监听响应入口,能满足基本需要。
    3. 与此同时,我们还提供了一个滚动监听,滚动监听的方法入口是onScrolled,该方法有一个参数currentItemView,表示当前被选中的item视图。
    4. 在updateView方法中,调用了 adaptiveItemViewSize(itemView)方法,这个方法的目的是保证item视图的最小高度和宽度,如下所示:
        private void adaptiveItemViewSize(View itemView) {
            int h = itemView.getHeight();
            if (h > maxItemH) {
                maxItemH = h;
            }
    
            int w = itemView.getWidth();
            if (w > maxItemW) {
                maxItemW = w;
            }
    
            itemView.setMinimumHeight(maxItemH);
            itemView.setMinimumWidth(maxItemW);
        }
    

    好了,adapter相关的基本阐述完了,那么还有一个问题,如何保证外部定制数据能直接有效的在视图构建前生效?这个问题在第一篇文章中分析过,那就是采用builder设计模式,如下所示:

        public static class ScrollPickerAdapterBuilder<T> {
            private ScrollPickerAdapter mAdapter;
    
            public ScrollPickerAdapterBuilder(Context context) {
                mAdapter = new ScrollPickerAdapter<T>(context);
            }
    
            public ScrollPickerAdapterBuilder<T> selectedItemOffset(int offset) {
                mAdapter.mSelectedItemOffset = offset;
                return this;
            }
    
            public ScrollPickerAdapterBuilder<T> setDataList(List<T> list) {
                mAdapter.mDataList.clear();
                mAdapter.mDataList.addAll(list);
                return this;
            }
    
            public ScrollPickerAdapterBuilder<T> setOnClickListener(OnClickListener listener) {
                mAdapter.mOnItemClickListener = listener;
                return this;
            }
    
            public ScrollPickerAdapterBuilder<T> visibleItemNumber(int num) {
                mAdapter.mVisibleItemNum = num;
                return this;
            }
    
            public ScrollPickerAdapterBuilder<T> setItemViewProvider(IViewProvider viewProvider) {
                mAdapter.mViewProvider = viewProvider;
                return this;
            }
    
            public ScrollPickerAdapterBuilder<T> setDivideLineColor(String colorString) {
                mAdapter.mLineColor = Color.parseColor(colorString);
                return this;
            }
    
            public ScrollPickerAdapterBuilder<T> setOnScrolledListener(OnScrollListener listener) {
                mAdapter.mOnScrollListener = listener;
                return this;
            }
    
            public ScrollPickerAdapter build() {
                adaptiveData(mAdapter.mDataList);
                mAdapter.notifyDataSetChanged();
                return mAdapter;
            }
    
            private void adaptiveData(List list) {
                int visibleItemNum = mAdapter.mVisibleItemNum;
                int selectedItemOffset = mAdapter.mSelectedItemOffset;
                for (int i = 0; i < mAdapter.mSelectedItemOffset; i++) {
                    list.add(0, null);
                }
    
                for (int i = 0; i < visibleItemNum - selectedItemOffset - 1; i++) {
                    list.add(null);
                }
            }
        }
    

    上面,我们通过ScrollPickerAdapterBuilder暴露给外界定制入口,主要关注一个方法,就是build方法。在build方法中主要调用了adaptiveData方法,这个方法功能很重要,下面分析下它的实现。

    首先来看下adaptiveData方法。该方法的功能是用于数据填充,比如两条分割线偏移量为n个item视图,那么我们就需要在其前面补充n个item视图,这样才能保证能有机会选中所有的item视图,如下所示:

            private void adaptiveData(List list) {
                int visibleItemNum = mAdapter.mVisibleItemNum;
                int selectedItemOffset = mAdapter.mSelectedItemOffset;
                for (int i = 0; i < mAdapter.mSelectedItemOffset; i++) {
                    list.add(0, null);//在滚动器前面增加数据,item数据值为空
                }
    
                for (int i = 0; i < visibleItemNum - selectedItemOffset - 1; i++) {
                    list.add(null);//在滚动器后面增加数据,item数据值为空
                }
            }
    

    上面代码需要注意的是,因为我们补充数据的时候,补充的是null,所以在接收数据的时候一定要进行非空判断,在阐述默认item视图的时候会有所阐述。

    item的默认视图提供者 DefaultItemViewProvider

    这个就是本案例提供的默认的item视图提供者,阐述DefaultItemViewProvider的目的更多的是为自定义view provider提供思路。其完整代码如下所示:

    public class DefaultItemViewProvider implements IViewProvider<String> {
        @Override
        public int resLayout() {
            return R.layout.scroll_picker_default_item_layout;
        }
    
        @Override
        public void onBindView(@NonNull View view, @Nullable String text) {
            TextView tv = view.findViewById(R.id.tv_content);
            tv.setText(text);
            view.setTag(text);
            tv.setTextSize(18);
        }
    
        @Override
        public void updateView(@NonNull View itemView, boolean isSelected) {
            TextView tv = itemView.findViewById(R.id.tv_content);
            tv.setTextSize(isSelected ? 18 : 14);
            tv.setTextColor(Color.parseColor(isSelected ? "#ED5275" : "#000000"));
        }
    }
    

    首先,view provider必须要实现IViewProvider接口,DefaultItemViewProvider也不例外,唯一注意的是IViewProvider本身是泛型的,所以我们需要提供item视图对应的数据类型,这里我们直接使用String类型即可。

    而对于DefaultItemViewProvider的逻辑,我们只需要实现IViewProvider接口中的抽象方法即可。所以这里对其中的方法实现进行下阐述。

    1. resLayout方法,这个方法很简单,就是提供我们自己的itme视图布局文件,默认的视图如下所示:
    <?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="wrap_content"
        android:layout_gravity="center"
        android:gravity="center">
    
        <TextView
            android:id="@+id/tv_content"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:singleLine="true"
            android:maxLines="1"
            android:padding="10dp"
            android:textColor="#666666" />
    </LinearLayout>
    

    很简单,就一个TextView,不再阐述。

    1. onBindView,这个在上面已经阐述过,就是对应于adapter中的onBindView,在这里主要是进行视图初始化,并设置了textview的一些属性,比如字体大小等。

    这里需要注意两点:

    第一点,textview设置的字体大小应该是你期望的最大的字体大小,比如,如果想要被选中的item字体大小是18sp,而未选中的item字体大小是16sp,那么这里应该设置最大的18sp;

    第二点,我们设置了view的tag(即 view.setTag(text);
    ),传入的是与item视图对应的数据,这么做是有原因的,因为我们前面通过adapter对外暴露的监听接口,无论是onClick接口还是onScroll接口,其回调数据都是item视图,并没有item对应的具体data数据,所以这里通过将item对应的数据设置为视图tag的方法,来进行数据传递,这样就可以通过getTag获取到对应的item数据了。

    1. updateView,这个方法前面也已经阐述过了,在这里我们对选中的item视图文字进行了处理,即设置选中的item视图字体大小为18sp,颜色是红色,而未选中的item视图字体大小为14sp,颜色是黑色。

    使用姿势

    本小节阐述下,该控件的使用姿势。
    首先,在需要使用滚动选择器的地方,引入我们滚动选择器视图,如下所示:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        tools:context=".demo.sample1.SampleActivity">
    
        <com.life2smile.scrollpicker.library.view.ScrollPickerView
            android:id="@+id/scroll_picker_view"
            android:layout_width="wrap_content"
            android:layout_height="match_parent">
    
        </com.life2smile.scrollpicker.library.view.ScrollPickerView>
    
    </LinearLayout>
    

    接着,和使用RecyclerView一样,完成正常的视图初始化即可,如下所示:

            ScrollPickerAdapter.ScrollPickerAdapterBuilder<String> builder =
                    new ScrollPickerAdapter.ScrollPickerAdapterBuilder<String>(this)
                            .setDataList(list)
                            .selectedItemOffset(1)
                            .visibleItemNumber(3)
                            .setDivideLineColor("#E5E5E5")
                            .setItemViewProvider(null)
                            .setOnClickListener(new ScrollPickerAdapter.OnClickListener() {
                                @Override
                                public void onSelectedItemClicked(View v) {
                                    String text = (String) v.getTag();
                                    if (text != null) {
                                        Toast.makeText(SampleActivity.this, text, Toast.LENGTH_SHORT).show();
                                    }
                                }
                            });
            ScrollPickerAdapter mScrollPickerAdapter = builder.build();
            mScrollPickerView.setAdapter(mScrollPickerAdapter);
    

    上述代码就是具体的调用姿态,具体不再展开。

    至此本篇文章的主题已阐述完毕。

    相关文章

      网友评论

        本文标题:android自定义滚动选择器(三)

        本文链接:https://www.haomeiwen.com/subject/qwxyjqtx.html