App快速实现置灰样式

背景:

4月4日,国家为表达全国各族人民对抗击新冠肺炎疫情斗争牺牲烈士和逝世同胞的深切哀悼,举行全国性哀悼活动,各大网站和App也都变成了灰色。

从宣布全国哀悼日到4号,期间也就1、2天时间,主流网站还是快速做出了响应,虽然这次我们公司没有跟进把网站和App置灰,但实现方案也可以作为一种技术储备,故有了这篇文章。

网站实现方案:

以管理后台为例,所有页面都包裹在一层html中,只要在html中加入一个灰度处理,就应该能实现效果。具体操作如下:



可以说是很简单了,我们看看实际的效果如何:



效果很完美,首页和子页面都能很好的实现置灰。

App实现方案:

App的每个页面布局都是一个单独的XML文件,没有像H5那样有一个公共的XML布局可以修改达到效果。
但我们可以试着从一个ImageView开始,让它变成灰色看看。
我们先新建一个CustomImageView:
了解自定义View的绘制过程的都应该知道,View的绘制主要就是3个方法:onMeasureonLayoutonDraw 分别是测量、计算位置、绘制,我们想要把view置灰,想来应该要从onDraw入手,但这次,我们不是使用这个onDraw,为什么呢?我们看下另外一个绘制方法draw,源码上的注释是这么写的:

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background // 画背景
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content // 绘制内容
     *      4. Draw children // 绘制子视图
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance) // 绘制装饰。主要是foreground与滚动条
     */

    // Step 1, draw the background, if needed
    int saveCount;

    drawBackground(canvas);

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        drawAutofilledHighlight(canvas);

        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);

        if (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }

Step 3, draw the content说了onDraw只绘制了content,一般我们自定义View,google推荐用onDraw就足够了,但这次我们要改变所有内容的颜色饱和度,那肯定要用draw方法。

draw的上下文有个canvas对象,再进入canvas类看下,里面有各种各样的绘制方法,drawBitmapdrawText等等,这些方法有个共同点,就是都会传入一个paint对象,这个对象就是画笔,我们要想显示置灰,就要从这个画笔入手。

@Override
public void draw(Canvas canvas) {
    super.draw(canvas);
}
public void drawBitmap(@NonNull Bitmap bitmap, @Nullable Rect src, @NonNull Rect dst,
        @Nullable Paint paint) {
    super.drawBitmap(bitmap, src, dst, paint);
}

public void drawText(@NonNull char[] text, int index, int count, float x, float y,
        @NonNull Paint paint) {
    super.drawText(text, index, count, x, y, paint);
}

我们通过查资料找到可以通过设置ColorMatrix的色彩饱和度就能实现置灰,具体代码如下:

ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0); // 设置色彩饱和度为0
mPaint.setColorFilter(new ColorMatrixColorFilter(cm));

设置完画笔后,就把画笔传入图层中:

@Override
public void draw(Canvas canvas) {
    canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
    super.draw(canvas);
    canvas.restore();
}

我们运行下代码,验证下我们的设想:



我们用一个普通的ImageView做对比,自定义的ImageView如我们预想的那样,相应的TextView、EditText等也能实现同样的效果,接下去我们就会想,我们把这段代码作用在这些View的父标签上面,是不是会把里面所有的子控件一起置灰,这里我们拿LinearLayout尝试下,代码如下:

public class GreyLinearLayout extends LinearLayout {
    private Paint mPaint = new Paint();

    public GreyLinearLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

        ColorMatrix cm = new ColorMatrix();
        cm.setSaturation(0); // 设置色彩饱和度为0
        mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
    }

    @Override
    public void draw(Canvas canvas) {
        canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
        super.draw(canvas);
        canvas.restore();
    }


    @Override
    protected void dispatchDraw(Canvas canvas) {
        canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
        super.dispatchDraw(canvas);
        canvas.restore();
    }
}

这里比ImageView多覆盖了一个方法dispatchDraw,具体原因就是当DecorView绘制完自己以后,会调用drawChild(canvas, child, drawingTime)依次绘制子View,当LinearLayout没有背景,就会跳过draw方法,直接调用dispatchDraw,所以必须覆盖2个方法。子View绘制代码如下:

/**
 * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 * 父容器分发,调用子View绘制自己
 * This is where the View specializes rendering behavior based on layer type,
 * and hardware acceleration.
 */
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ......
    // Fast path for layouts with no backgrounds
    // 没有背景时候的快速路径
    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        dispatchDraw(canvas);
    } else {
        draw(canvas);
    }
    ......
}

到这一步,如果我们要想实现效果,只要把所有的xml布局的最外层标签都换成各种GreyLinearLayoutGreyRelativeLayout等等就行了,但这样工作量依旧很大,并不是我们想要的快速实现方案。那有没有一个ViewGroup是这些所有的xml布局的父标签呢,答案我们慢慢来寻找。我们先看下Activity在启动时,首先调用的onCreate,接下去看下源码:

/**
 * Set the activity content from a layout resource.  The resource will be
 * inflated, adding all top-level views to the activity.
 *
 * @param layoutResID Resource ID to be inflated.
 *
 * @see #setContentView(android.view.View)
 * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
 */
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

这里的getWindow(),是Android为Window提供了的唯一实现类PhoneWindow,我们接下去看下PhoneWindowsetContentView方法

public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;
public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
}
@Override
public void setContentView(int layoutResID) {
    .....
    if (mContentParent == null) {
        installDecor(); // 初始化了DecoView
    }
    mLayoutInflater.inflate(layoutResID, mContentParent);
    .....
}
private void installDecor() {
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
}

从上面的代码我们可以看到我们自己写的xml布局塞在R.id.content这个里面,那我们把这个ViewGroup样式置灰是不是就达到了我们想要的结果。但是我们哪里去处理这个动作呢,我们只能接着看mLayoutInflater.inflate源码。下面列出部分关键逻辑代码:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        ......
        try {
            ......
            final String name = parser.getName();
            if (TAG_MERGE.equals(name)) {
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // 拿到root view
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ......

                // 拿到所有child view
                // Inflate all children under temp against its context.
                rInflateChildren(parser, temp, attrs, true);                
              }
        return result;
    }
}

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

    .....

    // 通过xml解析器,遍历所有的布局节点
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        final String name = parser.getName();

        if (TAG_REQUEST_FOCUS.equals(name)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            // 最终和root view一样,也是调用createViewFromTag去生成View
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }
    ......
}

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
    ......

    try {
        View view;
        // 1
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        // 2
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        // 3
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        // 4
        if (view == null) {
            ......
        }

        return view;
    }
}

从上面的代码可以看到,不管是root view还是自己xml的子view,都是通过createViewFromTag方法里面,按mFactory2mFactorymPrivateFactory, 分4步去尝试拿到当前遍历的view,mFactory2回调的就是activity的onCreateView,那我们覆写下activity的onCreateView方法,拿到view以后就不会执行其他factory了,就达到了我们想要替换的目标。让我们试试看,写之前我们通过android sdk自带的布局检查器Layout Inspector看下我们xml的布局样式:




可以看到我们自定义ViewGreyLinearLayout的父标签是一个id为contentFrameLayout,跟上面我们源码里面看到的情况一致,然后我们就要自定义个FrameLayout去替换它,代码如下:

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    try {
        if ("FrameLayout".equals(name)) {
            int count = attrs.getAttributeCount();
            for (int i = 0; i < count; i++) {
                String attributeName = attrs.getAttributeName(i);
                String attributeValue = attrs.getAttributeValue(i);
                if (attributeName.equals("id")) {
                    int id = Integer.parseInt(attributeValue.substring(1));
                    String idVal = getResources().getResourceName(id);
                    if ("android:id/content".equals(idVal)) {
                        GrayFrameLayout grayFrameLayout = new GrayFrameLayout(context, attrs);
                        return grayFrameLayout;
                    }
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return super.onCreateView(name, context, attrs);
}

代码运行下,效果就是我们想要实现的效果,至此,我们通过改动20来行代码实现了整个App的置灰效果,通过热更新等方式,以最小的代码改动,达到了先前我们快速实现样式置灰的效果。

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