一、Flutter-初遇

  • 2018-06月左右入坑Flutter,于是拿出美团和痘印等好看的界面感受了一波Flutter UI和绘制等写了三天的Demo也感受到了Flutter强大,当时匆匆忙忙就写了相关Demo上传了Github。不知不觉Github很多Star很开心,接着决定进行录制基础教学视频,在B站也收到了很多感谢私信、技术交流,记得2018年素未谋面的大哥因为我的热心无缘无故送我2018款MacBook Pro-在他的再三坚持下。带着这份感动和感激我也开始写了很多提供初学者一起学习的文章和依赖库。

Flutter开源库和文章

flutter_excle Flutter插件生成Excle文件    -   库   -   CSDN

Flutter3D玩的开心

flutter_time_axis时间轴插件    -   GitHub

flutter_pickter_plugin支持图片和文件视频的等的选择    -   库   -   GitHub

Flutter时间日期格式化等操作(一个月的最后一天日期,时间段内所有日期…)插件    -   库

WGS84与大地2000坐标转换(Java,C#,Dart)Flutter发布了支持库

当然还有很多在CSDN

B站教学链接

Android自定义绘制相关文章

Android自定义-艺术在于绘制

Android-自定义-曲线折线图表填充渐变,手势滑动,手势缩放等

Android自定义-任意区域可点击的折线图

Android自定义-手势缩放折线图

Android自定义-手势滑动缩放渐变填充曲线折线图表

Android-自定义可伸展的ViewGroup

Android自定义-曲线渐变填充

Jetpack-Compose

JetPack-Compose - 自定义绘制
当然还有很多在CSDN

IOS相关文章

SwiftUI学习笔记【基础UI

SwiftUI学习笔记-【列表】

SwiftUI学习笔记【path绘制】

SwiftUI学习笔记【Sqlite】

SwiftUI学习笔记【XML解析】

当然还有很多在CSDN

只要在编程的路上文章不会停下来...希望给你带来方便,你的点赞是我就是莫大的开心

二、Flutter UI带来的惊喜

  • Flutter带来的惊喜不仅点点:有状态的热重载可以快速重新渲染界面、富有表现力、极其灵活的UI。自身渲染引擎,脱离了原生基本组件不在作为映射,相对于Uni和RN等框架在性能方面占据绝对优势。滚动,导航,图标和字体,可以在iOS和Android上提供完整的原生性能体验,且支持Web,Desktop,Embedded…等多端。18年接触了Flutter就感受了一下UI的可塑性和创造性,完全不输原生且更加灵活多变,当时写的页面当然有很多这里列举其三:自定义裁剪绘制让应用的UI图破了天花板,而手势动画的加入更让你的应用,赏心悦目、超凡脱俗。那我们的Compose是否可以呢?今天主要的内容是探究Compose在UI上所表现的能力。

如果你对Flutter绘制和裁剪或者其他任何疑难杂症我想这个男人张风捷特烈一定能给你说的清清楚楚,透透彻彻

三、Compose UI-强大而简单

  • 前面两章我们学会了基本UI的编写和自定义绘制裁剪的应用。这节课我们将结合基本UI以及自定义加简单的手势动画来创造花里胡哨的界面,当然这并不代表美,目的在于学习Compose的可创造性,这节会穿插自定义曲线的讲解,好像几乎都离不开曲线。先看看下面的效果?

UI界面不仅是简单的官方组件排布,好的UI更需要精心的自定义,和动态的交互性。上面图虽然不代表美,但包含了很多交互和动态效果,我们从组件的交互动态可能性作为出发点进行探讨。当然你觉得什么样的UI最难可以发表你的看法,有时间我定还你一个朗朗乾坤。技术交流群QQ裙730772561

1、旋转、缩放、背景模糊

  • 组件的旋转缩放如果用Flutter来实现如何做呢?Transform或者RotationTransition这样的容器部件进行包裹,且需要设置动画控制AnimationController等。那Compose我们如何来旋转和缩放组件呢?Modifier不仅解决了参数过多可以统一配置问题,而且提供了极其强大的裁剪变换指针手势装饰等方法,大大的提高了便捷性和创造性。

1、Modifier.rotate(degrees: Float)

  • 任意的组件都可以通过Modifier进行旋转[Modifier.rotate(degrees: Float)]平移[Modifier.offset(x: Dp = 0.dp, y: Dp = 0.dp)]缩放[Modifier.scale(scale: Float)]、等变换。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OIlFcIoa-1616049802373)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/04e7f808b1bf4522b7e12281389df7db~tplv-k3u1fbpfcp-watermark.image)]

观察这部分动态特效
1、中间图片的旋转。
2、圆形图片外圆弧缩放。
3、背景的模糊和缩放。
4、运动的曲线。

2、动画创建和使用

  • 当我们鼠标点击界面时候中间的图片进行旋转了任意角度。我们已经知道旋转通过Modifier.rotate来设置,只需要在点击屏幕时候执行动画继续性更新角度值即可。所以简单动画创建需要掌握一下。
    动画储值器
    我们可以点击源码进行观看,动画执行过程中的值储存在mutableStateOf所以具有状态且发生改变时状态向下可以刷新UI,可以查看之前章节或者官网

//进行动画过程数值储存器
val animatedDegree = remember { Animatable(0f) }

执行动画
通过.animateTo(targetValue: T,animationSpec: AnimationSpec = defaultSpringSpec…)开始执行动画,可以设置动画目标值和动画规格速度等…

animatedOffset.animateTo(targetValue,animationSpec = spring(stiffness = StiffnessLow))

fun Modifier.pointerInput(key1: Any?, block: suspend PointerInputScope.() -> Unit):以处理修改后的元素区域内的指针输入。pointerInput可以调用PointerInputScope-awaitPointerEventScope来安装一个指针输入处理程序,该处理程序可以使waitPointerEventScope-awaitPointerEvent接收使用指针输入事件。 可以通过PointerInputScope.AwaitPointerEventScope上的扩展功能定义为执行更高级别的手势检测。 我们通过awaitPointerEventScope获取屏幕点击坐标作为我们的角度。

 @Composefun LoginPage(){//1.进行动画过程数值储存器val animatedDegree = remember { Animatable(0f) }Box() {//底部模糊大背景Image(//获取模糊bitmapbitmap = BitmapBlur.doBlur(getBitmap(resource =R.drawable.head_god).asAndroidBitmap(),animatedRound.value.toInt()+20,false).asImageBitmap(),contentDescription = "",contentScale = ContentScale.FillHeight,modifier = Modifier.fillMaxWidth().fillMaxHeight().scale(animatedScale.value, animatedScale.value),)//圆形图片和圆形弧圈Column(verticalArrangement = Arrangement.Center, modifier = Modifier.pointerInput(Unit) {coroutineScope {while (true) {//2.通过awaitPointerEventScope来处理指针事件val offset = awaitPointerEventScope {//获取第一次按下的位置坐标awaitFirstDown().position}//携程launch {//3.设置animated的目标值为按下的屏幕坐标系内的x值,且设置动画格式为比较平缓不生硬。开始执行动画。animatedDegree.animateTo(offset.x,animationSpec = spring(stiffness = StiffnessLow))}}}}) {Image( bitmap = getBitmap(R.drawable.head_god),contentDescription = "w",contentScale = ContentScale.FillBounds,modifier = Modifier.height(80.dp).width(80.dp).background(color = Color(0XFF0DBEBF), shape = CircleShape).padding(3.dp).clip(CircleShape).shadow(elevation = 150.dp, clip = true).rotate(//4.设置角度为初始化到目标x的动画值跟新UIanimatedDegreen.value))}}

3、半透明圆环缩放颜色动画

  • 同样的通过val animatedScale = remember { Animatable(1f) }来创建缩数值储存器,通过点击时候执行动画去跟新图片大小颜色也是同样设置val animatedColor = remember { Animatable(Color(206, 199, 250, 121)) }点击时候去设置目标缩放值和颜色执行.animalTo即可执行连续动画且向上去跟新储存状态再下发到跟新UI。圆形裁剪相关的看之前文章
  Box(contentAlignment = Alignment.Center,modifier = Modifier.padding(0.dp).clip(CicleImageShape()).background(animatedColor.value).width((130 * animatedScale.value).dp).height((130 * animatedScale.value).dp)) {Image(bitmap = getBitmap(R.drawable.head_god),contentDescription = "",contentScale = ContentScale.FillBounds,modifier = Modifier.height(80.dp).width(80.dp).background(color = Color(0XFF0DBEBF), shape = CircleShape).padding(3.dp).clip(CircleShape).shadow(elevation = 150.dp, clip = true).rotate(animatedOffset.value))}

4、图片高斯模糊的获取

Bitmap和ImageBitmap可以相互转换:Bitmap.asImageBitmap(): ImageBitmapfun ImageBitmap.asAndroidBitmap(): Bitmap

object BitmapBlur {fun doBlur(sentBitmap: Bitmap, radiu: Int = 1, canReuseInBitmap: Boolean): Bitmap {var radius: Int = radiuval bitmap: Bitmap = if (canReuseInBitmap) {sentBitmap} else {sentBitmap.copy(sentBitmap.config, true)}if (radius < 1) {radius = 0}val w = bitmap.widthval h = bitmap.heightval pix = IntArray(w * h)bitmap.getPixels(pix, 0, w, 0, 0, w, h)val wm = w - 1val hm = h - 1val wh = w * hval div = radius + radius + 1val r = IntArray(wh)val g = IntArray(wh)val b = IntArray(wh)var rsum: Intvar gsum: Intvar bsum: Intvar x: Intvar y: Intvar i: Intvar p: Intvar yp: Intvar yi: Intvar yw: Intval vmin = IntArray(Math.max(w, h))var divsum = div + 1 shr 1divsum *= divsumval dv = IntArray(256 * divsum)i = 0while (i < 256 * divsum) {dv[i] = i / divsumi++}yi = 0yw = yival stack = Array(div) {IntArray(3)}var stackpointer: Intvar stackstart: Intvar sir: IntArrayvar rbs: Intval r1 = radius + 1var routsum: Intvar goutsum: Intvar boutsum: Intvar rinsum: Intvar ginsum: Intvar binsum: Inty = 0while (y < h) {bsum = 0gsum = bsumrsum = gsumboutsum = rsumgoutsum = boutsumroutsum = goutsumbinsum = routsumginsum = binsumrinsum = ginsumi = -radiuswhile (i <= radius) {p = pix[yi + Math.min(wm, Math.max(i, 0))]sir = stack[i + radius]sir[0] = p and 0xff0000 shr 16sir[1] = p and 0x00ff00 shr 8sir[2] = p and 0x0000ffrbs = r1 - Math.abs(i)rsum += sir[0] * rbsgsum += sir[1] * rbsbsum += sir[2] * rbsif (i > 0) {rinsum += sir[0]ginsum += sir[1]binsum += sir[2]} else {routsum += sir[0]goutsum += sir[1]boutsum += sir[2]}i++}stackpointer = radiusx = 0while (x < w) {r[yi] = dv[rsum]g[yi] = dv[gsum]b[yi] = dv[bsum]rsum -= routsumgsum -= goutsumbsum -= boutsumstackstart = stackpointer - radius + divsir = stack[stackstart % div]routsum -= sir[0]goutsum -= sir[1]boutsum -= sir[2]if (y == 0) {vmin[x] = Math.min(x + radius + 1, wm)}p = pix[yw + vmin[x]]sir[0] = p and 0xff0000 shr 16sir[1] = p and 0x00ff00 shr 8sir[2] = p and 0x0000ffrinsum += sir[0]ginsum += sir[1]binsum += sir[2]rsum += rinsumgsum += ginsumbsum += binsumstackpointer = (stackpointer + 1) % divsir = stack[stackpointer % div]routsum += sir[0]goutsum += sir[1]boutsum += sir[2]rinsum -= sir[0]ginsum -= sir[1]binsum -= sir[2]yi++x++}yw += wy++}x = 0while (x < w) {bsum = 0gsum = bsumrsum = gsumboutsum = rsumgoutsum = boutsumroutsum = goutsumbinsum = routsumginsum = binsumrinsum = ginsumyp = -radius * wi = -radiuswhile (i <= radius) {yi = Math.max(0, yp) + xsir = stack[i + radius]sir[0] = r[yi]sir[1] = g[yi]sir[2] = b[yi]rbs = r1 - Math.abs(i)rsum += r[yi] * rbsgsum += g[yi] * rbsbsum += b[yi] * rbsif (i > 0) {rinsum += sir[0]ginsum += sir[1]binsum += sir[2]} else {routsum += sir[0]goutsum += sir[1]boutsum += sir[2]}if (i < hm) {yp += w}i++}yi = xstackpointer = radiusy = 0while (y < h) {// Preserve alpha channel: ( 0xff000000 & pix[yi] )pix[yi] =-0x1000000 and pix[yi] or (dv[rsum] shl 16) or (dv[gsum] shl 8) or dv[bsum]rsum -= routsumgsum -= goutsumbsum -= boutsumstackstart = stackpointer - radius + divsir = stack[stackstart % div]routsum -= sir[0]goutsum -= sir[1]boutsum -= sir[2]if (x == 0) {vmin[y] = Math.min(y + r1, hm) * w}p = x + vmin[y]sir[0] = r[p]sir[1] = g[p]sir[2] = b[p]rinsum += sir[0]ginsum += sir[1]binsum += sir[2]rsum += rinsumgsum += ginsumbsum += binsumstackpointer = (stackpointer + 1) % divsir = stack[stackpointer]routsum += sir[0]goutsum += sir[1]boutsum += sir[2]rinsum -= sir[0]ginsum -= sir[1]binsum -= sir[2]yi += wy++}x++}bitmap.setPixels(pix, 0, w, 0, 0, w, h)return bitmap}
}

5、运动的曲线

  • 曲线的弧度运动可以根据控制点的位置变换而变化。在自定义绘制章节我讲过可以看看。
1、贝塞尔曲线

凡是函数都可以和坐标系绘制进行一一映射,当然了贝塞尔曲线也是有方程式的。有如下:

线性贝塞尔曲线

  • 给定点P0、P1,线性贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:
         

二次方贝塞尔曲线

  • 二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
         

三次方贝塞尔曲线

P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝塞尔曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;公式如下:

当然在Android端的Native层已经封装好了方法,二次方贝塞尔曲线三次方贝塞尔曲线,已知函数当然可以进行封装。

在Android端提供了二阶和三阶`二次方贝塞尔曲线`:public void quadTo(float x1, float y1, float x2, float y2)public void rQuadTo(float dx1, float dy1, float dx2, float dy2) `三次方贝塞尔曲线`:public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)public void rCubicTo(float x1, float y1, float x2, float y2,float x3, float y3) 

接下来我们绘制一个二阶曲线,控制点可以随着手势的移动和下按进行对应的屏幕移动,对于手势坐标系和屏幕坐标系的映射转换上节折线里面说很明白了,这里不多做解释。

  • quadTo(float x1, float y1, float x2, float y2)
    //记录移动的canvas画布坐标,不是手势坐标,由手势坐标转换为canvas坐标进行刷新private var moveX: Float = 160fprivate var moveY: Float = 160fprivate fun drawQuz(canvas: Canvas) {controllRect = Rect((moveX - 30f).toInt(),(moveY + 30f).toInt(),(moveX + 30).toInt(),(moveY - 30f).toInt())val quePath = Path()canvas.drawCircle(0f, 0f, 10f, getPaintCir(Paint.Style.FILL))canvas.drawCircle(320f, 0f, 10f, getPaintCir(Paint.Style.FILL))//第一个点和控制点的连线到最后一个点链线。为了方便观察val lineLeft = Path()lineLeft.moveTo(0f, 0f)lineLeft.lineTo(moveX, moveY)lineLeft.lineTo(320f, 0f)canvas.drawPath(lineLeft, getPaint(Paint.Style.STROKE))//第一个p0处画一个圆。第二个p1处画一个控制点圆,最后画一个。canvas.drawCircle(moveX, moveY, 10f, getPaintCir(Paint.Style.FILL))quePath.quadTo(moveX, moveY, 320f, 0f)canvas.drawPath(quePath, getPaint(Paint.Style.STROKE))}override fun onTouchEvent(event: MotionEvent): Boolean {when (event.action) {ACTION_DOWN,ACTION_MOVE -> {//在控制点附近范围内部,进行移动Log.e("x=", "onTouchEvent: (x,y)"+(event.x - width / 2).toInt()+":"+(-(event.y - height / 2)).toInt())//将手势坐标转换为屏幕坐标moveX = event.x - width / 2moveY = -(event.y - height / 2)invalidate()}}return true}

上图可以拖动控制点,在起点和结尾之间的曲线随着控制点发生了变形。控制点靠近那一侧弧度的凸起就偏向那一侧,初步的认识这一个规律即可,而练习中不断的去调节控制点达到我们的需求。但是在上图中我们回发现弧度不够圆圈,在三阶函数里面可以很好的调节弧度。接下来我们来看看三阶函数

三阶曲线

  • public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
    同样我们在坐标系内绘制三阶曲线。
    为了很好的看到效果我们这次进行来精细的控制,我们可以拖动任意我们想要拖动的控制点进行观察我们的三阶曲线。在上章节折线中对于手势配合Rect的contains方法可以进行局部的点击,当然了拖动也是没问题的。
    如下图:我们只需要在控制点附近进行绘制距形包裹住控制点,手势滑动时时刷新控制点对应的距形即可。

  private fun drawCubic(canvas: Canvas) {val cubicPath=Path()cubicPath.moveTo(0f,0f)cubicLeftRect= Rect((moveCubiX - 30f).toInt(),(moveCubiY - 30f).toInt(),(moveCubiX + 30).toInt(),(moveCubiY + 30f).toInt())cubicRightRect=Rect((moveCubiXX - 30f).toInt(),(moveCubiYY - 30f).toInt(),(moveCubiXX + 30).toInt(),(moveCubiYY + 30f).toInt())val lineLeft = Path()lineLeft.moveTo(0f, 0f)lineLeft.lineTo(moveCubiX, moveCubiY)lineLeft.lineTo(moveCubiXX, moveCubiYY)lineLeft.lineTo(320f, 0f)canvas.drawPath(lineLeft, getPaint(Paint.Style.STROKE,Color.GRAY))//canvas.drawRect(cubicLeftRect, getPaint(Paint.Style.FILL,Color.RED))//canvas.drawRect(cubicRightRect, getPaint(Paint.Style.FILL,Color.RED))canvas.drawCircle(moveCubiX, moveCubiY, 10f, getPaintCir(Paint.Style.FILL))canvas.drawCircle(moveCubiXX, moveCubiYY, 10f, getPaintCir(Paint.Style.FILL))cubicPath.cubicTo(moveCubiX,moveCubiY,moveCubiXX,moveCubiYY,320f,0f)canvas.drawPath(cubicPath, getPaint(Paint.Style.STROKE,Color.RED))}override fun onTouchEvent(event: MotionEvent): Boolean {when (event.action) {ACTION_DOWN,ACTION_MOVE -> {//在控制点附近范围内部,进行移动Log.e("x=","onTouchEvent: (x,y)" + (event.x - width / 2).toInt() + ":" + (-(event.y - height / 2)).toInt())//二阶曲线if (controllRect.contains((event.x - width / 2).toInt(),(-(event.y - height / 2)).toInt())) {Log.e("点击来","对" )moveX = event.x - width / 2moveY = -(event.y - height / 2)invalidate()//三阶曲线控制点1}else if(cubicLeftRect.contains((event.x - width / 2).toInt(),(-(event.y - height / 2)).toInt())){moveCubiX= event.x - width / 2moveCubiY= -(event.y - height / 2)invalidate()//三阶曲线控制点2}else if(cubicRightRect.contains((event.x - width / 2).toInt(),(-(event.y - height / 2)).toInt())){moveCubiXX= event.x - width / 2moveCubiYY= -(event.y - height / 2)invalidate()}}}return true}

到这里我想我们应该大概的明白二阶和三阶曲线对于弧度的大致方向控制了吧。你以为这样就结束了么。接下来下来开始正式的进入曲线动画。

  • 如上图图片裁剪的弧度可以通过二阶曲线进行绘制路径进行裁剪。中间点为控制点即可,点击屏幕时候通过点击事件获取的屏幕x轴的值作为控制点的目标数值去执行动画即可。
 Image(bitmap = BitmapBlur.doBlur(getBitmap(resource =R.drawable.head_god).asAndroidBitmap(),animatedBitmap.value.toInt(),false).asImageBitmap(),contentDescription = "",contentScale = ContentScale.FillWidth,modifier = Modifier.fillMaxWidth().height(230.dp).clip(//二阶曲线进行裁剪。QureytoImageShapes(160f, animatedOffsetX.value)).scale(animatedScale.value, animatedScale.value)//头部背景图片缩放)@Stable
class QureytoImageShapes(var hudu: Float = 100f, var controller:Float=0f) : Shape {override fun createOutline(size: Size,layoutDirection: LayoutDirection,density: Density): Outline {val path = Path()path.moveTo(0f, 0f)path.lineTo(0f, size.height - hudu)//默认初始化选择中间作为控制点坐标x的数值if(controller==0f){controller =size.width / 2f}path.quadraticBezierTo(controller, size.height, size.width, size.height - hudu)path.lineTo(size.width, 0f)path.close()return Outline.Generic(path)}
}

2、文字上色和动效

  • 上图中我们可以看到文字有颜色动效,且下面的文字随着点击有一定的弯曲动效。

    文字颜色动效

    或者

我们将渐变进行移动变换,那随着移动给文字着色就成了形成了这种效果
我们前几个章节已经知道,路径、画布可以进行平移变换,其实我们的LinearGradient也可以设置变换例如移动等通过linearGradient.setLocalMatrix(transMatrix)

文字随着路径动效

  • 自定义中文字可以沿着Path绘制,想让文字动起来我们可以让Path动起来从而带动文字动。
    val animatedOffset = remember { Animatable(0f) }....点击时候触发animalTo设置目标值为屏幕坐标x即可。当然目标值自己可以定义其他的...看需求Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth().padding(top = 20.dp)) {Column(horizontalAlignment = Alignment.CenterHorizontally) {androidx.compose.foundation.Canvas(modifier = Modifier.fillMaxWidth().draggable(state = DraggableState {}, orientation = Orientation.Horizontal, onDragStarted = {}, onDragStopped = {}),) {drawIntoCanvas { canvas ->val paint = Paint()paint.style = PaintingStyle.Fillpaint.color = Color.Greenval text_paint = android.graphics.Paint()text_paint.strokeWidth = 2ftext_paint.style = android.graphics.Paint.Style.FILLtext_paint.color = android.graphics.Color.BLACKtext_paint.textSize = 52f//测量文字宽度val rect = android.graphics.Rect()text_paint.getTextBounds("ComposeUnit 登陆", 0, 6, rect)val colors = intArrayOf(android.graphics.Color.BLACK,android.graphics.Color.argb(250,121,animatedOffset.value.toInt(),206),android.graphics.Color.argb(250, 121, 206, animatedOffset.value.toInt()))val positions = floatArrayOf(0.2f, 11f, 0.2f)//让渐变动起来从而感觉到文字闪动起来了val transMatrix = android.graphics.Matrix()transMatrix.postTranslate(-rect.width() + rect.width() * 2 * (animatedScale.value * 1.5f),0f)//设置渐变val linearGradient = android.graphics.LinearGradient(0f,0f,rect.width().toFloat(),0f,colors,positions,android.graphics.Shader.TileMode.CLAMP)//设置矩阵变换linearGradient.setLocalMatrix(transMatrix)text_paint.shader = linearGradient//1.坐标变换canvas.nativeCanvas.drawText("ComposeUnit 登陆",size.width / 3.5f,size.height / 2.5f,text_paint)val secontextPath = android.graphics.Path()val rect1 = android.graphics.Rect()text_paint.getTextBounds("更多精彩,更多体验 ~", 0, 6, rect1)secontextPath.moveTo(340f, 100f)//0-110if (animatedOffset.value == 0f) {secontextPath.quadTo(350f, 10f, 710f, 100f)}//设置曲线路径的控制点通过点击的x轴坐标来控制文字跟随路径的动效secontextPath.quadTo(animatedOffset.value, 10f, 710f, 100f)text_paint.textSize = 32ftext_paint.letterSpacing = 0.3f//canvas.nativeCanvas.drawPath(secontextPath,text_paint)canvas.nativeCanvas.drawTextOnPath("更多精彩,更多体验 ~",secontextPath,0f,0f,text_paint)}}}}

3、没见过的输入框?

  • 上图应该是我们常见的输入框吧?如何让输入框变的与众不同花里胡哨,那绝对离不开绘制。如下我们点击屏幕时候输入框边界有明显的弧度变化。Modifier.border自定义绘制即可完成。

  Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxWidth().padding(top = 60.dp)) {TextField(value = "",onValueChange = { },// shape = AnimalRoundedCornerShape(animatedRound.value),colors = TextFieldDefaults.textFieldColors(unfocusedIndicatorColor = Color.Transparent,focusedIndicatorColor = Color.Transparent,backgroundColor = Color.Transparent),modifier = Modifier.height(48.dp).border(1.2.dp,//animatedColor.value.copy(alpha = 1f)Color(animatedColor.value.red,animatedColor.value.green,animatedColor.value.blue,1f),AnimalRoundedCornerShape(animatedRound.value)),leadingIcon = {Icon(bitmap = getBitmap(R.mipmap.yinzhang),contentDescription = "")})}
//输入框边界border动画
@Stable
class AnimalRoundedCornerShape(val value:Float=30f):Shape{override fun createOutline(size: Size,layoutDirection: LayoutDirection,density: Density): Outline {val path = Path()path.lineTo(value,0f)path.cubicTo(value,0f,0f,0f,0f,value)path.lineTo(0f,size.height-value)path.cubicTo(0f,size.height-value,0f,size.height,value,size.height)path.quadraticBezierTo(size.width/2,size.height-value,size.width-value,size.height)path.quadraticBezierTo(size.width,size.height,size.width,size.height-value)path.lineTo(size.width,value)path.quadraticBezierTo(size.width,0f,size.width-value,0f)path.quadraticBezierTo(size.width/2,value,value,0f)path.lineTo(value,0f)return Outline.Generic(path)}}          

4、动效CheckBox

  • CheckBox的动画同样 Modifier.clip()即可。只不过这里注意的是裁剪时候需要自定义裁剪进行内部缩近裁剪,裁剪使用API提供CircleShaper设置半径不会起作用.
Checkbox(checked = true,onCheckedChange = { },colors = CheckboxDefaults.colors(checkedColor = Color(0XFF0DBEBF)),modifier = Modifier.clip(CicleImageShape(animatedCheckBox.value))
//裁剪圆
@Stable
class CicleImageShape(val circle: Float = 0f) : Shape {override fun createOutline(size: Size,layoutDirection: LayoutDirection,density: Density): Outline {val minWidth = Math.min(size.width-circle, size.width-circle)val rect = Rect(circle, circle, minWidth, minWidth)val path = Path()path.addOval(rect)return Outline.Generic(path)}
}

四、Flutter and Compose --bottomBar

  • BottomBar底部导航栏在移动端必不可少,历史的长河中也没有太多大胆的设计?记得Flutter写过底部导航栏,当时Flutter群友们说很难自己写很费劲,我偷偷试了一下也就十几分钟对不对。那今天的主题是Compose那么是否Compose能够搞定这个?当然没问题,所有的UI无非曲线配合动画而已。接下来我们分析解决。

1、基本的曲线绘制

  • 镇楼图,接下来我们搞它ok?

Compose的bottomBar源码看到提供一个@Compose,自定义任意发挥。

我们在完成的图看到有三个切换按钮,那我们如何绘制按钮和曲线呢?


上图我们可以很清晰看到弧度最低点的X坐标=size.width/3/2,中间 X坐标=size.width/3/2+size.width/3,右边X坐标=size.width/3/2 + size.width/3*2。简单的小学运算。


为了曲线平滑我们用三阶曲线,控制点第二个虚线和第三个虚线与曲线的交点左右即可,不太明白三阶曲线的看上文。

   Box(modifier = Modifier.fillMaxWidth(),contentAlignment = Alignment.BottomEnd,) {Canvas(modifier = Modifier.fillMaxWidth().height(70.dp), onDraw = {drawIntoCanvas { canvas ->val paint = Paint()paint.color = Color(0XFF0DBEBF)paint.style = PaintingStyle.Fillval path = Path()//先固定分为三等分val widthOfOne = size.width / 3//每一个弧度的中心控制点val centerWidthOfOneX = widthOfOne / 2//弧度端口到两遍ONewidth距离val marginLeftAndRigth = centerWidthOfOneX / 1.6fval controllerX = centerWidthOfOneX / 6f//这个就是移动的过程从动画部分默认第一个选中有弧度。val keyAnimal = widthOfOne * 0canvas.save()//绘制圆圈背景//canvas.drawCircle(Offset(centerWidthOfOneX + keyAnimal, 0f), 60f, paint)//上文曲线懂了这里就是简单的调整,几分钟差不多path.moveTo(0f, 0f)path.lineTo(marginLeftAndRigth / 2 + keyAnimal, 0f)path.cubicTo(marginLeftAndRigth + keyAnimal,0f,centerWidthOfOneX - (centerWidthOfOneX - controllerX) / 2f + keyAnimal,size.height / 3f,centerWidthOfOneX + keyAnimal,size.height / 2.6f)path.cubicTo(centerWidthOfOneX + (centerWidthOfOneX - controllerX) / 2f + keyAnimal,size.height / 2.6f,widthOfOne - (marginLeftAndRigth) + keyAnimal,0f,widthOfOne - marginLeftAndRigth / 2 + keyAnimal,0f)path.lineTo(size.width, 0f)path.lineTo(size.width, size.height)path.lineTo(0f, size.height)path.close()canvas.clipPath(path)canvas.nativeCanvas.drawColor(Color(0XFF0DBEBF).toArgb())}})Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {Image(bitmap = getBitmap(resource = R.drawable.home),contentDescription = "1",modifier = Modifier.modifier(animalCenterIndex, 0, animalBooleanState).clickable {animalBoolean.value = !animalBoolean.valueanimalCenterIndex.value = 0})Image(bitmap = getBitmap(resource = R.drawable.center),contentDescription = "1",modifier = Modifier.modifier(animalCenterIndex, 1, animalBooleanState).clickable {animalBoolean.value = !animalBoolean.valueanimalCenterIndex.value = 1})Image(bitmap = getBitmap(resource = R.drawable.min),contentDescription = "1",modifier = Modifier.modifier(animalCenterIndex, 2, animalBooleanState).clickable {animalBoolean.value = !animalBoolean.valueanimalCenterIndex.value = 2})}}

背景圆圈如何添加当然简单的画布直接绘制圆圈最简单。

//绘制圆圈背景
canvas.drawCircle(Offset(centerWidthOfOneX + keyAnimal, 0f), 60f, paint)

2、曲线动画

点击时候弧度如何跟随变化呢?简单的整体坐标X平移size.width/3对应的倍数即可,例如第一个到第二个x轴坐标+size.width/31,第一个到第三个x轴坐标+size.width/32即可,其他同理。重点在如何获取点击选中的是哪一个index?Modifer.click点击我们可以设置选择的索引。加一个动画即可进行切换过渡效果。由于篇幅和时间问题动画单独开篇代码中案例有注可以自行观看。

@Composable
fun BottomNavigation(){//记录点击选择的索引val animalCenterIndex = remember { mutableStateOf(0) }val animalBoolean = remember { mutableStateOf(true) }val animalBooleanState: Float by animateFloatAsState(if (animalBoolean.value) {0f} else {1f}, animationSpec = TweenSpec(durationMillis = 600))//点击选择的状态变化,下发到animateFloatAsState里面动画执行开始val indexValue: Float by animateFloatAsState(//动画的目标值。当animalCenterIndex.value触发向下时候动画执行开始when (animalCenterIndex.value) {0 -> {0f}1 -> {1f}else -> {2f}},//设置动画的格式animationSpec = TweenSpec(durationMillis = 500))Box(modifier = Modifier.fillMaxWidth(),contentAlignment = Alignment.BottomEnd,) {Canvas(modifier = Modifier.fillMaxWidth().height(70.dp), onDraw = {drawIntoCanvas { canvas ->val paint = Paint()paint.color = Color(0XFF0DBEBF)paint.style = PaintingStyle.Fillval path = Path()//先固定分为三等分val widthOfOne = size.width / 3//每一个弧度的中心控制点val centerWidthOfOneX = widthOfOne / 2//弧度端口到两遍ONewidth距离val marginLeftAndRigth = centerWidthOfOneX / 1.6fval controllerX = centerWidthOfOneX / 6f//⭐️⭐️⭐️最重要的更新所有的坐标点就看这里val keyAnimal = widthOfOne * indexValue⭐️⭐️⭐️canvas.save()canvas.drawCircle(Offset(centerWidthOfOneX + keyAnimal, 0f), 60f, paint)path.moveTo(0f, 0f)path.lineTo(marginLeftAndRigth / 2 + keyAnimal, 0f)path.cubicTo(marginLeftAndRigth + keyAnimal,0f,centerWidthOfOneX - (centerWidthOfOneX - controllerX) / 2f + keyAnimal,size.height / 3f,centerWidthOfOneX + keyAnimal,size.height / 2.6f)path.cubicTo(centerWidthOfOneX + (centerWidthOfOneX - controllerX) / 2f + keyAnimal,size.height / 2.6f,widthOfOne - (marginLeftAndRigth) + keyAnimal,0f,widthOfOne - marginLeftAndRigth / 2 + keyAnimal,0f)path.lineTo(size.width, 0f)path.lineTo(size.width, size.height)path.lineTo(0f, size.height)path.close()canvas.clipPath(path)canvas.nativeCanvas.drawColor(Color(0XFF0DBEBF).toArgb())}})Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround) {Image(bitmap = getBitmap(resource = R.drawable.home),contentDescription = "1",modifier = Modifier.modifier(animalCenterIndex, 0, animalBooleanState).clickable {animalBoolean.value = !animalBoolean.valueanimalCenterIndex.value = 0})Image(bitmap = getBitmap(resource = R.drawable.center),contentDescription = "1",modifier = Modifier.modifier(animalCenterIndex, 1, animalBooleanState).clickable {animalBoolean.value = !animalBoolean.valueanimalCenterIndex.value = 1})Image(bitmap = getBitmap(resource = R.drawable.min),contentDescription = "1",modifier = Modifier.modifier(animalCenterIndex, 2, animalBooleanState).clickable {animalBoolean.value = !animalBoolean.valueanimalCenterIndex.value = 2})}}}
//将点击选择进行封装起来。单独处理返回按钮位置和是否旋转。
fun Modifier.modifier(animalCenterIndex: MutableState<Int>,i: Int,animalBooleanState: Float
): Modifier {return if (animalCenterIndex.value == i) {return Modifier.padding(bottom = 57.dp).width(25.dp).height(25.dp).rotate(animalBooleanState * 360)} else {return Modifier.padding(top = 20.dp).width(25.dp).height(25.dp)}}

3、还有什么UI办不到?

  • 写不好UI不是程序员的锅,只怪产品经理不够变态。当然了根据手机壳变颜色的我的确不行。百度Google了大半天,也没发现很难得界面,下面界面貌似还可以。

第一个图片貌似不错。我们在现在的基础上搞一波把?开始

这是UI第三篇文章,我想这里我就不在重复曲线的内容了。直接写了:不明白看前两章和自定义部分

 Box(contentAlignment = Alignment.TopStart) {androidx.compose.foundation.Canvas(modifier = Modifier.fillMaxHeight().width(250.dp),onDraw = {drawIntoCanvas { canvas ->val paint = Paint()paint.color = Color(36, 36, 92, 255)paint.style = PaintingStyle.Fillpaint.isAntiAlias = truepaint.blendMode = BlendMode.ColorDodgeval roundRect = Path()roundRect.moveTo(0f, 0f)roundRect.lineTo(size.width - 350f, 0f)roundRect.quadraticBezierTo(size.width,size.height / 2f,size.width - 350f,size.height)roundRect.lineTo(0f, size.height)roundRect.close()canvas.clipPath(roundRect)canvas.drawPath(roundRect, paint)}})Column(horizontalAlignment = Alignment.CenterHorizontally,modifier = Modifier.fillMaxHeight()) {Image(bitmap = getBitmap(R.drawable.head_god),contentDescription = "w",contentScale = ContentScale.FillBounds,modifier = Modifier.height(50.dp).width(50.dp).offset(x = 40.dp, y = 50.dp).background(color = Color(0XFF0DBEBF), shape = CircleShape).padding(3.dp).clip(CircleShape).shadow(elevation = 150.dp, clip = true).rotate(animatedOffset.value))Column(modifier = Modifier.offset(x = 40.dp, y = 50.dp),horizontalAlignment = Alignment.CenterHorizontally,) {Text(text = "Hello_World", fontSize = 13.sp, color = Color.White)Text(text = "路很长一加油", fontSize = 8.sp, color = Color.White)Row(modifier = Modifier.padding(top = 45.dp)) {Image(bitmap = getBitmap(resource = R.drawable.home),contentDescription = "1",modifier = Modifier.clickable {}.padding(end = 15.dp))Text(text = "Login", fontSize = 13.sp, color = Color.White)}Row(modifier = Modifier.padding(top = 45.dp)) {Image(bitmap = getBitmap(resource = R.drawable.home),contentDescription = "1",modifier = Modifier.clickable {}.padding(end = 15.dp))Text(text = "Login", fontSize = 13.sp, color = Color.White)}Row(modifier = Modifier.padding(top =45.dp)) {Image(bitmap = getBitmap(resource = R.drawable.home),contentDescription = "1",modifier = Modifier.clickable {}.padding(end = 15.dp))Text(text = "Login", fontSize = 13.sp, color = Color.White)}Row(modifier = Modifier.padding(top = 45.dp)) {Image(bitmap = getBitmap(resource = R.drawable.home),contentDescription = "1",modifier = Modifier.clickable {}.padding(end = 15.dp))Text(text = "Login", fontSize = 13.sp, color = Color.White)}Row(modifier = Modifier.padding(top = 95.dp)) {Image(bitmap = getBitmap(resource = R.drawable.home),contentDescription = "1",modifier = Modifier.clickable {}.padding(end = 15.dp))Text(text = "Login", fontSize = 13.sp, color = Color.White)}Row(modifier = Modifier.padding(top = 45.dp)) {Image(bitmap = getBitmap(resource = R.drawable.home),contentDescription = "1",modifier = Modifier.clickable {}.padding(end = 15.dp))Text(text = "Login", fontSize = 13.sp, color = Color.White)}Row(modifier = Modifier.padding(top = 45.dp)) {Image(bitmap = getBitmap(resource = R.drawable.home),contentDescription = "1",modifier = Modifier.clickable {}.padding(end = 15.dp))Text(text = "Login", fontSize = 13.sp, color = Color.White)}}}}

效果如下


如果你觉得不够花哨,我建议搞个动画点击哪里曲线凸起来跑哪里,我就不多废话。

4、我觉得你能行

三篇文章都是关于曲线,曲线的却在很多场景中发挥这不可替代的作用,如果你的UI和交互想要别具一格,自定义是一个很重要的技能。到这里我甚至想不出有什么样的二纬UI是我们搞不定的。如果有,请你加QQ裙730772561一起讨论研究。下面图留给你作业,感受一下自己的UI水平。

四、总结

文章一直在写,希望一起进步,你的点赞评论就是我创作的动力。最近被我小兄弟强行带入联想,可能最近时间得钻研一波Android四大组件了,希望顺利入职。所以Compose得搁置一段时间才能继续。

JetPack-Compose - Flutter 动态UI?相关推荐

  1. Android原生UI开发框架 《Jetpack Compose入门到精通》最全上手指南

    前言 在去年的Google/IO大会上,亮相了一个全新的 Android 原生 UI 开发框架-Jetpack Compose, 与苹果的SwiftIUI一样,Jetpack Compose是一个声明 ...

  2. Android 开发新技术:Jetpack Compose当仁不让

    前言 Jetpack Compose是用于构建原生Android 界面的新款工具包. 平时我们开发Android界面都是靠XML画出来,但是Compose 则是用代码来写界面,和Flutter写法有点 ...

  3. 重磅首发!Jetpack Compose 完全开发手册,从入门到精通!

    前 言 Jetpack 架构组件 及 标准化开发模式 的确立,意味着 Android 开发已步入成熟阶段.现在的Android岗招人的时候也非常看重应试者对 Jetpack 架构组件的理解程度. 今天 ...

  4. 随输入动态改变ui_深入详解 Jetpack Compose | 优化 UI 构建

    人们对于 UI 开发的预期已经不同往昔.现如今,为了满足用户的需求,我们构建的应用必须包含完善的用户界面,其中必然包括动画 (animation) 和动效 (motion),这些诉求在 UI 工具包创 ...

  5. Jetpack Compose 深入探索系列四: Compose UI

    通过 Compose runtime 集成 UI Compose UI 是一个 Kotlin 多平台框架.它提供了通过可组合函数发出 UI 的构建块和机制.除此之外,这个库还包括 Android 和 ...

  6. 告别XML,Android新声明式UI框架《Jetpack Compose入门到精通》最全开发指南

    什么是Jetpack Compose? Jetpack Compose是Android的新声明式UI框架.长期以来, Android 开发人员习惯于使用带有状态视图的xml编写UI,这些状态视图通过逐 ...

  7. 安卓开发: Jetpack compose + kotlin 实现 俄罗斯方块游戏

    文章目录 前言 俄罗斯方块开发文档 1.摘要 2.开发工具选取 2.1.Compose 的自身优点 2.2.数据驱动界面 3.设计需求 3.1.功能需求 3.1.1.基本游戏功能 3.1.2.拓展功能 ...

  8. 探索 Jetpack Compose

    前言 在大前端概念快速发展下,出现了很多声明式 UI 写法的语言或框架,像前端的 react.iOS 的 Swift UI,还有 Google 的 Flutter,但很少会听到 Android 原生有 ...

  9. jetpack compose原理解析

    目录 jetpack compose原理解析 jetpack compse 声明式ui开发 原理分析 整体框架介绍 compose LayoutNode布局介绍 @Composeable注解实现细节 ...

  10. Jetpack Compose学习笔记

    在前不久的 Android Dev Summit '19 上,Jetpack Compose 终于发布了一个可直接获得的预览版.现在的版本还是 0.1.0-dev02,处于非常早期的版本,官方也再三强 ...

最新文章

  1. 未来安防人工智能需要攻克的几大技术方向
  2. html盒子阴影的语法,css3 盒阴影box-shadow
  3. 自定义Android标题栏TitleBar布局
  4. 24小时临时邮箱_实用网站 | 临时邮箱,悄悄来悄悄去~
  5. vue 组件基本使用
  6. 如何在Linux上运行Windows软件?
  7. 03-29 健壮性测试
  8. 2015 HIAST Collegiate Programming Contest C
  9. 201671010119 2016-2017-2《Java程序设计》第十六周学习心得
  10. >>开发工具:IDEA格式化代码无效
  11. zedgraph控件使用
  12. 纯JAVA写的socket局域网斗地主游戏
  13. java udp转发_【Java】UDP发包的简单实现
  14. 从山寨机看手机的未来
  15. 定时语音提醒软件实现
  16. 都在说CI/CD,到底什么是CI/CD
  17. CISCO 关闭4786端口解决方法 cisco IOS and IOS XE software Smart Install protocol Misuse
  18. 微信自动跳转领支付宝红包JS实践
  19. 怎么提高程序的可修改性
  20. 郑州轻工业大学OJ python1102: 火车票退票费计算(函数专题)

热门文章

  1. Mstar的Monitor方案笔记(七)——EDID基本数据结构
  2. Mstar的Monitor方案OSD 菜单制作(三)——添加字符串文字
  3. 研究生期间论文发表经验总结
  4. [BZOJ4152][AMPPZ2014]The Captain题解
  5. 64位先行进位加法器的原理
  6. Talk预告 | 中国科学技术大学和微软亚洲研究院联合培养博士生冷燚冲:语音识别的快速纠错模型FastCorrect
  7. Angular学习总结-入门篇
  8. angular8封装http服务
  9. Python注释之TODO注释
  10. 【Python】turtle安装报错ERROR: Command errored out with exit status 1