由于第三版的13章是第三版独有,但目前个人能力还不足以完美翻译kotlin为java因此决定先学习两个版本都有的这一章节
1.全局获取 Context 的技巧
我们很多的操作都是在活动中进行的, 而活动本身就是一个 Context 对象。但是,当应用程序的架构逐渐开始复杂起来的时候,很多 的逻辑代码都将脱离 Activity 类,但此时你又恰恰需要使用 Context,也许这个时候你就会感 到有些伤脑筋了
Android提供了一个Application类,每当应用程序启动的时候,系统就会自动将这个类进行初始化。而我们可以定制一个自己的Application类,以便于管理程序内一些全局的状态信 息,比如全局Context
创建一个 MyApplication 类继 承自 Application
package com.example.materialtest;
import android.app.Application;
import android.content.Context;
public class MyApplication extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
public static Context getContext() {
return context;
}
}
MyApplication中的代码非常简单。这里我们在companion object中定义了一 个context变量,然后重写父类的onCreate()方法,并将调用 getApplicationContext()方法得到的返回值赋值给context变量,这样我们就可以以静 态变量的形式获取Context对象了。
将Context设置成静态变量很容易会产生内存泄漏的问题,这是一种有风险的做法,因此Android Studio会给出警告提示。
是由于这里获取的不是Activity或Service中的Context,而是Application中的Context, 它全局只会存在一份实例,并且在整个应用程序的生命周期内都不会回收,因此是不存在内存泄漏风险的。那么我们可以使用如下注解让Android Studio忽略上述警告提示
@SuppressLint("StaticFieldLeak")
接下来我们还需要告知系统,当程序启动的时候应该初始化MyApplication类,而不是默认的 Application类
在 AndroidManifest.xml 文件的标签下进行指定
android:name="com.example.networktest.MyApplication"
这样我们就实现了一种全局获取Context的机制,之后不管你想在项目的任何地方使用 Context,只需要调用一下MyApplication.context就可以了
可以在任何地方这样使用
Toast.makeText(MyApplication.getContext(), "network is unavailable", Toast.LENGTH_SHORT).show();
2.使用Intent传递对象
我们可以在Intent中添加一些附加数据,以达到传值的效果,比如在FirstActivity中添加如下代码
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("string_data", "hello");
intent.putExtra("int_data", 100);
startActivity(intent);
这里调用了 Intent 的 putExtra()方法来添加要传递的数据,之后在 SecondActivity 中就可以得到这些值了
getIntent().getStringExtra("string_data");
getIntent().getIntExtra("int_data", 0);
putExtra()方法中所支持的数据类型是有限的,虽然常用的一些 数据类型是支持的,但是当你想去传递一些自定义对象的时候,就会发现无从下手。下面我们学习一下使用Intent来传递对象的技巧
1.Serializable 方式
Serializable是序列化的意思,表示将一个对象转换成可存储或可传输的状态。序列化后的对象 可以在网络上进行传输,也可以存储到本地。
至于序列化的方法非常简单,只需要让一个类去 实现Serializable这个接口就可以了。 比如说有一个Person类,其中包含了name和age这两个字段,如果想要将它序列化,就可以这样写
package com.example.materialtest;
import java.io.Serializable;
public class Person implements Serializable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
接下来在 FirstActivity 中的写法非常简单
Person person = new Person();
person.setName("Tom");
person.setAge(20);
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("person_data", person);
startActivity(intent);
我们创建了一个Person的实例,并将它直接传入了Intent的putExtra()方法中。
接下来在 SecondActivity 中获取这个对象
Person person = getIntent().getSerializableExtra("person_data");
这里调用了 getSerializableExtra()方法来获取通过参数传递过来的序列化对象,接着再 将它向下转型成 Person 对象,这样我们就成功实现了使用 Intent 来传递对象的功能了
2.Parcelable 方式
使用Parcelable也可以实现相同的效果,不过不同于将对象进行序列 化,Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是 Intent所支持的数据类型,这样就能实现传递对象的功能了。
修改 Person 中的代码
package com.example.materialtest;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class Person implements Parcelable {
private String name;
private int age;
protected Person(Parcel in) {
name = in.readString();
age = in.readInt();
}
public static final Creator<Person> CREATOR = new Creator<Person>() {
@Override
public Person createFromParcel(Parcel in) {
Person person = new Person();
person.name = in.readString(); // 读取 name
person.age = in.readInt(); // 读取 age
return person;
}
@Override
public Person[] newArray(int size) {
return new Person[size];
}
};
public Person() {
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
parcel.writeString(name);
parcel.writeInt(age);
}
}
首先我们让 Person 类去实现了 Parcelable 接口,这样就必须重写 describeContents()和 writeToParcel()这两个方法。
describeContents()方法直接返回 0 就可以了,而 writeToParcel()方法中我们需要调用 Parcel 的 writeXxx()方法,将 Person 类中的字段一一写出。
字符串型数据就调用 writeString()方法,整型数据就调用 writeInt()方法,以此类推。
我们还必须在Person类中提供一个名为CREATOR的匿名类实现。
创建了 Parcelable.Creator接口的一个实现,并将泛型指定为Person。
重写 createFromParcel()和newArray()这两个方法,
在createFromParcel()方法中,创建一个Person对象进行返回,并读取刚才写出的name和age字段。name和age都是调用Parcel的readXxx()方法读取到的,这里读取的顺序一定要和刚才写出的顺序完全相同。
而newArray()方法只需要调用arrayOfNulls()方法,并使用参数中传入的size作为数组大小,创建一个空的Person数组即可。
接下来,在FirstActivity中我们仍然可以使用相同的代码来传递Person对象,只不过在 SecondActivity中获取对象的时候需要稍加改动
Person person = getIntent().getParcelableExtra("person_data");
这里不再是调用 getSerializableExtra()方法,而是调用 getParcelableExtra() 方法来获取传递过来的对象了,其他的地方都完全相同
3.定制自己的日志工具
之前用于调试的那些日志,在项目正式上线之后仍然会照常打印,这样不仅会降低程序的运行效率,还有可能将 一些机密性的数据泄露出去。
最理想的情况是能够自由地控制日志的打印,当程序处于开发阶段时就让日志打印出来,当程序上线了之后 就把日志屏蔽掉
新建一个 LogUtil 类
package com.example.materialtest;
import android.util.Log;
public class LogUtil {
public static final int VERBOSE = 1;
public static final int DEBUG = 2;
public static final int INFO = 3;
public static final int WARN = 4;
public static final int ERROR = 5;
public static int level = VERBOSE;
public static void v(String tag, String msg) {
if (level <= VERBOSE) {
Log.v(tag, msg);
}
}
public static void d(String tag, String msg) {
if (level <= DEBUG) {
Log.d(tag, msg);
}
}
public static void i(String tag, String msg) {
if (level <= INFO) {
Log.i(tag, msg);
}
}
public static void w(String tag, String msg) {
if (level <= WARN) {
Log.w(tag, msg);
}
}
public static void e(String tag, String msg) {
if (level <= ERROR) {
Log.e(tag, msg);
}
}
}
我们在LogUtil中首先定义了VERBOSE、DEBUG、INFO、WARN、ERROR这5个整型常量,并且它们对应的值都是递增的。
然后定义了一个静态变量level,可以将它的值指定为上面5个常量中的任意一个。
提供了v()、d()、i()、w()、e()这5个自定义的日志方法,在其内部分别调用 了Log.v()、Log.d()、Log.i()、Log.w()、Log.e()这5个方法来打印日志,只不过在这 些自定义的方法中都加入了一个if判断,只有当level的值小于或等于对应日志级别值的时候,才会将日志打印出来。
之后在项目里,我们可以像使用普通的日志工具一样使用LogUtil。比如打印一行DEBUG级别的日志可以这样写
LogUtil.d("TAG", "debug log");
打印一行 WARN 级别的日志就可以这样写:
LogUtil.w("TAG", "warn log");
我们只需要通过修改level变量的值,就可以自由地控制日志的打印行为。比如让level等于 VERBOSE就可以把所有的日志都打印出来,让level等于ERROR就可以只打印程序的错误日志。
你只需要在开发阶段将level指定成VERBOSE,当项目正式上线的时候将level指定成ERROR就可以了。
4.调试Android程序
实际上之前学习java以及开发android计算器的时候遇到很多新版与旧版的差异我就已经频繁使用调试功能了,这里不再赘述
5.深色主题
需要让我们的程序适配深色主题样式
最简单的一种适配方式就是使用Force Dark,它是一种能让应用程序快速适配深色主题,并且几乎不用编写额外代码的方式。Force Dark的工作原理是系统会分析浅色主题应用下的每一层 View,并且在这些View绘制到屏幕之前,自动将它们的颜色转换成更加适合深色主题的颜色。
只有原本使用浅色主题的应用才能使用这种方式,如果你的应用原本使用的就是深色主题,Force Dark将不会起作用。
我们尝试对MaterialTest项目使用Force Dark转换来进行举例。启用Force Dark功能需要借助android:forceDarkAllowed属性,不过这个属性是从API 29,也就是Android 10.0 系统开始才有的,之前的系统无法指定这个属性。因此,我们得进行一些系统差异型编程才行。
右击res目录→New→Directory,创建一个values-v29目录,然后右击values-v29目录 →New→Values resource file,创建一个styles.xml文件。接着对这个文件进行编写
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.MaterialTest" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<item name="android:forceDarkAllowed">true</item>
</style>
<style name="Theme.MaterialTest" parent="Base.Theme.MaterialTest" />
</resources>
因为书上的代码会爆红,就像之前的一样找不到颜色,书中说除了
<item name="android:forceDarkAllowed">true</item>
其他的都是复制来的,因此我直接复制了values/themes.xml文件并且添加了这一行代码
除了android:forceDarkAllowed属性之外,其他的内容都是从之前的styles.xml文件中复制过来的。这里给AppTheme主题增加了android:forceDarkAllowed属性并设置为 true,说明现在我们是允许系统使用Force Dark将应用强制转换成深色主题的。
values-v29目录是只有Android 10.0及以上的系统才会去读取的,因此这是一种系统差异型编程的实现方式。
Force Dark就是这样一种简单粗暴的转换方式,并且它的转换效果通常是不尽如人意的。因 此,这里我并不推荐你使用这种自动化的方式来实现深色主题,而是应该使用更加传统的实现方式——手动实现。
AppCompat库内置的主题主要分为浅色主题和深色主题两类,比如MaterialTest项目中目前使用的Theme.AppCompat.Light.NoActionBar就是浅色主题,而Theme.AppCompat.NoActionBar就是深色主题。
选用不同的主题,在控件的默认颜色等方面会有完全不同的效果。
我们多了一个DayNight主题的选项。使用了这个主题后,当用户在系统设置中开启深色主题时,应用程序会自动使用深色主题,反之则会使用浅色主题。
首先删除values-v29目录及其目录下的内容,然后修改 values/styles.xml中的代码,如下所示
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
<item name="android:forceDarkAllowed">true</item>
</style>
<style name="Theme.MaterialTest" parent="Base.Theme.MaterialTest" />
</resources>
这里我们将AppTheme的parent主题指定成了 Theme.AppCompat.DayNight.NoActionBar,这是一种DayNight主题。因此,在普通情况 下MaterialTest项目仍然会使用浅色主题,和之前并没有什么区别,但是一旦用户在系统设置 中开启了深色主题,MaterialTest项目就会自动使用相应的深色主题。
虽然现在界面中的主要内容都已经自动切换成了深色主题,但是你会发现标题栏和悬浮按钮仍然保持着和浅色主题时一样的颜色。这是因为标题栏以及悬浮按钮使用的是我们定义在 colors.xml中的几种颜色值,代码如下所示:
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
</resources>
(好好好那就是我前面没有加这个看漏了?emmm)
这种指定颜色值引用的方式相当于对控件的颜色进行了硬编码,DayNight主题是不能对这些颜色进行动态转换的
好在解决方案也并不复杂,我们只需要进行一些主题差异型编程就可以了。右击res目录 →New→Directory,创建一个values-night目录,然后右击values-night目录 →New→Values resource file,创建一个colors.xml文件。接着在这个文件中指定深色主题下的颜色值,如下所示
<resources>
<color name="colorPrimary">#303030</color>
<color name="colorPrimaryDark">#232323</color>
<color name="colorAccent">#008577</color>
</resources>
这样的话,在普通情况下,系统仍然会读取values/colors.xml文件中的颜色值,而一旦用户开 启了深色主题,系统就会去读取values-night/colors.xml文件中的颜色值了。
虽说使用主题差异型的编程方式几乎可以帮你解决所有的适配问题,但是在DayNight主题下, 我们最好还是尽量减少通过硬编码的方式来指定控件的颜色,而是应该更多地使用能够根据当前主题自动切换颜色的主题属性。
比如说黑色的文字通常应该衬托在白色的背景下,反之白色的文字通常应该衬托在黑色的背景下,那么此时我们就可以使用主题属性来指定背景以及文字的颜色,示例写法如下
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Hello world"
android:textSize="40sp"
android:textColor="?android:attr/textColorPrimary" />
</FrameLayout>
这些主题属性会自动根据系统当前的主题模式选择最合适的颜色值呈现给用户
或许你还会有一些特殊的需求,比如要在浅色主题和深色主题下分别执行不同的代码逻辑。对此Android也是支持的,你可以使用如下代码在任何时候判断当前系统是否是深色主题
public boolean isDarkTheme(Context context) {
Configuration configuration = context.getResources ().getConfiguration();
int flag = configuration.uiMode & Configuration.UI_MODE_NIGHT_MASK;
return flag == Configuration.UI_MODE_NIGHT_YES;
}
这个是我自行翻译的java版本
调用isDarkTheme ()方法,判断当前系统是浅色主题还是深色主题,然后根据返回值执行不同的代码逻辑即可。