前言
如你所见,本专栏的名称叫做《把苹果抄的裤衩子都不剩》
。
目的就是实现applemusic中一些炫酷的效果。
这篇博文写于2021-12-17。本文发布时(2022-3-9),AppleMusic已经实现了更真实的流动效果。
正文
老模板,上代码之前,先给大家放一下我实现的效果。
由于AppleMusic在最近的版本中已经加强了效果,我就不上它的演示了。
思路
刚看到肯定是一点思路都没有的,甚至不知道这个效果应该叫什么名字(有人可能会问:不是已经知道它叫流光溢彩了吗?别着急,我们往下看)。
既然没有思路,那我们可以通过某些手段看看它是如何实现的。
身为一个合格的安卓程序员,开发和逆向必须都 了如指掌 的。
打开jadX,拖入AppleMusic的安装包,打开一看发现它的类名被混淆的亲妈都不认识了。遇到这种情况,肯定是选择 直接放弃 百度了~
于是我翻遍了github和gitee,终于发现了一个具有同样效果的音乐播放器软件<椒*音乐>
就是因为这个播放器把背景效果叫做流光溢彩,所有才有了这篇文章的命名。
经过漫长的源码定位后,发现了一些蛛丝马迹,椒*播放器中存在一个类,内部有一个Log,它的TAG设置的是"FlowingLightView",翻译过来正好是流光溢彩。版权原因,我不贴出相关的代码,我只介绍实现的方式。
实际上就是把一张图片切分成几个部分,然后对他进行降低分辨率,再提高分辨率的处理,最后让这些图片在canvas上做无规律旋转。为了避免重复感和撕裂感,还会对边缘进行混色处理,并且做一次随机的mesh。
这种写法会创建多个bitmap对象,稍稍了解过安卓开发的朋友都知道bitmap是很吃内存的,操作起来也很废性能。所以我实现的时候,选择了简化某些操作,但还让它的观感和AppleMusic相同。
实战
根据上述的分析,我们可以把过程分为以下几步:
- 分割图片
- 将图片缩小
- mesh处理,色调处理
- 将图片放大
- 高斯模糊
- 让图片旋转
我再把过程修改一下:
- 将图片缩小
- 高斯模糊
- mesh处理
- 将图片放大
- mesh处理
- 将图片放大
- 高斯模糊
- 处理色调
图片缩小是为了让后面高斯模糊影响更大的区域。
mesh和色调处理是为了去掉重复感和撕裂感。
图片放大后再次模糊是为了去除放大后颜色落差过大形成的波纹。
实现这些效果,我们可以撰写一个处理bitmap的工具类。得益于kotlin强大的 拓展函数 ,我们可以把功能直接加在Bitmap类中
fun Bitmap.zoom(newHeight: Float, newWidth: Float): Bitmap {
val matrix = Matrix()
val scaleWidth = newWidth / width
val scaleHeight = newHeight / height
matrix.postScale(scaleWidth, scaleHeight)
return Bitmap.createBitmap(
this, 0, 0, width,
height, matrix, true
)
}
fun Bitmap.blur(context: Context, radius: Float, ty: Float): Bitmap {
val bitmap = Bitmap.createScaledBitmap(
this,
(width / ty).toInt(), (height / ty).toInt(), false
) //先缩放图片,增加模糊速度
val rs = RenderScript.create(context)
val input = Allocation.createFromBitmap(
rs, bitmap, Allocation.MipmapControl.MIPMAP_NONE,
Allocation.USAGE_SCRIPT
)
val output = Allocation.createTyped(rs, input.type)
val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
script.setRadius(25F.coerceAtLeast(radius))
script.setInput(input)
script.forEach(output)
output.copyTo(bitmap)
rs.destroy()
return bitmap
}
fun Bitmap.brightness(): Float {
val bmp = zoom(3F, 3F) //转3*3大小的位图
val pixel = bmp.getPixel(1, 1) //取中间位置的像素
val r = (pixel shr 16 and 0xff) / 255.0f
val g = (pixel shr 8 and 0xff) / 255.0f
val b = (pixel and 0xff) / 255.0f
return 0.299f * r + 0.587f * g + 0.114f * b //计算灰阶
}
fun Bitmap.drawColor(color: Int): Bitmap {
val newBit = Bitmap.createBitmap(this)
val canvas = Canvas(newBit)
canvas.drawColor(color)
return newBit
}
fun Bitmap.handleImageEffect(saturation: Float): Bitmap {
val saturationMatrix = ColorMatrix()
saturationMatrix.setSaturation(saturation)
val imageMatrix = ColorMatrix()
imageMatrix.postConcat(saturationMatrix)
val paint = Paint()
paint.colorFilter = ColorMatrixColorFilter(imageMatrix)
val bitmap = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawBitmap(this, 0F, 0F, paint)
return bitmap
}
fun Bitmap.mesh(floats: FloatArray): Bitmap {
val fArr2 = FloatArray(72)
var i = 0
while (i <= 5) {
var i2 = 0
var i3 = 5
while (i2 <= i3) {
val i4 = i * 12 + i2 * 2
val i5 = i4 + 1
fArr2[i4] = floats[i4] * width.toFloat()
fArr2[i5] = floats[i5] * height.toFloat()
i2++
i3 = 5
}
i++
}
val newBit = Bitmap.createBitmap(this)
val canvas = Canvas(newBit)
canvas.drawBitmapMesh(newBit, 5, 5, fArr2, 0, null, 0, null)
return newBit
}
然后我们实现对图片的链式处理
fun processBitmap(bitmap: Bitmap):Bitmap{
//这个mesh的参数是偷的AppleMusic中的一个
//AppleMusic中有5个Mesh参数,每次随机使用一个
val floats = floatArrayOf(-0.2351f, -0.0967f, 0.2135f, -0.1414f, 0.9221f, -0.0908f, 0.9221f, -0.0685f, 1.3027f, 0.0253f, 1.2351f, 0.1786f, -0.3768f, 0.1851f, 0.2f, 0.2f, 0.6615f, 0.3146f, 0.9543f, 0.0f, 0.6969f, 0.1911f, 1.0f, 0.2f, 0.0f, 0.4f, 0.2f, 0.4f, 0.0776f, 0.2318f, 0.6f, 0.4f, 0.6615f, 0.3851f, 1.0f, 0.4f, 0.0f, 0.6f, 0.1291f, 0.6f, 0.4f, 0.6f, 0.4f, 0.4304f, 0.4264f, 0.5792f, 1.2029f, 0.8188f, -0.1192f, 1.0f, 0.6f, 0.8f, 0.4264f, 0.8104f, 0.6f, 0.8f, 0.8f, 0.8f, 1.0f, 0.8f, 0.0f, 1.0f, 0.0776f, 1.0283f, 0.4f, 1.0f, 0.6f, 1.0f, 0.8f, 1.0f, 1.1868f, 1.0283f)
val tmp = bitmap.zoom(150f, (bitmap.getHeight() * 150 / bitmap.getWidth()).toFloat())
.blur(context, 25F, 1F)
.mesh(floats)
.zoom(1000F, 1000F)
.mesh(floats)
.blur(context, 12F, 1F)
.handleImageEffect(1.8f)
val float = tmp.brightness()
return when {
float > 0.8 ->
tmp.drawColor(Color.parseColor("#50000000"))
float < 0.2 ->
tmp.drawColor(Color.parseColor("#50FFFFFF"))
else ->
tmp
}
}
其实不需要用旋转的效果,只要让这个处理后的Bitmap动起来就可以了。
所以我选择交给开源库KenBurnsView
新建一个类FlowingLightView
package simon.music.widget
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.util.AttributeSet
import com.flaviofaria.kenburnsview.KenBurnsView
import simon.tool.*
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
/**
* 如果这段代码能够正常工作,那么请记住作者是Simon。
* 如果不能正常工作,那我也不知道是谁写的。
*/
class FlowingLightView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : KenBurnsView(context, attrs, defStyle) {
init {
val randomTransitionGenerator = RandomTransitionGenerator()
randomTransitionGenerator.setTransitionDuration(3400)
setTransitionGenerator(randomTransitionGenerator)
}
fun setFlowingLight(bitmap: Bitmap) {
val floats = floatArrayOf(-0.2351f, -0.0967f, 0.2135f, -0.1414f, 0.9221f, -0.0908f, 0.9221f, -0.0685f, 1.3027f, 0.0253f, 1.2351f, 0.1786f, -0.3768f, 0.1851f, 0.2f, 0.2f, 0.6615f, 0.3146f, 0.9543f, 0.0f, 0.6969f, 0.1911f, 1.0f, 0.2f, 0.0f, 0.4f, 0.2f, 0.4f, 0.0776f, 0.2318f, 0.6f, 0.4f, 0.6615f, 0.3851f, 1.0f, 0.4f, 0.0f, 0.6f, 0.1291f, 0.6f, 0.4f, 0.6f, 0.4f, 0.4304f, 0.4264f, 0.5792f, 1.2029f, 0.8188f, -0.1192f, 1.0f, 0.6f, 0.8f, 0.4264f, 0.8104f, 0.6f, 0.8f, 0.8f, 0.8f, 1.0f, 0.8f, 0.0f, 1.0f, 0.0776f, 1.0283f, 0.4f, 1.0f, 0.6f, 1.0f, 0.8f, 1.0f, 1.1868f, 1.0283f)
var tmp = bitmap.zoom(150f, (bitmap.getHeight() * 150 / bitmap.getWidth()).toFloat())
.blur(context, 25F, 1F)
.mesh(floats)
.zoom(1000F, 1000F)
.mesh(floats)
.blur(context, 12F, 1F)
.handleImageEffect(1.8f)
val float = tmp.brightness()
when {
float > 0.8 -> {//判断图片大体颜色是深色还是浅色
tmp = tmp.drawColor(Color.parseColor("#50000000"))
setImageBitmap(tmp)//浅色就加入黑色遮罩
}
float < 0.2 -> {
tmp = tmp.drawColor(Color.parseColor("#50FFFFFF"))
setImageBitmap(tmp)//深色就加入白色遮罩
}
else -> {
setImageBitmap(tmp)
}
}
}
}
这样,通过最简单的方式,实现了和AppleMusic可以媲美的效果。
而且内存占用远比AppleMusic低!
后话
已经在AppleMusic中找到了对应的实现,发现椒*音乐的实现有些相似啊,居然函数名字/变量作用 都一样。咋回事也不用我多说了吧。
补
22年-6月-22日,椒*音乐开发者主动联系我,证实了我的猜想。他说他已经改用了新的方式。嗯,就这样。