preface

I haven’t written an article for a long time, so I plan to write an article. After all, there are many articles about this knowledge.

Some time ago it became popular to suddenly not want to write Shape,Selector file articles, and then all kinds of schemes, writing custom views and so on. At that point, I think you might have seen one: You don’t need to customize a View, you completely free up your Shape. I found this idea quite good, so today I will explain the basic knowledge points related to this scheme step by step, after reading you will basically understand, and then you can write your own.

Therefore, we mainly study:

1. LayoutInflater related knowledge (mainly ⭐️)

2. SetFactory (⭐️⭐️ port)

3. The use of the actual project (⭐️⭐️ science popularization mainly for ️)

Many people will probably use the Tools-Layout Inspector function of AS to see the structure of their interface and the corresponding elements of their controls.

For example, we wrote a very simple example:

public class TestActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test); }}Copy the code
<? xml version="1.0" encoding="utf-8"? > <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button"
        />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="textview"
        />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher"
        />

</LinearLayout>
Copy the code

Then use AS to view:

Did you see anything special?

We wrote the Button in the Layout, TextView, ImageView, but in the Layout AS Inspector function view, Became AppCompatButton, AppCompatTextView AppComaptImageView, what the hell is really our button has automatically becomes a AppCompatXXX at compile time series, Or is it just plain looking inside this tool our controls just show us the AppCompatXXX family name.

Let’s change our Activity’s parent class to:

public class TestActivity extends AppCompatActivity{ ...... } becomes public class TestActivity extends Activity{...... }Copy the code

Let’s check the Layout Inspector again:

We can see that the controls automatically change to the names of the controls we wrote in the layout, which means we inherit AppCompatActivity to replace the controls we wrote in the XML.

The replacement of appcompatactivities is mainly through LayoutInflater setFactory

The body of the

1.LayoutInflater knowledge

The fact that most people use layoutInflaters is that they use the inflate method, which turns a Layout file into a View:

View view = LayoutInflater.from(this).inflate(R.layout.activity_test,null);
Copy the code

Even the setContentView(r.layout.xxx) we usually write in our activities; The interior of the method is also implemented through the inflate method.

Ever wonder why after calling this method, we can get the relevant View object?

In fact, it is very simple, is that we pass in an XML file, through the XML format to write our layout, and this method will help us to parse the XML format, and then help us instantiate the specific View object can be, we specific step by step to see the source:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    returninflate(resource, root, root ! = null); } public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources();if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                + Integer.toHexString(resource) + ")"); } / /"You can see that there are two main steps."
    //"Step 1: Retrieve the XmlResourceParser object using the res.getLayout method."
    final XmlResourceParser parser = res.getLayout(resource);
    
    try {
    
        //"Step 2: Eventually convert the XmlResourceParser into a View instance object through the Inflate method."
        returninflate(parser, root, attachToRoot); } finally { parser.close(); }}Copy the code

Originally I wanted to large source copy up, and then step by step to write content, but later found a good series of articles explaining the resource acquisition process, so I borrowed directly from the big guy, directly paste the link:

(For the details of this article, you can look at article 1 and Article 3. The source code for inflate is in Article 3.)

Android Asset Manager (Asset Manager

Android Resource Management framework (ii) AssetManager creation process

Android resource management framework (three) application resources search process


2. Factory related knowledge

2.1 Factory2 code for default Settings in the source code

We can see from the example in the preface that our Activity inherits AppCompatActivity, so let’s look at the onCreate method of AppCompatActivity:

protected void onCreate(@Nullable Bundle savedInstanceState) {
    //"1. Get proxy objects"
    AppCompatDelegate delegate = this.getDelegate();
    
    //"2. Call the installViewFactory method of the proxy class"delegate.installViewFactory(); . . super.onCreate(savedInstanceState); }Copy the code

The major difference between an AppCompatActivity and an Activity’s onCreate method is that any compatactivity can be made into an onCreate method on a proxy class AppCompatDelegate. Whereas AppCompatDelegate is an abstract class, the concrete implementation class is AppCompatDelegateImpl,

//"1. Obtain proxy class concrete method source code:"

@NonNull
public AppCompatDelegate getDelegate() {
    if (this.mDelegate == null) {
        this.mDelegate = AppCompatDelegate.create(this, this);
    }

    return this.mDelegate;
}
    
public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
    return new AppCompatDelegateImpl(activity, activity.getWindow(), callback);
}    
Copy the code

The installViewFactory method of the proxy class is implemented as follows:

public void installViewFactory() {

    //'Got the LayoutInflater object'
    LayoutInflater layoutInflater = LayoutInflater.from(this.mContext);
    
    
    if (layoutInflater.getFactory() == null) {
        //'If layoutInflater's factory2 is null, set factory to the layoutInflater object.'
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else if(! (layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) { Log.i("AppCompatDelegate"."The Activity's LayoutInflater already has a Factory installed so we can not install AppCompat's"); }}Copy the code

AppCompatDelegateImpl implements Fatory2 itself, so just setFactory2(xx,this), let’s see what Factory2 is:

public interface Factory2 extends Factory {
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
Copy the code

For those of you who have read this article before, the Factory interface and setFactory method are completely confused about Factory2. As you can see from the above Factory2 code, Factory2 actually inherits the Factory interface. The setFactory method is deprecated, and when you call setFactory, you actually call setFactory2 internally. SetFactory2 was introduced after SDK>=11:

The Factory2 class and setFactory2 method replace the Factory class and setFactory method

So execute the onCreateView method in AppCompatDelegateImpl:

//'Call method 1'
public View onCreateView(String name, Context context, AttributeSet attrs) {
    returnthis.onCreateView((View)null, name, context, attrs); } / /'Call method 2'public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) { this.createView(parent, name, context, attrs); } / /'Call method 3'public View createView(View parent, String name, @NonNull Context context, @nonnull AttributeSet attrs) {instantiate the mAppCpatViewInflater object code...... . //'look here directly, and the last call mAppCompatViewInflater. CreateView () method returns the corresponding View'
    return this.mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, true, VectorEnabledTintResources.shouldBeUsed());
}

Copy the code

So from above we can see that the onCreateView method that’s called after Factory2 is finally set, This is a createView method that calls AppCompatDelegateImpl (which ends up calling createView on AppCompatViewInflater)

So what we need to remember on this side is that we’re calling createView method on AppCompatDelegateImpl So what we need to remember here is that we’re actually calling AppCompatDelegateImpl’s createView method. The important thing is to say three times because we’re going to use this later

As we continue to analyze the source code, we can trace the createView method in the AppCompatViewInflater class (using Button as an example, the rest of the code is cut out for now) :

final View createView(View parent, String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { ...... . View view = null; byte var12 = -1; switch(name.hashCode()) { ...... .case 2001146706:
        if (name.equals("Button")) { var12 = 2; } } switch(var12) { ...... .case 2:
        view = this.createButton(context, attrs);
        this.verifyNotNull((View)view, name);
        break; . .return (View)view;
}
Copy the code

Let’s look at the createButton method:

@NonNull
protected AppCompatButton createButton(Context context, AttributeSet attrs) {
    return new AppCompatButton(context, attrs);
}
Copy the code

So we see that eventually we’re replacing our Button with an AppCompatButton.

2.2 Self-defining Factory2

Let’s take a look at Factory2’s onCreateView method and implement a custom Factory2 class instead of using the system’s own:


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {

    LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
        //'This method is inside the Factory interface, because Factory2 inherits from Factory.'
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            returnnull; } / /'This method is a method defined in Factory2'
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            Log.e(TAG, "parent:" + parent + ",name = " + name);
            int n = attrs.getAttributeCount();
            for (int i = 0; i < n; i++) {
                Log.e(TAG, attrs.getAttributeName(i) + "," + attrs.getAttributeValue(i));
            }
            returnnull; }}); super.onCreate(savedInstanceState);setContentView(R.layout.activity_test1);
}

Copy the code

We can see that Factory2’s onCreateView attribute parent refers to the parent View object, name is the XML name of the current View, and attrs contains the View attribute name and value.

After printing, we can see that we have printed out the three controls that were written in the Layout Layout in our demo.

. . . E: parent:android.widget.LinearLayout{4e37f38 V.E... },name = Button E: layout_width , -2 E: layout_height , -2 E: text , button E: parent:android.widget.LinearLayout{4e37f38 V.E... },name = TextView E: layout_width , -2 E: layout_height , -2 E: text , textview E: parent:android.widget.LinearLayout{4e37f38 V.E... },name = ImageView E: layout_width , -2 E: layout_height , -2 E: src , @2131361792Copy the code

This is exactly the value of the control we set in Layout. We know that in the onCreateView method, we can get the current View’s content, and we can replace the controls in our demo in a way that the system can replace AppCompatXXX controls, plus this code:

LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return null;
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        
        //'We're replacing the View that was passed in here.'
        //"TextView, ImageView, Button."
        if(name.equals("TextView") || name.equals("ImageView")){
            Button button = new Button(context, attrs);
            return button;
        }

        returnnull; }});Copy the code

We can see the effect:

We know that in onCreateView, you can see all the View names and property arguments that are iterated over, and you can also change the return value here to replace it.

However, we know that the system is replacing AppCompatXXX controls with compatibility. If we can replace TextView and ImageView with Button, then the system will not set Factory2 because we already set it. It doesn’t automatically make us appcompatButtons, it just makes three buttons.

So we can’t just blindly use our Factory2, so we use the way the system eventually builds the View, only to change the parameters before it builds, which will still run the system’s code.

The onCreateView method that is called after Factory2 is finally set is basically the createView method that calls AppCompatDelegateImpl.

So we can modify the parameters of the corresponding control and then re-generate the View from the createView method of AppCompatDelegateImpl so that the compatibility is still there.

So we’ll change the code here to:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {

    LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
        //'This method is inside the Factory interface, because Factory2 inherits from Factory.'
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            returnnull; } / /'This method is a method defined in Factory2'
        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            if(name.equals("TextView") || name.equals("ImageView")){
                name = "Button"; } / /'We're just changing the parameters, but the logic to instantiate the View is still up to AppCompatDelegateImpl'
            AppCompatDelegate delegate = getDelegate();
            View view = delegate.createView(parent, name, context, attrs);
            returnview; }}); super.onCreate(savedInstanceState);setContentView(R.layout.activity_test1);
}

Copy the code

We can finally see:

Buttons do become appcompatButtons.

Summary: Setting up Factory2 is more like running the onCreateView method before the system fills the View, and then modifying the View in this method before it fills.


3. Use in actual projects

In fact, I have seen some articles before, saying that suddenly you want to replace the whole Button to TextView, so that it is more convenient, but I personally prefer to change the whole control directly in XML file, because generally an app is developed by a team, and then you do this, and when others maintain it later, After reading XML, I was surprised, and I personally felt it was inconvenient to maintain it later.

So HERE are a few common features:

3.1. Global replacement font and other properties

Because font is a property of TextView, to add a property, we don’t need to change the whole layout, just go to our onCreateView, find a TextView, and set the font.

public static Typeface typeface;
@Override
protected void onCreate(Bundle savedInstanceState)
{
    if (typeface == null){
        typeface = Typeface.createFromAsset(getAssets(), "xxxx.ttf");
    }
    LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), new LayoutInflater.Factory2() {
        @Override
        public View onCreateView(String name, Context context, AttributeSet attrs) {
            return null;
        }

        @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            AppCompatDelegate delegate = getDelegate();
            View view = delegate.createView(parent, name, context, attrs);

            if( view! = null && (view instanceof TextView)){ ((TextView) view).setTypeface(typeface); }returnview; }}); super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);
}
Copy the code

3.2 Dynamic skin changing function

This dynamic skin function is also available on the web, but the basic principle is the same, also using our knowledge of this article, and similar to the above font change, we can identify the marked View, and then when onCreateView traverses it, change some of its properties, such as the background color, etc. And then you hand it over to the system to generate the View.

Specific can refer to: Android dynamic skin principle analysis and practice

3.3 Set values directly in XML without writing Shape and selector

Estimated front time everyone in Denver has seen this article:

You don’t have to customize your View, so let’s completely free up our Shape, selector

If you want to set the Angle of a control or something like that, you don’t have to write a specific shape or selector file, you just write it in XML:

Isn’t it amazing at first glance? what amazing !!

In the onCreateView method, check attrs parameter name, for example, find the stroke_color attribute we made. Let’s look at some of its code. Let’s look directly at the onCreateView method:

@Override public View onCreateView(String name, Context context, AttributeSet attrs) { ...... .if (typedArray.getBoolean(R.styleable.background_ripple_enable, false) &&
        typedArray.hasValue(R.styleable.background_ripple_color)) {
        int color = typedArray.getColor(R.styleable.background_ripple_color, 0);
            if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                Drawable contentDrawable = (stateListDrawable == null ? drawable : stateListDrawable);
                RippleDrawable rippleDrawable = new RippleDrawable(ColorStateList.valueOf(color), contentDrawable, contentDrawable);
                view.setClickable(true);
                view.setBackground(rippleDrawable);
            } else {
                StateListDrawable tmpDrawable = new StateListDrawable();
                GradientDrawable unPressDrawable = DrawableFactory.getDrawable(typedArray);
                unPressDrawable.setColor(color);
                tmpDrawable.addState(new int[]{-android.R.attr.state_pressed}, drawable);
                tmpDrawable.addState(new int[]{android.R.attr.state_pressed}, unPressDrawable);
                view.setClickable(true); view.setBackground(tmpDrawable); }}returnview; . . }Copy the code

If you look at it this way, you will basically understand the principle, so that you can look at its library, or add what their own specific attributes, have the ability to modify their own.

3.4 XXXXX

Still have the use of a lot of whimsy of course, want everybody imagination enough much only, can do all sorts of SAO operation among this.


Conclusion:

Carelessly finish the article water……. Welcome to point out any mistakes.