情景一

有这样一个需求,界面上需要显示一个标题文本,但是该标题的文案长度是不固定的,要求标题的文案全部显示出来,不能用省略号显示,并且标题所占的宽高是固定的。例如标题的文案为 “这是标题,该标题的名字比较长,产品要求不换行全部显示出来”,如下图所示,第一个为不符合需求的标题,第二个为符合需求的标题。


也就是说 TextView 控件的宽高需要固定,然后根据标题的文案长度动态改变文字大小,也就是上图第二个标题的效果。那是怎么实现的呢?

以前的做法一般是测量 TextView 字体所占的宽度与 TextView 控件的宽度对比,动态改变 TextView 的字体大小,写起来即麻烦又耗性能。但是现在不用这么麻烦了,Android 8.0 新增了用来动态改变 TextView 字体大小的新特性 Autosizing TextViews,只需要简单设置一下属性即可。

例如上图中符合需求的效果可以这样写:

xml 方式
<?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:gravity="center">

    <TextView
        android:layout_width="340dp"
        android:layout_height="50dp"
        android:background="@drawable/shape_bg_008577"
        android:gravity="center_vertical"
        android:maxLines="1"
        android:text="这是标题,该标题的名字比较长,产品要求不换行全部显示出来"
        android:textSize="18sp"
        android:autoSizeTextType="uniform"
        android:autoSizeMaxTextSize="18sp"
        android:autoSizeMinTextSize="10sp"
        android:autoSizeStepGranularity="1sp"/>
</LinearLayout>
复制代码

可以看到 TextView 控件多了如下属性:

  • autoSizeTextType:设置 TextView 是否支持自动改变文本大小,none 表示不支持,uniform 表示支持。
  • autoSizeMinTextSize:最小文字大小,例如设置为10sp,表示文字最多只能缩小到10sp。
  • autoSizeMaxTextSize:最大文字大小,例如设置为18sp,表示文字最多只能放大到18sp。
  • autoSizeStepGranularity:缩放粒度,即每次文字大小变化的数值,例如设置为1sp,表示每次缩小或放大的值为1sp。

上面的只是针对于8.0的设备有效,如果想要兼容8.0以下设备,则需要用AppCompatTextView 代替 TextView,并且上面几个属性的命名空间需要用 app 命名空间。如下:

<?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"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:gravity="center">

    <android.support.v7.widget.AppCompatTextView
        android:layout_width="340dp"
        android:layout_height="50dp"
        android:background="@drawable/shape_bg_008577"
        android:gravity="center_vertical"
        android:maxLines="1"
        android:text="这是标题,该标题的名字比较长,产品要求不换行全部显示出来"
        android:textSize="18sp"
        app:autoSizeTextType="uniform"
        app:autoSizeMaxTextSize="18sp"
        app:autoSizeMinTextSize="10sp"
        app:autoSizeStepGranularity="1sp"/>
</LinearLayout>
复制代码

肯定很多人说 “为什么自己写的时候不用 AppCompatTextView 也能兼容8.0以下设备呢?”,那是因为你当前的 xml 文件对应的 Activity 继承的是 AppCompatActivity,如果继承的是 Activity 或 FragmentActivity 是不能达到兼容的。这一点其实官方文档 Autosizing TextViews 也没有说清楚,导致很多人误解了,各位可以自己验证下。

动态编码方式

使用 TextViewCompat 的setAutoSizeTextTypeWithDefaults()方法设置 TextView 是否支持自动改变文字大小,setAutoSizeTextTypeUniformWithConfiguration() 方法设置最小文字大小、最大文字大小与缩放粒度。如下所示:

TextView tvText = findViewById(R.id.tv_text);
TextViewCompat.setAutoSizeTextTypeWithDefaults(tvText,TextViewCompat.AUTO_SIZE_TEXT_TYPE_UNIFORM);
TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(tvText,10,18,1, TypedValue.COMPLEX_UNIT_SP);
复制代码
  • setAutoSizeTextTypeWithDefaults()
    参数1为需要动态改变文字大小的 TextView,参数2为是否支持自动改变文字大小的类型,AUTO_SIZE_TEXT_TYPE_UNIFORM表示支持,AUTO_SIZE_TEXT_TYPE_NONE 表示不支持。
  • setAutoSizeTextTypeUniformWithConfiguration()
    参数1为需要动态改变文字大小的 TextView,参数2、3、4分别为最小文字大小、最大文字大小与缩放粒度,参数5为参数2、3、4的单位,例如sp 、dp、px等。

同样,如果要兼容8.0以下设备,要么在 xml 中用 AppCompatTextView 代替TextView,要么当前 Activity 继承 AppCompatActivity。

小结

Autosizing TextViews是Android 8.0 新增的特性,可以用来动态改变 TextView 字体大小。如果要兼容8.0以下设备,则需要满足以下2个条件中的其中一个。

  • 在 xml 中用 AppCompatTextView 代替 TextView,并且上面几个属性的命名空间用app 命名空间。
  • 当前 Activity 继承 AppCompatActivity,而不是 Activity 或 FragmentActivity。

Autosizing TextViews更多属性请参考 Autosizing TextViews


                                        情景二

很多人肯定遇到过这种情况,测试扔个图片过来,然后说怎么运行在这个测试机后下面的内容都挡住了(如下右图,左图为正常情况),你不是说做了屏幕适配的吗?然后你拿测试的手机一看,设置里面竟然选了 特大 字体。


嗯... 经过这么一看基本就知道什么问题了。原因是你在 xml 文件写死了控件的高度,并且TextView 的字体单位用的是 sp,这种情况下到手机设置中改变字体大小,那么界面中的字体大小就会随系统改变。

那么我们应该怎么解决这个问题呢?这时候我们可以观察下微信的做法,经过研究发现微信的字体是不会随着系统字体大小的改变而改变的,并且微信本身是有改变字体大小功能的。微信中改变字体大小后不仅字体大小改变了,控件的宽高也会跟着改变。所以可以猜到微信的字体适配是如下方式实现的:

字体大小不随系统改变

想要实现字体大小不随系统改变有两种方式:

1. xml方式

TextView 的字体单位不使用 sp,而是用 dp。因为 sp 单位的字体大小会随系统字体大小的改变而改变,而 dp 单位则不会。

2. 动态编码方式

字体大小是否随系统改变可以通过 Configuration 类的 fontScale 变量来控制,fontScale变量默认为1,表示字体大小不随系统字体大小的改变而改变,那么我们只需要保证fontScale 始终为1即可。具体代码如下,一般放在 Activity 的基类 BaseActivity 即可。

@Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (newConfig.fontScale != 1) { //fontScale不为1,需要强制设置为1
            getResources();
        }
    }

    @Override
    public Resources getResources() {
        Resources resources = super.getResources();
        if (resources.getConfiguration().fontScale != 1) { //fontScale不为1,需要强制设置为1
            Configuration newConfig = new Configuration();
            newConfig.setToDefaults();//设置成默认值,即fontScale为1
            resources.updateConfiguration(newConfig, resources.getDisplayMetrics());
        }
        return resources;
    }
复制代码

虽然两种方式都可以解决场景二的问题,但是一般都是使用动态编码方式,原因如下:

  • 若应用需要增加类似微信可以改变字体大小的功能,如果在 xml 中用的是 dp 单位,那么该功能将无法实现!
  • 若需求改成字体大小需要随系统字体大小的改变而改变,只需要删掉该段代码即可。
  • 官方推荐使用 sp 作为字体单位。
控件宽高尽量不要固定

原因是如果应用需要增加类似微信可以改变字体大小的功能,如果控件宽高固定的话,调大字体会导致控件显示不下,这不是我们需要的效果。

                                      情景三

1. Returning Null(返回 Null)

null 一直是开发者最好的朋友,也是最大的敌人,这在 Java 中也不例外。在高性能应用中,使用 null 是一种减少对象数量的可靠方法,它表明方法没有要返回的值。与抛出异常不同,如果要通知客户端不能获取任何值,使用 null 是一种快速且低开销的方法,它不需要捕获整个堆栈跟踪。

在高性能系统的环境之外,null 的存在会导致创建更繁琐的 null 返回值检查,从而破坏应用程序,并在解引用空对象时导致 NullPointerExceptions。在大多数应用程序中,返回 null 有三个主要原因:

  1. 表示列表中找不到元素;
  2. 表示即使没有发生错误,也找不到有效值;
  3. 表示特殊情况下的返回值。

除非有任何性能方面的原因,否则以上每一种情况都有更好的解决方案,它们不使用 null,并且强制开发人员处理出现 null 的情况。更重要的是,这些方法的客户端不会为该方法是否会在某些边缘情况下返回 null 而伤脑筋。在每种情况下,我们将设计一种不返回 null 值的简洁方法。

No Elements(集合中没有元素的情况)

在返回列表或其他集合时,通常会看到返回空集合,以表明无法找到该集合的元素。例如,我们可以创建一个服务来管理数据库中的用户,该服务类似于以下内容(为了简洁起见,省略了一些方法和类定义):

public class UserService {
    public List<User> getUsers() {
        User[] usersFromDb = getUsersFromDatabase();
        if (usersFromDb == null) {
            // No users found in database
            return null;
        }
        else {
            return Arrays.asList(usersFromDb);
        }
    }
}
UserServer service = new UserService();
List<Users> users = service.getUsers();
if (users != null) {
    for (User user: users) {
        System.out.println("User found: " + user.getName());
    }
}
复制代码

因为我们选择在没有用户的情况下返回 null 值,所以我们迫使客户端在遍历用户列表之前先处理这种情况。如果我们返回一个空列表来表示没有找到用户,那么客户端可以完全删除空检查并像往常一样遍历用户。如果没有用户,则隐式跳过循环,而不必手动处理这种情况;从本质上说,循环遍历用户列表的功能就像我们为空列表和填充列表所做的那样,而不需要手动处理任何一种情况:

public class UserService {
    public List<User> getUsers() {
        User[] usersFromDb = getUsersFromDatabase();
        if (usersFromDb == null) {
            // No users found in database
            return Collections.emptyList();
        }
        else {
            return Arrays.asList(usersFromDb);
        }
    }
}
UserServer service = new UserService();
List<Users> users = service.getUsers();
for (User user: users) {
    System.out.println("User found: " + user.getName());
}
复制代码

在上面的例子中,我们返回的是一个不可变的空列表。这是一个可接受的解决方案,只要我们记录该列表是不可变的并且不应该被修改(这样做可能会抛出异常)。如果列表必须是可变的,我们可以返回一个空的可变列表,如下例所示:

public List<User> getUsers() {
    User[] usersFromDb = getUsersFromDatabase();
    if (usersFromDb == null) {
        // No users found in database
        return new ArrayList<>();    // A mutable list
    }
    else {
        return Arrays.asList(usersFromDb);
    }
}
复制代码

一般来说,当没有发现任何元素的时候,应遵守以下规则:

  1. 返回一个空集合(或 list、set、queue 等等)表明找不到元素。
  2. 这样做不仅减少了客户端必须执行的特殊情况处理,而且还减少了接口中的不一致性(例如,我们常常返回一个 list 对象,而不是其他对象)。

Optional Value(可选值)

很多时候,我们希望在没有发生错误时通知客户端不存在可选值,此时返回 null。例如,从 web 地址获取参数。在某些情况下,参数可能存在,但在其他情况下,它可能不存在。缺少此参数并不一定表示错误,而是表示用户不需要提供该参数时包含的功能(例如排序)。如果没有参数,则返回 null;如果提供了参数,则返回参数值(为了简洁起见,删除了一些方法):

public class UserListUrl {
    private final String url;
    public UserListUrl(String url) {
        this.url = url;
    }
    public String getSortingValue() {
        if (urlContainsSortParameter(url)) {
            return extractSortParameter(url);
        }
        else {
            return null;
        }
    }
}
UserService userService = new UserService();
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
String sortingParam = url.getSortingValue();
if (sortingParam != null) {
    UserSorter sorter = UserSorter.fromParameter(sortingParam);
    return userService.getUsers(sorter);
}
else {
    return userService.getUsers();
}
复制代码

当没有提供参数时,返回 null,客户端必须处理这种情况,但是在 getSortingValue 方法的签名中,没有任何地方声明排序值是可选的。如果方法的参数是可选的,并且在没有参数时,可能返回 null,要知道这个事实,我们必须阅读与该方法相关的文档(如果提供了文档)。

相反,我们可以使可选性显式地返回一个 Optional 对象。正如我们将看到的,当没有参数存在时,客户端仍然需要处理这种情况,但是现在这个需求已经明确了。更重要的是,Optional 类提供了比简单的 null 检查更多的机制来处理丢失的参数。例如,我们可以使用 Optional 类提供的查询方法(一种状态测试方法)简单地检查参数是否存在:

public class UserListUrl {
    private final String url;
    public UserListUrl(String url) {
        this.url = url;
    }
    public Optional<String> getSortingValue() {
        if (urlContainsSortParameter(url)) {
            return Optional.of(extractSortParameter(url));
        }
        else {
            return Optional.empty();
        }
    }
}
UserService userService = new UserService();
UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
Optional<String> sortingParam = url.getSortingValue();
if (sortingParam.isPresent()) {
    UserSorter sorter = UserSorter.fromParameter(sortingParam.get());
    return userService.getUsers(sorter);
}
else {
    return userService.getUsers();
}
复制代码

这与「空检查」的情况几乎相同,但是我们已经明确了参数的可选性(即客户机在不调用 get() 的情况下无法访问参数,如果可选参数为空,则会抛出NoSuchElementException)。如果我们不希望根据 web 地址中的可选参数返回用户列表,而是以某种方式使用该参数,我们可以使用ifPresentOrElse 方法来这样做:

sortingParam.ifPresentOrElse(
    param -> System.out.println("Parameter is :" + param),
    () -> System.out.println("No parameter supplied.")
);
复制代码

这极大降低了「空检查」的影响。如果我们希望在没有提供参数时忽略参数,可以使用 ifPresent 方法:

sortingParam.ifPresent(param -> System.out.println("Parameter is :" + param));
复制代码

在这两种情况下,使用 Optional 对象要优于返回 null 以及显式地强制客户端处理返回值可能不存在的情况,为处理这个可选值提供了更多的途径。考虑到这一点,我们可以制定以下规则:

如果返回值是可选的,则通过返回一个 Optional 来确保客户端处理这种情况,该可选的值在找到值时包含一个值,在找不到值时为空

Special-Case Value(特殊情况值)

最后一个常见用例是特殊用例,在这种情况下无法获得正常值,客户端应该处理与其他用例不同的极端情况。例如,假设我们有一个命令工厂,客户端定期从命令工厂请求命令。如果没有命令可以获得,客户端应该等待 1 秒钟再请求。我们可以通过返回一个空命令来实现这一点,客户端必须处理这个空命令,如下面的例子所示(为了简洁起见,没有显示一些方法):

public interface Command {
    public void execute();
}
public class ReadCommand implements Command {
    @Override
    public void execute() {
        System.out.println("Read");
    }
}
public class WriteCommand implements Command {
    @Override
    public void execute() {
        System.out.println("Write");
    }
}
public class CommandFactory {
    public Command getCommand() {
        if (shouldRead()) {
            return new ReadCommand();
        }
        else if (shouldWrite()) {
            return new WriteCommand();
        }
        else {
            return null;
        }
    }
}
CommandFactory factory = new CommandFactory();
while (true) {
    Command command = factory.getCommand();
    if (command != null) {
        command.execute();
    }
    else {
        Thread.sleep(1000);
    }
}
复制代码

由于 CommandFactory 可以返回空命令,客户端有义务检查接收到的命令是否为空,如果为空,则休眠1秒。这将创建一组必须由客户端自行处理的条件逻辑。我们可以通过创建一个「空对象」(有时称为特殊情况对象)来减少这种开销。「空对象」将在 null 场景中执行的逻辑(休眠 1 秒)封装到 null 情况下返回的对象中。对于我们的命令示例,这意味着创建一个在执行时休眠的SleepCommand:

public class SleepCommand implements Command {
    @Override
    public void execute() {
        Thread.sleep(1000);
    }
}
public class CommandFactory {
    public Command getCommand() {
        if (shouldRead()) {
            return new ReadCommand();
        }
        else if (shouldWrite()) {
            return new WriteCommand();
        }
        else {
            return new SleepCommand();
        }
    }
}
CommandFactory factory = new CommandFactory();
while (true) {
    Command command = factory.getCommand();
    command.execute();
}
复制代码

与返回空集合的情况一样,创建「空对象」允许客户端隐式处理特殊情况,就像它们是正常情况一样。但这并不总是可行的;在某些情况下,处理特殊情况的决定必须由客户做出。这可以通过允许客户端提供默认值来处理,就像使用 Optional 类一样。在 Optional 的情况下,客户端可以使用 orElse 方法获取包含的值或默认值:

UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
Optional<String> sortingParam = url.getSortingValue();
String sort = sortingParam.orElse("ASC");
复制代码

如果有一个提供的排序参数(例如,如果 Optional 包含一个值),这个值将被返回。如果不存在值,默认情况下将返回「ASC」。Optional 类还允许客户端在需要时创建默认值,以防默认创建过程开销较大(即只在需要时创建默认值):

UserListUrl url = new UserListUrl("http://localhost/api/v2/users");
Optional<String> sortingParam = url.getSortingValue();
String sort = sortingParam.orElseGet(() -> {
    // Expensive computation
});
复制代码

结合「空对象」和默认值的用法,我们可以设计以下规则:

如果可能,使用「空对象」处理使用 null 关键字的情况,或者允许客户端提供默认值