【Android】造轮子:轮播图

作者: 带心情去旅行 | 来源:发表于2016-09-01 22:01 被阅读6261次

    前言

    目前市场上的APP中,轮播图可以说是很常见的。一个好的轮播图,基本上适用于所有的APP。是时候打造一个自己的轮播图了,不要等到用的时候才去Google。

    本文参考自Android实现Banner界面广告图片循环轮播(包括实现手动滑动循环),根据该代码改编

    功能

    轮播图需要实现一下功能

    • 图片循环轮播
    • 可添加文字
    • 最后一张到第一张的切换也要有切换效果
    • 循环、自动播放可控制

    还有我们都比较关注的一点:这轮子必须易拆、易装,可扩展性强。每次换个项目就要拷贝好几个文件,改一大堆代码,这是很烦的。

    实现

    再多的文字也不如一张图来得直观,先来个福利,回头再说怎实现的。

    效果

    思路

    这里使用ViewPager来实现轮播的效果,但是ViewPager是滑动到最后一张时,是不能跳转到第一张的。于是,我们可以这样:

    • 需要显示的轮播图有N张
    • ViewPager中添加NView,这时ViewPager中有:
      View(1)、View(2)、View(3) ... View(N)
    • 再往ViewPager中添加View(1),这时ViewPager中有:
      View(1)、View(2)、View(3) ... View(N)、View(1)

    这样就可以实现一种视觉效果:滑动到最后一张 View(N)的时候,再往后滑动就回到了第一张View(1)
    这也适用于从第一张条转到最后一张的实现。
    文字看着费解?那就看图吧(还好会那么一点点PS)
    例:
    需要显示三张图:

    需要轮播的图片

    经过处理,变成这样

    处理后的轮播图

    在界面上看到的是三张图片,而实际在ViewPager中的是这样的5张。

    • 当从View4跳转到View5时,在代码中立刻将视图切换到View2,应为图片是一样的,所有在界面上看不到任何效果。
    • 同理,当从View2跳转到View1时,在代码中将视图切换到View4。

    自动轮播流程:
    View2 -->View3 --> View4 --> View5 -->View2(完成一次循环)-->View3 -->View4....
    当显示View5的时候,立刻切换到View2View5View2显示的内容是相同的),这样就实现了图片轮播。
    这里View5 ->View2的切换巧妙利用了ViewPager中的方法:

    setCurrentItem(int item, boolean smoothScroll)
    

    参数smoothScroll为false的时候,实现了“看不见”的跳转。

    还是不大清楚?那就直接看代码吧

    代码

    思路说完,上代码

    • 创建model
      这里创建一个Info类,模拟实际应用中的数据。里面有titleurl字段。
    public class Info {
        private String url;
        private String title;
    
        public Info(String title, String url) {
            this.url = url;
            this.title = title;
        }
    
        public String getUrl() {
            return url;
        }
    
        public void setUrl(String url) {
            this.url = url;
        }
    
        public String getTitle() {
            return title;
        }
    
        public void setTitle(String title) {
            this.title = title;
        }
    }
    
    • 布局
      为了实现画面重叠的效果,这里用了相对布局,轮播图使用ViewPager来实现。后面有两个LinearLayout,第一个LinearLayout用来放指示器,在java代码中动态添加;第二个LinearLayout就用来显示Title了,当然,如果还需要显示的其他内容,可以在这个布局里面中添加。
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/white"
        android:orientation="vertical">
        <android.support.v4.view.ViewPager
            android:id="@+id/cycle_view_pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
        <LinearLayout
            android:id="@+id/cycle_indicator"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="10dp"
            android:gravity="center"
            android:orientation="horizontal" />
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_above="@id/cycle_indicator"
            android:orientation="vertical">
            <TextView
                android:id="@+id/cycle_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="10dp"
                android:gravity="center"
                android:textColor="@android:color/white"
                android:textSize="20sp" />
        </LinearLayout>
    </RelativeLayout>
    
    • CycleViewPager
      重点来了,自定义的轮播图。来个重磅炸弹,别看晕了
    public class CycleViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {
    
        private static final String TAG = "CycleViewPager";
    
        private Context mContext;
    
        private ViewPager mViewPager;//实现轮播图的ViewPager
    
        private TextView mTitle;//标题
    
        private LinearLayout mIndicatorLayout; // 指示器
    
        private Handler handler;//每几秒后执行下一张的切换
    
        private int WHEEL = 100; // 转动
    
        private int WHEEL_WAIT = 101; // 等待
    
        private List<View> mViews = new ArrayList<>(); //需要轮播的View,数量为轮播图数量+2
    
        private ImageView[] mIndicators;    //指示器小圆点
    
        private boolean isScrolling = false; // 滚动框是否滚动着
    
        private boolean isCycle = true; // 是否循环,默认为true
    
        private boolean isWheel = true; // 是否轮播,默认为true
    
        private int delay = 4000; // 默认轮播时间
    
        private int mCurrentPosition = 0; // 轮播当前位置
    
        private long releaseTime = 0; // 手指松开、页面不滚动时间,防止手机松开后短时间进行切换
    
        private ViewPagerAdapter mAdapter;
    
        private ImageCycleViewListener mImageCycleViewListener;
    
        private List<Info> infos;//数据集合
    
        private int mIndicatorSelected;//指示器图片,被选择状态
    
        private int mIndicatorUnselected;//指示器图片,未被选择状态
    
        final Runnable runnable = new Runnable() {
            @Override
            public void run() {
                if (mContext != null && isWheel) {
                    long now = System.currentTimeMillis();
                    // 检测上一次滑动时间与本次之间是否有触击(手滑动)操作,有的话等待下次轮播
                    if (now - releaseTime > delay - 500) {
                        handler.sendEmptyMessage(WHEEL);
                    } else {
                        handler.sendEmptyMessage(WHEEL_WAIT);
                    }
                }
            }
        };
    
        public CycleViewPager(Context context) {
            this(context, null);
        }
    
        public CycleViewPager(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public CycleViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            this.mContext = context;
            initView();
        }
    
        /**
         * 初始化View
         */
        private void initView() {
          LayoutInflater.from(mContext).inflate(R.layout.layout_cycle_view, this, true);
            mViewPager = (ViewPager) findViewById(R.id.cycle_view_pager);
            mTitle = (TextView) findViewById(R.id.cycle_title);
            mIndicatorLayout = (LinearLayout) findViewById(R.id.cycle_indicator);
            handler = new Handler() {
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    if (msg.what == WHEEL && mViews.size() > 0) {
                        if (!isScrolling) {
                            //当前为非滚动状态,切换到下一页
                            int posttion = (mCurrentPosition + 1) % mViews.size();
                            mViewPager.setCurrentItem(posttion, true);
                        }
                        releaseTime = System.currentTimeMillis();
                        handler.removeCallbacks(runnable);
                        handler.postDelayed(runnable, delay);
                        return;
                    }
                    if (msg.what == WHEEL_WAIT && mViews.size() > 0) {
                        handler.removeCallbacks(runnable);
                        handler.postDelayed(runnable, delay);
                    }
                }
            };
        }
    
        /**
         * 设置指示器图片,在setData之前调用
         *
         * @param select   选中时的图片
         * @param unselect 未选中时的图片
         */
        public void setIndicators(int select, int unselect) {
            mIndicatorSelected = select;
            mIndicatorUnselected = unselect;
        }
        public void setData(List<Info> list, ImageCycleViewListener listener) {
            setData(list, listener, 0);
        }
    
        /**
         * 初始化viewpager
         *
         * @param list         要显示的数据
         * @param showPosition 默认显示位置
         */
        public void setData(List<Info> list, ImageCycleViewListener listener, 
            int showPosition) {
            if (list == null || list.size() == 0) {
                //没有数据时隐藏整个布局
                this.setVisibility(View.GONE);
                return;
            }
            mViews.clear();
            infos = list;
            if (isCycle) {
                //添加轮播图View,数量为集合数+2
                // 将最后一个View添加进来
                mViews.add(getImageView(mContext, infos.get(infos.size() - 1).getUrl()));
                for (int i = 0; i < infos.size(); i++) {
                    mViews.add(getImageView(mContext, infos.get(i).getUrl()));
                }
                // 将第一个View添加进来
                mViews.add(getImageView(mContext, infos.get(0).getUrl()));
            } else {
                //只添加对应数量的View
                for (int i = 0; i < infos.size(); i++) {
                    mViews.add(getImageView(mContext, infos.get(i).getUrl()));
                }
            }
            if (mViews == null || mViews.size() == 0) {
                //没有View时隐藏整个布局
                this.setVisibility(View.GONE);
                return;
            }
            mImageCycleViewListener = listener;
            int ivSize = mViews.size();
            // 设置指示器
            mIndicators = new ImageView[ivSize];
            if (isCycle)
                mIndicators = new ImageView[ivSize - 2];
            mIndicatorLayout.removeAllViews();
            for (int i = 0; i < mIndicators.length; i++) {
                mIndicators[i] = new ImageView(mContext);
                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                        LinearLayout.LayoutParams.WRAP_CONTENT, 
                        LinearLayout.LayoutParams.WRAP_CONTENT);
                lp.setMargins(10, 0, 10, 0);
                mIndicators[i].setLayoutParams(lp);
                mIndicatorLayout.addView(mIndicators[i]);
            }
            mAdapter = new ViewPagerAdapter();
            // 默认指向第一项,下方viewPager.setCurrentItem将触发重新计算指示器指向
            setIndicator(0);
            mViewPager.setOffscreenPageLimit(3);
            mViewPager.setOnPageChangeListener(this);
            mViewPager.setAdapter(mAdapter);
            if (showPosition < 0 || showPosition >= mViews.size())
                showPosition = 0;
            if (isCycle) {
                showPosition = showPosition + 1;
            }
            mViewPager.setCurrentItem(showPosition);
            setWheel(true);//设置轮播
        }
    
        /**
         * 获取轮播图View
         *
         * @param context
         * @param url
         */
        private View getImageView(Context context, String url) {
            return MainActivity.getImageView(context, url);
        }
    
        /**
         * 设置指示器
         *
         * @param selectedPosition 默认指示器位置
         */
        private void setIndicator(int selectedPosition) {
            setText(mTitle, infos.get(selectedPosition).getTitle());
            try {
                for (int i = 0; i < mIndicators.length; i++) {
                    mIndicators[i]
                            .setBackgroundResource(mIndicatorUnselected);
                }
                if (mIndicators.length > selectedPosition)
                    mIndicators[selectedPosition]
                            .setBackgroundResource(mIndicatorSelected);
            } catch (Exception e) {
                Log.i(TAG, "指示器路径不正确");
            }
        }
    
        /**
         * 页面适配器 返回对应的view
         *
         * @author Yuedong Li
         */
        private class ViewPagerAdapter extends PagerAdapter {
            @Override
            public int getCount() {
                return mViews.size();
            }
            @Override
            public boolean isViewFromObject(View arg0, Object arg1) {
                return arg0 == arg1;
            }
            @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
                container.removeView((View) object);
            }
            @Override
            public View instantiateItem(ViewGroup container, final int position) {
                View v = mViews.get(position);
                if (mImageCycleViewListener != null) {
                    v.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                         mImageCycleViewListener.onImageClick(
                            infos.get(mCurrentPosition - 1), mCurrentPosition, v);
                        }
                    });
                }
                container.addView(v);
                return v;
            }
            @Override
            public int getItemPosition(Object object) {
                return POSITION_NONE;
            }
        }
        @Override
        public void onPageScrolled(
              int position, float positionOffset, int positionOffsetPixels) {
        }
        @Override
        public void onPageSelected(int arg0) {
            int max = mViews.size() - 1;
            int position = arg0;
            mCurrentPosition = arg0;
            if (isCycle) {
                if (arg0 == 0) {
                    //滚动到mView的1个(界面上的最后一个),将mCurrentPosition设置为max - 1
                    mCurrentPosition = max - 1;
                } else if (arg0 == max) {
                    //滚动到mView的最后一个(界面上的第一个),将mCurrentPosition设置为1
                    mCurrentPosition = 1;
                }
                position = mCurrentPosition - 1;
            }
            setIndicator(position);
        }
        @Override
        public void onPageScrollStateChanged(int state) {
            if (state == 1) { // viewPager在滚动
                isScrolling = true;
                return;
            } else if (state == 0) { // viewPager滚动结束
    
                releaseTime = System.currentTimeMillis();
                //跳转到第mCurrentPosition个页面(没有动画效果,实际效果页面上没变化)
                mViewPager.setCurrentItem(mCurrentPosition, false);
            }
            isScrolling = false;
        }
    
        /**
         * 为textview设置文字
         * @param textView
         * @param text
         */
        public static void setText(TextView textView, String text) {
            if (text != null && textView != null) textView.setText(text);
        }
    
        /**
         * 为textview设置文字
         *
         * @param textView
         * @param text
         */
        public static void setText(TextView textView, int text) {
            if (textView != null) setText(textView, text + "");
        }
    
        /**
         * 是否循环,默认开启。必须在setData前调用
         *
         * @param isCycle 是否循环
         */
        public void setCycle(boolean isCycle) {
            this.isCycle = isCycle;
        }
    
        /**
         * 是否处于循环状态
         *
         * @return
         */
        public boolean isCycle() {
            return isCycle;
        }
    
        /**
         * 设置是否轮播,默认轮播,轮播一定是循环的
         *
         * @param isWheel
         */
        public void setWheel(boolean isWheel) {
            this.isWheel = isWheel;
            isCycle = true;
            if (isWheel) {
                handler.postDelayed(runnable, delay);
            }
        }
    
        /**
         * 刷新数据,当外部视图更新后,通知刷新数据
         */
        public void refreshData() {
            if (mAdapter != null)
                mAdapter.notifyDataSetChanged();
        }
    
        /**
         * 是否处于轮播状态
         *
         * @return
         */
        public boolean isWheel() {
            return isWheel;
        }
    
        /**
         * 设置轮播暂停时间,单位毫秒(默认4000毫秒)
         * @param delay
         */
        public void setDelay(int delay) {
            this.delay = delay;
        }
    
        /**
         * 轮播控件的监听事件
         *
         * @author minking
         */
        public static interface ImageCycleViewListener {
    
            /**
             * 单击图片事件
             *
             * @param info
             * @param position
             * @param imageView
             */
            public void onImageClick(Info info, int position, View imageView);
        }
    }
    

    从里面挑了几个变量和方法说明一下:
    变量
    handlerrunnable:实现定时轮播
    mCurrentPosition:表示当前位置
    方法
    setIndicators():设置指示器的图片(必须在setData前调用)
    setData():根据数据,生成对应的轮播图
    setIndicator():设置指示器和文字内容
    onPageSelected()onPageScrollStateChanged():利用ViewPager的滚动监听,实现了上面的思路。onPageSelected()中根据ViewPager中显示的位置,改变mCurrentPosition的值,然后在onPageScrollStateChanged()中根据mCurrentPosition重新设置页面(这里的setCurrentItem没有动画效果)。
    getImageView():根据URL生成Viewpager中对应的各个View根据实际的图片加载框架来生成,这里使用了Picasso实现了网络图片的加载),看看getImageView()中调用的代码

        /**
         * 得到轮播图的View
         * @param context
         * @param url
         * @return
         */
        public static View getImageView(Context context, String url) {
            RelativeLayout rl = new RelativeLayout(context);
            //添加一个ImageView,并加载图片
            ImageView imageView = new ImageView(context);
            RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
                    RelativeLayout.LayoutParams.MATCH_PARENT,
                    RelativeLayout.LayoutParams.MATCH_PARENT);
            imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            imageView.setLayoutParams(layoutParams);
            //使用Picasso来加载图片
            Picasso.with(context).load(url).into(imageView);
            //在Imageview前添加一个半透明的黑色背景,防止文字和图片混在一起
            ImageView backGround = new ImageView(context);
            backGround.setLayoutParams(layoutParams);
            backGround.setBackgroundResource(R.color.cycle_image_bg);
            rl.addView(imageView);
            rl.addView(backGround);
            return rl;
        }
    
    <color name="cycle_image_bg">#44222222</color>
    

    代码很简单,创建了一个显示图片的布局,先在布局中添加了需要显示的图片,然后加了个半透明的图,防止显示时文字和图片中白色的部分重叠在一起,导致看不清文字。

    • 在Acitivty中使用
      轮子打造好了,不拿出来溜一溜?
    public class MainActivity extends AppCompatActivity {
    
        /**
         * 模拟请求后得到的数据
         */
        List<Info> mList = new ArrayList<>();
    
        /**
         * 轮播图
         */
        CycleViewPager mCycleViewPager;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            initData();
            initView();
        }
    
        /**
         * 初始化数据
         */
        private void initData() {
            mList.add(new Info("标题1", 
                  "http://img2.3lian.com/2014/c7/25/d/40.jpg"));
            mList.add(new Info("标题2", 
                  "http://img2.3lian.com/2014/c7/25/d/41.jpg"));
            mList.add(new Info("标题3", 
                 "http://imgsrc.baidu.com/forum/pic/item/b64543a98226cffc8872e00cb9014a90f603ea30.jpg"));
            mList.add(new Info("标题4", 
                 "http://imgsrc.baidu.com/forum/pic/item/261bee0a19d8bc3e6db92913828ba61eaad345d4.jpg"));
        }
    
        /**
         * 初始化View
         */
        private void initView() {
            mCycleViewPager = (CycleViewPager) findViewById(R.id.cycle_view);
            //设置选中和未选中时的图片
            mCycleViewPager.setIndicators(R.mipmap.ad_select, R.mipmap.ad_unselect);
            //设置轮播间隔时间
            mCycleViewPager.setDelay(2000);
            mCycleViewPager.setData(mList, mAdCycleViewListener);
        }
    
        /**
         * 轮播图点击监听
         */
        private CycleViewPager.ImageCycleViewListener mAdCycleViewListener = 
                      new CycleViewPager.ImageCycleViewListener() {
    
            @Override
            public void onImageClick(Info info, int position, View imageView) {
    
                if (mCycleViewPager.isCycle()) {
                    position = position - 1;
                }
                Toast.makeText(MainActivity.this, info.getTitle() +
                     "选择了--" + position, Toast.LENGTH_LONG).show();
            }
        };
    }
    

    使用起来也是很简单的,只要设置下图片、数据、点击监听就可以了。(之前贴过MainActivity.getImageView()方法了,这里就不贴了)
    放到自己的项目中?
    只需要调下布局,根据自己的图片加载框架改下getImageView(或者也可以直接用我的),然后把CycleViewPager中的Info改成自己的Model就可以了。

    源码地址:Github

    以上有错误之处,感谢指出

    投稿给鸿洋大神后,大神帮我测了下,发现这轮播图在MOTO nexus 6上,快速滑动会卡住,然后跳跃,类似应该在小米5上也会复现。
    大神的建议:无限循环banner,不如使用MAX页,然后currentItem=MAX/2来做。

    (这段时间忙着找工作,就先搁下了,有兴趣的同学可以先尝试下。)

    相关文章

      网友评论

      • lfork:清晰易懂
      • 899e2d74bdeb:手动向左滑动,会出现短暂的空白,这个问题怎么解决
        899e2d74bdeb:@带心情去旅行 每次都会,没有加懒加载,快速的向左滑的时候很明显,向右滑动就正常;
        除非你稍等一下再向左滑动,就不会有空白了,
        带心情去旅行:@哎疯 会不会是你加了懒加载
        带心情去旅行:@哎疯 每次都会吗?
      • WangGavin:大哥,你用handler考虑没考虑内存泄漏?
      • Cancrileo:快速滑动会卡顿 有没有什么解决的办法
        带心情去旅行:结尾有大概说了下解决的方法,你可以尝试下
      • Clendy:大神的这种实现无限轮播的方案是通过回滚吗?如果是这样那么有可能出现回滚时的页面跳动问题,以前就遇到过...
        Clendy:其实就是文末鸿大神提到的那种方式...
        Clendy:@带心情去旅行 ViewPager实现无限轮播有2种方式:一种是你现实的这种回滚方式;另外一种就是设置item数量为无限大(Integer.MAX)个。第二种方式可以有效解决回滚时的跳动问题,而且轮播中感觉不到任何缝隙,楼主可以尝试下。
        带心情去旅行:@Clendy 确实存在一些问题,在最后有指出
      • 小人物灌篮:很nice的轮子,可以转载么
        带心情去旅行: @小人物灌篮 可以的

      本文标题:【Android】造轮子:轮播图

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