Android折叠屏开发学习(三)---使用MotionLayout实现折叠屏分屏效果

学更好的别人,

做更好的自己。

——《微卡智享》

f82b8d7c6f652d877ba1845ee7044976.png

本文长度为6259,预计阅读11分钟

前言

今天是折叠屏开发的第三篇,前面已经介绍了铰链的角度监听和Jetpack Window实现监听效果,今天我们就来做个折叠状态和展开状态显示的不同效果Demo,本篇的重点主要是两个,一是布局文件的设计,另一个就是MotionLayout的动画效果。

83ccc21e6331984e2fc2217d4417fd6d.png

实现效果

cc78d54b8ea204fcc1c8fbecf2a77949.gif

f36173700f46785469556eb617e850c7.png

竖屏折叠

9bc225ff60fac2174413b272da4f8a2f.png

竖屏展开

26b744c513dc13476fff8432d37f57a8.png

横屏折叠

1a5e2dfec1e7f82aa4cff36d85faf63c.png

横屏展开

上图中可以看到,竖屏折叠时,宫格布局和按钮都在同一界面,按钮在下方,当竖屏展开后,宫格布局移动到左边,而按钮布局移动到右边了,并且由原来的水平排列改为了垂直排列(完整的效果视频看P2)。接下来就来看看怎么实现的。

代码实现

24bed6fa54cc5f4258842bbeb6e5c64e.png

微卡智享

核心代码

实现分屏布局,最主要的就是靠我们自己定义的一个FrameLayout,里面内置了WindowLayoutInfo的参数,参数传入的WindowLayoutInfo来判断当前的什么状态,而应用什么样的布局(左右,上下还是合并)

首先要创建一个attr.xml

c9ab3adb0b2859d39984eae6345a6796.png

<resources>
    <declare-styleable name="SplitLayout">
        <attr name="startViewId" format="reference" />
        <attr name="endViewId" format="reference" />
    </declare-styleable>
</resources>

SplitLayout的代码:

package pers.vaccae.mvidemo.ui.view


import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.constraintlayout.widget.ConstraintAttribute.setAttributes
import androidx.window.layout.DisplayFeature
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowLayoutInfo
import pers.vaccae.mvidemo.R


/**
 * 作者:Vaccae
 * 邮箱:3657447@qq.com
 * 创建时间:15:07
 * 功能模块说明:
 */
class SplitLayout :FrameLayout{


    private var windowLayoutInfo: WindowLayoutInfo? = null
    private var startViewId = 0
    private var endViewId = 0


    private var lastWidthMeasureSpec: Int = 0
    private var lastHeightMeasureSpec: Int = 0


    constructor(context: Context) : super(context)


    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        setAttributes(attrs)
    }


    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        setAttributes(attrs)
    }


    private fun setAttributes(attrs: AttributeSet?) {
        context.theme.obtainStyledAttributes(attrs, R.styleable.SplitLayout, 0, 0).apply {
            try {
                startViewId = getResourceId(R.styleable.SplitLayout_startViewId, 0)
                endViewId = getResourceId(R.styleable.SplitLayout_endViewId, 0)
            } finally {
                recycle()
            }
        }
    }




    fun updateWindowLayout(windowLayoutInfo: WindowLayoutInfo) {
        this.windowLayoutInfo = windowLayoutInfo
        requestLayout()
    }


    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
        val startView = findStartView()
        val endView = findEndView()
        val splitPositions = splitViewPositions(startView, endView)


        if (startView != null && endView != null && splitPositions != null) {
            val startPosition = splitPositions[0]
            val startWidthSpec = MeasureSpec.makeMeasureSpec(startPosition.width(),
                MeasureSpec.EXACTLY
            )
            val startHeightSpec = MeasureSpec.makeMeasureSpec(startPosition.height(),
                MeasureSpec.EXACTLY
            )
            startView.measure(startWidthSpec, startHeightSpec)
            startView.layout(
                startPosition.left, startPosition.top, startPosition.right,
                startPosition.bottom
            )


            val endPosition = splitPositions[1]
            val endWidthSpec = MeasureSpec.makeMeasureSpec(endPosition.width(), MeasureSpec.EXACTLY)
            val endHeightSpec = MeasureSpec.makeMeasureSpec(endPosition.height(),
                MeasureSpec.EXACTLY
            )
            endView.measure(endWidthSpec, endHeightSpec)
            endView.layout(
                endPosition.left, endPosition.top, endPosition.right,
                endPosition.bottom
            )
        } else {
            super.onLayout(changed, left, top, right, bottom)
        }
    }


    private fun findStartView(): View? {
        var startView = findViewById<View>(startViewId)
        if (startView == null && childCount > 0) {
            startView = getChildAt(0)
        }
        return startView
    }


    private fun findEndView(): View? {
        var endView = findViewById<View>(endViewId)
        if (endView == null && childCount > 1) {
            endView = getChildAt(1)
        }
        return endView
    }


    private fun splitViewPositions(startView: View?, endView: View?): Array<Rect>? {
        if (windowLayoutInfo == null || startView == null || endView == null) {
            return null
        }


        // Calculate the area for view's content with padding
        val paddedWidth = width - paddingLeft - paddingRight
        val paddedHeight = height - paddingTop - paddingBottom


        windowLayoutInfo?.displayFeatures
            ?.firstOrNull { feature -> isValidFoldFeature(feature) }
            ?.let { feature ->
                getFeaturePositionInViewRect(feature, this)?.let {
                    if (feature.bounds.left == 0) { // Horizontal layout
                        val topRect = Rect(
                            paddingLeft, paddingTop,
                            paddingLeft + paddedWidth, it.top
                        )
                        val bottomRect = Rect(
                            paddingLeft, it.bottom,
                            paddingLeft + paddedWidth, paddingTop + paddedHeight
                        )


                        if (measureAndCheckMinSize(topRect, startView) &&
                            measureAndCheckMinSize(bottomRect, endView)
                        ) {
                            return arrayOf(topRect, bottomRect)
                        }
                    } else if (feature.bounds.top == 0) { // Vertical layout
                        val leftRect = Rect(
                            paddingLeft, paddingTop,
                            it.left, paddingTop + paddedHeight
                        )
                        val rightRect = Rect(
                            it.right, paddingTop,
                            paddingLeft + paddedWidth, paddingTop + paddedHeight
                        )


                        if (measureAndCheckMinSize(leftRect, startView) &&
                            measureAndCheckMinSize(rightRect, endView)
                        ) {
                            return arrayOf(leftRect, rightRect)
                        }
                    }
                }
            }


        // We have tried to fit the children and measured them previously. Since they didn't fit,
        // we need to measure again to update the stored values.
        measure(lastWidthMeasureSpec, lastHeightMeasureSpec)
        return null
    }


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        lastWidthMeasureSpec = widthMeasureSpec
        lastHeightMeasureSpec = heightMeasureSpec
    }


    private fun measureAndCheckMinSize(rect: Rect, childView: View): Boolean {
        val widthSpec = MeasureSpec.makeMeasureSpec(rect.width(), MeasureSpec.AT_MOST)
        val heightSpec = MeasureSpec.makeMeasureSpec(rect.height(), MeasureSpec.AT_MOST)
        childView.measure(widthSpec, heightSpec)
        return childView.measuredWidthAndState and MEASURED_STATE_TOO_SMALL == 0 &&
                childView.measuredHeightAndState and MEASURED_STATE_TOO_SMALL == 0
    }


    private fun isValidFoldFeature(displayFeature: DisplayFeature) =
        (displayFeature as? FoldingFeature)?.let { feature ->
            getFeaturePositionInViewRect(feature, this) != null
        } ?: false




    private fun getFeaturePositionInViewRect(
        displayFeature: DisplayFeature,
        view: View,
        includePadding: Boolean = true
    ): Rect? {
        // The the location of the view in window to be in the same coordinate space as the feature.
        val viewLocationInWindow = IntArray(2)
        view.getLocationInWindow(viewLocationInWindow)


        // Intersect the feature rectangle in window with view rectangle to clip the bounds.
        val viewRect = Rect(
            viewLocationInWindow[0], viewLocationInWindow[1],
            viewLocationInWindow[0] + view.width, viewLocationInWindow[1] + view.height
        )


        // Include padding if needed
        if (includePadding) {
            viewRect.left += view.paddingLeft
            viewRect.top += view.paddingTop
            viewRect.right -= view.paddingRight
            viewRect.bottom -= view.paddingBottom
        }


        val featureRectInView = Rect(displayFeature.bounds)
        val intersects = featureRectInView.intersect(viewRect)
        if ((featureRectInView.width() == 0 && featureRectInView.height() == 0) ||
            !intersects
        ) {
            return null
        }


        // Offset the feature coordinates to view coordinate space start point
        featureRectInView.offset(-viewLocationInWindow[0], -viewLocationInWindow[1])


        return featureRectInView
    }


}

01

创建分屏的布局文件xml

要实现分屏的效果显示,需要创建两个不同的布局文件,像图中的宫格列表,还有按钮的布局分别在两个不同的xml中。

cc084ba791907a076f9012b5b0a82015.png

split_layout_start.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"
    android:id="@+id/startLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp" />


</androidx.constraintlayout.widget.ConstraintLayout>

split_layout_end.xml(按钮布局)

<?xml version="1.0" encoding="utf-8"?>


<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/endLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="20dp"
    app:layoutDescription="@xml/split_layout_end_scene">


    <Button
        android:id="@+id/btncreate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="生成数据"
        android:layout_marginBottom="30dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toStartOf="@+id/btnadd"
        app:layout_constraintStart_toStartOf="parent" />


    <Button
        android:id="@+id/btnadd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="插入数据"
        android:layout_marginBottom="30dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />


    <Button
        android:id="@+id/btndel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="删除数据"
        android:layout_marginBottom="30dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@id/btnadd" />


</androidx.constraintlayout.motion.widget.MotionLayout>

02

创建新的Activity

创建好了我们的SplitLayout后,我们再创建一个FoldActivity。其中布局文件就要引用我们创建的SplitLayout,里面包括了刚才创建的宫格列表和按钮布局。

activity_fold.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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".ui.view.FoldActivity">


    <pers.vaccae.mvidemo.ui.view.SplitLayout
        android:id="@+id/split_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:startViewId="@id/startLayout"
        app:endViewId="@id/endLayout"
        android:padding="5dp">


        <include
            android:id="@id/startLayout"
            layout="@layout/split_layout_start" />


        <include
            android:id="@+id/endLayout"
            layout="@layout/split_layout_end" />
    </pers.vaccae.mvidemo.ui.view.SplitLayout>




</androidx.constraintlayout.widget.ConstraintLayout>

03

实现动画效果

效果图片中可以看到,我们实现位移动画的是按钮的布局,其实就是通过MotionLayout实现的。

1b016bebac35fcd6e9e203af1c3b8317.png

其中app:layoutDescription="@xml/split_layout_end_scene"是动画属性,我们当布局改为MotionLayout时,会提示要缺少layoutDescription,使用ALT+ENTER会自动创建这个xml文件,位置在res.xml下

da1572b9715b5fb9054975726c895d61.png

split_layout_end_scene.xml

<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">


    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@+id/btncreate" />
        <Constraint android:id="@+id/btnadd" />
        <Constraint android:id="@+id/btndel" />
    </ConstraintSet>


    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@id/btncreate"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="30dp"
            app:layout_constraintBottom_toTopOf="@+id/btnadd"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"/>


        <Constraint android:id="@id/btnadd"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="30dp"
            app:layout_constraintBottom_toTopOf="@+id/btndel"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btncreate"/>


        <Constraint android:id="@id/btndel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="30dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btnadd"/>
    </ConstraintSet>


    <Transition
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@+id/start"
        app:duration="500"/>
</MotionScene>

MotionScene的子元素属性标签

<Transition> 包含运动的基本定义。

其中里面的app:constraintSetStart 和 app:constraintSetEnd 指的是运动的端点。这些端点在 MotionScene 后面的 <ConstraintSet> 元素中定义。

app:duration 指定完成运动所需的毫秒数 。

e83c4f3ba054a435445997c874f72de2.png

<ConstraintSet>子元素定义一个场景约束集,并在 <ConstraintSet> 元素中使用 <Constraint> 元素定义单个 View 的属性约束。

android:id:设置当前约束集的 id。这个 id 值可被 <Transition> 元素的 app:constraintSetStart 或者 app:constraintSetEnd 引用。

4920a63a9d71278dda1a5ce3ef6c4478.png

<Constraint> 元素用来定义单个 View 的属性约束。

它支持对 View 的所有 ConstraintLayout 属性定义约束,以及对 View 的下面这些标准属性定义约束。

b7488bd5f381093f67f082d0bb6a5242.png

由上面的布局文件中可以看到,在start中,我们三个按钮的布局不变,而在end中,三个按钮的布局改为垂直布局了。代码中调用方式直接就是通过motionLayout.transitionToEnd()motionLayout.transitionToStart()跳转即可

8f88d592f97b418cb1d9dbc3fb15c0ce.png

定义motionlayout

c26ca35ee50bd044b7b0925eba8e90fb.png

判断竖屏展开时调用transitionToEnd,合上状态时调用transitionStart

FoldActivity代码:

package pers.vaccae.mvidemo.ui.view


import android.content.res.Configuration
import android.graphics.drawable.ClipDrawable.HORIZONTAL
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.Toast
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.*
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowInfoTrackerDecorator
import androidx.window.layout.WindowLayoutInfo


import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.fold
import kotlinx.coroutines.launch
import pers.vaccae.mvidemo.R
import pers.vaccae.mvidemo.bean.CDrugs
import pers.vaccae.mvidemo.ui.adapter.DrugsAdapter
import pers.vaccae.mvidemo.ui.intent.ActionIntent
import pers.vaccae.mvidemo.ui.intent.ActionState
import pers.vaccae.mvidemo.ui.viewmodel.MainViewModel


class FoldActivity : AppCompatActivity() {
    private val TAG = "X Fold"


    private val recyclerView: RecyclerView by lazy { findViewById(R.id.recycler_view) }
    private val btncreate: Button by lazy { findViewById(R.id.btncreate) }
    private val btnadd: Button by lazy { findViewById(R.id.btnadd) }
    private val btndel: Button by lazy { findViewById(R.id.btndel) }


    private lateinit var mainViewModel: MainViewModel
    private lateinit var drugsAdapter: DrugsAdapter


    //adapter的位置
    private var adapterpos = -1


    private lateinit var windowInfoTracker :WindowInfoTracker
    private lateinit var windowLayoutInfoFlow : Flow<WindowLayoutInfo>


    private val splitLayout: SplitLayout by lazy { findViewById(R.id.split_layout) }
    private val motionLayout :MotionLayout by lazy { findViewById(R.id.endLayout) }


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


        windowInfoTracker = WindowInfoTracker.getOrCreate(this@FoldActivity)
        windowLayoutInfoFlow = windowInfoTracker.windowLayoutInfo(this@FoldActivity)
        observeFold()


        mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)


        drugsAdapter = DrugsAdapter(R.layout.rcl_item, mainViewModel.listDrugs)
        drugsAdapter.setOnItemClickListener { baseQuickAdapter, view, i ->
            adapterpos = i
        }


        val gridLayoutManager = GridLayoutManager(this, 3)
        recyclerView.layoutManager = gridLayoutManager
        recyclerView.adapter = drugsAdapter


        //初始化ViewModel监听
        observeViewModel()


        btncreate.setOnClickListener {
            Log.i(TAG, "create")
            lifecycleScope.launch {
                mainViewModel.actionIntent.send(ActionIntent.LoadDrugs)
            }
        }


        btnadd.setOnClickListener {
            lifecycleScope.launch {
                mainViewModel.actionIntent.send(ActionIntent.InsDrugs)
            }
        }


        btndel.setOnClickListener {
            lifecycleScope.launch {
                Log.i("status", "$adapterpos")
                val item = try {
                    drugsAdapter.getItem(adapterpos)
                } catch (e: Exception) {
                    CDrugs()
                }
                mainViewModel.actionIntent.send(ActionIntent.DelDrugs(adapterpos, item))
            }
        }
    }


    private fun observeFold() {
        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                windowLayoutInfoFlow.collect { layoutInfo ->
                        Log.i(TAG, "size:${layoutInfo.displayFeatures.size}")
                        splitLayout.updateWindowLayout(layoutInfo)
                        // New posture information
                        val foldingFeature = layoutInfo.displayFeatures
                            .filterIsInstance<FoldingFeature>()
                            .firstOrNull()
                        foldingFeature?.let {
                            Log.i(TAG, "state:${it.state}")
                        }
                        when {
                            isTableTopPosture(foldingFeature) ->
                                Log.i(TAG, "TableTopPosture")
                            isBookPosture(foldingFeature) ->
                                Log.i(TAG, "BookPosture")
                            isSeparating(foldingFeature) ->
                                // Dual-screen device
                                foldingFeature?.let {
                                    if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {
                                        Log.i(TAG, "Separating HORIZONTAL")
                                    } else {
                                        Log.i(TAG, "Separating VERTICAL")
                                        motionLayout.transitionToEnd()
                                    }
                                }
                            else -> {
                                Log.i(TAG, "NormalMode")
                                motionLayout.transitionToStart()
                            }
                        }
                    }
            }
        }
    }


    fun isTableTopPosture(foldFeature: FoldingFeature?): Boolean {
        return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
                foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
    }


    fun isBookPosture(foldFeature: FoldingFeature?): Boolean {
        return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
                foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
    }


    fun isSeparating(foldFeature: FoldingFeature?): Boolean {
        return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating
    }


    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        Log.i(TAG, "configurationchanged")
    }


    private fun observeViewModel() {
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mainViewModel.state.collect {
                    when (it) {
                        is ActionState.Normal -> {
                            btncreate.isEnabled = true
                            btnadd.isEnabled = true
                            btndel.isEnabled = true
                        }
                        is ActionState.Loading -> {
                            btncreate.isEnabled = false
                            btncreate.isEnabled = false
                            btncreate.isEnabled = false
                        }
                        is ActionState.Drugs -> {
                            drugsAdapter.setList(it.drugs)
//                            drugsAdapter.setNewInstance(it.drugs)
                        }
                        is ActionState.Error-> {
                            Toast.makeText(this@FoldActivity, it.msg, Toast.LENGTH_SHORT).show()
                        }
                        is ActionState.Info ->{
                            Toast.makeText(this@FoldActivity, it.msg, Toast.LENGTH_SHORT).show()
                        }
                    }
                }
            }
        }
    }
}

这样折叠屏展开的Demo就完成了。

源码地址

https://github.com/Vaccae/AndroidMVIDemo.git

点击阅读原文可以看到“码云”的地址

73f55d4dd2e369c43bb5008a2b0c4557.png

537165e3dfb5de2bd5a3c5d34ed43e72.png

往期精彩回顾

 

0b4b5463ced1526bf6a33a7f58a93bba.png

Android折叠屏开发学习(二)---使用Jetpack WindowManager监听折叠屏开合状态

 

 

0753a524cead911d38428983b1324075.png

Android折叠屏开发学习(一)---通过传感器获取铰链角度

 

 

4bc96ae1feebaa105889595dd4e0dbca.png

Android MVI架构初探

 

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Vaccae

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

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

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

打赏作者

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

抵扣说明:

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

余额充值