前言
本篇文章,代码部分均为Kotlin 如你所见,本专栏的名称叫做《把苹果抄的裤衩子都不剩》 。 目的就是实现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)
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{
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
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实现的控件。 经过对比,它和椒*的代码有90%的相似度!就是说,椒*通过逆向的手段直接盗版了AppleMusic的源码,而且没有进行标注! 逆向分析 对家 的实现是很正常的手段,我能理解。但是身为一个合格的开发者,就算知道了实现的流程,我们也应该自己做一个不一样的,我在此呼吁大家抵制这种行为。 其实分析椒*播放器的源码还是很有意思的。 它的播放器中,这个背景控件是addView实现的,所以在xml中根本定位不到。我的jadX开启了根据开源库包名剔除无关代码的功能,所以默认剔除了目录在androidx.*的代码。 初期逆向椒*播放器时找不到代码就是因为它把控件混淆到了androidx.*下,也算是一直巧妙的保护措施吧,值得借鉴。
|