android多主题之坑

声明:本文已授权微信公众号Android程序员 (Android Trending) 在微信公众号平台原创首发。

年后重构了一版多主题框架,在重构过程中遇到了不少的坑,特此记录下与君共勉。(Tips: 多主题框架也将于6月初开源啦.)

  • 多彩主题和夜间主题
    在写多主题框架时,首先一个概念要分清就是多彩主题和夜间模式。

    • 多彩主题其实是白天模式的衍生,与夜间模式是对立的。
    • 虽然夜间和多彩是对立,但还是建议多彩主题应该与夜间模式解偶,因为有时夜间模式的颜色变化并不是简单的颜色取反,受产品设计的影响较大,有时甚至一个tag在夜间和多彩中的取色完全不一样的,这时如果还在强求通过一次编码“通吃“多彩和夜间,这样的做法完全是不明智的,同时也会导致框架易用性变差。
      当然如果某些控件在夜间模式下的需求只是简单的颜色取反,对于这种情况,框架是应当给予适配支持的(不能一棒子打死嘛),因为这种特性支持很简单,所以可以在基本不增加框架学习使用成本的前提下,大大减少程序员的重复编码,提高了开发效率。
    • 关于夜间模式的具体实现方式有很多,在这里推荐一篇文章 Android夜间模式最佳实践,文中一共概述了三种实现方式,其中第三种通过修改uiMode来切换夜间模式 其实就是Google在support库23.2.0版本(新增支持夜间模式,其实早就支持了0,0)中采用的方式,只不过在AppcompatDeleglate中进行了封装,使用起来更加简单了。
  • 关于ColorDrawable
    API21以下是不支持染色的,所以从兼容性上考虑,一般地对ColorDrawable直接new而不是染色。
    源码如下(API19):

/**
* Setting a color filter on a ColorDrawable has no effect.
*
* @param colorFilter Ignore.
*/
public void setColorFilter(ColorFilter colorFilter) {
}


- 关于GradientDrawable
 比较特殊,API22以下是不支持直接tint的,这点在support库中有很清楚的说明(DrawableCompatLollipop.java$setTintList):
 ```java
 public static void setTintList(Drawable drawable, ColorStateList tint) {
      if (drawable instanceof DrawableWrapperLollipop) {
          // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible
          // functionality instead
          DrawableCompatBase.setTintList(drawable, tint);
      } else {
          // Else, we'll use the framework API
          drawable.setTintList(tint);
      }
  }

另外值得注意的是,GradientDrawable不支持tint的原因有两点,1).在API21以下它并没有实现onStateChange方法,而onStateChange在view中的默认实现是直接返回false,所以它就不会随着状态的变化刷新UI了。2). 在API21的GradientDrawable源码中并没有支持setTint,这有点奇怪,因为其他Drawable基本都支持了,有时间要仔细对比下源码。

    protected boolean onStateChange(int[] state) { 
        return false; 
    }

  • 关于setPressed(boolean)
    setPressed 方法不同于setSelected方法,虽然它在执行过程中会更新Drawable的state状态,但是不会调用invalidate函数(备注:并不是针对所有Drawable,stateDrawableList会在setPressed执行过程中调用invalidate())。
    附上API23部分源码:

    //View.java
    public void setPressed(boolean pressed) {
        ... ...
        if (needsRefresh) {
            refreshDrawableState();
        }
         ... ...
     }
    protected void drawableStateChanged() {
        ... ...
        final Drawable bg = mBackground;
        if (bg != null && bg.isStateful()) {
            bg.setState(state);
        }
        ... ...
    }
    
    //BitmapDrawable
    @Override
    protected boolean onStateChange(int[] stateSet) {
        ... ...
            mTintFilter = updateTintFilter(mTintFilter, state.mTint, state.mTintMode);
        ... ...
    }
    
    //Drawable
    /**
     * Ensures the tint filter is consistent with the current tint color and
     * mode.
     */
    PorterDuffColorFilter updateTintFilter(PorterDuffColorFilter tintFilter, ColorStateList tint,
            PorterDuff.Mode tintMode) {
            ... ...
        if (tintFilter == null) {
            return new PorterDuffColorFilter(color, tintMode);
        }
        tintFilter.setColor(color);
        tintFilter.setMode(tintMode);
        return tintFilter;
    }
    

    特别地,当一个textview设置了一张png为background并对该background设置了normaltint和pressed tint,然后你会发现background在按下时背景色并没有tint。
    解决的方法:1). 在view的drawstateChanged()中手动调用invalidate方法。2). 在view的drawstateChanged()中apply新的drawable state。3). 等待你来补充。

  • 关于.9png
    .9png在绘制时如果.9png内含有padding值,则5.0以下时view的padding会消失。如果想要view的padding保留,目前比较好的做法就是在set前先将view的padding值保存下来,然后等set之后再重新setPadding回去(首先要明确的一点是drawable和view的padding是有区别的)。

  • 关于StateListDrawable对child tint 无效
    这是一个5.0以下的bug,现在比较好的解决方案就是继承StateListDrawable,重写它的selectDrawable方法,每次在状态切换获取对应的drawable时,手动进行setColorFilter设置。附上链接

  • 关于setButtonDrawable方法
    setButtonDrawable方法在API21以下存在一个 非常隐蔽的bug。
    在API21以下,如果CompoundButton已设置了一个buttonDrawable(非空),然后在调用setButtonDrawable(null),你会发现之前设置的buttonDrawable仍然存在!根本没有被置空。
    至于原因非常简单,对比一下源码就一目了然了。下面附上API23 和API19的相关源码

    //API19
    /**
       * Set the background to a given Drawable
       *
       * @param d The Drawable to use as the background
       */
      public void setButtonDrawable(Drawable d) {
          if (d != null) {
              if (mButtonDrawable != null) {
                  mButtonDrawable.setCallback(null);
                  unscheduleDrawable(mButtonDrawable);
              }
              ... ...
      }
    
    //API21
      /**
       * Sets a drawable as the compound button image.
       *
       * @param drawable the drawable to set
       * @attr ref android.R.styleable#CompoundButton_button
       */
      @Nullable
      public void setButtonDrawable(@Nullable Drawable drawable) {
          if (mButtonDrawable != drawable) {
              if (mButtonDrawable != null) {
                  mButtonDrawable.setCallback(null);
                  unscheduleDrawable(mButtonDrawable);
              }
              
              mButtonDrawable = drawable;
              
              if (drawable != null) {
                  drawable.setCallback(this);
                  ... ...
              }
          }
      }
    
  • 关于obtain属性
    好吧,这个obtain属性非常怪,有时候会出些莫名其妙的bug。

    • 在API21以下,如果在int [] ATTRS数组中将android属性放在自定义属性之后读取,则你会发现android属性的值将无法取到,-,-是不是很奇葩。
    • 在API19上,如果将drawableLeft之类的android属性放在一个int [] ATTRS中通过TypeArray读取时,除了第一个android属性能取到resourceId,之后的drawableXxx的resourceId解析的值都为0。
    • 目前的解决方案是针对每个attr都单独obtain一次,如果有更好的解决方案,欢迎支持。

拖沓了两个月终于踩着五月份的尾巴把文章发了,唏嘘...(拖延症害死人--|||)


欢迎查看 个人博客.

你可能感兴趣的:(android多主题之坑)