内容简介:由于业务需求,需要做一个卡片分享功能,前期做了一些预研,实现类似效果可以采用如下两种方式:由于 RecyclerView 自带复用设计,方便后期拓展,所以就采用 RecyclerView这个方案,主要实现的细节效果和功能如下:效果如下图
前言
由于业务需求,需要做一个卡片分享功能,前期做了一些预研,实现类似效果可以采用如下两种方式:
-
采用 ViewPager 实现
-
采用 RecyclerView 实现
由于 RecyclerView 自带复用设计,方便后期拓展,所以就采用 RecyclerView这个方案,主要实现的细节效果和功能如下:
-
分页,自动居中
-
卡片样式及效果,阴影等
-
背景色渐变
-
切换卡片,卡片的缩放效果
-
指示器
-
卡片分享
效果如下图
RecyclerView 这个方向的资料还是比较好查找,不过细节和想实现的效果还是有些许出入。针对这些问题,逐步探索,经过多次改良后,得到了较为满意的结果。
本文滑动是横向滑动,如果读者想要纵向的,可以使用 RecyclerView 的 LinearLayoutManager 设置方向,其他代码大体相同。
下面我就根据效果逐一给读者提供相关代码实现,并针对实现细节、难点,附上开发思路供大家参考。
难点:
-
卡片比例适配
-
滑动时卡片缩放动画。
-
滑动时距离计算、速度控制、页码计算。
-
内存控制
技术实现
分页、自动居中
1public class CardPagerSnapHelper extends PagerSnapHelper {
2 public boolean mNoNeedToScroll = false;
3
4 @Override
5 public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager, @NonNull View targetView) {
6 if (mNoNeedToScroll) {
7 return new int[]{0, 0};
8 } else {
9 return super.calculateDistanceToFinalSnap(layoutManager, targetView);
10 }
11 }
12}
13
14//使用.
15mPageSnapHelp.attachToRecyclerView(mRecyclerView);
这里继承 PagerSnapHelper 是因为想要的效果是一页的滑动。如果想要的是可以滑动多页,可以使用 LinearSnapHelper,设置对应的朝向即可,另外继承这个也可以设置阻尼大小,还可控制滑动速度。
卡片效果
我这里主要是根据要求做了如下方面的修改,读者可以根据需求,增加动画,列表,点击反馈等。
1)阴影、圆角等
-
cardElevation 设置z轴阴影
-
cardCornerRadius 设置圆角大小
-
cardMaxElevation 设置z轴最大高度值
-
cardPreventCornerOverlap 是否添加内边距(避免内容与边缘重叠)
-
cardUseCompatPadding 设置内边距,V21+的版本和之前的版本仍旧具有一样的计算方式
这是我用到的设置,读者可以根据实际效果对比界面设计做调整:
1<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
2 xmlns:app="http://schemas.android.com/apk/res-auto"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="wrap_content"
5 android:layout_height="match_parent"
6 app:cardBackgroundColor="@color/white"
7 app:cardElevation="6dp"
8 app:cardMaxElevation="12dp"
9 app:cardPreventCornerOverlap="true"
10 app:cardUseCompatPadding="false">
2)卡片比例动态调整
卡片
要保持在不同屏幕下卡片比例保持不变,就需要根据屏幕的分辨率动态的设置卡片的宽高。
1---- CardAdapter.java ----
2 @Override
3 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
4 View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_poster, parent, false);
5 mCardAdapterHelper.onCreateViewHolder(parent, itemView, 0.72f, (float) (17.0 / 25.0));
6 return new ViewHolder(itemView);
7 }
8
9
10---- CardAdapterHelper.java ----
11 /**
12 * @param parent
13 * @param itemView
14 * @param cardPercentWidth 卡片占据屏幕宽度的百分比.
15 * @param aspectRatio 宽高比.
16 */
17 public void onCreateViewHolder(ViewGroup parent, View itemView, float cardPercentWidth, float aspectRatio) {
18 RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) itemView.getLayoutParams();
19 lp.width = (int) (DisplayUtil.getScreenWidth(parent.getContext()) * cardPercentWidth);
20 lp.height = (int) (lp.width / aspectRatio);
21 itemView.setLayoutParams(lp);
22 }
二维码
由于整个卡片都是按比例划分的,为了展示尽可能大的二维码区域,二维码卡片也需要动态设置,按照底部栏的最大高度的80%作为宽高(二维码是正方形)。
1//根据实际底部栏大小设置宽高.
2private void setQRCodeImageView(final ImageView imageView, final ViewGroup root) {
3 if (imageView == null || root == null) {
4 return;
5 }
6 imageView.post(new Runnable() {
7 @Override
8 public void run() {
9 int height = root.getMeasuredHeight();
10 int targetHeight = (int) (height * 0.8);
11 if (height == 0) {
12 return;
13 }
14
15 ViewGroup.LayoutParams params = imageView.getLayoutParams();
16 params.width = targetHeight;
17 params.height = targetHeight;
18 imageView.setLayoutParams(params);
19 }
20 });
21}
背景色渐变
这部分主要方法网上都有,就不重复造轮子了。这里是连贯步骤,就是根据当前卡片的底图做一张模糊图,列举出来只是方便读者快速实现。
1----QRCodePosterActivity.java----
2private void initBlurBackground() {
3 mBlurView = (ImageView) findViewById(R.id.blurView);
4 mContentRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
5 @Override
6 public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
7 super.onScrollStateChanged(recyclerView, newState);
8 if (newState == RecyclerView.SCROLL_STATE_IDLE) {
9 notifyBackgroundChange();
10 //指示器
11 }
12 }
13 });
14 setDefaultBackground();
15 }
16
17 private void notifyBackgroundChange() {
18 if (mPosterModule == null || mPosterModule.getBannerInfo().size() == 0) {
19 setDefaultBackground();
20 return;
21 }
22
23 /**
24 * 延时设置说明,由于滑动距离会出现正好一页的距离或偏离.
25 * 所以滑动停止事件触发会出现一次或两次(偏离的时候,偏差.
26 * 量将自动修正后再次停止),所以延时并取消上一次背景切换可以消除画面闪烁。.
27 */
28 mBlurView.removeCallbacks(mBlurRunnable);
29 mBlurRunnable = new Runnable() {
30 @Override
31 public void run() {
32 Bitmap bitmap = mCardScaleHelper.getCurrentBitmap();
33 ViewSwitchUtils.startSwitchBackgroundAnim(mBlurView, BlurBitmapUtils.getBlurBitmap(mBlurView.getContext(), bitmap, 15));
34 }
35 };
36 mBlurView.postDelayed(mBlurRunnable, 500);
37}
38
39private void setDefaultBackground() {
40 if (mBlurView == null) {
41 return;
42 }
43 Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_card_default);
44 mBlurView.setImageBitmap(BlurBitmapUtils.getBlurBitmap(mBlurView.getContext(), bitmap, 15));
45}
1---- CardScaleHelper.java ----
2public Bitmap getCurrentBitmap() {
3 View view = mRecyclerView.getLayoutManager().findViewByPosition(getCurrentItemPos());
4 if (view == null) {
5 return null;
6 }
7 ImageView mBgIv = (ImageView) view.findViewById(R.id.iv_bg);
8 final Bitmap bitmap = ((BitmapDrawable) mBgIv.getDrawable()).getBitmap();
9 return bitmap;
10}
1---- ViewSwitchUtils.java ----
2public static void startSwitchBackgroundAnim(ImageView view, Bitmap bitmap) {
3 if (view == null || bitmap == null) {
4 return;
5 }
6 Drawable oldDrawable = view.getDrawable();
7 Drawable oldBitmapDrawable;
8 TransitionDrawable oldTransitionDrawable = null;
9 if (oldDrawable instanceof TransitionDrawable) {
10 oldTransitionDrawable = (TransitionDrawable) oldDrawable;
11 oldBitmapDrawable = oldTransitionDrawable.findDrawableByLayerId(oldTransitionDrawable.getId(1));
12 } else if (oldDrawable instanceof BitmapDrawable) {
13 oldBitmapDrawable = oldDrawable;
14 } else {
15 oldBitmapDrawable = new ColorDrawable(0xffc2c2c2);
16 }
17
18 if (oldTransitionDrawable == null) {
19 oldTransitionDrawable = new TransitionDrawable(new Drawable[]{oldBitmapDrawable, new BitmapDrawable(view.getResources(), bitmap)});
20 oldTransitionDrawable.setId(0, 0);
21 oldTransitionDrawable.setId(1, 1);
22 oldTransitionDrawable.setCrossFadeEnabled(true);
23 view.setImageDrawable(oldTransitionDrawable);
24 } else {
25 oldTransitionDrawable.setDrawableByLayerId(oldTransitionDrawable.getId(0), oldBitmapDrawable);
26 oldTransitionDrawable.setDrawableByLayerId(oldTransitionDrawable.getId(1), new BitmapDrawable(view.getResources(), bitmap));
27 }
28 oldTransitionDrawable.startTransition(1000);
29}
1---- BlurBitmapUtils.java ----
2 /**
3 * 得到模糊后的bitmap
4 *
5 * @param context
6 * @param bitmap
7 * @param radius
8 * @return
9 */
10public static Bitmap getBlurBitmap(Context context, Bitmap bitmap, int radius) {
11 if (bitmap == null || context == null) {
12 return null;
13 }
14 // 将缩小后的图片做为预渲染的图片。
15 Bitmap inputBitmap = Bitmap.createScaledBitmap(bitmap, SCALED_WIDTH, SCALED_HEIGHT, false);
16 // 创建一张渲染后的输出图片。
17 Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap);
18 try {
19 // 创建RenderScript内核对象
20 RenderScript rs = RenderScript.create(context);
21 // 创建一个模糊效果的RenderScript的 工具 对象
22 ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
23
24 // 由于RenderScript并没有使用VM来分配内存,所以需要使用Allocation类来创建和分配内存空间。
25 // 创建Allocation对象的时候其实内存是空的,需要使用copyTo()将数据填充进去。
26 Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap);
27 Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap);
28
29 // 设置渲染的模糊程度, 25f是最大模糊度
30 blurScript.setRadius(radius);
31 // 设置blurScript对象的输入内存
32 blurScript.setInput(tmpIn);
33 // 将输出数据保存到输出内存中
34 blurScript.forEach(tmpOut);
35
36 // 将数据填充到Allocation中
37 tmpOut.copyTo(outputBitmap);
38
39 } catch (Exception e) {
40 e.printStackTrace();
41 }finally {
42 inputBitmap.recycle();
43 }
44
45 return outputBitmap;
46}
切换卡片,卡片的缩放效果
我们要实现如上效果,基本的滑动展示,RecyclerView 都有实现,需要解决是滑动过程中卡片的缩放问题、卡片透明度变化、滑动距离的判定、页码的计算、多张卡片的内存问题等。
为了复用,主要的代码都是通过帮助类实现。用法如下
1---- QRCodePosterActivity.java ----
2 // mRecyclerView绑定scale效果.
3 mCardScaleHelper = new CardScaleHelper();
4 mCardScaleHelper.setCurrentItemPos(0);//初始化指定页面.
5 mCardScaleHelper.setScale(0.8f);//两侧缩放比例.
6 mCardScaleHelper.setCardPercentWidth(0.72f);//卡片占屏幕宽度比例.
7 mCardScaleHelper.attachToRecyclerView(mContentRv);
下面我们来看看具体实现
初始化
我们从绑定开始初始化
1---- CardScaleHelper.java ----
2 private int mCardWidth; // 卡片宽度.
3 private int mOnePageWidth; // 滑动一页的距离.
4 private int mCardGalleryWidth;
5 private int mCurrentItemPos;
6 private int mCurrentItemOffset;
7 private float mScale = 0.9f; // 两边视图scale.
8 private float mCardPercentWidth = 0.60f;//卡片占据屏幕宽度的百分比,需要与CardAdapterHelper中的一致.
9 private CardPagerSnapHelper mPageSnapHelp = new CardPagerSnapHelper();
10
11 public void attachToRecyclerView(final RecyclerView mRecyclerView) {
12 this.mRecyclerView = mRecyclerView;
13 mContext = mRecyclerView.getContext();
14 mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
15 @Override
16 public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
17 super.onScrollStateChanged(recyclerView, newState);
18 if (newState == RecyclerView.SCROLL_STATE_IDLE) {
19 mPageSnapHelp.mNoNeedToScroll = mCurrentItemOffset == 0 || mCurrentItemOffset == getDestItemOffset(mRecyclerView.getAdapter().getItemCount() - 1);
20 } else {
21 mPageSnapHelp.mNoNeedToScroll = false;
22 }
23 }
24
25 @Override
26 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
27 super.onScrolled(recyclerView, dx, dy);
28 if (dx == 0) {
29 initWidth();
30 return;
31 }
32 // dx>0则表示右滑, dx<0表示左滑, dy<0表示上滑, dy>0表示下滑
33 mCurrentItemOffset += dx;
34 computeCurrentItemPos();
35 onScrolledChangedCallback();
36 }
37
38
39 });
40 mPageSnapHelp.attachToRecyclerView(mRecyclerView);
41 }
42
43 /** 初始化卡片宽度**/
44 private void initWidth() {
45 mCardGalleryWidth = mRecyclerView.getWidth();
46 mCardWidth = (int) (mCardGalleryWidth * mCardPercentWidth);
47 mOnePageWidth = mCardWidth;
48 mRecyclerView.smoothScrollToPosition(mCurrentItemPos);
49 onScrolledChangedCallback();
50 }
计算当前卡片索引
1---- CardScaleHelper.java ----
2private void computeCurrentItemPos() {
3 if (mOnePageWidth <= 0) return;
4 boolean pageChanged = false;
5 // 滑动超过一页说明已翻页.
6 if (Math.abs(mCurrentItemOffset - mCurrentItemPos * mOnePageWidth) >= (mOnePageWidth)) {
7 pageChanged = true;
8 }
9 if (pageChanged) {
10 int tempPos = mCurrentItemPos;
11 mCurrentItemPos = mCurrentItemOffset / (mOnePageWidth);
12 }
13}
卡片滑动切换计算
下面的这个方法是比较核心,包含了所有卡片的缩放比计算,透明度计算,为了达到平滑过度,这里用到了三角函数,也包含了一些适配问题的解决。由于水平有限,如下方法可能还是存在优化的空间或细节修正,仅供参考,感兴趣的朋友可以自行研究。
1---- CardScaleHelper.java ----
2/**
3 * RecyclerView位移事件监听, view大小随位移事件变化.
4 */
5public void onScrolledChangedCallback() {
6 for (int i = 0; i < mRecyclerView.getAdapter().getItemCount(); i++) {
7 LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
8 final View view = layoutManager.getChildAt(i);
9 if (view == null) {
10 continue;
11 }
12 //计算当前这个view相对于中间View的偏移页码量.
13 //(view相对的X的起始位置-当前scrollview滚动的位置)/每页大小.
14 // = 0 为居中页.
15 // = 1 为下一页 2 为下下页.
16 // = -1 为上一页 -2 为上上页.
17 double offsetPage = ((int) view.getTag() * (double) mOnePageWidth - mCurrentItemOffset) / (double) mOnePageWidth;
18 double scale = (float) Math.cos(offsetPage);
19 if (Math.abs(scale) < mScale)
20 scale = mScale;
21 view.setScaleX((float) scale);
22 view.setScaleY((float) scale);
23
24 BigDecimal bd = new BigDecimal((scale * 0.8)).setScale(1, RoundingMode.UP);
25 if (scale > 0.99f) {
26 view.setAlpha(1);
27 } else {
28 view.setAlpha((bd.floatValue()));
29 // 解决透明显示异常的问题,强制重新绘制.
30 view.invalidate();
31 }
32 }
33
34}
初始化 Tag 值及滑动时卡片间隙计算
1---- CardAdapter.java ----
2 @Override
3 public void onBindViewHolder(final ViewHolder holder, final int position) {
4 holder.itemView.setTag(position);
5 mCardAdapterHelper.onBindViewHolder(holder.itemView, position, getItemCount());
6 setQRCodeImageView(holder.mQRCodeIv, holder.mBottomLl);
7 //业务代码.
8 }
9
10---- CardScaleHelper.java ----
11 private int mPagePadding = 15;
12
13 public void onBindViewHolder(View itemView, final int position, int itemCount) {
14 int mOneSideWidth = (int) ((DisplayUtil.getScreenWidth(itemView.getContext()) - itemView.getLayoutParams().width) / 2.0);
15 int leftMarin = position == 0 ? mOneSideWidth : 0;
16 int rightMarin = position == itemCount - 1 ? mOneSideWidth : 0;
17 setViewMargin(itemView, leftMarin, 0, rightMarin, 10);
18 }
19
20 private void setViewMargin(View view, int left, int top, int right, int bottom) {
21 ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
22 if (lp.leftMargin != left || lp.topMargin != top || lp.rightMargin != right || lp.bottomMargin != bottom) {
23 lp.setMargins(left, top, right, bottom);
24 view.setLayoutParams(lp);
25 }
26 }
多张卡片内存控制
-
方案一:利用第三方框架去显示,如 glide,pessones 等,最简单,如果对内存没有极细的要求的话推荐使用这个方案。
-
方案二:可以考虑回收不显示卡片的那部分内存,然后利用LruCache进行缓存管理。
指示器
由于指示器比较简单,这里简述一种实现思路, 可以直接用 LinearLayout 动态添加包含指示器图案的 view,每次滑动结束后更新指示器位置。
卡片分享
在铜板街的应用上,卡片最终是要分享出去,所以我们继续分析下,如何在分享前做好准备,由于分享有需要文件,也有需要 Bitmap 的。
1@Override
2public void onClick(View v) {
3 if (v.getId() == R.id.iv_back) {
4 finish();
5 return;
6 }
7 createBitmap(v);
8}
9
10public void createBitmap(final View clickView) {
11 showLoadingCustomDialog();
12 ThreadPoolManager.getInstance().addTask(new Runnable() {
13 @Override
14 public void run() {
15 View view = linearLayoutManager.findViewByPosition(mCardScaleHelper.getCurrentItemPos());
16 View mContentRl = view.findViewById(R.id.rl_content);
17 mContentRl.setDrawingCacheEnabled(true);
18 mContentRl.buildDrawingCache(); //启用 DrawingCache 并创建位图.
19 final Bitmap bitmap = Bitmap.createBitmap(mContentRl.getDrawingCache()); // 创建一个 DrawingCache 的拷贝,因为DrawingCache得到的位图在禁用后会被回收.
20 mContentRl.setDrawingCacheEnabled(false); //禁用 DrawingCahce 否则会影响性能.
21 mContentRl.destroyDrawingCache();
22 file = FileUtil.saveImage(Constant.IMAGE_CACHE_PATH, "share" + System.currentTimeMillis(), bitmap);
23 dismissLoadingCustomDialog();
24 clickView.post(new Runnable() {
25 @Override
26 public void run() {
27 //分享.
28 }
29 });
30 }
31 });
32}
注意几个细节,一个是bitmap的回收,第二个是文件的处理,由于QQ分享的问题,我们并不能分享完成后立马删除原文件,所以我的做法是关闭当前页面时,会清理(文件有最后修改时间方法:lastModified)过期的文件缓存。
总结
本文总结了在开发画廊型卡片分享的一些心得和体会,对于一个复杂的程序来说,算法往往是最关键的,整个功能的开发可以说一半的时间都是在调试滑动时卡片的缩放效果。而工作中多数应用开发用到的算法往往比较简单,所以如果想提升,就必须自己去专研。
作者简介
苏哲,铜板街Android开发工程师,2017年12月加入团队,目前主要负责APP端 Android 日常开发。
---------- END ----------
长按关注我们
以上所述就是小编给大家介绍的《海报分享功能实现详解》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。