【Android】造輪子:輪播圖
前言
目前市場上的APP中,輪播圖可以說是很常見的。一個好的輪播圖,基本上適用于所有的APP。是時候打造一個自己的輪播圖了,不要等到用的時候才去Google。
功能
輪播圖需要實現一下功能
- 圖片循環輪播
- 可添加文字
- 最后一張到第一張的切換也要有切換效果
- 循環、自動播放可控制
還有我們都比較關注的一點: 這輪子必須易拆、易裝,可擴展性強 。每次換個項目就要拷貝好幾個文件,改一大堆代碼,這是很煩的。
實現
再多的文字也不如一張圖來得直觀,先來個福利,回頭再說怎實現的。
效果
思路
這里使用 ViewPager 來實現輪播的效果,但是 ViewPager 是滑動到最后一張時,是不能跳轉到第一張的。于是,我們可以這樣:
- 需要顯示的輪播圖有N張
- 往 ViewPager 中添加 N 個 View ,這時 ViewPager 中有:
View(1)、View(2)、View(3) ... View(N) - 再往 ViewPager 中添加 View(1) ,這時 ViewPager 中有:
View(1)、View(2)、View(3) ... View(N)、View(1)
這樣就可以實現一種視覺效果:滑動到最后一張 View(N) 的時候,再往后滑動就回到了第一張 View(1) 。
這也適用于從第一張條轉到最后一張的實現。
文字看著費解?那就看圖吧(還好會那么一點點PS)
例:
需要顯示三張圖:
需要輪播的圖片
經過處理,變成這樣
處理后的輪播圖
在界面上看到的是三張圖片,而實際在ViewPager中的是這樣的5張。
- 當從View4跳轉到View5時,在代碼中立刻將視圖切換到View2,應為圖片是一樣的,所有在界面上看不到任何效果。
- 同理,當從View2跳轉到View1時,在代碼中將視圖切換到View4。
自動輪播流程:
View2 --> View3 --> View4 --> View5 --> View2 (完成一次循環)--> View3 --> View4 ....
當顯示 View5 的時候,立刻切換到 View2 ( View5 和 View2 顯示的內容是相同的),這樣就實現了圖片輪播。
這里 View5 -> View2 的切換巧妙利用了 ViewPager 中的方法:
setCurrentItem(int item, boolean smoothScroll)
參數 smoothScroll為false 的時候,實現了“看不見”的跳轉。
還是不大清楚?那就直接看代碼吧
代碼
思路說完,上代碼
-
創建model
這里創建一個 Info 類,模擬實際應用中的數據。里面有 title 和 url 字段。
public class Info { private String url; private String title;
public Info(String title, String url) { this.url = url; this.title = title; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; } }</code></pre> </li>
布局
為了實現畫面重疊的效果,這里用了相對布局,輪播圖使用 ViewPager 來實現。后面有兩個 LinearLayout ,第一個 LinearLayout 用來放指示器,在 java 代碼中動態添加;第二個 LinearLayout 就用來顯示 Title 了,當然,如果還需要顯示的其他內容,可以在這個布局里面中添加。<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" android:orientation="vertical"> <android.support.v4.view.ViewPager android:id="@+id/cycle_view_pager" android:layout_width="match_parent" android:layout_height="match_parent" /> <LinearLayout android:id="@+id/cycle_indicator" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginBottom="10dp" android:gravity="center" android:orientation="horizontal" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@id/cycle_indicator" android:orientation="vertical"> <TextView android:id="@+id/cycle_title" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="10dp" android:gravity="center" android:textColor="@android:color/white" android:textSize="20sp" /> </LinearLayout> </RelativeLayout>
CycleViewPager
重點來了,自定義的輪播圖。來個重磅炸彈,別看暈了
public class CycleViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {
private static final String TAG = "CycleViewPager";
private Context mContext;
private ViewPager mViewPager;//實現輪播圖的ViewPager
private TextView mTitle;//標題
private LinearLayout mIndicatorLayout; // 指示器
private Handler handler;//每幾秒后執行下一張的切換
private int WHEEL = 100; // 轉動
private int WHEEL_WAIT = 101; // 等待
private List<View> mViews = new ArrayList<>(); //需要輪播的View,數量為輪播圖數量+2
private ImageView[] mIndicators; //指示器小圓點
private boolean isScrolling = false; // 滾動框是否滾動著
private boolean isCycle = true; // 是否循環,默認為true
private boolean isWheel = true; // 是否輪播,默認為true
private int delay = 4000; // 默認輪播時間
private int mCurrentPosition = 0; // 輪播當前位置
private long releaseTime = 0; // 手指松開、頁面不滾動時間,防止手機松開后短時間進行切換
private ViewPagerAdapter mAdapter;
private ImageCycleViewListener mImageCycleViewListener;
private List<Info> infos;//數據集合
private int mIndicatorSelected;//指示器圖片,被選擇狀態
private int mIndicatorUnselected;//指示器圖片,未被選擇狀態
final Runnable runnable = new Runnable() { @Override public void run() { if (mContext != null && isWheel) { long now = System.currentTimeMillis(); // 檢測上一次滑動時間與本次之間是否有觸擊(手滑動)操作,有的話等待下次輪播 if (now - releaseTime > delay - 500) { handler.sendEmptyMessage(WHEEL); } else { handler.sendEmptyMessage(WHEEL_WAIT); } } } };
public CycleViewPager(Context context) { this(context, null); }
public CycleViewPager(Context context, AttributeSet attrs) { this(context, attrs, 0); }
public CycleViewPager(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); this.mContext = context; initView(); }
/**
初始化View */ private void initView() { LayoutInflater.from(mContext).inflate(R.layout.layout_cycle_view, this, true); mViewPager = (ViewPager) findViewById(R.id.cycle_view_pager); mTitle = (TextView) findViewById(R.id.cycle_title); mIndicatorLayout = (LinearLayout) findViewById(R.id.cycle_indicator); handler = new Handler() {
@Override public void handleMessage(Message msg) { super.handleMessage(msg); if (msg.what == WHEEL && mViews.size() > 0) { if (!isScrolling) { //當前為非滾動狀態,切換到下一頁 int posttion = (mCurrentPosition + 1) % mViews.size(); mViewPager.setCurrentItem(posttion, true); } releaseTime = System.currentTimeMillis(); handler.removeCallbacks(runnable); handler.postDelayed(runnable, delay); return; } if (msg.what == WHEEL_WAIT && mViews.size() > 0) { handler.removeCallbacks(runnable); handler.postDelayed(runnable, delay); } }
}; }
/**
- 設置指示器圖片,在setData之前調用 *
- @param select 選中時的圖片
@param unselect 未選中時的圖片 */ public void setIndicators(int select, int unselect) { mIndicatorSelected = select; mIndicatorUnselected = unselect; } public void setData(List<Info> list, ImageCycleViewListener listener) { setData(list, listener, 0); }
/**
- 初始化viewpager *
- @param list 要顯示的數據
@param showPosition 默認顯示位置 */ public void setData(List<Info> list, ImageCycleViewListener listener, int showPosition) { if (list == null || list.size() == 0) {
//沒有數據時隱藏整個布局 this.setVisibility(View.GONE); return;
} mViews.clear(); infos = list; if (isCycle) {
//添加輪播圖View,數量為集合數+2 // 將最后一個View添加進來 mViews.add(getImageView(mContext, infos.get(infos.size() - 1).getUrl())); for (int i = 0; i < infos.size(); i++) { mViews.add(getImageView(mContext, infos.get(i).getUrl())); } // 將第一個View添加進來 mViews.add(getImageView(mContext, infos.get(0).getUrl()));
} else {
//只添加對應數量的View for (int i = 0; i < infos.size(); i++) { mViews.add(getImageView(mContext, infos.get(i).getUrl())); }
} if (mViews == null || mViews.size() == 0) {
//沒有View時隱藏整個布局 this.setVisibility(View.GONE); return;
} mImageCycleViewListener = listener; int ivSize = mViews.size(); // 設置指示器 mIndicators = new ImageView[ivSize]; if (isCycle)
mIndicators = new ImageView[ivSize - 2];
mIndicatorLayout.removeAllViews(); for (int i = 0; i < mIndicators.length; i++) {
mIndicators[i] = new ImageView(mContext); LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); lp.setMargins(10, 0, 10, 0); mIndicators[i].setLayoutParams(lp); mIndicatorLayout.addView(mIndicators[i]);
} mAdapter = new ViewPagerAdapter(); // 默認指向第一項,下方viewPager.setCurrentItem將觸發重新計算指示器指向 setIndicator(0); mViewPager.setOffscreenPageLimit(3); mViewPager.setOnPageChangeListener(this); mViewPager.setAdapter(mAdapter); if (showPosition < 0 || showPosition >= mViews.size())
showPosition = 0;
if (isCycle) {
showPosition = showPosition + 1;
} mViewPager.setCurrentItem(showPosition); setWheel(true);//設置輪播 }
/**
- 獲取輪播圖View *
- @param context
@param url */ private View getImageView(Context context, String url) { return MainActivity.getImageView(context, url); }
/**
- 設置指示器 *
@param selectedPosition 默認指示器位置 */ private void setIndicator(int selectedPosition) { setText(mTitle, infos.get(selectedPosition).getTitle()); try {
for (int i = 0; i < mIndicators.length; i++) { mIndicators[i] .setBackgroundResource(mIndicatorUnselected); } if (mIndicators.length > selectedPosition) mIndicators[selectedPosition] .setBackgroundResource(mIndicatorSelected);
} catch (Exception e) {
Log.i(TAG, "指示器路徑不正確");
} }
/**
- 頁面適配器 返回對應的view *
@author Yuedong Li */ private class ViewPagerAdapter extends PagerAdapter { @Override public int getCount() {
return mViews.size();
} @Override public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == arg1;
} @Override public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
} @Override public View instantiateItem(ViewGroup container, final int position) {
View v = mViews.get(position); if (mImageCycleViewListener != null) { v.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { mImageCycleViewListener.onImageClick( infos.get(mCurrentPosition - 1), mCurrentPosition, v); } }); } container.addView(v); return v;
} @Override public int getItemPosition(Object object) {
return POSITION_NONE;
} } @Override public void onPageScrolled( int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int arg0) { int max = mViews.size() - 1; int position = arg0; mCurrentPosition = arg0; if (isCycle) {
if (arg0 == 0) { //滾動到mView的1個(界面上的最后一個),將mCurrentPosition設置為max - 1 mCurrentPosition = max - 1; } else if (arg0 == max) { //滾動到mView的最后一個(界面上的第一個),將mCurrentPosition設置為1 mCurrentPosition = 1; } position = mCurrentPosition - 1;
} setIndicator(position); } @Override public void onPageScrollStateChanged(int state) { if (state == 1) { // viewPager在滾動
isScrolling = true; return;
} else if (state == 0) { // viewPager滾動結束
releaseTime = System.currentTimeMillis(); //跳轉到第mCurrentPosition個頁面(沒有動畫效果,實際效果頁面上沒變化) mViewPager.setCurrentItem(mCurrentPosition, false);
} isScrolling = false; }
/**
- 為textview設置文字
- @param textView
@param text */ public static void setText(TextView textView, String text) { if (text != null && textView != null) textView.setText(text); }
/**
- 為textview設置文字 *
- @param textView
@param text */ public static void setText(TextView textView, int text) { if (textView != null) setText(textView, text + ""); }
/**
- 是否循環,默認開啟。必須在setData前調用 *
@param isCycle 是否循環 */ public void setCycle(boolean isCycle) { this.isCycle = isCycle; }
/**
- 是否處于循環狀態 *
@return */ public boolean isCycle() { return isCycle; }
/**
- 設置是否輪播,默認輪播,輪播一定是循環的 *
@param isWheel */ public void setWheel(boolean isWheel) { this.isWheel = isWheel; isCycle = true; if (isWheel) {
handler.postDelayed(runnable, delay);
} }
/**
刷新數據,當外部視圖更新后,通知刷新數據 */ public void refreshData() { if (mAdapter != null)
mAdapter.notifyDataSetChanged();
}
/**
- 是否處于輪播狀態 *
@return */ public boolean isWheel() { return isWheel; }
/**
- 設置輪播暫停時間,單位毫秒(默認4000毫秒)
@param delay */ public void setDelay(int delay) { this.delay = delay; }
/**
- 輪播控件的監聽事件 *
@author minking */ public static interface ImageCycleViewListener {
/**
- 單擊圖片事件 *
- @param info
- @param position
- @param imageView
*/
public void onImageClick(Info info, int position, View imageView);
}
}</code></pre>
從里面挑了幾個變量和方法說明一下:
變量:
handler 、 runnable :實現定時輪播
mCurrentPosition :表示當前位置
方法:
setIndicators() :設置指示器的圖片(必須在 setData 前調用)
setData() :根據數據,生成對應的輪播圖
setIndicator() :設置指示器和文字內容
onPageSelected() 、 onPageScrollStateChanged() :利用 ViewPager 的滾動監聽,實現了上面的思路。 onPageSelected() 中根據 ViewPager 中顯示的位置,改變 mCurrentPosition 的值,然后在 onPageScrollStateChanged() 中根據 mCurrentPosition 重新設置頁面(這里的 setCurrentItem 沒有動畫效果)。
getImageView() :根據URL生成 Viewpager 中對應的各個 View ( 根據實際的圖片加載框架來生成,這里使用了Picasso實現了網絡圖片的加載 ),看看 getImageView() 中調用的代碼
/**
- 得到輪播圖的View
- @param context
- @param url
@return */ public static View getImageView(Context context, String url) { RelativeLayout rl = new RelativeLayout(context); //添加一個ImageView,并加載圖片 ImageView imageView = new ImageView(context); RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setLayoutParams(layoutParams); //使用Picasso來加載圖片 Picasso.with(context).load(url).into(imageView); //在Imageview前添加一個半透明的黑色背景,防止文字和圖片混在一起 ImageView backGround = new ImageView(context); backGround.setLayoutParams(layoutParams); backGround.setBackgroundResource(R.color.cycle_image_bg); rl.addView(imageView); rl.addView(backGround); return rl; }</code></pre>
<color name="cycle_image_bg">#44222222</color>
代碼很簡單,創建了一個顯示圖片的布局,先在布局中添加了需要顯示的圖片,然后加了個半透明的圖,防止顯示時文字和圖片中白色的部分重疊在一起,導致看不清文字。
</li>-
在Acitivty中使用
輪子打造好了,不拿出來溜一溜?
public class MainActivity extends AppCompatActivity {
/**
模擬請求后得到的數據 */ List<Info> mList = new ArrayList<>();
/**
輪播圖 */ CycleViewPager mCycleViewPager;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); initView(); }
/**
初始化數據 */ private void initData() { mList.add(new Info("標題1",
"http://img2.3lian.com/2014/c7/25/d/40.jpg"));
mList.add(new Info("標題2",
"http://img2.3lian.com/2014/c7/25/d/41.jpg"));
mList.add(new Info("標題3",
"http://imgsrc.baidu.com/forum/pic/item/b64543a98226cffc8872e00cb9014a90f603ea30.jpg"));
mList.add(new Info("標題4",
"http://imgsrc.baidu.com/forum/pic/item/261bee0a19d8bc3e6db92913828ba61eaad345d4.jpg"));
}
/**
初始化View */ private void initView() { mCycleViewPager = (CycleViewPager) findViewById(R.id.cycle_view); //設置選中和未選中時的圖片 mCycleViewPager.setIndicators(R.mipmap.ad_select, R.mipmap.ad_unselect); //設置輪播間隔時間 mCycleViewPager.setDelay(2000); mCycleViewPager.setData(mList, mAdCycleViewListener); }
/**
輪播圖點擊監聽 */ private CycleViewPager.ImageCycleViewListener mAdCycleViewListener =
new CycleViewPager.ImageCycleViewListener() {
@Override public void onImageClick(Info info, int position, View imageView) {
if (mCycleViewPager.isCycle()) { position = position - 1; } Toast.makeText(MainActivity.this, info.getTitle() + "選擇了--" + position, Toast.LENGTH_LONG).show();
} }; }</code></pre> </li> </ul>
使用起來也是很簡單的,只要設置下圖片、數據、點擊監聽就可以了。(之前貼過 MainActivity.getImageView() 方法了,這里就不貼了)
放到自己的項目中?
只需要調下布局,根據自己的圖片加載框架改下 getImageView (或者也可以直接用我的),然后把 CycleViewPager 中的 Info 改成自己的 Model 就可以了。