android自定义滚动选择器(三)
本篇文章将会阐述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();
}
我们分开来阐述下每个方法完成的功能:
- 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的实现,会在下面进行分析。
- 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中,我们主要完成以下几个工作:
- 调用用户提供的view provider的updateView,即通知对方视图要更新。
- 对于滚动选择器中的item视图,提供了一个默认的点击事件,这个事件只有在item视图被选中的时候才有。其实用户完全可以在自己的view provider中,通过updateView来完成业务逻辑处理,这里只是通过adapter对外暴露一个监听响应入口,能满足基本需要。
- 与此同时,我们还提供了一个滚动监听,滚动监听的方法入口是onScrolled,该方法有一个参数currentItemView,表示当前被选中的item视图。
- 在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接口中的抽象方法即可。所以这里对其中的方法实现进行下阐述。
- 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,这个在上面已经阐述过,就是对应于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数据了。
- 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);
上述代码就是具体的调用姿态,具体不再展开。
至此本篇文章的主题已阐述完毕。