前言
很多APP的首頁通常會有一個帶有動畫切換的各種輪播圖效果,剛好新項目中也要實現輪播圖的效果,于是便研究了Android平臺下各種輪播效果,網上也有很多實現輪播相關的方案,但是質量參差不齊,為此踩了不少的坑。下面就來關于輪播圖實現方面的一些學習心得,希望對大家有所幫助。
**(一)使用ViewPager實現輪播圖切換效果 **
開源項目:Android-Coverflow
GitHub地址:https://github.com/crosswall/Android-Coverflow
其效果如下:
**(1)ViewPager切換動畫實現原理 **
- 使用View.PageTransformer實現動畫切換效果,ViewPager的動畫切換效果都是重寫PageTransformer這個類來實現的。PageTransformer方法中就一個transformPage方法,里面有兩個參數,viewPager的滑動時的子View,這個很好理解,最難理解的是這個position參數。切換時的動畫實現主要靠position來顯示。下面來介紹其中含義。
public interface PageTransformer {
void transformPage(View page, float position);
}
ViewPager左右切換時,position的值范圍說明:
- **(-oo,-1) 相對于左邊第一頁,其左邊的所有頁面 **
- *** [-1, 0 ) 相對于當前選中頁,其左邊的第一頁**
- *** [0, 1 ) 相對于當前選中頁,其右邊第一頁 **
- [1,+oo) 相對于右邊第一頁,其右邊的所有頁面
下面舉例說明:
- **當前ViewPage選中的頁為,其右邊的頁面為B,現在向左滑動A,慢慢由頁面A切換到頁面B **
**頁面A的position值是由 0 慢慢減小到 -1 [0,-1] **
**頁面B的position值是由 1 慢慢減小到 0 [1,0] **
**此時頁面B為ViewPage當前選中的頁面 **
- **再向右滑動頁面B,慢慢由頁面B切換到頁面A **
**頁面A的position值由 -1 慢慢增加到 0 [-1,0] **
頁面B的position值由 0 慢慢增加到 1 [ 0,1]
理解了transformPage方法中position的含義,那么ViewPager動畫切換效果的實現就很好理解了。下面是項目中我實現的一個ViewPager切換效果,如果所示:
代碼:
public class ZoomPageTransformer implements ViewPager.PageTransformer {
private static final float MAX_SCALE = 1.0f;
private static final float MIN_SCALE = 0.85f;//0.85f
private static final float MIN_ALPHA = 0.3f;
private static final String TAG = "PageTransformer";
@Override
public void transformPage(View view, float position) {
//setScaleY只支持api11以上
if (position < -1) {
view.setScaleX(MIN_SCALE);
view.setScaleY(MIN_SCALE);
view.setAlpha(MIN_ALPHA);//左邊的左邊的Page
} else if (position <= 1) {
float scaleFactor = MIN_SCALE + (1 - Math.abs(position)) * (MAX_SCALE - MIN_SCALE);
if (position > 0) {
view.setTranslationX(-scaleFactor);
} else if (position < 0) {
view.setTranslationX(scaleFactor);
}
view.setScaleY(scaleFactor);
view.setScaleX(scaleFactor);
// float alpha = 1f - Math.abs(position) * (1 - );
float alpha = MIN_ALPHA + (1 - MIN_ALPHA) * (1 - Math.abs(position));
view.setAlpha(alpha);
Log.i(TAG,"position = " + position + " alpha = " + alpha);
} else { // (1,+Infinity]
view.setScaleX(MIN_SCALE);
view.setScaleY(MIN_SCALE);
view.setAlpha(MIN_ALPHA);
}
}
}
當position的范圍在(-oo,-1) 和[1,+oo)時,view的比例縮小MIN_SCALE(0.85f),透明度縮小到 MIN_ALPHA(0.3f)
當position的范圍在 [-1,1]的時候,View的scale值和position絕對值成反比
float scaleFactor = MIN_SCALE + (1 - Math.abs(position)) * (MAX_SCALE - MIN_SCALE);
if (position > 0) {
view.setTranslationX(-scaleFactor);
} else if (position < 0) {
view.setTranslationX(scaleFactor);
}
view.setScaleY(scaleFactor);
view.setScaleX(scaleFactor);
當position的值等于-1或者1的時候,此時View就是左邊第一頁和右邊第一頁,此時scale值就是MIN_SCALE(0.85f)
當position的值范圍在[-1,0]時,scale的值慢慢由MIN_SCALE變大到MAX_SCALE,也就是說ViewPager右滑動時,左邊第一個View是慢慢放大的,直到其放大到MAX_SCALE。
當position的值范圍在(0,1]時,scale的值由MAX_SCALE變小到MIN_SCALE,也就是說ViewPager右滑動時,右邊第一個View是慢慢縮小的,直到其比例縮放到MIN_SCALE。
透明度變化也是同樣一個原理。
**(2)讓ViewPager顯示多頁 **
一般情況下,ViewPager只能顯示一頁,那如何讓其顯示多個子頁面呢?那就不得不說setClipChildren(false)這個方法了。
- 默認情況下為setClipChildren(true),如果子View的布局范圍超過了父View,那么它的邊界將會被裁減掉,也就是說超過父View的部分是看不到的。
- 當setClipChildren(false)的情況下,子View的布局范圍超過了父View部分將不會被裁減掉 ,而將會以動畫的形式顯示出來。
<me.crosswall.lib.coverflow.core.PagerContainer
android:id="@+id/pager_container"
android:layout_width="match_parent"
android:layout_height="220dp"
android:clipChildren="false"
android:background="?attr/colorPrimary">
<android.support.v4.view.ViewPager
android:id="@+id/overlap_pager"
android:layout_width="300dp"
android:layout_height="200dp"
android:layout_gravity="center" />
</me.crosswall.lib.coverflow.core.PagerContainer>
Layout.xml
<com.mtime.cinema.business.recommend.widget.BannerView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/recommend_banner_height"
android:clipChildren="false">
<android.support.v4.view.ViewPager
android:id="@+id/fragment_recommend_viewPager"
android:layout_width="@dimen/recommend_banner_image_width"
android:layout_height="@dimen/recommend_banner_image_height"
android:layout_centerHorizontal="true"
android:clipChildren="false" />
<LinearLayout
android:id="@+id/fragment_recommend_banner_indicator"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="25dp"
android:gravity="center_horizontal"
android:orientation="horizontal" />
</com.mtime.cinema.business.recommend.widget.BannerView>
JAVA代碼
/**
* Created by liuyu on 2017/4/17.
* 推薦頁輪播圖控件
*/
public class BannerView extends RelativeLayout {
private static final String TAG = "BannerView";
private LinearLayout mBannerIndicator;
private ViewPager mViewPager;
private long VIEWPAGER_SWITCH_DURING = 8000;//輪播時間
private int MSG_START_SCROLL = 100;//消息的名稱
private int mPointRadius;
private int mPointTotalCount;//小圓點真正個數,有可能接口返回數據記錄 < DEFAULT_POINT_COUNT
private Drawable mNormalColor, mSelectedColor;
private BannerAdapter mBannerAdapter;
private int mPointMarginTop;
public BannerView(Context context) {
super(context);
initBanner();
}
public BannerView(Context context, AttributeSet attrs) {
super(context, attrs);
initBanner();
}
private void initBanner() {
this.setClipChildren(false);
mPointRadius = getContext().getResources().getDimensionPixelSize(R.dimen.recommend_banner_point_radius);
mPointMarginTop = getContext().getResources().getDimensionPixelSize(R.dimen.recommend_banner_point_margin_top);
mNormalColor = getResources().getDrawable(R.drawable.bg_shape_recommend_banner_point_normal);
mSelectedColor = getResources().getDrawable(R.drawable.bg_shape_recommend_banner_point_selected);
}
public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initBanner();
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG_START_SCROLL) {
if (mHandler != null) {
mHandler.removeMessages(MSG_START_SCROLL);
mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);
mHandler.sendEmptyMessageDelayed(MSG_START_SCROLL, VIEWPAGER_SWITCH_DURING);
}
}
}
};
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mBannerIndicator = (LinearLayout) findViewById(R.id.fragment_recommend_banner_indicator);
mViewPager = (ViewPager) findViewById(R.id.fragment_recommend_viewPager);
}
/**
* 添加小圓點
*/
private void addDots(int count) {
mPointTotalCount = count;
// mBannerIndicator.removeAllViews();
Logger.i(TAG, "addDots count = " + count);
//加點
for (int i = 0; i < count; i++) {
ImageView pointView = new ImageView(getContext());
pointView.setImageDrawable(getResources().getDrawable(R.drawable.bg_shape_recommend_banner_point_normal));
//點的大小
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(mPointRadius * 2, mPointRadius * 2);
//點的間隔
layoutParams.leftMargin = mPointRadius * 2;
//居中顯示
layoutParams.topMargin = mPointMarginTop;
Logger.i(TAG, "addDots index = " + i);
pointView.setLayoutParams(layoutParams);
//把點添加到容器中
mBannerIndicator.addView(pointView);
}
}
/**
* @param dataList
*/
public void show(List<BannerBean> dataList) {
Logger.i(TAG, "start show banner");
if (dataList == null || dataList.size() == 0) {
return;
}
/* if (mViewPager.getChildCount() > 0 && mBannerIndicator.getChildCount() > 0) {
mBannerAdapter.notifyDataSetChanged();
}*/
//重置數據
if (mViewPager.getChildCount() > 0) {
mViewPager.removeAllViews();
}
if (mBannerIndicator.getChildCount() > 0) {
mBannerIndicator.removeAllViews();
}
addDots(dataList.size());
/**** 重要部分 ******/
//clipChild用來定義他的子控件是否要在他應有的邊界內進行繪制。 默認情況下,clipChild被設置為true。 也就是不允許進行擴展繪制。
mViewPager.setClipChildren(false);
//父容器一定要設置這個,否則看不出效果
//mViewPager.setPageMargin(getResources().getDimensionPixelOffset(R.dimen.recommend_banner_image_space));
mViewPager.setOffscreenPageLimit(5);
final List<String> imageList = new ArrayList<>();
//add image url
mBannerAdapter = new BannerAdapter(getContext(), dataList);
mBannerAdapter.setImageList(imageList);
mViewPager.setAdapter(mBannerAdapter);
int currentItem = Integer.MAX_VALUE / 2;
mViewPager.setCurrentItem(currentItem);
changeIndicatorStatus(currentItem);
//設置ViewPager切換效果,即實現畫廊效果
mViewPager.setPageTransformer(true, new ZoomPageTransformer());
//將容器的觸摸事件反饋給ViewPager
this.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// dispatch the events to the ViewPager, to solve the problem that we can swipe only the middle view.
return mViewPager.dispatchTouchEvent(event);
}
});
//圖片數量大于1的時候,才進行自動輪播
if (dataList.size() > 1) {
startAutoScroll();
}
mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
changeIndicatorStatus(position);
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
/**
* 開始自動切換
*/
public void startAutoScroll() {
if (mHandler != null) {
mHandler.removeMessages(MSG_START_SCROLL);
mHandler.sendEmptyMessageDelayed(MSG_START_SCROLL, VIEWPAGER_SWITCH_DURING);
}
}
/**
* 停止自動切換
*/
public void stopScroll() {
mHandler.removeMessages(MSG_START_SCROLL);
}
public void changeIndicatorStatus(int position) {
if (position == 0) {
return;
}
int realPos = position % mPointTotalCount;
for (int i = 0; i < mPointTotalCount; i++) {
if (i == realPos) {
((ImageView) mBannerIndicator.getChildAt(i)).setImageDrawable(mSelectedColor);
} else {
((ImageView) mBannerIndicator.getChildAt(i)).setImageDrawable(mNormalColor);
}
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
clear();
}
/**
* 數據清理
*/
public void clear() {
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
}
}
(二)使用FancyCoverFlow實現輪播圖切換效果
Github地址:https://github.com/davidschreiber/FancyCoverFlow
(1) FancyCoverFlow的使用
- Java代碼
this.fancyCoverFlow.setUnselectedAlpha(0.0f);
// 未選中的飽和度
this.fancyCoverFlow.setUnselectedSaturation(0.0f);
// 未選中的比例
this.fancyCoverFlow.setUnselectedScale(0.8f);
// child間距
this.fancyCoverFlow.setSpacing(-60);
// 旋轉度數
this.fancyCoverFlow.setMaxRotation(0);
// 非選中的重心偏移,負的向上
this.fancyCoverFlow.setScaleDownGravity(-1f);
// 作用距離
this.fancyCoverFlow.setActionDistance(FancyCoverFlow.ACTION_DISTANCE_AUTO);
- XML布局文件
<at.technikum.mti.fancycoverflow.FancyCoverFlow
android:layout_width="match_parent"
android:layout_height="match_parent"
fcf:maxRotation="45"
fcf:unselectedAlpha="0.3"
fcf:unselectedSaturation="0.0"
fcf:unselectedScale="0.4" />
(2) 實現無限循環效果
FancyCoverFlow是通過繼承Galley實現的,那么我們可以利用Galley的setSelection()方法實現一些特殊的效果,例如打造無限循環的錄播圖。
FancyCoverFlowSampleAdapter.java
public class FancyCoverFlowSampleAdapter extends FancyCoverFlowAdapter {
// =============================================================================
// Private members
// =============================================================================
private int[] images = {
R.drawable.image1, R.drawable.image2, R.drawable.image3,
R.drawable.image6, R.drawable.image5, R.drawable.image4
};
// =============================================================================
// Supertype overrides
// =============================================================================
@Override
public Integer getItem(int i) {
return images[ i];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getCount() {
return Integer.MAX_VALUE;
}
@Override
public View getCoverFlowItem(int i, View reuseableView, ViewGroup viewGroup) {
ImageView imageView = null;
if (reuseableView != null) {
imageView = (ImageView) reuseableView;
} else {
imageView = new ImageView(viewGroup.getContext());
imageView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
imageView.setLayoutParams(new FancyCoverFlow.LayoutParams(300, 400));
}
imageView.setImageResource(this.getItem(i % images.length));
return imageView;
}
}
MainActivity.java
this.fancyCoverFlow = (FancyCoverFlow) this.findViewById(R.id.fancyCoverFlow);
this.fancyCoverFlow.setAdapter(new FancyCoverFlowSampleAdapter());
this.fancyCoverFlow.setUnselectedAlpha(1.0f);
this.fancyCoverFlow.setUnselectedSaturation(0.0f);
this.fancyCoverFlow.setUnselectedScale(0.6f);
this.fancyCoverFlow.setSpacing(-20);
this.fancyCoverFlow.setMaxRotation(40);
this.fancyCoverFlow.setScaleDownGravity(0.2f);
this.fancyCoverFlow.setActionDistance(FancyCoverFlow.ACTION_DISTANCE_AUTO);
this.fancyCoverFlow.setSelection(Integer.MAX_VALUE/2);
效果如下:
(三)使用RecyclerCoverFlow實現輪折疊的輪播圖效果
Github地址:https://github.com/ChenLittlePing/RecyclerCoverFlow
效果如下:
** (1) RecyclerCoverFlow的使用**
- xml布局
<recycler.coverflow.RecyclerCoverFlow
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent">
</recycler.coverflow.RecyclerCoverFlow>
- Activity中引入
mList = (RecyclerCoverFlow) findViewById(R.id.list);
// mList.setFlatFlow(true); //平面滾動
mList.setAdapter(new Adapter(this));
mList.setOnItemSelectedListener(new CoverFlowLayoutManger.OnSelected() {
@Override
public void onItemSelected(int position) {
((TextView)findViewById(R.id.index)).setText((position+1)+"/"+mList.getLayoutManager().getItemCount());
}
});
(2) 實現原理
RecyclerCoverFlow是通過集成RecyclerView實現的,由于默認情況下,ViewGroup中的子view繪制順序,index越大,其繪制順序會越靠后,所以后面的子View會遮住前面的子view,導致居中顯示的子View右邊重疊部分會被靠后的子View遮住。
@Override
protected int getChildDrawingOrder(int childCount, int i) {
int center = getCoverFlowLayout().getCenterPosition()
- getCoverFlowLayout().getFirstVisiblePosition(); //計算正在顯示的所有Item的中間位置
if (center < 0) center = 0;
else if (center > childCount) center = childCount;
int order;
if (i == center) {
order = childCount - 1;
} else if (i > center) {
order = center + childCount - 1 - i;
} else {
order = i;
}
return order;
}