您好,登錄后才能下訂單哦!
由于業務需求,需要做一個卡片分享功能,前期做了一些預研,實現類似效果可以采用如下兩種方式:
由于RecyclerView自帶復用設計,方便后期拓展,所以就采用RecyclerView這個方案,主要實現的細節效果和功能如下:
1.分頁,自動居中
2.卡片樣式及效果,陰影等
3.背景色漸變
4.切換卡片,卡片的縮放效果
5.指示器
6.卡片分享
效果圖:
RecyclerView這個方向的資料還是比較好查找,不過細節和想實現的效果還是有些許出入。針對這些問題,逐步探索,經過多次改良后,得到了較為滿意的結果。
本文滑動是橫向滑動,如果讀者想要縱向的,可以使用RecyclerView的LinearLayoutManager設置方向,其他代碼大體相同。
下面我就根據效果逐一給讀者提供相關代碼實現,并針對實現細節、難點,附上開發思路供大家參考。
難點:
public class CardPagerSnapHelper extends PagerSnapHelper {
public boolean mNoNeedToScroll = false;
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
if (mNoNeedToScroll) {
return new int[]{0, 0};
} else {
return super.calculateDistanceToFinalSnap(layoutManager, targetView);
}
}
}
//使用.
mPageSnapHelp.attachToRecyclerView(mRecyclerView);
這里繼承PagerSnapHelper是因為想要的效果是一頁的滑動。如果想要的是可以滑動多頁,可以使用LinearSnapHelper,設置對應的朝向即可,另外繼承這個也可以設置阻尼大小,還可控制滑動速度。
我這里主要是根據要求做了如下方面的修改,讀者可以根據需求,增加動畫,列表,點擊反饋等。
1)陰影、圓角等
這是我用到的設置,讀者可以根據實際效果對比界面設計做調整:
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:cardBackgroundColor="@color/white"
app:cardElevation="6dp"
app:cardMaxElevation="12dp"
app:cardPreventCornerOverlap="true"
app:cardUseCompatPadding="false">
2)卡片比例動態調整
卡片
要保持在不同屏幕下卡片比例保持不變,就需要根據屏幕的分辨率動態的設置卡片的寬高。
---- CardAdapter.java ----
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_poster, parent, false);
mCardAdapterHelper.onCreateViewHolder(parent, itemView, 0.72f, (float) (17.0 / 25.0));
return new ViewHolder(itemView);
}
---- CardAdapterHelper.java ----
/**
* @param parent
* @param itemView
* @param cardPercentWidth 卡片占據屏幕寬度的百分比.
* @param aspectRatio 寬高比.
*/
public void onCreateViewHolder(ViewGroup parent, View itemView, float cardPercentWidth, float aspectRatio) {
RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) itemView.getLayoutParams();
lp.width = (int) (DisplayUtil.getScreenWidth(parent.getContext()) * cardPercentWidth);
lp.height = (int) (lp.width / aspectRatio);
itemView.setLayoutParams(lp);
}
二維碼
由于整個卡片都是按比例劃分的,為了展示盡可能大的二維碼區域,二維碼卡片也需要動態設置,按照底部欄的最大高度的80%作為寬高(二維碼是正方形)
//根據實際底部欄大小設置寬高.
private void setQRCodeImageView(final ImageView imageView, final ViewGroup root) {
if (imageView == null || root == null) {
return;
}
imageView.post(new Runnable() {
@Override
public void run() {
int height = root.getMeasuredHeight();
int targetHeight = (int) (height * 0.8);
if (height == 0) {
return;
}
ViewGroup.LayoutParams params = imageView.getLayoutParams();
params.width = targetHeight;
params.height = targetHeight;
imageView.setLayoutParams(params);
}
});
}
背景色漸變
這部分主要方法網上都有,就不重復造輪子了。這里是連貫步驟,就是根據當前卡片的底圖做一張模糊圖,列舉出來只是方便讀者快速實現。
----QRCodePosterActivity.java----
private void initBlurBackground() {
mBlurView = (ImageView) findViewById(R.id.blurView);
mContentRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
notifyBackgroundChange();
//指示器
}
}
});
setDefaultBackground();
}
private void notifyBackgroundChange() {
if (mPosterModule == null || mPosterModule.getBannerInfo().size() == 0) {
setDefaultBackground();
return;
}
/**
* 延時設置說明,由于滑動距離會出現正好一頁的距離或偏離.
* 所以滑動停止事件觸發會出現一次或兩次(偏離的時候,偏差.
* 量將自動修正后再次停止),所以延時并取消上一次背景切換可以消除畫面閃爍。.
*/
mBlurView.removeCallbacks(mBlurRunnable);
mBlurRunnable = new Runnable() {
@Override
public void run() {
Bitmap bitmap = mCardScaleHelper.getCurrentBitmap();
ViewSwitchUtils.startSwitchBackgroundAnim(mBlurView, BlurBitmapUtils.getBlurBitmap(mBlurView.getContext(), bitmap, 15));
}
};
mBlurView.postDelayed(mBlurRunnable, 500);
}
private void setDefaultBackground() {
if (mBlurView == null) {
return;
}
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_card_default);
mBlurView.setImageBitmap(BlurBitmapUtils.getBlurBitmap(mBlurView.getContext(), bitmap, 15));
}
---- CardScaleHelper.java ----
public Bitmap getCurrentBitmap() {
View view = mRecyclerView.getLayoutManager().findViewByPosition(getCurrentItemPos());
if (view == null) {
return null;
}
ImageView mBgIv = (ImageView) view.findViewById(R.id.iv_bg);
final Bitmap bitmap = ((BitmapDrawable) mBgIv.getDrawable()).getBitmap();
return bitmap;
}
---- ViewSwitchUtils.java ----
public static void startSwitchBackgroundAnim(ImageView view, Bitmap bitmap) {
if (view == null || bitmap == null) {
return;
}
Drawable oldDrawable = view.getDrawable();
Drawable oldBitmapDrawable;
TransitionDrawable oldTransitionDrawable = null;
if (oldDrawable instanceof TransitionDrawable) {
oldTransitionDrawable = (TransitionDrawable) oldDrawable;
oldBitmapDrawable = oldTransitionDrawable.findDrawableByLayerId(oldTransitionDrawable.getId(1));
} else if (oldDrawable instanceof BitmapDrawable) {
oldBitmapDrawable = oldDrawable;
} else {
oldBitmapDrawable = new ColorDrawable(0xffc2c2c2);
}
if (oldTransitionDrawable == null) {
oldTransitionDrawable = new TransitionDrawable(new Drawable[]{oldBitmapDrawable, new BitmapDrawable(view.getResources(), bitmap)});
oldTransitionDrawable.setId(0, 0);
oldTransitionDrawable.setId(1, 1);
oldTransitionDrawable.setCrossFadeEnabled(true);
view.setImageDrawable(oldTransitionDrawable);
} else {
oldTransitionDrawable.setDrawableByLayerId(oldTransitionDrawable.getId(0), oldBitmapDrawable);
oldTransitionDrawable.setDrawableByLayerId(oldTransitionDrawable.getId(1), new BitmapDrawable(view.getResources(), bitmap));
}
oldTransitionDrawable.startTransition(1000);
}
---- BlurBitmapUtils.java ----
/**
* 得到模糊后的bitmap
*
* @param context
* @param bitmap
* @param radius
* @return
*/
public static Bitmap getBlurBitmap(Context context, Bitmap bitmap, int radius) {
if (bitmap == null || context == null) {
return null;
}
// 將縮小后的圖片做為預渲染的圖片。
Bitmap inputBitmap = Bitmap.createScaledBitmap(bitmap, SCALED_WIDTH, SCALED_HEIGHT, false);
// 創建一張渲染后的輸出圖片。
Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap);
try {
// 創建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(radius);
// 設置blurScript對象的輸入內存
blurScript.setInput(tmpIn);
// 將輸出數據保存到輸出內存中
blurScript.forEach(tmpOut);
// 將數據填充到Allocation中
tmpOut.copyTo(outputBitmap);
} catch (Exception e) {
e.printStackTrace();
}finally {
inputBitmap.recycle();
}
return outputBitmap;
}
切換卡片,卡片的縮放效果
我們要實現如上效果,基本的滑動展示,RecyclerView都有實現,需要解決是滑動過程中卡片的縮放問題、卡片透明度變化、滑動距離的判定、頁碼的計算、多張卡片的內存問題等。
為了復用,主要的代碼都是通過幫助類實現。用法如下
---- QRCodePosterActivity.java ----
// mRecyclerView綁定scale效果.
mCardScaleHelper = new CardScaleHelper();
mCardScaleHelper.setCurrentItemPos(0);//初始化指定頁面.
mCardScaleHelper.setScale(0.8f);//兩側縮放比例.
mCardScaleHelper.setCardPercentWidth(0.72f);//卡片占屏幕寬度比例.
mCardScaleHelper.attachToRecyclerView(mContentRv);
下面我們來看看具體實現
初始化
我們從綁定開始初始化
---- CardScaleHelper.java ----
private int mCardWidth; // 卡片寬度.
private int mOnePageWidth; // 滑動一頁的距離.
private int mCardGalleryWidth;
private int mCurrentItemPos;
private int mCurrentItemOffset;
private float mScale = 0.9f; // 兩邊視圖scale.
private float mCardPercentWidth = 0.60f;//卡片占據屏幕寬度的百分比,需要與CardAdapterHelper中的一致.
private CardPagerSnapHelper mPageSnapHelp = new CardPagerSnapHelper();
public void attachToRecyclerView(final RecyclerView mRecyclerView) {
this.mRecyclerView = mRecyclerView;
mContext = mRecyclerView.getContext();
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
mPageSnapHelp.mNoNeedToScroll = mCurrentItemOffset == 0 || mCurrentItemOffset == getDestItemOffset(mRecyclerView.getAdapter().getItemCount() - 1);
} else {
mPageSnapHelp.mNoNeedToScroll = false;
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dx == 0) {
initWidth();
return;
}
// dx>0則表示右滑, dx<0表示左滑, dy<0表示上滑, dy>0表示下滑
mCurrentItemOffset += dx;
computeCurrentItemPos();
onScrolledChangedCallback();
}
});
mPageSnapHelp.attachToRecyclerView(mRecyclerView);
}
/** 初始化卡片寬度**/
private void initWidth() {
mCardGalleryWidth = mRecyclerView.getWidth();
mCardWidth = (int) (mCardGalleryWidth * mCardPercentWidth);
mOnePageWidth = mCardWidth;
mRecyclerView.smoothScrollToPosition(mCurrentItemPos);
onScrolledChangedCallback();
}
計算當前卡片索引
---- CardScaleHelper.java ----
private void computeCurrentItemPos() {
if (mOnePageWidth <= 0) return;
boolean pageChanged = false;
// 滑動超過一頁說明已翻頁.
if (Math.abs(mCurrentItemOffset - mCurrentItemPos * mOnePageWidth) >= (mOnePageWidth)) {
pageChanged = true;
}
if (pageChanged) {
int tempPos = mCurrentItemPos;
mCurrentItemPos = mCurrentItemOffset / (mOnePageWidth);
}
}
卡片滑動切換計算
下面的這個方法是比較核心,包含了所有卡片的縮放比計算,透明度計算,為了達到平滑過度,這里用到了三角函數,也包含了一些適配問題的解決。由于水平有限,如下方法可能還是存在優化的空間或細節修正,僅供參考,感興趣的朋友可以自行研究。
---- CardScaleHelper.java ----
/**
* RecyclerView位移事件監聽, view大小隨位移事件變化.
*/
public void onScrolledChangedCallback() {
for (int i = 0; i < mRecyclerView.getAdapter().getItemCount(); i++) {
LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
final View view = layoutManager.getChildAt(i);
if (view == null) {
continue;
}
//計算當前這個view相對于中間View的偏移頁碼量.
//(view相對的X的起始位置-當前scrollview滾動的位置)/每頁大小.
// = 0 為居中頁.
// = 1 為下一頁 2 為下下頁.
// = -1 為上一頁 -2 為上上頁.
double offsetPage = ((int) view.getTag() * (double) mOnePageWidth - mCurrentItemOffset) / (double) mOnePageWidth;
double scale = (float) Math.cos(offsetPage);
if (Math.abs(scale) < mScale)
scale = mScale;
view.setScaleX((float) scale);
view.setScaleY((float) scale);
BigDecimal bd = new BigDecimal((scale * 0.8)).setScale(1, RoundingMode.UP);
if (scale > 0.99f) {
view.setAlpha(1);
} else {
view.setAlpha((bd.floatValue()));
//解決透明顯示異常的問題,強制重新繪制.
view.invalidate();
}
}
}
Tag值,及滑動時卡片間隙計算。
---- CardAdapter.java ----
@Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
holder.itemView.setTag(position);
mCardAdapterHelper.onBindViewHolder(holder.itemView, position, getItemCount());
setQRCodeImageView(holder.mQRCodeIv, holder.mBottomLl);
//業務代碼.
}
---- CardScaleHelper.java ----
private int mPagePadding = 15;
public void onBindViewHolder(View itemView, final int position, int itemCount) {
int mOneSideWidth = (int) ((DisplayUtil.getScreenWidth(itemView.getContext()) - itemView.getLayoutParams().width) / 2.0);
int leftMarin = position == 0 ? mOneSideWidth : 0;
int rightMarin = position == itemCount - 1 ? mOneSideWidth : 0;
setViewMargin(itemView, leftMarin, 0, rightMarin, 10);
}
private void setViewMargin(View view, int left, int top, int right, int bottom) {
ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
if (lp.leftMargin != left || lp.topMargin != top || lp.rightMargin != right || lp.bottomMargin != bottom) {
lp.setMargins(left, top, right, bottom);
view.setLayoutParams(lp);
}
}
多張卡片內存控制
由于指示器比較簡單,這里簡述一種實現思路, 可以直接用LinearLayout動態添加包含指示器圖案的view,每次滑動結束后更新指示器位置。
在銅板街的應用上,卡片最終是要分享出去,所以我們繼續分析下,如何在分享前做好準備,由于分享有需要文件,也有需要Bitmap的.
@Override
public void onClick(View v) {
if (v.getId() == R.id.iv_back) {
finish();
return;
}
createBitmap(v);
}
public void createBitmap(final View clickView) {
showLoadingCustomDialog();
ThreadPoolManager.getInstance().addTask(new Runnable() {
@Override
public void run() {
View view = linearLayoutManager.findViewByPosition(mCardScaleHelper.getCurrentItemPos());
View mContentRl = view.findViewById(R.id.rl_content);
mContentRl.setDrawingCacheEnabled(true);
mContentRl.buildDrawingCache(); //啟用DrawingCache并創建位圖.
final Bitmap bitmap = Bitmap.createBitmap(mContentRl.getDrawingCache()); //創建一個DrawingCache的拷貝,因為DrawingCache得到的位圖在禁用后會被回收.
mContentRl.setDrawingCacheEnabled(false); //禁用DrawingCahce否則會影響性能.
mContentRl.destroyDrawingCache();
file = FileUtil.saveImage(Constant.IMAGE_CACHE_PATH, "share" + System.currentTimeMillis(), bitmap);
dismissLoadingCustomDialog();
clickView.post(new Runnable() {
@Override
public void run() {
//分享.
}
});
}
});
}
注意幾個細節,一個是bitmap的回收,第二個是文件的處理,由于QQ分享的問題,我們并不能分享完成后立馬刪除原文件,所以我的做法是關閉當前頁面時,會清理(文件有最后修改時間方法:lastModified)過期的文件緩存。
本文總結了在開發畫廊型卡片分享的一些心得和體會,對于一個復雜的程序來說,算法往往是最關鍵的,整個功能的開發可以說一半的時間都是在調試滑動時卡片的縮放效果。而工作中多數應用開發用到的算法往往比較簡單,所以如果想提升,就必須自己去專研。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。