用RecyclerView作一個小清新的Gallery效果

1、簡介

RecyclerView如今已是愈來愈強大,且不說已經被你們用到倒背如流的代替ListView的基礎功能,如今RecyclerView還能夠取代ViewPager實現Banner效果,固然,如下作的小清新的Gallery效果也是相似於一些輪播圖的效果,以下圖所示,這其中使用到了24.2.0版本後RecyclerView增長的SnapHelper這個輔助類,在實現如下效果起來也是很是簡單。因此這也是爲何RecyclerView強大之處,由於Google一直在對RecyclerView不斷地進行更新補充,從而它內部的API也是愈來愈豐富。git

小清新的Gallery水平滑動效果

小清新的Gallery垂直滑動效果

210*378

210*378

那麼咱們從水平滑動爲例,咱們細分爲如下幾個小問題:程序員

  1. 每一次滑動都讓圖片保持在正中間。
  2. 第一張圖片的左邊距和最後一張的右邊距須要保持和其餘照片的左右邊距同樣。
  3. 滑動時,中間圖片滑動到左邊時從大變小,右邊圖片滑動到中間時從小變大。
  4. 背景實現高斯模糊。
  5. 滑動結束時背景有一個漸變效果,從上一張圖片淡入淡出到當前圖片。

2、實現思路

解決以上問題固然也不難,咱們分步來說解下實現思路:github

(1) 每一次滑動都讓圖片保持在正中間

保持讓圖片保持在正中間,正如簡介中所說,在ToolsVersion24.2.0以後,Google給咱們提供了一個SnapHelper的輔助類,它只須要幾行代碼就能幫助咱們實現滑動結束時保持在居中位置:算法

LinearSnapHelper mLinearySnapHelper = new LinearSnapHelper();
mLinearySnapHelper.attachToRecyclerView(mGalleryRecyclerView);
複製代碼

LinearSnapHelper類繼承於SnapHelper,固然SnapHelper還有一個子類,叫作PagerSnapHelper。它們之間的區別是,LinearSnapHelper可使RecyclerView一次滑動越過多個Item,而PagerSnapHelper像ViewPager同樣限制你一次只能滑動一個Item。ide

(2) 第一張圖片的左邊距和最後一張的右邊距須要保持和其餘照片的左右邊距同樣

因爲第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());
複製代碼

(3) 滑動時,中間圖片滑動到左邊時從大變小,右邊圖片滑動到中間時從小變大

這個問題涉及到比較多的問題。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 / 移動一張照片所須要的距離來獲取滑動結束時的位置了。

RecyclerView距離示意圖

/**
 * 獲取位置
 *
 * @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);  // 右邊頁
複製代碼

認真觀察圖中變化,兩種變化:

  1. 位置的變化:第一張圖片是從mCurView慢慢變成mLeftView,而第二張圖片是從mRightView慢慢變成mCurView。
  2. 大小變化:第一張圖是從大變小,第二張圖是從小變大。

理解了以上的變化以後,咱們就能夠作動畫了。

首先說明一點,你們觀察個人getPosition(mConsumeX, shouldConsumeX)方法,裏面的實現是,當一頁滑動的偏移率超過了0.5以後,position就會自動切換到下一頁。固然你的實現邏輯不同,那麼後面你的設置動畫的方法就不同。爲何須要明確這一點呢?由於當我滑動超過圖片超過它的一半寬度以後,上面的mCurView就會切換成下一張圖片了,因此我在設置動畫的方法裏以0.5爲一個臨界點,由於0.5臨界點的兩邊,mCurViewmRightViewmLeftView的指向都已經不同了。

假如咱們定義大小變化因子 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);
        }
    }
}
複製代碼

(4)背景實現高斯模糊

高斯模糊有挺多種實現方法的,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便可。

(5)滑動結束時背景有一個漸變效果,從上一張圖片淡入淡出到當前圖片

實現這個效果最好不要使用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

相關文章
相關標籤/搜索