本篇文章將會闡述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();
}
我們分開來闡述下每個方法完成的功能:
- 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),會在下面進行分析。
- 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中,我們主要完成以下幾個工作:
- 調(diào)用用戶提供的view provider的updateView,即通知對方視圖要更新。
- 對于滾動選擇器中的item視圖,提供了一個默認的點擊事件,這個事件只有在item視圖被選中的時候才有。其實用戶完全可以在自己的view provider中,通過updateView來完成業(yè)務邏輯處理,這里只是通過adapter對外暴露一個監(jiān)聽響應入口,能滿足基本需要。
- 與此同時,我們還提供了一個滾動監(jiān)聽,滾動監(jiān)聽的方法入口是onScrolled,該方法有一個參數(shù)currentItemView,表示當前被選中的item視圖。
- 在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)進行下闡述。
- 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,不再闡述。
- 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ù)了。
- 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),具體不再展開。
至此本篇文章的主題已闡述完畢。