【译】Android LayerDrawable 和 Drawable.Callback

Android LayerDrawable 和 Drawable.Callback

LayerDrawable是一个特殊的Drawable,它内部保持着一个Drawable数组,其中每一个Drawable都是视图中的一层。如果你不了解LayerDrawable的机制,当程序出了问题后是很难去找到bug在哪里的。我发这些文章就是为了分享在使用LayerDrawableDrawable.Callback时可能出现的一个bug。

Callback调用链

LayerDrawable中,每层视图(Drawable)都会将LayerDrawable注册为它的Drawable.Callback。这允许Drawable能够在需要重绘自己的时候告知LayerDrawable重绘它。我们可以在下面这个Callback.invalidateSelf()函数中看到是由注册callback端(在此处为LayerDrawable)来执行invalidateDrawable(Drawable drawable)的。

public void invalidateSelf() {
    /* 获取注册的Callback实例,如果无则返回null。 */
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

我们知道View是实现了Drawable.Callback接口的,所以当图片需要重绘的时候就能够告知View。如果我们把View的背景图片设置成了LayerDrawable,在Drawable需要更新的时候callback的调用将有一个传递的过程,首先会调用注册的LayerDrawableinvalidateDrawable(Drawable drawable)方法,LayerDrawable又会调用ViewinvalidateDrawable(Drawable drawable)方法。如下图所示:

Alt text

View改变背景时移除原背景Callback

ViewsetBackgroundDrawable(Drawable background)中有这么一段代码:

    if (mBackground != null) {
        mBackground.setCallback(null);
        unscheduleDrawable(mBackground);
    }

    …
    if (background != null) {
        background.setCallback(this);
    }

我们可以看出:当View改变背景时将会无条件将原背景(如果原背景是Drawable的话)的Drawable.Callback设置为null

什么情况下会出现Bug?

有了上面这些知识,我们可以通过下面这个步骤产生一个Bug:

  1. DrawableA 设置成ViewV的背景。现在A的callback指向V
  2. 将A设置成LayerDrawable L中的一层。现在A的callback指向L
  3. 现在为V设置另一个背景,V会把原背景(A)的callback强制设置成null,破坏了A与L之间的联系。
  4. BUG出现了:更新DrawableA不会让L更新了。

解决方法就是在更新V的背景之后再创造LayerDrawableL。Bug发生与解决的例子可以在这里下载。

为了方便看官,我也贴了一部分关键代码到这边来,你可以通过注释理解这段代码。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Button btn1 = (Button) findViewById(R.id.button1);
    btn1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // 1. 将 launcherIconDrawable.callback 赋值给 actionBar
            actionBar.setBackgroundDrawable(launcherIconDrawable);
            animateActionBarWorking();
        }
    });
    Button btn2 = (Button) findViewById(R.id.button2);
    btn2.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // 1. 将 launcherIconDrawable.callback 赋值给 actionBar
            actionBar.setBackgroundDrawable(launcherIconDrawable);
            animateActionBarNotWorking();
        }
    });
    actionBar = getSupportActionBar();
    launcherIconDrawable = getResources().getDrawable(R.drawable.launcher_repeat);
    colorLayer = new ColorDrawable(Color.rgb(0, 255, 0));
    actionBar.setBackgroundDrawable(colorLayer);
}

/* 这个函数运行后ActionBar不会得到更新。 */
private void animateActionBarNotWorking() {
    Drawable[] layers = new Drawable[] { colorLayer, launcherIconDrawable };
    LayerDrawable layerDrawable = new LayerDrawable(layers);
    actionBar.setBackgroundDrawable(layerDrawable);
    ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 255);
    valueAnimator.setDuration(1000);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            // 4. Updates launcherIconDrawable will not trigger action bar background to update
            // as launcherIconDrawable.callback is null
            launcherIconDrawable.setAlpha((Integer) animation.getAnimatedValue());
        }
    });
    valueAnimator.start();
}

/* 由于先移除了launcherIconDrawable与ActionBar的联系,这个函数运行后会让ActionBar得到更新。
private void animateActionBarWorking() {
    actionBar.setBackgroundDrawable(null);
    animateActionBarNotWorking();
}

参考文章:

LayerDrawable - Android Developers

package com.example.bulbpage import android.annotation.SuppressLint import android.content.res.Resources import android.graphics.Outline import android.graphics.Path import android.graphics.drawable.ClipDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.LayerDrawable import android.os.Bundle import android.view.GestureDetector import android.view.Gravity import android.view.MotionEvent import android.view.View import android.view.ViewOutlineProvider import android.widget.FrameLayout import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import com.example.bulbpage.databinding.ActivityMainBinding import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.button.MaterialButton class MainActivity : AppCompatActivity() { private var _binding: ActivityMainBinding? = null private val binding get() = _binding!! // ✅ 使用 by lazy 延迟初始化 private val colorOverlay: View by lazy { binding.colorOverlay } private lateinit var layerDrawable: LayerDrawable private lateinit var colorDrawable: ColorDrawable private var currentColorResId = R.color.white @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) _binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) val llBottomSheet = binding.llBottomSheet val pullImageView = binding.pull colorOverlay.setBackgroundColor(ContextCompat.getColor(this, currentColorResId)) pullImageView.clipToOutline = true pullImageView.outlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { val cornerRadius = 80f val path = Path() val width = view.width.toFloat() val height = view.height.toFloat() path.moveTo(0f, height) path.lineTo(0f, cornerRadius) path.quadTo(width / 2, 0f, width, cornerRadius) path.lineTo(width, height) path.close() outline.setConvexPath(path) } } //灯泡亮度操作 val lampImageView = binding.lampImageView var startY = 0f var totalDelta = 0f var isDragging = false // 是否已进入拖动状态 val maxDelta = 150 * resources.displayMetrics.density // 灯泡图高度 val gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() { override fun onDown(e: MotionEvent): Boolean { // 只有起点在 lampImageView 内部才开始检测滑动 if (e.x >= 0 && e.x <= lampImageView.width && e.y >= 0 && e.y <= lampImageView.height) { startY = e.y isDragging = true return true } isDragging = false return false } override fun onScroll( e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { if (!isDragging) return false totalDelta += distanceY totalDelta = totalDelta.coerceIn(0f, maxDelta) val percent = (totalDelta / maxDelta) * 100 val level = (percent * 100).toInt() (binding.lampImageView.drawable as? ClipDrawable)?.level = level binding.percentText.text = "${percent.toInt()}%" // 设置遮罩层透明度:0% -> 50%, 100% -> 100% val alpha =0.4f * (1 - percent / 100f) binding.coverView.alpha = alpha return true } }) // 为 lampImageView 设置触摸监听器(处理 ACTION_UP) lampImageView.setOnTouchListener { _, event -> when (event.action) { MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { isDragging = false } } gestureDetector.onTouchEvent(event) } // 按钮组点击绑定 val buttons = listOf( binding.buttonAuto, binding.buttonWhite, binding.buttonOrange, binding.buttonDeepblue, binding.buttonSkyblue, binding.buttonFlamered ) buttons.forEach { button -> button.setOnClickListener { onButtonClicked(button) } } val behavior = BottomSheetBehavior.from(llBottomSheet) updatePullSelectorState(behavior.state) val gradientDrawable = ContextCompat.getDrawable(this, R.drawable.radial_gradient) ?: throw Resources.NotFoundException("radial_gradient.xml not found") colorDrawable = ColorDrawable(ContextCompat.getColor(this, currentColorResId)) val layers = arrayOf(gradientDrawable, colorDrawable) layerDrawable = LayerDrawable(layers) for (i in layers.indices) { layerDrawable.setLayerWidth(i, LayerDrawable.LayoutParams.MATCH_PARENT) layerDrawable.setLayerHeight(i, LayerDrawable.LayoutParams.MATCH_PARENT) layerDrawable.setLayerGravity(i, Gravity.FILL) } binding.colorOverlay.background = layerDrawable } //按钮组点击事件 // 声明 lastSelectedIcon lastSelectedRing 用于记录上一个选中的图标圆环 private var lastSelectedIcon: ImageView? = null private var lastSelectedRing: View? = null private fun onButtonClicked(button: MaterialButton) { when (button.id) { R.id.button_auto -> { currentColorResId = R.color.white clearSelection() } else -> { val container = button.parent as? FrameLayout ?: return val icon = when (button.id) { R.id.button_white -> binding.editiconWhite R.id.button_orange -> binding.editiconOrange R.id.button_deepblue -> binding.editiconDeepblue R.id.button_skyblue -> binding.editiconSkyblue R.id.button_flamered -> binding.editiconFlamered else -> null } val ring = when (button.id) { R.id.button_white -> binding.ringWhite R.id.button_orange -> binding.ringOrange R.id.button_deepblue -> binding.ringDeepblue R.id.button_skyblue -> binding.ringSkyblue R.id.button_flamered -> binding.ringFlamered else -> null } lastSelectedIcon?.visibility = View.GONE lastSelectedRing?.visibility = View.GONE icon?.visibility = View.VISIBLE ring?.visibility = View.VISIBLE lastSelectedIcon = icon lastSelectedRing = ring when (button.id) { R.id.button_white -> currentColorResId = R.color.white R.id.button_orange -> currentColorResId = R.color.orange R.id.button_deepblue -> currentColorResId = R.color.deepblue R.id.button_skyblue -> currentColorResId = R.color.skyblue R.id.button_flamered -> currentColorResId = R.color.flamered } } } // 更新颜色层 val newColor = ContextCompat.getColor(this, currentColorResId) colorDrawable.color = newColor binding.colorOverlay.background = layerDrawable } private fun clearSelection() { lastSelectedIcon?.visibility = View.GONE lastSelectedRing?.visibility = View.GONE lastSelectedIcon = null lastSelectedRing = null } private fun updatePullSelectorState(state: Int) { when (state) { BottomSheetBehavior.STATE_EXPANDED -> { binding.pull.isSelected = true } else -> { binding.pull.isSelected = false } } } override fun onDestroy() { super.onDestroy() _binding = null } } 标红:Unresolved reference 'LayoutParams'.
最新发布
08-21
你遇到的错误: ``` Unresolved reference 'LayoutParams' ``` 是因为你在使用: ```kotlin LayerDrawable.LayoutParams.MATCH_PARENT ``` 但 **`LayerDrawable` 并没有 `LayoutParams` 这个内部类**,这是你从 `ViewGroup` 或 `View` 中误用过来的。 --- ### ✅ 错误代码段 ```kotlin layerDrawable.setLayerWidth(i, LayerDrawable.LayoutParams.MATCH_PARENT) layerDrawable.setLayerHeight(i, LayerDrawable.LayoutParams.MATCH_PARENT) ``` 这段代码中,`LayerDrawable.LayoutParams` 并不存在,所以编器报错:`Unresolved reference: LayoutParams`。 --- ### ✅ 正确做法 `LayerDrawable.setLayerWidth()` `setLayerHeight()` 接受的是 **具体的像素值(int)**,而不是 `MATCH_PARENT` 这样的常量。 你可以使用 `-1` 来表示“匹配父布局”。 --- ### ✅ 修改后代码如下: 将原来的: ```kotlin for (i in layers.indices) { layerDrawable.setLayerWidth(i, LayerDrawable.LayoutParams.MATCH_PARENT) layerDrawable.setLayerHeight(i, LayerDrawable.LayoutParams.MATCH_PARENT) layerDrawable.setLayerGravity(i, Gravity.FILL) } ``` 修改为: ```kotlin for (i in layers.indices) { layerDrawable.setLayerWidth(i, -1) // -1 表示 MATCH_PARENT layerDrawable.setLayerHeight(i, -1) layerDrawable.setLayerGravity(i, Gravity.FILL) } ``` --- ### ✅ 补充说明 | 方法 | 参数说明 | 示例值 | |------|----------|--------| | `setLayerWidth(i, value)` | 设置第 i 个图层的宽度 | `-1` 表示 MATCH_PARENT | | `setLayerHeight(i, value)` | 设置第 i 个图层的高度 | `-1` 表示 MATCH_PARENT | --- ### ✅ 总结 | 问题 | 原因 | 解决方案 | |------|------|----------| | `Unresolved reference 'LayoutParams'` | 错误使用 `LayerDrawable.LayoutParams` | 使用 `-1` 表示 MATCH_PARENT | | `LayerDrawable` 图层大小设置 | 不能使用 `MATCH_PARENT` 常量 | 使用像素值或 `-1` | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值