[Android]引导页

news2024/11/20 20:28:13

使用Kotlin + Jetpack Compose创建一个左右滑动的引导页, 效果如图.

1.添加依赖项

androidx.compose.ui最新版本查询:https://maven.google.com/web/index.html

com.google.accompanist:accompanist-pager最新版本查询:https://central.sonatype.com/

确保在 build.gradle (Module: app) 文件中添加:

dependencies {
    implementation("androidx.compose.ui:ui:1.7.0-alpha06")
    implementation("com.google.accompanist:accompanist-pager:0.35.0-alpha")
}

2.定义引导页

  • HorizontalPager 是一个实现水平滑动页面的组件,常用于实现引导页。它是通过Pager库提供的,支持滑动动画和状态保持。
  • rememberPagerState 是用于记忆并管理HorizontalPager的状态,例如当前页面和总页面数。
  • rememberCoroutineScope 用于创建一个协程作用域,允许在Compose函数外异步执行任务(例如页面滚动)。
package com.randomdt.www.main.guide

import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.randomdt.www.R
import com.randomdt.www.support.data.PrefKey
import com.randomdt.www.support.data.PrefsManager
import com.randomdt.www.ui.theme.customScheme
import kotlinx.coroutines.launch

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun GuideScreen(onGuideComplete: (Boolean) -> Unit) {
    val pages = listOf(
        GuidePage("Enhance your video recording with smooth script scrolling.", R.drawable.icon_guide1),
        GuidePage("Personalize settings to meet your recording needs.", R.drawable.icon_guide2),
        GuidePage("Intelligent scrolling for effortless recording control.", R.drawable.icon_guide3),
        GuidePage("Subscribe to the premium version and unlock additional features.", R.drawable.icon_guide4)
    )

    val pagerState = rememberPagerState(pageCount = { pages.count() })
    val scope = rememberCoroutineScope()

    Box(modifier = Modifier
        .fillMaxSize()
        .background(color = MaterialTheme.colorScheme.background)){
        HorizontalPager(
            state = pagerState,
            modifier = Modifier.matchParentSize()  // Use matchParentSize instead
        ) { page ->
            GuidePageContent(page = pages[page], modifier = Modifier.fillMaxSize())
        }

        val isLast = pagerState.currentPage == pages.size - 1
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(horizontal = 16.dp),
            verticalArrangement = Arrangement.Bottom
        ) {
            if (isLast) {
                Text(
                    "3 Days Trial, \$4.99/week, cancel anytime",
                    fontSize = 14.sp,
                    fontWeight = FontWeight.Normal,
                    color = MaterialTheme.customScheme.text_aux99,
                    textAlign = TextAlign.Center,
                    modifier = Modifier
                        .fillMaxWidth()  // 使宽度充满屏幕
                        .padding(horizontal = 16.dp)  // 水平填充
                        .padding(bottom = 16.dp)  // 与按钮之间的空隙
                )
            }

            // 渐变色定义
            val gradient = Brush.horizontalGradient(
                colors = listOf(
                    MaterialTheme.customScheme.gradient_start_color,  // 渐变起始颜色
                    MaterialTheme.customScheme.gradient_end_color  // 渐变结束颜色
                )
            )
            // Next/Subscribe按钮
            Button(
                onClick = {
                    if (pagerState.currentPage < pages.size - 1) {
                        scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
                    } else {
                        // Navigate to Home Screen
                        goHome(onGuideComplete)
                    }
                },
                colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), // 设置背景透明
                contentPadding = PaddingValues(0.dp),  // 移除内部填充
                border = BorderStroke(1.dp, Color.White), // 设置按钮的边框和背景
                shape = RoundedCornerShape(25.dp),  // 按钮圆角设置. Button 的 shape 只影响按钮本身的边界形状,而不会应用到渐变色背景上。
                modifier = Modifier
                    .fillMaxWidth() // 使宽度充满屏幕
                    .height(50.dp)
                    .background(
                        gradient,
                        shape = RoundedCornerShape(25.dp)
                    ), // 方式一: 添加渐变色背景, 已经为渐变背景导角
            ) {
                Text(
                    if (pagerState.currentPage == pages.size - 1) "Subscribe" else "Next",
                    fontSize = 17.sp,
                    fontWeight = FontWeight.Bold
                )
                /*
                // 方式二: 设置Button渐变色
                Box(
                    modifier = Modifier
                        .fillMaxSize()
                        .background(gradient, shape = RoundedCornerShape(25.dp))
                ) {
                    Text(
                        if (pagerState.currentPage == pages.size - 1) "Subscribe" else "Next",
                        modifier = Modifier.align(Alignment.Center)
                    )
                }*/
            }

            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .alpha(if (isLast) 1f else 0f)
            ) {
                // Restore Purchases
                Button(
                    onClick = {

                    },
                    colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), // 设置背景透明
                    contentPadding = PaddingValues(0.dp),  // 移除内部填充
                    modifier = Modifier.height(40.dp)
                ) {
                    Text(
                        "Restore Purchases",
                        fontSize = 13.sp,
                        fontWeight = FontWeight.Normal,
                        style = TextStyle(textDecoration = TextDecoration.Underline) // 下划线
                    )
                }

                // Privacy Policy
                Button(
                    onClick = {

                    },
                    colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), // 设置背景透明
                    contentPadding = PaddingValues(0.dp),  // 移除内部填充
                    modifier = Modifier
                        .align(Alignment.TopEnd)
                        .padding(end = 95.dp)
                        .height(40.dp)
                ) {
                    Text(
                        "Privacy Policy",
                        fontSize = 13.sp,
                        fontWeight = FontWeight.Normal,
                        style = TextStyle(textDecoration = TextDecoration.Underline) // 下划线
                    )
                }

                // Terms of Use
                Button(
                    onClick = {

                    },
                    colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), // 设置背景透明
                    contentPadding = PaddingValues(0.dp),  // 移除内部填充
                    modifier = Modifier
                        .align(Alignment.TopEnd)
                        .height(40.dp)
                ) {
                    Text(
                        "Terms of Use",
                        fontSize = 13.sp,
                        fontWeight = FontWeight.Normal,
                        style = TextStyle(textDecoration = TextDecoration.Underline) // 下划线
                    )
                }

                //
                val scrollState = rememberScrollState()
                Box(modifier = Modifier.fillMaxWidth().padding(top = 40.dp)) {
                    // 可滚动的详细文本视图
                    Text(
                        text = "This subscription automatically renews unless you cancel at least 24 hours before the end of the current subscription period. Your account will be charged for renewal within 24-hours prior to the end of the current subscription period. You can manage your subscription and auto-renewal in your Google Play account settings.",
                        fontSize = 13.sp,
                        color = MaterialTheme.customScheme.text_aux99,
                        fontWeight = FontWeight.Normal,
                        lineHeight = 20.sp,  // 设置行间距为20sp
                        modifier = Modifier
                            .fillMaxWidth()
                            .verticalScroll(scrollState)
                            //.heightIn(max = 100.dp)  // 设置最大高度以限制视图高度
                            .padding(bottom = 10.dp)
                    )
                }
            }
        }

        if (isLast) {
            // 跳过按钮
            Button(
                onClick = {
                    // Navigate to Home Screen
                    goHome(onGuideComplete)
                },
                colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), // 设置背景透明
                contentPadding = PaddingValues(0.dp),  // 移除内部填充
                modifier = Modifier
                    .align(Alignment.TopStart)
                    .size(60.dp)
                    .padding(start = 8.dp, top = 8.dp)
            ) {
                Image(
                    painter = painterResource(R.drawable.icon_alert_close),
                    contentDescription = "",
                )
            }
        }
    }

}

private fun goHome(onGuideComplete: (Boolean) -> Unit) {
    PrefsManager.set(PrefKey.IS_DID_GUIDE, true)
    onGuideComplete(true)
}

@Composable
fun GuidePageContent(page: GuidePage, modifier: Modifier = Modifier) {
    Column(modifier = modifier) {
        Image(
            painter = painterResource(id = page.imageRes),
            contentDescription = null,
            modifier = Modifier
                .fillMaxWidth() // 填充最大宽度
                .aspectRatio(1167 / 1320f) // 设置宽高比例,例如 16:9 的比例为 1.77
        )
        Text(
            text = page.description,
            modifier = Modifier
                .padding(horizontal = 16.dp) // 设置水平间距
                .align(Alignment.CenterHorizontally), // 居中
            style = TextStyle(
                fontSize = 20.sp,
                textAlign = TextAlign.Center, // 让换行的文案也居中对齐
            )
        ) // 高度根据内容自适应
    }
}

// imageRes 是一个整数 (int),通常在 Android 开发中,这种整数类型用来代表资源文件(如图片)的 ID。
data class GuidePage(val description: String, val imageRes: Int)

3.定义PrefsManager

package com.randomdt.www.support.data

import android.content.Context
import android.content.SharedPreferences
import android.util.Log

object PrefsManager {
    private lateinit var sharedPreferences: SharedPreferences

    fun init(context: Context) {
        sharedPreferences = context.getSharedPreferences("AppPreferences", Context.MODE_PRIVATE)
    }

    fun <T> get(prefKey: PrefKey): T {
        val defaultValue: T = PrefDefaults.getDefaultValue(prefKey)
        return when (defaultValue) {
            is Boolean -> sharedPreferences.getBoolean(prefKey.key, defaultValue) as? T ?: defaultValue
            is Int -> sharedPreferences.getInt(prefKey.key, defaultValue) as? T ?: defaultValue
            is String -> sharedPreferences.getString(prefKey.key, defaultValue)  as? T ?: defaultValue
            else -> {
                Log.w("SharedPreferences", "Unsupported type for SharedPreferences.get")
                defaultValue
            }
        }
    }

    fun <T> set(prefKey: PrefKey, value: T) {
        with(sharedPreferences.edit()) {
            when (value) {
                is Boolean -> putBoolean(prefKey.key, value)
                is Int -> putInt(prefKey.key, value)
                is String -> putString(prefKey.key, value)
                else -> Log.w("SharedPreferences", "Unsupported type for SharedPreferences.set")
            }
            apply()
        }
    }
}

/// 让 PrefKey 枚举仅包含用户定义的键(key)
enum class PrefKey(val key: String) {
    IS_DID_GUIDE("isDidGuide"),
    USER_AGE("userAge"),
    USER_NAME("userName");
}

/// 管理默认值和类型
object PrefDefaults {
    private val defaultValues = mapOf<PrefKey, Any>(
        PrefKey.IS_DID_GUIDE to false,
        PrefKey.USER_AGE to 18,
        PrefKey.USER_NAME to "John Doe"
    )

    @Suppress("UNCHECKED_CAST")
    fun <T> getDefaultValue(prefKey: PrefKey): T = defaultValues[prefKey] as T
}

/*
// 初始化(通常在应用启动时进行)
PrefsManager.init(context)

// 存储数据
PrefsManager.set(PrefKey.IS_LOGGED_IN, true)
PrefsManager.set(PrefKey.USER_AGE, 30)
PrefsManager.set(PrefKey.USER_NAME, "Alice")

// 读取数据
val isLoggedIn: Boolean = PrefsManager.get(PrefKey.IS_LOGGED_IN)
val userAge: Int = PrefsManager.get(PrefKey.USER_AGE)
val userName: String = PrefsManager.get(PrefKey.USER_NAME)
*/

4.引导页进入/离开

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        PrefsManager.init(this)

        setContent {
            RandomdtTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MainContent()
                }
            }
        }
    }
}

@Composable
fun MainContent() {
    val isDidGuideState = remember { mutableStateOf(PrefsManager.get<Boolean>(PrefKey.IS_DID_GUIDE)) }
    if (isDidGuideState.value) {
        Greeting("Android")
    } else {
        GuideScreen { isDidGuideCompleted ->
            isDidGuideState.value = isDidGuideCompleted
        }
    }
}

TO

HorizontalPager用法:https://juejin.cn/post/6978831090693701639

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1628935.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++|继承(菱形+虚拟)

目录 一、继承的概念及定义 1.1概念 1.2定义 1.2.1定义格式 1.2.2继承关系和访问限定符 1.2.3继承基类成员访问方式的变化 二、基类和派生类对象赋值转换 三、继承中的作用域 四、派生类的默认成员函数 五、继承与友元、静态成员 六、菱形继承与虚拟继承 6.1单继承与…

Redis - Set 集合

目录 前言 命令 SADD 将一个或者多个元素添加到 set 中 语法 SMEMBERS 获取一个 set 中的所有元素 语法 SISMEMBER 判断⼀个元素在不在 set 中 语法 SCARD 获取 set 中的元素个数 语法 SPOP 从 set 中随机删除并返回⼀个或者多个元素 语法 SMOME 将⼀个元素从源 se…

电脑教程1

一、介绍几个桌面上面的软件 1、火绒&#xff1a;主要用于电脑的安全防护和广告拦截 1.1 广告拦截 1.打开火绒软件点击安全工具 点击弹窗拦截 点击截图拦截 拦截具体的小广告 2、向日葵远程控制&#xff1a;可以通过这个软件进行远程协助 可以自己去了解下 这个软件不要…

每日算法之两两交换链表中的节点

题目描述 给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4] 输出&…

Apollo 7周年大会自动驾驶生态利剑出鞘

前言 4月22日&#xff0c;百度Apollo在北京车展前夕举办了以“破晓•拥抱智变时刻”为主题的智能汽车产品发布会&#xff0c;围绕汽车智能化&#xff0c;发布了智驾、智舱、智图等全新升级的“驾舱图”系列产品。 1、7周年大会 自2013年百度开始布局自动驾驶&#xff0c;201…

Kotlin基础​​

数据类型 定义变量 var表示定义变量&#xff0c;可以自动推导变量类型&#xff0c;所以Int可以不用写。 定义常量 条件语句 if表达式可以返回值&#xff0c;该值一般写在if里的最后一行 类似switch的用法 区间 循环 a是标签&#xff0c;可以直接break到标签的位置&#xf…

Docker 的数据管理 端口映射 容器互联 镜像创建

一 Docker 的数据管理 1 管理 Docker 容器中数据主要有两种方式&#xff1a; 数据卷&#xff08;Data Volumes&#xff09; 数据卷容器&#xff08;DataVolumes Containers&#xff09;。 1.1 数据卷 数据卷是一个供容器使用的特殊目录&#xff0c;位于容器中。可将宿主机…

【PLC学习十四】TIA.V18无法启动仿真的问题

【PLC学习十四】TIA.V18无法启动仿真的问题 文章目录 【PLC学习十四】TIA.V18无法启动仿真的问题前言一、程序仿真出现的问题二、解决方法1.无法仿真的问题2.因安全问题&#xff0c;无法编译的问题3、在TIA V18内部设置完成PG/PC接口后&#xff0c;下一次打开仍然不能仿真&…

【Vue3+Tres 三维开发】01-HelloWord

预览 什么是TRESJS 简单的说,就是基于THREEJS封装的能在vue3中使用的一个组件,可以像使用组件的方式去创建场景和模型。优势就是可以快速创建场景和要素的添加,并且能很明确知道创景中的要素构成和结构。 项目创建 npx create-vite@latest # 选择 vue typescript安装依赖…

【Linux 进程间通信】管道

文章目录 1.为什么操作系统需要向用户提供进程间通信方式&#xff1f;2.进程间通信的种类3.管道3.1管道的作用3.2管道的本质3.3管道的通信原理3.4管道的分类 1.为什么操作系统需要向用户提供进程间通信方式&#xff1f; ①&#x1f34e;资源共享&#xff1a;有的时候两个进程需…

QT——简易计算器(从0开始)

目录 一、题目描述&#xff1a; 二、创建工程&#xff1a; 1. ​编辑 2. 3. 4. 默认 5. 6. 7. 8. 默认 9. 创建完成 三、UI界面设计&#xff1a; 1. 添加按钮 1. 2. 按钮界面 3. 按钮绑定快捷键 2. 文本框添加 1. 文本框字体 2. 默认文本 3. 文本对齐方式…

英智数字孪生机器人解决方案,赋能仓库物流模式全面升级

工业机械臂、仓储机器人、物流机器人等模式的机器人系统在现代产业中扮演着愈发重要的角色&#xff0c;他们的发展推动了自动化和智能化水平的提高&#xff0c;有助于为制造业、物流业、医疗保健业和服务业等行业创造新效率并提升人们的生活质量。 行业面临的挑战 机器人开发、…

为何要与云产商进行云端防护合作,上云企业如何保障云端安全

随着大数据、云计算等信息技术的迅猛发展&#xff0c;企业为了降低成本、提高效率&#xff0c;纷纷将业务迁移至云端。 随着大数据、云计算等信息技术的迅猛发展&#xff0c;企业为了降低成本、提高效率&#xff0c;纷纷将业务迁移至云端。这一全面的上云浪潮对传统的安全企业格…

YOLOv8+PyQt5野外火焰检测系统(可以从图像、视频和摄像头三种路径检测)

1.效果视频&#xff1a;https://www.bilibili.com/video/BV1Tm421s7te/?spm_id_from333.999.0.0 2.资源包含可视化的野外火焰检测系统&#xff0c;可用于火灾预警或火灾救援&#xff0c;该系统可自动检测和识别图片或视频当中出现的火焰&#xff0c;以及自动开启摄像头&#…

使用Windows GDI进行绘图

使用Windows GDI绘图&#xff0c;可以使用MFC&#xff0c;也可以直接使用Windows API绘图&#xff0c;两者其实都一样。MFC也是封装了Windows API。 下面以MFC为例&#xff0c;进行说明。因为MFC帮我们做好了一些底层&#xff0c;可以直接使用Windows GDI的函数。 在MFC中使用…

如此建立网络根文件系统 Mount NFS RootFS

安静NFS系统服务 sudo apt-get install nfs-kernel-server 创建目录 sudo mkdir /rootfsLee 将buildroot编译的根文件系统解压缩到 sudo tar xvf rootfs.tar -C /rootfsLee/ 添加文件NFS访问路径 sudo vi /etc/exports sudo /etc/exports文件&#xff0c;添加如下一行 …

SecureCRT中添加命令显示为空如何处理?(原因添加了空行)

相关背景信息 配置相关路径:~/Library/Application\ Support/VanDyke/SecureCRT/Config包括的配置信息 按钮、命令、全局配置、色彩、以及license都在$ ls ButtonBarV4.ini Commands Global.ini SSH2.ini Button…

STM32单片机通过ST-Link 烧录和调试

系列文章目录 STM32单片机系列专栏 C语言术语和结构总结专栏 文章目录 1. ST-LINK V2 2. 操作步骤 2.1 连接方式 2.2 驱动安装常规步骤 2.3 Keil中的设置 3. 调式仿真 4. 常见问题排查 1. ST-LINK V2 ST LINK v2下载器用于STM32单片机&#xff0c;可以下载程序、调试…

代码随想录第49天|121. 买卖股票的最佳时机 122.买卖股票的最佳时机II

121. 买卖股票的最佳时机 121. 买卖股票的最佳时机 - 力扣&#xff08;LeetCode&#xff09; 代码随想录 (programmercarl.com) 动态规划之 LeetCode&#xff1a;121.买卖股票的最佳时机1_哔哩哔哩_bilibili 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一…

为什么常用氢化物

知识星球&#xff08;星球名&#xff1a;芯片制造与封测社区&#xff09;里的学员问&#xff1a;diffusion工序&#xff0c;所需要的气体种类有哪些&#xff1f; Diffusion是什么工序&#xff1f; "Diffusion"工序是通过热能将掺杂剂原子扩散到硅片中&#xff0c;以形…