前言
目前市場(chǎng)上的APP中箩兽,輪播圖可以說(shuō)是很常見的薇溃。一個(gè)好的輪播圖,基本上適用于所有的APP放前。是時(shí)候打造一個(gè)自己的輪播圖了忿磅,不要等到用的時(shí)候才去Google。
本文參考自Android實(shí)現(xiàn)Banner界面廣告圖片循環(huán)輪播(包括實(shí)現(xiàn)手動(dòng)滑動(dòng)循環(huán))凭语,根據(jù)該代碼改編
功能
輪播圖需要實(shí)現(xiàn)一下功能
- 圖片循環(huán)輪播
- 可添加文字
- 最后一張到第一張的切換也要有切換效果
- 循環(huán)葱她、自動(dòng)播放可控制
還有我們都比較關(guān)注的一點(diǎn):這輪子必須易拆、易裝似扔,可擴(kuò)展性強(qiáng)吨些。每次換個(gè)項(xiàng)目就要拷貝好幾個(gè)文件,改一大堆代碼炒辉,這是很煩的豪墅。
實(shí)現(xiàn)
再多的文字也不如一張圖來(lái)得直觀,先來(lái)個(gè)福利黔寇,回頭再說(shuō)怎實(shí)現(xiàn)的偶器。
思路
這里使用ViewPager
來(lái)實(shí)現(xiàn)輪播的效果,但是ViewPager
是滑動(dòng)到最后一張時(shí),是不能跳轉(zhuǎn)到第一張的屏轰。于是颊郎,我們可以這樣:
- 需要顯示的輪播圖有N張
- 往
ViewPager
中添加N
個(gè)View
,這時(shí)ViewPager
中有:
View(1)霎苗、View(2)姆吭、View(3) ... View(N) - 再往
ViewPager
中添加View(1)
,這時(shí)ViewPager
中有:
View(1)唁盏、View(2)内狸、View(3) ... View(N)、View(1)
這樣就可以實(shí)現(xiàn)一種視覺(jué)效果:滑動(dòng)到最后一張 View(N)
的時(shí)候厘擂,再往后滑動(dòng)就回到了第一張View(1)
昆淡。
這也適用于從第一張條轉(zhuǎn)到最后一張的實(shí)現(xiàn)。
文字看著費(fèi)解驴党?那就看圖吧(還好會(huì)那么一點(diǎn)點(diǎn)PS)
例:
需要顯示三張圖:
經(jīng)過(guò)處理瘪撇,變成這樣
在界面上看到的是三張圖片,而實(shí)際在ViewPager中的是這樣的5張港庄。
- 當(dāng)從View4跳轉(zhuǎn)到View5時(shí)倔既,在代碼中立刻將視圖切換到View2,應(yīng)為圖片是一樣的鹏氧,所有在界面上看不到任何效果渤涌。
- 同理,當(dāng)從View2跳轉(zhuǎn)到View1時(shí)把还,在代碼中將視圖切換到View4实蓬。
自動(dòng)輪播流程:
View2
-->View3
--> View4
--> View5
-->View2
(完成一次循環(huán))-->View3
-->View4
....
當(dāng)顯示View5
的時(shí)候,立刻切換到View2
(View5
和View2
顯示的內(nèi)容是相同的)吊履,這樣就實(shí)現(xiàn)了圖片輪播安皱。
這里View5
->View2
的切換巧妙利用了ViewPager
中的方法:
setCurrentItem(int item, boolean smoothScroll)
參數(shù)smoothScroll為false
的時(shí)候,實(shí)現(xiàn)了“看不見”的跳轉(zhuǎn)艇炎。
還是不大清楚酌伊?那就直接看代碼吧
代碼
思路說(shuō)完,上代碼
-
創(chuàng)建model
這里創(chuàng)建一個(gè)Info
類缀踪,模擬實(shí)際應(yīng)用中的數(shù)據(jù)居砖。里面有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;
}
}
-
布局
為了實(shí)現(xiàn)畫面重疊的效果驴娃,這里用了相對(duì)布局奏候,輪播圖使用ViewPager
來(lái)實(shí)現(xiàn)。后面有兩個(gè)LinearLayout
唇敞,第一個(gè)LinearLayout
用來(lái)放指示器蔗草,在java
代碼中動(dòng)態(tài)添加咒彤;第二個(gè)LinearLayout
就用來(lái)顯示Title
了,當(dāng)然蕉世,如果還需要顯示的其他內(nèi)容蔼紧,可以在這個(gè)布局里面中添加。
<?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
重點(diǎn)來(lái)了狠轻,自定義的輪播圖。來(lái)個(gè)重磅炸彈彬犯,別看暈了
public class CycleViewPager extends FrameLayout implements ViewPager.OnPageChangeListener {
private static final String TAG = "CycleViewPager";
private Context mContext;
private ViewPager mViewPager;//實(shí)現(xiàn)輪播圖的ViewPager
private TextView mTitle;//標(biāo)題
private LinearLayout mIndicatorLayout; // 指示器
private Handler handler;//每幾秒后執(zhí)行下一張的切換
private int WHEEL = 100; // 轉(zhuǎn)動(dòng)
private int WHEEL_WAIT = 101; // 等待
private List<View> mViews = new ArrayList<>(); //需要輪播的View向楼,數(shù)量為輪播圖數(shù)量+2
private ImageView[] mIndicators; //指示器小圓點(diǎn)
private boolean isScrolling = false; // 滾動(dòng)框是否滾動(dòng)著
private boolean isCycle = true; // 是否循環(huán),默認(rèn)為true
private boolean isWheel = true; // 是否輪播谐区,默認(rèn)為true
private int delay = 4000; // 默認(rèn)輪播時(shí)間
private int mCurrentPosition = 0; // 輪播當(dāng)前位置
private long releaseTime = 0; // 手指松開湖蜕、頁(yè)面不滾動(dòng)時(shí)間,防止手機(jī)松開后短時(shí)間進(jìn)行切換
private ViewPagerAdapter mAdapter;
private ImageCycleViewListener mImageCycleViewListener;
private List<Info> infos;//數(shù)據(jù)集合
private int mIndicatorSelected;//指示器圖片宋列,被選擇狀態(tài)
private int mIndicatorUnselected;//指示器圖片昭抒,未被選擇狀態(tài)
final Runnable runnable = new Runnable() {
@Override
public void run() {
if (mContext != null && isWheel) {
long now = System.currentTimeMillis();
// 檢測(cè)上一次滑動(dòng)時(shí)間與本次之間是否有觸擊(手滑動(dòng))操作,有的話等待下次輪播
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) {
//當(dāng)前為非滾動(dòng)狀態(tài)炼杖,切換到下一頁(yè)
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);
}
}
};
}
/**
* 設(shè)置指示器圖片灭返,在setData之前調(diào)用
*
* @param select 選中時(shí)的圖片
* @param unselect 未選中時(shí)的圖片
*/
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 要顯示的數(shù)據(jù)
* @param showPosition 默認(rèn)顯示位置
*/
public void setData(List<Info> list, ImageCycleViewListener listener,
int showPosition) {
if (list == null || list.size() == 0) {
//沒(méi)有數(shù)據(jù)時(shí)隱藏整個(gè)布局
this.setVisibility(View.GONE);
return;
}
mViews.clear();
infos = list;
if (isCycle) {
//添加輪播圖View,數(shù)量為集合數(shù)+2
// 將最后一個(gè)View添加進(jìn)來(lái)
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()));
}
// 將第一個(gè)View添加進(jìn)來(lái)
mViews.add(getImageView(mContext, infos.get(0).getUrl()));
} else {
//只添加對(duì)應(yīng)數(shù)量的View
for (int i = 0; i < infos.size(); i++) {
mViews.add(getImageView(mContext, infos.get(i).getUrl()));
}
}
if (mViews == null || mViews.size() == 0) {
//沒(méi)有View時(shí)隱藏整個(gè)布局
this.setVisibility(View.GONE);
return;
}
mImageCycleViewListener = listener;
int ivSize = mViews.size();
// 設(shè)置指示器
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();
// 默認(rèn)指向第一項(xiàng)坤邪,下方viewPager.setCurrentItem將觸發(fā)重新計(jì)算指示器指向
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);//設(shè)置輪播
}
/**
* 獲取輪播圖View
*
* @param context
* @param url
*/
private View getImageView(Context context, String url) {
return MainActivity.getImageView(context, url);
}
/**
* 設(shè)置指示器
*
* @param selectedPosition 默認(rèn)指示器位置
*/
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, "指示器路徑不正確");
}
}
/**
* 頁(yè)面適配器 返回對(duì)應(yīng)的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) {
//滾動(dòng)到mView的1個(gè)(界面上的最后一個(gè))熙含,將mCurrentPosition設(shè)置為max - 1
mCurrentPosition = max - 1;
} else if (arg0 == max) {
//滾動(dòng)到mView的最后一個(gè)(界面上的第一個(gè)),將mCurrentPosition設(shè)置為1
mCurrentPosition = 1;
}
position = mCurrentPosition - 1;
}
setIndicator(position);
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == 1) { // viewPager在滾動(dòng)
isScrolling = true;
return;
} else if (state == 0) { // viewPager滾動(dòng)結(jié)束
releaseTime = System.currentTimeMillis();
//跳轉(zhuǎn)到第mCurrentPosition個(gè)頁(yè)面(沒(méi)有動(dòng)畫效果艇纺,實(shí)際效果頁(yè)面上沒(méi)變化)
mViewPager.setCurrentItem(mCurrentPosition, false);
}
isScrolling = false;
}
/**
* 為textview設(shè)置文字
* @param textView
* @param text
*/
public static void setText(TextView textView, String text) {
if (text != null && textView != null) textView.setText(text);
}
/**
* 為textview設(shè)置文字
*
* @param textView
* @param text
*/
public static void setText(TextView textView, int text) {
if (textView != null) setText(textView, text + "");
}
/**
* 是否循環(huán)怎静,默認(rèn)開啟。必須在setData前調(diào)用
*
* @param isCycle 是否循環(huán)
*/
public void setCycle(boolean isCycle) {
this.isCycle = isCycle;
}
/**
* 是否處于循環(huán)狀態(tài)
*
* @return
*/
public boolean isCycle() {
return isCycle;
}
/**
* 設(shè)置是否輪播黔衡,默認(rèn)輪播,輪播一定是循環(huán)的
*
* @param isWheel
*/
public void setWheel(boolean isWheel) {
this.isWheel = isWheel;
isCycle = true;
if (isWheel) {
handler.postDelayed(runnable, delay);
}
}
/**
* 刷新數(shù)據(jù)蚓聘,當(dāng)外部視圖更新后,通知刷新數(shù)據(jù)
*/
public void refreshData() {
if (mAdapter != null)
mAdapter.notifyDataSetChanged();
}
/**
* 是否處于輪播狀態(tài)
*
* @return
*/
public boolean isWheel() {
return isWheel;
}
/**
* 設(shè)置輪播暫停時(shí)間,單位毫秒(默認(rèn)4000毫秒)
* @param delay
*/
public void setDelay(int delay) {
this.delay = delay;
}
/**
* 輪播控件的監(jiān)聽事件
*
* @author minking
*/
public static interface ImageCycleViewListener {
/**
* 單擊圖片事件
*
* @param info
* @param position
* @param imageView
*/
public void onImageClick(Info info, int position, View imageView);
}
}
從里面挑了幾個(gè)變量和方法說(shuō)明一下:
變量:
handler
盟劫、runnable
:實(shí)現(xiàn)定時(shí)輪播
mCurrentPosition
:表示當(dāng)前位置
方法:
setIndicators()
:設(shè)置指示器的圖片(必須在setData
前調(diào)用)
setData()
:根據(jù)數(shù)據(jù)夜牡,生成對(duì)應(yīng)的輪播圖
setIndicator()
:設(shè)置指示器和文字內(nèi)容
onPageSelected()
、onPageScrollStateChanged()
:利用ViewPager
的滾動(dòng)監(jiān)聽捞高,實(shí)現(xiàn)了上面的思路氯材。onPageSelected()
中根據(jù)ViewPager
中顯示的位置,改變mCurrentPosition
的值硝岗,然后在onPageScrollStateChanged()
中根據(jù)mCurrentPosition
重新設(shè)置頁(yè)面(這里的setCurrentItem
沒(méi)有動(dòng)畫效果)氢哮。
getImageView()
:根據(jù)URL生成Viewpager
中對(duì)應(yīng)的各個(gè)View
(根據(jù)實(shí)際的圖片加載框架來(lái)生成,這里使用了Picasso實(shí)現(xiàn)了網(wǎng)絡(luò)圖片的加載)型檀,看看getImageView()
中調(diào)用的代碼
/**
* 得到輪播圖的View
* @param context
* @param url
* @return
*/
public static View getImageView(Context context, String url) {
RelativeLayout rl = new RelativeLayout(context);
//添加一個(gè)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來(lái)加載圖片
Picasso.with(context).load(url).into(imageView);
//在Imageview前添加一個(gè)半透明的黑色背景,防止文字和圖片混在一起
ImageView backGround = new ImageView(context);
backGround.setLayoutParams(layoutParams);
backGround.setBackgroundResource(R.color.cycle_image_bg);
rl.addView(imageView);
rl.addView(backGround);
return rl;
}
<color name="cycle_image_bg">#44222222</color>
代碼很簡(jiǎn)單,創(chuàng)建了一個(gè)顯示圖片的布局裂七,先在布局中添加了需要顯示的圖片皆看,然后加了個(gè)半透明的圖,防止顯示時(shí)文字和圖片中白色的部分重疊在一起背零,導(dǎo)致看不清文字腰吟。
-
在Acitivty中使用
輪子打造好了,不拿出來(lái)溜一溜徙瓶?
public class MainActivity extends AppCompatActivity {
/**
* 模擬請(qǐng)求后得到的數(shù)據(jù)
*/
List<Info> mList = new ArrayList<>();
/**
* 輪播圖
*/
CycleViewPager mCycleViewPager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initData();
initView();
}
/**
* 初始化數(shù)據(jù)
*/
private void initData() {
mList.add(new Info("標(biāo)題1",
"http://img2.3lian.com/2014/c7/25/d/40.jpg"));
mList.add(new Info("標(biāo)題2",
"http://img2.3lian.com/2014/c7/25/d/41.jpg"));
mList.add(new Info("標(biāo)題3",
"http://imgsrc.baidu.com/forum/pic/item/b64543a98226cffc8872e00cb9014a90f603ea30.jpg"));
mList.add(new Info("標(biāo)題4",
"http://imgsrc.baidu.com/forum/pic/item/261bee0a19d8bc3e6db92913828ba61eaad345d4.jpg"));
}
/**
* 初始化View
*/
private void initView() {
mCycleViewPager = (CycleViewPager) findViewById(R.id.cycle_view);
//設(shè)置選中和未選中時(shí)的圖片
mCycleViewPager.setIndicators(R.mipmap.ad_select, R.mipmap.ad_unselect);
//設(shè)置輪播間隔時(shí)間
mCycleViewPager.setDelay(2000);
mCycleViewPager.setData(mList, mAdCycleViewListener);
}
/**
* 輪播圖點(diǎn)擊監(jiān)聽
*/
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();
}
};
}
使用起來(lái)也是很簡(jiǎn)單的毛雇,只要設(shè)置下圖片、數(shù)據(jù)侦镇、點(diǎn)擊監(jiān)聽就可以了灵疮。(之前貼過(guò)MainActivity.getImageView()
方法了,這里就不貼了)
放到自己的項(xiàng)目中壳繁?
只需要調(diào)下布局震捣,根據(jù)自己的圖片加載框架改下getImageView
(或者也可以直接用我的),然后把CycleViewPager
中的Info
改成自己的Model
就可以了闹炉。
源碼地址:Github
以上有錯(cuò)誤之處蒿赢,感謝指出
投稿給鴻洋大神后,大神幫我測(cè)了下剩胁,發(fā)現(xiàn)這輪播圖在MOTO nexus 6上诉植,快速滑動(dòng)會(huì)卡住,然后跳躍昵观,類似應(yīng)該在小米5上也會(huì)復(fù)現(xiàn)晾腔。
大神的建議:無(wú)限循環(huán)banner,不如使用MAX頁(yè)啊犬,然后currentItem=MAX/2來(lái)做灼擂。
(這段時(shí)間忙著找工作,就先擱下了觉至,有興趣的同學(xué)可以先嘗試下剔应。)