前言

由于业务需求,需要做一个卡片分享功能,前期做了一些预研,实现类似效果可以采用如下两种方式:

  • 采用ViewPager实现
  • 采用RecyclerView实现

由于RecyclerView自带复用设计,方便后期拓展,所以就采用RecyclerView这个方案,主要实现的细节效果和功能如下:

  1. 分页,自动居中
  2. 卡片样式及效果,阴影等
  3. 背景色渐变
  4. 切换卡片,卡片的缩放效果
  5. 指示器
  6. 卡片分享

效果图:

RecyclerView这个方向的资料还是比较好查找,不过细节和想实现的效果还是有些许出入。针对这些问题,逐步探索,经过多次改良后,得到了较为满意的结果。

本文滑动是横向滑动,如果读者想要纵向的,可以使用RecyclerView的LinearLayoutManager设置方向,其他代码大体相同。

下面我就根据效果逐一给读者提供相关代码实现,并针对实现细节、难点,附上开发思路供大家参考。


难点:

  • 卡片比例适配
  • 滑动时卡片缩放动画。
  • 滑动时距离计算、速度控制、页码计算。
  • 内存控制

技术实现

分页、自动居中

public class CardPagerSnapHelper extends PagerSnapHelper {public boolean mNoNeedToScroll = false;@Overridepublic 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)阴影、圆角等
  • cardElevation 设置z轴阴影
  • cardCornerRadius 设置圆角大小
  • cardMaxElevation 设置z轴最大高度值
  • cardPreventCornerOverlap 是否添加内边距(避免内容与边缘重叠)
  • cardUseCompatPadding 设置内边距,V21+的版本和之前的版本仍旧具有一样的计算方式

这是我用到的设置,读者可以根据实际效果对比界面设计做调整:

<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 ----@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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() {@Overridepublic 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;}}@Overridepublic 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 ----   @Overridepublic 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);}}复制代码

多张卡片内存控制

  • 方案一:利用第三方框架去显示,如glide,pessones等,最简单,如果对内存没有极细的要求的话推荐使用这个方案。
  • 方案二:可以考虑回收不显示卡片的那部分内存,然后利用LruCache进行缓存管理。

指示器

由于指示器比较简单,这里简述一种实现思路, 可以直接用LinearLayout动态添加包含指示器图案的view,每次滑动结束后更新指示器位置。

卡片分享

在铜板街的应用上,卡片最终是要分享出去,所以我们继续分析下,如何在分享前做好准备,由于分享有需要文件,也有需要Bitmap的.

 @Overridepublic 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() {@Overridepublic 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() {@Overridepublic void run() {//分享.}});}});}
复制代码

注意几个细节,一个是bitmap的回收,第二个是文件的处理,由于QQ分享的问题,我们并不能分享完成后立马删除原文件,所以我的做法是关闭当前页面时,会清理(文件有最后修改时间方法:lastModified)过期的文件缓存。

总结

本文总结了在开发画廊型卡片分享的一些心得和体会,对于一个复杂的程序来说,算法往往是最关键的,整个功能的开发可以说一半的时间都是在调试滑动时卡片的缩放效果。而工作中多数应用开发用到的算法往往比较简单,所以如果想提升,就必须自己去专研。

作者简介

苏哲,铜板街Android开发工程师,2017年12月加入团队,目前主要负责APP端 Android 日常开发。

更多精彩内容,请扫码关注 “铜板街技术” 微信公众号。

海报分享功能实现详解相关推荐

  1. python画简单的图形的代码-Python实现画图软件功能方法详解

    概述 虽然Python的强项在人工智能,数据处理方面,但是对于日常简单的应用,Python也提供了非常友好的支持(如:Tkinter),本文主要一个简单的画图小软件,简述Python在GUI(图形用户 ...

  2. [系统安全] 四十五.APT系列(10)Metasploit后渗透技术信息收集、权限提权和功能模块详解

    您可能之前看到过我写的类似文章,为什么还要重复撰写呢?只是想更好地帮助初学者了解病毒逆向分析和系统安全,更加成体系且不破坏之前的系列.因此,我重新开设了这个专栏,准备系统整理和深入学习系统安全.逆向分 ...

  3. android搜索功能xml,Android_Android ActionBar搜索功能用法详解,本文实例讲述了Android ActionBar - phpStudy...

    Android ActionBar搜索功能用法详解 本文实例讲述了Android ActionBar搜索功能用法.分享给大家供大家参考,具体如下: 使用ActionBar SearchView时的注意 ...

  4. python实现文本编辑器_Python实现文本编辑器功能实例详解

    这篇文章主要介绍了Python实现的文本编辑器功能,结合实例形式详细分析了基于wxpython实现文本编辑器所需的功能及相关实现技巧,需要的朋友可以参考下 本文实例讲述了Python实现的文本编辑器功 ...

  5. python画图代码大全-Python实现画图软件功能方法详解

    概述 虽然Python的强项在人工智能,数据处理方面,但是对于日常简单的应用,Python也提供了非常友好的支持(如:Tkinter),本文主要一个简单的画图小软件,简述Python在GUI(图形用户 ...

  6. python画图软件是哪个_Python实现画图软件功能方法详解

    Python实现画图软件功能方法详解,按钮,事件,绑定,快捷键,直线 Python实现画图软件功能方法详解 易采站长站,站长之家为您整理了Python实现画图软件功能方法详解的相关内容. 概述 虽然P ...

  7. android收藏功能demo,Android使用Realm数据库实现App中的收藏功能(代码详解)

    前 言 App数据持久化功能是每个App必不可少的功能,而Android最常用的数据持久化方式主要有以下的五种方式: 使用SharedPreferences存储数据: 文件存储数据: SQLite数据 ...

  8. SAP UI5 应用开发教程之一百零二 - SAP UI5 应用的打印(Print)功能实现详解试读版

    一套适合 SAP UI5 初学者循序渐进的学习教程 作者简介 Jerry Wang,2007 年从电子科技大学计算机专业硕士毕业后加入 SAP 成都研究院工作至今.Jerry 是 SAP 社区导师,S ...

  9. HTML5实现视频直播功能思路详解

    HTML5实现视频直播功能思路详解 最近视频直播比较火,发现目前 WEB 上主流的视频直播方案有 HLS 和 RTMP,移动 WEB 端目前以 HLS 为主,PC端则以 RTMP 为主实时性较好,接下 ...

最新文章

  1. 『SHELL』--SHELL脚本执行方式(转)
  2. 成功解决基于VS2015(Visual Studio2015)编写C++程序调试时弹出窗口一闪而过的问题
  3. ML之回归预测:机器学习中的各种Regression回归算法、关键步骤配图
  4. [云炬python3玩转机器学习笔记] 3-6Numpy数组和矩阵的合并和分割
  5. 用存储过程还原数据库
  6. SpringMVC-Controller怎么直接在页面上传递参数
  7. Hive_ 对比分区,分桶
  8. android广播唤醒app,Android APP唤醒打开其他APP
  9. Mask-SLAM:基于语义分割掩模的鲁棒特征单目SLAM
  10. win10上的docker怎么设置开机不要自动启动 [问题点数:20分,结帖人xyq1986]
  11. innosetup 安装前、卸载前判断是否有进程正在运行转
  12. 通过在群晖上安装虚拟机,实现群晖与115网盘的双向同步
  13. 计算机网络管理员试题2016,2016年 -1月自考计算机网络管理试题真题.doc
  14. 无状态,无连接的理解
  15. HTML5 UI 模板
  16. 浅析竞技游戏匹配机制-ELO算法
  17. PTA-莫尔斯码(字符串,模拟)
  18. 用arduino和OLED制作火柴人奔跑动画
  19. 视频教程-Python数据分析与案例教程:分析人口普查数据-Python
  20. java castor_Castor简单介绍

热门文章

  1. 佳佳的筷子 Chopsticks
  2. 【Flink】浅谈Flink背压问题(1)
  3. H5实现复制淘口令功能
  4. Java GUI基础
  5. MATLAB(一)Matlab“帮助”的使用
  6. phoenix的元数据一般存在哪里_【Python基础】hive的元数据存在哪里
  7. 求两个整数的商和余数(不用乘,除,取余)计算
  8. incaseformat蠕虫病毒爆发,深信达助力安全防护
  9. 自身知识浅薄,开发积累问题
  10. java 打印详解_Java格式化输出printf()详解