【Android 自定义View】封装好的轮播图组件,可直接使用(附demo)

如果项目急用,可直接下载demo!非常容易使用!轮播组件都已经封装好!点我下载

一、背景

在做Android产品详情页的时候,我也造了一次轮子——把轮播图自己实现了一遍。经过产品经理的一次又一次的改版要求,我认为这个轮播的实现还是不错的。在完成需求的同时,我也规避掉了潜在的危险,比如内存泄漏问题。其实,一个简单的轮播图,要想真正应用到实际上线项目中,还是有很多细节值得仔细斟酌的,并且轮播流程也有很多说道的。下面来看下效果图(gif的问题,看起来可能卡顿,实际轮播很流畅):

 

二、ViewPager如何无限循环起来?

重点是在ViewPager的adapter里。原理是这样:给ViewPager设置无限多个item(不用担心ViewPager的会内存溢出,因为它有缓存机制!),如果实际只有三张图,等滑到第四个的时候,就是把第一张图add到ViewPager里。这样我们只负责给ViewPager添加item就好了,具体的内存操作交给ViewPager去实现就好了。

@Override
    public Object instantiateItem(ViewGroup container, int position) {
        position %= items.size();
        if (position < 0) {
            position = items.size() + position;
        }
        View view = items.get(position);
        ViewParent viewParent = view.getParent();
        if (viewParent != null) {
            ViewGroup parent = (ViewGroup) viewParent;
            parent.removeView(view);
        }
        container.addView(view);
        return view;
    }

三、ViewPager如何动起来?

viewPager使用起来大家肯定驾轻就熟了,可如何让ViewPager滑动起来呢?

本文使用的是Handler的sendMessageDelayed方法,来让以下代码块每2s执行一次:

 

//ViewPager轮播
currentViewPagerItem++;
fragment.bannerDelegate.banner.setCurrentItem(currentViewPagerItem);

这样还没有完,少了ViewPager的OnPageChangeListener,ViewPager也是轮播不起来:

 

如图,OnPageChangeListener方法有两个重要方法:onPageSelected和onPageScrollStateChanged方法。

○ onPageSelected方法在每一个page选中时回调,在这里去更新currentViewPagerItem的值。

○ onPageScrollChanged方法紧跟着onPageSelected回调,这里去通过handler的sendMessageDelayed延时2s发送消息

 

四、手指触摸时停止轮播,手指离开时恢复轮播

通过上文的描述可知,viewPager的轮播其实是通过handler的handleMessage中去把viewPager移到下一个位置。那么要想让手指触摸时停止轮播,只需发一个空message;在手指放开时,再调用sendMessageDelayed方法。

 

五、避免内存泄漏

在任何使用handler的地方,都应该注意是否有内存泄漏的风险。因为如果handler在Activity finish掉之后,还陆续需要handleMessage时,Activity是不会被成功销毁的,如果多个Activity都无法被销毁,就有可能产生内存泄漏。

通常避免handler内存泄漏的方法有两种:

○ 把handler中持有的fragment对象设置为弱引用

○ 在fragment的onDestroy方法里handler.removeCallbacksAndMessages(null)

看过handler源码的人都知道,通过handler 来post runnable或者sendEmptyMessage,其实都会转成Message,放到消息队列里,所以清空消息队列就意味着把Handler重置了。

本文采用的是第一种方法。

 

六、具体实现步骤

【1】初始化ViewPager

private void initBannerView(ProductInfo productInfo) {
        RelativeLayout bannerLayout = (RelativeLayout) mRootView.findViewById(R.id.banner_layout);
        bannerLayout.getLayoutParams().height = CommonUtil.getScreenWidth(context) * 247 / 375;
        //得到ViewPager的数据源
        List<View> items = new ArrayList<>();
        final int size = productInfo.getImgList().size();
        for (int i = 0; i < size; i++) {
            if (!isValidUrl(productInfo.getImgList().get(i))) {
                continue;
            }
            View view = inflater.inflate(R.layout.vacation_detail_banner_item, null);
            //图片
            ImageView img = (ImageView) view.findViewById(R.id.img);
            ImageLoaderHelper.displaySmallImage(img, productInfo.getImgList().get(i).getUrlList().get(0).getValue());
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (size < 2) {
                        return;
                    }
                    int productId = VacationDetailUtils.getProductId();
                    int saleCityId = VacationDetailUtils.getSaleCityId();
                    int departCityId = VacationDetailUtils.getDepartCityId();
                    String url = GetEnvH5URL() + "vacations/tour/detail_picture_list?productId=" + productId + "&saleCityId=" + saleCityId + "&departCityId=" + departCityId;
                    CtripH5Manager.openUrl(context, url, null);
                    //埋点
                    VacationDetailBuryPoint.LogAction("picture-more");
                }
            });

            items.add(view);
        }
        if (size == 0) {
            View view = inflater.inflate(R.layout.vacation_detail_banner_item, null);
            ImageView img = (ImageView) view.findViewById(R.id.img);
            ImageLoaderHelper.displaySmallImage(img, "https://pic.c-ctrip.com/vacation_v2/h5/group_travel/pic_none.png");
            items.add(view);
        }
        //设置ViewPager的adapter
        BannerPagerAdapter adapter = new BannerPagerAdapter(items);
        banner = (ViewPager) mRootView.findViewById(R.id.banner);
        banner.setAdapter(adapter);
        //设置ViewPager切换时间
        VacationDetailUtils.controlViewPagerSpeed(context, banner, 1000);
        //当手指在触摸Banner时,暂停轮播
        banner.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int i, float v, int i1) {

            }

            @Override
            public void onPageSelected(int position) {
                //更新ViewPager的item位置
                mHandler.sendMessage(Message.obtain(mHandler, BannerHandler.MSG_PAGE_CHANGED, position, 0));
            }

            @Override
            public void onPageScrollStateChanged(int state) {
                switch (state) {
                    case ViewPager.SCROLL_STATE_DRAGGING:
                        mHandler.sendEmptyMessage(BannerHandler.MSG_KEEP_SILENT);
                        break;
                    case ViewPager.SCROLL_STATE_IDLE:
                        mHandler.sendEmptyMessageDelayed(BannerHandler.MSG_UPDATE_IMAGE, BannerHandler.MSG_DELAY);
                        break;
                }
            }
        });

        //ViewPager初始位置
        banner.setCurrentItem(BannerDelegate.MAX_VALUE / 2);

        //开始轮播
        mRootView.postDelayed(new Runnable() {
            @Override
            public void run() {
                startScroll();
            }
        }, 200);
    }//得到ViewPager的数据源
        List<View> items = new ArrayList<>();
        final int size = productInfo.getImgList().size();
        for (int i = 0; i < size; i++) {
            if (!isValidUrl(productInfo.getImgList().get(i))) {
                continue;
            }
            View view = inflater.inflate(R.layout.vacation_detail_banner_item, null);
            //图片
            ImageView img = (ImageView) view.findViewById(R.id.img);
            ImageLoaderHelper.displaySmallImage(img, productInfo.getImgList().get(i).getUrlList().get(0).getValue());
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (size < 2) {
                        return;
                    }
                    int productId = VacationDetailUtils.getProductId();
                    int saleCityId = VacationDetailUtils.getSaleCityId();
                    int departCityId = VacationDetailUtils.getDepartCityId();
                    String url = GetEnvH5URL() + "vacations/tour/detail_picture_list?productId=" + productId + "&saleCityId=" + saleCityId + "&departCityId=" + departCityId;
                    CtripH5Manager.openUrl(context, url, null);
                    //埋点
                    VacationDetailBuryPoint.LogAction("picture-more");
                }
            });

            items.add(view);
        }
        if (size == 0) {
            View view = inflater.inflate(R.layout.vacation_detail_banner_item, null);
            ImageView img = (ImageView) view.findViewById(R.id.img);
            ImageLoaderHelper.displaySmallImage(img, "https://pic.c-ctrip.com/vacation_v2/h5/group_travel/pic_none.png");
            items.add(view);
        }
        //设置ViewPager的adapter
        BannerPagerAdapter adapter = new BannerPagerAdapter(items);
        banner = (ViewPager) mRootView.findViewById(R.id.banner);
        banner.setAdapter(adapter);
        //设置ViewPager切换时间
        VacationDetailUtils.controlViewPagerSpeed(context, banner, 1000);
        //当手指在触摸Banner时,暂停轮播
        banner.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int i, float v, int i1) {

            }

            @Override
            public void onPageSelected(int position) {
                //更新ViewPager的item位置
                mHandler.sendMessage(Message.obtain(mHandler, BannerHandler.MSG_PAGE_CHANGED, position, 0));
            }

            @Override
            public void onPageScrollStateChanged(int state) {
                switch (state) {
                    case ViewPager.SCROLL_STATE_DRAGGING:
                        mHandler.sendEmptyMessage(BannerHandler.MSG_KEEP_SILENT);
                        break;
                    case ViewPager.SCROLL_STATE_IDLE:
                        mHandler.sendEmptyMessageDelayed(BannerHandler.MSG_UPDATE_IMAGE, BannerHandler.MSG_DELAY);
                        break;
                }
            }
        });

        //ViewPager初始位置
        banner.setCurrentItem(BannerDelegate.MAX_VALUE / 2);

        //开始轮播
        mRootView.postDelayed(new Runnable() {
            @Override
            public void run() {
                startScroll();
            }
        }, 200);
    }
public void startScroll() {
        //开始轮播
        if (mHandler.hasMessages(BannerHandler.MSG_UPDATE_IMAGE)) {
            mHandler.removeMessages(BannerHandler.MSG_UPDATE_IMAGE);
        }
        mHandler.sendEmptyMessageDelayed(BannerHandler.MSG_UPDATE_IMAGE, 500);
    }

总结一下ViewPager的初始化:

 

首先,设置ViewPager的数据源;

其次,设置ViewPager的adapter;

然后,设置ViewPager的滑动速度;

接着,设置ViewPager的touch事件,使得手指放在ViewPager上时,滚动停止;手指离开时,滚动继续;

最后,通过handler发送消息,使ViewPager轮播起来。

另外,还应注意:ViewPager的onPageChangeListener的几个回调方法的回调时机。在setCurrentItem之后,onPageSelected会先回调,然后onPageScrollChanged方法会回调。所以,onPageSelected时,把viewpager的位置更新;onPageChangeListener时,把viewPager的位置加1,在setCurrentItem

【2】设置ViewPager的adapter

public class BannerPagerAdapter extends PagerAdapter {
    private final static String TAG = VacationDetailUtils.TAG;
    private List<View> items;

    public BannerPagerAdapter(List<View> items) {
        this.items = items;
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        //取模,使得ViewPager的view能依次轮询下去
        position %= items.size();
        if (position < 0) {
            position = items.size() + position;
        }
        View view = items.get(position);
        ViewParent viewParent = view.getParent();
        if (viewParent != null) {
            ViewGroup parent = (ViewGroup) viewParent;
            parent.removeView(view);
        }
        container.addView(view);
        return view;
    }

    @Override
    public int getCount() {
        if (items.size() > 1) {
            return BannerDelegate.MAX_VALUE;
        }
        return items.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object o) {
        return view == (View) o;
    }

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

    }
}
取模,使得ViewPager的view能依次轮询下去
        position %= items.size();
        if (position < 0) {
            position = items.size() + position;
        }
        View view = items.get(position);
        ViewParent viewParent = view.getParent();
        if (viewParent != null) {
            ViewGroup parent = (ViewGroup) viewParent;
            parent.removeView(view);
        }
        container.addView(view);
        return view;
    }

    @Override
    public int getCount() {
        if (items.size() > 1) {
            return BannerDelegate.MAX_VALUE;
        }
        return items.size();
    }

    @Override
    public boolean isViewFromObject(View view, Object o) {
        return view == (View) o;
    }

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

    }
}

【3】设置handler

public class BannerHandler extends Handler {
    private String TAG = VacationDetailUtils.TAG;
    private WeakReference<VacationDetailFragment> mWeakReference;
    //轮播间隔时间
    public static final int MSG_DELAY = 3000;
    //轮播
    public static final int MSG_UPDATE_IMAGE = 1;
    //暂停轮播
    public static final int MSG_KEEP_SILENT = 2;
    //恢复轮播
    public static final int MSG_BREAK_SILENT = 3;
    //记录最新的页号
    public static final int MSG_PAGE_CHANGED = 4;
    private int currentViewPagerItem = BannerDelegate.MAX_VALUE / 2;

    public BannerHandler(VacationDetailFragment fragment) {
        mWeakReference = new WeakReference<VacationDetailFragment>(fragment);
    }

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        VacationDetailFragment fragment = mWeakReference.get();
        if (fragment == null || fragment.bannerDelegate == null || fragment.bannerDelegate.mHandler == null) {
            return;
        }
        //当队列中有消息时,移除消息
        if ((fragment.bannerDelegate.mHandler.hasMessages(MSG_UPDATE_IMAGE)) && (currentViewPagerItem != BannerDelegate.MAX_VALUE / 2)) {
            fragment.bannerDelegate.mHandler.removeMessages(MSG_UPDATE_IMAGE);
        }
        switch (msg.what) {
            case MSG_UPDATE_IMAGE:
                //ViewPager轮播
                currentViewPagerItem++;
                fragment.bannerDelegate.banner.setCurrentItem(currentViewPagerItem);
                fragment.bannerDelegate.mHandler.sendEmptyMessageDelayed(MSG_UPDATE_IMAGE, MSG_DELAY);
                break;
            case MSG_KEEP_SILENT:
                //不发消息
                break;
            case MSG_BREAK_SILENT:
                //恢复轮播
                fragment.bannerDelegate.mHandler.sendEmptyMessageDelayed(MSG_UPDATE_IMAGE, MSG_DELAY);
                break;
            case MSG_PAGE_CHANGED:
                currentViewPagerItem = msg.arg1;
                break;
        }
    }
}

 

接下来,我会附上demo(点击查看demo)。如果觉得对你的开发有启发,烦请给上你的star。另外有任何问题,可以邮件或留言联系我,我的邮箱zhshan@ctrip.com。

 

~~~~~~~~~~~~~~~~~~~华丽丽的分割线~~~~~~~~~~~~~~~~~~~~~~~~

在这一版轮播图上线之后,暴露出了一些问题。我这边在改版之后,就轮播流程重新强调一下!

 

 

轮播流程

这里为什么要介绍一下轮播流程?如果不了解轮播的流程,很有可能影响轮播图的展示效果。比如,在第一版上线的详情页就存在以下几个问题:

(1)由于详情页使用了缓存机制,进入页面首先加载缓存,等到接口请求回来,再去重新刷新UI。因为这样,我的轮播图会先初始化一次,等到接口请求回来再请求一次。出现的现象就是,轮播图首先展示出来,过大于1秒后,轮播图又被销毁,重新初始化(这尼玛,很坑爹啊,那还用缓存干啥)。

(2)进入页面就开始轮播,这导致轮播的前两幅图片非常快!

在项目不急时,我就开始了对详情页的轮播图的优化,并总结了轮播图的轮播流程:

○ 在页面加载时,就去初始化轮播图ViewPager,并且在这个页面只进行这一次(与轮播无关)。

○ 在接口请求成功之后,只去更新数据源,然后notifyDataChanged(与轮播无关)。

○ startScroll方法在有数据之后就只调用一次,即使后面有数据更新,也不调用该方法(与轮播有关)。

○ 当接口返回数据与缓存数据一致时,不再去刷新轮播图(与轮播无关)。

 

~~~~~~~~~~~~~~~~~~~华丽丽的分割线~~~~~~~~~~~~~~~~~~~~~~~~

通过对轮播图的深入解读,我已经对轮播图的整个流程都很理解了,为了能给大家日常开发带来便捷,我特意进行了一下封装。大家日后需要轮播图组件,直接拿来用就好了!!

欢迎点击下载demo!!(点我!)

 

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值