[Kotlin]引导页

news2024/11/16 13:25: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/1616300.html

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

相关文章

海康Visionmaster-常见问题排查方法-安装阶段

VM软加密安装失败&#xff0c;报错&#xff1a;软件未激活&#xff0c;是否进行授权激活&#xff1b; 解决方法&#xff1a;如确认已完成授权&#xff0c;此时打上试用用补丁即可。补充VM400试用版本正确安装顺序如下&#xff1a; 安装顺序&#xff1a; ①安装基础安装包&…

14.接口自动化测试-造数据

1.测试造数据 工作场景&#xff1a; 需要造一批测试数据 解决方案&#xff1a; &#xff08;1&#xff09;使用字符串拼接 135XXXXX &#xff08;2&#xff09;使用第三方库去做 faker 安装&#xff1a; pip install Faker 若安装不成功&#xff0c;可能是需要清下缓存&a…

ChatGPT在线网页版(与GPT Plus会员完全一致)

ChatGPT镜像 今天在知乎看到一个问题&#xff1a;“平民不参与内测的话没有账号还有机会使用ChatGPT吗&#xff1f;” 从去年GPT大火到现在&#xff0c;关于GPT的消息铺天盖地&#xff0c;真要有心想要去用&#xff0c;途径很多&#xff0c;别的不说&#xff0c;国内GPT的镜像…

Linux及tmux、vim常用命令

Linux 关于Linux的简介、诞生、迭代&#xff0c;大家可以去网上查一查&#xff0c;这里不多做赘述了 Linux文件类型 非常重要的文件类型有: 普通文件&#xff0c;目录文件&#xff0c;链接文件&#xff0c;设备文件&#xff0c;管道文件&#xff0c;Socket 套接字文件 等。 …

SLICEM是如何将查找表配置为分布式RAM/移位寄存器的

1.首先说SliceM和SliceL如何配置为ROM的 一个SLICE包含4个六输入查找表&#xff0c;因此每个查找表就能存储64bit的数据&#xff0c;要实现128bit的ROM&#xff0c;只需要通过两个LUT就可实现&#xff0c;具体如下表: 2.如何配置成为分布式RAM SLICEM中的LUT如下图&#xff…

macbook内存怎么清理?2024年有哪些好用的软件

当你的MacBook运行缓慢时&#xff0c;这很可能是因为内存&#xff08;RAM&#xff09;满了。内存是计算机的临时存储区&#xff0c;用于存放当前正在使用的程序和数据。当内存满时&#xff0c;MacBook就会使用硬盘作为临时内存&#xff0c;这大大降低了运行速度。那么&#xff…

ffmpeg初体验

一&#xff1a;安装 sudo yum install epel-release -y sudo yum update -ysudo rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpmyum -y install …

docker-MySQL 8 主从搭建

一.目录结构&#xff1a; 我是在/home目录下&#xff0c;建立个sql文件夹&#xff1a; 二、配置文件 1.mysql配置 mysql-master下.conf文件配置 ###### [mysqld] server-id1 # 启用二进制日志 log-binmaster-bin # 指定需要复制的数据库 binlog-do-dbtest_db # 指定二进制日…

《动手学深度学习(Pytorch版)》Task01:初识深度学习——4.22打卡

深度学习介绍 AI地图 自然语言处理&#xff1a;起源于符号学&#xff0c;如机器翻译&#xff0c;人在几秒钟能反应过来&#xff0c;属于感知问题计算机视觉&#xff1a;图片由像素组成&#xff0c;难以用符号学解释&#xff0c;在图片中进行推理&#xff0c;大部分用概率模型或…

Android驱动开发之如何编译和更换内核

编译内核可以使用图形化的界面配置,也可以直接使用脚本。在X86_64模拟器环境下,不用交叉编译,而交叉编译工具很容易出现兼容问题,一般也只能使用芯片厂商提供的工具,而不是GNU提供的工具。 android内核开发流程以及架构变化了很多,详情请看 内核官网 内核版本选择 由…

若依集成mybatisplus报错找不到xml

引用&#xff1a;https://blog.csdn.net/qq_65080131/article/details/136677276 MybatisPlusAutoConfiguration中可以知道&#xff0c;系统会自动配置SqlSessionFactory&#xff0c;&#xff0c;但是&#xff0c;当你有自定义的SqlSessionFactory&#xff0c;&#xff0c;就会…

如何使用rdtsc和C/C++来测量运行时间(如何使用内联汇编和获取CPU的TSC时钟频率)

本文主要是一个实验和思维扩展&#xff0c;除非你有特殊用途&#xff0c;不然不要使用汇编指令来实现这个功能。扩展阅读就列出了一些不需要内联汇编实现的 写本文是因为为了《Windows上的类似clock_gettime(CLOCK_MONOTONIC)的高精度测量时间函数》这篇文章找资料的时候&…

arm架构,django4.2.7适配达梦8数据库

【Python相关包版本信息】 Django 4.2.7 django-dmPython 3.1.7 dmPython 2.5.5 【达梦数据库版本】 DM Database Server 64 V8 DB Version: 0x7000c 适配过程中发现的问题如下&#xff1a; 错误一&#xff1a;d…

Opencv | 图像卷积与形态学变换操作

这里写目录标题 一. 滤波 / 卷积操作1. 平滑均值滤波/卷积2. 平滑中值滤波/卷积3. 平滑高斯滤波/卷积3.1 关注区域3.2 分解特性 二. 形态学变换1. 常用核2. cv.erode ( ) 腐蚀操作3. cv.dilate ( ) 膨胀操作4. Open 操作5. Close 操作6. Morphological Gradient 形态梯度操作7.…

为什么单片机控制电机需要加电机驱动

通常很多地方只是单纯的单片机MCU没有对电机的驱动能力&#xff0c;或者是介绍关于电机驱动的作用&#xff0c;如&#xff1a; 提高电机的效率和精度。驱动器采用先进的电子技术和控制算法&#xff0c;能够精准控制电机的参数和运行状态&#xff0c;提高了电机的效率和精度。拓…

vos3000外呼系统客户端无法安装如何解决?

如果 VOS3000 外呼系统客户端无法安装&#xff0c;可以尝试以下解决方法&#xff1a; 检查系统要求&#xff1a; 确保你的计算机满足 VOS3000 外呼系统客户端的系统要求&#xff0c;包括操作系统版本、内存、处理器等。如果系统不符合要求&#xff0c;可能会导致安装失败或者运…

[BT]BUUCTF刷题第20天(4.22)

第20天 Web [GWCTF 2019]我有一个数据库 打开网站发现乱码信息&#xff08;查看其他题解发现显示的是&#xff1a;我有一个数据库&#xff0c;但里面什么也没有~ 不信你找&#xff09; 但也不是明显信息&#xff0c;通过dirsearch扫描得到robots.txt&#xff0c;然后在里面得…

echarts 双堆叠柱状图(数据整理)

1.后台返回的数据格式 {"code": "0000","message": "","messageCode": "操作成功","sign": null,"detail": null,"data": {"pieChart": [{"key": "产品…

C++之写时复制(CopyOnWrite)

设计模式专栏&#xff1a;http://t.csdnimg.cn/4j9Cq 目录 1.简介 2.实现原理 3.QString的实现分析 3.1.内部结构 3.2.写入时复制 4.示例分析 5.使用场景 6.总结 1.简介 CopyOnWrite (COW) 是一种编程思想&#xff0c;用于优化内存使用和提高性能。COW 的基本思想是&am…

编译支持播放H265的cef控件

接着在上次编译的基础上增加h265支持编译支持视频播放的cef控件&#xff08;h264&#xff09; 测试页面&#xff0c;直接使用cef_enhancement,里边带着的那个html即可&#xff0c;h265视频去这个网站下载elecard,我修改的这个版本参考了里边的修改方式&#xff0c;不过我的这个…