[Android]引导页

使用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.mfbz.cn/a/576207.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

无人机+集群组网:机载自组网电台技术详解

无人机与集群组网的结合为现代通信带来了独特的优势。在集群组网中&#xff0c;每个节点&#xff08;例如无人机&#xff09;都兼具路由器和主机的功能&#xff0c;它们不仅可以运行各种面向用户的应用程序&#xff0c;还可以执行路由协议&#xff0c;根据路由策略和路由表完成…

基于SpringBoot + Vue实现的校园(通知、投票)管理系统设计与实现+毕业论文(12000字)+答辩PPT+指导搭建视频

目录 项目介绍 运行环境 技术栈 效果展示 论文展示 总结 项目介绍 本系统包含管理员、用户、院校管理员三个角色。 管理员角色&#xff1a;用户管理、院校管理、单位类别管理、院校管理员管理、单位管理、通知推送管理、投票信息管理、通知回复管理等。 用户角色&#…

2款README.md生成器

&#x1f3f7;️ readme-md-generator 确保你已经安装了npx (npx从npm 5.2.0开始默认安装) 执行命令 只需在项目的根目录下运行以下命令并回答问题: npx readme-md-generator 在项目根目录执行上面bash命令&#xff0c;结果&#xff1a; # npx readme-md-generator D:\vinc…

上新啦!讯飞首个支持长文本、长图文、长语音的大模型发布

IDC预测&#xff0c;全球数据信息产生和复制量将在2025年达到175ZB&#xff08;1ZB相当于1万亿GB&#xff09;。假设每个人的大脑功能记忆容量约为1.25TB&#xff0c;那么需要超过280亿个人脑来处理这些信息&#xff0c;相当于全球人口的4倍。 4月26日&#xff0c;讯飞星火V3.…

安卓NetworkStatsManager使用及demo

目录 一、TrafficStats类简介二、demo示例 一、TrafficStats类简介 TrafficStats Android API 8提供了android.net.TrafficStats类。 通过此类能获取设备重启以来网络信息&#xff0c;部分函数如下所示&#xff1a; static long getMobileRxBytes() //获取通过移动数据网络…

[C++]STL---unordered_set与unordered_map的模拟实现

目录 前言 哈希桶的改造 哈希桶的初步改造 迭代器的模拟实现 operator() 类互相typedef时的前置声明 友元声明 迭代器的出口 插入Insert() 查找Find(&#xff09; 哈希表的最终改造 unordered_set的模拟实现 unordered_map的模拟实现 前言 unordered_set与set的区…

嵌入式系统中的实时操作系统(RTOS)深入应用与优化

引言 实时操作系统&#xff08;RTOS&#xff09;在嵌入式系统中扮演着至关重要的角色&#xff0c;特别是在需要快速响应和高度可靠性的应用中。 我将探讨如何在STM32单片机上实现RTOS&#xff0c;包括任务管理、内存管理以及中断处理&#xff0c;以提高系统的效率和响应速度。…

C++:const成员和取地址操作符

目录 一、const成员 二、取地址及const取地址操作符重载 一、const成员 将const修饰的“成员函数”称之为const成员函数&#xff0c;const修饰类成员函数&#xff0c;实际修饰该成员函数 隐含的this指针&#xff0c;表明在该成员函数中不能对类的任何成员进行修改。 注&…

JavaScript+B/S架构云LIS系统源码C# 6.0+MVC+SQLSugar医院版检验科云LIS系统源码 可提供演示

JavaScriptB/S架构云LIS系统源码MVCSQLSugar医院版检验科云LIS系统源码 可提供演示随着医疗技术的不断发展&#xff0c;医疗机构对于信息化、智能化的需求也越来越高。特别是对于检验科这样的核心科室&#xff0c;如何提高工作效率、降低误差率、提高数据安全性成为了亟待解决的…

2024年【安全生产监管人员】考试技巧及安全生产监管人员模拟考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【安全生产监管人员】考试技巧及安全生产监管人员模拟考试&#xff0c;包含安全生产监管人员考试技巧答案和解析及安全生产监管人员模拟考试练习。安全生产模拟考试一点通结合国家安全生产监管人员考试最新大纲…

Type-C接口取电IC6500:优势与应用场景的深度解析

Type-C接口PD芯片取电IC的优势 随着科技的不断进步和移动设备在日常生活中的广泛应用&#xff0c;充电技术的革新变得愈发重要。Type-C接口PD芯片取电IC作为现代充电技术的关键组件&#xff0c;其优势日益凸显&#xff0c;为移动设备充电带来了革命性的改变。本文将深入探讨Ty…

(三)登录和注册(handle_auto.go)

登录和注册(handle_auto.go) 文章目录 登录和注册(handle_auto.go)一、所需要的结构体信息二、注册三、登录四、退出 一、所需要的结构体信息 type UserAuth struct{}type LoginReq struct {Username string json:"username" binding:"required"Password …

【汇编语言】直接定址表

【汇编语言】直接定址表 文章目录 【汇编语言】直接定址表前言一、移位指令移位指令过程逻辑移位指令shl 和 shr 二、操作显存数据显示的原理显示缓冲区的结构显示信息的一种“直接”方式 三、描述内存单元的标号关于标号去了冒号的数据标号数据标号同时描述内存地址和单元长度…

前端JS必用工具【js-tool-big-box】,防抖和节流的方法调用学习

这一小节&#xff0c;我们针对前端工具包&#xff08;npm&#xff09;js-tool-big-box的使用做一些讲解&#xff0c;主要是防抖和节流方面的。 目录 前言 1 安装和引入 2 防抖的调用学习 3 节流的调用学习 4 使用方法总结 前言 在前端项目中&#xff0c;经常涉及到防抖…

CNAS软件测评报告收费标准

随着信息技术的快速发展&#xff0c;软件测评在保障软件质量、提升用户体验等方面扮演着越来越重要的角色。CNAS&#xff08;中国合格评定国家认可委员会&#xff09;作为国内权威的认可机构&#xff0c;其软件测评报告收费标准受到了广泛关注。本文旨在解析CNAS软件测评报告的…

[华为OD]幼儿园两个班的小朋友 100

题目&#xff1a; 幼儿园两个班的小朋友在排队时混在了一起&#xff0c;每位小朋友都知道自己是否与前面一位小朋友 是否同班&#xff0c;请你帮忙把同班的小朋友找出来。 小朋友的编号为整数&#xff0c;与前一位小朋友同班用 Y 表示&#xff0c;不同班用 N 表示。 输入描…

索引【MySQL】

文章目录 什么是索引测试表 磁盘和 MySQL 的交互了解磁盘MySQL 的工作原理Buffer Pool 理解索引引入Page 的结构页内目录&#xff08;Page Directory&#xff09;多页情况B 树和 B树聚簇索引和非聚簇索引 主键索引创建 唯一索引主要特点与主键索引的区别使用场景创建 联合索引工…

思维+数学期望,CF 1525E Assimilation IV

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 Problem - 1525E - Codeforces 二、解题报告 1、思路分析 看数据量盲猜O(…

树的层序遍历(详解)

下面以一道力扣题为例&#xff1a; 代码和解释如下&#xff1a; /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(…

零基础HTML教程(31)--HTML5多媒体

文章目录 1. 背景2. audio音频3. video视频4. audio与video常用属性5. 小结 1. 背景 在H5之前&#xff0c;我们要在网页上播放音频、视频&#xff0c;需要借助第三方插件。 这些插件里面最火的就是Flash了&#xff0c;使用它有几个问题&#xff1a; 首先要单独安装Flash&…
最新文章