篇章目标要点

之前写的一篇文章展示了RecyclerView实现的画廊效果,适用于专辑/图片/列表浏览效果。本篇文章阐述如何基于RecyclerView实现如下图所示的3D画廊效果。以下效果的重点在于实现子视图的图层叠加,滑动过程中的3D旋转效果较为简单。
(1)无3D旋转效果图片

(1)带3D旋转效果图片

实现效果

如下图所示,代码效果可以确保当前显示的子视图居于中间显示,当前显示视图两侧的子视图均会被居中的视图遮挡一部分。
(1) 不增加3D旋转的效果

(2) 增加3D旋转的效果

子视图叠加原理

默认情况下子视图是按照顺序绘制和放置的,无法做到图示的效果。RecyclerView支持重置子视图的绘制顺序,设置绘制的思路是当前显示的视图设置为最后绘制,这样即可实现当前显示的视图可以叠加在临近视图的上方,详细设计如下:

绘制的绘制设置如下

绘制批次 子视图序号 绘制顺序
1 当前视图以左 0 ~ i-1
2 当前视图以右 i ~ N-2
3 当前视图 N-1
备注:i:当前显示视图在RecyclerView中的序号
N: RecyclerView子视图长度

叠加实现过程

了解了原理之后,按照这个思路实现其代码开发,主要代码是包含对RecyclerView和LayoutManager进行重写。

1. 重写RecylcerView进行子视图绘制顺序重排

RecyclerView工作时是按照其默认顺序规则排列子视图的,如要进行顺序重新排列,则首先需要开启顺序重拍

public GalleryRecyclerView(@NonNull Context context) {super(context);init();
}public GalleryRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {super(context, attrs);init();
}public GalleryRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();
}……
//开启顺序重新排列
private void init(){setChildrenDrawingOrderEnabled(true);
}

2. 设置子视图的绘制顺序

其基本思路在子视图叠加原理片段中已经介绍,当前显示视图最后一个绘制,显示视图以左第一批次绘制顺序绘制,显示视图以右第二批次绘制逆序绘制,自定义RecyclerView中相应的实现代码如下

/*** 重写视图布置顺序:前半顺序绘制,后半倒序绘制,中间位置* 中间位置最后一个绘制count-1* 中间位置之前的视图绘制顺序为i* 中间位置之后的视图绘制顺序为center+count-1-i*/
@Override
protected int getChildDrawingOrder(int childCount, int i) {GalleryLayoutManager layoutManager = (GalleryLayoutManager) getLayoutManager();//计算出中间位置,即当前显示视图的位置int center = layoutManager.getCenterVisiblePosition() - layoutManager.getFirstVisiblePosition();//序号为i的视图的绘制序号int order;if(i == center){order = childCount - 1;}else if(i < center){order = i;}else{order = center + childCount - 1 - i;}Log.d(TAG,"childCount = "+childCount+",center = "+center+",order = "+order+",i = "+i);return order;
}

上述代码实现中依赖自定义LayoutManager计算当前已显示视图的第一个子视图位置,中间的子视图位置,相应的代码如下

/*** 计算显示的视图的中间视图的位置,基本思路是基于RecyclerView滑动的距离除以子视图间距*/
public int getCenterVisiblePosition(){int position = mScrollDistanceX / mChildIntervalWidth;int offset = mScrollDistanceX % mChildIntervalWidth;if(offset > mChildIntervalWidth/2){position++;}return position;
}//计算显示的第一个视图的位置
public int getFirstVisiblePosition(){if(getChildCount() < 0){return  0;}View item = getChildAt(0);return getPosition(item);
}

3. 布置子视图

由于布置子视图需要子视图的layout位置,因为在自定义LayoutManager内部使用HashMap分别缓存全部子视图的layout位置,已经是否已经添加显示的信息,定义如下:

/*** 用于存储子视图的在RecyclerView中的位置<P/>* key为子视图的序号,Rect为子视图的位置<P/>*/
private Map<Integer , Rect> mChildPositionRects = new HashMap<>();
/*** 用于记录子视图是否已经添加至RecyclerView中* key为子视图的序号,value为为该子视图是否在可视区域,true表示已显示,false未显示*/
private Map<Integer , Boolean> mChildHasAttached = new HashMap<>();

布置子视图这部分的主要工作上在可见区域放置子视图,并且对于处于非可见区域的子视图进行回收管理。在自定义LayoutManager中的代码如下

@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {if(getItemCount() == 0){detachAndScrapAttachedViews(recycler);return;}mChildHasAttached.clear();mChildPositionRects.clear();detachAndScrapAttachedViews(recycler);//计算子视图宽度,相邻子视图间距if(mChildIntervalWidth <= 0){View firstItem = recycler.getViewForPosition(0);measureChildWithMargins(firstItem, 0, 0);mChildWidth = getDecoratedMeasuredWidth(firstItem);mChildHeight = getDecoratedMeasuredHeight(firstItem);mChildIntervalWidth = (int) (mChildWidth*OVERLYING_RATIO);}//子视图水平方向的偏移量int offsetX = 0;mStartX = getWidth()/2 - mChildWidth/2;for(int i = 0 ; i < getItemCount() ; i++){Rect rect = new Rect(offsetX + mStartX , 0 ,offsetX + mChildWidth + mStartX , mChildHeight);mChildPositionRects.put(i , rect);mChildHasAttached.put(i,false);offsetX += mChildIntervalWidth;}//添加可视区域的视图int visibleCount = getHorizontalSpace() / mChildIntervalWidth;Rect visibleRect = getVisibleArea();for(int i = 0; i < visibleCount; i++){insertView(i, visibleRect, recycler, false);Log.d(TAG,"the i ="+i+" visible count = "+visibleCount+",rect left = "+visibleRect.left);}
}

在进行横向移动时,在自定义LayoutManager中需要回收非显示区域的子视图,并且放置显示区域的子视图,相应代码如下

//横向移动的绝对距离
private int mScrollDistanceX = 0;
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {//如视图内无可显示的子视图,不累加if(getChildCount() <= 0){return dx;}int travel = dx;//左边缘if(mScrollDistanceX + dx < 0){Log.d(TAG,"到达左边缘");travel = -mScrollDistanceX;}else if(mScrollDistanceX + dx > ((getItemCount() -1)*mChildIntervalWidth)){//右边缘Log.d(TAG,"到达右边缘");travel = (getItemCount() -1)*mChildIntervalWidth - mScrollDistanceX;}mScrollDistanceX += travel;//回收非显示区域子视图,并且在可见区域放置子视图Rect visibleRect = getVisibleArea();for(int i = getChildCount()-1; i >=0 ; i--){View item = getChildAt(i);int position = getPosition(item);Rect rect = mChildPositionRects.get(position);//判断子视图与可见区域无交集时,移除并回收视图if(!Rect.intersects(rect,visibleRect)){removeAndRecycleView(item,recycler);mChildHasAttached.put(position,false);Log.d(TAG,"移除视图 位置:"+position);}else{//可视区域放置子视图layoutDecoratedWithMargins(item , rect.left - mScrollDistanceX, rect.top ,rect.right - mScrollDistanceX, rect.bottom);mChildHasAttached.put(position , true);Log.d(TAG,"放置视图 位置:"+position);}}//RecyclerView头尾填充空白区域View firstItem = getChildAt(0);View lastItem = getChildAt(getChildCount() - 1);if(travel >= 0 ){//左滑:向底部滑动int minPos = getPosition(firstItem);//填充可视区域右侧的Viewfor(int i = minPos; i < getItemCount(); i++){insertView(i, visibleRect, recycler, false);}}else{//右滑:向顶部滑动int maxPos = getPosition(lastItem);//填充可视区域左侧的Viewfor(int i = maxPos; i >= 0; i--){insertView(i, visibleRect, recycler, true);}}return travel;
}//针对可视区域插入子视图
private void insertView(int pos , Rect visibleRect , RecyclerView.Recycler recycler , boolean firstPos){Rect rect = mChildPositionRects.get(pos);if(Rect.intersects(visibleRect , rect) && !mChildHasAttached.get(pos)){//仅在可视区域其未显示的视图才执行插入视图View item = recycler.getViewForPosition(pos);if(firstPos){addView(item , 0);}else{addView(item);}measureChildWithMargins(item,0,0);layoutDecoratedWithMargins(item, rect.left - mScrollDistanceX, rect.top ,rect.right - mScrollDistanceX, rect.bottom);mChildHasAttached.put(pos, true);}
}

至此已经可以做到展示1中的非3D画廊效果

实现滑动过程3D旋转效果

实现3D旋转效果主要在于计算旋转角度,先了解下Android的三维坐标系,详细图示如下图所示。对于围绕坐标轴旋转的情况,顺时针为正向,逆时针为负向。

要实现图示的效果,关键在于设置子视图围绕y方向的旋转,其思路如下两侧视图距离中心视图的距离offsetX越大,旋转角度越大。且offsetX为负时,旋转角度为正。为了避免UI效果明显变形,实际操作过程中要限定最大变换角度。

在自定义RecyclerView中计算旋转角度的代码如下

private final float MAX_ROTATION_Y = 20.0f;
//根据与中心点的距离计算y轴旋转角度,距离越远旋转越大
private float calculateRotationY(int offsetX){float rotation = -MAX_ROTATION_Y * offsetX / mIntervalDistance;if(rotation < -MAX_ROTATION_Y){rotation = -MAX_ROTATION_Y;}else if(rotation > MAX_ROTATION_Y){rotation = MAX_ROTATION_Y;}return rotation;
}

设置子视图的旋转则是在自定义RecyclerView中重写drawChild(Canvas canvas, View child, long drawingTime)方法实现,其代码较为简单,贴上代码如下

/*** 设置子视图的缩放系数/旋转角度* @param canvas* @param child* @param drawingTime* @return*/
@Override
public boolean drawChild(Canvas canvas, View child, long drawingTime) {int childWidth = child.getWidth() - child.getPaddingLeft() - child.getPaddingRight();int childHeight = child.getHeight() - child.getPaddingTop() - child.getPaddingBottom();int width = getWidth();if(width <= child.getWidth()){return super.drawChild(canvas, child, drawingTime);}int pivot = (width - childWidth)/2;int x = child.getLeft();float scale , alpha;alpha = 1 - 0.6f*Math.abs(x - pivot)/pivot;if(x <= pivot){scale = 2f*(1-mSelectedScale)*(x+childWidth) / (width+childWidth) + mSelectedScale;}else{scale = 2f*(1-mSelectedScale)*(width - x) / (width+childWidth) + mSelectedScale;}child.setPivotX(childWidth / 2);child.setPivotY(childHeight*2 / 5);child.setScaleX(scale);child.setScaleY(scale);float rotationY = calculateRotationY(x - pivot);if(Math.abs(x - pivot) < 5){child.setRotationY(0);rotationY = 0;}else {child.setRotationY(rotationY);}return super.drawChild(canvas, child, drawingTime);
}

至此3D画廊效果完全实现了

代码使用

实现本文所述效果只是针对LayoutManager和RecyclerView进行了自定义,复制该两个文件至项目中即可实现初步效果。相关代码已经上传至Gitee中

https://gitee.com/com_mailanglidegezhe/solid_gallery.git

Bug修复记录

1. 快速滑动场景下右侧内容空白问题

问题原因:快速滑动场景下,添加子视图触发了onLayoutChildren逻辑重走,该场景下子视图位置并未重置,无法满足重新布局的要求。(在此特别感谢热心网友提供的线索)
问题对策:在onLayoutChildren方法执行时增加判断当前布局是否已经layout,如果已经layout直接返回即可

学习心得

最后还是要感谢启舰著的《Android自定义控件高级进阶与精彩实现》书中第8章给予的技术培训,本文所述的主要方法和思想来自于本书的指引。由于时间和能力水平限制,目前该3D画廊效果截至目前存在的快速滑动时偶发子视图放置逻辑失效的问题已经优化。如有其他待优化的问题,欢迎大家指正。

玩转RecyclerView | 实现子视图叠加 | 3D画廊效果 | 高级动效 | Android 3D坐标系介绍相关推荐

  1. [原创]自定义ViewPager实现3D画廊效果

    经常在群里看到有些开发者在提问:怎么实现3D画廊效果,没思路. 有人出谋划策,你重写onTouch,在里面去判断:或者你去重写滑动监听事件,滑动的时候去动态设置左右两边的图片的大小和缩放效果.可能你们 ...

  2. 使用RecyclerView实现旋转3D画廊效果

    3D旋转画廊效果实现有哪些方式? 1.Gallery实现(官方已不推荐使用). 2.RecyclerView通过自定义LayoutManager实现. 一.简介 RecyclerView是google ...

  3. Android自带组件之Gallery 实现3D画廊效果

    1: 首先我们要了解到这个该控件的常用属性: 如图: 2:通过该组件定义属于我们自己的组件 iphone 中的coverflow中图片切换是有旋转和缩放效果的,而自带的gallery中并没有实现.因此 ...

  4. HTML+CSS做出3D照片效果(HTML+CSS for 3D photo effect)

    2022.10.14大家好,我最近看到一个关于用HTML+CSS实现的3D照片觉得非常好看,如图: Hello everyone, I recently saw a 3D photo about us ...

  5. android画廊效果的轮播图,轮播图(3d画廊效果)

    首先需要将轮播图的依赖导入 implementation 'com.github.xiaohaibin:XBanner:1.6.1' 接下来就是在项目目录下bulidgradle中导入(allproj ...

  6. XBanner实现3D画廊效果

    导依赖 在工程的build.gradle中allprojects {repositories {google()jcenter()maven { url 'https://jitpack.io' }} ...

  7. echarts:实现3D地图版块叠加动效散点+轮播高亮效果

    需求描述 如下图所示,展示3D效果的地图版块,并叠加显示动效散点: 实现思路 首先是3D地图版块效果的实现,可以参考广州3D地图:而动效散点的实现,可以参考地图发散分布. 这里再提一个经过尝试并不行的 ...

  8. html3d旋转效果相册,HTML5css3:3D旋转木马效果相册

    这篇博客的目的是因为上篇HTML5 CSS3专题 诱人的实例 CSS3打造百度贴吧的3D翻牌效果中有个关于CSS 3D效果的比较重要的知识点没讲到,就是perspective和tranlateY 效果 ...

  9. html5 相册翻转效果,HTML5 css3:3D旋转木马效果相册

    这篇博客的目的是因为上篇HTML5 CSS3专题 诱人的实例 CSS3打造百度贴吧的3D翻牌效果中有个关于CSS 3D效果的比较重要的知识点没讲到,就是perspective和tranlateY 效果 ...

  10. HTML5 CSS3 专题 诱人的实例 3D旋转木马效果相册

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/32964301 首先说明一下创意的出处:http://www.zhangxinxu ...

最新文章

  1. 【UML建模】机房中的UML图
  2. Matlab与线性代数 -- 寻找矩阵的非零元素
  3. linux标准i/o,Linux 标准I/O笔记
  4. 请求中文乱码_【1】执行Http请求访问网页
  5. linux下php远程连接mysql_Linux下PHP远程连接Oracle数据库 | 系统运维
  6. MySQL5.7新特性——在线收缩undo表空间 (转载)
  7. 在 WASI 上运行 .NET 7 应用程序
  8. 安装rpm报错:requires Ruby version >= 2.*.*
  9. 《Hadoop与大数据挖掘》——2.6 TF-IDF算法原理及Hadoop MapReduce实现
  10. 当今将Windows应用程序迁移到Windows on Arm的实践
  11. 力扣—— 224. 基本计算器(困难)
  12. Java 8 中处理集合的优雅姿势——Stream
  13. docker 常用操作-push分享及下载
  14. ArcGIS拓扑检查教程
  15. AWS EMR 上 Spark 任务 Container killed Exit code 137 错误
  16. 一文说明白ECDSA spec256k1 spec256r1 EdDSA ed25519千丝万缕的关系
  17. 计算机专业答辩开场白,计算机专业论文答辩开场白范文
  18. 云计算应用(上) -- 云计算应用概述
  19. 图表背后的秘密 | 技术指标讲解:ATR指标
  20. 盘点2020年网红品牌营销案例,它们刷屏凭什么?

热门文章

  1. golang读取pdf
  2. 认知差距决定我们的人生差距?!
  3. 清华大学朱小燕教授做客雷锋网沙龙,分享 NLP 和人工智能的那些事儿| AAAI 2017...
  4. Bondareva-Shapley 定理
  5. sqlite3 表创建后设置主外键 联合主键 外键设置
  6. 开源IT监控系统对比
  7. 什么是RS232串口RS232电平
  8. 影响世界的100个经典管理定律
  9. 计算机病毒是如何入侵你的电脑吗,怎么样正确处理被病毒侵入的电脑
  10. Linux虚拟网络基础——Bridge