RecyclerView如今已是愈來愈強大,且不說已經被你們用到倒背如流的代替ListView的基礎功能,如今RecyclerView還能夠取代ViewPager實現Banner效果,固然,如下作的小清新的Gallery效果也是相似於一些輪播圖的效果,以下圖所示,這其中使用到了24.2.0版本後RecyclerView增長的SnapHelper這個輔助類,在實現如下效果起來也是很是簡單。因此這也是爲何RecyclerView強大之處,由於Google一直在對RecyclerView不斷地進行更新補充,從而它內部的API也是愈來愈豐富。git
那麼咱們從水平滑動爲例,咱們細分爲如下幾個小問題:程序員
解決以上問題固然也不難,咱們分步來說解下實現思路:github
保持讓圖片保持在正中間,正如簡介中所說,在ToolsVersion24.2.0以後,Google給咱們提供了一個SnapHelper
的輔助類,它只須要幾行代碼就能幫助咱們實現滑動結束時保持在居中位置:算法
LinearSnapHelper mLinearySnapHelper = new LinearSnapHelper();
mLinearySnapHelper.attachToRecyclerView(mGalleryRecyclerView);
複製代碼
LinearSnapHelper
類繼承於SnapHelper
,固然SnapHelper
還有一個子類,叫作PagerSnapHelper
。它們之間的區別是,LinearSnapHelper
可使RecyclerView一次滑動越過多個Item,而PagerSnapHelper
像ViewPager同樣限制你一次只能滑動一個Item。ide
因爲第0個位置,和最後一個位置的圖片比較特殊,其餘圖片都默認設置他們的頁邊距和左右圖片的可視距離,因爲第0頁左邊沒有圖片,因此左邊只有1倍頁邊距,這樣滑動到最左邊時看起來就會比較奇怪,以下圖所示。工具
讓第0位置的圖片左邊保持和其餘圖片同樣的距離,那麼就須要動態設置第0位置圖片的左邊距爲2倍頁邊距 + 可視距離。同理,最後一張也是作一樣的操做。佈局
動態修改圖片的LayoutParams
,因爲RecyclerView對Holder的複用機制,咱們最好不要在Adapter裏面動態修改,這樣子首先不夠優雅,這裏感謝@W_BinaryTree
的建議,咱們給RecyclerView添加一個自定義的Decoration會讓咱們的代碼更加優雅,只須要重寫RecyclerView.ItemDecoration
裏面的getItemOffsets(Rect outRect, final View view, final RecyclerView parent, RecyclerView.State state)
方法,並在裏面設置每一頁的參數便可,修改以下:性能
public class GalleryItemDecoration extends RecyclerView.ItemDecoration {
int mPageMargin = 0; // 每個頁面默認頁邊距
int mLeftPageVisibleWidth = 50; // 中間頁面左右兩邊的頁面可見部分寬度
public static int mItemComusemX = 0; // 一頁理論消耗距離
@Override
public void getItemOffsets(Rect outRect, final View view, final RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
// ...
// 動態修改頁面的寬度
int itemNewWidth = parent.getWidth() - dpToPx(4 * mPageMargin + 2 * mLeftPageVisibleWidth);
// 一頁理論消耗距離
mItemComusemX = itemNewWidth + OsUtil.dpToPx(2 * mPageMargin);
// 第0頁和最後一頁沒有左頁面和右頁面,讓他們保持左邊距和右邊距和其餘項同樣
int leftMargin = position == 0 ? dpToPx(mLeftPageVisibleWidth + 2 * mPageMargin) : dpToPx(mPageMargin);
int rightMargin = position == itemCount - 1 ? dpToPx(mLeftPageVisibleWidth + 2 * mPageMargin) : dpToPx(mPageMargin);
// 設置參數
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) itemView.getLayoutParams();
lp.setMargins(leftMargin, 0, rightMargin, 0);
lp.width = itemWidth;
itemView.setLayoutParams(lp);
// ...
}
public int dpToPx(int dp) {
return (int) (dp * Resources.getSystem().getDisplayMetrics().density + 0.5f);
}
}
複製代碼
而後,把GalleryItemDecoration
傳入便可:動畫
mGalleryRecyclerView.addItemDecoration(new GalleryItemDecoration());
複製代碼
這個問題涉及到比較多的問題。spa
(a) 獲取滑動過程當中當前位置。
首先,RecyclerView當前的API,並不能讓咱們在滑動的過程當中,簡單地獲取到咱們圖中效果中間圖片的位置,或許你會說,能夠經過 mGalleryRecyclerView.getLinearLayoutManager().findFirstVisibleItemPosition()
能拿到RecyclerView中第一個可見的位置,可是經過效果能夠知道,咱們每個張照片(除去第一張和最後一張)左右兩邊都是有前一張照片和最後一張照片的部份內容的,因此須要作區分判斷是不是中間的照片仍是第一張亦或最後一張,而後返回mGalleryRecyclerView.getLinearLayoutManager().findFirstVisibleItemPosition() + 1
或者其餘。 那麼這樣又會引出一個問題,當咱們把先後照片展現的寬度設置成可配置,即先後照片的露出部分寬度是可配置,那麼當咱們把屏幕不顯示先後照片遺留部分在屏幕的話,那麼咱們這一個方法又不能兼容了,因此經過這一個方法來獲取,或許不那麼靠譜。
咱們能夠這樣來計算出比較準確的位置。在RecyclerView中,咱們能夠監聽它的滑動事件:
// 滑動監聽
mGalleryRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// 經過dx或者dy來計算位置。
}
});
複製代碼
裏面有一個onScrolled(int dx, int dy)
方法,這裏面的dx,dy很是有用。首先,經過判斷dx,dy是否大於0能夠判斷它是上、下、左、右滑動,dx > 0右滑,反之左滑,dy > 0 下滑,反之上滑(固然,我這裏的滑動是相對於RecyclerView,即列表的滑動方向,手指的滑動方向和這裏相反)。其次,dx和dy還能監聽每一次滑動在x,y軸上消耗的距離。
舉個例子,當咱們迅速至列表右邊時,onScrolled(int dx, int dy)
會不斷被調用,經過在方法裏面Log輸出,你會看到不斷輸出dx的值,並且他們的大小都是無規律的,而這裏的dx就是每一次onScroll
方法調用一次,RecyclerView在x軸上的消耗距離。
因此咱們能夠經過一個全局變量mConsumeX
來累加全部dx,當這樣咱們就能夠知道當前RecyclerView滑動的總距離。而咱們Demo中每移動到下一張照片的距離(即以下圖中所示的移動一頁理論消耗距離)是必定的,那麼就能夠經過當前位置 = mConsumeX / 移動一張照片所須要的距離
來獲取滑動結束時的位置了。
/**
* 獲取位置
*
* @param mConsumeX 實際消耗距離
* @param shouldConsumeX 移動一頁理論消耗距離
* @return
*/
private int getPosition(int mConsumeX, int shouldConsumeX) {
float offset = (float) mConsumeX / (float) shouldConsumeX;
int position = Math.round(offset); // 四捨五入獲取位置
return position;
}
複製代碼
(b) 根據位置獲取當前頁的滑動偏移率
當咱們能夠準確拿到當前位置時,咱們就須要明確一下幾個概念。
總的偏移距離
:意思是從第一個位置移動到如今當前位置偏移的總距離,即dx的累加結果(也就是上述的mConsumX)。
當前頁偏移距離
:意思是從上一個位置移動到當前位置偏移距離。
總的偏移率
:意思是 總的偏移距離 / 移動一頁理論消耗距離。
當前頁的偏移率
:意思是 當前頁偏移距離 / 移動一頁理論消耗距離。
咱們都知道,獲取當前位置方法裏面有一個
float offset = (float) mConsumeX / (float) shouldConsumeX;
複製代碼
它的意思就是總的偏移率,例如圖中咱們當前位置是3,咱們從3移動到4時,onScroll
方法會不斷被調用,那麼這個offset就會不斷變化,從3.0逐漸增長一直到4.0,圖中此時的offset大概是3.2左右,咱們知道這一個有什麼用呢?試想一下,offset是一個浮點型數,將它向下取整,那就是變成3了,那麼3.2 - 3 = 0.2就是咱們當前頁的偏移率了。而咱們經過偏移率就能夠動態設置圖片的大小,就造成了咱們這個問題中所說的圖片大小變化效果。因此這裏的關鍵就是獲取到當前頁的偏移率
。
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
// ...
// 移動一頁理論消耗距離
int shouldConsumeX = GalleryItemDecoration.mItemComusemX;
// 獲取當前的位置
int position = getPosition(mConsumeX, shouldConsumeX);
// 位置浮點值(即總消耗距離 / 每一頁理論消耗距離 = 一個浮點型的位置值)
float offset = (float) mConsumeX / (float) shouldConsumeX;
// 避免offset值取整時進一,從而影響了percent值
if (offset >= mGalleryRecyclerView.getLinearLayoutManager().findFirstVisibleItemPosition() + 1 && slideDirct == SLIDE_RIGHT) {
return;
}
// 當前頁的偏移率
float percent = offset - ((int) offset);
// 設置動畫變化
setAnimation(recyclerView, position, percent);
// ...
}
複製代碼
(c) 根據偏移率實現動畫
如今咱們拿到了偏移率,就能夠動態修改它們的尺寸大小了,首先,咱們須要拿到當前View,前一個View和後一個View,並同時對它們作Scale伸縮。即上面的setAnimation(recyclerView, position, percent)
方法裏面進行動畫操做。
View mCurView = recyclerView.getLayoutManager().findViewByPosition(position); // 中間頁
View mRightView = recyclerView.getLayoutManager().findViewByPosition(position + 1); // 左邊頁
View mLeftView = recyclerView.getLayoutManager().findViewByPosition(position - 1); // 右邊頁
複製代碼
認真觀察圖中變化,兩種變化:
理解了以上的變化以後,咱們就能夠作動畫了。
首先說明一點,你們觀察個人getPosition(mConsumeX, shouldConsumeX)
方法,裏面的實現是,當一頁滑動的偏移率超過了0.5以後,position就會自動切換到下一頁。固然你的實現邏輯不同,那麼後面你的設置動畫的方法就不同。爲何須要明確這一點呢?由於當我滑動超過圖片超過它的一半寬度以後,上面的mCurView就會切換成下一張圖片了,因此我在設置動畫的方法裏以0.5爲一個臨界點,由於0.5臨界點的兩邊,mCurView
,mRightView
,mLeftView
的指向都已經不同了。
假如咱們定義大小變化因子 float mAnimFactor = 0.2f
,它的意思就是控制咱們的圖片從1.0伸縮至0.8。以上圖爲例,當percent <= 0.5時,mCurView
的ScaleX和ScaleY從大慢慢變小,至於這個變化範圍,就根據咱們定義的變化因子和percent來修改;而當percent > 0.5時,剛纔那個View就變成了mLeftView
,此時咱們繼續剛纔的操做,整個過程咱們就實現了第一張圖片的Scale從1.0變化到了0.8。而另外兩張圖片也是同理,大概代碼邏輯以下:
private void setBottomToTopAnim(RecyclerView recyclerView, int position, float percent) {
View mCurView = recyclerView.getLayoutManager().findViewByPosition(position); // 中間頁
View mRightView = recyclerView.getLayoutManager().findViewByPosition(position + 1); // 左邊頁
View mLeftView = recyclerView.getLayoutManager().findViewByPosition(position - 1); // 右邊頁
if (percent <= 0.5) {
if (mLeftView != null) {
// 變大
mLeftView.setScaleX((1 - mAnimFactor) + percent * mAnimFactor);
mLeftView.setScaleY((1 - mAnimFactor) + percent * mAnimFactor);
}
if (mCurView != null) {
// 變小
mCurView.setScaleX(1 - percent * mAnimFactor);
mCurView.setScaleY(1 - percent * mAnimFactor);
}
if (mRightView != null) {
// 變大
mRightView.setScaleX((1 - mAnimFactor) + percent * mAnimFactor);
mRightView.setScaleY((1 - mAnimFactor) + percent * mAnimFactor);
}
} else {
if (mLeftView != null) {
mLeftView.setScaleX(1 - percent * mAnimFactor);
mLeftView.setScaleY(1 - percent * mAnimFactor);
}
if (mCurView != null) {
mCurView.setScaleX((1 - mAnimFactor) + percent * mAnimFactor);
mCurView.setScaleY((1 - mAnimFactor) + percent * mAnimFactor);
}
if (mRightView != null) {
mRightView.setScaleX(1 - percent * mAnimFactor);
mRightView.setScaleY(1 - percent * mAnimFactor);
}
}
}
複製代碼
高斯模糊有挺多種實現方法的,Google一下就出來了。可是仍是推薦Native層的實現算法,由於Java層的實現對性能影響實在太大了,例子裏使用的是RenderScript
,固然是參考博主湫水
的教你一分鐘實現動態模糊效果,你們感興趣能夠過去看看,用法也是很是簡單。直接調用blurBitmap(Context context, Bitmap image, float blurRadius)
方法便可。
public class BlurBitmapUtil {
//圖片縮放比例
private static final float BITMAP_SCALE = 0.4f;
/**
* 模糊圖片的具體方法
*
* @param context 上下文對象
* @param image 須要模糊的圖片
* @return 模糊處理後的圖片
*/
public static Bitmap blurBitmap(Context context, Bitmap image, float blurRadius) {
// 計算圖片縮小後的長寬
int width = Math.round(image.getWidth() * BITMAP_SCALE);
int height = Math.round(image.getHeight() * BITMAP_SCALE);
// 將縮小後的圖片作爲預渲染的圖片
Bitmap inputBitmap = Bitmap.createScaledBitmap(image, width, height, false);
// 建立一張渲染後的輸出圖片
Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap);
// 建立RenderScript內核對象
RenderScript rs = RenderScript.create(context);
// 建立一個模糊效果的RenderScript的工具對象
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
// 因爲RenderScript並無使用VM來分配內存,因此須要使用Allocation類來建立和分配內存空間
// 建立Allocation對象的時候其實內存是空的,須要使用copyTo()將數據填充進去
Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);
// 設置渲染的模糊程度, 25f是最大模糊度
blurScript.setRadius(blurRadius);
// 設置blurScript對象的輸入內存
blurScript.setInput(tmpIn);
// 將輸出數據保存到輸出內存中
blurScript.forEach(tmpOut);
// 將數據填充到Allocation中
tmpOut.copyTo(outputBitmap);
return outputBitmap;
}
}
複製代碼
這個方法只要傳入Context,Bitmap,和一個模糊程度便可,而後返回一個高斯模糊後的Bitmap給咱們,咱們只須要將RecyclerView的父佈局設置背景爲這個Bitmap便可。
實現這個效果最好不要使用Tween動畫,由於它的實現效果比較生硬,使用TransitionDrawable
會讓效果更佳接近淡入淡出效果。那咱們怎麼記錄先後兩個位置的照片呢?方法不少種,這裏就使用了一個Map<String, Drwable>來記錄每一次顯示的圖片,在它切換到下一個圖片時,便從上一次記錄的圖片淡入淡出到本次的圖片。
// 獲取當前位置的圖片資源ID
int resourceId = ((RecyclerAdapter) mRecyclerView.getAdapter()).getResId(mRecyclerView.getScrolledPosition());
// 將該資源圖片轉爲Bitmap
Bitmap resBmp = BitmapFactory.decodeResource(getResources(), resourceId);
// 將該Bitmap高斯模糊後返回到resBlurBmp
Bitmap resBlurBmp = BlurBitmapUtil.blurBitmap(mRecyclerView.getContext(), resBmp, 15f);
// 再將resBlurBmp轉爲Drawable
Drawable resBlurDrawable = new BitmapDrawable(resBlurBmp);
// 獲取前一頁的Drawable
Drawable preBlurDrawable = mTSDraCacheMap.get(KEY_PRE_DRAW) == null ? resBlurDrawable : mTSDraCacheMap.get(KEY_PRE_DRAW);
/* 如下爲淡入淡出效果 */
Drawable[] drawableArr = {preBlurDrawable, resBlurDrawable};
TransitionDrawable transitionDrawable = new TransitionDrawable(drawableArr);
mContainer.setBackgroundDrawable(transitionDrawable);
transitionDrawable.startTransition(500);
// 存入到cache中
mTSDraCacheMap.put(KEY_PRE_DRAW, resBlurDrawable);
複製代碼
以上所講的都是實現的一個思路,雖然效果和小清新搭不上關係哈,可是配了幾張小清新的圖片仍是讓咱們的程序員生活增添一絲精彩。其實你們實現了基礎效果以後,還能夠深挖更多輔助功能,例如不一樣的切換效果,支持橫屏,動態修改滑動速度等,相信這個過程可讓你收穫良多。
Github:Recyclerview-Gallery