1. 程式人生 > >實現安卓無限輪播元件Banner

實現安卓無限輪播元件Banner

前言

前些天需要使用到安卓的banner,也就是現在主流app主頁的無限輪播的橫幅,現在已經有很多好的開源專案可以直接使用,不過我還是想自己去實現一遍。因為是訪問的網路資料,實際過程中還是有些坑的,所以還是記錄一下。

具體實現

首先還是來看看最終的效果,gif是有些卡頓,跑起來還是很流暢的
BannViewDemo
瞭解到,現在實現這種橫幅,基本上是2種方式,一種是使用RecyclerView的橫向滾動去實現,因為橫幅是從一個頁面直接跳轉到下一頁,用RecyclerView需要監聽滑動的過程,計算滑動的距離,然後進行跳轉,後面官方考慮到這一點提供了PagerSnapHelper這個工具類來解決這個問題,這裡就不多說了。
這篇部落格主要就是寫的就是第二種方式,使用ViewPager去實現。

首先,要用ViewPager實現無限輪播,可以使PagerAdapter的getCount方法返回Integer.MAX_VALUE。也就是讓頁面數量返回一個Integer的最大值,這樣在滑動過程中產生一種無限迴圈的假象,首先寫一個抽象基類,繼承自PagerAdapter

定義介面卡

public abstract class BannerViewBaseAdapter extends PagerAdapter {

    private List<View> mList;
    private View mView;

    public BannerViewBaseAdapter
() { mList = new ArrayList<>(); } /** * 返回Integer的最大值 */ @Override public int getCount() { return Integer.MAX_VALUE; } @Override public boolean isViewFromObject(View view, Object object) { return view == object; } @Override
public Object instantiateItem(ViewGroup container, int position) { Log.d("I am postion cx cx xcx", String.valueOf(position)); if (getSize() != 0) { if (mList.size() <= (position % getSize())) { for (int i = mList.size();i <= position % getSize();++i) { mList.add(getView(container,i)); } } mView = mList.get(position % getSize()); if (mView.getParent() != null) { container.removeView(mView); } container.addView(mView); } return mView; } @Override public void destroyItem(ViewGroup container, int position, Object object) { // 滑動下一張圖時當前的圖 if (getSize() != 0 && position != 0) { container.removeView(mList.get(position % getSize())); } } /** * 獲取要顯示的View * @param container * @param position * @return */ public abstract View getView(ViewGroup container,int position); /** * 獲取實際ItemView的數量 * @return */ public abstract int getSize();

這裡主要是重寫instantiateItem方法,進行新增頁面的邏輯,首先要判斷getSize不為0,因為實際網路載入時可能是一個非同步的耗時操作,如果執行到下面的計算時,除數為0肯定會報錯的,其它類似地方也都是這樣,然後注意到下面的position%getSize,這裡是用當前位置對實際item的數量進行模運算取餘,得到的值就是當前item的實際位置(前面說過無限輪播是不停的增加頁面,造成輪播的假象,實際位置就是指在進行輪播的幾個item中,當前處的位置),接下來就判斷,如果item不在集合中,就把view新增到一個List集合裡面,最後要防止同一個view的重複新增,所以每次新增前需要移除這個view。
接著重寫destroyItem方法,每次迴圈跳轉時都要銷燬掉之前的view。下面兩個抽象方法就是用來獲取到具體的值了。

然後就是我們的具體檢視的介面卡

public class BannerViewAdapter extends BannerViewBaseAdapter {

    private List<TestBean> mBeansList;
    private Context mContext;

    public BannerViewAdapter(List<TestBean> bannerBeans) {
        this.mBeansList = bannerBeans;
    }

    @Override
    public View getView(ViewGroup container, int position) {
        AppCompatImageView imageView;
        TextView title;

        if (mContext == null) {
            mContext = container.getContext();
        }
        View mView = LayoutInflater.from(mContext).inflate(R.layout.banner_item_layout,null);

        final TestBean bean = mBeansList.get(position);
        imageView = mView.findViewById(R.id.image);
        title = mView.findViewById(R.id.banner_title);
        title.setText(bean.getTitle());
        Glide.with(mContext).load(bean.getImageId())
                .error(R.drawable.ic_launcher_background)
                .into(imageView);
        notifyDataSetChanged();

        imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext,"你點選了"+bean.getTitle(),Toast.LENGTH_SHORT).show();
            }
        });
        return mView;
    }

    @Override
    public int getSize() {
        return mBeansList.size();
    }
}

這裡基礎抽象基類,邏輯比較簡單,沒什麼好講的。

檢視繪製

然後重點就是自定義的BannerView類了


public class BannerView extends FrameLayout implements ViewPager.OnPageChangeListener {

    private ViewPager mViewPager;

    /**
     *  圓點佈局
     */
    private LinearLayout mPointContainer;

    private BannerViewBaseAdapter mAdapter;

    /**
     *  圓點數量
     */
    private int mPointCount;

    /**
     *  圓點圖片
     */
    private ImageView[] mPoints;

    /**
     *  最後一個圓點
     */
    private int mLastPos;

    /**
     *  當前是否觸控
     */
    private boolean isTouch = false;

    private ScheduledExecutorService executorService;

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 0:
                    postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mViewPager.setCurrentItem(mViewPager.getCurrentItem()+1);
                        }
                    },1000);
                    break;
                default:
                    break;
            }
        }
    };

    public BannerView(@NonNull Context context, AttributeSet attributeSet) {
        super(context,attributeSet);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        initView();
    }

    private void initView() {
        mViewPager = findViewById(R.id.views_container);
        mPointContainer = findViewById(R.id.point_container);
        mViewPager.addOnPageChangeListener(this);
    }

    public void setAdapter(BannerViewAdapter adapter) {
        this.mAdapter = adapter;
        mPointCount = mAdapter.getSize();
        mViewPager.setAdapter(mAdapter);
        Log.d("sddsccdsvdsvdv", String.valueOf(mPointCount*100));
        initPoint();
        /**
         *  防止第二次重新整理後 顯示空白頁面
         */
        mViewPager.setCurrentItem(mPointCount*100+3);
        startScroll();
    }

    /**
     *  載入圓點
     */
    private void initPoint() {
       if (mPointCount == 0) {
           return;
       }

       mPoints = new ImageView[mPointCount];
       // 清chu所有圓點
       mPointContainer.removeAllViews();
       for (int i=0;i < mPointCount;i++) {
           ImageView view = new ImageView(getContext());
           view.setImageResource(R.drawable.point_normal);
           mPointContainer.addView(view);
           mPoints[i] = view;
       }
       if (mPoints[0] != null) {
           mPoints[0].setImageResource(R.drawable.point_selected);
       }
       mLastPos = 0;
    }
    /**
     * 改變圓點位置
     */
    private void changePoint(int currentPoint) {
        if (mLastPos == currentPoint) {
            return;
        }
        mPoints[currentPoint].setImageResource(R.drawable.point_selected);
        mPoints[mLastPos].setImageResource(R.drawable.point_normal);
        mLastPos = currentPoint;
    }

    public void startScroll() {

        executorService = new ScheduledThreadPoolExecutor(1);
        executorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                if (isTouch) {
                    return;
                }
                handler.sendEmptyMessage(0);
            }
        },1000,3000, TimeUnit.MILLISECONDS);
    }

    public void cancelScroll() {
        if (executorService != null) {
            executorService.shutdown();
        }
    }

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

    }

    @Override
    public void onPageSelected(int position) {
        if (mPointCount != 0) {
            changePoint(position % mPointCount);
        }
    }

    @Override
    public void onPageScrollStateChanged(int state) {

    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isTouch = true;
                break;
            case MotionEvent.ACTION_UP:
                isTouch = false;
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }
}

BannerView繼承FrameLayout,實現滑動監聽的介面,這裡先看看initView方法,這裡給viewpager設定了介面卡,然後接著就是載入圓點指示器的方法,邏輯也比較簡單,載入前注意先移除掉之前的圓點,防止重新整理後點的數量重複新增。然後就是第一次載入時,時ViewPager跳轉到mPoinCoun*100+x的位置,也是為了防止首次載入無法向左滑動,就不是無限迴圈的假象了。然後就讓我們的ViewPager執行定時滑動任務,定時任務有很多的實現方式,可以使用Timer+handler的方式,可以使用CountDownTimer類來實現,這裡的話,我使用的是執行緒池來進行的定時任務,後面分別傳入預載入與跳轉週期,然後裡面需要進行判斷當前是否觸控式螢幕幕,所以要重寫dispatchEvent方法,監聽當前的動作,然後向handler傳送訊息,再進行頁面滑動,這裡可能會有疑惑,執行緒池已經設定了定時任務,為什麼還要向handler去傳送訊息,進行延時處理,handler裡面才是真正的滾動延時,除了是非Ui執行緒不進行Ui更新的操作 也是因為在我們重新整理後,執行緒池會造成阻塞,無法正常執行。我使用了其它幾種方式,還是出現了各種問題,這裡就不多說了。

整個BannerView的流程就走完了
再看看佈局檔案 banner_item_layout


<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <android.support.v7.widget.AppCompatImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"/>

    <TextView
        android:id="@+id/banner_title"
        android:text="我是圈子名字"
        android:textSize="15sp"
        android:textColor="@android:color/white"
        android:layout_width="match_parent"
        android:gravity="center_vertical"
        android:paddingLeft="10dp"
        android:layout_gravity="bottom"
        android:background="@color/transparency"
        android:layout_height="35dp" />
</android.support.design.widget.CoordinatorLayout>

banner_view_layout

<com.legend.bannerviewdemo.banner.BannerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/my_slide_view"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v4.view.ViewPager
        android:id="@+id/views_container"
        android:layout_width="match_parent"
        android:layout_height="180dp"></android.support.v4.view.ViewPager>

    <LinearLayout
        android:id="@+id/indicator"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:layout_marginBottom="5dp"
        android:orientation="vertical"
        android:visibility="visible">

        <LinearLayout
            android:id="@+id/point_container"
            android:layout_width="match_parent"
            android:layout_height="11dp"
            android:gravity="center_horizontal"
            android:orientation="horizontal" />
    </LinearLayout>

</com.legend.bannerviewdemo.banner.BannerView>

使用的話就直接給adapter新增資料,然後例項化給BannerView設定adapter就可以了。這裡很要注意一點,就是每次重新整理之前記得手動關閉執行緒池,也就是呼叫BannView中的cancelScroll()方法,不然會造成進行網路載入,第二次重新整理載入資料時,banner直接出現空白頁面,這肯定不是我們想要的。 另外,配合RecyclerView使用時,可以把BannerView動態新增到RecyclerView的頭部,直接放到佈局中,如果要實現RecyclerView上滑時,banner跟著一起滾動的話,可以使用NestedScrollView,但是會造成巢狀滑動衝突,這一點也沒看到好的解決辦法,設定了NestedScrollingEnable屬性,但是會導致RecyclerView無法上拉載入,如果一次性載入完資料的話,RecyclerView的複用和回收機制就沒起到作用了,很容易出現OOM,這也不是我們想看到的,所以最好還是配合RecyclerView使用。

總結

總的來說,實現一個並不困難,難在實際過程中會出現各種各樣的問題,也正是常說的,debug時間遠遠多於寫程式碼的是時間(說到底還是經驗不足的,理解不夠深的緣故)。廢話不多說,最後該Demo地址github