Sử dụng các trình bổ trợ của thư viện giao diện người dùng trên ô tô để tạo các chế độ triển khai hoàn chỉnh về chế độ tuỳ chỉnh thành phần trong thư viện giao diện người dùng trên ô tô thay vì sử dụng lớp phủ tài nguyên thời gian chạy (RRO). RRO cho phép bạn chỉ thay đổi các tài nguyên XML của các thành phần trong thư viện giao diện người dùng trên ô tô, điều này hạn chế mức độ tuỳ chỉnh mà bạn có thể thực hiện.
Tạo trình bổ trợ
Trình bổ trợ thư viện giao diện người dùng trên ô tô là một APK chứa các lớp triển khai một nhóm API trình bổ trợ. Bạn có thể biên dịch API trình bổ trợ thành một trình bổ trợ dưới dạng thư viện tĩnh.
Xem ví dụ trong Soong và Gradle:
Soong
Hãy xem xét ví dụ sau về Soong:
android_app {
name: "my-plugin",
min_sdk_version: "28",
target_sdk_version: "30",
aaptflags: ["--shared-lib"],
sdk_version: "current",
manifest: "src/main/AndroidManifest.xml",
srcs: ["src/main/java/**/*.java"],
resource_dirs: ["src/main/res"],
static_libs: [
"car-ui-lib-oem-apis",
],
// Disable optimization is mandatory to prevent R.java class from being
// stripped out
optimize: {
enabled: false,
},
certificate: ":my-plugin-certificate",
}
Gradle
Xem tệp build.gradle
này:
apply plugin: 'com.android.application'
android {
compileSdkVersion 30
defaultConfig {
minSdkVersion 28
targetSdkVersion 30
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
signingConfigs {
debug {
storeFile file('chassis_upload_key.jks')
storePassword 'chassis'
keyAlias 'chassis'
keyPassword 'chassis'
}
}
}
dependencies {
implementation project(':oem-apis')
// Or use the following if you'd like to use the maven artifact
// implementation 'com.android.car.ui:car-ui-lib-plugin-apis:1.0.0'
}
Settings.gradle
:
// You can remove the ':oem-apis' if you're using the maven artifact.
include ':oem-apis'
project(':oem-apis').projectDir = new File('./path/to/oem-apis')
include ':my-plugin'
project(':my-plugin').projectDir = new File('./my-plugin')
Trình bổ trợ phải có một trình cung cấp nội dung được khai báo trong tệp kê khai có các thuộc tính sau:
android:authorities="com.android.car.ui.plugin"
android:enabled="true"
android:exported="true"
android:authorities="com.android.car.ui.plugin"
giúp thư viện giao diện người dùng trên ô tô phát hiện trình bổ trợ. Bạn phải xuất trình cung cấp để có thể truy vấn tại thời gian chạy. Ngoài ra, nếu bạn đặt thuộc tính enabled
thành false
, thì chế độ triển khai mặc định sẽ được dùng thay vì chế độ triển khai trình bổ trợ. Lớp trình cung cấp nội dung không nhất thiết phải tồn tại. Trong trường hợp đó, hãy nhớ thêm tools:ignore="MissingClass"
vào định nghĩa nhà cung cấp. Hãy xem mục nhập tệp kê khai mẫu bên dưới:
<application>
<provider
android:name="com.android.car.ui.plugin.PluginNameProvider"
android:authorities="com.android.car.ui.plugin"
android:enabled="false"
android:exported="true"
tools:ignore="MissingClass"/>
</application>
Cuối cùng, để đảm bảo an toàn, hãy Ký ứng dụng của bạn.
Các trình bổ trợ dưới dạng thư viện dùng chung
Không giống như các thư viện tĩnh Android được biên dịch trực tiếp vào các ứng dụng, các thư viện dùng chung Android được biên dịch thành một APK độc lập mà các ứng dụng khác tham chiếu trong thời gian chạy.
Các trình bổ trợ được triển khai dưới dạng một thư viện dùng chung trên Android sẽ tự động thêm các lớp của chúng vào trình tải lớp dùng chung giữa các ứng dụng. Khi một ứng dụng sử dụng thư viện giao diện người dùng trên ô tô chỉ định một phần phụ thuộc thời gian chạy trên thư viện chia sẻ trình bổ trợ, trình tải lớp của ứng dụng đó có thể truy cập vào các lớp của thư viện chia sẻ trình bổ trợ. Các trình bổ trợ được triển khai dưới dạng ứng dụng Android thông thường (không phải là thư viện dùng chung) có thể ảnh hưởng tiêu cực đến thời gian khởi động nguội của ứng dụng.
Triển khai và tạo thư viện dùng chung
Việc phát triển bằng các thư viện dùng chung của Android cũng tương tự như các ứng dụng Android thông thường, chỉ có một vài điểm khác biệt chính.
- Sử dụng thẻ
library
trong thẻapplication
có tên gói trình bổ trợ trong tệp kê khai ứng dụng của trình bổ trợ:
<application>
<library android:name="com.chassis.car.ui.plugin" />
...
</application>
- Định cấu hình quy tắc bản dựng Soong
android_app
(Android.bp
) bằng cờ AAPTshared-lib
. Cờ này dùng để tạo một thư viện dùng chung:
android_app {
...
aaptflags: ["--shared-lib"],
...
}
Phụ thuộc vào thư viện dùng chung
Đối với mỗi ứng dụng trên hệ thống sử dụng Thư viện giao diện người dùng trên ô tô, hãy thêm thẻ uses-library
vào tệp kê khai ứng dụng trong thẻ application
bằng tên gói trình bổ trợ:
<manifest>
<application
android:name=".MyApp"
...>
<uses-library android:name="com.chassis.car.ui.plugin" android:required="false"/>
...
</application>
</manifest>
Cài đặt trình bổ trợ
Bạn PHẢI cài đặt sẵn các trình bổ trợ trên phân vùng hệ thống bằng cách thêm mô-đun vào PRODUCT_PACKAGES
. Bạn có thể cập nhật gói được cài đặt sẵn tương tự như mọi ứng dụng đã cài đặt khác.
Nếu bạn đang cập nhật một trình bổ trợ hiện có trên hệ thống, thì mọi ứng dụng sử dụng trình bổ trợ đó sẽ tự động đóng. Sau khi người dùng mở lại, họ sẽ thấy các thay đổi mới nhất. Nếu ứng dụng không chạy, thì lần tiếp theo khi ứng dụng này khởi động, ứng dụng sẽ có trình bổ trợ đã cập nhật.
Khi cài đặt một trình bổ trợ bằng Android Studio, bạn cần cân nhắc thêm một số điều. Tại thời điểm viết bài này, có một lỗi trong quy trình cài đặt ứng dụng Android Studio khiến các bản cập nhật cho một trình bổ trợ không có hiệu lực. Bạn có thể khắc phục vấn đề này bằng cách chọn tuỳ chọn Luôn cài đặt bằng trình quản lý gói (tắt chế độ tối ưu hoá triển khai trên Android 11 trở lên) trong cấu hình bản dựng của trình bổ trợ.
Ngoài ra, khi cài đặt trình bổ trợ, Android Studio báo cáo lỗi rằng không tìm thấy hoạt động chính để chạy. Điều này là bình thường vì trình bổ trợ không có hoạt động nào (ngoại trừ ý định trống dùng để phân giải ý định). Để loại bỏ lỗi này, hãy thay đổi lựa chọn Launch (Khởi chạy) thành Nothing (Không có gì) trong cấu hình bản dựng.
Hình 1. Cấu hình trình bổ trợ Android Studio
Trình bổ trợ proxy
Việc tuỳ chỉnh các ứng dụng bằng Thư viện giao diện người dùng ô tô yêu cầu một RRO nhắm đến từng ứng dụng cụ thể cần được sửa đổi, kể cả khi các chế độ tuỳ chỉnh giống nhau trên các ứng dụng. Điều này có nghĩa là bạn phải có một RRO cho mỗi ứng dụng. Xem những ứng dụng sử dụng thư viện Giao diện người dùng trên ô tô.
Trình bổ trợ proxy thư viện giao diện người dùng trên ô tô là một ví dụ về thư viện dùng chung của trình bổ trợ. Thư viện này uỷ quyền việc triển khai thành phần cho phiên bản tĩnh của thư viện giao diện người dùng trên ô tô. Bạn có thể nhắm đến trình bổ trợ này bằng một RRO. RRO có thể được dùng làm một điểm tuỳ chỉnh duy nhất cho các ứng dụng sử dụng Thư viện giao diện người dùng trên ô tô mà không cần triển khai trình bổ trợ chức năng. Để biết thêm thông tin về RRO, hãy xem bài viết Thay đổi giá trị của tài nguyên ứng dụng trong thời gian chạy.
Plugin proxy chỉ là một ví dụ và điểm bắt đầu để tuỳ chỉnh bằng cách sử dụng một plugin. Để tuỳ chỉnh ngoài RRO, bạn có thể triển khai một nhóm nhỏ các thành phần trình bổ trợ và sử dụng trình bổ trợ proxy cho phần còn lại hoặc triển khai tất cả các thành phần trình bổ trợ hoàn toàn từ đầu.
Mặc dù trình bổ trợ proxy cung cấp một điểm tuỳ chỉnh RRO duy nhất cho các ứng dụng, nhưng những ứng dụng chọn không dùng trình bổ trợ này vẫn sẽ yêu cầu một RRO nhắm trực tiếp đến chính ứng dụng đó.
Triển khai các API trình bổ trợ
Điểm truy cập chính vào trình bổ trợ là lớp com.android.car.ui.plugin.PluginVersionProviderImpl
. Tất cả các trình bổ trợ phải có một lớp có tên và tên gói chính xác như vậy. Lớp này phải có một hàm khởi tạo mặc định và triển khai giao diện PluginVersionProviderOEMV1
.
Các trình bổ trợ CarUi phải hoạt động với những ứng dụng cũ hơn hoặc mới hơn trình bổ trợ. Để tạo điều kiện thuận lợi cho việc này, tất cả các API trình bổ trợ đều được gắn phiên bản bằng V#
ở cuối tên lớp. Nếu một phiên bản mới của thư viện Giao diện người dùng trên ô tô được phát hành cùng với các tính năng mới, thì các tính năng đó sẽ thuộc phiên bản V2
của thành phần. Thư viện Giao diện người dùng trên ô tô cố gắng hết sức để các tính năng mới hoạt động trong phạm vi của một thành phần trình bổ trợ cũ.
Ví dụ: bằng cách chuyển đổi một loại nút mới trên thanh công cụ thành MenuItems
.
Tuy nhiên, một ứng dụng có phiên bản cũ của thư viện Car UI không thể thích ứng với một trình bổ trợ mới được viết dựa trên các API mới hơn. Để giải quyết vấn đề này, chúng tôi cho phép các trình bổ trợ trả về các cách triển khai khác nhau của chính chúng dựa trên phiên bản API OEM mà các ứng dụng hỗ trợ.
PluginVersionProviderOEMV1
có một phương thức trong đó:
Object getPluginFactory(int maxVersion, Context context, String packageName);
Phương thức này trả về một đối tượng triển khai phiên bản cao nhất của PluginFactoryOEMV#
mà trình bổ trợ hỗ trợ, trong khi vẫn nhỏ hơn hoặc bằng maxVersion
. Nếu một trình bổ trợ không có quá trình triển khai PluginFactory
cũ, thì trình bổ trợ đó có thể trả về null
. Trong trường hợp này, quá trình triển khai được liên kết tĩnh của các thành phần CarUi sẽ được dùng.
Để duy trì khả năng tương thích ngược với các ứng dụng được biên dịch dựa trên các phiên bản cũ hơn của thư viện Car Ui tĩnh, bạn nên hỗ trợ maxVersion
từ 2, 5 trở lên trong quá trình triển khai maxVersion
của trình bổ trợ đối với lớp PluginVersionProvider
. Phiên bản 1, 3 và 4 không được hỗ trợ. Để biết thêm thông tin, hãy xem PluginVersionProviderImpl
.
PluginFactory
là giao diện tạo tất cả các thành phần CarUi khác. Nó cũng xác định phiên bản giao diện nào nên được sử dụng. Nếu không tìm cách triển khai bất kỳ thành phần nào trong số này, thì trình bổ trợ có thể trả về null
trong hàm tạo của các thành phần (ngoại trừ thanh công cụ có hàm customizesBaseLayout()
riêng).
pluginFactory
giới hạn những phiên bản thành phần CarUi có thể dùng cùng nhau. Ví dụ: sẽ không bao giờ có pluginFactory
có thể tạo phiên bản 100 của Toolbar
và cả phiên bản 1 của RecyclerView
, vì sẽ có rất ít đảm bảo rằng nhiều phiên bản của các thành phần sẽ hoạt động cùng nhau. Để sử dụng thanh công cụ phiên bản 100, nhà phát triển phải cung cấp một quy trình triển khai phiên bản pluginFactory
để tạo thanh công cụ phiên bản 100. Sau đó, quy trình này sẽ giới hạn các lựa chọn về phiên bản của những thành phần khác có thể được tạo. Các phiên bản của những thành phần khác có thể không bằng nhau, ví dụ: pluginFactoryOEMV100
có thể tạo ToolbarControllerOEMV100
và RecyclerViewOEMV70
.
Thanh công cụ
Bố cục cơ bản
Thanh công cụ và "bố cục cơ bản" có mối quan hệ rất chặt chẽ, do đó, hàm tạo thanh công cụ được gọi là installBaseLayoutAround
. Bố cục cơ bản là một khái niệm cho phép đặt thanh công cụ ở bất kỳ vị trí nào xung quanh nội dung của ứng dụng, cho phép có một thanh công cụ ở trên cùng/dưới cùng của ứng dụng, theo chiều dọc dọc theo các cạnh hoặc thậm chí là một thanh công cụ hình tròn bao quanh toàn bộ ứng dụng. Điều này được thực hiện bằng cách truyền một khung hiển thị đến installBaseLayoutAround
để thanh công cụ/bố cục cơ bản bao quanh.
Trình bổ trợ sẽ lấy thành phần hiển thị được cung cấp, tách thành phần hiển thị đó khỏi thành phần hiển thị gốc, tăng cường bố cục riêng của trình bổ trợ ở cùng chỉ mục của thành phần hiển thị gốc và có cùng LayoutParams
với thành phần hiển thị vừa được tách, sau đó đính kèm lại thành phần hiển thị ở đâu đó bên trong bố cục vừa được tăng cường. Bố cục được mở rộng sẽ chứa thanh công cụ, nếu ứng dụng yêu cầu.
Ứng dụng có thể yêu cầu bố cục cơ bản mà không có thanh công cụ. Nếu có, installBaseLayoutAround
sẽ trả về giá trị rỗng. Đối với hầu hết các trình bổ trợ, đó là tất cả những gì cần xảy ra, nhưng nếu tác giả trình bổ trợ muốn áp dụng chẳng hạn như một thành phần trang trí xung quanh cạnh của ứng dụng, thì vẫn có thể thực hiện việc đó bằng bố cục cơ bản. Những thành phần trang trí này đặc biệt hữu ích cho các thiết bị có màn hình không phải hình chữ nhật, vì chúng có thể đẩy ứng dụng vào một không gian hình chữ nhật và thêm các hiệu ứng chuyển đổi rõ ràng vào không gian không phải hình chữ nhật.
installBaseLayoutAround
cũng được truyền một Consumer<InsetsOEMV1>
. Người dùng này có thể được dùng để thông báo cho ứng dụng rằng trình bổ trợ đang che phủ một phần nội dung của ứng dụng (bằng thanh công cụ hoặc cách khác). Sau đó, ứng dụng sẽ biết cách tiếp tục vẽ trong không gian này, nhưng vẫn giữ mọi thành phần quan trọng mà người dùng có thể tương tác bên ngoài không gian đó. Hiệu ứng này được dùng trong thiết kế tham chiếu của chúng tôi để làm cho thanh công cụ bán trong suốt và có các danh sách cuộn bên dưới. Nếu tính năng này không được triển khai, mục đầu tiên trong danh sách sẽ bị kẹt bên dưới thanh công cụ và không thể nhấp vào. Nếu không cần hiệu ứng này, thì trình bổ trợ có thể bỏ qua Consumer.
Hình 2. Nội dung cuộn bên dưới thanh công cụ
Theo góc độ của ứng dụng, khi trình bổ trợ gửi phần lồng ghép mới, ứng dụng sẽ nhận được phần lồng ghép đó từ mọi hoạt động hoặc mảnh triển khai InsetsChangedListener
. Nếu một hoạt động hoặc mảnh không triển khai InsetsChangedListener
, thì thư viện Car Ui sẽ xử lý phần lồng ghép theo mặc định bằng cách áp dụng phần lồng ghép làm khoảng đệm cho Activity
hoặc FragmentActivity
chứa mảnh đó. Theo mặc định, thư viện không áp dụng phần lồng ghép cho các mảnh. Dưới đây là một đoạn mã mẫu của một quy trình triển khai áp dụng phần lồng ghép làm khoảng đệm trên RecyclerView
trong ứng dụng:
public class MainActivity extends Activity implements InsetsChangedListener {
@Override
public void onCarUiInsetsChanged(Insets insets) {
CarUiRecyclerView rv = requireViewById(R.id.recyclerview);
rv.setPadding(insets.getLeft(), insets.getTop(),
insets.getRight(), insets.getBottom());
}
}
Cuối cùng, trình bổ trợ sẽ nhận được một gợi ý fullscreen
. Gợi ý này được dùng để cho biết liệu khung hiển thị cần được bao bọc có chiếm toàn bộ ứng dụng hay chỉ một phần nhỏ.
Bạn có thể dùng thuộc tính này để tránh áp dụng một số thành phần trang trí dọc theo cạnh mà chỉ có ý nghĩa nếu chúng xuất hiện dọc theo cạnh của toàn bộ màn hình. Một ứng dụng mẫu sử dụng bố cục cơ sở không ở chế độ toàn màn hình là Settings (Cài đặt), trong đó mỗi ngăn của bố cục hai ngăn đều có thanh công cụ riêng.
Vì installBaseLayoutAround
dự kiến sẽ trả về giá trị rỗng khi toolbarEnabled
là false
, nên để trình bổ trợ cho biết rằng trình bổ trợ không muốn tuỳ chỉnh bố cục cơ bản, trình bổ trợ phải trả về false
từ customizesBaseLayout
.
Bố cục cơ sở phải chứa một FocusParkingView
và một FocusArea
để hỗ trợ đầy đủ các chế độ điều khiển xoay. Bạn có thể bỏ qua các khung hiển thị này trên những thiết bị không hỗ trợ thao tác xoay. FocusParkingView/FocusAreas
được triển khai trong thư viện CarUi tĩnh, vì vậy, setRotaryFactories
được dùng để cung cấp các nhà máy nhằm tạo thành phần hiển thị từ các ngữ cảnh.
Các ngữ cảnh dùng để tạo khung hiển thị Lấy tiêu điểm phải là ngữ cảnh nguồn, chứ không phải ngữ cảnh của trình bổ trợ. FocusParkingView
phải gần với khung hiển thị đầu tiên trong cây nhất có thể, vì đây là khung hiển thị được lấy làm tiêu điểm khi không có tiêu điểm nào hiển thị cho người dùng. FocusArea
phải bao bọc thanh công cụ trong bố cục cơ sở để cho biết đó là vùng thao tác xoay. Nếu không cung cấp FocusArea
, người dùng sẽ không thể chuyển đến bất kỳ nút nào trong thanh công cụ bằng bộ điều khiển xoay.
Bộ điều khiển thanh công cụ
ToolbarController
thực tế được trả về sẽ dễ triển khai hơn nhiều so với bố cục cơ bản. Nhiệm vụ của nó là lấy thông tin được truyền đến các setter và hiển thị thông tin đó trong bố cục cơ sở. Hãy xem Javadoc để biết thông tin về hầu hết các phương thức. Một số phương pháp phức tạp hơn sẽ được thảo luận dưới đây.
getImeSearchInterface
được dùng để hiển thị kết quả tìm kiếm trong cửa sổ IME (bàn phím). Điều này có thể hữu ích khi hiển thị/tạo hiệu ứng cho kết quả tìm kiếm cùng với bàn phím, chẳng hạn như nếu bàn phím chỉ chiếm một nửa màn hình. Hầu hết chức năng được triển khai trong thư viện CarUi tĩnh, giao diện tìm kiếm trong trình bổ trợ chỉ cung cấp các phương thức để thư viện tĩnh nhận các lệnh gọi lại TextView
và onPrivateIMECommand
. Để hỗ trợ việc này, trình bổ trợ phải sử dụng một lớp con TextView
ghi đè onPrivateIMECommand
và truyền lệnh gọi đến trình nghe được cung cấp dưới dạng TextView
của thanh tìm kiếm.
setMenuItems
chỉ hiển thị MenuItems trên màn hình, nhưng sẽ được gọi thường xuyên một cách đáng ngạc nhiên. Vì API trình bổ trợ cho MenuItem là bất biến, nên bất cứ khi nào MenuItem thay đổi, một lệnh gọi setMenuItems
hoàn toàn mới sẽ xảy ra. Điều này có thể xảy ra đối với một việc nhỏ nhặt như người dùng nhấp vào một MenuItem chuyển đổi và lượt nhấp đó khiến công tắc chuyển đổi. Vì cả lý do hiệu suất và ảnh động, bạn nên tính toán sự khác biệt giữa danh sách MenuItems cũ và mới, đồng thời chỉ cập nhật những khung hiển thị thực sự thay đổi. MenuItems cung cấp một trường key
có thể giúp bạn thực hiện việc này, vì khoá phải giống nhau trong các lệnh gọi khác nhau đến setMenuItems
cho cùng một MenuItem.
AppStyledView
AppStyledView
là một vùng chứa cho khung hiển thị hoàn toàn không được tuỳ chỉnh. Bạn có thể dùng thuộc tính này để cung cấp một đường viền xung quanh khung hiển thị đó, giúp khung hiển thị nổi bật so với phần còn lại của ứng dụng và cho người dùng biết rằng đây là một loại giao diện khác. Khung hiển thị được AppStyledView bao bọc sẽ xuất hiện trong setContent
. AppStyledView
cũng có thể có nút quay lại hoặc đóng theo yêu cầu của ứng dụng.
AppStyledView
không chèn ngay các thành phần hiển thị của nó vào hệ phân cấp thành phần hiển thị như installBaseLayoutAround
, mà thay vào đó, chỉ trả về thành phần hiển thị của nó cho thư viện tĩnh thông qua getView
, sau đó thư viện này sẽ thực hiện việc chèn. Bạn cũng có thể kiểm soát vị trí và kích thước của AppStyledView
bằng cách triển khai getDialogWindowLayoutParam
.
Ngữ cảnh
Plugin phải cẩn thận khi sử dụng Ngữ cảnh, vì có cả ngữ cảnh plugin và ngữ cảnh "nguồn". Ngữ cảnh trình bổ trợ được đưa ra dưới dạng một đối số cho getPluginFactory
và là ngữ cảnh duy nhất có tài nguyên của trình bổ trợ trong đó. Điều này có nghĩa là đây là ngữ cảnh duy nhất có thể dùng để mở rộng bố cục trong trình bổ trợ.
Tuy nhiên, ngữ cảnh của trình bổ trợ có thể không được thiết lập đúng cấu hình. Để có được cấu hình chính xác, chúng tôi cung cấp các ngữ cảnh nguồn trong các phương thức tạo thành phần. Ngữ cảnh nguồn thường là một hoạt động, nhưng trong một số trường hợp cũng có thể là một Dịch vụ hoặc thành phần Android khác. Để sử dụng cấu hình từ ngữ cảnh nguồn với các tài nguyên từ ngữ cảnh trình bổ trợ, bạn phải tạo một ngữ cảnh mới bằng cách sử dụng createConfigurationContext
. Nếu bạn không sử dụng cấu hình chính xác, sẽ có một lỗi vi phạm chế độ nghiêm ngặt của Android và các khung hiển thị được mở rộng có thể không có kích thước chính xác.
Context layoutInflationContext = pluginContext.createConfigurationContext(
sourceContext.getResources().getConfiguration());
Thay đổi chế độ
Một số trình bổ trợ có thể hỗ trợ nhiều chế độ cho các thành phần của chúng, chẳng hạn như chế độ thể thao hoặc chế độ tiết kiệm năng lượng có vẻ ngoài khác biệt. CarUi không có hỗ trợ tích hợp cho chức năng này, nhưng không có gì ngăn trình bổ trợ triển khai hoàn toàn chức năng này trong nội bộ. Trình bổ trợ có thể theo dõi bất kỳ điều kiện nào mà trình bổ trợ muốn tìm hiểu thời điểm chuyển đổi chế độ, chẳng hạn như nghe các thông báo truyền tin. Trình bổ trợ này không thể kích hoạt thay đổi về cấu hình để thay đổi chế độ, nhưng bạn không nên dựa vào các thay đổi về cấu hình, vì việc cập nhật giao diện của từng thành phần theo cách thủ công sẽ mượt mà hơn đối với người dùng và cũng cho phép các hiệu ứng chuyển đổi không thể thực hiện được bằng các thay đổi về cấu hình.
Jetpack Compose
Bạn có thể triển khai các trình bổ trợ bằng Jetpack Compose, nhưng đây là một tính năng ở cấp độ alpha và không được coi là ổn định.
Các trình bổ trợ có thể dùng ComposeView
để tạo một nền tảng hỗ trợ Compose để kết xuất. ComposeView
này sẽ là giá trị được trả về từ ứng dụng từ phương thức getView
trong các thành phần.
Một vấn đề lớn khi sử dụng ComposeView
là nó đặt các thẻ trên khung hiển thị gốc trong bố cục để lưu trữ các biến toàn cục được chia sẻ trên nhiều ComposeView trong hệ phân cấp. Vì mã tài nguyên của trình bổ trợ không được đặt tên riêng biệt với mã tài nguyên của ứng dụng, nên điều này có thể gây ra xung đột khi cả ứng dụng và trình bổ trợ đều đặt thẻ trên cùng một khung hiển thị. Một ComposeViewWithLifecycle
tuỳ chỉnh di chuyển các biến chung này xuống ComposeView
được cung cấp bên dưới. Xin nhắc lại rằng bạn không nên coi đây là một phiên bản ổn định.
ComposeViewWithLifecycle
:
class ComposeViewWithLifecycle @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr),
LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
private val lifeCycle = LifecycleRegistry(this)
private val modelStore = ViewModelStore()
private val savedStateRegistryController = SavedStateRegistryController.create(this)
private var composeView: ComposeView? = null
private var content = @Composable {}
init {
ViewTreeLifecycleOwner.set(this, this)
ViewTreeViewModelStoreOwner.set(this, this)
ViewTreeSavedStateRegistryOwner.set(this, this)
compositionContext = createCompositionContext()
}
fun setContent(content: @Composable () -> Unit) {
this.content = content
composeView?.setContent(content)
}
override fun getLifecycle(): Lifecycle {
return lifeCycle
}
override fun getViewModelStore(): ViewModelStore {
return modelStore
}
override fun getSavedStateRegistry(): SavedStateRegistry {
return savedStateRegistryController.savedStateRegistry
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
savedStateRegistryController.performRestore(Bundle())
lifeCycle.currentState = Lifecycle.State.RESUMED
composeView = ComposeView(context)
composeView?.setContent(content)
addView(composeView, LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
lifeCycle.currentState = Lifecycle.State.DESTROYED
modelStore.clear()
removeAllViews()
composeView = null
}
// Exact copy of View.createCompositionContext() in androidx's WindowRecomposer.android.kt
private fun createCompositionContext(): CompositionContext {
val currentThreadContext = AndroidUiDispatcher.CurrentThread
val pausableClock = currentThreadContext[MonotonicFrameClock]?.let {
PausableMonotonicFrameClock(it).apply { pause() }
}
val contextWithClock = currentThreadContext + (pausableClock ?: EmptyCoroutineContext)
val recomposer = Recomposer(contextWithClock)
val runRecomposeScope = CoroutineScope(contextWithClock)
val viewTreeLifecycleOwner = checkNotNull(ViewTreeLifecycleOwner.get(this)) {
"ViewTreeLifecycleOwner not found from $this"
}
viewTreeLifecycleOwner.lifecycle.addObserver(
LifecycleEventObserver { _, event ->
@Suppress("NON_EXHAUSTIVE_WHEN")
when (event) {
Lifecycle.Event.ON_CREATE ->
// Undispatched launch since we've configured this scope
// to be on the UI thread
runRecomposeScope.launch(start = CoroutineStart.UNDISPATCHED) {
recomposer.runRecomposeAndApplyChanges()
}
Lifecycle.Event.ON_START -> pausableClock?.resume()
Lifecycle.Event.ON_STOP -> pausableClock?.pause()
Lifecycle.Event.ON_DESTROY -> {
recomposer.cancel()
}
}
}
)
return recomposer
}
// TODO: ComposeViewWithLifecycle should handle saving state and other lifecycle things
// override fun onSaveInstanceState(): Parcelable? {
// val superState = super.onSaveInstanceState()
// val bundle = Bundle()
// savedStateRegistryController.performSave(bundle)
// }
}