Android-skin-support 换肤原理全面解析,2024年最新android实战开发教程

a.recycle();

}

}

}

class AppCompatTextHelper {

@SuppressLint(“NewApi”)

void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {

final Context context = mView.getContext();

final AppCompatDrawableManager drawableManager = AppCompatDrawableManager.get();

// First read the TextAppearance style id

TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,

R.styleable.AppCompatTextHelper, defStyleAttr, 0);

final int ap = a.getResourceId(R.styleable.AppCompatTextHelper_android_textAppearance, -1);

// Now read the compound drawable and grab any tints

if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableLeft)) {

mDrawableLeftTint = createTintInfo(context, drawableManager,

a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableLeft, 0));

}

if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableTop)) {

mDrawableTopTint = createTintInfo(context, drawableManager,

a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableTop, 0));

}

if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableRight)) {

mDrawableRightTint = createTintInfo(context, drawableManager,

a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableRight, 0));

}

if (a.hasValue(R.styleable.AppCompatTextHelper_android_drawableBottom)) {

mDrawableBottomTint = createTintInfo(context, drawableManager,

a.getResourceId(R.styleable.AppCompatTextHelper_android_drawableBottom, 0));

}

a.recycle();

// PasswordTransformationMethod wipes out all other TransformationMethod instances

// in TextView’s constructor, so we should only set a new transformation method

// if we don’t have a PasswordTransformationMethod currently…

final boolean hasPwdTm =

mView.getTransformationMethod() instanceof PasswordTransformationMethod;

boolean allCaps = false;

boolean allCapsSet = false;

ColorStateList textColor = null;

ColorStateList textColorHint = null;

ColorStateList textColorLink = null;

// First check TextAppearance’s textAllCaps value

if (ap != -1) {

a = TintTypedArray.obtainStyledAttributes(context, ap, R.styleable.TextAppearance);

if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) {

allCapsSet = true;

allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false);

}

updateTypefaceAndStyle(context, a);

if (Build.VERSION.SDK_INT < 23) {

// If we’re running on < API 23, the text color may contain theme references

// so let’s re-set using our own inflater

if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {

textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor);

}

if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) {

textColorHint = a.getColorStateList(

R.styleable.TextAppearance_android_textColorHint);

}

if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) {

textColorLink = a.getColorStateList(

R.styleable.TextAppearance_android_textColorLink);

}

}

a.recycle();

}

// Now read the style’s values

a = TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.TextAppearance,

defStyleAttr, 0);

if (!hasPwdTm && a.hasValue(R.styleable.TextAppearance_textAllCaps)) {

allCapsSet = true;

allCaps = a.getBoolean(R.styleable.TextAppearance_textAllCaps, false);

}

if (Build.VERSION.SDK_INT < 23) {

// If we’re running on < API 23, the text color may contain theme references

// so let’s re-set using our own inflater

if (a.hasValue(R.styleable.TextAppearance_android_textColor)) {

textColor = a.getColorStateList(R.styleable.TextAppearance_android_textColor);

}

if (a.hasValue(R.styleable.TextAppearance_android_textColorHint)) {

textColorHint = a.getColorStateList(

R.styleable.TextAppearance_android_textColorHint);

}

if (a.hasValue(R.styleable.TextAppearance_android_textColorLink)) {

textColorLink = a.getColorStateList(

R.styleable.TextAppearance_android_textColorLink);

}

}

updateTypefaceAndStyle(context, a);

a.recycle();

if (textColor != null) {

mView.setTextColor(textColor);

}

if (textColorHint != null) {

mView.setHintTextColor(textColorHint);

}

if (textColorLink != null) {

mView.setLinkTextColor(textColorLink);

}

if (!hasPwdTm && allCapsSet) {

setAllCaps(allCaps);

}

if (mFontTypeface != null) {

mView.setTypeface(mFontTypeface, mStyle);

}

mAutoSizeTextHelper.loadFromAttributes(attrs, defStyleAttr);

if (PLATFORM_SUPPORTS_AUTOSIZE) {

// Delegate auto-size functionality to the framework implementation.

if (mAutoSizeTextHelper.getAutoSizeTextType()

!= TextViewCompat.AUTO_SIZE_TEXT_TYPE_NONE) {

final int[] autoSizeTextSizesInPx =

mAutoSizeTextHelper.getAutoSizeTextAvailableSizes();

if (autoSizeTextSizesInPx.length > 0) {

if (mView.getAutoSizeStepGranularity() != AppCompatTextViewAutoSizeHelper

.UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {

// Configured with granularity, preserve details.

mView.setAutoSizeTextTypeUniformWithConfiguration(

mAutoSizeTextHelper.getAutoSizeMinTextSize(),

mAutoSizeTextHelper.getAutoSizeMaxTextSize(),

mAutoSizeTextHelper.getAutoSizeStepGranularity(),

TypedValue.COMPLEX_UNIT_PX);

} else {

mView.setAutoSizeTextTypeUniformWithPresetSizes(

autoSizeTextSizesInPx, TypedValue.COMPLEX_UNIT_PX);

}

}

}

}

}

这里的这里,不得不说Android源码真是让在下佩服的五体投地,又一次体现了单一职责原则.你问我在哪里? 系统将背景相关的交给AppCompatBackgroundHelper去处理,将文字相关的交给AppCompatTextHelper处理.

AppCompatBackgroundHelper和AppCompatTextHelper拿到了xml中定义的属性的值之后,将其值赋值给控件.就是这么简单.

看到了这里,预备知识就介绍得差不多了,看了半天,你说的这些乱七八糟的东西与我的换肤有个毛关系啊?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V0PhJfle-1600323763201)(http://olg7c0d2n.bkt.clouddn.com/2018%E5%B9%B47%E6%9C%8829%E6%97%A5155555.jpg)]

请各位看官放下手中的砖头,且听贫道细细道来.

五.换肤原理详细解析


1.上文预备知识与换肤的关系

源码中可以通过拦截View创建过程, 替换一些基础的组件(比如TextView -> AppCompatTextView), 然后对一些特殊的属性(比如:background, textColor) 做处理, 那我们为什么不能将这种思想拿到换肤框架中来使用呢?我擦,一语惊醒梦中人啊,老哥.我们也可以搞一个委托啊,我们也可以搞一个类似于AppCompatViewInflater的控件加载器啊,我们也可以设置mFactory2啊,相当于创建View的过程由我们接手.既然我们接手了,那岂不是对所有控件都可以为所欲为???那是当然啦. 既然都可以为所欲为了,那换个肤算什么,so easy.

2.源码一,创建控件全过程

SkinCompatManager.withoutActivity(application)

.addInflater(new SkinAppCompatViewInflater());

首先我们从库的初始化处着手,这里将Application传入,又添加了一个SkinAppCompatViewInflater,SkinAppCompatViewInflater其实就是用来创建View的,和系统的AppCompatViewInflater差不多.我们来看看withoutActivity(application)做了什么.

//SkinCompatManager.java

public static SkinCompatManager withoutActivity(Application application) {

init(application);

SkinActivityLifecycle.init(application);

return sInstance;

}

//SkinActivityLifecycle.java

public static SkinActivityLifecycle init(Application application) {

if (sInstance == null) {

synchronized (SkinActivityLifecycle.class) {

if (sInstance == null) {

sInstance = new SkinActivityLifecycle(application);

}

}

}

return sInstance;

}

private SkinActivityLifecycle(Application application) {

//就是这里,注册了ActivityLifecycleCallbacks,可以监听所有Activity的生命周期

application.registerActivityLifecycleCallbacks(this);

//这个方法稍后看

installLayoutFactory(application);

SkinCompatManager.getInstance().addObserver(getObserver(application));

}

可以看到,初始化时在SkinActivityLifecycle中其实就注册了ActivityLifecycleCallbacks,现在我们可以监听app所有Activity的生命周期.

来看看SkinActivityLifecycle中监听到Activity的onCreate()方法时干了什么

@Override

public void onActivityCreated(Activity activity, Bundle savedInstanceState) {

//判断是否需要换肤 这个可以外部初始化时控制

if (isContextSkinEnable(activity)) {

//在Activity创建的时候,直接将Factory设置成三方库里面的

installLayoutFactory(activity);

//更新状态栏颜色

updateStatusBarColor(activity);

//更新window背景颜色

updateWindowBackground(activity);

if (activity instanceof SkinCompatSupportable) {

((SkinCompatSupportable) activity).applySkin();

}

}

}

/**

  • 设置Factory(创建View的工厂)

*/

private void installLayoutFactory(Context context) {

LayoutInflater layoutInflater = LayoutInflater.from(context);

try {

//setFactory只能调用一次,用于设置Factory(创建View), 设置了Factory了mFactorySet就会是true

//如果需要重新设置Factory,则需要先将mFactorySet设置为false,不然系统判断到mFactorySet是true则会抛异常.

//这里使用自己构建的Factory去创建View,在创建View时当然也就可以控制它的背景或者文字颜色.

//(在这里之前需要知道哪些控件需要换肤,其中一部分是继承自三方库的控件,这些控件是实现了SkinCompatSupportable接口的,可以很方便的控制.

// 还有一部分是系统的控件,在创建时直接创建三方库中的控件(比如View就创建SkinCompatView).

// 在设置系统控件的背景颜色和文字颜色时,直接从三方库缓存颜色中取值,然后进行设置.)

Field field = LayoutInflater.class.getDeclaredField(“mFactorySet”);

field.setAccessible(true);

field.setBoolean(layoutInflater, false);

LayoutInflaterCompat.setFactory(layoutInflater, getSkinDelegate(context));

} catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException e) {

e.printStackTrace();

}

}

在我们的Activity创建的时候,首先判断一下是否需要换肤,需要换肤才去搞.

我们重点看看installLayoutFactory()方法,在上面的预备知识中说了mFactory只能设置一次,不然就要抛异常,所以需要先利用发射将mFactorySet的值设置为false才不会抛异常.然后才能setFactory().

下面我们来看看setFactory()的第二个参数创建过程,第二个参数其实是一个创建View的工厂.

//SkinActivityLifecycle.java

private SkinCompatDelegate getSkinDelegate(Context context) {

if (mSkinDelegateMap == null) {

mSkinDelegateMap = new WeakHashMap<>();

}

SkinCompatDelegate mSkinDelegate = mSkinDelegateMap.get(context);

if (mSkinDelegate == null) {

mSkinDelegate = SkinCompatDelegate.create(context);

mSkinDelegateMap.put(context, mSkinDelegate);

}

return mSkinDelegate;

}

//SkinCompatDelegate.java

public class SkinCompatDelegate implements LayoutInflaterFactory {

private final Context mContext;

//主角 在这里 在这里!!!

private SkinCompatViewInflater mSkinCompatViewInflater;

private List<WeakReference> mSkinHelpers = new ArrayList<>();

private SkinCompatDelegate(Context context) {

mContext = context;

}

@Override

public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {

View view = createView(parent, name, context, attrs);

if (view == null) {

return null;

}

if (view instanceof SkinCompatSupportable) {

mSkinHelpers.add(new WeakReference<>((SkinCompatSupportable) view));

}

return view;

}

public View createView(View parent, final String name, @NonNull Context context,

@NonNull AttributeSet attrs) {

if (mSkinCompatViewInflater == null) {

mSkinCompatViewInflater = new SkinCompatViewInflater();

}

List wrapperList = SkinCompatManager.getInstance().getWrappers();

for (SkinWrapper wrapper : wrapperList) {

Context wrappedContext = wrapper.wrapContext(mContext, parent, attrs);

if (wrappedContext != null) {

context = wrappedContext;

}

}

return mSkinCompatViewInflater.createView(parent, name, context, attrs);

}

public static SkinCompatDelegate create(Context context) {

return new SkinCompatDelegate(context);

}

public void applySkin() {

if (mSkinHelpers != null && !mSkinHelpers.isEmpty()) {

for (WeakReference ref : mSkinHelpers) {

if (ref != null && ref.get() != null) {

((SkinCompatSupportable) ref.get()).applySkin();

}

}

}

}

}

可以看到SkinCompatDelegate是一个SkinCompatViewInflater的委托.这里其实和系统的AppCompatDelegateImplV9很类似.

当系统需要创建View的时候,就会回调SkinCompatDelegate的@Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs)方法,因为前面设置了LayoutInflater的Factory为SkinCompatDelegate. 然后SkinCompatDelegate将创建View的工作交给SkinCompatViewInflater去处理(也是和系统一模一样).

来看看SkinCompatViewInflater是如何创建View的

public final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {

View view = createViewFromHackInflater(context, name, attrs);

if (view == null) {

view = createViewFromInflater(context, name, attrs);

}

if (view == null) {

view = createViewFromTag(context, name, attrs);

}

if (view != null) {

// If we have created a view, check it’s android:onClick

checkOnClickListener(view, attrs);

}

return view;

}

private View createViewFromInflater(Context context, String name, AttributeSet attrs) {

View view = null;

//这里的SkinLayoutInflater(我理解为控件创建器)就是我们之前在初始化时设置的SkinAppCompatViewInflater

//当然,SkinLayoutInflater可以有多个

for (SkinLayoutInflater inflater : SkinCompatManager.getInstance().getInflaters()) {

view = inflater.createView(context, name, attrs);

if (view == null) {

continue;

} else {

break;

}

}

return view;

}

//这个方法和系统的完全一模一样嘛,so easy

public View createViewFromTag(Context context, String name, AttributeSet attrs) {

if (“view”.equals(name)) {

name = attrs.getAttributeValue(null, “class”);

}

try {

mConstructorArgs[0] = context;

mConstructorArgs[1] = attrs;

//自定义控件

if (-1 == name.indexOf(‘.’)) {

for (int i = 0; i < sClassPrefixList.length; i++) {

final View view = createView(context, name, sClassPrefixList[i]);

if (view != null) {

return view;

}

}

return null;

} else {

return createView(context, name, null);

}

} catch (Exception e) {

// We do not want to catch these, lets return null and let the actual LayoutInflater

// try

return null;

} finally {

// Don’t retain references on context.

mConstructorArgs[0] = null;

mConstructorArgs[1] = null;

}

}

可以看到,这些实现其实是和系统的实现是差不多的.原理已在上面的预备知识中给出.

这里也有不同的地方,createViewFromInflater()方法中利用了我们在初始化库时设置的SkinLayoutInflater(我觉得是控件创造器)去创建view.

为什么要在SkinCompatViewInflater还要细化,还需要交由更细的SkinLayoutInflater来处理呢?我觉得是因为方便扩展,库中给出了几个SkinLayoutInflater,有SkinAppCompatViewInflater(基础控件构建器)、SkinMaterialViewInflater(material design控件构造器)、SkinConstraintViewInflater(ConstraintLayout构建器)、SkinCardViewInflater(CardView v7构建器)。

由于初始化时我们设置的是SkinAppCompatViewInflater,其他的构建器都是类似的原理.我们就来看看

//SkinAppCompatViewInflater.java

@Override

public View createView(Context context, String name, AttributeSet attrs) {

View view = createViewFromFV(context, name, attrs);

if (view == null) {

view = createViewFromV7(context, name, attrs);

}

return view;

}

private View createViewFromFV(Context context, String name, AttributeSet attrs) {

View view = null;

if (name.contains(“.”)) {

return null;

}

switch (name) {

case “View”:

view = new SkinCompatView(context, attrs);

break;

case “LinearLayout”:

view = new SkinCompatLinearLayout(context, attrs);

break;

case “RelativeLayout”:

view = new SkinCompatRelativeLayout(context, attrs);

break;

case “FrameLayout”:

view = new SkinCompatFrameLayout(context, attrs);

break;

case “TextView”:

view = new SkinCompatTextView(context, attrs);

break;

case “ImageView”:

view = new SkinCompatImageView(context, attrs);

break;

case “Button”:

view = new SkinCompatButton(context, attrs);

break;

case “EditText”:

view = new SkinCompatEditText(context, attrs);

break;

default:

break;

}

return view;

}

private View createViewFromV7(Context context, String name, AttributeSet attrs) {

View view = null;

switch (name) {

case “android.support.v7.widget.Toolbar”:

view = new SkinCompatToolbar(context, attrs);

break;

default:

break;

}

return view;

}

柳暗花明又一村?这不就是之前我们在Android源码中看过的代码吗?几乎是一模一样。我们在这里将View的创建拦截,然后创建自己的控件。既然是我们自己创建的控件,想干啥还不容易么?

我们看一下SkinCompatTextView的源码

//SkinCompatTextView.java

public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {

private SkinCompatTextHelper mTextHelper;

private SkinCompatBackgroundHelper mBackgroundTintHelper;

public SkinCompatTextView(Context context) {

this(context, null);

}

public SkinCompatTextView(Context context, AttributeSet attrs) {

this(context, attrs, android.R.attr.textViewStyle);

}

public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {

super(context, attrs, defStyleAttr);

mBackgroundTintHelper = new SkinCompatBackgroundHelper(this);

mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);

mTextHelper = SkinCompatTextHelper.create(this);

mTextHelper.loadFromAttributes(attrs, defStyleAttr);

}

@Override

public void applySkin() {

if (mBackgroundTintHelper != null) {

mBackgroundTintHelper.applySkin();

}

if (mTextHelper != null) {

mTextHelper.applySkin();

}

}

}

还是那套经典的操作,将background相关的属性交给SkinCompatBackgroundHelper去处理,将textColor相关的操作交给SkinCompatTextHelper去处理。与源码中一模一样。

3.源码二,从皮肤包加载皮肤

其实皮肤包就是一个apk,只不过里面没有任何代码,只有一些需要换肤的资源或者颜色什么的.而且这些资源的名称必须和当前app中的资源名称是一致的,才能替换. 需要什么皮肤资源,直接去皮肤包里面去拿就好了.

使用方式

SkinCompatManager.getInstance().loadSkin(“night.skin”, null, CustomSDCardLoader.SKIN_LOADER_STRATEGY_SDCARD);

来吧,我们进入loadSkin()方法看一下:

/**

  • 加载皮肤包.

  • @param skinName 皮肤包名称.

  • @param listener 皮肤包加载监听.

  • @param strategy 皮肤包加载策略.

*/

public AsyncTask loadSkin(String skinName, SkinLoaderListener listener, int strategy) {

//加载策略 分为好几种:从SD卡中加载皮肤,从assets文件中加载皮肤等等

SkinLoaderStrategy loaderStrategy = mStrategyMap.get(strategy);

if (loaderStrategy == null) {

return null;

}

return new SkinLoadTask(listener, loaderStrategy).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, skinName);

}

可以看到SkinLoadTask应该是一个AsyncTask,然后在后台去解析这个皮肤包.既然是AsyncTask,那肯定看doInBackground()方法咯

我们来看看SkinLoadTask的doInBackground()

//SkinLoadTask.java

@Override

protected String doInBackground(String… params) {

try {

if (params.length == 1) {

//根据加载策略去后台加载皮肤

String skinName = mStrategy.loadSkinInBackground(mAppContext, params[0]);

if (TextUtils.isEmpty(skinName)) {

SkinCompatResources.getInstance().reset(mStrategy);

}

return params[0];

}

} catch (Exception e) {

e.printStackTrace();

}

SkinCompatResources.getInstance().reset();

return null;

}

//加载策略 随便挑一个吧 SkinSDCardLoader.java 从SD卡加载皮肤

@Override

public String loadSkinInBackground(Context context, String skinName) {

if (TextUtils.isEmpty(skinName)) {

return skinName;

}

//获取皮肤路径

String skinPkgPath = getSkinPath(context, skinName);

if (SkinFileUtils.isFileExists(skinPkgPath)) {

//获取皮肤包包名.

String pkgName = SkinCompatManager.getInstance().getSkinPackageName(skinPkgPath);

//获取皮肤包的Resources

Resources resources = SkinCompatManager.getInstance().getSkinResources(skinPkgPath);

if (resources != null && !TextUtils.isEmpty(pkgName)) {

SkinCompatResources.getInstance().setupSkin(

resources,

pkgName,

skinName,

this);

return skinName;

}

}

return null;

}

//SkinCompatManager.java

//获取皮肤包包名.

public String getSkinPackageName(String skinPkgPath) {

PackageManager mPm = mAppContext.getPackageManager();

PackageInfo info = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);

return info.packageName;

}

//获取皮肤包资源{@link Resources}.

@Nullable

public Resources getSkinResources(String skinPkgPath) {

try {

AssetManager assetManager = AssetManager.class.newInstance();

Method addAssetPath = assetManager.getClass().getMethod(“addAssetPath”, String.class);

addAssetPath.invoke(assetManager, skinPkgPath);

Resources superRes = mAppContext.getResources();

return new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

大概就是去子线程获取皮肤包的包名和Resources(要这个干啥?后面我们需要获取皮肤包中的颜色或者资源时需要通过这个进行获取).

SkinCompatResources.getInstance().setupSkin()方法中就是将这些从皮肤包中加载的Resources,包名,皮肤名,加载策略全部存下来.有了这些东西,待会儿就能取皮肤包里面的资源了.

库中定义的控件都是实现了SkinCompatSupportable接口的,方便控制换肤。比如SkinCompatTextView的applySkin()方法中调用了BackgroundTintHelper和TextHelper的applySkin()方法,就是说换肤时会去动态的更换背景或文字颜色什么的。我们来看看mBackgroundTintHelper.applySkin()的实现

//SkinCompatBackgroundHelper.java

@Override

public void applySkin() {

//该控件是否有背景 检测

mBackgroundResId = checkResourceId(mBackgroundResId);

if (mBackgroundResId == INVALID_ID) {

return;

}

Drawable drawable = SkinCompatVectorResources.getDrawableCompat(mView.getContext(), mBackgroundResId);

if (drawable != null) {

int paddingLeft = mView.getPaddingLeft();

int paddingTop = mView.getPaddingTop();

int paddingRight = mView.getPaddingRight();

int paddingBottom = mView.getPaddingBottom();

ViewCompat.setBackground(mView, drawable);

mView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);

}

}

就是获取drawable然后给view设置背景嘛.关键在于这里的获取drawable是怎么实现的.来看看具体实现

//SkinCompatVectorResources.java

private Drawable getSkinDrawableCompat(Context context, int resId) {

//当前是非默认皮肤

if (!SkinCompatResources.getInstance().isDefaultSkin()) {

try {

return SkinCompatDrawableManager.get().getDrawable(context, resId);

} catch (Exception e) {

e.printStackTrace();

}

}

return AppCompatResources.getDrawable(context, resId);

}

//SkinCompatDrawableManager.java

public Drawable getDrawable(@NonNull Context context, @DrawableRes int resId) {

return getDrawable(context, resId, false);

}

Drawable getDrawable(@NonNull Context context, @DrawableRes int resId,

boolean failIfNotKnown) {

//检查Drawable是否能被正确解码

checkVectorDrawableSetup(context);

Drawable drawable = loadDrawableFromDelegates(context, resId);

if (drawable == null) {

drawable = createDrawableIfNeeded(context, resId);

}

if (drawable == null) {

//这里是关键

drawable = SkinCompatResources.getDrawable(context, resId);

}

if (drawable != null) {

// Tint it if needed

drawable = tintDrawable(context, resId, failIfNotKnown, drawable);

}

if (drawable != null) {

// See if we need to ‘fix’ the drawable

SkinCompatDrawableUtils.fixDrawable(drawable);

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

尾声

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

  • 思维脑图
  • 性能优化学习笔记


  • 性能优化视频

    当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**
[外链图片转存中…(img-6xcai9v9-1712610573880)]
[外链图片转存中…(img-XK6jaIEk-1712610573881)]
[外链图片转存中…(img-MBwRWEuq-1712610573881)]
[外链图片转存中…(img-bgobg0vH-1712610573881)]
[外链图片转存中…(img-2F6RqDmZ-1712610573882)]
[外链图片转存中…(img-KrTTx6dz-1712610573882)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-qBQ81qiC-1712610573883)]

尾声

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

  • 思维脑图
    [外链图片转存中…(img-H5aLfQoJ-1712610573883)]
  • 性能优化学习笔记
    [外链图片转存中…(img-cEWU9nzy-1712610573883)]
    [外链图片转存中…(img-1wR66BjE-1712610573884)]

[外链图片转存中…(img-lGm4LQz3-1712610573884)]
[外链图片转存中…(img-3VFmSh2S-1712610573884)]

  • 性能优化视频
    [外链图片转存中…(img-v9wYd9qg-1712610573885)]
    当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-jmL9zaNA-1712610573885)]

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值