最近在项目上产品(产品很烦有木有?)有这么个需求,在同一块区域需要根据后台返回的数据数量情况进行不同布局的展示,只有一个数据全屏宽度展示,两个数据需要分主次用不同大小比例进行展示,三个及以上的数据需要均分展示并且可以滑动。我大致的思路就是通过自定义ViewPager来实现这个需求。大致效果如下:
接下来的内容会涉及到下面的知识点:
1.自定义控件;2. 根据数据加载不同布局;3.可在xml文件中声明属性或者Build模式声明 4.接口封装
1.自定义ViewPager
首先就是定义布局文件,很简单就是放置一个系统自带的ViewPager,有一个需要注意的就是* clipChildren*这个属性,该属性在ViewGroup中,设置为false系统在绘制children是不进行裁剪,反之亦然。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false">
<android.support.v4.view.ViewPager
android:clipChildren="false"
android:id="@+id/side_banner_viewpager"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"/>
</RelativeLayout>
接下来就是自定义属性,在attr.xml中进行定义,right_margin就是用来设置viewpager的marginRight, page_margin用来设置viewpager中每一个page之间的距离。
<declare-styleable name="SideViewPager">
<attr name="right_margin" format="dimension"/>
<attr name="page_margin" format="dimension"/>
</declare-styleable>
看个图对比下, right_margin设置得越大,viewpager的下一页视图也会显示出来。
图1: right_margin = 144 , page_margin = 8;
图2:right_margin = 300 , page_margin = 50。
由于viewpager自带的scroller滑动时页面会比较快,我们需要自己能设置滑动时页面切换的速度怎么办?viewpager有个属性,有同学可能要急了,这是个private属性,并且没有提供set方法,你在逗我吗???
不要急,我们可以通过反射拿到这个属性,然后想怎么玩就怎么玩。
private Scroller mScroller;
我们只需要自定义一个SideBannerScroller集成Scroller,然后在通过反射设置给viewpager。
private static class SideBannerScroller extends Scroller {
private int mDuration = SCROLLER_DURATION_TIME;
public SideBannerScroller(Context context, int duration) {
this(context);
mDuration = duration;
}
public SideBannerScroller(Context context) {
super(context);
}
public SideBannerScroller(Context context, Interpolator interpolator) {
super(context, interpolator);
}
public SideBannerScroller(Context context, Interpolator interpolator, boolean flywheel) {
super(context, interpolator, flywheel);
}
@Override
public void startScroll(int startX, int startY, int dx, int dy) {
super.startScroll(startX, startY, dx, dy, mDuration);
}
@Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
super.startScroll(startX, startY, dx, dy, mDuration);
}
public void setScrollDuration(int mDuration) {
this.mDuration = mDuration;
}
}
private void setBannerScroll() {
try {
Field mScroller = ViewPager.class.getDeclaredField("mScroller");
mScroller.setAccessible(true);
mSideBannerScroller = new SideBannerScroller(mViewPager.getContext());
mScroller.set(mViewPager, mSideBannerScroller);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
自定义需要注意的基本就是上面这些,最后一个就是设置adapter,这个地球人都会,这里就不细说了,后面我们会通过接口封装adapter,让用户在使用这个控件时可以定制布局。
2.Adapter的接口封装
自定义Adapter一般的做法:
private class MyAdapter extends PagerAdapter{
@Override
public int getCount() {
return 0;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return false;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
return super.instantiateItem(container, position);
}
}
为了外面传进来的数据能够控制布局,我们在自定义控件中预留接口,比较简单,看注释就能明白什么意思。
public interface SideViewPagerAdapter<T> {
/**
* 获取数据个数
*/
int getCount();
/**
* ClickListener
*/
void onPageClick(View view, int position);
void onLeftViewClick(View view, T data);//左边布局点击事件
void onRightViewClick(View view, T data);//右边布局点击事件
/**
* @return Holder Creator
*/
SideViewHolderCreator getSideViewHolderCreator();
/**
* 单个数据
*/
Object getItemData(int position);
}
我们往往在自定义adapter中的public Object instantiateItem(ViewGroup container, int position)方法中初始化view和绑定数据,为了根据数据加载不同布局,我们就抽象出两个接口, SideViewHolderCreator返回SideViewHolder,SideViewHolder就是用来创建view和绑定对应数据及设置点击事件。
public interface SideViewHolderCreator<S extends SideViewHolder> {
S createSideViewHolder();
}
public interface SideViewHolder<T> {
View getView(Context context);
void onBind(Context context, int position, List<T> datas);
View getLeftClickView();
View getRightClickView();
}
我们来看下adapter的instantiateItem具体实现,首先就是通过mSideViewHolder获取布局,再通过mSideViewHolder.onBind将数据绑定到布局上。而mSideViewHolder正是通过对外接口getSideViewHolderCreator获取的,所以我们在使用的时候就可以传入布局进行加载,实现了很好的扩展。
@Override
public Object instantiateItem(ViewGroup container, int position) {
clearDataRealPosition();
getDataRealPosition(position);
View view = mSideViewHolder.getView(container.getContext());
ViewGroup parent = (ViewGroup) view.getParent();
if (parent != null) {
parent.removeAllViews();
}
bindView(container, position);
container.addView(view);
return view;
}
private void bindView(ViewGroup container, int position) {
datas.clear();
datas.add(mSideViewPagerAdapter.getItemData(mLeftDataRealPos));
if (isRightDataAvailable()) {
datas.add(mSideViewPagerAdapter.getItemData(mRightDataRealPos));
}
mSideViewHolder.onBind(container.getContext(), position, datas);
}
3.根据数据加载不同布局
从上面的分析知道,我们在使用这个控件的时候只需要实现接口SideViewPagerAdapter,由于接口的方法略多,因此我们学习google的源码,提供一个默认实现DefaultSideViewPagerAdapter:
public class DefaultSideViewPagerAdapter implements SideViewPager.SideViewPagerAdapter<SideBean> {
private List<SideBean> datas;
public DefaultSideViewPagerAdapter(List<SideBean> datas) {
this.datas = datas;
}
@Override
public int getCount() {
return datas.size();
}
@Override
public void onPageClick(View view, int position) {
}
@Override
public void onLeftViewClick(View view, SideBean data) {
}
@Override
public void onRightViewClick(View view, SideBean data) {
}
@Override
public SideViewHolderCreator getSideViewHolderCreator() {
return new SideViewHolderCreator() {
@Override
public SideViewHolder createSideViewHolder() {
SideViewHolder sideViewHolder;
switch (datas.size()) {
case 1:
sideViewHolder = new ImageSideViewHolder();
break;
case 2:
sideViewHolder = new RatioSideViewHolder();
break;
default:
sideViewHolder = new EqualSideViewHolder();
break;
}
return sideViewHolder;
}
};
}
@Override
public Object getItemData(int position) {
return datas.get(position);
}
}
然后使用我们这个自定义控件只需要实现少数几个点击事件的处理方法就可以:
private class MySideViewPagerAdapter extends DefaultSideViewPagerAdapter {
public MySideViewPagerAdapter(List<SideBean> datas) {
super(datas);
}
@Override
public void onPageClick(View view, int position) {
Toast.makeText(MainActivity.this, "点击了" + String.valueOf(position), Toast.LENGTH_SHORT).show();
}
@Override
public void onLeftViewClick(View view, SideBean data) {
Toast.makeText(MainActivity.this, data.getBtnTitle() + "be clicked", Toast.LENGTH_SHORT).show();
}
@Override
public void onRightViewClick(View view, SideBean data) {
Toast.makeText(MainActivity.this, data.getBtnTitle() + "be clicked", Toast.LENGTH_SHORT).show();
}
}
4.Build模式
为了提供更好的使用性,比如有时候不是在xml文件中使用控件,而是代码中动态添加,这个时候build模式就比较方便了,使用起来就是一行代码,很是清爽有木有:
sideViewPager = SideViewPager.builder(this).
setSideViewPagerAdapter(new MySideViewPagerAdapter(datas2)).
build();
实现起来也比较简单,要注意的细节就是要处理好xml中声明的属性和通过build中的set方法设置的属性之间的先后顺序。
public static class Builder{
private int pageMargin;
private int rightMargin;
private SideViewPagerAdapter sideViewPagerAdapter;
private SideViewPager sideViewPager;
public Builder(Context context){
sideViewPager = new SideViewPager(context);
pageMargin = -1;
rightMargin = -1;
}
public Builder setPageMargin(int pageMargin) {
this.pageMargin = pageMargin;
return this;
}
public Builder setRightMargin(int rightMargin) {
this.rightMargin = rightMargin;
return this;
}
public Builder setSideViewPagerAdapter(SideViewPagerAdapter sideViewPagerAdapter) {
this.sideViewPagerAdapter = sideViewPagerAdapter;
return this;
}
public SideViewPager build(){
if (pageMargin != -1){
sideViewPager.setPageMargin(pageMargin);
}else {
sideViewPager.setPageMargin(sideViewPager.getResources().getDimensionPixelOffset(R.dimen.side_vp_page_margin));
}
if (rightMargin != -1){
sideViewPager.setRightMargin(rightMargin);
}
sideViewPager.setSideViewPagerAdapter(sideViewPagerAdapter);
return sideViewPager;
}
}
5.总结
到这里我们的自定义数据可扩展的ViewPager之旅就结束了,做下简单的总价,将adapter中初始化view和数据绑定的功能通过接口留出,就可以实现布局和绑定由用户指定的良好扩展性。通过build模式可以提升控件使用的便捷性。
最后,如果觉得文章有帮到你请点赞,你们的赞是我坚持写下去的动力,谢谢!
欢迎关注公众号:JueCode