Android Shape 的工具 Duck

photo

由于自己在开发中遇到的问题,所有弄了一个 Android Shape 的工具 Duck ,能帮助开发者直接在 xml 的任意控件上实现 Shape 效果,无需创建额外的xml文件,并且没有任何侵入性。

废话不多说,直接看效果:

duck

]

项目地址:Duck


初衷

这个库的由来,是因为公司一个维护了 4 年的项目。

经历 4 年的项目,产品设计不知道改了多少版,期间产生并堆砌大量shape.xml 文件,这些文件因为索引的问题往往还无法清理。

同时,同一个 shape.xml 文件,因为设计存在不规范的问题,在不同页面改动了一点颜色、倒角或线宽等,就无法复用,必须据此创建新的文件。

最后,大量的文件堆积,开发人员开发时,花时间找 shape.xml ,还不如自己创建新的方便,这样恶性循环, 只能 GG。

我想说,Android 设计 Shape 的初衷是好的: 一个 APP,统一的设计规范,就应该复用 Shape

但这种情况对于国内的生态来说并不适用。

首先,相同屏幕尺寸,中文承载信息的能力远大于英文,这就导致国外大部分 APP 界面设计简洁清爽,国内就显得非常复杂,同时国内互联网更新速度很快,界面是生命周期短,人员流动,很难做到界面统一。

所有,Android 的 Shape 并不适合国内生态。

开发时,超级羡慕对面 IOS 开发们可以直接在控件上进行花式倒角、加线框等骚操作,想不通为啥 Android 不能在这一点上借鉴IOS。哎,Android 与 IOS 的宿命之争,说多了都是泪。

基于上面种种原因,所以出现了想开发这个库。


这个库只实现最常用的 Shape 功能,selector 及 layout-list 并未实现,因为有两点考虑:

  1. shape 使用场景更多,并且更频繁,其他两种只在少数特定场景中使用。
  2. selector 及 layout-list 需要更多精细的代码控制,如全部挤在 xml 中一个控件上,会非常臃肿,难以维护。


原理

在考虑用什么技术实现时,考虑这几点:

  1. 任何控件都能有效,即使是自定义控件。
  2. 不能有侵入性,即使更换或废弃本库,也能保证稳定性。

最开始,第一个想到的是 LayoutInflater.Factory ,xml 控件解析成 View时,必须经过它,也是换肤的解决方案,但这样得一个个替换成自己的,非常麻烦。

有没有更好的解决方案呢?

得益于 AspectJ 的 AOP(面向切面编程)能力,我们可以在编译时期,直接在 View 及其子类的构造方法中插入相关代码,解析xml 中自定义的属性,最后设置到控件上。

    @Pointcut("execution(android.view.View+.new(..))")
    public void callViewConstructor() {
    }

    @After("callViewConstructor()")
    public void inject(JoinPoint joinPoint) throws Throwable {

        Signature signature = joinPoint.getSignature();
        Object target = joinPoint.getTarget();
        Object[] args = joinPoint.getArgs();

        int length = args.length;
        if (!(target instanceof View) || length < 2 || target.hashCode() == lastHash || !(args[0] instanceof Context) || !(args[1] instanceof AttributeSet)) {
            return;
        }
        lastHash = target.hashCode();

        Context context = (Context) args[0];
        AttributeSet attrs = (AttributeSet) args[1];

        int count = attrs.getAttributeCount();

        for (int i = 0; i < count; i++) {
            Log.i(TAG, attrs.getAttributeName(i) + " = " + attrs.getAttributeValue(i));
        }

        Log.i(TAG, "inject =====> " + signature.toString());
        DuckFactor.getFactor().inject((View) target, context, attrs);
    }

AOP 相关内容,可以查看AOP 系列 包含:

1.OOP 与 AOP

2.Java 注解处理器

3.Aspect

4.Android中使用 Javassist


由于 AspectJ 能遍历项目中所有依赖包,因此,无论是 support 库,还是第三方库都能得到很好支持。

但是 AOP 也存在一定问题,我们的 apk 中是不会存在系统原生 Android SDK 的,例如 TextView 这个系统控件,在编译时是不会打包到 apk 中,因此,AOP 技术对这种原生控件无能为力。

幸好,我们绝大部分项目为了兼容性,一般都会直接依赖官方的兼容库,即 support 相关的库。

在 support· 库中,会将一些原生控件,直接替换成 support 相关控件。相关代码如下:

android/support/v7/app/AppCompatViewInflater

switch (name) {
            case "TextView":
                view = createTextView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "ImageView":
                view = createImageView(context, attrs);
                verifyNotNull(view, name);
                break;
            case "Button":
                view = createButton(context, attrs);
                verifyNotNull(view, name);
                break;
            case "EditText":
                view = createEditText(context, attrs);
                verifyNotNull(view, name);
                break;
            ......
        }

而对于这些控件,我们的 AOP 都能够生效了。

在 support 库中,没有替换掉 ViewGroup 的几个常用子类,如LinearLayoutRelativeLayoutFrameLayout等,

所以,我们我们仿照 support 的替换方式,直接在 LayoutInflater.Factory.onCreateView 方法中注入相应的替换代码。

    
    @Pointcut("execution(* *..LayoutInflater.Factory+.onCreateView(..))")
    public void callLayoutInflater() {
    }

    @Around("callLayoutInflater()")
    public Object replaceView(ProceedingJoinPoint joinPoint) throws Throwable {

        ....
            
        switch (name) {
            case "RelativeLayout":
                return new DuckRelativeLayout(context, attrs);
            case "LinearLayout":
                return new DuckLinearLayout(context, attrs);
            case "FrameLayout":
                return new DuckFrameLayout(context, attrs);
            case "TableLayout":
                return new DuckTableLayout(context, attrs);
            case "ScrollView":
                return new DuckScrollView(context, attrs);
            default:
                break;
        }

        return result;
    }

这个库的代码其实很少,我这里也只是实现了 Shape 这一个功能。

    private static Injector mInjector;

    public static void setFactor(Injector injector) {
        mInjector = injector;
    }

    public static Injector getFactor() {
        if (mInjector == null) {
            mInjector = new ShapeInjector();
        }
        return mInjector;
    }

这里保留的 Duck 的扩展性,如果觉得不够,可以自行实现功能更强大的 Injector。

AOP 的能力远不止如此,还有很多事情可以做,建议大家可以发挥想象,进行更多的扩展。


优化

核心原理已经搞定,还有两点需要优化:

  1. 在 xml 中使用时,没有提示不方便,这一点可以通过Live Template 来解决。

    演示
  2. 无法预览,AOP 在编译时工作,所有无法实时预览,看到别人的库是替换成自定义 View 来查看效果,感觉这种实现方式不够完美,所有就放弃了,后续想着能否通过 AS 插件实现预览。

项目地址:Duck


小插曲

这个库的出现挺坎坷的。

在 18 年 8 月份左右,就开始写了这个库,当时核心功能的实现,xml 的代码提示问题都已经想好了解决方案。

但写到一半出现了功能相同的库,

BackgroundLibrary

仔细看了项目代码,对比发现实现原理不一样,xml 提示解决方案相同,预览问题通过替换解决。

额~

好吧!就瞬间没写下去的动力了,再加上公司赶工期,代码就扔在那没动了。

直到最近空闲了,突然想着不管这样,还是弄完吧,搞个烂尾实在是不好。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容