android自定義滾動選擇器(三)

本篇文章將會闡述ScrollPickerAdapter及默認的item視圖DefaultItemViewProvider的具體實現(xiàn),ScrollPickerAdapter的設計在文章android自定義滾動選擇器(一)
已經(jīng)詳細闡述過,這里照例直接從代碼的角度進行闡述。

如果來不及閱讀文章,或者想直接獲取源碼,見git:android自定義滾動選擇器

ScrollPickerAdapter解析

根據(jù)前面分析,ScrollPickerAdapter首先要繼承RecyclerView.Adapter并實現(xiàn)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視圖該如何提供?

按照常規(guī)方法,可以直接在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視圖實現(xiàn),當用戶不需要自定義的時候,可以使用默認的item視圖,所以在onCreateViewHolder中,我們做了一下判斷:

//當用戶沒有提供view provider的時候,使用默認item視圖提供者
        if (mViewProvider == null) {
            mViewProvider = new DefaultItemViewProvider();
        }

對于DefaultItemViewProvider的實現(xiàn),會在下面進行分析。

  1. onBindViewHolder方法,其實現(xiàn)代碼如下所示:
    @Override
    public void onBindViewHolder(@NonNull ScrollPickerAdapterHolder holder, int position) {
        mViewProvider.onBindView(holder.itemView, mDataList.get(position));
    }

onBindViewHolder本身的功能就是完成holder視圖和數(shù)據(jù)的綁定,這里因為我們允許用戶自定義item視圖,所以就直接委托給view provider進行實現(xiàn)。

至此,關于adapter自身的一些方法就闡述完了,下面來看一下IPickerViewOperation相應的方法,在ScrollPickerAdapter中的實現(xiàn),如下所示:

    @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,其他的方法都是數(shù)據(jù)提供功能,這些都是由外界塞進來的,所以這里主要關注下updateView。

再次闡述下updateView方法的作用,updateView就是在ScrollPickerView滾動的時候,及時通知外界。updateView包括兩個入?yún)ⅲ阂粋€是當前的item視圖,一個是標識當前item視圖是否被選中。外界拿到這兩個參數(shù)后可以根據(jù)自己的需求來定制化,比如將選中的item視圖文字變大、變色等。

在ScrollPickerAdapter中的updateView中,我們主要完成以下幾個工作:

  1. 調(diào)用用戶提供的view provider的updateView,即通知對方視圖要更新。
  2. 對于滾動選擇器中的item視圖,提供了一個默認的點擊事件,這個事件只有在item視圖被選中的時候才有。其實用戶完全可以在自己的view provider中,通過updateView來完成業(yè)務邏輯處理,這里只是通過adapter對外暴露一個監(jiān)聽響應入口,能滿足基本需要。
  3. 與此同時,我們還提供了一個滾動監(jiān)聽,滾動監(jiān)聽的方法入口是onScrolled,該方法有一個參數(shù)currentItemView,表示當前被選中的item視圖。
  4. 在updateView方法中,調(diào)用了 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相關的基本闡述完了,那么還有一個問題,如何保證外部定制數(shù)據(jù)能直接有效的在視圖構建前生效?這個問題在第一篇文章中分析過,那就是采用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方法中主要調(diào)用了adaptiveData方法,這個方法功能很重要,下面分析下它的實現(xiàn)。

首先來看下adaptiveData方法。該方法的功能是用于數(shù)據(jù)填充,比如兩條分割線偏移量為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);//在滾動器前面增加數(shù)據(jù),item數(shù)據(jù)值為空
            }

            for (int i = 0; i < visibleItemNum - selectedItemOffset - 1; i++) {
                list.add(null);//在滾動器后面增加數(shù)據(jù),item數(shù)據(jù)值為空
            }
        }

上面代碼需要注意的是,因為我們補充數(shù)據(jù)的時候,補充的是null,所以在接收數(shù)據(jù)的時候一定要進行非空判斷,在闡述默認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必須要實現(xiàn)IViewProvider接口,DefaultItemViewProvider也不例外,唯一注意的是IViewProvider本身是泛型的,所以我們需要提供item視圖對應的數(shù)據(jù)類型,這里我們直接使用String類型即可。

而對于DefaultItemViewProvider的邏輯,我們只需要實現(xiàn)IViewProvider接口中的抽象方法即可。所以這里對其中的方法實現(xiàn)進行下闡述。

  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,這個在上面已經(jīng)闡述過,就是對應于adapter中的onBindView,在這里主要是進行視圖初始化,并設置了textview的一些屬性,比如字體大小等。

這里需要注意兩點:

第一點,textview設置的字體大小應該是你期望的最大的字體大小,比如,如果想要被選中的item字體大小是18sp,而未選中的item字體大小是16sp,那么這里應該設置最大的18sp;

第二點,我們設置了view的tag(即 view.setTag(text);
),傳入的是與item視圖對應的數(shù)據(jù),這么做是有原因的,因為我們前面通過adapter對外暴露的監(jiān)聽接口,無論是onClick接口還是onScroll接口,其回調(diào)數(shù)據(jù)都是item視圖,并沒有item對應的具體data數(shù)據(jù),所以這里通過將item對應的數(shù)據(jù)設置為視圖tag的方法,來進行數(shù)據(jù)傳遞,這樣就可以通過getTag獲取到對應的item數(shù)據(jù)了。

  1. updateView,這個方法前面也已經(jīng)闡述過了,在這里我們對選中的item視圖文字進行了處理,即設置選中的item視圖字體大小為18sp,顏色是紅色,而未選中的item視圖字體大小為14sp,顏色是黑色。

使用姿勢

本小節(jié)闡述下,該控件的使用姿勢。
首先,在需要使用滾動選擇器的地方,引入我們滾動選擇器視圖,如下所示:

<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);

上述代碼就是具體的調(diào)用姿態(tài),具體不再展開。

至此本篇文章的主題已闡述完畢。

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

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

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