自定义View之无限大图轮播ShufBanner

Posted Alex_MaHao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义View之无限大图轮播ShufBanner相关的知识,希望对你有一定的参考价值。

无限大图轮播–ShufBanner

轮播图作为一个app的宣传,展示等,往往占据着一个很重要的地位,大部分app都将其放在首页。那么通常的做法都是使用ViewPager,使其能够作用滑动,而无限轮播无外乎两种做法。
- 第一种是将ViewPager的size定义为无限大,定义其初始显示的位置为中间,这样的话因为左或者右都有很多的页面,所以造成了一种可以无限轮播的假象。同时因为ViewPager的特性,其只是加载当前显示page以及左和右的三个页面,不用担心OOM。
- 第二种是,将ViewPager的最前和最后的页面复制一份之后,分别加入到最后和最前,当ViewPager滑动到最前,或者最后的位置,直接跳转到相对应的位置。即可。

本次自定义的ShufBanner使用的是第二种方式。

首先看图
技术分享

他的使用方法很简单,如下即可在xml文件中添加控件

  <mahao.alex.shuffingbanner.ShufBanner
        android:id="@+id/shuf"
        android:layout_width="match_parent"
        android:layout_height="300dp"/>

public class MainActivity extends AppCompatActivity  {


    private ShufBanner mShufBanner;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        List<String> mImages = new ArrayList<>();
        mImages.add("http://pic.huodongjia.com/event/2015-11-18/event151612.jpg");
        mImages.add("http://pic.huodongjia.com/event/2016-03-17/event177131.jpg");
        mImages.add("http://pic.huodongjia.com/event/2015-12-03/event156106.jpg");


        mShufBanner = ((ShufBanner) findViewById(R.id.shuf));

        //启动轮播图
        mShufBanner.startShuf(mImages,true);


        //设置监听
        mShufBanner.setItemClcikListener(new ShufBannerClickListener() {
            @Override
            public void onClick(int position) {
                Toast.makeText(MainActivity.this, ""+position, Toast.LENGTH_SHORT).show();
            }
        });
    }

}

只需要启动轮播,设置监听即可。下面我们就开始封装。

  • 首先考虑布局,布局应该是一个ViewPager,下面是一个线性布局用来放置导航点。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:orientation="vertical"
    >


    <android.support.v4.view.ViewPager
        android:id="@+id/vp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <RelativeLayout
        android:layout_alignParentBottom="true"
        android:background="#6fff"
        android:layout_width="match_parent"
        android:layout_height="20dp">

        <LinearLayout
            android:id="@+id/ll_navigation"
            android:layout_centerInParent="true"
            android:orientation="horizontal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </RelativeLayout>

</RelativeLayout>

ViewPager没有什么疑问,而之所以在这里放置一个LinearLayout,是因为我们并不确定有多少页,所以我们需要根据实际情况在代码里添加。

  • 创建类shufBanner继承RelativeLayout,加载布局文件,初始化ViewPager,查找控件等等。

    public ShufBanner(Context context, AttributeSet attrs) {
        super(context, attrs);

        inflate(getContext(), R.layout.widget_shufbanner, this);

        initImageLoader();

        initViewPager();

    }

    /**
     * 初始化ViewPager
     */
    private void initViewPager() {
        mVp = ((ViewPager) findViewById(R.id.vp));
        //图片url地址
        mImages = new ArrayList<>();
        //imageView对象
        mImageViews = new ArrayList<>();

        mAdapter = new ShufBannerAdapter(getContext(), mImageViews);

        mVp.setAdapter(mAdapter);

        mVp.addOnPageChangeListener(this);

        mNavigationLayout = (LinearLayout) findViewById(R.id.ll_navigation);


    }

在这里我们首先将布局文件加载到ShufBanner中,其次初始化ViewPager,并设置其Adapter,添加滚动监听,监听事件的逻辑后面再说。同时查找到我们mNavigationLayout(导航点的父控件),我们看一下ShufBannerAdapter的代码

public class ShufBannerAdapter extends PagerAdapter {

    private Context context;
    private List<ImageView> mImageViews;

    public ShufBannerAdapter(Context context, List<ImageView> mImages) {
        this.context = context;
        this.mImageViews = mImages;

    }

    @Override
    public int getCount() {
        return mImageViews==null?0:mImageViews.size();
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        container.addView(mImageViews.get(position));

        return mImageViews.get(position);
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView(mImageViews.get(position));
    }

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

ShufBannerAdapter很简单,只是一个最基本的适配器,没有在里面处理什么逻辑。

基本的东西已经添加完,下面就是开始实现具体的逻辑,即完成startShuf()方法的逻辑。

 public void startShuf(List<String> urls, boolean isStartShuf) {

        if (urls.size() == 0 || urls == null) {
            return;
        }
        /**
         * 清楚数据
         */
        clearAllData();


        mImages.addAll(urls);
        /**
         * 保存图片地址
         */
        saveUrl();

        /**
         * 根据图片url创建imgView
         */
        createImageView();

        /**
         * 生成导航点
         */
        addPoint2Navigation();


        //开始刷新
        mAdapter.notifyDataSetChanged();
        mVp.setCurrentItem(1);

        //发送循环请求
        this.isStartShuf = isStartShuf;
        if (this.isStartShuf&&mImages.size()>3) {
            handler.sendEmptyMessageAtTime(1, 2000);
        }
    }

首先判断为null之类的都懂。下面清除数据,该方法主要目的是,当我们刷新数据时,我们必须把之前老的数据清楚,同时一些属性置空。

mImages.addAll(urls);将我们的图片集合添加到当前集合中,因为我们需要实现无限循环,即把最前和最后添加一张图片,所以我们通过saveUrl()方法添加数据。


    private void saveUrl() {

        String startImageUrl = mImages.get(0);
        String endImageUrl = mImages.get(mImages.size() - 1);

        mImages.add(0, endImageUrl);
        mImages.add(mImages.size(), startImageUrl);
    }

我们获取到我们图片url集合的第一张和最后一张,将第一张加到List末尾,将最后一张加到list的最前端。就好比,我们本来有三个url:1,2,3。经过saveUrl方法之后,url:_3,1,2,3,_1。

createImageView()方法根据url创建对应的imageView对象并存储到list集合中。

addPoint2Navigation()方法是根据当前的ImageView添加导航点,我们看一下他的详细实现过程:

 /**
     * 添加导航点到布局中
     */
    private void addPoint2Navigation() {
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT
        );
        int pointPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics());
        params.setMargins(pointPadding, 0, pointPadding, 0);
        for (int i = 0; i < mImageViews.size() - 2; i++) {
            ImageView point = new ImageView(getContext());
            point.setLayoutParams(params);
            point.setImageResource(R.drawable.select_navgation_point);
            if (i == 0) {
                point.setEnabled(true);
            } else {
                point.setEnabled(false);
            }
            mNavigationLayout.addView(point);
        }
    }

我们添加导航点的时候需要注意,因为我们添加了两张图片,所以我们在添加的实际导航点数 = 图片数-2;导航点使用的shape图形资源画出的,同时导航点因为有选中和不选中的两种状态,通过定义selector改变颜色变化,因为使用的是enabled属性来判断,所以在这里设置Enable属性。

 //发送循环请求
        this.isStartShuf = isStartShuf;
        if (this.isStartShuf&&mImages.size()>3) {
            handler.sendEmptyMessageAtTime(1, 2000);
        }

ok 下面开始进入到了轮播的逻辑了。

我们发送一个message给handler。

private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {

            int showPonit = mVp.getCurrentItem();

            showPonit = showPonit + 1;

            //getChildCount()获取的是当前存在的子类,销毁机制

           /* if (showPonit >= mImageViews.size()) {

                showPonit = 1;
            }*/

            //获得的是正常位置
            mVp.setCurrentItem(showPonit);

            if (isStartShuf) {
                sendEmptyMessageDelayed(1, 2000);
            }
        }
    };

在handler中,我们获取到当前的的页面,并+1。

判断是否超过了最大页面,如果超过了最大页面,就置为1,即我们的第一页,但后来又被我注掉,因为,我发现根本不需要。有点想多了。我们先继续往下看,设置页面,判断是否轮播,继续发送message。

到这里,我们的页面开始滚动了,但我们还需要处理逻辑,因为我们的页面现在是_3,1,2,3,_1.现在我们要加入判断,我们利用setCurrentItem(int,boolean)方法,到页面_1时,跳转到1,当页面在_3时,我们默认跳转到3。很明显,添加mVp.addOnPageChangeListener(this);

该方法有三个回调,分别是onPageScrolled:页面滚动距离,onPageSelected:页面选择,onPageScrollStateChanged:页面滚动状态的选择。

在这里我选择在onPageScrolled方法

  @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

        //是否滚动过渡完结
        if (positionOffset == 0) {
            if (position == 0) {
                mVp.setCurrentItem(mImageViews.size() - 2, false);
            }

            if (position == mImageViews.size() - 1) {
                mVp.setCurrentItem(1, false);
            }
        }
    }

最初,我是在onPageSelected(int position)方法中,添加跳转逻辑,但有一个问题。当我们调用
setCurrentItem()方法时,会立即回掉onPageSelected,但此时我们页面还是在滚动过渡中,如果此时3想_1滚动,但滚动到一半时,有调用了跳转到1的方法,切该方法无过渡,导致页面闪烁。所以我们在onPageScrolled判断,当滚动完毕,我们开始进行对应跳转。

在之前handler中,我添加了一个边界的判断,但在这里会发现,我们有了这个过渡跳转,所以不会存在边界。

页面已经滚动,下面开始就是要将页面的滚动和导航点联动,这个在onPageSelected中即可

    @Override
    public void onPageSelected(int position) {
        /**
         * 修改当前点击点的位置
         */
        changePosition(position);

        /**
         * 修改点的状态
         */
        changePointState(position);
    }

changePointState(position);为修改点的状态。

**
     * 改变导航点的状态
     *
     * @param position
     */
    private void changePointState(int position) {

        if (position == 0) {
            position = mImageViews.size() - 2;
        }
        if (position == mImageViews.size() - 1) {
            position = 1;
        }

        position = position - 1;

        for (int i = 0; i < mNavigationLayout.getChildCount(); i++) {
            if (i == position) {
                mNavigationLayout.getChildAt(i).setEnabled(true);

            } else {
                mNavigationLayout.getChildAt(i).setEnabled(false);
            }
        }
    }

这个就是判断一下是否是_3和_1,改变position对应的3和1,最后遍历修改状态。

changePosition(position);这个方法,是干什么呢。这就涉及到下一个关键,及ShufBanner的点击事件回调。

对于轮播图,往往都会有详情页,及跳转链接。如果我们对于每个ImageView都加入监听,这无疑很麻烦,我们还要判断具体的点击,以及_3,_1的问题。所以在这里我定义了一个字段

    /**
     * 点击事件的位置
     */
    private int position = -1;

该字段,用来保存当前ViewPager的position。默认等于-1。

   /**
     * 改变当前点击的点
     *
     * @param position
     */
    private void changePosition(int position) {
        if (position == 0) {
            position = mImageViews.size() - 2;
        }
        if (position == mImageViews.size() - 1) {
            position = 1;
        }

        position--;
        this.position = position;
    }

因为我需要接口回调

public interface ShufBannerClickListener {
    /**
     *
     * @param position -1 未设置任何点击
     */
    void onClick(int position);
}

我们如果直接记录position,我们自己内部悄悄添加了两个页面。这样,对于调用者来说,我传入的数据和返回的角标数据无法一一对应,这样肯定是不对。所以在这里改变点的状态。

这样应该差不多了,我们看一下调用方式


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        List<String> mImages = new ArrayList<>();
        mImages.add("http://pic.huodongjia.com/event/2015-11-18/event151612.jpg");
        mImages.add("http://pic.huodongjia.com/event/2016-03-17/event177131.jpg");
        mImages.add("http://pic.huodongjia.com/event/2015-12-03/event156106.jpg");


        mShufBanner = ((ShufBanner) findViewById(R.id.shuf));

        //启动轮播图
        mShufBanner.startShuf(mImages,true);


        //设置监听
        mShufBanner.setItemClcikListener(new ShufBannerClickListener() {
            @Override
            public void onClick(int position) {
                Toast.makeText(MainActivity.this, ""+position, Toast.LENGTH_SHORT).show();
            }
        });
    } 

Over。

总结:
1. 大图轮播实现的原理:1,2,3三个页面,我们最后和之前各添加一个页面,_3,1,2,3,_1,然后实现逻辑当_3页面时跳转到3,_1跳转1.
2. ViewPager的getCount()方法,ViewPager的自带销毁机制,所以,和我们想要获取的值会有出入。

该项目于已发布与github,有意者请移步ShuffingBanner







以上是关于自定义View之无限大图轮播ShufBanner的主要内容,如果未能解决你的问题,请参考以下文章

ViewPager,ViewPager2 无限轮播功能。自定义 Indicator,支持一屏三页,支持仿魅族 banner 效果。极其简单的使用方式

自定义完美的ViewPager 真正无限循环的轮播图

Android之仿京东淘宝的自动无限轮播控件

android-自定义广告轮播Banner(无限循环实现)

vue element 框架 自定义轮播图,点击上下翻图,并让图片居中

ViewPager网络图片无限轮播