热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Android两行代码实现换肤从appcompatv7原理出发

背景换肤方案原理在网上已经很多了,这里不再详细描述,强迫症的我总是想让提供给别人使用的SDK尽量好用,哪怕是给自己带来额外的工作量,经过一段时间的奋斗,实现了一个自我感觉良好的换肤

背景

换肤方案原理在网上已经很多了, 这里不再详细描述, 强迫症的我总是想让提供给别人使用的SDK尽量好用, 哪怕是给自己带来额外的工作量, 经过一段时间的奋斗, 实现了一个自我感觉良好的换肤框架.

这里主要来看看Android 源码中”com.android.support:appcompat-v7”包的实现, 以及源码思想在Android-skin-support中的应用 – 如何打造一款好用的换肤框架.


appcompat-v7包实现

首先来看一下源码的实现: 
AppCompatActivity源码

public class AppCompatActivity extends FragmentActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {final AppCompatDelegate delegate = getDelegate();delegate.installViewFactory();delegate.onCreate(savedInstanceState);...}@Overridepublic MenuInflater getMenuInflater() {return getDelegate().getMenuInflater();}@Overridepublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);}@Overridepublic void setContentView(View view) {getDelegate().setContentView(view);}....
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

AppCompatActivity 将大部分生命周期委托给了AppCompatDelegate

再看看相关的类图 
这里写图片描述

AppCompateDelegate的子类AppCompatDelegateImplV9

class AppCompatDelegateImplV9 extends AppCompatDelegateImplBaseimplements MenuBuilder.Callback, LayoutInflaterFactory {@Overridepublic void installViewFactory() {LayoutInflater layoutInflater = LayoutInflater.from(mContext);if (layoutInflater.getFactory() == null) {LayoutInflaterCompat.setFactory(layoutInflater, this);} else {if (!(LayoutInflaterCompat.getFactory(layoutInflater)instanceof AppCompatDelegateImplV9)) {Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"+ " so we can not install AppCompat's");}}}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

从这可以看出通过实现LayoutInflaterFactory接口来实现换肤至少可以支持到api 9以上

网上很多换肤框架的实现, 通过LayoutInflater.setFactory的方式, 在回调的onCreateView中解析每一个View的attrs, 判断是否有已标记需要换肤的属性, 比方说background, textColor, 或者说相应资源是否为skin_开头等等. 
然后保存到map中, 对每一个View做for循环去遍历所有的attr, 想要对更多的属性进行换肤, 需要Activity实现接口, 将需要换肤的View, 以及相应的属性收集到一起 
那么是不是能够寻求一种让使用者更方便的方式来实现, 做一个侵入性尽量小的框架呢?

本着开发者应有的好奇心, 深入的研究了一些v7包的实现 
onCreate
setContentView
AppCompatDelegateImplV9中, 在LayoutInflaterFactory的接口方法onCreateView 中将View的创建交给了AppCompatViewInflater

@Override
public final View onCreateView(View parent, String name,Context context, AttributeSet attrs) {// First let the Activity's Factory try and inflate the viewfinal View view = callActivityOnCreateView(parent, name, context, attrs);if (view != null) {return view;}// If the Factory didn't handle it, let our createView() method tryreturn createView(parent, name, context, attrs);
}@Override
public View createView(View parent, final String name, &#64;NonNull Context context,&#64;NonNull AttributeSet attrs) {final boolean isPre21 &#61; Build.VERSION.SDK_INT <21;if (mAppCompatViewInflater &#61;&#61; null) {mAppCompatViewInflater &#61; new AppCompatViewInflater();}// We only want the View to inherit its context if we&#39;re running pre-v21final boolean inheritContext &#61; isPre21 && shouldInheritContext((ViewParent) parent);return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,isPre21, /* Only read android:theme pre-L (L&#43; 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 */);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

再来看一下AppCompatViewInflater中createView的实现

public final View createView(View parent, final String name, &#64;NonNull Context context,&#64;NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {......View view &#61; null;switch (name) {case "TextView":view &#61; new AppCompatTextView(context, attrs);break;case "ImageView":view &#61; new AppCompatImageView(context, attrs);break;case "Button":view &#61; new AppCompatButton(context, attrs);break;case "EditText":view &#61; new AppCompatEditText(context, attrs);break;case "Spinner":view &#61; new AppCompatSpinner(context, attrs);break;case "ImageButton":view &#61; new AppCompatImageButton(context, attrs);break;case "CheckBox":view &#61; new AppCompatCheckBox(context, attrs);break;......}......return view;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

再看一下其中一个类AppCompatTextView的实现

public class AppCompatTextView extends TextView implements TintableBackgroundView {public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(TintContextWrapper.wrap(context), attrs, defStyleAttr);mBackgroundTintHelper &#61; new AppCompatBackgroundHelper(this);mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);mTextHelper &#61; AppCompatTextHelper.create(this);mTextHelper.loadFromAttributes(attrs, defStyleAttr);mTextHelper.applyCompoundDrawablesTints();}&#64;Overridepublic void setBackgroundResource(&#64;DrawableRes int resId) {super.setBackgroundResource(resId);if (mBackgroundTintHelper !&#61; null) {mBackgroundTintHelper.onSetBackgroundResource(resId);}}......
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

AppCompatBackgroundHelper.Java

void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {TintTypedArray a &#61; TintTypedArray.obtainStyledAttributes(mView.getContext(), attrs,R.styleable.ViewBackgroundHelper, defStyleAttr, 0);......if (a.hasValue(R.styleable.ViewBackgroundHelper_android_background)) {mBackgroundResId &#61; a.getResourceId(R.styleable.ViewBackgroundHelper_android_background, -1);ColorStateList tint &#61; mDrawableManager.getTintList(mView.getContext(), mBackgroundResId);if (tint !&#61; null) {setInternalBackgroundTint(tint);}}......
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

到这里我仿佛是发现了新大陆一样兴奋, 源码中可以通过拦截View创建过程, 替换一些基础的组件, 然后对一些特殊的属性(eg: background, textColor) 做处理, 那我们为什么不能将这种思想拿到换肤框架中来使用呢?


Android-skin-support换肤框架实现

抱着试一试不会少块肉的心情, 开始了我的换肤框架开发之路

先简单讲一下原理: 
1. 参照源码实现在Activity onCreate中为LayoutInflater setFactory, 将View的创建过程交给自定义的SkinCompatViewInflater类来实现 
2. 重写系统组件, 实现换肤接口, 表明该控件支持换肤, 并在View创建之后统一收集 
3. 在重写的View中解析出需要换肤的属性, 并保存ResId到成员变量 
4. 重写类似setBackgroundResource方法, 解析需要换肤的属性, 并保存变量 
5. applySkin 在切换皮肤的时候, 从皮肤资源中获取资源

下面说一个简单的例子(SkinCompatTextView): 
1. 实现SkinCompatSupportable接口 
2. 在构造方法中通过SkinCompatBackgroundHelper和SkinCompatTextHelper分别解析出background, textColor并保存 
3. 重写setBackgroundResource和setTextAppearance, 解析出对应的资源Id, 表明该控件支持从代码中设置资源, 且支持该资源换肤 
4. 在用户点击切换皮肤时调用applySkin方法设置皮肤

public interface SkinCompatSupportable {void applySkin();
}public class SkinCompatTextView extends AppCompatTextView implements SkinCompatSupportable {public SkinCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);mBackgroundTintHelper &#61; new SkinCompatBackgroundHelper(this);mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);mTextHelper &#61; new SkinCompatTextHelper(this);mTextHelper.loadFromAttributes(attrs, defStyleAttr);}&#64;Overridepublic void setBackgroundResource(&#64;DrawableRes int resId) {super.setBackgroundResource(resId);if (mBackgroundTintHelper !&#61; null) {mBackgroundTintHelper.onSetBackgroundResource(resId);}}&#64;Overridepublic void setTextAppearance(Context context, int resId) {super.setTextAppearance(context, resId);if (mTextHelper !&#61; null) {mTextHelper.onSetTextAppearance(context, resId);}}&#64;Overridepublic void applySkin() {if (mBackgroundTintHelper !&#61; null) {mBackgroundTintHelper.applySkin();}if (mTextHelper !&#61; null) {mTextHelper.applySkin();}}
}public class SkinCompatTextHelper extends SkinCompatHelper {private static final String TAG &#61; SkinCompatTextHelper.class.getSimpleName();private final TextView mView;private int mTextColorResId &#61; INVALID_ID;private int mTextColorHintResId &#61; INVALID_ID;public SkinCompatTextHelper(TextView view) {mView &#61; view;}public void loadFromAttributes(AttributeSet attrs, int defStyleAttr) {final Context context &#61; mView.getContext();// First read the TextAppearance style idTintTypedArray a &#61; TintTypedArray.obtainStyledAttributes(context, attrs,R.styleable.SkinCompatTextHelper, defStyleAttr, 0);final int ap &#61; a.getResourceId(R.styleable.SkinCompatTextHelper_android_textAppearance, INVALID_ID);SkinLog.d(TAG, "ap &#61; " &#43; ap);a.recycle();if (ap !&#61; INVALID_ID) {a &#61; TintTypedArray.obtainStyledAttributes(context, ap, R.styleable.SkinTextAppearance);if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {mTextColorResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);SkinLog.d(TAG, "mTextColorResId &#61; " &#43; mTextColorResId);}if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {mTextColorHintResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);SkinLog.d(TAG, "mTextColorHintResId &#61; " &#43; mTextColorHintResId);}a.recycle();}// Now read the style&#39;s valuesa &#61; TintTypedArray.obtainStyledAttributes(context, attrs, R.styleable.SkinTextAppearance,defStyleAttr, 0);if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {mTextColorResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);SkinLog.d(TAG, "mTextColorResId &#61; " &#43; mTextColorResId);}if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {mTextColorHintResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);SkinLog.d(TAG, "mTextColorHintResId &#61; " &#43; mTextColorHintResId);}a.recycle();applySkin();}public void onSetTextAppearance(Context context, int resId) {final TintTypedArray a &#61; TintTypedArray.obtainStyledAttributes(context,resId, R.styleable.SkinTextAppearance);if (a.hasValue(R.styleable.SkinTextAppearance_android_textColor)) {mTextColorResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColor, INVALID_ID);SkinLog.d(TAG, "mTextColorResId &#61; " &#43; mTextColorResId);}if (a.hasValue(R.styleable.SkinTextAppearance_android_textColorHint)) {mTextColorHintResId &#61; a.getResourceId(R.styleable.SkinTextAppearance_android_textColorHint, INVALID_ID);SkinLog.d(TAG, "mTextColorHintResId &#61; " &#43; mTextColorHintResId);}a.recycle();applySkin();}public void applySkin() {mTextColorResId &#61; checkResourceId(mTextColorResId);if (mTextColorResId !&#61; INVALID_ID) {ColorStateList color &#61; SkinCompatResources.getInstance().getColorStateList(mTextColorResId);mView.setTextColor(color);}mTextColorHintResId &#61; checkResourceId(mTextColorHintResId);if (mTextColorHintResId !&#61; INVALID_ID) {ColorStateList color &#61; SkinCompatResources.getInstance().getColorStateList(mTextColorHintResId);mView.setHintTextColor(color);}}
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120

开发过程中遇到的一些问题

在5.0以上, 使用color为ImageView设置src, 可以通过getColorStateList获取资源, 而在5.0以下, 需要通过ColorDrawable setColor的方式实现

String typeName &#61; mView.getResources().getResourceTypeName(mSrcResId);
if ("color".equals(typeName)) {if (Build.VERSION.SDK_INT int color &#61; SkinCompatResources.getInstance().getColor(mSrcResId);Drawable drawable &#61; mView.getDrawable();if (drawable instanceof ColorDrawable) {((ColorDrawable) drawable.mutate()).setColor(color);} else {mView.setImageDrawable(new ColorDrawable(color));}} else {ColorStateList colorStateList &#61; SkinCompatResources.getInstance().getColorStateList(mSrcResId);Drawable drawable &#61; mView.getDrawable();DrawableCompat.setTintList(drawable, colorStateList);mView.setImageDrawable(drawable);}
} else if ("drawable".equals(typeName)) {Drawable drawable &#61; SkinCompatResources.getInstance().getDrawable(mSrcResId);mView.setImageDrawable(drawable);
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

还有很多问题, 有兴趣的同学可以来一起交流解决.


总结


  1. 这样的做法与网上其他框架相比优势在哪里, 为什么重复造轮子

    • 在增加框架开发成本的基础上降低了框架使用的成本, 我觉得更有意义, 一次开发, 所有Android 开发者都受用;
    • 换肤框架对业务代码的侵入性比较小, 业务代码只需要继承自SkinCompatActivity, 不需要实现接口重写方法, 不需要其他额外的代码, 接入方便, 假如将来不想再使用本框架, 只需要把SkinCompatActivity改为原生Activity即可;
    • 深入源码, 和源码实现方式类似, 兼容性更好.
  2. 为什么选择继承自AppCompatActivity, AppCompatTextView…而不是选择直接继承自Activity, TextView…

    • 本身appcompat-v7包是一个support包, 兼容原生控件, 同时符合Material design, 我们只需要获取我们想要换肤的属性就可以在不破坏support包属性的前提下进行换肤;
    • 参与开发的同学更多的话, 完全可以支持一套继承自Activity, TextView…的skin support包.
  3. 自定义View能否支持, 第三方控件是否支持换肤

    • 答案是肯定的, 完全可以参照SkinCompatTextView的实现, 自己去实现自定义控件, 对于使用者来说, 扩展性很好.

源码地址: https://github.com/ximsfei/Android-skin-support


推荐阅读
  • Flutter 核心技术与混合开发模式深入解析
    本文深入探讨了 Flutter 的核心技术,特别是其混合开发模式,包括统一管理模式和三端分离模式,以及混合栈原理。通过对比不同模式的优缺点,帮助开发者选择最适合项目的混合开发策略。 ... [详细]
  • 实践指南:使用Express、Create React App与MongoDB搭建React开发环境
    本文详细介绍了如何利用Express、Create React App和MongoDB构建一个高效的React应用开发环境,旨在为开发者提供一套完整的解决方案,包括环境搭建、数据模拟及前后端交互。 ... [详细]
  • ArcBlock 发布 ABT 节点 1.0.31 版本更新
    2020年11月9日,ArcBlock 区块链基础平台发布了 ABT 节点开发平台的1.0.31版本更新,此次更新带来了多项功能增强与性能优化。 ... [详细]
  • 尽管在WPF中工作了一段时间,但在菜单控件的样式设置上遇到了一些基础问题,特别是关于如何正确配置前景色和背景色。 ... [详细]
  • 本文详细介绍了JQuery Mobile框架中特有的事件和方法,帮助开发者更好地理解和应用这些特性,提升移动Web开发的效率。 ... [详细]
  • 本文介绍了如何通过C#语言调用动态链接库(DLL)中的函数来实现IC卡的基本操作,包括初始化设备、设置密码模式、获取设备状态等,并详细展示了将TextBox中的数据写入IC卡的具体实现方法。 ... [详细]
  • 问题场景用Java进行web开发过程当中,当遇到很多很多个字段的实体时,最苦恼的莫过于编辑字段的查看和修改界面,发现2个页面存在很多重复信息,能不能写一遍?有没有轮子用都不如自己造。解决方式笔者根据自 ... [详细]
  • 理解浏览器历史记录(2)hashchange、pushState
    阅读目录1.hashchange2.pushState本文也是一篇基础文章。继上文之后,本打算去研究pushState,偶然在一些信息中发现了锚点变 ... [详细]
  • Jenkins API当前未直接提供获取任务构建队列长度的功能,因此需要通过解析HTML页面来间接实现这一需求。 ... [详细]
  • Android与JUnit集成测试实践
    本文探讨了如何在Android项目中集成JUnit进行单元测试,并详细介绍了修改AndroidManifest.xml文件以支持测试的方法。 ... [详细]
  • 本文将在前几篇关于Android测试理论知识的基础上,通过ApiDemoTest实例详细探讨如何使用ApplicationTestCase进行Android应用测试。建议读者先阅读Android测试教程系列中的相关内容,以便更好地理解本文的实践部分。 ... [详细]
  • CSS Border 属性:solid 边框的使用详解
    本文详细介绍了如何在CSS中使用solid边框属性,包括其基本语法、应用场景及高级技巧,适合初学者和进阶用户参考。 ... [详细]
  • 使用TabActivity实现Android顶部选项卡功能
    本文介绍如何通过继承TabActivity来创建Android应用中的顶部选项卡。通过简单的步骤,您可以轻松地添加多个选项卡,并实现基本的界面切换功能。 ... [详细]
  • 深入探讨前端代码优化策略
    本文深入讨论了前端开发中代码优化的关键技术,包括JavaScript、HTML和CSS的优化方法,旨在提升网页加载速度和用户体验。 ... [详细]
  • 本文详细介绍了如何在Android L版本中应用Material Design的主题和布局,包括Material主题的应用方法、自定义主题和颜色方案、状态栏和导航条的自定义,以及Material Design布局的特点和兼容性处理。 ... [详细]
author-avatar
家居生活我最大_386
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有