源码解读,support v4、v7包是如何实现向下兼容的

写在前面

上一篇文章 Android 字体国际化适配方法以及源码解析
讲到了如何 适配各国语言长度不同的问题。

为了兼容低版本用到了V4 包。

用了那么久的v4包,但他是如何做到乡下兼容的呢?

这次通过同一个适配的小例子来查看一下 。v4包是如何做到向下兼容的。

目的

  • 了解v4 v7 是如何实现向下兼容的
  • AppCompatActivity是如何在Activity的基础上实现扩展的

看源码

为了适配字体,我们用了v4包下的这些方法


public static void setAutoSizeTextTypeWithDefaults(
           TextView textView,
           int autoSizeTextType) 设置字体类型


public static void setAutoSizeTextTypeUniformWithConfiguration(
            TextView textView,
            int autoSizeMinTextSize,
            int autoSizeMaxTextSize,
            int autoSizeStepGranularity, 设置颗粒度 字体变大变小的幅度,默认是1
            int unit)

public static void setAutoSizeTextTypeUniformWithPresetSizes(
            TextView textView,
            @NonNull int[] presetSizes,  设置字体预设值,字体的变化只会在预设的几个值内变化
            int unit)

并且在上一篇中详细的讲解了 setAutoSizeTextTypeUniformWithConfiguration 方法是如何做到适配的。
但是 我们在上一篇中 是讲解的 TextView 类中的 setAutoSizeTextTypeUniformWithConfiguration 方法,是如何适配的
其实TextView中的方法在低于sdk-26的时候是不会调用的。
但是在v4包中 有一个也叫作setAutoSizeTextTypeUniformWithConfiguration 的方法和TextView中是一样的。

**如何适配呢。必须是android 包中有该方法,但某些版本不适用,所以在v4 v7包中,实现一个 方法原理 一样的 方法。供低版本使用。 马上就想到了 委派模式 **

首先来看这个方法

这个是v4包中的 字体适配的方法。
    public static void setAutoSizeTextTypeUniformWithConfiguration(
            TextView textView,
            int autoSizeMinTextSize,
            int autoSizeMaxTextSize,
            int autoSizeStepGranularity,
            int unit) throws IllegalArgumentException {
        IMPL.setAutoSizeTextTypeUniformWithConfiguration(textView, autoSizeMinTextSize,
                autoSizeMaxTextSize, autoSizeStepGranularity, unit);
    }

在看IMPL



    static final TextViewCompatBaseImpl IMPL;

    static {
        if (Build.VERSION.SDK_INT >= 26) {
            IMPL = new TextViewCompatApi26Impl();
        } else if (Build.VERSION.SDK_INT >= 23) {
            IMPL = new TextViewCompatApi23Impl();
        } else if (Build.VERSION.SDK_INT >= 18) {
            IMPL = new TextViewCompatApi18Impl();
        } else if (Build.VERSION.SDK_INT >= 17) {
            IMPL = new TextViewCompatApi17Impl();
        } else if (Build.VERSION.SDK_INT >= 16) {
            IMPL = new TextViewCompatApi16Impl();
        } else {
            IMPL = new TextViewCompatBaseImpl();
        }
    }

上面代码中 TextViewCompatApi26Impl 方法继承自TextViewCompatApi23Impl ,然后所有的方法都是继承自 TextViewCompatBaseImpl

先看TextViewCompatApi26Impl 方法


    @RequiresApi(26)
    static class TextViewCompatApi26Impl extends TextViewCompatApi23Impl {

      ...省略

        @Override
        public void setAutoSizeTextTypeUniformWithConfiguration(
                TextView textView,
                int autoSizeMinTextSize,
                int autoSizeMaxTextSize,
                int autoSizeStepGranularity,
                int unit) throws IllegalArgumentException {
            textView.setAutoSizeTextTypeUniformWithConfiguration(
                    autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
        }

       ...省略
    }


可见该类继承自TextViewCompatApi23Impl
这里有我们前面说的那三个方法。但是改方法只能在sdk大于26 才会使用

假如我们的sdk版本号不大于26呢?
我们不断的看 TextViewCompatApi26Impl 的父类。最终查看到
TextViewCompatBaseImpl 方法


    static class TextViewCompatBaseImpl {
        ...省略
        public void setAutoSizeTextTypeUniformWithConfiguration(
                TextView textView,
                int autoSizeMinTextSize,
                int autoSizeMaxTextSize,
                int autoSizeStepGranularity,
                int unit) throws IllegalArgumentException {
            if (textView instanceof AutoSizeableTextView) {
                ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithConfiguration(
                        autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
            }
        }

         ...省略
    }

在这里也发现了setAutoSizeTextTypeUniformWithConfiguration 方法。继续看他的方法。

if (textView instanceof AutoSizeableTextView) {
                ((AutoSizeableTextView) textView).setAutoSizeTextTypeUniformWithPresetSizes(
                        presetSizes, unit);
            }
//判断textview 是否是实现过 AutoSizeableTextView 方法

AutoSizeableTextView 是一个借口类

不要慌,我们全局查看 AutoSizeableTextView 方法
在AutoSizeableTextView 方法中,右键,选择Find Usages

这里写图片描述

发现在三个类里面使用过。
TextViewCompat 方法 不需要看了。

下面看AppCompatTextView 方法 (support.v7下的)

public class AppCompatTextView extends TextView implements TintableBackgroundView,
        AutoSizeableTextView {
        ...省略


    /**
     * This should be accessed via
     * {@link android.support.v4.widget.TextViewCompat#setAutoSizeTextTypeUniformWithConfiguration(
     *        TextView, int, int, int, int)}
     *
     * @hide
     */
    @RestrictTo(LIBRARY_GROUP)
    @Override
    public void setAutoSizeTextTypeUniformWithConfiguration(
            int autoSizeMinTextSize,
            int autoSizeMaxTextSize,
            int autoSizeStepGranularity,
            int unit) throws IllegalArgumentException {
        if (Build.VERSION.SDK_INT >= 26) {
            super.setAutoSizeTextTypeUniformWithConfiguration(
                    autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
        } else {
            if (mTextHelper != null) {
                mTextHelper.setAutoSizeTextTypeUniformWithConfiguration(
                        autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
            }
        }
    }

        ...省略
}

这里就很明朗了。如果sdk大于26,使用TextView中的方法。如果不大于26.使用mTextHelper 下的方法


@RequiresApi(9)
class AppCompatTextHelper {
       ...省略
    void setAutoSizeTextTypeUniformWithConfiguration(
            int autoSizeMinTextSize,
            int autoSizeMaxTextSize,
            int autoSizeStepGranularity,
            int unit) throws IllegalArgumentException {
        mAutoSizeTextHelper.setAutoSizeTextTypeUniformWithConfiguration(
                autoSizeMinTextSize, autoSizeMaxTextSize, autoSizeStepGranularity, unit);
    }
        ...省略
}


class AppCompatTextViewAutoSizeHelper {
        ...省略
 @RestrictTo(LIBRARY_GROUP)
    void setAutoSizeTextTypeUniformWithConfiguration(
            int autoSizeMinTextSize,
            int autoSizeMaxTextSize,
            int autoSizeStepGranularity,
            int unit) throws IllegalArgumentException {
        if (supportsAutoSizeText()) {
            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
            final float autoSizeMinTextSizeInPx = TypedValue.applyDimension(
                    unit, autoSizeMinTextSize, displayMetrics);
            final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
                    unit, autoSizeMaxTextSize, displayMetrics);
            final float autoSizeStepGranularityInPx = TypedValue.applyDimension(
                    unit, autoSizeStepGranularity, displayMetrics);

            validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx,
                    autoSizeMaxTextSizeInPx,
                    autoSizeStepGranularityInPx);
            if (setupAutoSizeText()) {
                autoSizeText();
            }
        }
    }
        ...省略
}

看到这里我们就发现和TextView 中的方法是一样的了。如果想跟进一步了解接下来代码实现的原理。可以查看上一篇
Android 字体国际化适配方法以及源码解析

到这里我们肯定有一个疑惑。这个方法是在 AppCompatTextView 中。我们有没有使用这个方法。

我们全局搜一下 AppCompatTextView 方法


这里写图片描述

我们查看该方法

class AppCompatViewInflater {

        ...省略

    public final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
        final Context originalContext = context;

        // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
        // by using the parent's context
        if (inheritContext && parent != null) {
            context = parent.getContext();
        }
        if (readAndroidTheme || readAppTheme) {
            // We then apply the theme on the context, if specified
            context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
        }
        if (wrapContext) {
            context = TintContextWrapper.wrap(context);
        }

        View view = null;

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            case "Spinner":
                view = new AppCompatSpinner(context, attrs);
                break;
            case "ImageButton":
                view = new AppCompatImageButton(context, attrs);
                break;
            case "CheckBox":
                view = new AppCompatCheckBox(context, attrs);
                break;
            case "RadioButton":
                view = new AppCompatRadioButton(context, attrs);
                break;
            case "CheckedTextView":
                view = new AppCompatCheckedTextView(context, attrs);
                break;
            case "AutoCompleteTextView":
                view = new AppCompatAutoCompleteTextView(context, attrs);
                break;
            case "MultiAutoCompleteTextView":
                view = new AppCompatMultiAutoCompleteTextView(context, attrs);
                break;
            case "RatingBar":
                view = new AppCompatRatingBar(context, attrs);
                break;
            case "SeekBar":
                view = new AppCompatSeekBar(context, attrs);
                break;
        }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            view = createViewFromTag(context, name, attrs);
        }

        if (view != null) {
            // If we have created a view, check its android:onClick
            checkOnClickListener(view, attrs);
        }

        return view;
    }

        ...省略
}

我看到这里突然感觉豁然开朗。

原来那么多控件都是在这里new 出来的。

到这里基本能理解 是v4 v7包是如何做到向下兼容的了。

但还不过瘾,直觉告诉我这个肯定跟AppCompatActivity 有关系。

我们在全局搜索这个 createView 方法


@RequiresApi(14)
class AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
        implements MenuBuilder.Callback, LayoutInflater.Factory2 {

    ...省略

    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        if (mAppCompatViewInflater == null) {
            mAppCompatViewInflater = new AppCompatViewInflater();
        }

        boolean inheritContext = false;
        if (IS_PRE_LOLLIPOP) {
            inheritContext = (attrs instanceof XmlPullParser)
                    // If we have a XmlPullParser, we can detect where we are in the layout
                    ? ((XmlPullParser) attrs).getDepth() > 1
                    // Otherwise we have to use the old heuristic
                    : shouldInheritContext((ViewParent) parent);
        }

        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
    }
            ...省略

}

AppCompatDelegateImplV9 感觉像是最低版本支持sdk9 的样子 是一个 委派
看看哪里调用该方法呢?

public abstract class AppCompatDelegate {

    ...省略
 /**
     * Create a {@link android.support.v7.app.AppCompatDelegate} to use with {@code activity}.
     *
     * @param callback An optional callback for AppCompat specific events
     * 用于扩展activity
     */
    public static AppCompatDelegate create(Activity activity, AppCompatCallback callback) {
        return create(activity, activity.getWindow(), callback);
    }


    /**
     * Create a {@link android.support.v7.app.AppCompatDelegate} to use with {@code dialog}.
     *  用于扩展dialog
     * @param callback An optional callback for AppCompat specific events
     */
    public static AppCompatDelegate create(Dialog dialog, AppCompatCallback callback) {
        return create(dialog.getContext(), dialog.getWindow(), callback);
    }

    private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        if (Build.VERSION.SDK_INT >= 24) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (Build.VERSION.SDK_INT >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV9(context, window, callback);
        }
    }

    //到这里我们又发现了sdk版本号的判断。和之前在TextViewCompact方法中遇到的是一样的

    ...省略

}

AppCompatDelegate 类是一个 委派类,用来实现activity的扩展。
我们继续查看这个AppCompatDelegate 下的create 在哪里用

public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
        TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {

    ...省略

    private AppCompatDelegate mDelegate;

    @SuppressWarnings("TypeParameterUnusedInFormals")
    @Override
    public <T extends View> T findViewById(@IdRes int id) {
        return getDelegate().findViewById(id);
    }
    /**
     * @return The {@link AppCompatDelegate} being used by this Activity.
     */
    @NonNull
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }

    ...省略

    }
  • 可以发现这个方法 在AppCompatActivity 中使用了。并且每次findviewbyid都会调用。
  • 不仅如此 生命周期中也会调用getDelegate() 方法。

这个时候我们发现 如果我们创建的Activity 如果不继承AppCompatActivity的话 好多控件的方法都无法做到向下兼容。

总结

如果你跟着一步步的读完,很佩服你的毅力。

但并没什么暖用,还是得需要你自己合上网页,自己去androidstudio中,根据我的思路 看一篇源码,这样才有收获。

同时这里面使用了 委派模式 来巧妙的 完成了。

推荐阅读更多精彩内容