Android Html.from() 末尾大量空白去除

Android Html.from() 末尾大量空白去除

解决方法

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
/**
* Trims trailing whitespace.
*
* Removes any of these characters:
* - 0009, HORIZONTAL TABULATION
* - 000A, LINE FEED
* - 000B, VERTICAL TABULATION
* - 000C, FORM FEED
* - 000D, CARRIAGE RETURN
* - 001C, FILE SEPARATOR
* - 001D, GROUP SEPARATOR
* - 001E, RECORD SEPARATOR
* - 001F, UNIT SEPARATOR
*
* @return "" if source is null, otherwise string with all trailing whitespace removed
*/
private fun CharSequence.trimTrailingWhitespace(): CharSequence {
var i = length

// loop back to the first non-whitespace character
while (--i >= 0 && Character.isWhitespace(this[i])) {
continue
}
return this.subSequence(0, i + 1)
}

Android 中的 MVP | MVVM 技术架构

Android 中的 MVP | MVVM 技术架构

1、MVP

全称:Model-View-Presenter ;MVP 是从经典的模式 MVC 演变而来,它们的基本思想有相通的地方 Controller / Presenter 负责逻辑的处理,Model 提供数据,View 负责显示。

优点

  1. 模型与视图完全分离,我们可以修改视图而不影响模型

  2. 可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部

  3. 我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。

  4. 如果我们把逻辑放在Presenter中,那么我们就可以脱离用户接口来测试这些逻辑(单元测试)

缺点

由于对视图的渲染放在了 Presenter 中,所以视图和 Presenter 的交互会过于频繁。还有一点需要明白,如果 Presenter 过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么 Presenter 也需要变更了。

2、MVVM

MVVM 是 Model-View-ViewModel 的简写。它本质上就是 MVP 的改进版。MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。唯一的区别是,它采用双向绑定(DataBinding),View的变动,自动反映在 ViewModel,反之亦然。

其中的 VM 是 ViewModel 的缩写,ViewModel 可以理解成是 View 的数据模型和 Presenter 的合体,ViewModel 和 View 之间的交互通过 DataBinding完成,而 DataBinding 可以实现双向的交互,这就使得视图和控制层之间的耦合程度进一步降低,关注点分离更为彻底,同时减轻了 Activity 的压力。

优点

  1. 双向绑定技术,当 Model 变化时,View-Model 会自动更新,View 也会自动变化。很好做到数据的一致性,不用担心,在模块的这一块数据是这个值,在另一块就是另一个值了。所以 MVVM 模式有些时候又被称作:model-view-binder 模式。
  2. View 的功能进一步的强化,具有控制的部分功能,若想无限增强它的功能,甚至控制器的全部功几乎都可以迁移到各个 View 上(不过这样不可取,那样 View 干了不属于它职责范围的事情)。View 可以像控制器一样具有自己的 View-Model。
  3. 由于控制器的功能大都移动到 View 上处理,大大的对控制器进行了瘦身。不用再为看到庞大的控制器逻辑而发愁了。
  4. 可以对 View 或 ViewController 的数据处理部分抽象出来一个函数处理 model。这样它们专职页面布局和页面跳转,它们必然更一步的简化。

缺点

  1. 数据绑定使得 Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
  2. 一个大的模块中,model 也会很大,虽然使用方便了也很容易保证了数据的一致性,当时长期持有,不释放内存,就造成了花费更多的内存。
  3. 数据双向绑定不利于代码重用。客户端开发最常用的重用是 View,但是数据双向绑定技术,让你在一个 View 都绑定了一个 model ,不同模块的 model 都不同。那就不能简单重用 View 了。

个人觉得 MVVM 有点像 H5 的 Vue 框架,微信小程序开发也是这种模式。

常见的设计模式

常见的设计模式

设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解。

当然,软件设计模式只是一个引导,在实际的软件开发中,必须根据具体的需求来选择:

  • 对于简单的程序,可能写一个简单的算法要比引入某种设计模式更加容易;
  • 但是对于大型项目开发或者框架设计,用设计模式来组织代码显然更好。

1、单例模式

单例模式,它的定义就是确保某一个类只有一个实例,并且提供一个全局访问点。

单例模式具备典型的3个特点:

  1. 只有一个实例。
  2. 自我实例化。
  3. 提供全局访问点。

因此当系统中只需要一个实例对象或者系统中只允许一个公共访问点,除了这个公共访问点外,不能通过其他访问点访问该实例时,可以使用单例模式。

单例模式的主要优点就是节约系统资源、提高了系统效率,同时也能够严格控制客户对它的访问。也许就是因为系统中只有一个实例,这样就导致了单例类的职责过重,违背了“单一职责原则”,同时也没有抽象类,所以扩展起来有一定的困难。

EventBus.getDefault()、Fragment 创建、网络请求工具类、SP 储存的工具类、弹窗工具类等等

2、抽象工厂模式

所谓抽象工厂模式就是提供一个接口,用于创建相关或者依赖对象的家族,而不需要明确指定具体类。他允许客户端使用抽象的接口来创建一组相关的产品,而不需要关系实际产出的具体产品是什么。这样一来,客户就可以从具体的产品中被解耦。

它的优点是隔离了具体类的生成,使得客户端不需要知道什么被创建了,而缺点就在于新增新的行为会比较麻烦,因为当添加一个新的产品对象时,需要更加需要更改接口及其下所有子类。

3、工厂方法模式

作为抽象工厂模式的孪生兄弟,工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个,也就是说工厂方法模式让实例化推迟到子类。

工厂方法模式非常符合“开闭原则”,当需要增加一个新的产品时,我们只需要增加一个具体的产品类和与之对应的具体工厂即可,无须修改原有系统。同时在工厂方法模式中用户只需要知道生产产品的具体工厂即可,无须关系产品的创建过程,甚至连具体的产品类名称都不需要知道。虽然他很好的符合了“开闭原则”,但是由于每新增一个新产品时就需要增加两个类,这样势必会导致系统的复杂度增加。

Activity 基类

4、建造者模式

对于建造者模式而已,它主要是将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。适用于那些产品对象的内部结构比较复杂。

建造者模式将复杂产品的构建过程封装分解在不同的方法中,使得创建过程非常清晰,能够让我们更加精确的控制复杂产品对象的创建过程,同时它隔离了复杂产品对象的创建和使用,使得相同的创建过程能够创建不同的产品。但是如果某个产品的内部结构过于复杂,将会导致整个系统变得非常庞大,不利于控制,同时若几个产品之间存在较大的差异,则不适用建造者模式,毕竟这个世界上存在相同点大的两个产品并不是很多,所以它的使用范围有限。

Android Dialog 创建

5、观察者模式

何谓观察者模式?观察者模式定义了对象之间的一对多依赖关系,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并且自动更新。

RxJava 的运用

6、代理模式

代理模式就是给一个对象提供一个代理,并由代理对象控制对原对象的引用。它使得客户不能直接与真正的目标对象通信。代理对象是目标对象的代表,其他需要与这个目标对象打交道的操作都是和这个代理对象在交涉。

代理对象可以在客户端和目标对象之间起到中介的作用,这样起到了的作用和保护了目标对象的,同时也在一定程度上面减少了系统的耦合度。

AIDL 应用

7、职责链模式

职责链模式描述的请求如何沿着对象所组成的链来传递的。它将对象组成一条链,发送者将请求发给链的第一个接收者,并且沿着这条链传递,直到有一个对象来处理它或者直到最后也没有对象处理而留在链末尾端。

避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止,这就是职责链模式。在职责链模式中,使得每一个对象都有可能来处理请求,从而实现了请求的发送者和接收者之间的解耦。同时职责链模式简化了对象的结构,它使得每个对象都只需要引用它的后继者即可,而不必了解整条链,这样既提高了系统的灵活性也使得增加新的请求处理类也比较方便。但是在职责链中我们不能保证所有的请求都能够被处理,而且不利于观察运行时特征。

OkHttp 拦截器相关的封装

附:单例模式的八种写法

1、饿汉式(静态常量)[可用]

1
2
3
4
5
6
7
8
9
10
public class Singleton {

private final static Singleton INSTANCE = new Singleton();

private Singleton(){}

public static Singleton getInstance(){
return INSTANCE;
}
}

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

2、饿汉式(静态代码块)[可用]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {

private static Singleton instance;

static {
instance = new Singleton();
}

private Singleton() {}

public Singleton getInstance() {
return instance;
}
}

这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。

3、懒汉式(线程不安全)[不可用]

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {

private static Singleton singleton;

private Singleton() {}

public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

4、懒汉式(线程安全,同步方法)[不推荐用]

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {

private static Singleton singleton;

private Singleton() {}

public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}

解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。

缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

5、懒汉式(线程安全,同步代码块)[不可用]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {

private static Singleton singleton;

private Singleton() {}

public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}

由于第四种实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

6、双重检查[推荐用]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton {

private static volatile Singleton singleton;

private Singleton() {}

public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

Double-Check概念对于多线程开发者来说不会陌生,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。

优点:线程安全;延迟加载;效率较高。

7、静态内部类[推荐用]

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {

private Singleton() {}

private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
}

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:避免了线程不安全,延迟加载,效率高。

8、枚举[推荐用]

1
2
3
4
5
6
public enum Singleton {
INSTANCE;
public void whateverMethod() {

}
}

借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。可能是因为枚举在JDK1.5中才添加,所以在实际项目开发中,很少见人这么写过。

Android 启动优化常用方案整理

Android 启动优化常用的方案整理

首先附上启动流程:Launcher 点击图标开始

  1. Launcher startActivity
  2. AMS 查找应用进程是否创建
  3. 未创建, AMS 通知从 Zygote 进程中 fork 创建出一个新的进程分配给该应用 
  4. 进程创建完毕, AMS 通过 Binder 通知应用进程
  5. ActivityThread 调用 performLaunchActivity
  6. Application 构造函数及 attachBaseContext() , onCreate()
  7. new Activity() , 并为此 Activity 创建一个new PhoneWindow(this)
  8. Activity onCreate()
  9. Activity setContentView()
  10. new DecorView() & addView(contentRoot, contentParent)
  11. onFinishInflate(): 此步骤只是 inflate 所有的 DecorView 上的布局 views,并不可见
  12. Activity onStart()
  13. Activity onResume()
  14. window.addView(mDecorView)
  15. View onAttachedToWindow() 、 onMeasure() 、 onSizeChanged() 、 onLayout() 、 onDraw()
  16. 至此步为止才把 DecorView 加给 Window , 应用首页才可见, onWindowFocusChanged(true)
  17. Activity onPause()
  18. View onWindowFocusChanged(false)
  19. Activity onStop()
  20. Activity onDestroy()
  21. View onDetackedFromWindow()

1、Application 初始化方面优化

在业务代码中,Application 启动时经常会初始化很多项目内的第三方库,这对 App 的启动会有很大影响,所以尽量把一些不太紧要的初始化异步执行,把必要的一些初始化才放到 onCreate 中执行。

至于异步初始化的方法则有:其他线程 、IntentService 、WorkManager 、MessageQueue.addIdleHandler() 等等进行处理。

IntentService Android 8.0+ 由于 Service 后台被限制已经弃用,推荐使用 JobIntentService 或 Jetpack 组件包的 WorkManager 进行代替使用。

2、优化首次安装或冷启动时,卡在白屏的视觉效果优化

在首次安装后或冷启动时,从 Launcher 中点击图标到闪屏页,在性能一般的机器上面,会有短时间的白屏或黑屏,这样对用户而言显然是不太好的。

我们可以在闪屏页配置一个背景,通常是一个和闪屏页相似的背景,然后就不会出现白屏或黑屏,而是设置的闪屏页背景,然后在显示闪屏页。

闪屏页即启动的第一个 Activity,App 启动速度和闪屏页的 onCreate 方法也有关系,所以尽量不要在闪屏页的 onCreate 做太多操作。

启动页主题

1
2
3
4
<style name="SplashTheme" parent="@android:style/Theme.Light.NoTitleBar.Fullscreen">
<item name="android:windowBackground">@drawable/splash_bg</item>
<item name="android:windowFullscreen">true</item>
</style>

splash_bg 可以是图片也可以是自定义的 layer 组合。

1
2
3
4
5
6
7
8
9
10
<activity
android:name=".SplashActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

3、MultiDex OPT 优化

Android 低版本4.x及以下,SDK < 21的设备,采用的 Java 运行环境是 Dalvik 虚拟机。它相比于高版本,最大的问题就是在安装或者升级更新之后,首次冷启动的耗时漫长。这常常需要花费几十秒甚至几分钟,用户不得不面对一片黑屏,熬过这段时间才能正常使用 APP。这个问题的根本原因就在于,安装或者升级后首次 MultiDex 花费的时间过于漫长。

推荐使用 抖音的 BoostMultiDex 优化
  1. 利用系统隐藏函数,直接加载原始 DEX 字节码,避免 ODEX 耗时
  2. 多级加载,在 DEX 字节码、DEX 文件、ODEX 文件中选取最合适的产物启动 APP
  3. 单独进程做 OPT ,并实现合理的中断及恢复机制

4、利用 Redex 工具优化 Dex

Redex 是 Facebook 的一个开源工具,官方的介绍是 An Android Bytecode Optimizer ,Android 字节码优化器。可以减小 Android Apk 的大小和提高 App 启动速度。

根据官方文档的说明,主要的优化项有以下几点:

  • 1、混淆和压缩
    类似 Android 中的 proguard ,将字节码中的类名、方法名等替换成简短的字符串。

  • 2、内联函数
    functionA -> functionB -> functionC ,内联后变成 functionA (包含 functionB 代码)-> functionC ,减少函数调用时间。

  • 3、删除代码
    类似于标记回收算法,从某些入口函数可以遍历标记出可以访问到的方法,最后未标记到的方法可被移除。理论比较简单,但在 Android 中还有反射,或者资源文件中对代码的引用等特殊情况需要考虑到。

  • 4、基于反馈的class分布
    也就是可以对 Dex 进行重组,根据使用方提供的 App 启动时加载的类序列配置文件调整 Dex 中类的顺序,把 App 冷启动时需要加载的类,放到 Dex前部。

  • 5、删除接口
    删除只有一个实现类的接口,直接使用实现类。

  • 6、删除 MetaData
    Dex 文件中的一些元数据在运行中并不会使用到。所以可以用 Dex 中已有的字符串代替 Java 源文件引用,删除运行时使用不到的注解。

5、其它优化方案

  • 提前加载 SharedPreferences

    在 Multidex 之前CPU是空闲的,加载系统类是可行的,所以可充分利用这段时间加载SharedPreferences 。

  • 启动阶段不启动子进程

    初始化子进程会消耗CPU资源,在启动阶段会导致主进程CPU资源紧张,导致启动阶段资源紧张初始化过慢。

  • 启动阶段不启动 Service 、ContentProvider

    在 Application 生命周期方法 attachBaseContext 方法和 onCreate 方法中间还会执行ContentProvider 生命周期方法,如果在 Application 初始化过程中启动了 Service 或者 Contentprovider 会执行 ContentProvider 生命周期方法,非常耗时。

  • 提前异步类加载

    对启动阶段用到的类进行提前异步加载,加载方法有两种:

    • Class.forName() 只加载类本身及其静态变量的引用类。
    • new 类实例 可以额外加载类成员变量的引用类。
  • 启动阶段抑制 GC

    支付宝就用了该方案

  • CPU 锁频

Android Studio Gradle Usage

Android Studio 常用的 Gradle 配置

1、排除第三方库的某些依赖

单条依赖排除引用的某个第三方库

1
2
3
implementation("android.support.v7.widget.Toolbar") {
exclude group: 'com.android.support', module: 'support-annotations'
}

全局配置排除

在需要排除的模块的配置文件 build.gradle 中,顶层插入下方配置相关的内容,在 all 内一行行添加需要排除的库即可,就不用在单个依赖下去排除。

1
2
3
4
5
6
7
8
configurations {
all {
// 排除某个组全部内容
exclude group: 'com.android.support'
// 排除某个组的具体模块
exclude group: 'com.android.support', module: 'support-annotations'
}
}

2、开启 ViewBinding

在 Android 项目的配置文件 build.gradle 中的 android 块中添加配置,重新同步即可

1
2
3
4
5
6
7
8
9
android {
//...

buildFeatures {
viewBinding true
}

//...
}

3、MultiDex 定义 main dex 中必须保留的类

1
2
3
4
5
6
7
8
9
10
android {
//...

defaultConfig {
//...

// 定义 main dex 中必须保留的类
multiDexKeepProguard file('mainDexClasses.pro')
}
}

mainDexClasses.pro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 这里只是示例
-keep public class * extends java.lang.Thread { *; }

-keep public class com.synaric.common.utils.SystemUtils { *; }

-keep public class com.synaric.common.utils.SPUtils { *; }

-keep interface android.content.SharedPreferences { *; }

-keep class android.os.Handler { *; }

-keep class com.synaric.common.BaseSPKey { *; }

-keep class android.os.Messenger { *; }

-keep class android.content.Intent { *; }

Gradle 配置请求超时时间

### Gradle 配置请求超时时间

打开 gradle.properties 文件

增加下面的内容:

1
2
systemProp.org.gradle.internal.http.socketTimeout=10000
systemProp.org.gradle.internal.http.connectionTimeout=10000

默认 Gradle 超时设置的较长,某些情况依赖加载不了或仓库一直阻塞不响应,但 Gradle 不会及时抛出超时,造成程序任务一直处于构建中,也不知道具体是哪个环节卡住,配置一个较短的超时可以及时发现每个依赖或仓库的连通性有问题,避免浪费宝贵的时间去找问题。

Androis Studio 引入 AspectJx 报错:Invalid byte tag in constant pool 19 解决方案

Androis Studio 引入 AspectJx 报错:Invalid byte tag in constant pool 19 (Zip file is empty) 解决方案

​ 一般出现该问题可能是项目使用了切面编程,同时又引入了 Kotlin,由于 AspectJx 版本过低未维护,不支持高版本的字节码导致的问题。
​ 解决方法如下:

  • 在 aspectjx 配置代码块内,添加一个排除组 versions.9
1
2
3
4
5
aspectjx {
enabled true
exclude 'xxx', 'xxx', 'versions.9'
include 'xxx', 'xxx'
}

然后重新 Sync 即可。

BottomSheetDialogFragment 去除白色背景,设置透明背景

BottomSheetDialogFragment 去除白色背景,设置透明背景

先直接贴代码:

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
/**
* 设置透明背景 BottomSheetDialogFragment
*
* @param skipCollapsed 是否跳过折叠
* @param dimAmount 对话框外部阴影比重(0f ~ 1f)
* @param dialogBehavior 其他交互控制
*/
@JvmStatic
fun BottomSheetDialogFragment.onStartTransparentDialog(
skipCollapsed: Boolean = true,
@FloatRange(from = 0.0, to = 1.0) dimAmount: Float = 0.25f,
dialogBehavior: BottomSheetBehavior<FrameLayout>.() -> Unit = {}
) {
val dialog = dialog as? BottomSheetDialog ?: return
val window = dialog.window ?: return
val bottomSheet = window.findViewById<View>(R.id.design_bottom_sheet)

window.setDimAmount(dimAmount)
window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))

dialogBehavior(dialog.behavior)
dialog.behavior.skipCollapsed = skipCollapsed

// 设置透明背景,光设置 setBackgroundColor 还不够,高版本的库默认加入了 backgroundTintList 的属性
// 所以还要修改背景色的着色为透明才行
bottomSheet?.backgroundTintMode = PorterDuff.Mode.CLEAR
bottomSheet?.backgroundTintList = ColorStateList.valueOf(Color.TRANSPARENT)
bottomSheet?.setBackgroundColor(Color.TRANSPARENT)
}

然后在 BottomSheetDialogFragment 的 onStart 回调内调用该拓展方法即可,也可传入相关参数调整样式。

1
2
3
4
5
6

override fun onStart() {
super.onStart()
onStartTransparentDialog()
}

查看白色背景在源码中哪里设置的

查看 BottomSheetDialog 源码,在下面的方法中,引入了一个布局 design_bottom_sheet_dialog.xml,其中的 bottomSheet 就是我们自定义传入视图的容器控件。

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

private FrameLayout ensureContainerAndBehavior() {
if (container == null) {
container =
(FrameLayout) View.inflate(getContext(), R.layout.design_bottom_sheet_dialog, null);

coordinator = (CoordinatorLayout) container.findViewById(R.id.coordinator);
bottomSheet = (FrameLayout) container.findViewById(R.id.design_bottom_sheet);

behavior = BottomSheetBehavior.from(bottomSheet);
behavior.addBottomSheetCallback(bottomSheetCallback);
behavior.setHideable(cancelable);
}
return container;
}

布局文件 design_bottom_sheet_dialog.xml 内容如下

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

<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">

<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">

<View
android:id="@+id/touch_outside"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="false"
android:importantForAccessibility="no"
android:soundEffectsEnabled="false"
tools:ignore="UnusedAttribute"/>

<FrameLayout
android:id="@+id/design_bottom_sheet"
style="?attr/bottomSheetStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|top"
app:layout_behavior="@string/bottom_sheet_behavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

</FrameLayout>

可以看出 bottomSheet 默认会去引入一个全局的主题样式 bottomSheetStyle,点进去发现是引入的 Widget.MaterialComponents.BottomSheet.Modal,挨着挨着查找下去:

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

<style name="Base.V14.ThemeOverlay.MaterialComponents.BottomSheetDialog" parent="Base.ThemeOverlay.MaterialComponents.Dialog">
<item name="android:windowIsFloating">false</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowAnimationStyle">@style/Animation.MaterialComponents.BottomSheetDialog</item>
<item name="bottomSheetStyle">@style/Widget.MaterialComponents.BottomSheet.Modal</item>
<item name="enableEdgeToEdge">true</item>
<item name="paddingBottomSystemWindowInsets">true</item>
<item name="paddingLeftSystemWindowInsets">true</item>
<item name="paddingRightSystemWindowInsets">true</item>
<item name="paddingTopSystemWindowInsets">true</item>
</style>

...

<style name="Widget.MaterialComponents.BottomSheet.Modal" parent="Widget.MaterialComponents.BottomSheet">
<item name="android:elevation" ns1:ignore="NewApi">
@dimen/design_bottom_sheet_modal_elevation
</item>
</style>

...

<style name="Widget.MaterialComponents.BottomSheet" parent="Widget.Design.BottomSheet.Modal">
...
<item name="android:background">@null</item>
<item name="backgroundTint">?attr/colorSurface</item>
...
</style>

...

<style name="Base.V14.Theme.MaterialComponents.Bridge" parent="Platform.MaterialComponents">
...
<item name="colorSurface">@color/design_dark_default_color_surface</item>
...
</style>

...

<color name="design_dark_default_color_surface">#121212</color>

很容易可以看出,这个默认白色背景的来源了。

在 Ubuntu 上编译 FreeRDP Android 端的二进制 so 库

在 Ubuntu 上编译 FreeRDP Android 端的二进制 so 库

需要用到的环境和软件:

  • Java
  • make (Ubuntu 自带,没有需要安装)
  • git (自行安装)
  • python3 (Ubuntu 自带,没有需要安装)
  • Android Studio
  • cmake (使用Adnroid Studio 下载)
  • Android Sdk (使用Adnroid Studio 下载)
  • Android Ndk (使用Adnroid Studio 下载)

1、安装 Jdk17

  1. 下载官方 Jdk17 文件:

    1
    2
    3
    4
    mkdir /usr/local/java/jdk17
    wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz -P /usr/local/java/jdk17
    或
    wget http://49.232.8.65/jdk/jdk17/jdk-17_linux-x64_bin.tar.gz -P /usr/local/java/jdk17
  2. 解压

    1
    tar xf /home/jdk17/jdk-17_linux-x64_bin.tar.gz -C /usr/local/java/jdk17
  3. 设置环境变量

    gedit /etc/profile ,在文件最下方添加以下内容:

    后面的版本号根据你自己解压下来的 Jdk 文件夹版本名称而定

    1
    2
    3
    # JAVA
    export JAVA_HOME=/usr/local/java/jdk17/jdk-17.0.1
    export PATH=$JAVA_HOME/bin:$PATH

2、安装 Android Studio

  1. 下载最新版本的 Android Studio,下载完成后解压到你想要的目录即可。

    1
    wget https://redirector.gvt1.com/edgedl/android/studio/ide-zips/2021.1.1.21/android-studio-2021.1.1.21-linux.tar.gz

    我这里解压后,将解压出来的 android-studio 目录放到了 /usr/local/ 目录下:

    1
    /usr/local/android-studio

    进入 android-studio/bin 目录,执行 ./studio.sh 启动软件。

  2. 启动成功后,配置完成,记录下你配置的 SDK 目录,建议一路配置都用默认即可

    我这里的 SDK 目录默认为:

    1
    /root/Android/Sdk

    然后进入新建项目的界面,我们不用新建项目,点击左侧的 Customize -> All settings ,在设置搜索框里输入 SDK 按回车键。

    然后点开 Android SDK 的配置页面:选中 SDK Tools Tab,勾选中 NDK (Size by side) 和 cmake ,然后点击 Apply,等待下载完成关闭 Android Studio。

  3. 然后进入 SDK 目录,你会发现 cmake 和 ndk 都在该目录内。

    注意:

    cmake 目录下没有直接放相关文件,而是多了一层版本号的文件夹,这里需要修改一下,将版本号文件夹内的全部文件复制一份到上一级目录下,免得后面执行编译脚本报错。

    cmake 路径修改,$SDK/cmake/3.18.1/xxxxx -> $SDK/cmake/xxxxx

  4. 添加环境变量

    gedit /etc/profile ,在文件最下方添加以下内容:

    版本号和你的有差异请自己替换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # ANDROID_SDK
    export ANDROID_SDK=/root/Android/Sdk
    export PATH=$ANDROID_SDK/tools:$PATH

    # ANDROID_NDK
    export ANDROID_NDK=$ANDROID_SDK/ndk/23.1.7779620
    export PATH=$ANDROID_NDK:$PATH

    # ANDROID_CMAKE
    export ANDROID_CMAKE=$ANDROID_SDK/cmake/bin:$PATH
    export PATH=$ANDROID_CMAKE:$PATH

    然后刷新配置

    1
    source /etc/profile

    此时可以输入:java -version 、cmake 、 ndk-build 进行检测是否配置好

3、下载 FreeRdp

  1. 在 /root 目录新建一个文件夹 Work,进入该文件夹:

  2. 下载 Github 源码:

    1
    git clone https://github.com/FreeRDP/FreeRDP.git
  3. 开始编译

    进入克隆下来的 FreeRDP 目录,输入以下命令开始编译

    1
    ./scripts/android-build-freerdp.sh --ndk $ANDROID_NDK --sdk $ANDROID_SDK --release

    若需要编译 H264,修改 android-build-release.conf 开启 H264 WITH_OPENH264=1

    1
    ./scripts/android-build-freerdp.sh --ndk $ANDROID_NDK --sdk $ANDROID_SDK --openh264-ndk $ANDROID_NDK_15C --openh264 --release

    等待编译完成,若前面的步骤有错误,期间可能会编译报错,看错误提示解决后,重新执行上面的编译命令即可。

  4. 查看编译好的 so 文件

    编译完成后,so 存放目录为:

    1
    /root/Work/FreeRDP/client/Android/Studio/freeRDPCore/src/main/jniLibs

4、到此编译完成

剩下的就是把这个导出来,放到 Android Studio 相关的项目里就行了,具体相关的源码可以访问 FreeRDP Github 地址:https://github.com/FreeRDP/FreeRDP