Compose 动画api之我的电子木鱼青春版

提示:需要对基本的compose语法有所了解`


前言

阅读本文需要一定compose基础,如果没有请移步Jetpack Compose入门详解(实时更新)

前阵子敲电子木鱼,见初音未来比较火,博主空闲时也去下了一个解闷,但是市面上大多数都是这类型的app全是广告,敲的时候木鱼都不动,也太假了,换个音效还要收费,于是乎萌生了自己弄一个的想法

阅读本文你可以学习到

  • Compose沉浸式样式
  • Compose一些动画API例如 animateSizeAsStateinfiniteTransitionAnimatedVisibility
  • Compose沉底样式的Dialog
  • Compose LazyRow 中的ListItem
  • Compose的手势监听

资源文件是反编译某apk的,仅用于学习
根据官网教程总结,如有不对请在评论区指教


一、总体规划

app长这个样子,这就是我们的目标
在这里插入图片描述
为了界面的简洁我们把换音效放到设置中,所以总体界面上来说,我们需要一个中间的木鱼图片,和顶部的一些显示按钮的实现
在这里插入图片描述

二、我的木鱼

它需要在点击时播放木鱼敲击的音效和动画,并提示我们功德+1,
.

1.敲击监听 pointerInput,detectTapGestures

一开始我监听了点击木鱼时的方法Modifier. .clickable发现并不能实现我想要的效果 ,于是我使用了监听手势的更低级别的api,使用了Modifier.pointerInput来监听木鱼图片的输入事件
代码如下(示例):

 Image(
       painter = painterResource(R.mipmap.wood_fish),
       contentDescription = "电子木鱼",
       modifier = Modifier
            .size(50.dp, 50.dp)
            .pointerInput(Unit) {
                        detectTapGestures(
                            onPress = {
								//TO DO
                            }
                        )
                    }
       )

这里重点说下 detectTapGestures这个方法看下它的源码
在这里插入图片描述
这里包含了四个方法,我们顾名思义四个方法分别监听了 两次在上方;长按;按压;在上方四个监听,并且这个函数上方还有
在这里插入图片描述
这两个方法可以监听按压取消,我们可以在图片被按压时将木鱼缩小,在按压取消时将木鱼复原以达到模拟木鱼被敲击的目的,因此我们可以在上方被注释的TO DO 代码中加入如下判断

                               if (tryAwaitRelease()) {
                                   //按压被取消了
                                } else {
                                  //按压被打断了
                                }

2.木鱼动画

现在我们来看如何改变木鱼的大小来模拟木鱼被敲击的动画和音效
音效倒是简单,在监听到按压时直接播放就好了
代码如下(示例):

mMediaPlayer.start()

动画需要动态的改变图片的大小,这里我使用了动画api animateSizeAsState 并配合按压的状态实现,于是代码变成了如下这个样子

val press = rememberSaveable {mutableStateOf(false) }
val size  = animateSizeAsState(targetValue = if (press.value) Size(120f, 120f) else Size(150f, 150f) )
var mMediaPlayer by remember {mutableStateOf( MediaPlayer.create(context,R.raw.wooden_fish01)) }
 Image(
                painter = painterResource(R.mipmap.wood_fish),
                contentDescription = "电子木鱼",
                modifier = Modifier
                    .size(size.value.width.dp, size.value.height.dp)
                    .pointerInput(Unit) {
                        detectTapGestures(
                            onPress = {
                                press.value = !press.value
                                mMediaPlayer.start()
                                if (tryAwaitRelease()) {
                                    press.value = !press.value
                                } else {
                                    press.value = !press.value
                                }
                            }
                        )
                    }
            )

3.木鱼文字

然后就是功德+1的显示,使用了AnimatedVisibility动画api,同样绑定按压的状态

            AnimatedVisibility(
                visible = press.value,
                enter =  slideInVertically(
                    initialOffsetY = { fullHeight -> -fullHeight },
                    animationSpec = tween(durationMillis = 250, easing = LinearOutSlowInEasing)
                ) ,
                exit = slideOutVertically(
                    targetOffsetY = { fullHeight -> -fullHeight },
                    animationSpec = tween(durationMillis = 250, easing = LinearOutSlowInEasing)
                ) + fadeOut()
            ){
                Text(
                    text = "功德+1",
                    textAlign = TextAlign.Center,
                    fontSize = 24.sp,
                    modifier = Modifier.width(150.dp)
                    )
            }

这里使用了水平平滑的 slideInVertically和slideOutVertically ,一个是显示进入时的动画,一个是退出隐藏时的动画效果;initialOffsetY 是偏移量,tween是整个动画播放的时间,LinearOutSlowInEasing顾名思义就是缓慢的播放,退出时多加了个fadeOut(),淡出,说明显示和隐藏的动画是可以组合的,除了使用的三个api,Compose主要将动画分为EnterTransition和ExitTransition两个基类分别继承以实现多种效果,淡入淡出,滑动,缩放之类的,Compose的动画API光看的话还是很烧脑的,还是要动手才理的清楚

到这里我们的木鱼基本就已经可以实现敲击的效果了

三、顶部实现及逻辑补充

1.Compose沉浸式样式

compose应用一般会使用脚手架比如TopAppBar之类的顶部控件,但是我们的木鱼不需要,它需要的是一个沉浸式的黑色样式而不是一个上方的菜单,再查阅一翻资料后找到了别人的方法(忘记在哪儿看的了…很多都没效果)

  
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        WindowCompat.setDecorFitsSystemWindows(window, false)
        setContent {
            ElectronicWoodfishComposeTheme {
                ProvideWindowInsets{
                    val systemUiController = rememberSystemUiController()
                    SideEffect {
                        systemUiController.setStatusBarColor(Color.Transparent, darkIcons = false)
                    }
                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colors.background
                    ) {
                        //页面

                    }
                }

            }
        }
    }

更优雅的方法(补充)

最近 学习一个仿网易云的项目了解到更优雅的实现方式,分为了两个步骤来更改状态栏,可以看需求使用,采用了kotlin的拓展方法,将其作为activity的拓展方法,比较适合写在基类,优雅,实在是太优雅了

/**
 * Created by ssk on 2022/4/17.
 */

/**
 * 设置为沉浸式状态栏
 */
fun Activity.transparentStatusBar() {
    WindowCompat.setDecorFitsSystemWindows(window, false)
}

/**
 * 状态栏反色
 */
fun Activity.setAndroidNativeLightStatusBar() {
    val decor = window.decorView
    val isDark = resources.configuration.uiMode == 0x21
    if (!isDark) {
        decor.systemUiVisibility =
            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
    } else {
        decor.systemUiVisibility =
            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
    }
}
/**
 * @author zengyifeng
 * @date createDate:2023-03-21
 * @brief description  设置状态栏透明,图标白色
 */

@Composable
fun setFixSystemBarsColor() {
    val sysUiController = rememberSystemUiController()
	sysUiController.setSystemBarsColor(Color.Transparent, false)
}

依赖
    implementation "com.google.accompanist:accompanist-systemuicontroller:0.15.0"

在setContent前使用

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        transparentStatusBar()
        setAndroidNativeLightStatusBar()
        setContent {
		setFixSystemBarsColor()
		...
		}

1.顶部布局

这里就是一个简单的Compose布局,一个从左到右的Row,然后加了一个number 来记录敲击木鱼的数量,并通过封装好的SharedPreferences将数量存起来,showDialog 则是后面展示dialog使用到的变量


val number = rememberSaveable {mutableStateOf(num)}
 var showDialog by remember {mutableStateOf(false)}
 Row {
                Image(
                    painter = painterResource(R.mipmap.ic_set_up),
                    contentDescription = "设置",
                    modifier = Modifier
                        .weight(1f)
                        .padding(start = 15.dp)
                        .clickable {
                            showDialog = !showDialog
                        }
                )
                Text(
                    text = number.value.toString(),
                    textAlign = TextAlign.Center,
                    fontSize = 24.sp,
                    modifier =  Modifier.weight(2f)
                )
                Text(
                    text = "重置",
                    textAlign = TextAlign.Right,
                    fontSize = 18.sp,
                    modifier =
                    Modifier
                        .clickable {
                            number.value = 0
                            //保存number
                            saveMerits(number.value.toString(), context, "Merits")
                        }
                        .weight(1f)
                        .padding(end = 15.dp)
                )
            }

2.底部dialog

通过showDialog控制显示,将图片和音频作为状态来保存便于切换
dialog代码部分如下:

        val scrollerLazyStata = rememberLazyListState()
        val mp3List :MutableList<Int> = mutableListOf()
        mp3List.add(R.raw.wooden_fish01)
        mp3List.add(R.raw.wooden_fish02)
        mp3List.add(R.raw.wooden_fish03)
        mp3List.add(R.raw.wooden_fish04)
        mp3List.add(R.raw.wooden_fish05)
        mp3List.add(R.raw.wooden_fish06)
        mp3List.add(R.raw.wooden_fish07)
        mp3List.add(R.raw.wooden_fish08)
        mp3List.add(R.raw.wooden_fish09)
        mp3List.add(R.raw.wooden_fish10)
        mp3List.add(R.raw.wooden_fish11)
        mp3List.add(R.raw.wooden_fish12)
        mp3List.add(R.raw.wooden_fish13)
        mp3List.add(R.raw.wooden_fish14)

        var woodFishPic = rememberSaveable {
            mutableStateOf(R.mipmap.wood_fish)
        }

        //Dialog
        AnimatedVisibility(
            visible = showDialog
        ){
            Dialog(onDismissRequest = { }, properties = DialogProperties(usePlatformDefaultWidth = false)) {
                Box(
                    Modifier
                        .fillMaxWidth()
                        .fillMaxHeight()
                        .clickable {
                            showDialog = false
                         
                        }) {
                    Column(
                        Modifier
                            .fillMaxWidth()
                            .height(300.dp)
                            .clickable { }
                            .align(Alignment.BottomCenter)
                            .background(MaterialTheme.colors.background)){
                        Row(Modifier.padding(top = 15.dp, bottom = 15.dp),verticalAlignment = Alignment.Top) {
                            Spacer(Modifier.weight(1f))
                            TextField(value = text.value, onValueChange = {
                                text.value = it
                                saveMerits(it,context,"MeritsText")
                            } )
                            Spacer(Modifier.weight(1f))

                        }
                        Row(Modifier.height(45.dp)) {
                            Image(
                                painter = painterResource(id = R.mipmap.wood_fish),
                                contentDescription = null,
                                modifier = Modifier
                                    .clickable {
                                        woodFishPic.value = R.mipmap.wood_fish
                                    }
                                    .padding(start = 45.dp, end = 45.dp)
                            )
                            Image(
                                painter = painterResource(id = R.mipmap.wooden_fish),
                                contentDescription = null,
                                modifier = Modifier.clickable {
                                    woodFishPic.value = R.mipmap.wooden_fish
                                })
                        }
                        Spacer(modifier = Modifier.height(15.dp))
                        LazyRow(Modifier.height(45.dp), state = scrollerLazyStata){
                            items(14){ index ->
                                ListItem(
                                    trailing = { Text(text = "木鱼音效$index ", color = Color.White) },
                                    secondaryText  = { Text(text = "木鱼音效$index ", color = Color.White) },
                                    text  = { Text(text = "木鱼音效$index ", color = Color.White) },
                                    icon = { Icon(painter = painterResource(R.mipmap.wood_fish), contentDescription = "")},
                                    modifier = Modifier.clickable {
                                        mMediaPlayer = MediaPlayer.create(context,
                                            mp3List[index]
                                        )
                                    }
                                )
                            }
                        }
                        Column (    modifier = Modifier
                                    .fillMaxWidth(),
                                horizontalAlignment = Alignment.CenterHorizontally,
                                ) {
                           Button(onClick = {
                               showDialog = false
                              
                           }) {
                               Text(text = "确定")
                           }

                        }
                    }
                }

            }
        }


这里的Modifier.align(Alignment.BottomCenter)控制了dialog从此下方弹出;ListItem包含了木鱼的14个音效,我们来看下ListItem的源码
在这里插入图片描述
ListItem的这些参数包含了图标和文本等插槽可以自行填写,但注意有些插槽像Text函数一样,文本是必不可少的,而且因为是实验性api,目前还需要在使用的方法前加上两个注解,以表明是实验性的

@OptIn(ExperimentalMaterialApi::class)
@ExperimentalComposeUiApi

3.加载loading

我们做这个绝不是为了敲电子木鱼哦,是为了学习,所以在点击确认时我人为的加了一个loading图案,这里使用了infiniteTransition函数来使图片永远的旋转以达到加载的一个假象

        val infiniteTransition = rememberInfiniteTransition()
        val angle by infiniteTransition.animateFloat(
            initialValue = 0F,
            targetValue = 360F,
            animationSpec = infiniteRepeatable(
                animation = tween(2000, easing = LinearEasing)
            )
        )

让图片在两秒内从0旋转360度,再调用图片的

Modifier..graphicsLayer {  rotationZ = angle}

方法让其绕Z坐标轴旋转,完成!

四、完整源码

效果

在这里插入图片描述

做了一点点优化的源码,ShareUtil是SharedPreferences工具类



import android.content.Context
import android.media.MediaPlayer
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.*
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.constraintlayout.compose.ConstraintLayout
import com.zyf.electronicwoodfish.util.ShareUtil
import java.util.*
import kotlin.concurrent.schedule
import com.zyf.electronicwoodfish.R
import com.zyf.electronicwoodfish.nav.NavController
import com.zyf.electronicwoodfish.nav.RouterUrls

/**
 * @author zengyifeng
 * @date createDate:2023-03-21
 * @brief description
 */

@OptIn(ExperimentalMaterialApi::class)
@ExperimentalComposeUiApi
@Composable
fun ScreenPage(context: Context){
    ConstraintLayout (Modifier.background(Color(0xFF000000))){
        val (topLayout,center,btn) =createRefs()

        var showDialog by remember { mutableStateOf(false) }
        var showLoading by remember { mutableStateOf(false) }
        val text = rememberSaveable{
            mutableStateOf(getMeritsText(context))
        }
        val num:Int = getMerits(context)
        val number = rememberSaveable {
            mutableStateOf(num)
        }
        var mMediaPlayer by remember {
            mutableStateOf( MediaPlayer.create(context,R.raw.wooden_fish01))
        }

        val press = rememberSaveable {
            mutableStateOf(false)
        }
        val size  = animateSizeAsState(targetValue = if (press.value) Size(120f, 120f) else Size(150f, 150f) )


        val infiniteTransition = rememberInfiniteTransition()
        val angle by infiniteTransition.animateFloat(
            initialValue = 0F,
            targetValue = 360F,
            animationSpec = infiniteRepeatable(
                animation = tween(2000, easing = LinearEasing)
            )
        )
        val scrollerLazyStata = rememberLazyListState()
        val mp3List :MutableList<Int> = mutableListOf()
        mp3List.add(R.raw.wooden_fish01)
        mp3List.add(R.raw.wooden_fish02)
        mp3List.add(R.raw.wooden_fish03)
        mp3List.add(R.raw.wooden_fish04)
        mp3List.add(R.raw.wooden_fish05)
        mp3List.add(R.raw.wooden_fish06)
        mp3List.add(R.raw.wooden_fish07)
        mp3List.add(R.raw.wooden_fish08)
        mp3List.add(R.raw.wooden_fish09)
        mp3List.add(R.raw.wooden_fish10)
        mp3List.add(R.raw.wooden_fish11)
        mp3List.add(R.raw.wooden_fish12)
        mp3List.add(R.raw.wooden_fish13)
        mp3List.add(R.raw.wooden_fish14)

        var woodFishPic = rememberSaveable {
            mutableStateOf(R.mipmap.wood_fish)
        }
        /*
        * 顶部按钮
        * */
        Column(
            Modifier.constrainAs(topLayout){
                top.linkTo(parent.top)
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }
        ){

            Text(
                "",
                modifier =
                Modifier
                    .height(50.dp)
            )

            Row {
                Image(
                    painter = painterResource(R.mipmap.ic_set_up),
                    contentDescription = "设置",
                    modifier = Modifier
                        .weight(1f)
                        .padding(start = 15.dp)
                        .clickable {
                            showDialog = !showDialog
                        }
                )
                Text(
                    text = number.value.toString(),
                    textAlign = TextAlign.Center,
                    fontSize = 24.sp,
                    modifier = Modifier.weight(2f)
                )
                Text(
                    text = "重置",
                    textAlign = TextAlign.Right,
                    fontSize = 18.sp,
                    modifier =
                    Modifier
                        .clickable {
                            number.value = 0
                            saveMerits(number.value.toString(), context, "Merits")
                        }
                        .weight(1f)
                        .padding(end = 15.dp)
                )
            }




        }

        /*
        * 中间的木鱼
        * */
        Column(
            modifier = Modifier.constrainAs(center){
                top.linkTo(topLayout.top)
                bottom.linkTo(parent.bottom)
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }

        ) {
            AnimatedVisibility(
                visible = press.value,
                enter =  slideInVertically(
                    initialOffsetY = { fullHeight -> -fullHeight },
                    animationSpec = tween(durationMillis = 250, easing = LinearOutSlowInEasing)
                ) ,
                exit = slideOutVertically(
                    targetOffsetY = { fullHeight -> -fullHeight },
                    animationSpec = tween(durationMillis = 250, easing = LinearOutSlowInEasing)
                ) + fadeOut()
            ){
                Text(
                    text = text.value,
                    textAlign = TextAlign.Center,
                    fontSize = 24.sp,
                    modifier = Modifier.width(150.dp)
                )
            }
            Image(
                painter = painterResource(woodFishPic.value),
                contentDescription = "电子木鱼",
                modifier = Modifier
                    .size(size.value.width.dp, size.value.height.dp)
                    .pointerInput(Unit) {
                        detectTapGestures(
                            onPress = {
                                press.value = !press.value
                                number.value++
                                saveMerits(number.value.toString(), context, "Merits")
                                mMediaPlayer.start()
                                if (tryAwaitRelease()) {
                                    press.value = !press.value
                                } else {
                                    press.value = !press.value
                                }
                            }
                        )
                    }
            )
        }



        Button(
            modifier = Modifier
                .background(Color(0xFF000000))
                .constrainAs(btn) {
                    bottom.linkTo(parent.bottom)
                },
            onClick = { NavController.instance.navigate(RouterUrls.SURPROSEPAGE) }
        ) {

        }

        //Dialog
        AnimatedVisibility(
            visible = showDialog
        ){
            Dialog(onDismissRequest = { }, properties = DialogProperties(usePlatformDefaultWidth = false)) {
                Box(
                    Modifier
                        .fillMaxWidth()
                        .fillMaxHeight()
                        .clickable {
                            showDialog = false
                            showLoading = true
                        }) {
                    Column(
                        Modifier
                            .fillMaxWidth()
                            .height(300.dp)
                            .clickable { }
                            .align(Alignment.BottomCenter)
                            .background(MaterialTheme.colors.background)){
                        Row(Modifier.padding(top = 15.dp, bottom = 15.dp),verticalAlignment = Alignment.Top) {
                            Spacer(Modifier.weight(1f))
                            TextField(value = text.value, onValueChange = {
                                text.value = it
                                saveMerits(it,context,"MeritsText")
                            } )
                            Spacer(Modifier.weight(1f))

                        }
                        Row(Modifier.height(45.dp)) {
                            Image(
                                painter = painterResource(id = R.mipmap.wood_fish),
                                contentDescription = null,
                                modifier = Modifier
                                    .clickable {
                                        woodFishPic.value = R.mipmap.wood_fish
                                    }
                                    .padding(start = 45.dp, end = 45.dp)
                            )
                            Image(
                                painter = painterResource(id = R.mipmap.wooden_fish),
                                contentDescription = null,
                                modifier = Modifier.clickable {
                                    woodFishPic.value = R.mipmap.wooden_fish
                                })
                        }
                        Spacer(modifier = Modifier.height(15.dp))
                        LazyRow(Modifier.height(45.dp), state = scrollerLazyStata){
                            items(14){ index ->
                                ListItem(
                                    trailing = { Text(text = "木鱼音效$index ", color = Color.White) },
                                    secondaryText  = { Text(text = "木鱼音效$index ", color = Color.White) },
                                    text  = { Text(text = "木鱼音效$index ", color = Color.White) },
                                    icon = { Icon(painter = painterResource(R.mipmap.wood_fish), contentDescription = "") },
                                    modifier = Modifier.clickable {
                                        mMediaPlayer = MediaPlayer.create(context,
                                            mp3List[index]
                                        )
                                    }
                                )
                            }
                        }
                        Column (    modifier = Modifier
                            .fillMaxWidth(),
                            horizontalAlignment = Alignment.CenterHorizontally,
                        ) {
                            Button(onClick = {
                                showDialog = false
                                showLoading = true
                            }) {
                                Text(text = "确定")
                            }

                        }
                    }
                }

            }
        }

        //loading
        if (showLoading){
            Timer().schedule(200){
                showLoading = false
            }
            Dialog(onDismissRequest = { }, properties = DialogProperties(usePlatformDefaultWidth = false)) {
                Box(
                    Modifier
                        .fillMaxWidth()
                        .fillMaxHeight()
                        .clickable {
                            showLoading = false
                        }) {
                    Column(
                        Modifier
                            .width(75.dp)
                            .height(75.dp)
                            .clickable { }
                            .align(Alignment.Center)
                    ){
                        Image(
                            painter = painterResource(R.mipmap.icon_loading),
                            contentDescription = "设置",
                            modifier =
                            Modifier
                                .width(75.dp)
                                .height(75.dp)
                                .graphicsLayer {
                                    rotationZ = angle
                                }

                        )
                    }
                }

            }
        }


    }
}

fun getMeritsText(context: Context): String {
    val value: String? = ShareUtil.getString("MeritsText", context)
    if (value.isNullOrEmpty()) {
        ShareUtil.putString("MeritsText", "功德+1", context)
        return "功德+1"
    }
    return  value
}


fun getMerits(context: Context): Int {
    val value: String? = ShareUtil.getString("Merits", context)
    if (value.isNullOrEmpty()) {
        ShareUtil.putString("Merits", "0", context)
        return 0
    }
    return  value.toInt()
}


fun saveMerits(number: String, context: Context, key : String){
    ShareUtil.putString(key, number, context)
}





总结

我绝对不是因为音效收费自己写的,我是为了学习

最近

附上体验的下载链接电子木鱼,2023年8月19日
14:25到期,到期过后还需要的私信我

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我怀里的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值