App快速實(shí)現(xiàn)置灰樣式

背景:

4月4日摔握,國家為表達(dá)全國各族人民對抗擊新冠肺炎疫情斗爭犧牲烈士和逝世同胞的深切哀悼央勒,舉行全國性哀悼活動,各大網(wǎng)站和App也都變成了灰色蚀乔。

從宣布全國哀悼日到4號,期間也就1菲茬、2天時間吉挣,主流網(wǎng)站還是快速做出了響應(yīng),雖然這次我們公司沒有跟進(jìn)把網(wǎng)站和App置灰婉弹,但實(shí)現(xiàn)方案也可以作為一種技術(shù)儲備睬魂,故有了這篇文章。

網(wǎng)站實(shí)現(xiàn)方案:

以管理后臺為例镀赌,所有頁面都包裹在一層html中氯哮,只要在html中加入一個灰度處理,就應(yīng)該能實(shí)現(xiàn)效果商佛。具體操作如下:



可以說是很簡單了喉钢,我們看看實(shí)際的效果如何:



效果很完美,首頁和子頁面都能很好的實(shí)現(xiàn)置灰良姆。

App實(shí)現(xiàn)方案:

App的每個頁面布局都是一個單獨(dú)的XML文件肠虽,沒有像H5那樣有一個公共的XML布局可以修改達(dá)到效果。
但我們可以試著從一個ImageView開始玛追,讓它變成灰色看看税课。
我們先新建一個CustomImageView:
了解自定義View的繪制過程的都應(yīng)該知道闲延,View的繪制主要就是3個方法:onMeasureonLayout韩玩、onDraw 分別是測量慨代、計(jì)算位置、繪制啸如,我們想要把view置灰,想來應(yīng)該要從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 // 繪制內(nèi)容
     *      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就足夠了,但這次我們要改變所有內(nèi)容的顏色飽和度互婿,那肯定要用draw方法捣郊。

draw的上下文有個canvas對象,再進(jìn)入canvas類看下慈参,里面有各種各樣的繪制方法呛牲,drawBitmapdrawText等等驮配,這些方法有個共同點(diǎn)娘扩,就是都會傳入一個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);
}

我們通過查資料找到可以通過設(shè)置ColorMatrix的色彩飽和度就能實(shí)現(xiàn)置灰猜绣,具體代碼如下:

ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0); // 設(shè)置色彩飽和度為0
mPaint.setColorFilter(new ColorMatrixColorFilter(cm));

設(shè)置完畫筆后灰殴,就把畫筆傳入圖層中:

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

我們運(yùn)行下代碼,驗(yàn)證下我們的設(shè)想:



我們用一個普通的ImageView做對比掰邢,自定義的ImageView如我們預(yù)想的那樣验懊,相應(yīng)的TextView、EditText等也能實(shí)現(xiàn)同樣的效果尸变,接下去我們就會想义图,我們把這段代碼作用在這些View的父標(biāo)簽上面,是不是會把里面所有的子控件一起置灰召烂,這里我們拿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); // 設(shè)置色彩飽和度為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,具體原因就是當(dāng)DecorView繪制完自己以后,會調(diào)用drawChild(canvas, child, drawingTime)依次繪制子View怕篷,當(dāng)LinearLayout沒有背景历筝,就會跳過draw方法,直接調(diào)用dispatchDraw廊谓,所以必須覆蓋2個方法梳猪。子View繪制代碼如下:

/**
 * This method is called by ViewGroup.drawChild() to have each child view draw itself.
 * 父容器分發(fā),調(diào)用子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);
    }
    ......
}

到這一步蒸痹,如果我們要想實(shí)現(xiàn)效果春弥,只要把所有的xml布局的最外層標(biāo)簽都換成各種GreyLinearLayoutGreyRelativeLayout等等就行了叠荠,但這樣工作量依舊很大匿沛,并不是我們想要的快速實(shí)現(xiàn)方案。那有沒有一個ViewGroup是這些所有的xml布局的父標(biāo)簽?zāi)亻欢Γ鸢肝覀兟齺韺ふ姨雍簟N覀兿瓤聪翧ctivity在啟動時,首先調(diào)用的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提供了的唯一實(shí)現(xiàn)類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樣式置灰是不是就達(dá)到了我們想要的結(jié)果蔫缸。但是我們哪里去處理這個動作呢,我們只能接著看mLayoutInflater.inflate源碼际起。下面列出部分關(guān)鍵邏輯代碼:

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解析器拾碌,遍歷所有的布局節(jié)點(diǎn)
    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一樣,也是調(diào)用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方法里面,按mFactory2灾前、mFactory防症、mPrivateFactory, 分4步去嘗試拿到當(dāng)前遍歷的view哎甲,mFactory2回調(diào)的就是activity的onCreateView蔫敲,那我們覆寫下activity的onCreateView方法,拿到view以后就不會執(zhí)行其他factory了炭玫,就達(dá)到了我們想要替換的目標(biāo)奈嘿。讓我們試試看,寫之前我們通過android sdk自帶的布局檢查器Layout Inspector看下我們xml的布局樣式:




可以看到我們自定義ViewGreyLinearLayout的父標(biāo)簽是一個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);
}

代碼運(yùn)行下,效果就是我們想要實(shí)現(xiàn)的效果叶圃,至此袄膏,我們通過改動20來行代碼實(shí)現(xiàn)了整個App的置灰效果,通過熱更新等方式掺冠,以最小的代碼改動沉馆,達(dá)到了先前我們快速實(shí)現(xiàn)樣式置灰的效果。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末德崭,一起剝皮案震驚了整個濱河市斥黑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌接癌,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扣讼,死亡現(xiàn)場離奇詭異缺猛,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)椭符,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進(jìn)店門荔燎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人销钝,你說我怎么就攤上這事有咨。” “怎么了蒸健?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵座享,是天一觀的道長。 經(jīng)常有香客問我似忧,道長渣叛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任盯捌,我火速辦了婚禮淳衙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘饺著。我一直安慰自己箫攀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布幼衰。 她就那樣靜靜地躺著靴跛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪渡嚣。 梳的紋絲不亂的頭發(fā)上汤求,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天俏险,我揣著相機(jī)與錄音,去河邊找鬼扬绪。 笑死竖独,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的挤牛。 我是一名探鬼主播莹痢,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼墓赴!你這毒婦竟也來了竞膳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤诫硕,失蹤者是張志新(化名)和其女友劉穎坦辟,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體章办,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锉走,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了藕届。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片挪蹭。...
    茶點(diǎn)故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖休偶,靈堂內(nèi)的尸體忽然破棺而出梁厉,到底是詐尸還是另有隱情,我是刑警寧澤踏兜,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布词顾,位于F島的核電站,受9級特大地震影響碱妆,放射性物質(zhì)發(fā)生泄漏计技。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一山橄、第九天 我趴在偏房一處隱蔽的房頂上張望垮媒。 院中可真熱鬧拜轨,春花似錦肥荔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽它抱。三九已至,卻和暖如春朴艰,著一層夾襖步出監(jiān)牢的瞬間观蓄,已是汗流浹背混移。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留侮穿,地道東北人歌径。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像亲茅,于是被迫代替她去往敵國和親回铛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評論 2 351