由于業(yè)務(wù)需求,需要做一個(gè)卡片分享功能,前期做了一些預(yù)研,實(shí)現(xiàn)類似效果可以采用如下兩種方式:
成都創(chuàng)新互聯(lián)公司是一家專注于成都網(wǎng)站制作、網(wǎng)站建設(shè)與策劃設(shè)計(jì),章丘網(wǎng)站建設(shè)哪家好?成都創(chuàng)新互聯(lián)公司做網(wǎng)站,專注于網(wǎng)站建設(shè)十年,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:章丘等地區(qū)。章丘做網(wǎng)站價(jià)格咨詢:18982081108
由于RecyclerView自帶復(fù)用設(shè)計(jì),方便后期拓展,所以就采用RecyclerView這個(gè)方案,主要實(shí)現(xiàn)的細(xì)節(jié)效果和功能如下:
1.分頁(yè),自動(dòng)居中
2.卡片樣式及效果,陰影等
3.背景色漸變
4.切換卡片,卡片的縮放效果
5.指示器
6.卡片分享
效果圖:
RecyclerView這個(gè)方向的資料還是比較好查找,不過細(xì)節(jié)和想實(shí)現(xiàn)的效果還是有些許出入。針對(duì)這些問題,逐步探索,經(jīng)過多次改良后,得到了較為滿意的結(jié)果。
本文滑動(dòng)是橫向滑動(dòng),如果讀者想要縱向的,可以使用RecyclerView的LinearLayoutManager設(shè)置方向,其他代碼大體相同。
下面我就根據(jù)效果逐一給讀者提供相關(guān)代碼實(shí)現(xiàn),并針對(duì)實(shí)現(xiàn)細(xì)節(jié)、難點(diǎn),附上開發(fā)思路供大家參考。
難點(diǎn):
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是因?yàn)橄胍男Ч且豁?yè)的滑動(dòng)。如果想要的是可以滑動(dòng)多頁(yè),可以使用LinearSnapHelper,設(shè)置對(duì)應(yīng)的朝向即可,另外繼承這個(gè)也可以設(shè)置阻尼大小,還可控制滑動(dòng)速度。
我這里主要是根據(jù)要求做了如下方面的修改,讀者可以根據(jù)需求,增加動(dòng)畫,列表,點(diǎn)擊反饋等。
1)陰影、圓角等
這是我用到的設(shè)置,讀者可以根據(jù)實(shí)際效果對(duì)比界面設(shè)計(jì)做調(diào)整:
2)卡片比例動(dòng)態(tài)調(diào)整
卡片
要保持在不同屏幕下卡片比例保持不變,就需要根據(jù)屏幕的分辨率動(dòng)態(tài)的設(shè)置卡片的寬高。
---- 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 卡片占據(jù)屏幕寬度的百分比.
* @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);
}
二維碼
由于整個(gè)卡片都是按比例劃分的,為了展示盡可能大的二維碼區(qū)域,二維碼卡片也需要?jiǎng)討B(tài)設(shè)置,按照底部欄的最大高度的80%作為寬高(二維碼是正方形)
//根據(jù)實(shí)際底部欄大小設(shè)置寬高.
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);
}
});
}
背景色漸變
這部分主要方法網(wǎng)上都有,就不重復(fù)造輪子了。這里是連貫步驟,就是根據(jù)當(dāng)前卡片的底圖做一張模糊圖,列舉出來只是方便讀者快速實(shí)現(xiàn)。
----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;
}
/**
* 延時(shí)設(shè)置說明,由于滑動(dòng)距離會(huì)出現(xiàn)正好一頁(yè)的距離或偏離.
* 所以滑動(dòng)停止事件觸發(fā)會(huì)出現(xiàn)一次或兩次(偏離的時(shí)候,偏差.
* 量將自動(dòng)修正后再次停止),所以延時(shí)并取消上一次背景切換可以消除畫面閃爍。.
*/
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;
}
// 將縮小后的圖片做為預(yù)渲染的圖片。
Bitmap inputBitmap = Bitmap.createScaledBitmap(bitmap, SCALED_WIDTH, SCALED_HEIGHT, false);
// 創(chuàng)建一張渲染后的輸出圖片。
Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap);
try {
// 創(chuàng)建RenderScript內(nèi)核對(duì)象
RenderScript rs = RenderScript.create(context);
// 創(chuàng)建一個(gè)模糊效果的RenderScript的工具對(duì)象
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
// 由于RenderScript并沒有使用VM來分配內(nèi)存,所以需要使用Allocation類來創(chuàng)建和分配內(nèi)存空間。
// 創(chuàng)建Allocation對(duì)象的時(shí)候其實(shí)內(nèi)存是空的,需要使用copyTo()將數(shù)據(jù)填充進(jìn)去。
Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);
// 設(shè)置渲染的模糊程度, 25f是最大模糊度
blurScript.setRadius(radius);
// 設(shè)置blurScript對(duì)象的輸入內(nèi)存
blurScript.setInput(tmpIn);
// 將輸出數(shù)據(jù)保存到輸出內(nèi)存中
blurScript.forEach(tmpOut);
// 將數(shù)據(jù)填充到Allocation中
tmpOut.copyTo(outputBitmap);
} catch (Exception e) {
e.printStackTrace();
}finally {
inputBitmap.recycle();
}
return outputBitmap;
}
切換卡片,卡片的縮放效果
我們要實(shí)現(xiàn)如上效果,基本的滑動(dòng)展示,RecyclerView都有實(shí)現(xiàn),需要解決是滑動(dòng)過程中卡片的縮放問題、卡片透明度變化、滑動(dòng)距離的判定、頁(yè)碼的計(jì)算、多張卡片的內(nèi)存問題等。
為了復(fù)用,主要的代碼都是通過幫助類實(shí)現(xiàn)。用法如下
---- QRCodePosterActivity.java ----
// mRecyclerView綁定scale效果.
mCardScaleHelper = new CardScaleHelper();
mCardScaleHelper.setCurrentItemPos(0);//初始化指定頁(yè)面.
mCardScaleHelper.setScale(0.8f);//兩側(cè)縮放比例.
mCardScaleHelper.setCardPercentWidth(0.72f);//卡片占屏幕寬度比例.
mCardScaleHelper.attachToRecyclerView(mContentRv);
下面我們來看看具體實(shí)現(xiàn)
初始化
我們從綁定開始初始化
---- CardScaleHelper.java ----
private int mCardWidth; // 卡片寬度.
private int mOnePageWidth; // 滑動(dòng)一頁(yè)的距離.
private int mCardGalleryWidth;
private int mCurrentItemPos;
private int mCurrentItemOffset;
private float mScale = 0.9f; // 兩邊視圖scale.
private float mCardPercentWidth = 0.60f;//卡片占據(jù)屏幕寬度的百分比,需要與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();
}
計(jì)算當(dāng)前卡片索引
---- CardScaleHelper.java ----
private void computeCurrentItemPos() {
if (mOnePageWidth <= 0) return;
boolean pageChanged = false;
// 滑動(dòng)超過一頁(yè)說明已翻頁(yè).
if (Math.abs(mCurrentItemOffset - mCurrentItemPos * mOnePageWidth) >= (mOnePageWidth)) {
pageChanged = true;
}
if (pageChanged) {
int tempPos = mCurrentItemPos;
mCurrentItemPos = mCurrentItemOffset / (mOnePageWidth);
}
}
卡片滑動(dòng)切換計(jì)算
下面的這個(gè)方法是比較核心,包含了所有卡片的縮放比計(jì)算,透明度計(jì)算,為了達(dá)到平滑過度,這里用到了三角函數(shù),也包含了一些適配問題的解決。由于水平有限,如下方法可能還是存在優(yōu)化的空間或細(xì)節(jié)修正,僅供參考,感興趣的朋友可以自行研究。
---- CardScaleHelper.java ----
/**
* RecyclerView位移事件監(jiān)聽, 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;
}
//計(jì)算當(dāng)前這個(gè)view相對(duì)于中間View的偏移頁(yè)碼量.
//(view相對(duì)的X的起始位置-當(dāng)前scrollview滾動(dòng)的位置)/每頁(yè)大小.
// = 0 為居中頁(yè).
// = 1 為下一頁(yè) 2 為下下頁(yè).
// = -1 為上一頁(yè) -2 為上上頁(yè).
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()));
//解決透明顯示異常的問題,強(qiáng)制重新繪制.
view.invalidate();
}
}
}
Tag值,及滑動(dòng)時(shí)卡片間隙計(jì)算。
---- 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);
//業(yè)務(wù)代碼.
}
---- 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);
}
}
多張卡片內(nèi)存控制
由于指示器比較簡(jiǎn)單,這里簡(jiǎn)述一種實(shí)現(xiàn)思路, 可以直接用LinearLayout動(dòng)態(tài)添加包含指示器圖案的view,每次滑動(dòng)結(jié)束后更新指示器位置。
在銅板街的應(yīng)用上,卡片最終是要分享出去,所以我們繼續(xù)分析下,如何在分享前做好準(zhǔn)備,由于分享有需要文件,也有需要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并創(chuàng)建位圖.
final Bitmap bitmap = Bitmap.createBitmap(mContentRl.getDrawingCache()); //創(chuàng)建一個(gè)DrawingCache的拷貝,因?yàn)镈rawingCache得到的位圖在禁用后會(huì)被回收.
mContentRl.setDrawingCacheEnabled(false); //禁用DrawingCahce否則會(huì)影響性能.
mContentRl.destroyDrawingCache();
file = FileUtil.saveImage(Constant.IMAGE_CACHE_PATH, "share" + System.currentTimeMillis(), bitmap);
dismissLoadingCustomDialog();
clickView.post(new Runnable() {
@Override
public void run() {
//分享.
}
});
}
});
}
注意幾個(gè)細(xì)節(jié),一個(gè)是bitmap的回收,第二個(gè)是文件的處理,由于QQ分享的問題,我們并不能分享完成后立馬刪除原文件,所以我的做法是關(guān)閉當(dāng)前頁(yè)面時(shí),會(huì)清理(文件有最后修改時(shí)間方法:lastModified)過期的文件緩存。
本文總結(jié)了在開發(fā)畫廊型卡片分享的一些心得和體會(huì),對(duì)于一個(gè)復(fù)雜的程序來說,算法往往是最關(guān)鍵的,整個(gè)功能的開發(fā)可以說一半的時(shí)間都是在調(diào)試滑動(dòng)時(shí)卡片的縮放效果。而工作中多數(shù)應(yīng)用開發(fā)用到的算法往往比較簡(jiǎn)單,所以如果想提升,就必須自己去專研。