andriod集成x5内核

说明

因为手机自带的webview内核不统一,而且大都版本过低。
为了更好的体验,选择了x5
虽然x5内置的chrome版本不是最新的,但是也是相当新了(截止目前为109)!

x5内核的集成方式分为两种,在线版本(也叫公网版)和离线版本!
不过后来又增加了自运营版本(方便部署到内网服务器等不方便访问外网的环境) 并把离线版改名为 自运营静态!

3者区别是
公网版:App在启动后,从腾讯服务器动态下载并共享X5内核,接入简单,APK体积小;内核自动更新,无需随App升级。
自运营静态内核版:启动快,无网络依赖;与App绑定将X5内核的.so库文件直接打包到APK中,体积大。
自运营动态内核版:将X5内核服务部署在自有或内网服务器上,完全可控,不依赖外网,而且安装包也小,启动后从私有服务器动态下载并共享X5内核。

这里我们讲的是公网版!

前置工作

注册、实名认真和创建APP,下载内核(因为是在线版,所以非常小)和 配置文件
600
600

上传apk的时候,记得使用v1签名,不支持v2和v3!

开始编码

创建项目名字随意 我这里叫 x5demo

kotlin(java)代码

java/com/example/x5demo 目录有3个文件

MainActivity.kt

package com.example.x5demo

import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.tencent.smtt.export.external.TbsCoreSettings
import com.tencent.smtt.sdk.QbSdk
import com.tencent.smtt.sdk.TbsFramework
import com.tencent.smtt.sdk.core.dynamicinstall.DynamicInstallManager
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import com.tencent.smtt.sdk.ProgressListener;
import android.widget.ProgressBar;

class MainActivity : AppCompatActivity() {
    private val TAG = "MainActivity"
    private lateinit var progressBar: ProgressBar

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        progressBar = findViewById(R.id.progress)
        findViewById<View>(R.id.public_btn).setOnClickListener {
            initPublicTBS()
        }
    }

    private fun saveInputStreamToFile(inputStream: InputStream, filePath: String): File? {
        return try {
            // 1. 创建目标文件对象 - 在应用的内部存储目录中
            val file = File(applicationContext.filesDir, filePath)
            // 最终路径类似:/data/data/你的包名/files/config/config_47405.tbs

            // 2. 创建输出流准备写入
            val out = FileOutputStream(file)

            // 3. 创建缓冲区,提高复制效率
            val buffer = ByteArray(1024)  // 1KB 缓冲区
            var bytesRead: Int

            // 4. 循环读取输入流并写入输出流
            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                out.write(buffer, 0, bytesRead)  // 将读取的数据写入文件
            }

            // 5. 关闭流,释放资源
            out.close()
            inputStream.close()

            // 6. 记录成功信息
            Log.e(TAG, "saveInputStreamToFile: ${file.path}")
            file  // 返回复制后的文件对象
        } catch (e: IOException) {
            Log.e(TAG, "出现异常: ${e.localizedMessage}")
            null  // 出错时返回 null
        }
    }

    private fun getConfigFile(): File? {
        return try {
            val inputStream = assets.open(TBSEnv.CONFIG_PATH)
            val inputFileName = TBSEnv.CONFIG_PATH.substringAfter("/")
            saveInputStreamToFile(inputStream, inputFileName)
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }

    // 预初始化回调
    private val preInitCallback = object : QbSdk.PreInitCallback {
        override fun onCoreInitFinished() {
            Log.e(TAG, "onCoreInitFinished: 初始化成功")
            val intent = Intent(this@MainActivity, WebActivity::class.java)
            startActivity(intent)
            finish()
        }

        override fun onViewInitFinished(isX5Code: Boolean) {
            Log.e(TAG, "是否使用X5内核: $isX5Code")
        }
    }

    private fun downloadConfigTBS(configFile: File) {
        // 3. 设置TBS框架
        TbsFramework.setUp(this, configFile)

        // 4. 动态安装管理
        val manager = DynamicInstallManager(applicationContext)
        manager.registerListener(object : ProgressListener {
            override fun onProgress(progress: Int) {
                Log.i(TAG, "downloadConfigTBS: $progress")
                runOnUiThread {
                    progressBar.progress = progress
                }
            }

            override fun onFinished() {
                Log.i(TAG, "下载完成,开始预初始化")
                QbSdk.preInit(this@MainActivity, preInitCallback)
            }

            override fun onFailed(code: Int, msg: String) {
                Log.i(TAG, "onError: $code; msg: $msg")
            }
        })
        manager.startInstall()
    }


    private fun initPublicTBS() {
        // 1. 初始化TBS设置
        val map = HashMap<String, Any>()
        map[TbsCoreSettings.MULTI_PROCESS_ENABLE] = 1
        QbSdk.initTbsSettings(map)

        // 2. 获取配置文件
        val configFile = getConfigFile()
        if (configFile != null && configFile.exists()) {
            Log.e(TAG, "拿到文件")
            Log.e(TAG, "文件路径: ${configFile.absolutePath}")
            downloadConfigTBS(configFile)
        } else {
            Log.e(TAG, "未拿到文件")
        }
    }
}

TBSEnv.kt

package com.example.x5demo

object TBSEnv {
    const val CONFIG_PATH = "config.tbs"
    const val LOAD_URL = "https://www.baidu.com"
}

WebActivity.kt

package com.example.x5demo

import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.tencent.smtt.export.external.interfaces.JsResult
import com.tencent.smtt.sdk.QbSdk
import com.tencent.smtt.sdk.WebChromeClient
import com.tencent.smtt.sdk.WebView
import com.tencent.smtt.sdk.WebViewClient

class WebActivity : AppCompatActivity() {
    private var webView: WebView? = null
    private val TAG = "WebActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_web)
        webView = findViewById<WebView>(R.id.webview)

        webView?.let { web ->
            val settings = web.settings
            settings.javaScriptEnabled = true
            settings.allowFileAccess = true
            settings.setSupportZoom(true)
            settings.databaseEnabled = true
            settings.allowFileAccess = true
            settings.domStorageEnabled = true
            WebView.setWebContentsDebuggingEnabled(true)

            web.loadUrl(TBSEnv.LOAD_URL)

            val tbsVersion = QbSdk.getTbsVersion(this)
            Log.e("webActivity", "QbSdk.getTbsVersion: $tbsVersion")
            Toast.makeText(
                this@WebActivity,
                "内核版本:" + tbsVersion + web.isX5Core,
                Toast.LENGTH_LONG
            ).show()

            web.webChromeClient = object : WebChromeClient() {
                override fun onJsAlert(
                    webView: WebView,
                    url: String,
                    message: String,
                    result: JsResult
                ): Boolean {
                    AlertDialog.Builder(this@WebActivity).setTitle("JS弹窗Override")
                        .setMessage(message)
                        .setPositiveButton(
                            "OK"
                        ) { _: DialogInterface?, _: Int -> result.confirm() }
                        .setCancelable(false)
                        .show()
                    return true
                }
            }

            web.webViewClient = object : WebViewClient() {
                override fun shouldOverrideUrlLoading(webView: WebView, url: String): Boolean {
                    Log.e(TAG, "overrideUrlLoading: $url")
                    return !url.startsWith("http")
                }
            }
        }
    }

    override fun onDestroy() {
        webView?.destroy()
        super.onDestroy()
    }
}

配置文件

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />


    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.X5demo"
        tools:targetApi="31">
        <activity
            android:name=".WebActivity"
            android:exported="false" />
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <meta-data android:name="com.tencent.smtt.multiprocess.NUM_PRIVILEGED_SERVICES" android:value="1" />
        <service
            android:name="com.tencent.smtt.services.ChildProcessService$Privileged0"
            android:exported="false"
            android:isolatedProcess="false"
            android:process=":privileged_process0" />

    </application>

</manifest>

/x5demo/app/build.gradle.kts

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
}

android {
    namespace = "com.example.x5demo"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.example.x5demo"
        minSdk = 24
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    // 增加这一段(填写自己的证书信息)
    signingConfigs {
        create("release") {
            storeFile = file("wutong.jks")
            storePassword = "123456789"
            keyAlias = "cert"
            keyPassword = "123456789"
            enableV1Signing = true
        }
        getByName("debug") {
            storeFile = file("wutong.jks")
            storePassword = "123456789"
            keyAlias = "cert"
            keyPassword = "123456789"
            enableV1Signing = true
        }
    }

    // 增加这一段
    buildTypes {
        getByName("release") {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
            signingConfig = signingConfigs.getByName("release")
        }
        getByName("debug") {
            signingConfig = signingConfigs.getByName("debug")
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }


}

dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar")))) // 增加这一行
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.appcompat)
    implementation(libs.material)
    implementation(libs.androidx.activity)
    implementation(libs.androidx.constraintlayout)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
}

x5demo/app/src/main/assets/config.tbs
将前置工作中下载的配置文件复制到此处!

xxx.jks证书文件
比如我这里是wutong.jsk,复制到 app 目录下!

布局文件

x5demo/app/src/main/res/layout新增两个文件
activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity"
    android:orientation="vertical"
    android:gravity="center">

    <Button
        android:id="@+id/public_btn"
        android:text="初始化X5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <ProgressBar
        android:id="@+id/progress"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="200dp"
        android:layout_height="30dp"
        android:layout_gravity="center"
        android:max="100"
        android:layout_marginTop="40dp"/>

</LinearLayout>

activity_web.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".WebActivity">

    <com.tencent.smtt.sdk.WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

预览

400

其它

以上我的代码都是kotlin,如果你是java,那正好官网提供了java版本的demo
这里是公网版本的官方文档

Android Kotlin 中页面跳转的标准方式

Android 页面跳转的基本方式

// 1. 创建 Intent(意图)对象
val intent = Intent(mainActivity, WebActivity::class.java)

// 2. 启动目标 Activity
mainActivity.startActivity(intent)

// 3. 关闭当前 Activity(可选)
mainActivity.finish()

Intent(意图)

val intent = Intent(mainActivity, WebActivity::class.java)
  • Intent 是 Android 中用于组件间通信的对象
  • 第一个参数:mainActivity - 当前的上下文(Context)
  • 第二个参数:WebActivity::class.java - 目标 Activity 的 Class 对象
  • ::class.java 是 Kotlin 获取 Java Class 对象的语法

startActivity()

mainActivity.startActivity(intent)
  • 启动目标 Activity
  • 会打开 WebActivity 页面

finish()

mainActivity.finish()
  • 关闭当前 Activity(MainActivity)
  • 调用后,用户按返回键不会回到 MainActivity
  • 如果不调用 finish(),返回键会回到 MainActivity

其他常见的跳转方式

带参数跳转

val intent = Intent(this, WebActivity::class.java)
intent.putExtra("url", "https://www.baidu.com")
intent.putExtra("title", "百度")
startActivity(intent)

简化写法

startActivity(Intent(this, WebActivity::class.java))

带动画跳转

val intent = Intent(this, WebActivity::class.java)
startActivity(intent)
overridePendingTransition(R.anim.slide_in, R.anim.slide_out)

获取返回结果(新方式)

val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    if (result.resultCode == RESULT_OK) {
        // 处理返回结果
    }
}

优化版本

弹窗展示加载内核,加载完成自动打开页面。

MainActivity.kt

package com.example.x5demo

import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.tencent.smtt.export.external.interfaces.JsResult
import com.tencent.smtt.sdk.QbSdk
import com.tencent.smtt.sdk.WebChromeClient
import com.tencent.smtt.sdk.WebView
import com.tencent.smtt.sdk.WebViewClient

class MainActivity : AppCompatActivity() {
    private var webView: WebView? = null
    private val TAG = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 初始化 X5 内核
        TbsHelper(this, this@MainActivity) {
            // 初始化成功后的回调,创建并加载 WebView
            Log.e(TAG, "初始化成功后的回调,创建并加载 WebView")
            initWebView()
        }.initPublicTBS()
    }

    private fun initWebView() {
        // X5 已经初始化完成后,动态创建 WebView
        // (不能直接写到activity_main里,然后 webView = findViewById<WebView>(R.id.webview)
        // 因为这样会被提前自动提前加载(则会启用系统自带的),必须在这里显式手动加载)
        webView = WebView(this)
        val container = findViewById<FrameLayout>(R.id.webview_container)
        container.addView(webView, FrameLayout.LayoutParams(
            FrameLayout.LayoutParams.MATCH_PARENT,
            FrameLayout.LayoutParams.MATCH_PARENT
        ))


        // 设置webView
        webView?.let { web ->
            val settings = web.settings
            settings.javaScriptEnabled = true
            settings.allowFileAccess = true
            settings.setSupportZoom(true)
            settings.databaseEnabled = true
            settings.domStorageEnabled = true
            WebView.setWebContentsDebuggingEnabled(true)

            val tbsVersion = QbSdk.getTbsVersion(this)
            val isX5Core = web.isX5Core

            Log.e(TAG, "=== X5内核状态 ===")
            Log.e(TAG, "TBS版本: $tbsVersion")
            Log.e(TAG, "是否X5内核: $isX5Core")

            web.loadUrl("file:///android_asset/index.html")

            web.webChromeClient = object : WebChromeClient() {
                override fun onJsAlert(
                    webView: WebView,
                    url: String,
                    message: String,
                    result: JsResult
                ): Boolean {
                    AlertDialog.Builder(this@MainActivity)
                        .setTitle("JS弹窗")
                        .setMessage(message)
                        .setPositiveButton("OK") { _: DialogInterface?, _: Int ->
                            result.confirm()
                        }
                        .setCancelable(false)
                        .show()
                    return true
                }
            }

            web.webViewClient = object : WebViewClient() {
                override fun shouldOverrideUrlLoading(webView: WebView, url: String): Boolean {
                    Log.e(TAG, "overrideUrlLoading: $url")
                    return !url.startsWith("http")
                }
            }
        }
    }

    override fun onDestroy() {
        webView?.destroy()
        super.onDestroy()
    }
}

TbsHelper.kt

package com.example.x5demo

import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.example.x5demo.TBSEnv.CONFIG_PATH
import com.tencent.smtt.export.external.TbsCoreSettings
import com.tencent.smtt.sdk.QbSdk
import com.tencent.smtt.sdk.TbsFramework
import com.tencent.smtt.sdk.core.dynamicinstall.DynamicInstallManager
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import com.tencent.smtt.sdk.ProgressListener

class TbsHelper (
    private val applicationContext: Context,
    private val mainActivity: MainActivity,
    private val onInitSuccess: () -> Unit  // 初始化成功的回调
) {
    private val TAG = "MainActivity"
    private var progressDialog: AlertDialog? = null
    private var progressBar: ProgressBar? = null
    private var progressText: TextView? = null

    companion object {
        private const val PREF_NAME = "tbs_config"
        private const val KEY_FIRST_INSTALL = "first_install_done"
    }


    private fun saveInputStreamToFile(inputStream: InputStream, filePath: String): File? {
        return try {
            // 1. 创建目标文件对象 - 在应用的内部存储目录中
            val file = File(applicationContext.filesDir, filePath)
            // 最终路径类似:/data/data/你的包名/files/config/config_47405.tbs

            // 2. 创建输出流准备写入
            val out = FileOutputStream(file)

            // 3. 创建缓冲区,提高复制效率
            val buffer = ByteArray(1024)  // 1KB 缓冲区
            var bytesRead: Int

            // 4. 循环读取输入流并写入输出流
            while (inputStream.read(buffer).also { bytesRead = it } != -1) {
                out.write(buffer, 0, bytesRead)  // 将读取的数据写入文件
            }

            // 5. 关闭流,释放资源
            out.close()
            inputStream.close()

            // 6. 记录成功信息
            Log.e(TAG, "saveInputStreamToFile: ${file.path}")
            file  // 返回复制后的文件对象
        } catch (e: IOException) {
            Log.e(TAG, "出现异常: ${e.localizedMessage}")
            null  // 出错时返回 null
        }
    }

    private fun getConfigFile(): File? {
        return try {
            val inputStream = applicationContext.assets.open(CONFIG_PATH)
            val inputFileName = CONFIG_PATH.substringAfter("/")
            saveInputStreamToFile(inputStream, inputFileName)
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }

    // 预初始化回调
    private val preInitCallback = object : QbSdk.PreInitCallback {
        override fun onCoreInitFinished() {
            Log.e(TAG, "onCoreInitFinished: 初始化成功")
            mainActivity.runOnUiThread {
                progressDialog?.dismiss()
            }
        }

        override fun onViewInitFinished(isX5Code: Boolean) {
            Log.e(TAG, "是否使用X5内核: $isX5Code")
            mainActivity.runOnUiThread {
                val kernelType = if (isX5Code) "X5内核" else "系统WebView内核"
                val message = "初始化完成\n使用: $kernelType"
                Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()

                // 调用初始化成功的回调
                onInitSuccess()
            }
        }
    }

    fun initPublicTBS() {
        // 1. 初始化TBS设置
        val map = HashMap<String, Any>()
        map[TbsCoreSettings.MULTI_PROCESS_ENABLE] = 1
        QbSdk.initTbsSettings(map)

        // 2. 获取配置文件
        val configFile = getConfigFile()

        // 3. 设置TBS框架
        TbsFramework.setUp(applicationContext, configFile)

        // 显示进度对话框
        mainActivity.runOnUiThread {
            val dialogView = LayoutInflater.from(mainActivity).inflate(R.layout.dialog_progress, null)
            progressBar = dialogView.findViewById(R.id.progress_bar)
            progressText = dialogView.findViewById(R.id.progress_percent)

            progressDialog = AlertDialog.Builder(mainActivity)
                .setView(dialogView)
                .setCancelable(false)
                .create()

            progressDialog?.show()
        }

        // 4. 动态安装管理
        val manager = DynamicInstallManager(applicationContext)
        manager.registerListener(object : ProgressListener {
            override fun onProgress(progress: Int) {
                Log.i(TAG, "downloadConfigTBS: $progress")
                mainActivity.runOnUiThread {
                    progressBar?.progress = progress
                    progressText?.text = "$progress%"
                }
            }

            override fun onFinished() {
                Log.i(TAG, "下载完成,开始预初始化")
                mainActivity.runOnUiThread {
                    val messageText = progressDialog?.findViewById<TextView>(R.id.progress_message)
                    messageText?.text = "初始化中..."
                    progressBar?.isIndeterminate = true
                }
                QbSdk.preInit(mainActivity, preInitCallback)
            }

            override fun onFailed(code: Int, msg: String) {
                Log.e(TAG, "onError: $code; msg: $msg")
            }
        })
        manager.startInstall()
    }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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/webview_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- WebView 将在 X5 初始化完成后动态添加 -->

</FrameLayout>

dialog_progress.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="wrap_content"
    android:orientation="vertical"
    android:padding="24dp">

    <TextView
        android:id="@+id/progress_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="正在加载X5内核"
        android:textSize="18sp"
        android:textStyle="bold"
        android:textColor="@android:color/black"
        android:layout_marginBottom="16dp"/>

    <TextView
        android:id="@+id/progress_message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="请稍候..."
        android:textSize="14sp"
        android:textColor="@android:color/darker_gray"
        android:layout_marginBottom="16dp"/>

    <ProgressBar
        android:id="@+id/progress_bar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100"
        android:progress="0"/>

    <TextView
        android:id="@+id/progress_percent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="0%"
        android:textSize="12sp"
        android:textColor="@android:color/darker_gray"
        android:layout_gravity="end"
        android:layout_marginTop="8dp"/>

</LinearLayout>
posted @ 2025-11-03 19:47  丁少华  阅读(31)  评论(0)    收藏  举报