實現安卓無限輪播元件Banner
前言
前些天需要使用到安卓的banner,也就是現在主流app主頁的無限輪播的橫幅,現在已經有很多好的開源專案可以直接使用,不過我還是想自己去實現一遍。因為是訪問的網路資料,實際過程中還是有些坑的,所以還是記錄一下。
具體實現
首先還是來看看最終的效果,gif是有些卡頓,跑起來還是很流暢的
瞭解到,現在實現這種橫幅,基本上是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