This article has been authorized public account hongyangAndroid original release.

On April 4, many websites and apps expressed deep condolences by turning black and white.

In this article we are purely technical.

I also put a black and white effect on wanandroid.com on the same day:

We may do app more, web end full station to achieve this effect, only need a sentence:

html {filter:progid:DXImageTransform.Microsoft.BasicImage(grayscale=1); -webkit-filter: grayscale(100%); }Copy the code

Simply add a CSS style to the HTML, which you can interpret as adding a grayscale effect to the entire page.

It’s done. It’s really convenient.

Looking back at the APP, everyone thinks that it is difficult to develop, and the common thinking is:

  1. In the skin;
  2. To show the pictures delivered by the server, it is necessary to do gray processing separately;

That still seems like a lot of work.

Later, I was thinking, since the Web end can add a grayscale effect to the whole page, our APP should be able to do the same?

So how do we add a grayscale effect to the app page?

Our app page is normally drawn by Canvas, right?

Canvas’s API must also support grayscale.

So is it ok to set a grayscale effect before the control is drawn, such as draw?

I think I found something.

1. Try applying a grayscale effect to the ImageView

So we first through ImageView to verify the feasibility of gray effect.

Let’s write a custom ImageView called GrayImageView

The layout file looks like this:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".TestActivity">

    <ImageView
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:src="@mipmap/logo">

    </ImageView>

    <com.imooc.imooc_wechat_app.view.GrayImageView
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:src="@mipmap/logo" />

</LinearLayout>
Copy the code

Very simple, we put in an ImageView for comparison.

Take a look at the GrayImageView code:

public class GrayImageView extends AppCompatImageView { private Paint mPaint = new Paint(); public GrayImageView(Context context, AttributeSet attrs) { super(context, attrs); ColorMatrix cm = new ColorMatrix(); cm.setSaturation(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(); }}Copy the code

Before analyzing the code, let’s take a look at the rendering:

It’s perfect, we’ve managed to make the WanAndroid icon grey.

Take a look at the code. The code is very simple. We have copied the draw method and done a special treatment to the canvas in that method.

What special treatment? In fact, set a grayscale effect.

In App, we often use color matrix for color processing, which is a 4*5 matrix. The principle is as follows:

[ a, b, c, d, e,
    f, g, h, i, j,
    k, l, m, n, o,
    p, q, r, s, t ] 
Copy the code

Applied to A specific color [R, G, B, A], the final color calculation looks like this:

R '= a*R + b*G + c* b + d* a + e; G '= f*R + G *G + h*B + I *A + j; B '= k*R + l*G + m*B + n*A + o; A '= p*R + q*G + R *B + s*A + t;Copy the code

Doesn’t it look bad? Yeah, I do, too. I hate algebra.

Since we are sad, then Android is more intimate, give us a ColorMartrix class, this class provides a lot of API, you can directly call the API to get most of the desired effect, unless you have a special operation, then you can calculate through the matrix.

Effects like grayscale can be manipulated via the saturation API:

setSaturation(float sat)
Copy the code

You just pass in zero, and you go to the source code, and you pass in a specific matrix to do the operations.

Ok, ok, forget that, just remember that you have an API that grays everything drawn on the canvas.

So now that we’ve done that we can grayscale our ImageView, can we do TextView? Is Button ok?

2. Try to draw inferences

Let’s try TextView, Button.

The code is exactly the same, except for a different implementation class, such as GrayTextView:

public class GrayTextView extends AppCompatTextView { private Paint mPaint = new Paint(); public GrayTextView(Context context, AttributeSet attrs) { super(context, attrs); ColorMatrix cm = new ColorMatrix(); cm.setSaturation(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(); }}Copy the code

There is no difference, GrayButton will not paste, we look at the layout file:

<? xml version="1.0" encoding="utf-8"? > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".TestActivity">

    <ImageView
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:src="@mipmap/logo">

    </ImageView>

    <com.imooc.imooc_wechat_app.view.GrayImageView
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:src="@mipmap/logo" />

    <TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Hongyang is so handsome."
    android:textColor="@android:color/holo_red_light"
    android:textSize="30dp" />


    <com.imooc.imooc_wechat_app.view.GrayTextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hongyang is so handsome."
        android:textColor="@android:color/holo_red_light"
        android:textSize="30dp" />


    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hongyang is so handsome."
        android:textColor="@android:color/holo_red_light"
        android:textSize="30dp" />


    <com.imooc.imooc_wechat_app.view.GrayButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hongyang is so handsome."
        android:textColor="@android:color/holo_red_light"
        android:textSize="30dp" />

</LinearLayout>
Copy the code

Corresponding renderings:

As you can see TextView and Button successfully changed the red font to gray.

Did you suddenly feel like you knew it?

In fact, we just need to change the various related views into this custom View, using the AppCompat skin that does not need the Server to participate, the client side to do it.

Isn’t it? Do we need to replace all views with custom views?

That sounds pretty expensive, too.

Think again, what could be easier?

3. Look up

Although the layout file was very simple, I invite you to take a look at the layout file again, and I’m going to ask you a question:

Watch.

  1. Who is the parent of the ImageView in the XML above?
  2. Who is the parent of a TextView?
  3. Who is the parent View of Button?

A little insight!

Do we need to customize one by one?

The parent View is a LinearLayout, so we’ll just use a GrayLinearLayout. All the views inside the View will turn gray, because the Canvas object is passed down.

Let’s try:

GrayLinearLayout:

public class GrayLinearLayout extends LinearLayout { private Paint mPaint = new Paint(); public GrayLinearLayout(Context context, AttributeSet attrs) { super(context, attrs); ColorMatrix cm = new ColorMatrix(); cm.setSaturation(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(); }}Copy the code

The code is pretty simple, but notice one detail, we’ve also copied dispatchDraw, why? Think for yourself:

Let’s replace the XML:

<? xml version="1.0" encoding="utf-8"? > <com.imooc.imooc_wechat_app.view.GrayLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".TestActivity">

    <ImageView
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:src="@mipmap/logo" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hongyang is so handsome."
        android:textColor="@android:color/holo_red_light"
        android:textSize="30dp" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hongyang is so handsome."
        android:textColor="@android:color/holo_red_light"
        android:textSize="30dp" />

</com.imooc.imooc_wechat_app.view.GrayLinearLayout>
Copy the code

We put the ImageView with the blue Logo and the TextView and Button with the red font.

Perfect!

Is that another revelation?!

Just change the root layout of the Activity we set!

The root of the Activity layout may be a LinearLayout, FrameLayout, RelativeLayout, ConstraintLayout…

For a chicken… When would that be? It wouldn’t be any different.

Any ideas? No definite View?

Think again.

Where will the root layout of our Activity be placed?

android.id.content
Copy the code

Is it on this Content View?

This content view is currently FrameLayout!

So we just need to generate the FrameLayout corresponding to android.id.content and replace it with GrayFrameLayout.

How do I change it?

The AppCompat thing? To make LayoutFactory?

Yes, but then to set up the LayoutFactory, you need to consider the appCompat logic.

Is there a solution that doesn’t require any process changes?

4. Details in LayoutInflater

There is.

We have an AppCompatActivity that can clone the onCreateView method, which is called back by LayoutFactory when building the View, and generally corresponds to its internal mPrivateFactory.

Factory2 has a lower priority than Factory and Factory2.

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

if(view == null && mPrivateFactory ! = null) { view = mPrivateFactory.onCreateView(parent, name, context, attrs); }if (view == null) {
    final Object lastContext = mConstructorArgs[0];
    mConstructorArgs[0] = context;
    try {
        if (-1 == name.indexOf('. ')) {
            view = onCreateView(parent, name, attrs);
        } else{ view = createView(name, null, attrs); } } finally { mConstructorArgs[0] = lastContext; }}Copy the code

Appcompat does not currently handle FrameLayout, which means you can construct the FrameLayout object in the onCreateView callback.

Simply copy the Activity’s onCreateView method:

public class TestActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        returnsuper.onCreateView(name, context, attrs); }}Copy the code

So in this method we’re going to replace the FrameLayout for the Content View with GrayFrameLayout.

@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);
//                            grayFrameLayout.setWindow(getWindow());
                        return grayFrameLayout;
                    }
                }
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return super.onCreateView(name, context, attrs);
}
Copy the code

We found that id is Android: ID /content, replaced by our GrayFrameLayout. I didn’t test this code logic carefully, there may be some abnormalities, please sort it out by yourself.

One last look at GrayFrameLayout:

public class GrayFrameLayout extends FrameLayout { private Paint mPaint = new Paint(); public GrayFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); ColorMatrix cm = new ColorMatrix(); cm.setSaturation(0); mPaint.setColorFilter(new ColorMatrixColorFilter(cm)); } @Override protected void dispatchDraw(Canvas canvas) { canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG); super.dispatchDraw(canvas); canvas.restore(); } @Override public void draw(Canvas canvas) { canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG); super.draw(canvas); canvas.restore(); }}Copy the code

Ok, run it and see what it looks like:

The effect is ok.

And then you just put this onCreateView piece of code inside your BaseActivity.

What, no BaseActivity?

.

5. Check with an App

So far, not a single Activity has been removed.

Let’s find a more complicated project to test.

I’ll go to Github and find a Java open source project for WanAndroid:

Selected:

Github.com/jenly1314/W…

Once imported, just add the code we just described to BaseActivity.

Operation effect picture:

Well, yes, the text and images in the WebView are black and white.

I didn’t find any problems, so the app is completely black and white.

Wait, I found that the status bar has not changed, the status bar is not API, I call a line of code in BaseActivity processing.

Reply inside number: “good article written”, get black and white apK, experience yourself.

6. Is everything all right?

It’s a shame it didn’t work out.

Let me ask you a few questions.

1. What if the background of the Activity Window is set?

Because we’re dealing with a Content view, it’s definitely under the window, so it’s definitely not going to cover the window’s backgroud.

What to do?

Don’t panic.

GrayFrameLayout that we generated can also set background, right?

if ("android:id/content".equals(idVal)) {
    GrayFrameLayout grayFrameLayout = new GrayFrameLayout(context, attrs);
    grayFrameLayout.setBackgroundDrawable(getWindow().getDecorView().getBackground());
    return grayFrameLayout;
}
Copy the code

Note that if you call getWindow().setBackgroundDrawable too early, getDecorView().getBackground() may not be available. So can make grayFrameLayout. SetBackgroundDrawable before actually draw call.

If you are a windowBackground set to the theme, then you need to extract drawable from the theme as follows:

TypedValue a = new TypedValue();
getTheme().resolveAttribute(android.R.attr.windowBackground, a, true);
if(a.type >= TypedValue.TYPE_FIRST_COLOR_INT && a.type <= TypedValue.TYPE_LAST_COLOR_INT) { // windowBackground is a color  int color = a.data; }else {
    // windowBackground is not a color, probably a drawable
    Drawable c = getResources().getDrawable(a.resourceId);
}
Copy the code

Source search stackOverflow.

2. Is Dialog supported?

This scheme already supports Dialog blackening by default. Why? The View structure inside a Dialog looks like this.

3. What if Android.r.D.C. Tent is not FrameLayout?

It’s a real possibility.

You can also make PhoneWindow’s internal View look like this:

decorView
	GrayFrameLayout
		android.R.id.content
			activity rootView
Copy the code

Or this:

decorView
	android.R.id.content
		GrayFrameLayout
			activity rootView
Copy the code

Ok.

4. Current known problems

Originally, I tested the Webview and thought it was normal, but I did not expect the webview effect to be abnormal on some apps.

Also feedback abnormal video playback.

We haven’t found a good solution yet.

Let’s wrap it up.

I suggest you follow the article to do some experiments, in order to learn knowledge for the purpose; Because the general blog is a bit of a loser, feel a key to change the whole APP feel very handsome.

In fact, in real projects, even if the technical solution is determined, it is customized as far as possible, and the pursuit of the impact is as small as possible. Therefore, such practice of changing the global View at random is generally very risky, and is not recommended in actual project development.

This paper is only an exploration of the black-and-white scheme of THE APP. If it is used for online projects, it must be fully tested. For some problems, I will update it if there is further progress, and the official account cannot be modified.

Hope you can get enough knowledge from it, bye bye!