阅读完本文约需7分钟。

废不说,看图,有图有**

带有立体纵深的卡片翻页效果,稍加组合和颜色变化就可以搭配出多种不同的风格,如:

比赛比分牌

卡片翻页时钟

一、设计思路

如何使得数字的变化更为灵动?3D纵深效果是个值得思考的方案。在现实生活中的一些特定场景,数字的变化就是通过翻页实现的,比如日历、比分牌、数学教具等等。

灵活替换翻页卡片的颜色和数字图片,即可展现多种不同风格

二、实现方案

2.1 3D纵深效果实现方案参考

为了在Android上实现3D纵深效果,可以考虑使用OpenGL进行,这样实现的效果是细节最丰富、最拟物的,但成本也巨大,为了一个控件使用OpenGL实在是用核弹打苍蝇——可以但没必要。

这里考虑采用Matrix+Camera结合的方案。

2.2 Matrix简介

android中的matrix是一个3*3的矩阵,如下图

scale控制缩放;

skew控制错切;

trans控制位移;

persp控制透视;

值得注意的是,旋转操作是通过scale和skew共同作用完成的。

android中matix的封装非常完善,使用者无需再去重拾体育老师教授的数学知识,直接调用。

2.3 Camera简介

这里指的是android.graphics包下的Camera,用于计算3D变换。封装得同样十分完善,使用者无需再用屁股思考控件坐标计算过程,直接调用,同时搭配好对应的matrix。

需要注意的是,Camera中摄像头的位置是对准画布左上角的。

坐标系如下,是左手坐标系:

常用方法如下:

2.4 UI拆解

2.4.1 形状分析

从形状上观察并不复杂,算上正在翻转的卡片,3个圆角矩形即可搞定。数字部分采用图片绘制,可以灵活替换,最后在指定卡片区域绘制图片即可。

2.4.2 模型设计

卡片翻转过程中,最多同一时刻出现3个卡片,故只需要按位置关系定义上中下三个卡片,中间的卡片负责翻转。

2.5 纵深效果实现

2.5.1 卡片绘制

绘制上中下卡片,中间卡片为活动页,需要最后绘制

override fun onDraw(canvas: Canvas?) {super.onDraw(canvas)setLayerType(LAYER_TYPE_SOFTWARE, null)//判断状态,不同状态绘制不同内容judgeState(curState)canvas?.let {drawUpCard(it)drawDownCard(it)// 中间活动card最后绘制drawMidCard(it)}
}

2.5.2 实现卡片翻页

中间卡片翻页效果通过Camera的roate方法实现,绕x轴进行旋转

private fun drawMidCard(canvas: Canvas) {if (!isNeedDrawMidCard) returnwith(canvas) {save()mMatrix.reset()mCamera.save()mCamera.translate(0F, 0F, depthZ)mCamera.rotateX(rotateX)mCamera.rotateY(rotateY)mCamera.getMatrix(mMatrix)mCamera.restore()val scale = resources.displayMetrics.densityval mValues = FloatArray(9)mMatrix.getValues(mValues)mValues[6] = mValues[6] / scalemValues[7] = mValues[7] / scalemMatrix.setValues(mValues)mMatrix.preTranslate(-width / 2F, -height / 2F)mMatrix.postTranslate(width / 2F, height / 2F)concat(mMatrix)mPaint.color = Color.WHITEmPaint.setShadowLayer(cardShadowSize, 0F, cardShadowDistance, Color.GRAY)val rectF = RectF(paddingSize, paddingSize + cardHeight, paddingSize + cardWidth, paddingSize + cardHeight * 2)drawRoundRect(rectF,20F,20F,mPaint)//todo: 绘制数字图片restore()}
}

结合我在《今日头条loading控件,隔壁产品都馋哭了》文章中提到过的坐标计算框架,用户手指在屏幕上移动的距离与中间卡片旋转角度存在某种函数关系,通过IFunc进行保存和计算。

代码如下:

/**
* Card翻转函数
*/
var cardRotateFunc: IFunc? = null
/**
* 阴影大小变化函数
*/
var cardShadowSizeFunc: IFunc? = null
/**
* 阴影距离变化函数
*/
var cardShadowDistanceFunc: IFunc? = null
/**
* 配置各个函数
*/
private fun configFunc() {cardRotateFunc = CardRotateFunc()with(cardRotateFunc!!) {inParamMin = 0FinParamMax = cardHeight * 2outParamMin = 0FoutParamMax = 180FinitValue = 45F}cardShadowSizeFunc = CardShadowSizeFunc()with(cardShadowSizeFunc!!) {inParamMin = 0FinParamMax = 180FoutParamMax = 50FoutParamMin = 0FinitValue = 10F}cardShadowDistanceFunc = CardShadowDistanceFunc()with(cardShadowDistanceFunc!!) {inParamMin = 0FinParamMax = 180FoutParamMax = 50FoutParamMin = 0FinitValue = 10F}
}

2.5.3 阴影变化

为了更好地模拟3D效果,卡片阴影也存在微小的变化

/**
* 根据旋转角度计算阴影大小、距离
*/
private fun executeShadowFunc(rotate: Float) {cardShadowSizeFunc?.let {cardShadowSize = it.execute(rotate)}cardShadowDistanceFunc?.let {cardShadowDistance = it.execute(rotate)}
}

2.5.4 数字图片绘制

数字的绘制就是图片的绘制,需要注意的是中间活动卡片在上翻或下翻转超过90度时,绘制的数字需要改变,涉及到图片的水平镜像翻转,调用matrix.postScale(-1F, 1F)实现。以向下翻转为例,代码如下:

if (curState == STATE_DOWN_ING) {//往下翻if (abs(cardRotateFunc!!.initValue - rotateX) >= 90F) {//绘制前一个数字if (curShowNum - 1 >= 0) {tempBm = Bitmap.createBitmap(numBms[curShowNum - 1], 0, 0, curNumBm.width, curNumBm.height, matrix, false)} else {tempBm = Bitmap.createBitmap(numBms[0], 0, 0, curNumBm.width, curNumBm.height, matrix, false)}} else {tempBm = Bitmap.createBitmap(numBms[curShowNum], 0, 0, curNumBm.width, curNumBm.height, matrix, false)}
}
tempBm?.let {drawBitmap(it, Rect(0, it.height / 2, it.width, it.height), rectF, mPaint)
}

2.6 实现交互

2.6.1 上下翻转

逻辑主要在onTouchEvent方法中,通过中间卡片初始角度和当前的翻转角度判断是上翻还是下翻

/**
* 手指按下的初始坐标
*/
private var downX: Float = 0F
private var downY: Float = 0F
private var offsetY: Float = 0F
override fun onTouchEvent(event: MotionEvent?): Boolean {when (event?.action) {MotionEvent.ACTION_DOWN -> {downX = event.xdownY = event.yif (downY >= height / 2) {//绘制下方的mid cardrotateX = 0FcurState = STATE_UP_ING} else {rotateX = 180FcurState = STATE_DOWN_ING}resetInitValue()postInvalidate()}MotionEvent.ACTION_MOVE -> {offsetY = event.y - downYexecuteFunc(offsetY)postInvalidate()}MotionEvent.ACTION_UP -> {//判断是上翻还是下翻if (rotateX >= 90F) {if (abs(cardRotateFunc!!.initValue - rotateX) >= 90F) {if (curShowNum + 1 <= 9) {startCardUpAnim(curShowNum + 1)} else {curShowNum = 9startCardDownAnim(9)}} else {startCardUpAnim(curShowNum)}} else {if (abs(cardRotateFunc!!.initValue - rotateX) >= 90F) {if (curShowNum - 1 >= 0) {startCardDownAnim(curShowNum - 1)} else {curShowNum = 0startCardUpAnim(0)}} else {startCardDownAnim(curShowNum)}}downX = 0FdownY = 0F}else -> {}}return true
}

2.6.2 翻页动画、阴影动画

由于采用了坐标计算框架,动画的实现就变得非常简单了,控制好offset变量,即可控制中间卡片的翻转角度、阴影距离、阴影大小。以上翻动画为例:

/**
* 卡片上翻动画
*/
private fun startCardUpAnim(curNum: Int) {cardRotateAnim?.cancel()cardRotateAnim = ValueAnimator.ofFloat(rotateX, 180F)with(cardRotateAnim!!) {duration = 400LaddUpdateListener {rotateX = it.animatedValue as FloatexecuteShadowFunc(rotateX)postInvalidate()}addListener(object : AnimatorListenerAdapter() {override fun onAnimationEnd(animation: Animator?) {super.onAnimationEnd(animation)resetInitValue()curState = STATE_NORMALcurShowNum = curNum}})start()}
}

2.6.3 显示数字的变化

当动画结束时,当前显示数字才是真正地发生了改变,对curShowNum进行赋值即可。也可以方便添加数字变化时的监听暴露给调用者。

2.7 加点细节

2.7.1 边界控制

当前数字为0时,无法再向下翻转,这时无需绘制上卡片;同样的,当前数字为9时,无需绘制下卡片

private fun judgeState(state: Int) {when (state) {STATE_NORMAL -> {isNeedDrawMidCard = falseisNeedDrawUpCard = trueisNeedDrawDownCard = true}STATE_UP_ING -> {isNeedDrawMidCard = trueif (curShowNum + 1 > 9) {isNeedDrawDownCard = false}}STATE_DOWN_ING -> {isNeedDrawMidCard = trueif (curShowNum - 1 < 0) {isNeedDrawUpCard = false}}}
}

2.7.2 翻页回弹

处于边界数字时,用户继续进行翻转操作,需要进行回弹至原位。

if (curShowNum + 1 <= 9) {startCardUpAnim(curShowNum + 1)
} else {curShowNum = 9startCardDownAnim(9)
}

三、后记

Android中的Camera+Matrix很好地模拟了3D效果,在很多控件设计场景中,相较于OpenGL更为轻量易用。控件中还存在很多细节问题的处理,限于文章篇幅不再展开详说。

控件放在了gitee上,点击“阅读原文”获取

技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

推荐阅读:

音视频面试基础题

OpenGL ES 学习资源分享

开通专辑 | 细数那些年写过的技术文章专辑

NDK 学习进阶免费视频来了

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

花里胡哨的3D翻页卡片,隔壁产品都馋哭了相关推荐

  1. 做个锤子的开关,隔壁产品都馋哭了

    码个蛋(codeegg) 第1049 次推文 作者:彭也 链接:https://zhuanlan.zhihu.com/p/193117308 前言 废话不多,有图有** 虽然老罗的锤子处于倒闭与即将倒 ...

  2. 便利贴撕页效果,隔壁产品都馋哭了

    阅读完本文约需 10 分钟. 废不说,有图 逼真模拟便利贴撕页效果,在一些需要分步操作的场景非常适合. 用户的每一步操作都非常清晰,撕页的效果可以提醒用户上一步操作已经圆满完成了. 比如,现在用户在填 ...

  3. android 控件随手指移动_液体流动控件,隔壁产品都馋哭了

    作者:彭也 链接: https://www.jianshu.com/p/4f0844c72e8a 模拟液体流动的展开特效,适合一些需要侧边展开进行辅助说明的页面,如用户在填写某个表单,需要操作很多步骤 ...

  4. android 根据bounds坐标进行点击操作_炫酷的Android时钟UI控件,隔壁产品都馋哭了...

    废话不多说,先上效果效果酷炫,动画丰富,效果爆炸boom-设计思路看腻了市面上各种丑陋难看的时钟控件,是时候整点新活!将现实生活中的摆钟圆形表盘设计.电子手表的数显表盘设计抽象出来,提取出" ...

  5. 程序员为这支笔掰头10个月,隔壁小学生都馋哭了

    鱼羊 萧箫 发自 凹非寺 量子位 报道 | 公众号 QbitAI 见证一款互联网风格的新智能硬件诞生,是一种怎样的体验? 产品经理和软硬件工程师们的面对面battle,总是其中见(xi)怪(wen)不 ...

  6. Python自动抢购脚本,学废了双十一双十二帮女票抢购心爱的礼物,隔壁女孩都馋哭了。

    Python版本:3.10 分享一个秒杀抢购的脚本程序,感兴趣的朋友一起看看吧 第一步:需要把想要的商品加进购物车( 此脚本是对购物车内全部商品进行下单操作,所以不够买的商品最好先从购物车内删除.) ...

  7. 用了5年的旧笔记本不要丢,1/4新机价格升级机器学习战斗本,隔壁研究员都馋哭了...

    大数据文摘出品 来源:medium 编译:zeroinfinity.Andy 高性能的硬件配置在机器学习研究中是枪炮一般的存在.面对大规模数据和多层结构算法,普通的计算机根本无法胜任. 但是,高配置的 ...

  8. 电脑主机,晚上就煎肉,把隔壁宿舍都馋哭了!

    点击上方"大鱼机器人",选择"置顶/星标公众号" 福利干货,第一时间送达! 本文转自网络,如有侵权请联系我们删除 ------------------分割线-- ...

  9. 全国最好吃的大学食堂来啦!隔壁小孩都馋哭了!

    考研择校千千万 有人看学术实力.有人看院校排名 有人看导师水准.有人看未来发展 但俗话说的好 民以食为天 不吃饱怎么搞学习? 全国最好吃的大学食堂 今天给大家盘一下 凭食堂打下优秀口碑 干饭人看完别流 ...

最新文章

  1. SitemapBaiduspider
  2. handler回调主线程_Android使用Handler实现子线程与子线程、子线程与主线程之间通信...
  3. SAP CO Report
  4. 【Numpy学习记录】np.cov详解
  5. web容器启动过程与web.xml
  6. OpenCL memory object 之 传输优化
  7. Halcon算子学习:create_surface_model
  8. 达梦工作笔记-将A表的ID插入到B表,将A表的数据更新到B表
  9. ajax error的用法,JQuery ajaxError()用法及代码示例
  10. python爬虫模拟浏览器的两种方法_python爬虫模拟浏览器访问-User-Agent过程解析
  11. 如何让路由器摆脱安全困扰
  12. 电脑麦克风使用不了没有声音了怎么办
  13. 传文件 华为云桌面_怎么避免亚马逊账号关联(二)?阿里云华为云ECS远程桌面教程...
  14. mysql查询时间段内数据
  15. IMDB TOP250中文版(截止2011.6.18)
  16. (Spring+SpringMVC+mybatis)SSM企业职工工资管理系统
  17. 毕业论文文献综述写作技巧,超级详细!
  18. 苹果电脑忘记开机密码怎么办?一段代码轻松解决
  19. OpenCV数字图像处理基于C++:灰度变换
  20. 前端基础面试题(HTML + CSS)

热门文章

  1. 使用NSSM将agent注册为windows系统服务说明
  2. Kingston将联合Intel进入SSD市场
  3. IDEA中一个项目如何导入另一个依赖的项目?
  4. 不懂就得问,买流量卡填写身份证号有风险吗?
  5. font-weight
  6. Derby 数据库的应用
  7. 发明复制粘贴的计算机科学家去世了
  8. 大数据算法(课后答案总汇)
  9. 关于es6中Proxy的学习笔记
  10. 追猎者:希腊债务初见曙光,贵金属隔夜小幅走高