前言
Banner廣告位是APP 中的一個非常重要的位置,為什麼呢?因為它能帶來money。是一個公司很重要的一個營收點。像那種使用者數基數特別大的產品,如facebook、twitter、QQ、微信等等。Banner廣告位日營收估計得上千萬美刀(猜的,不知道具體資料)。一個漂亮的Banner往往能夠吸引使用者的眼球,引導使用者點選,從而提高轉化率。遺憾的是現在的大多數產品的Banner都是千篇一律的,沒有什麼亮點可言。但是前幾天在魅族手機上發現了一個效果不錯的Banner,魅族所有自家的APP所用的Banner 引起了我的注意。效果是這樣子的:
看到這個Banner 第一眼就吸引了我,隨後就反覆的體驗了幾次了,感覺這種Banner的效果還不錯。最後想著高仿一個和這種效果差不多的BannerView 。那麼本文就講一下如何實現這樣一個BannerView。最終實現的效果如下:
目錄
本文會講實現仿魅族Banner效果所要用到的一些關鍵知識點,目錄如下圖所示。所有的效果已經封裝成一個庫。詳細程式碼請看github: github.com/pinguo-zhou…
仿魅族Banner 效果
在開始實現魅族Banner效果之前,我們先來整理一下實現一個BannerView的思路,首先需要用ViewPager,其次讓ViewPager無限輪播。其實BannerView就是一個無限輪播的ViewPager,然後做一些封裝處理,讓使用更加簡單就ok。
現在我們在來看一下魅族的這個Banner。他與普通的banner的區別是當前頁顯示了前一頁和後一頁的部分內容。
拋開切換時的動畫先不說,要實現這個效果的第一步就是要讓ViewPager在一個頁面顯示多頁的內容(當前頁+前後頁部分)。
1 . ViewPager展示多頁
要讓ViewPager頁面展示多頁的內容,就要用到ViewGroup的一個強大的屬性。這個屬性雖然強大,但是也不常用,可能有些小夥伴不知道(之前我也沒用過…),那就是clipChildren
屬性。這個屬性有什麼作用呢,我們看一下它的文件介紹:
/**
* By default, children are clipped to their bounds before drawing. This
* allows view groups to override this behavior for animations, etc.
*
* @param clipChildren true to clip children to their bounds,
* false otherwise
* @attr ref android.R.styleable#ViewGroup_clipChildren
*/複製程式碼
clipChildren: 預設值為true, 子View 的大小隻能在父View規定的範圍之內,比如父View的高為50,子View的高為60 ,那麼多處的部分就會被裁剪。如果我們設定這個值為false的話,那麼多處的部分就不會被裁剪了。
這裡我們就可以利用這個屬性來實現了這個效果了,我們設定ViewPager的父佈局的clipChildren
為false。然後設定ViewPager 左右一定的邊距,那麼左右就空出了一定的區域,利用clipChildren
屬性,就能讓前後頁面的部分顯示在當前頁了。佈局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:clipChildren="false"
android:orientation="vertical"
>
<android.support.v4.view.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
/>
</LinearLayout>複製程式碼
這樣就能實現ViewPager 展示前後頁面的部分內容。
2 . 自定義ViewPager.PageTransformer動畫
上面實現了ViewPager當前頁面顯示前後頁的部分內容,但是從最開始魅族的Banner效果我們可以看出,滑動的時候是有 一個放大縮小的動畫的。左右顯示的部分有一定比例的縮小。這就要用到ViewPager.PageTransformer了。
ViewPager.PageTransformer 幹什麼的呢?ViewPager.PageTransformer 是用來做ViewPager切換動畫的,它是一個介面,裡面只有一個方法transformPage
。
public interface PageTransformer {
/**
* Apply a property transformation to the given page.
*
* @param page Apply the transformation to this page
* @param position Position of page relative to the current front-and-center
* position of the pager. 0 is front and center. 1 is one full
* page position to the right, and -1 is one page position to the left.
*/
void transformPage(View page, float position);
}複製程式碼
雖然只有一個方法,但是它很強大,它能反映出在ViewPager滑動過程中,各個View的位置變化。我們拿到了這些位置變化,就能在這個過程中對View做各種各樣的動畫了。
要自定義動畫,我們就來需要知道positon這個值的變化區間。從官方給的ViewPager的兩個示例我們知道,position的變換有三個區間,[-Infinity,-1),[-1,1],(1.Infinity)。
[-Infinity,-1):已經在螢幕之外,看不到了
(1.Infinity): 已經在螢幕之外,看不到了。
[-1,1]: 這個區間是我門操作View動畫的重點區間。
我們來看一下官方對於position的解釋:
官方的解釋:The position parameter indicates where a given page is located relative to the center of the screen. It is a dynamic property that changes as the user scrolls through the pages. When a page fills the screen, its position value is 0. When a page is drawn just off the right side of the screen, its position value is 1. If the user scrolls halfway between pages one and two, page one has a position of -0.5 and page two has a position of 0.5.
根據解釋,也就是說當前停留的頁面的位置為 0,右邊螢幕之外繪製的這個頁面位置為 1。那麼,A 頁面滑到 B 頁面有 2 種情況:第一種:左邊劃出螢幕,那麼 A:0 -> -1,B :1 -> 0。第二種:右邊劃出螢幕,A:0->1, B :-1-> 0
瞭解了這個方法的變化後,我們就來自定義我們的切換動畫,這裡很簡單,我們只需要一個scale動畫。程式碼如下:
/**
* Created by zhouwei on 17/5/26.
*/
public class CustomTransformer implements ViewPager.PageTransformer {
private static final float MIN_SCALE = 0.9F;
@Override
public void transformPage(View page, float position) {
if(position < -1){
page.setScaleY(MIN_SCALE);
}else if(position<= 1){
//
float scale = Math.max(MIN_SCALE,1 - Math.abs(position));
page.setScaleY(scale);
/*page.setScaleX(scale);
if(position<0){
page.setTranslationX(width * (1 - scale) /2);
}else{
page.setTranslationX(-width * (1 - scale) /2);
}*/
}else{
page.setScaleY(MIN_SCALE);
}
}
}複製程式碼
效果圖是這樣的:
到此,我們仿魅族Banner的靜態效果就實現了。接下來我們就要讓Banner動起來,實現無限輪播效果。
圖片輪播
上面我們已經實現了Bannerd的靜態展示和切換動畫,那麼我們現在就需要讓Banner動起來,實現無限輪播。
ViewPager實現Banner無效輪播效果有2種方案,第一種是:在列表的最前面插入最後一條資料,在列表末尾插入第一個資料,造成迴圈的假象。第二種方案是:採用getCount 返回 Integer.MAX_VALUE。結下來分別看一下這兩種方案。
1 . 在列表的最前面插入最後一條資料,在列表末尾插入第一個資料,造成迴圈的假象。
這種方法是怎麼做的呢?,是這樣的:假如我們的列表有3條資料,用三個頁面展示,分別編號為1,2,3。我們再建立一個新的列表,長度為真實列表的長度+2,也就是5。在最前面插入最後一條資料,然後在末尾插入第一條資料。新列表就變成了這樣了,3-1-2-3-1。如果當前滑到的是0位置(頁面3),那就通過ViewPager的setCurrentItem(int item, boolean smoothScroll)
方法神不知鬼不覺的切換到3位置(頁面3),當滑到4的位置時(頁面1),也用這個方法滑到1位置(頁面1)。這樣給我們的感覺就是無限輪播了。來一張圖輔助理解一下。
2 . 採用getCount 返回 Integer.MAX_VALUE
讓ViewPager 的Adapter getCount 方法返回一個很大的數(這裡用Integer.MAX_VALUE),理論上可以無限滑動。當顯示完一個真實列表的週期後,又從真實列表的0位置顯示資料,造成無限迴圈輪播的假象。開始時呼叫 mViewPager.setCurrentItem(Integer.MAX_VALUE /2)設定選中中間位置,這樣最開始就可以向左滑動。關鍵程式碼:
int currentItem = getStartSelectItem();
//設定當前選中的Item
mViewPager.setCurrentItem(currentItem);
private int getStartSelectItem(){
// 我們設定當前選中的位置為Integer.MAX_VALUE / 2,這樣開始就能往左滑動
// 但是要保證這個值與getRealPosition 的 餘數為0,因為要從第一頁開始顯示
int currentItem = Integer.MAX_VALUE / 2;
if(currentItem % getRealCount() ==0 ){
return currentItem;
}
// 直到找到從0開始的位置
while (currentItem % getRealCount() != 0){
currentItem++;
}
return currentItem;
}複製程式碼
3 . 兩種方案選哪一種?
兩種方案我都試了一下,都可以實現輪播,但是第一種 方案在有切換動畫的時候是有問題的,因為上面我們說了滑動到最後一頁切換到第一頁時,用的是ViewPager的setCurrentItem(int item, boolean smoothScroll)
方法,smoothScroll 的值為false,這樣介面就感覺不到我們偷偷的切換。但是這樣切換就沒有了動畫。這樣每次切換就會很生硬,因此就拋棄這種方法。選擇第二種方案。
輪播我們採用Hanlder的postDelayed方法,關鍵程式碼如下:
private final Runnable mLoopRunnable = new Runnable() {
@Override
public void run() {
if(mIsAutoPlay){
mCurrentItem = mViewPager.getCurrentItem();
mCurrentItem++;
if(mCurrentItem == mAdapter.getCount() - 1){
mCurrentItem = 0;
mViewPager.setCurrentItem(mCurrentItem,false);
mHandler.postDelayed(this,mDelayedTime);
}else{
mViewPager.setCurrentItem(mCurrentItem);
mHandler.postDelayed(this,mDelayedTime);
}
}else{
mHandler.postDelayed(this,mDelayedTime);
}
}
};複製程式碼
在Adapter instantiateItem(ViewGroup container, final int position) 中,現在的這個position是一個很大的數字,我們需要將它轉換成一個真實的position,否則會越界報錯。
final int realPosition = position % getRealCount();複製程式碼
/**
* 獲取真實的Count
* @return
*/
private int getRealCount(){
return mDatas==null ? 0:mDatas.size();
}複製程式碼
通過以上就實現了仿魅族的BannerView,但是這還沒完,雖然功能實現了,要想在任何地方拿來就可以使用,簡單方便,我們還需要進一步的封裝。
封裝輪子:MZBannerView
通過上面幾步就可以實現仿魅族的BannerView,但是為了使用方便,我們將它封裝成一個庫,前面一篇文章講了,如何封裝一個通用的ViewPager(文章地址:ViewPager系列之 打造一個通用的ViewPager)。既然要想Banner使用方便,我們也需要封裝得通用,可擴充套件。因為我們的Banner也是用ViewPager 實現的,因此,我們可用上一篇文章的方法,封裝一個通用的BannerView。
MZBannerView 有以下功能:
1 . 仿魅族BannerView 效果。
2 . 當普通Banner 使用
3 . 當普通ViewPager 使用。
4 . 當普通ViewPager使用(有魅族Banner效果)
自定義屬性
屬性名 | 屬性意義 | 取值 |
---|---|---|
open_mz_mode | 是否開啟魅族模式 | true 為魅族Banner效果,false 則普通Banner效果 |
canLoop | 是否輪播 | true 輪播,false 則為普通ViewPager |
indicatorPaddingLeft | 設定指示器距離左側的距離 | 單位為 dp 的值 |
indicatorPaddingRight | 設定指示器距離右側的距離 | 單位為 dp 的值 |
indicatorAlign | 設定指示器的位置 | 有三個取值:left 左邊,center 劇中顯示,right 右側顯示 |
通過open_mz_mode
和canLoop
這兩個屬性來控制MZBannerView 是用作Banner還是普通ViewPager,有4種組合方式
1,仿魅族BannerView(預設的模式)
app:open_mz_mode="true"
app:canLoop="true"複製程式碼
2, 普通BannerView
app:open_mz_mode="false"
app:canLoop="true"複製程式碼
3 ,普通ViewPager (有魅族Banner的切換動畫)
app:open_mz_mode="true"
app:canLoop="false"複製程式碼
4, 普通ViewPager
app:open_mz_mode="false"
app:canLoop="false"複製程式碼
使用方法:
1 . xml 佈局檔案
<com.zhouwei.mzbanner.MZBannerView
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="10dp"
app:open_mz_mode="true"
app:canLoop="true"
app:indicatorAlign="center"
app:indicatorPaddingLeft="10dp"
/>複製程式碼
2 . activity中程式碼:
mMZBanner = (MZBannerView) view.findViewById(R.id.banner);
// 設定頁面點選事件
mMZBanner.setBannerPageClickListener(new MZBannerView.BannerPageClickListener() {
@Override
public void onPageClick(View view, int position) {
Toast.makeText(getContext(),"click page:"+position,Toast.LENGTH_LONG).show();
}
});
List<Integer> list = new ArrayList<>();
for(int i=0;i<RES.length;i++){
list.add(RES[i]);
}
// 設定資料
mMZBanner.setPages(list, new MZHolderCreator<BannerViewHolder>() {
@Override
public BannerViewHolder createViewHolder() {
return new BannerViewHolder();
}
});
public static class BannerViewHolder implements MZViewHolder<Integer> {
private ImageView mImageView;
@Override
public View createView(Context context) {
// 返回頁面佈局檔案
View view = LayoutInflater.from(context).inflate(R.layout.banner_item,null);
mImageView = (ImageView) view.findViewById(R.id.banner_image);
return view;
}
@Override
public void onBind(Context context, int position, Integer data) {
// 資料繫結
mImageView.setImageResource(data);
}
}複製程式碼
3 .如果是當Banner使用,注意在onResume 中呼叫start()方法,在onPause中呼叫 pause() 方法。如果當普通ViewPager使用,則不需要。
@Override
public void onPause() {
super.onPause();
mMZBanner.pause();//暫停輪播
}
@Override
public void onResume() {
super.onResume();
mMZBanner.start();//開始輪播
}複製程式碼
其他對外API
/******************************************************************************************************/
/** 對外API **/
/******************************************************************************************************/
//開始輪播
start()
//停止輪播
pause()
//設定BannerView 的切換時間間隔
setDelayedTime(int delayedTime)
// 設定頁面改變監聽器
addPageChangeLisnter(ViewPager.OnPageChangeListener onPageChangeListener)
//新增Page點選事件
setBannerPageClickListener(BannerPageClickListener bannerPageClickListener)
//設定是否顯示Indicator
setIndicatorVisible(boolean visible)
// 獲取ViewPager
ViewPager getViewPager()
// 設定 Indicator資源
setIndicatorRes(int unSelectRes,int selectRes)
//設定頁面資料
setPages(List<T> datas,MZHolderCreator mzHolderCreator)
//設定指示器顯示位置
setIndicatorAlign(IndicatorAlign indicatorAlign)
//設定ViewPager(Banner)切換速度
setDuration(int duration)複製程式碼
因為是對ViewPager的包裝,所有要設定某些ViewPager的屬性,可以通過getViewPager 獲取到ViewPager再設定對應屬性
效果圖:
1, BannerView 輪播效果圖:
2 普通ViewPager效果圖:
總結
本文講了如何實現一個仿魅族Banner效果。其中講了一些關鍵的點和關鍵程式碼。其實普通的BannerView 是一樣的,只是少了動畫而已。最後,將這些功能封裝成了一個通用的BannerView 控制元件。這個控制元件既有仿魅族Banner的效果,又可以當普通Banner使用。而且還可以當作一個普通的ViewPager使用。
更多詳細程式碼和使用方法請看github:github.com/pinguo-zhou…
最後,可能還有不完善的地方,如有問題,歡迎留言和提Issues。如果你覺得不錯,歡迎star和 Fxxk 。。。。啊呸。。。是fork。