目录

  • 1 前言
  • 2 正文
    • 2.1 方案选择
    • 2.2 给布局增加前景边框
      • 2.2.1 根据绘制顺序产生前景效果
      • 2.2.2 自定义 Drawable 绘制边框
    • 2.3 让边框炫彩
    • 2.4 让边框流动起来
  • 3 最后

1 前言

本文打算一步一步地实现流动的炫彩边框,用来装饰一个布局,如广告布局,图片,使它们可以看起来更加地醒目,更加地吸引用户。

流动的炫彩边框就是这样的效果:

2 正文

2.1 方案选择

先不考虑具体的实现细节,从大的方面来说,可以选择的方案有:

  • 使用自定义 View 来实现;
  • 使用自定义 Drawable 来实现。

流动的炫彩边框仅仅是为了装饰一个布局,它自身并不需要处理触摸反馈事件,所以,这种情况下,使用自定义 Drawable 来实现是比较合适的。

如果使用自定义 View 来实现,就需要把自定义的边框 View 和原有的待装饰的布局叠加在一起显示,这会使得布局变得复杂一些;但是,自定义 View 的实现方式,如果使用到 SurfaceView 这种双缓冲技术,会比自定义 Drawable 有一定的性能优势。

本文采用自定义 Drawable 的方式来实现。

2.2 给布局增加前景边框

2.2.1 根据绘制顺序产生前景效果

这里我们以一个图片的布局来举例子,也就说,我们要做的是给图片控件增加前景边框。

自然地,会想到有没有官方支持的 setForeground() 这样的 API 呢?这种想法是合理的,如果官方有相应支持的 API,就应该去使用官方提供的 API;如果没有的话,自己再去想办法。

但是,官方确实没有提供这样的 API。只好自己去想办法了。

我们打算创建一个继承于 ImageView 的子类,重写它的 onDraw() 方法,在这个方法里面的 super.onDraw(canvas) 之后添加绘制边框的代码,这样不就是前景边框的效果了。

class MyImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {override fun onDraw(canvas: Canvas) {super.onDraw(canvas)// 在这里添加绘制边框的代码,就是前景边框效果了。}
}

activity_main.xml 中使用自定义的 MyImageView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"android:layout_height="match_parent"android:padding="16dp"tools:context=".MainActivity"><com.example.fluidcolorfulframe.MyImageViewandroid:id="@+id/iv"app:srcCompat="@drawable/road"android:scaleType="fitXY"app:layout_constraintDimensionRatio="h,16:9"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintEnd_toEndOf="parent"android:layout_width="0dp"android:layout_height="0dp" /></androidx.constraintlayout.widget.ConstraintLayout>

好了,我们已经知道在哪里添加前景边框了。

运行程序,只可以看到一张图片显示:

2.2.2 自定义 Drawable 绘制边框

下面就要写绘制前景边框的代码了,前面已经说过要使用自定义 Drawable 的方式来做。

创建继承于 Drawable 的子类 FluidColorfulFrameDrawable

class FluidColorfulFrameDrawable: Drawable() {override fun draw(canvas: Canvas) {TODO("Not yet implemented")}override fun setAlpha(alpha: Int) {TODO("Not yet implemented")}override fun setColorFilter(colorFilter: ColorFilter?) {TODO("Not yet implemented")}override fun getOpacity(): Int {TODO("Not yet implemented")}
}

居然有 4 个方法还未实现。别担心,我们现在就去实现它们吧。

class FluidColorfulFrameDrawable: Drawable() {private val paint = Paint(Paint.ANTI_ALIAS_FLAG)override fun draw(canvas: Canvas) {// 这里就是绘制边框的地方}override fun setAlpha(alpha: Int) {paint.alpha = alpha}override fun setColorFilter(colorFilter: ColorFilter?) {paint.colorFilter = colorFilter}override fun getOpacity(): Int {return PixelFormat.TRANSLUCENT}
}

对于自定义 Drawable 来说,还需要重写一个 setBounds 方法,用来决定绘制的范围,也就是说当它的 draw() 方法被调用时,Drawable 要绘制在哪里。

class FluidColorfulFrameDrawable : Drawable() {private val paint = Paint(Paint.ANTI_ALIAS_FLAG)private lateinit var bounds: RectFprivate val rectF = RectF()// 10.dp 是代码里面对 Int 类型定义的扩展属性,把 dp 值转为 px。private val defaultRadius: Float = 10.dpprivate val defaultStrokeWidth: Float = 5.dpinit {// 配置画笔paint.color = Color.REDpaint.style = Paint.Style.STROKEpaint.strokeWidth = defaultStrokeWidth}override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {super.setBounds(left, top, right, bottom)// 记录 Drawable 的绘制范围在 bounds 对象里面bounds = RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())}override fun draw(canvas: Canvas) {// 绘制带圆角的矩形边框canvas.drawRoundRect(bounds, defaultRadius, defaultRadius, paint)}...
}

现在直接去运行,是看不到效果的。因为我们还没有去使用自定义的 Drawable。

MyImageView 里面使用自定义 Drawable:

class MyImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {// 创建自定义 Drawable 对象private val drawable = FluidColorfulFrameDrawable()override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)// 设置 Drawable 的范围drawable.setBounds(0, 0, w, h)}override fun onDraw(canvas: Canvas) {super.onDraw(canvas)// 绘制 Drawabledrawable.draw(canvas)}
}

运行程序,查看效果如下:

可以看到,边框是作为前景显示的,这点是没有问题的。

但是,有两个不对的地方:图片的每个角都在圆角边框之外了;边框的线宽显示偏细。

图片的每个角都在圆角边框之外了:这是因为图片的显示区域和边框的边界是一样大的,而边框是有圆角的,这样图片的四个角就一定是在边框之外了。这个问题可以通过给图片添加 padding 来解决。代码如下:

class MyImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {...init {// 这里的 5dp 是和边框的线宽保持一致的。setPadding(5.dp.toInt())}...
}

运行效果如下:

边框的线宽显示偏细:这是因为绘制边框时使用的矩形区域是 MyImageView 传入的边界矩形,而画笔是有 5dp 的宽度的。这个问题可以通过创建新的矩形对象,给这个矩形对象设置抵消掉画笔宽度的左上右下值,并使用新的矩形对象来绘制。代码如下:

class FluidColorfulFrameDrawable : Drawable() {private val paint = Paint(Paint.ANTI_ALIAS_FLAG)private lateinit var bounds: RectFprivate val rectF = RectF()private val defaultRadius: Float = 10.dpprivate val defaultStrokeWidth: Float = 5.dp...override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {super.setBounds(left, top, right, bottom)bounds = RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())rectF.left = defaultStrokeWidth / 2rectF.top = defaultStrokeWidth / 2rectF.right = bounds.width() - defaultStrokeWidth / 2rectF.bottom = bounds.height() - defaultStrokeWidth / 2}override fun draw(canvas: Canvas) {canvas.drawRoundRect(rectF, defaultRadius, defaultRadius, paint)}...
}

运行效果如下:

2.3 让边框炫彩

由一种颜色组成的边框,看着实在单调。我们希望边框可以由多种颜色组成,看着流光溢彩一样地。

这可以通过给画笔设置一个 SweepGradient 类型对象的着色器来实现。

class FluidColorfulFrameDrawable : Drawable() {...private val colors: IntArrayprivate val positions: FloatArrayinit {paint.style = Paint.Style.STROKEpaint.strokeWidth = defaultStrokeWidthcolors = intArrayOf("#FF0000FF".toColorInt(), // 蓝 0f"#FF000000".toColorInt(), // 黑 0.02f"#FF000000".toColorInt(), // 黑 0.25f"#FFFF0000".toColorInt(), // 红 0.27f"#FFFF0000".toColorInt(), // 红 0.37f"#FF00FF00".toColorInt(), // 绿 0.39f"#FF0000FF".toColorInt(), // 蓝 0.49f"#FFFFFF00".toColorInt(), // 黄 0.51f"#FF000000".toColorInt(), // 黑 0.53f"#FF000000".toColorInt(), // 黑 0.75f"#FFFF0000".toColorInt(), // 红 0.77f"#FFFF0000".toColorInt(), // 红 0.87f"#FFFFFF00".toColorInt(), // 黄 0.91f"#FF0000FF".toColorInt(), // 蓝 0.96f)positions = floatArrayOf(0f,0.02f,0.25f,0.27f,0.37f,0.39f,0.49f,0.51f,0.53f,0.75f,0.77f,0.87f,0.91f,0.96f,)}override fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {super.setBounds(left, top, right, bottom)...paint.shader = SweepGradient(bounds.centerX(), bounds.centerY(), colors, positions)}...
}

这里使用的是 SweepGradient 可以配置多个颜色,多个位置的构造方法。

public SweepGradient(float cx, float cy, @NonNull @ColorInt int[] colors,@Nullable float[] positions)

需要说明的是:

colors 参数是不可以为 null 的,并且至少要包括两个颜色值。

positons 参数是可以为 null 的:

  • 如果 positionsnull,那么 colors 中的颜色值会自动均匀分布开来。

  • 如果 positions 不为 null,那么它的长度必须与 colors 的长度保持一致;而且,它的元素值需要是依次递增的,范围在 0f 到 1f 之间。另外,官方文档里面说:

    The relative position of each corresponding color in the colors array, beginning with 0 and ending with 1.0.

    positions 数组里的元素以 0 开始,以 1.0 结束。

    官方文档的说法是不对的。实际上,positions 数组的元素并非要以 0 开始,以 1.0 结束。

运行程序,查看效果:

2.4 让边框流动起来

让边框流动起来,就是让边框旋转起来。这里要使用到属性动画和 Shader 的本地矩阵方法来处理。

class FluidColorfulFrameDrawable : Drawable() {...private val mtx = Matrix()private var degree: Float = 0fset(value) {field = value// 刷新自己invalidateSelf() }...override fun draw(canvas: Canvas) {// 设置本地矩阵mtx.reset()mtx.setRotate(degree, bounds.centerX(), bounds.centerY())(paint.shader as SweepGradient).setLocalMatrix(mtx)canvas.drawRoundRect(rectF, defaultRadius, defaultRadius, paint)}...private var fluidAnim: ObjectAnimator? = null// 开始流动fun startFluid() {fluidAnim = ObjectAnimator.ofFloat(this, "degree", 0f, 360f).apply {duration = 2000Linterpolator = LinearInterpolator()repeatCount = ValueAnimator.INFINITEstart()}}// 取消流动fun cancelFluid() {fluidAnim?.cancel()}
}

MyImageView 里面调用开始流动和取消流动的方法:

class MyImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {private val drawable = FluidColorfulFrameDrawable()...override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {super.onSizeChanged(w, h, oldw, oldh)drawable.setBounds(0, 0, w, h)drawable.startFluid()}...override fun onDetachedFromWindow() {super.onDetachedFromWindow()drawable.cancelFluid()}
}

运行程序,查看流动效果:

居然没有流动效果!

需要具体分析一下,这里在 degree 的 setter 方法里面和 draw 方法里面增加日志打印:

private var degree: Float = 0fset(value) {field = valueLog.d(TAG, "degree setter called")invalidateSelf()}
override fun draw(canvas: Canvas) {Log.d(TAG, "draw: ")mtx.reset()mtx.setRotate(degree, bounds.centerX(), bounds.centerY())(paint.shader as SweepGradient).setLocalMatrix(mtx)canvas.drawRoundRect(rectF, defaultRadius, defaultRadius, paint)
}

运行程序后,可以看到下面这行日志在不停地打印:

D/FluidColorfulFrame: draw:
D/FluidColorfulFrame: degree setter called
D/FluidColorfulFrame: degree setter called
D/FluidColorfulFrame: degree setter called
... // 后面全是重复 degree setter called 的日志

这就说明 invalidateSelf() 方法并没有触发 draw 方法的调用了。现在去看一下 Drawable 类的 invalidateSelf 方法的源码:

public void invalidateSelf() {final Callback callback = getCallback();if (callback != null) {callback.invalidateDrawable(this);}
}

内部是通过 getCallback() 方法获取一个 Callback 对象;如果 Callback 对象不为 null,则调用其 invalidateDrawable 方法并且把 Drawable 对象传入这个方法。

继续查看 getCallback() 方法以及相关的字段和方法:

private WeakReference<Callback> mCallback = null;
public final void setCallback(@Nullable Callback cb) {mCallback = cb != null ? new WeakReference<>(cb) : null;
}
public Callback getCallback() {return mCallback != null ? mCallback.get() : null;
}

我们并没有调用自定义 Drawable 对象的 setCallback 方法,所以 getCallback() 方法的返回值是 null,在 invalidateSelf 方法里面就不会回调 invalidateDrawable 方法了。

MyImageView 中设置 Drawable 对象的 setCallback 方法并重写 invalidateDrawable 方法:

class MyImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {private val drawable = FluidColorfulFrameDrawable()init {setPadding(5.dp.toInt())// 设置 callbackdrawable.callback = this}...// 重写 invalidateDrawableoverride fun invalidateDrawable(dr: Drawable) {super.invalidateDrawable(dr)// 如果回调的 dr 就是 drawable,就调用重绘方法。if (dr === drawable) {invalidate()}}
}

重新运行程序,查看效果:

3 最后

本文一步一步地展示了流动炫彩边框的实现过程,用到了自定义 Drawable,画笔的着色器(扫描渐变,本地矩阵),属性动画,Drawable 与 View 的刷新回调等知识。

本文并没有演示如何给一个 ViewGroup 类型的控件添加边框,但是相信这个是难不倒大家了。

代码已经上传到 Github。希望可以帮助到大家,也欢迎大家点赞分享。

Android实战——一步一步实现流动的炫彩边框相关推荐

  1. 一步一步教你在 Android 里创建自己的账号系统(一)

    大家假设喜欢我的博客,请关注一下我的微博,请点击这里(http://weibo.com/kifile),谢谢 转载请标明出处(http://blog.csdn.net/kifile),再次感谢 大家在 ...

  2. Android华容道之一步一步实现-4-图像块移动算法

    下一个关键点就是图像块的移动,以如图为例. 假设空格处于第二行第三格,那么此时只有触摸第二行以及第三列的图像块的时候才需要移动图像块,因为别的图像块不能移动. 当触摸发生在合法的图像块的时候,即上面图 ...

  3. Android华容道之一步一步实现-3 -手指触摸处理

    华容道关键点之一出现了,就是处理触摸,包括手指按下,抬起,移动等. 自己实现一个处理触摸的类,然后处理 onTouchEvent(MotionEvent event) 事件,在这里处理手指按下,抬起, ...

  4. Android华容道之一步一步实现-2-图片分割

    因为华容道是16个格子,所以要把一张大一点的图片分割成16个相等的小图片. 可以使用Bitmap.createBitmap方法来进行. 直接上代码 ori_bitmap = BitmapFactory ...

  5. Android华容道之一步一步实现-序言

    女儿看了最强大脑的数字华容道节目之后,就缠着要玩数字华容道,买了实物版,玩了几天,感觉好像还没有过瘾,就让我做个手机版的数字华容道游戏. 说明一下,最终要实现的版本并不是最强大脑那种数字版的华容道,而 ...

  6. 一步一步学ROP之Android ARM 32位篇

    蒸米 · 2015/12/17 9:41 0x00 序 ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术,可以用来绕过现代操作系统的各种 ...

  7. iphone看python文件_Python实战 | 只需 ”三步“ 爬取二手iphone手机信息(转发送源码)...

    原标题:Python实战 | 只需 "三步" 爬取二手iphone手机信息(转发送源码) 本次实战是爬取二手苹果手机的信息,共爬取了300部手机信息,效果如下: 开发环境 环境:M ...

  8. Android一步一步实现一款实用的Android广告栏

    源码:BannerLayoutDemo 有图有真相: bannerLayoutDemo 开源界有一句很有名的话叫"不要重复发明轮子",当然,我今天的观点不是要反驳这句话,轮子理论给 ...

  9. 用Linux开发板制作智能音箱,【工程师实战】只要几步,普通音箱秒变小度智能音箱...

    原标题:[工程师实战]只要几步,普通音箱秒变小度智能音箱 电子大改造来了,这次把家里的闲置音箱改造下,来个智能语音识别 这是普通的小音箱 我们需要在树莓派3B上搭载DuerOS的人工智能操作系统-度秘 ...

  10. 【Python实战】用Scrapyd把Scrapy爬虫一步一步部署到腾讯云上,有彩蛋

    接着之前的几篇文章说. 我把爬虫已经写好了,而且在本地可以运行了. 这个不是最终的目的啊. 我们是要在服务器上运行爬虫. 利用周末,同时腾讯送的7天云服务器体验也快到期了 就在这里再来一篇手把手的将爬 ...

最新文章

  1. P2055 [ZJOI2009]假期的宿舍
  2. 直播回顾|结构光编码与三维重建技术
  3. 使用npm install XXX 时出现“failed to fetch from regi...
  4. 构建根文件系统之启动第1个程序init
  5. 动态添加组件_使用vue.js的动态组件模板
  6. 什么从什么写短句_新年新气象跨年了,准备好发什么说说了吗
  7. 计算机考研408试题及答案,2015年计算机专业408考研试题及答案
  8. kotlin 泛型约束
  9. atom 基础使用教程
  10. 【大数据部落】R语言GAM(广义相加模型)对物业耗电量进行预测
  11. 机顶盒装linux教程,一种Linux机顶盒焦点控制方法与流程
  12. python字典包含指定键_筛选python字典中键包含特定字符串的项
  13. 巨斧砍大树-sdut
  14. 2022山东国际养老服务业博览会,智慧养老产业展会
  15. TCP聊天文件服务器v2.0 - 重大bug修复+PyQt5文件传输可视化
  16. android 程序root权限管理,手机root之后进行软件程序授权管理 手机root权限管理方法...
  17. 市场研究中的数据分析知识整理 (七)-结构方程模型
  18. 简述相关与回归分析的关系_相关分析与回归分析的联系与区别
  19. matlab kdj线,kdj指标详什么时候买入,kdj金叉和死叉图解
  20. 小程序社交电商案例分享

热门文章

  1. 光流法的视频移动目标检测(matlab代码)
  2. 可验证随机函数(Verifiable Random Function, VRF)
  3. 3399 android root,RK3399 android8.1 app获取root权限
  4. 今天,你脸上还长痤疮吗?
  5. 官宣 | iPayLinks与Shopyy达成战略合作
  6. IT项目经理岗位职责
  7. 关于灰排线两端压接类型简介
  8. ARM嵌入式——堆栈寻址
  9. 2021年田野的风响彻了整个冰岛
  10. 来诈金花嘛?Python实现的那种