自定义Android图片轮播控件

说到轮播图,想必大家都不陌生。常见的APP都会有一个图片轮播的区域。之前使用过轮播图,最近项目又一次用到了,就把原来的代码照搬过来,结果由于数据结构的差异和照搬使有些代码的疏忽,调试了很久才让原本已经OK的轮播图再次运转起来。所以决定将这个轮播图模块化,做成一个可以通用的组件,方便以后使用。

通过总结网络上各位大神的思路,这里本着学习的态度自定义一个可以无限循环轮播,并且支持手势滑动的轮播图控件。

轮播效果

自定义控件###

自定义View的实现方式大概可以分为三种,自绘控件、组合控件、以及继承控件。这里的实现方式是用第二种方式,组合控件。

组合控件,顾名思义,就是利用Android原生控件通过xml文件布局重定义为自己所需要的UI,然后就此布局文件的控件实现自身需要的功能。

  • 定义布局文件

carousel_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <android.support.v4.view.ViewPager
        android:id="@+id/gallery"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:unselectedAlpha="1"></android.support.v4.view.ViewPager>
    <LinearLayout
        android:id="@+id/CarouselLayoutPage"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center|bottom"
        android:gravity="center"
        android:orientation="horizontal"
        android:padding="10dip"></LinearLayout>
</FrameLayout>

这里定义一个Viewpager(用于放置图片),并在下方定义一个横向的LinearLayout(用于放置隋图滚动的小圆点)

  • 加载布局文件到View

接下来的步骤,就是将这个xml布局文件结合到需要实现的自定义View当中。

一般,我们在实现自定义控件时,都会继承某一个View(比如LinearLayout,Button或者直接就是View及ViewGroup)。
然后,就需要实现其相应的构造方法,构造方法一般会有3个

    public BannerView(Context context) {
        super(context);
        this.context = context;
    }
    public BannerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }
    public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
    }

建议是这三个构造方法都实现一下。原因可以看看这篇文章
为什么要实现全部三个构造方法

加载布局文件到当前自定义view中

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        View view = LayoutInflater.from(context).inflate(R.layout.carousel_layout, null);
        this.viewPager = (ViewPager) view.findViewById(R.id.gallery);
        this.carouselLayout = (LinearLayout) view.findViewById(R.id.CarouselLayoutPage);
        IndicatorDotWidth = ConvertUtils.dip2px(context, IndicatorDotWidth);
        this.viewPager.addOnPageChangeListener(this);
        addView(view);
    }

可以在onFinishInflate这个方法中,加载上述布局文件,并添加到当前view当中.这里这个关于加载view的逻辑,放到构造函数中实现也是可以的。至于放在两个地方的区别我们可以从API文档看出

protected void onFinishInflate ()
Added in API level 1
Finalize inflating a view from XML. This is called as the last phase of inflation, after all child views have been added.
Even if the subclass overrides onFinishInflate, they should always be sure to call the super method, so that we get called.

大概意思就是这个方法会在xml文件所有内容“填充”完成后触发。说白了就是,如果在这个方法里实现了xml的加载,那么在Activity中用java代码new出一个当前自定义View对象时,将没有内容(因为new对象的时候,执行了构造方法,而构造方法中没有加载内容)。其实,大部分情况下,自定义的控件,都会按照完全路径放到xml布局文件中中使用(如本文使用的情况),不会说在代码中new一个,所以,这个addview(view)的逻辑在哪里实现,可以根据实际情况决定(当然,这只是我一时的理解)。

  • 初始化

接下来,就需要做一些初始化的工作。

首先可以根据,内容可以绘制出轮播图指示器(即随图滑动的小圆点)

 carouselLayout.removeAllViews();
        if (adapter.isEmpty()) {
            return;
        }
        int count = adapter.getCount();
        showCount = adapter.getCount();
        //绘制切换小圆点
        for (int i = 0; i < count; i++) {
            View view = new View(context);
            if (currentPosition == i) {
                view.setPressed(true);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            } else {
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            }
            view.setBackgroundResource(R.drawable.carousel_layout_dot);
            carouselLayout.addView(view);
        }

这里看一下这个carousel_layout_dot.xml 布局文件

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_selected="true">
        <shape  android:shape="oval">
            <solid android:color="#eb6100"></solid>
        </shape>
    </item>
    <item>
        <shape  android:shape="oval">
            <solid android:color="@android:color/transparent"></solid>
            <stroke android:width="1dp" android:color="#FFF"> </stroke>
        </shape>
    </item>
</selector>

做过Button点击效果的同学,对这种模式一定很熟悉。通过view的当前状态,设置不同的色值,可以呈现丰富的视觉效果。这里对小圆点也是一样,选中项设置了高亮的颜色。
通过修改这个文件,可以实现自定义小圆点的效果。列如可以将圆点修改为横线,或者将小圆点切换为图片等,这完全可以根据实际需求决定。

这里使用到了ViewPager,那么Adapter是必不可少了了。这里主要需要实现其selected方法

  @Override
    public void onPageSelected(int position) {
        currentPosition = position;
        int count = carouselLayout.getChildCount();
        for (int i = 0; i < count; i++) {
            View view = carouselLayout.getChildAt(i);
            if (position % showCount == i) {
                view.setSelected(true);
                //当前位置的点要绘制的较大一点
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            } else {
                view.setSelected(false);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            }
        }
    }

  • 轮播实现

其实,轮播的实现,思路很简单,通过一个独立的线程,不断更改当前位置position,然后使用handler在UI线程中通过ViewPager的SetCurrentItem(position)方法即可实现图片轮播效果。

这里有三点需要注意:
1.选择合适的定时器在适当的位置开始定时任务
2.当用户手指滑动时,如何处理独立线程中对当前位置的更改
3.若要实现无限循环滑动时,滑到第一页和最后一页时如何处理

带着这三个问题,可以看一下完整的代码(这部分代码拆开之后叙述起来会有点乱,所以就给出全部代码)

public class BannerView extends FrameLayout implements ViewPager.OnPageChangeListener {
    private Context context;
    private static final int MSG = 0X100;
    /**
     * 轮播图最大数
     */
    private int totalCount = Integer.MAX_VALUE;
    /**
     * 当前banner需要显式的数量
     */
    private int showCount;
    private int currentPosition = 0;
    private ViewPager viewPager;
    private LinearLayout carouselLayout;
    private Adapter adapter;
    /**
     * 轮播切换小圆点宽度默认宽度
     */
    private static final int DOT_DEFAULT_W = 5;
    /**
     * 轮播切换小圆点宽度
     */
    private int IndicatorDotWidth = DOT_DEFAULT_W;
    /**
     * 用户是否干预
     */
    private boolean isUserTouched = false;
    /**
     * 默认的轮播时间
     */
    private static final int DEFAULT_TIME = 3000;
    /**
     * 设置轮播时间
     */
    private int switchTime = DEFAULT_TIME;
    /**
     * 轮播图定时器
     */
    private Timer mTimer = new Timer();
    
    public BannerView(Context context) {
        super(context);
        this.context = context;
    }
    public BannerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
    }
    public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
    }
    private void init() {
        viewPager.setAdapter(null);
        carouselLayout.removeAllViews();
        if (adapter.isEmpty()) {
            return;
        }
        int count = adapter.getCount();
        showCount = adapter.getCount();
        //绘制切换小圆点
        for (int i = 0; i < count; i++) {
            View view = new View(context);
            if (currentPosition == i) {
                view.setPressed(true);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            } else {
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            }
            view.setBackgroundResource(R.drawable.carousel_layout_dot);
            carouselLayout.addView(view);
        }
        viewPager.setAdapter(new ViewPagerAdapter());
        viewPager.setCurrentItem(0);
        this.viewPager.setOnTouchListener(new OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                    case MotionEvent.ACTION_MOVE:
                    //有用户滑动事件发生
                        isUserTouched = true;
                        break;
                    case MotionEvent.ACTION_UP:
                        isUserTouched = false;
                        break;
                }
                return false;
            }
        });
        //以指定周期和岩石开启一个定时任务
        mTimer.schedule(mTimerTask, switchTime, switchTime);
    }

    //设置adapter,这个方法需要再使用时设置
    public void setAdapter(Adapter adapter) {
        this.adapter = adapter;
        if (adapter != null) {
            init();
        }
    }
    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        View view = LayoutInflater.from(context).inflate(R.layout.carousel_layout, null);
        this.viewPager = (ViewPager) view.findViewById(R.id.gallery);
        this.carouselLayout = (LinearLayout) view.findViewById(R.id.CarouselLayoutPage);
        IndicatorDotWidth = ConvertUtils.dip2px(context, IndicatorDotWidth);
        this.viewPager.addOnPageChangeListener(this);
        addView(view);
    }
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
       
    }
    @Override
    public void onPageSelected(int position) {
        currentPosition = position;
        int count = carouselLayout.getChildCount();
        for (int i = 0; i < count; i++) {
            View view = carouselLayout.getChildAt(i);
            if (position % showCount == i) {
                view.setSelected(true);
                //当前位置的点要绘制的较大一点,高亮显示
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
                        IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            } else {
                view.setSelected(false);
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
                params.setMargins(IndicatorDotWidth, 0, 0, 0);
                view.setLayoutParams(params);
            }
        }
    }
    @Override
    public void onPageScrollStateChanged(int state) {
        
    }
    class ViewPagerAdapter extends PagerAdapter {
        @Override
        public int getCount() {
            return totalCount;
        }
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return view == object;
        }
        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            position %= showCount;
            View view = adapter.getView(position);
            container.addView(view);
            return view;
        }
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }
        @Override
        public int getItemPosition(Object object) {
            return super.getItemPosition(object);
        }
        @Override
        public void finishUpdate(ViewGroup container) {
            super.finishUpdate(container);
            int position = viewPager.getCurrentItem();
            if (position == 0) {
                position = showCount;
                viewPager.setCurrentItem(position, false);
            } else if (position == totalCount - 1) {
                position = showCount - 1;
                viewPager.setCurrentItem(position, false);
            }
        }
    }
    private TimerTask mTimerTask = new TimerTask() {
        @Override
        public void run() {
        //用户滑动时,定时任务不响应
            if (!isUserTouched) {
                currentPosition = (currentPosition + 1) % totalCount;
                handler.sendEmptyMessage(MSG);
            }
        }
    };
    public void cancelTimer() {
        if (this.mTimer != null) {
            this.mTimer.cancel();
        }
    }
    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == MSG) {
                Log.e("Pos", "the position is " + currentPosition);
                if (currentPosition == totalCount - 1) {
                    viewPager.setCurrentItem(showCount - 1, false);
                } else {
                    viewPager.setCurrentItem(currentPosition);
                }
            }
        }
    };
    /**
    *可自定义设置轮播图切换时间,单位毫秒
     * @param switchTime millseconds
     */
    public void setSwitchTime(int switchTime) {
        this.switchTime = switchTime;
    }
    /**
     * @param indicatorDotWidth
     */
    public void setIndicatorDotWidth(int indicatorDotWidth) {
        IndicatorDotWidth = indicatorDotWidth;
    }
    public interface Adapter {
        boolean isEmpty();
        View getView(int position);
        int getCount();
    }
}

这里将totalCount的值设置为一个很大的值(这个貌似是实现无限轮播的一个取巧的方法,网上大部分实现都是这样),并将这个值作为ViewPager的个数。每次位置更改时,通过取余数,避免了数组越界,同时巧妙的实现了无限循环轮播效果。

  • 测试效果
public class BannerViewActivity extends Activity {
    private ListView listview;
    private List<String> datas;
    private List<String> banners;
    private View headView;
    private BannerView carouselView;
    private Context mContext;
    private LayoutInflater mInflater;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = this;
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.carouse_layout);
        InitView();

    }

    private void InitView() {
        InitDatas();
        mInflater = LayoutInflater.from(this);
        headView = mInflater.inflate(R.layout.carouse_layout_header, null);
        carouselView = (BannerView) headView.findViewById(R.id.CarouselView);
        //这里考虑到不同手机分辨率下的情况
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, ConvertUtils.dip2px(this, 200));
        carouselView.setLayoutParams(params);
        carouselView.setSwitchTime(2000);
        carouselView.setAdapter(new MyAdapter());
        listview = V.f(this, R.id.list);
        listview.addHeaderView(headView);
        ArrayAdapter<String> myAdapter = new ArrayAdapter<String>(mContext, android.R.layout.simple_expandable_list_item_1, datas);
        listview.setAdapter(myAdapter);

    }

    /**
     * 设定虚拟数据
     */
    private void InitDatas() {
        datas = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            datas.add("the Item is " + i);
        }
        //图片来自百度
        banners = Arrays.asList("http://img1.imgtn.bdimg.com/it/u=2826772326,2794642991&fm=15&gp=0.jpg",
                "http://img15.3lian.com/2015/f2/147/d/39.jpg",
                "http://img1.3lian.com/2015/a1/107/d/65.jpg",
                "http://img1.3lian.com/2015/a1/93/d/225.jpg",
                "http://img1.3lian.com/img013/v4/96/d/44.jpg");
    }

//这里可以按实际需求做调整,在适当的位置可停止轮播,节省资源
    @Override
    protected void onPause() {
        super.onPause();
        if (carouselView != null) {
            carouselView.cancelTimer();
        }
    }

    private class MyAdapter implements BannerView.Adapter {

        @Override
        public boolean isEmpty() {
            return banners.size() > 0 ? false : true;
        }

        @Override
        public View getView(final int position) {
            View view = mInflater.inflate(R.layout.item, null);
            ImageView imageView = (ImageView) view.findViewById(R.id.image);           Picasso.with(mContext).load(banners.get(position)).into(imageView);
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    T.showShort(mContext,"Now is "+position);
                }
            });
            return view;
        }

        @Override
        public int getCount() {
            return banners.size();
        }
    }
}

carouse_layout_header.xml

<?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="match_parent"
    android:orientation="vertical">
    <com.example.dreamwork.activity.CarouselView.BannerView
        android:id="@+id/CarouselView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">   </com.example.dreamwork.activity.CarouselView.BannerView>
</LinearLayout>

这里BannerViewActivity的布局文件就是一个ListView,这里代码就不在贴出,将BannView自定义控件作为其头部添加到ListView上即可。还可以很灵活的设置轮播图的切换时间,最后设置其Adapter即可。当然,这里 很简单的自定义了一个List存放图片地址,作为测试。实际开发中,可选取接口返回的后台配置的图片地址。

这里在说一下关于这个轮播图高度的设置,Android手机的碎片化,导致现在市场上各种分辨率手机都存在,适配起来就显得特别纠结,这里的处理方法很值得借鉴

//这里考虑到不同手机分辨率下的情况
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.MATCH_PARENT, ConvertUtils.dip2px(this, 200));
        carouselView.setLayoutParams(params);

dip2px,顾名思义就是根据当前手机分辨率将dp转换为px,这类通用的方法,想必大家都很熟悉,这里就是使用这个方法,设定高度为200dp,然后按照不同手机的分辨率再去分配,这中思路不但在这里,很多地方都可以使用。


好了,这样定义一个轮播图控件后,以后使用时只需要在xml文件中定义BannerView,然后根据业务数据设置其Adapter即可,不必在重新复制粘贴一大堆代码;关于这个图片轮播控件的学习就到这里。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • [我的100个成长感悟]@kiki-083 2015年5月22日 今日话题:你认为自己是伸手党吗?你的行动力强吗?...
    吉吉kiki阅读 296评论 0 1
  • 初到成都,屋外花团锦簇,俨如春天。而室内却冷如冰窖。没有生活经验,时常被淋雨。这座口口相传的都市因为不适应和糟糕的...
    千年胡杨阅读 426评论 2 1
  • 大宝是个聪明的孩子,基本上说过的东西都能马上有反馈,即使偶尔忘记了,点拨一下就马上通了。上次说的清辅音字母p, t...
    jiajiasoso阅读 1,246评论 0 3