Is there a one-line code solution for the "black and white" of Android App?

Preface

Ching Ming Festival is a traditional Chinese festival. On this day last year, many websites and apps expressed their deep condolences by turning black and white.

Of course, here we only talk about technology-the exploration of the realization of black and white Android App.

Today I am sharing with you the implementation plan of Hongyang Dazhi.

Original address: Hongyang

Master this method, change to black and white on Tomb-sweeping Day, change to a big red on National Day... Turn over as a product manager, follow the party's leadership closely, and realize the Chinese dream!

text

On the same day, Hongyang also launched a black and white effect on wanandroid.com:

You may make a lot of apps, and the whole site on the web side can achieve this effect. It only needs one sentence:

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

Just add a css style to html, you can understand it as adding a grayscale effect to the entire page.

That's it, it's really convenient.

Looking back at the app, everyone felt that it was more troublesome to develop. The general idea is:

  • Peeling

  • To display the pictures sent by the server, grayscale processing is required separately;

It seems that the workload is still very large.

Later, I was thinking, since the web side can add a grayscale effect to the entire page in this way, should our app be able to do that too?

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

Normally, our app page is actually drawn by Canvas, right?

Corresponding APIs for Canvas must also support grayscale .

So can we set a grayscale effect before drawing when we draw the control?

It seems that something mysterious has been discovered.

An attempt to effect the gray ImageView

Then we first verify the feasibility of the grayscale effect through ImageView.

We write a custom ImageView called: GrayImageView

The layout file is 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>

Very simple, we put an ImageView for comparison.

Look at the code of GrayImageView:

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();
    }

}

Before analyzing the code, let's look at the renderings:

It's perfect, we successfully made the wanandroid icon gray.

Take a look at the code, the code is very simple, we overwrite the draw method, in this method to do a special treatment for canvas.

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

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 ] 

Applied to a specific color [R, G, B, A], the final color calculation is 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;

Does it look uncomfortable? Yes, I also feel uncomfortable. It's annoying to see algebra.

Since everyone is uncomfortable, then Android is more intimate and gave us a ColorMartrix class. This class provides a lot of APIs. You can directly call the API to get most of the desired effects, unless you have special operations. , Then you can use the matrix to calculate it yourself.

For effects like grayscale, we can manipulate the saturation API:

setSaturation(float sat)

Just pass in 0. You can look at the source code. The bottom layer passes in a specific matrix to perform the operation.

Ok, okay, forget the above, just remember that you have an API that can make things drawn by canvas gray.

So we have already made the ImageView grayscale, can TextView be used? Is Button OK?

Try to draw inferences

Let's try TextView and Button.

The code is exactly the same. In fact, it is 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();
    }
}

There is no difference, GrayButton will not be posted, let's 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="鸿洋真帅"
    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="鸿洋真帅"
        android:textColor="@android:color/holo_red_light"
        android:textSize="30dp" />


    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="鸿洋真帅"
        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="鸿洋真帅"
        android:textColor="@android:color/holo_red_light"
        android:textSize="30dp" />

</LinearLayout>

Corresponding renderings:

You can see that TextView and Button have also successfully changed the red font to gray.

At this moment, do you suddenly feel that you will?

In fact, we only need to replace various related Views with this kind of custom Views, and use appcompat to change the skin. The server does not need to be involved, and the client can do it.

is it? Do we need to replace all Views with custom Views?

This sounds like a high cost.

Think about it, is there something simpler?

Look up

Although the layout file just now is very simple, I invite you to take a look at the layout file just now. I want to ask you a question:

Be optimistic.

  1. In the above xml, who is the parent View of the ImageView?

  2. Who is the parent View of TextView?

  3. Who is the parent View of the Button?

Is it a bit awkward!

Do we need to customize one by one?

The parent View is LinearLayout, so we can just create a GrayLinearLayout, and the internal View will turn gray. After all, 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();
    }

}

The code is very simple, but note that there is a detail, note that we also overwrite dispatchDraw, why? Think for yourself.

We 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="鸿洋真帅"
        android:textColor="@android:color/holo_red_light"
        android:textSize="30dp" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="鸿洋真帅"
        android:textColor="@android:color/holo_red_light"
        android:textSize="30dp" />

</com.imooc.imooc_wechat_app.view.GrayLinearLayout>

We put a blue Logo ImageView, red font TextView and Button, take a look at the effect:

perfect!

Isn't it a bit awkward again!

As long as we change the root layout of the Activity we set up!

The root layout of Activity may be LinearLayout, FrameLayout, RelativeLayout, ConstraintLayout...

Change the chicken... When does this have to be changed, and what's the difference from just now.

Are there any ideas? Isn't there any definite View?

Think again.

Where will the root layout of our set Activity be placed?

android.id.content

Is it on this Content View?

This content view is currently FrameLayout!

Then we only need to generate the FrameLayout corresponding to this android.id.content and replace it with GrayFrameLayout.

How to change it?

What is appcompat? Go to LayoutFactory?

It is indeed possible, but then to set up LayoutFactory, you also need to consider appcompat related logic.

Is there a solution that does not need to modify any process?

Details in LayoutInflater

It really does.

Our AppCompatActivity can override the onCreateView method. This method is actually called back when the LayoutFactory is building the View, and generally corresponds to its internal mPrivateFactory.

His priority is lower than Factory, Factory2, related code:

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;
    }
}   

But currently for FrameLayout, appcompat has no special treatment, which means you can construct the FrameLayout object in the onCreateView callback.

It's very simple, just overwrite 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) {
        return super.onCreateView(name, context, attrs);
    }
}

In this method, we replace the FrameLayout corresponding to the content view with GrayFrameLayout.

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    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;
                }
            }
        }
    }
    return super.onCreateView(name, context, attrs);
}

The code should be able to understand, we found the id is android:id/content, and replaced it with our GrayFrameLayout.

Finally, take a 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();
    }

}

Okay, run it to see the effect:

The effect is ok.

Then put the code of onCreateView into your BaseActivity.

What, no BaseActivity?

…Go on your own.

Find an App to verify

Speaking of now, there is no Activity out of it.

Let's find a more complicated project to verify it.

I went to github to find a wanandroid Java open source project:

Selected: https://github.com/jenly1314/WanAndroid

After importing, just add the code we just added to BaseActivity.

Running effect chart:

Well, yes, the text and pictures in the webview are black and white.

Such an app is completely black and white.

Wait, I found that the status bar has not changed. Does the status bar have an API? Call a line of code in BaseActivity to handle it.

Click here to reply: "The article is written really well", you can get the black and white apk and experience it yourself.

Is it really okay?

In fact, it is a pity that it did not run.

Then I blew myself a few questions.

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

Because we are dealing with the content view, it must be under the window, and it must not cover the backgroud of the window.

What to do?

Don't panic.

The GrayFrameLayout we generated can also set the background?

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

If you set the windowBackground in the theme, you need to extract the drawable from the theme. The reference code is 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);
}

2. Does Dialog support?

This solution already supports Dialog black and white by default, why? Take a look at the Dialog related source code yourself to see what the View structure inside the Dialog looks like.

In addition, the image text inside the webview is also supported.

3. What if android.R.id.content is not FrameLayout in the future?

It is indeed possible.

Presumably you can also make the internal View of PhoneWindow look like this:

decorView
    GrayFrameLayout
        android.R.id.content
            activity rootView

Or something like this:

decorView
    android.R.id.content
        GrayFrameLayout
            activity rootView

OK.

Okay, I'm going to finish it, can it be realized by one line of code? No, there seems to be 30 lines of code, but it's simple enough.

The code was written for 3 minutes, and the article was written all afternoon.

At last

This article is by no means simply talking about how to achieve black and white, because it only takes about 30 lines of code to post the code and it is over. In fact, this article contains more than 1W characters, I hope you can get enough knowledge from it.

Android learning is a long road. What we have to learn is not only the superficial technology, but also the bottom layer and understand the following principles. Only in this way can we improve our competitiveness. In today's highly competitive world Foothold in.

A journey of a thousand miles begins with a single step, I hope you and I encourage each other.

I put the most important and popular learning direction materials for Android that I have compiled during this time on my GitHub , which also contains self-learning programming routes in different directions, interview questions/face-to-face, and a series of technical articles.

The resources are continuously updated, and everyone is welcome to learn and discuss together.

Guess you like

Origin blog.csdn.net/weixin_49559515/article/details/112853151