android Banner控件的优雅实现

作者: Sivin | 来源:发表于2016-05-01 13:06 被阅读21032次

    标签(空格分隔): android


    最近在项目开发中需要用到广告轮播控件,由于项目时间比较紧张,看到开源社区类也有类似的实现,于是就偷懒用了一下,但是悲剧就此来了,参考着给定的demo实现,一切是那么完美,一开始的时候,没有后台数据,但是,当伪造的数据替换成网络异步加载数据的时候,发现控件直接crash,于是查看了一下原因,瞬间蒙了,这个控件在当初设计的时候,原来没有考虑异步加载数据,可能是作者只是想要展示一下实现原理,并没有考虑这么多,如果除去这个缺点,这个控件整体实现还是很好的,但是,不能异步网络加载,就代表它再好也没什么卵用,并且我们一般还要它有数据刷新的功能,感觉这么好的实现思路,不能就这么废了,很可惜了,于是画了一天的时间,在保持原作者理论的基础上,重写了控件,增加了异步数据的实现能力,同时,以一种更加优雅的实现方式,提供给调用者使用,这里特来跟大家分享一下。

    准备知识

    一、ViewPager与pagerAdapter详解

    ViewPager有过一定android开发知识的人应该都很熟悉,用途我就不再这里详述了,我们在开发的过程中常用的是这样的使用方式:viewPager+fragment的方式

    private ViewPager mViewPager;
    private Fragment[] mFragments;
    ...
    ...
    mViewPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
            @Override
            public Fragment getItem(int position) {
                 return mFragments[position];
            }
            @Override
            public int getCount() {
               return mFragments.length;
             }
        });
    

    这样的使用方式是我们关注的重点放到到Fragment上,从而加快我们的开发,其实ViewPager并没有什么,重点是Adapter的实现方式,ViewPager的缤纷多彩的实现效果,其实都是得益于Adapter的实现方式,今天我们的重点也在这个Adapter上面,来实现我们的无限滑动的ViewPager

    pagerAdapter

    pagerAdapterFragementPagerAdapter的父类,相比FragmentPagerAdapter它通用性更强,可定制性更加灵活。我们在定制自己的pagerAdapter首先是继承PagerAdapter重写里面的方法,实现自己的功能:
    pagerAdapter重写方法分析:

    public Object instantiateItem (ViewGroup container, int position)
    

    这个函数的功能是创建指定位置的页面视图。适配器的责任就是将创建的view添加到指定的container中,返回值表示的是新增视图页面的key,一般的情况下我们将创建的视图view返回就可以了。

    public void destroyItem (ViewGroup container, int position, Object object)
    

    这个方法的功能是是移除一个给定位置的页面。适配器的责任就是从容器中删除这个视图。

    public abstract int getCount ()
    

    返回当前有效视图的个数。

    public abstract boolean isViewFromObject (View view, Object object)
    

    该函数用来判断instantiateItem(ViewGroup,int)函数所返回来的Key与一个页面视图是否是代表的同一个视图(即它俩是否是对应的,对应的表示同一个View)

    好了差不多就这些基础知识,下面我们来说实现原理

    实现原理

    网上的实现原理大体上分为两种:
    一种是在适配器中将getcount的值设置为无限大,这种实现的效果可查看淘宝的客户端,在第一次进去的时候向右滑动是无法滑动的最后一页的,可见他并不是一个真正意义上的无限轮播方式,我们今天不讨论这个;
    第二种:实现思路,首先看图说话


    Paste_Image.png

    可以看出,它分别映射出两个边界的页面,下面的个数是我们viewpager的条目,但是我们的viewPager只会在·
    1-3(下标)之间切换,当viewpager1的位置时,我们向右滑动,会出现0位置的页面,0位置上的页面实际上和3页面的内容一样,当我们松手,它会瞬间切换到下标3页面,同理,当我们滑到最后一个页面的时候,也是如此,那么这样就完成了无限滑动的viewPager的效果了,那么如何实现则个效果呢,请看下面的SLooperAdapterSLooperViewPager类,基本上每句代码都有相关的说明。
    以上就是无限滑动的ViewPager的原理。
    有了这个viewPager我们就可以构造出我们的banner,正常情况下我们的banner控件有两大部分组成,一·展示的图片,这里就是我们的ViewPager,二、下面的指示器。
    指示器的组成通常也有两部分,一个是文本 一个是一组圆点。
    我们的实现思路就是,父容器用一个RelativeLayout将我们的ViewPager和指示器容器包裹住就行了。具体实现思路请看banner类。

    Paste_Image.png
    有人说我不想看原理,只想怎么用好了,ok。
    为了增加使用的方便性,在此我模仿listView的实现习惯,增加了一个适配器,调用者只需要这样一下几步就可以完成:

    在项目的app的gradle文件中加如下代码

    compile 'com.xiwenhec:banner:1.0.2'  
    

    第一步:在xml代码写入控件

    <com.sivin.Banner
            android:id="@+id/id_banner"
            android:layout_width="match_parent"
            android:layout_height="180dp"
            app:banner_pointGravity="right"
            />
    

    第二步:java代码中绑定控件

     mBanner = (Banner) findViewById(R.id.id_banner);
    

    第三步:实例化适配器,并设置适配器,建议您在new BannerAdapter<BannerModel>的时候将后面的<>中的泛型加上,然后在根据工具提示实现未完成的方法。这样bindData(ImageView imageView, BannerModel bannerModel) 的第二个参数就是你加入的泛型类型。其中mDatas你的banner的数据集合,具体过程使用就会有所体会。
    注意:不要忘了mDatas的初始化

     BannerAdapter adapter = new BannerAdapter<BannerModel>(mDatas) {
        @Override
       protected void bindTips(TextView tv, BannerModel bannerModel) {
          tv.setText(bannerModel.getTips());
       }
       @Override
        public void bindImage(ImageView imageView, BannerModel bannerModel) {
            Glide.with(mContext)
            .load(bannerModel
            .getImageUrl())
            .placeholder(R.mipmap.empty)
            .error(R.mipmap.error)
            .into(imageView);
        }
     };
     mBanner.setBannerAdapter(adapter);
    

    最后一步:告诉banner数据不部署完成,为什么这样做呢,正常情况下,我们的数据都是从网络上异步加载的,一般的情况下会以集合的形式传递过来,当我们在完成网络加载的时候,改变了mDatas数据,然后调用mBanner.notifiDataHasChanged();通知banner就行了,使用起来和listview的习惯是不是很相似呢,对就是这样,我们已经完成了。

     mBanner.notifiDataHasChanged();
    

    本想附上apk但是不知道如何上传上去,项目的github地址:Banner:github地址,欢迎forkandstart

    以下是具体代码的实现逻辑:
    关键类:SLooperAdapter

    package com.pactera.banner.SivinBanner;
    import android.support.v4.view.PagerAdapter;
    import android.view.View;
    import android.view.ViewGroup;
    
    /**
     * 无限轮播的viewPager适配器
     * Created by xiwen on 2016/4/13.
     */
    public class SLooperAdapter extends PagerAdapter {
        private PagerAdapter mAdapter;
    
        private int mItemCount=0;
    
        public SLooperAdapter(PagerAdapter adapter) {
            mAdapter = adapter;
        }
    
        @Override
        public int getCount() {
            //如果层ViewPager中有两个或两个以上的Item的时候,则映射出边界Item,否则显示与内层个数一致
            return mAdapter.getCount() < 1 ? mAdapter.getCount() : mAdapter.getCount() + 2;
        }
    
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return mAdapter.isViewFromObject(view, object);
        }
    
    
        @Override
        public void startUpdate(ViewGroup container) {
            mAdapter.startUpdate(container);
        }
    
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
    
            return mAdapter.instantiateItem(container, getInnerAdapterPosition(position));
        }
    
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
    
            mAdapter.destroyItem(container, getInnerAdapterPosition(position), object);
        }
    
        @Override
        public void setPrimaryItem(ViewGroup container, int position, Object object) {
            mAdapter.setPrimaryItem(container, position, object);
        }
    
        @Override
        public void finishUpdate(ViewGroup container) {
            mAdapter.finishUpdate(container);
        }
    
        @Override
        public void notifyDataSetChanged() {
            mItemCount = getCount();
            super.notifyDataSetChanged();
        }
    
        @Override
        public int getItemPosition(Object object) {
            if (mItemCount>0){
                mItemCount--;
                return POSITION_NONE;
            }
            return super.getItemPosition(object);
        }
    
        /**
         * 根据外层position的获取内层的position
         * @param position 外层ViewPager的position
         * @return 外层viewPager当前数据位置对应的内层viewPager对应的位置。
         */
        public int getInnerAdapterPosition(int position) {
            //viewPager真正的可用的个数
            int realCount = getInnerCount();
            //内层没有可用的Item则换回为零
            if (realCount == 0)
                return 0;
            int realPosition = (position - 1) % realCount;
            if (realPosition < 0)
                realPosition += realCount;
            return realPosition;
        }
    
        /**
         * @return 内层ViewPager中可用的item个数
         */
        public int getInnerCount() {
            return mAdapter.getCount();
        }
    
        /**
         * 根据内层postion的位置,返回映射后外层position的位置
         * @param position 内层position的位置
         * @return 无限轮播ViewPager的切换位置
         */
        public int toLooperPosition(int position) {
            if (getInnerCount() > 1) {
                return position + 1;
            } else return position;
        }
    }
    
    

    关键类:SLooperViewPager

    package com.pactera.banner.SivinBanner;
    
    import android.content.Context;
    import android.support.v4.view.PagerAdapter;
    import android.support.v4.view.ViewPager;
    import android.util.AttributeSet;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * 无限轮播的ViewPager
     * Created by xiwen on 2016/4/13.
     */
    public class SLooperViewPager extends ViewPager {
        private SLooperAdapter mAdapter;
        private List<OnPageChangeListener> mOnPageChangeListeners;
        public SLooperViewPager(Context context) {
            this(context, null);
        }
    
    
        public SLooperViewPager(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(context);
        }
    
    
        @Override
        public void setAdapter(PagerAdapter adapter) {
            mAdapter = new SLooperAdapter(adapter);
            super.setAdapter(mAdapter);
            setCurrentItem(0, false);
        }
    
        @Override
        public PagerAdapter getAdapter() {
            return mAdapter;
        }
    
        @Override
        public void setCurrentItem(int item) {
            setCurrentItem(item, true);
        }
    
        @Override
        public void setCurrentItem(int position, boolean smoothScroll) {
            //item的被调用者传递过来的位置是没有原始的位置,即切换位置是从0到DataSize-1之间切换
            //但是对于外层ViewPager而言,他需要的位置范围应该是映射后的位置切换,即:出去两边映射的页面
            //应该是从1到映射后的倒数第二个位置
    
            super.setCurrentItem(mAdapter.toLooperPosition(position), smoothScroll);
        }
    
    
        /**
         * 外层ViewPager中的item是通过内层位置映射关系得到的
         *
         * @return 返回映射后的
         */
        @Override
        public int getCurrentItem() {
            return mAdapter.getInnerAdapterPosition(super.getCurrentItem());
        }
        
        @Override
        public void clearOnPageChangeListeners() {
            if (mOnPageChangeListeners != null) {
                mOnPageChangeListeners.clear();
            }
        }
    
        @Override
        public void removeOnPageChangeListener(OnPageChangeListener listener) {
            if (mOnPageChangeListeners != null) {
                mOnPageChangeListeners.remove(listener);
            }
        }
    
        @Override
        public void addOnPageChangeListener(OnPageChangeListener listener) {
            if (mOnPageChangeListeners == null) {
                mOnPageChangeListeners = new ArrayList<>();
            }
            mOnPageChangeListeners.add(listener);
        }
    
        private void init(Context context) {
            if (mOnPageChangeListener != null) {
                super.removeOnPageChangeListener(mOnPageChangeListener);
            }
            super.addOnPageChangeListener(mOnPageChangeListener);
        }
    
        private OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() {
            //上一次的偏移量
            private float mPreviousOffset = -1;
            //上一次的位置
            private float mPreviousPosition = -1;
    
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                if (mAdapter != null) {
                    int innerPosition = mAdapter.getInnerAdapterPosition(position);
                    /*
                        positionOffset =0:滚动完成,
                        position =0 :开始的边界
                        position =mAdapter.getCount()-1:结束的边界
                     */
                    if (positionOffset == 0 && mPreviousOffset == 0 && (position == 0 || position == mAdapter.getCount() - 1)) {
                        //强制回到映射位置
                        setCurrentItem(innerPosition, false);
                    }
                    mPreviousOffset = positionOffset;
    
                    if (mOnPageChangeListeners != null) {
                        for (int i = 0; i < mOnPageChangeListeners.size(); i++) {
                            OnPageChangeListener listener = mOnPageChangeListeners.get(i);
                            if (listener != null) {
                                //如果内层的位置没有达到最后一个,内层滚动监听器正常设置
                                if (innerPosition != mAdapter.getInnerCount() - 1) {
                                    listener.onPageScrolled(innerPosition, positionOffset, positionOffsetPixels);
                                } else {
                                    //如果到达最后一个位置,当偏移量达到0.5以上,这告诉监听器,这个页面已经到达内层的第一个位置
                                    //否则还是最后一个位置
                                    if (positionOffset > 0.5) {
                                        listener.onPageScrolled(0, 0, 0);
                                    } else {
                                        listener.onPageScrolled(innerPosition, 0, 0);
                                    }
                                }
                            }
                        }
                    }
                }
    
            }
    
            @Override
            public void onPageSelected(int position) {
                int realPosition = mAdapter.getInnerAdapterPosition(position);
                if (mPreviousPosition != realPosition) {
                    mPreviousPosition = realPosition;
                    if (mOnPageChangeListeners != null) {
                        for (int i = 0; i < mOnPageChangeListeners.size(); i++) {
                            OnPageChangeListener listener = mOnPageChangeListeners.get(i);
                            if (listener != null) {
                                listener.onPageSelected(realPosition);
                            }
                        }
                    }
                }
            }
            @Override
            public void onPageScrollStateChanged(int state) {
                if (mAdapter != null) {
                    int position = SLooperViewPager.super.getCurrentItem();
                    int realPosition = mAdapter.getInnerAdapterPosition(position);
                    if (state == ViewPager.SCROLL_STATE_IDLE && (position == 0 || position == mAdapter.getCount() - 1)) {
                        setCurrentItem(realPosition, false);
                    }
                }
                if (mOnPageChangeListeners != null) {
                    for (int i = 0; i < mOnPageChangeListeners.size(); i++) {
                        OnPageChangeListener listener = mOnPageChangeListeners.get(i);
                        if (listener != null) {
                            listener.onPageScrollStateChanged(state);
                        }
                    }
                }
            }
        };
    }
    

    关键类:Banner

    package com.pactera.banner.SivinBanner;
    
    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.Color;
    import android.graphics.drawable.ColorDrawable;
    import android.graphics.drawable.Drawable;
    import android.os.Build;
    import android.os.Handler;
    import android.os.Message;
    import android.support.v4.view.PagerAdapter;
    import android.text.TextUtils;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.util.SparseArray;
    import android.util.TypedValue;
    import android.view.Gravity;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ImageView;
    import android.widget.LinearLayout;
    import android.widget.RelativeLayout;
    import android.widget.TextView;
    
    import com.pactera.banner.R;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    /**
     * Created by xiwen on 2016/4/12.
     */
    public class Banner extends RelativeLayout {
        private static final String TAG = Banner.class.getSimpleName();
    
        private Context mContext;
    
        private SparseArray<ImageView> mItemArrays;
    
        /**
         * 布局参数
         */
        private static final int RMP = LayoutParams.MATCH_PARENT;
        private static final int RWC = LayoutParams.WRAP_CONTENT;
        private static final int LWC = LinearLayout.LayoutParams.WRAP_CONTENT;
        /**
         * 循环轮播的Viewpager
         */
        private SLooperViewPager mViewPager;
    
    
        //下面这两个控件,存放到一个相对布局中,由于不需要设成成员变量,故此没写
    
        /**
         * 轮播控件的提示文字
         */
        private TextView mTipTextView;
        /**
         * 提示文字的大小
         */
        private int mTipTextSize;
    
        /**
         * 提示文字的颜色
         */
        private int mTipTextColor = Color.WHITE;
    
        /**
         * 存放点的容器
         */
        private LinearLayout mPointContainerLl;
        /**
         * 点的drawable资源id
         */
        private int mPointDrawableResId = R.drawable.selector_basebanner_point;
    
        /**
         * 点的layout的属性
         */
        private int mPointGravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
        private int mPointLeftRightMargin;
        private int mPointTopBottomMargin;
        private int mPointContainerLeftRightPadding;
    
        /**
         * 存放TipTextView和mPointContainerLl的相对布局的背景资源Id;
         */
        private Drawable mPointContainerBackgroundDrawable;
    
        /**
         * 存放轮播信息的数据集合
         */
        protected List mData = new ArrayList<>();
    
        /**
         * 自动播放的间隔
         */
        private int mAutoPlayInterval = 3;
    
        /**
         * 页面切换的时间(从下一页开始出现,到完全出现的时间)
         */
        private int mPageChangeDuration = 800;
        /**
         * 是否正在播放
         */
        private boolean mIsAutoPlaying = false;
    
        /**
         * 当前的页面的位置
         */
        protected int currentPosition;
    
        private BannerAdapter mBannerAdapter;
    
        /**
         * 任务执行器
         */
        protected ScheduledExecutorService mExecutor;
    
    
        /**
         * 播放下一个执行器
         */
        private Handler mPlayHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                scrollToNextItem(currentPosition);
            }
        };
    
    
        public Banner(Context context) {
            this(context, null);
        }
    
        public Banner(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public Banner(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
    
            //初始化默认属性
            initDefaultAttrs(context);
    
            //初始化自定义属性
            initCustomAttrs(context, attrs);
    
            //控件初始化
            initView(context);
        }
    
        private void initDefaultAttrs(Context context) {
    
            //默认点指示器的左右Margin3dp
            mPointLeftRightMargin = dp2px(context, 3);
            //默认点指示器的上下margin为6dp
            mPointTopBottomMargin = dp2px(context, 6);
            //默认点容器的左右padding为10dp
            mPointContainerLeftRightPadding = dp2px(context, 10);
            //默认指示器提示文字大小8sp
            mTipTextSize = sp2px(context, 8);
            //默认指示器容器的背景图片
            mPointContainerBackgroundDrawable = new ColorDrawable(Color.parseColor("#33aaaaaa"));
        }
    
        public static int dp2px(Context context, float dpValue) {
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.getResources().getDisplayMetrics());
        }
    
        public static int sp2px(Context context, float spValue) {
            return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, context.getResources().getDisplayMetrics());
        }
    
        /**
         * 初始化自定义属性
         *
         * @param context context
         * @param attrs   attrs
         */
        private void initCustomAttrs(Context context, AttributeSet attrs) {
            TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BaseBanner);
            final int N = typedArray.getIndexCount();
            for (int i = 0; i < N; i++) {
                initCustomAttr(typedArray.getIndex(i), typedArray);
            }
            typedArray.recycle();
        }
    
        private void initCustomAttr(int attr, TypedArray typedArray) {
            if (attr == R.styleable.BaseBanner_banner_pointDrawable) {
                //指示器点的样式资源id
                mPointDrawableResId = typedArray.getResourceId(attr, R.drawable.selector_basebanner_point);
            } else if (attr == R.styleable.BaseBanner_banner_pointContainerBackground) {
                //指示器容器背景样式
                mPointContainerBackgroundDrawable = typedArray.getDrawable(attr);
    
            } else if (attr == R.styleable.BaseBanner_banner_pointLeftRightMargin) {
                //指示器左右边距
                mPointLeftRightMargin = typedArray.getDimensionPixelSize(attr, mPointLeftRightMargin);
            } else if (attr == R.styleable.BaseBanner_banner_pointContainerLeftRightPadding) {
                //指示器容器的左右padding
                mPointContainerLeftRightPadding = typedArray.getDimensionPixelSize(attr, mPointContainerLeftRightPadding);
            } else if (attr == R.styleable.BaseBanner_banner_pointTopBottomMargin) {
    
                //指示器的上下margin
                mPointTopBottomMargin = typedArray.getDimensionPixelSize(attr, mPointTopBottomMargin);
            } else if (attr == R.styleable.BaseBanner_banner_pointGravity) {
                //指示器在容器中的位置属性
                mPointGravity = typedArray.getInt(attr, mPointGravity);
            } else if (attr == R.styleable.BaseBanner_banner_pointAutoPlayInterval) {
                //轮播的间隔
                mAutoPlayInterval = typedArray.getInteger(attr, mAutoPlayInterval);
            } else if (attr == R.styleable.BaseBanner_banner_pageChangeDuration) {
                //页面切换的持续时间
                mPageChangeDuration = typedArray.getInteger(attr, mPageChangeDuration);
            } else if (attr == R.styleable.BaseBanner_banner_tipTextColor) {
                //提示文字颜色
                mTipTextColor = typedArray.getColor(attr, mTipTextColor);
            } else if (attr == R.styleable.BaseBanner_banner_tipTextSize) {
                //提示文字大小
                mTipTextSize = typedArray.getDimensionPixelSize(attr, mTipTextSize);
            }
    
        }
    
        /**
         * 控件初始化
         *
         * @param context context
         */
        private void initView(Context context) {
            mContext = context;
    
            mItemArrays = new SparseArray();
    
            //初始化ViewPager
            mViewPager = new SLooperViewPager(context);
    
            //以matchParent的方式将viewPager填充到控件容器中
            addView(mViewPager, new LayoutParams(RMP, RMP));
    
            //设置页面切换的持续时间
            setPageChangeDuration(mPageChangeDuration);
    
            //创建指示器容器的相对布局
            RelativeLayout indicatorContainerRl = new RelativeLayout(context);
            //设置指示器容器的背景
            if (Build.VERSION.SDK_INT >= 16) {
                indicatorContainerRl.setBackground(mPointContainerBackgroundDrawable);
            } else {
                indicatorContainerRl.setBackgroundDrawable(mPointContainerBackgroundDrawable);
            }
            //设置指示器容器Padding
            indicatorContainerRl.setPadding(mPointContainerLeftRightPadding, 0, mPointContainerLeftRightPadding, 0);
            //初始化指示器容器的布局参数
            LayoutParams indicatorContainerLp = new LayoutParams(RMP, RWC);
    
            // 设置指示器容器内的子view的布局方式
            if ((mPointGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.TOP) {
                indicatorContainerLp.addRule(RelativeLayout.ALIGN_PARENT_TOP);
            } else {
                indicatorContainerLp.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
            }
            //将指示器容器添加到父View中
            addView(indicatorContainerRl, indicatorContainerLp);
    
            //初始化存放点的线性布局
            mPointContainerLl = new LinearLayout(context);
            //设置线性布局的id
            mPointContainerLl.setId(R.id.banner_pointContainerId);
            //设置线性布局的方向
            mPointContainerLl.setOrientation(LinearLayout.HORIZONTAL);
            //设置点容器的布局参数
            LayoutParams pointContainerLp = new LayoutParams(RWC, RWC);
            //将点容器存放到指示器容器中
            indicatorContainerRl.addView(mPointContainerLl, pointContainerLp);
            //初始化tip的layout尺寸参数,高度和点的高度一致
            LayoutParams tipLp = new LayoutParams(RMP, getResources().getDrawable(mPointDrawableResId).getIntrinsicHeight() + 2 * mPointTopBottomMargin);
            mTipTextView = new TextView(context);
            mTipTextView.setGravity(Gravity.CENTER_VERTICAL);
            mTipTextView.setSingleLine(true);
            mTipTextView.setEllipsize(TextUtils.TruncateAt.END);
            mTipTextView.setTextColor(mTipTextColor);
            mTipTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTipTextSize);
            //将TieTextView存放于指示器容器中
            indicatorContainerRl.addView(mTipTextView, tipLp);
            int horizontalGravity = mPointGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
            // 处理圆点容器位于指示器容器的左边、右边还是水平居中
            if (horizontalGravity == Gravity.LEFT) {
                pointContainerLp.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
                //提示文字设置在点容器的右边
                tipLp.addRule(RelativeLayout.RIGHT_OF, R.id.banner_pointContainerId);
                mTipTextView.setGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
            } else if (horizontalGravity == Gravity.RIGHT) {
                pointContainerLp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
                tipLp.addRule(RelativeLayout.LEFT_OF, R.id.banner_pointContainerId);
            } else {
                pointContainerLp.addRule(RelativeLayout.CENTER_HORIZONTAL);
                tipLp.addRule(RelativeLayout.LEFT_OF, R.id.banner_pointContainerId);
            }
        }
    
    
        /**
         * 初始化点
         * 这样的做法,可以使在刷新获数据的时候提升性能
         */
        private void initPoints() {
    
            int childCount = mPointContainerLl.getChildCount();
            int dataSize = mData.size();
            int offset = dataSize - childCount;
            if (offset == 0)
                return;
            if (offset > 0) {
                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LWC, LWC);
                lp.setMargins(mPointLeftRightMargin, mPointTopBottomMargin, mPointLeftRightMargin, mPointTopBottomMargin);
                ImageView imageView;
                for (int i = 0; i < offset; i++) {
                    imageView = new ImageView(getContext());
                    imageView.setLayoutParams(lp);
                    imageView.setImageResource(mPointDrawableResId);
                    imageView.setEnabled(false);
                    mPointContainerLl.addView(imageView);
                }
                return;
            }
            if (offset < 0) {
                mPointContainerLl.removeViews(dataSize, -offset);
            }
        }
    
    
        private final class ChangePointListener extends SLooperViewPager.SimpleOnPageChangeListener {
            @Override
            public void onPageSelected(int position) {
                currentPosition = position % mData.size();
                switchToPoint(currentPosition);
            }
    
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                if (mTipTextView != null) {
                    if (positionOffset > 0.5) {
                        onTitleSlect(mTipTextView, currentPosition);
                        mTipTextView.setAlpha(positionOffset);
                    } else {
                        mTipTextView.setAlpha(1 - positionOffset);
                        onTitleSlect(mTipTextView, currentPosition);
                    }
                }
            }
        }
    
        /**
         * 将点切换到指定的位置
         * 就是将指定位置的点设置成Enable
         *
         * @param newCurrentPoint 新位置
         */
        private void switchToPoint(int newCurrentPoint) {
            for (int i = 0; i < mPointContainerLl.getChildCount(); i++) {
                mPointContainerLl.getChildAt(i).setEnabled(false);
            }
            mPointContainerLl.getChildAt(newCurrentPoint).setEnabled(true);
    
            if (mTipTextView != null) {
                onTitleSlect(mTipTextView, currentPosition);
            }
        }
    
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            int action = ev.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    pauseScroll();
                    break;
                case MotionEvent.ACTION_UP:
                    goScroll();
                    break;
                case MotionEvent.ACTION_CANCEL:
                    goScroll();
                    break;
            }
            return super.dispatchTouchEvent(ev);
        }
    
    
        /**
         * 重写方法,当Viewpager滚动到下一个位置的时候,设置title的内容,
         * 同时你也可以设置title的属性,例如textColor
         * 如果指示器的setIndicatorGravity设置的是center属性,则不做任何事情
         */
        public void onTitleSlect(TextView tv, int position) {
        }
    
    
        /**
         * 设置页码切换过程的时间长度
         *
         * @param duration 页码切换过程的时间长度
         */
        public void setPageChangeDuration(int duration) {
    
        }
    
        /**
         * 滚动到下一个条目
         *
         * @param position
         */
        private void scrollToNextItem(int position) {
            position++;
            mViewPager.setCurrentItem(position, true);
        }
    
    
        /**
         * viewPager的适配器
         */
        private final class InnerPagerAdapter extends PagerAdapter {
            int mCount = 0;
    
            @Override
            public int getCount() {
                return mData.size();
            }
    
            @Override
            public Object instantiateItem(ViewGroup container, final int position) {
                ImageView  view = createItemView(position);
                mBannerAdapter.setImageViewSource(view, mTipTextView, position);
                view.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (onVpItemClickListener != null) {
                            onVpItemClickListener.onItemClick(position);
                        }
                    }
                });
    
                container.addView(view);
                return view;
            }
    
            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
                container.removeView((View) object);
                object=null;
            }
    
            @Override
            public boolean isViewFromObject(View view, Object object) {
                return view == object;
            }
    
    
            @Override
            public int getItemPosition(Object object) {
                return POSITION_NONE;
            }
        }
    
        /**
         * 创建itemView
         *
         * @param position
         * @return
         */
        private ImageView createItemView(int position) {
            ImageView iv = new ImageView(mContext);
            iv.setScaleType(ImageView.ScaleType.CENTER_CROP);
            mItemArrays.put(position, iv);
            return iv;
        }
    
        ;
    
    
        private OnVpItemClickListener onVpItemClickListener;
    
        /**
         * 设置viewPage的Item点击监听器
         *
         * @param listener
         */
        public void setOnItemClickListener(OnVpItemClickListener listener) {
            this.onVpItemClickListener = listener;
        }
    
        public interface OnVpItemClickListener {
            void onItemClick(int position);
        }
    
    
        /**
         * 方法使用状态 :viewpager处于暂停的状态
         * 开始滚动
         */
        public void goScroll() {
            if (!isValid()) {
                return;
            }
            if (mIsAutoPlaying) {
                return;
            } else {
                pauseScroll();
                mExecutor = Executors.newSingleThreadScheduledExecutor();
                //command:执行线程
                //initialDelay:初始化延时
                //period:两次开始执行最小间隔时间
                //unit:计时单位
                mExecutor.scheduleAtFixedRate(new Runnable() {
                    @Override
                    public void run() {
                        mPlayHandler.obtainMessage().sendToTarget();
                    }
                }, mAutoPlayInterval, mAutoPlayInterval, TimeUnit.SECONDS);
                mIsAutoPlaying = true;
            }
        }
    
        /**
         * 暂停滚动
         */
        public void pauseScroll() {
            if (mExecutor != null) {
                mExecutor.shutdown();
                mExecutor = null;
            }
            mIsAutoPlaying = false;
        }
    
        @Override
        protected void onVisibilityChanged(View changedView, int visibility) {
            super.onVisibilityChanged(changedView, visibility);
            if (visibility == VISIBLE) {
                goScroll();
            } else if (visibility == INVISIBLE) {
                pauseScroll();
            }
        }
    
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            pauseScroll();
        }
    
        /**
         * 判断控件是否可用
         *
         * @return
         */
        protected boolean isValid() {
            if (mViewPager == null) {
                Log.e(TAG, "ViewPager is not exist!");
                return false;
            }
            if (mData == null || mData.size() == 0) {
                Log.e(TAG, "DataList must be not empty!");
                return false;
            }
            return true;
        }
    
        /**
         * 设置数据的集合
         */
        public void setSource() {
            List list = mBannerAdapter.getDatas();
            if (list == null) {
                Log.d(TAG, "setSource: list==null");
                return;
            }
            this.mData = list;
            setAdapter();
        }
    
        /**
         * 给viewpager设置适配器
         */
        private void setAdapter() {
            mViewPager.setAdapter(new InnerPagerAdapter());
            mViewPager.addOnPageChangeListener(new ChangePointListener());
        }
    
        public void setBannerAdapter(BannerAdapter adapter) {
            mBannerAdapter = adapter;
            setSource();
        }
        /**
         * 通知数据已经放生改变
         */
        public void notifiDataHasChanged() {
            initPoints();
            mViewPager.getAdapter().notifyDataSetChanged();
            mViewPager.setCurrentItem(0, false);
            goScroll();
        }
    }
    

    关键类:BannerAdapter

    package com.pactera.banner.SivinBanner;
    
    import android.widget.ImageView;
    import android.widget.TextView;
    
    import java.util.List;
    
    /**
     * Created by sivin on 2016/5/1.
     */
    public abstract class BannerAdapter<T> {
        private static final String TAG = "BannerAdapter";
        private List<T> mDatas;
    
        public List<T> getDatas() {
            return mDatas;
        }
    
        public BannerAdapter(List<T> datas) {
            mDatas = datas;
        }
    
        public void setImageViewSource(ImageView imageView, TextView textView, int position) {
            bindImage(imageView, mDatas.get(position));
        }
    
        public void selectTips(TextView tv, int position) {
            if (mDatas != null && mDatas.size() > 0)
                bindTips(tv, mDatas.get(position));
        }
    
        protected abstract void bindTips(TextView tv, T t);
    
        public abstract void bindImage(ImageView imageView, T t);
    
    }
    
    

    相关文章

      网友评论

      • Aldrich_N:你这种两边各加一个item的方式快速滑动时不卡顿么
        Sivin:@Aldrich_N卡顿主要是到边界的时候强制设置导致的,可以参考第一种方式,将起点设置到一个很大的数,这样就可以减少到达边界的次数,从而减少卡顿
        Aldrich_N:@Sivin 是不影响使用,但是快速滑动要求高峰话就不好解决
        Sivin:@Aldrich_N 快速滑动的话,会,这个卡并不影响使用,你也可以结合两种方式来解决。
      • yask:滑到边界快速滑动会有卡顿现象,因该是性能问题
        yask:@Sivin 应该是头尾映射view的时候创建的太多了
        Sivin: @黑马飞马 快速滑动会大量的创建view
        Sivin: @黑马飞马 嗯,确实有这部分原因
      • 7e7398cb3655:我给recyclerview的添加Header里面有个轮播,添加后不显示指示圆点也不能滑动滑动就会碰掉这是什么原因呢?楼主
      • eae52a7e90eb:首先谢谢分享,非常好用,学习了。就有一个小缺点,快速滑动有一点卡。
      • 冷酷的睡睡:哥,点击事件咋办
        Sivin: @shuike 里面有监听器,提示就可以看到
        Sivin: @shuike 里面有
      • 小强大草莓:写得真的非常好,控件适用性也很强,谢谢。希望能继续写出更好的控件。
        Sivin: @小强大草莓 谢谢,源代码很少,希望可以帮到你
      • 5116674dbdbe:楼主,初次加载完成显示第1张图片时,第一个圆点没有被默认选中。希望能尽快修复出个新版,我项目里用到了这个控件。再次感谢!
        Sivin:1.0.4 增加一定会被选中
        Sivin:你好,刚才查看了一下,第一张图片会被选中的,具体可查看github上的demo代码,在notifyDataHasChanged这个方法调用的时候就会被选中,看看你是否调用了这个方法
        Sivin: @胡不归_6602 好的,我检查一下
      • suniney:为什么每次回到第一张 会闪动一下
        suniney:@Sivin 您好 用Glide加载图片不会出现这个问题 我当时用的是okhttp 加载bitmap 方式
        suniney:@Sivin 最后一张 回到第一张 第一张会重新加载 你看看你那面是否有这样的问题
        Sivin: @suniney 我没发现有什么闪动啊,,具体现象是什么,很明显吗
      • Android_松哥:涨姿势了。。。
      • 9240449b731e:pageChangeDuration这个是控制页面切换过程的时间的吧,为什么设置了没用呢,一直都是一闪就切换完了
        Sivin:@天行Aptx 在gradle中 使用banner:1.0.2,其他的不用改动就可以了,改动代码,可以到github上查看
        Sivin:@天行Aptx 你好,首先感谢你发现了这个问题,bug已经修复
      • JinLiag:你好,楼主!看了看,问个问题:因为BannerModel里面是图片的url,那么图片的二进制数据是从哪里传入控件显示的?
        Sivin:@JinLiag mode是自定义的,里面的成员可以根据你的需要写入不同类型的数据,只要imageview可以显示就行了,这个示例应为用的glide加载图片,所以使用的是url,你也可以使用bitmap
      • 乘风破浪的程序员:你好楼主 ,有源码么 分享一下,感觉非常实用
        Sivin:@hante 项目在github上,可以自行下载
      • TheLights:思路很奇特学习了
        Sivin:@TheLights 谢谢
      • 岁月无痕灬灬:等会去试试,看效果好不好,本来想自己造轮子的,可惜不知从何下手!看完这个再试试!谢谢楼主分享!
        Sivin: @岁月无痕灬灬 有啊
        岁月无痕灬灬:@Sivin 给个链接啊 :kissing_closed_eyes:
        Sivin:@岁月无痕灬灬 github上有,可以看看

      本文标题:android Banner控件的优雅实现

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