背景:
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個方法:onMeasure
、onLayout
韩玩、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
類看下慈参,里面有各種各樣的繪制方法呛牲,drawBitmap
、drawText
等等驮配,這些方法有個共同點(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)簽都換成各種GreyLinearLayout
、GreyRelativeLayout
等等就行了叠荠,但這樣工作量依舊很大匿沛,并不是我們想要的快速實(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
,我們接下去看下PhoneWindow
的setContentView
方法
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的布局樣式:
可以看到我們自定義View
GreyLinearLayout
的父標(biāo)簽是一個id為content
的FrameLayout
吞加,跟上面我們源碼里面看到的情況一致裙犹,然后我們就要自定義個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)樣式置灰的效果。