Android自定义view之ViewPager指示器——1

Android自定义view之ViewPager指示器——1

在上两篇文章《Android自定义view之measure、layout、draw三大流程》以及《Android自定义view之事件传递机制》中,我们主要讲解了Android中视图的一些理论上的东西,为我们的自定义view打下基础。本次我们就将之前的理论应用到实际中动手自定义一个view。

ViewPager应该是我们平日里接触很多的控件,它允许我们在一个Activity中容纳多个界面,可以来回切换。ViewPager这个是在support v4包中,但是我们切换页面的时候总想知道我们现在在哪个页面、或者当前页面的标题,也就是我们需要一个指示器(Indicator)。可是官方好像一直都没有一个像样的这样的控件来做这件事。其实要实现的方法也很多。完全可以自己用xml布局文件写出来,不过这样就不好动态改变页面了,或者干脆指示器也用一个ViewPager来写,但ViewPager一次只显示一个界面也有点别扭。那我们就自己动手实现一个吧。

首先来看一下效果:
ViewPagerIndicator效果
可以看到,指示器中的横线以及文字是会随着ViewPager的变化而变化,并且指示器可以容纳超出自己边界的tags,并在适当的时候移动。点击对应的tag,ViewPager也会发生对应的变化。
接下来我们一步一步开始写。

1. 制定参数

每个View都有可以设置的参数,来设置View的外观和行为。对于一个指示器,我们应该有以下一些可自行定制的参数:
(1) 指示器的文字和横线颜色(文字颜色包括被选中的和未被选中的)
(2) 文字大小以及横线的高度
(3) tag之间的距离
(4) 布局模式,分为平衡模式和间距布局模式。平衡模式是为了所有tag的长度和不足以填满指示器时,将tags平均地进行分布
(5) 除此之外,还包括其他的一些常规view的熟悉,比如padding等
接下来我们就可以制定View的属性了,首先在values文件夹下新建attrs_text_indicator.xml。内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TextViewPagerIndicator">
        <attr name="textColor" format="color"/>
        <attr name="selectedTextColor" format="color"/>
        <attr name="backgroundColor" format="color"/>
        <attr name="textSize" format="dimension"/>
        <attr name="indicatorColor" format="color"/>
        <attr name="indicatorHeight" format="dimension"/>
        <attr name="intervalBetweenTags" format="dimension"/>
        <attr name="textPadding" format="dimension"/>
        <attr name="textPaddingTop" format="dimension"/>
        <attr name="textPaddingBottom" format="dimension"/>
        <attr name="textPaddingLeft" format="dimension"/>
        <attr name="textPaddingRight" format="dimension"/>
        <attr name="indicatorStyle" format="enum">
            <enum name="line" value="0"/>
            <enum name="background" value="1"/>
        </attr>
        <attr name="indicatorDrawable" format="reference"/>
        <attr name="balanceLayout" format="boolean"/>
    </declare-styleable>
</resources>

注意的是,某些属性由于时间的关系,只是想到了但并没有实现它,日后有空了再完善。
另外很中要的是我们指示器的布局原则:(1)指示器横线紧贴指示器底部,不考虑指示器的paddingBottom影响。(2)显示文字的TextView宽和高都是WRAP_CONTENT,并且在指示横线的顶部到指示器的顶部这片空间中居中布局。(3)指示器中的横线的宽度应该和当前对应的tag宽度一致。(4)平衡布局应用的情况应该是子View的宽度和比指示器的宽度小,此时子view的左右边界不会超出指示器左右边界。如果在相反的情况下仍然应用平衡布局,子view会紧挨着彼此,并且会超出边界进行布局,有可能会出现不期望的行为,因此不应该在这种情况下使用平衡布局。

2. 开始编写

在之前的文章中,我们已经谈到过,对于自定义一个View,我们大概能有4种选择。1:完全重写一个View。2:继承特定的View并重写其中的一些方法。3:完全重写一个ViewGroup。4:继承特定的ViewGroup并重写某些方法。以我们目前的需求,是对文字的操作,并且其中还涉及到对一条横线的操作。显然不需要用View,我们就继承ViewGroup。思路就是将文字放在TextView中,然后将TextView按照要求在这个View中布局。
新建一个类:

public class TextViewPagerIndicator extends ViewGroup
{
    MyLog log = new MyLog("TextViewPagerIndicator", true);
    /**
     * 布局显示相关的参数
     * */
    private int textColor = Color.parseColor("#000000");
    private int backgroundColor = Color.parseColor("#ffffff");
    private int textSize = 10;
    private int indicatorColor = Color.parseColor("#ffffff");
    private int indicatorHeight = 3;
    /*tag之间的间隔*/
    private int interval = 10;
    private int textPadding = 0;
    private int textPaddingTop = 0;
    private int textPaddingBottom = 0;
    private int textPaddingLeft = 0;
    private int textPaddingRight = 0;
    private int selectedTextColor;
    private boolean balanceLayout = false;


    private enum IndicatorStyle
    {
        line, background
    }

    private IndicatorStyle indicatorStyle = IndicatorStyle.line;
    private int indicatorDrawable = -1;




    private ArrayList<String> tags = new ArrayList<>();
    private HashMap<String, TextView> tagMap = new HashMap<>();
    private ImageView indicatorLine;
    /*目前选中的位置*/
    private int currentPosition = 0;

    private boolean expanded = false;
    /*最前面的TextView的左边与指示器左边的偏离值,为0时代表对准指示器左侧,小于0代表在指示器左侧的左边,大于0时相反*/
    private int textOffset = 0;
    /*tag的点击监听器*/
    private ArrayList<OnTagClickedListener> onTagClickedListeners = new ArrayList<>();


    /**
     * 事件相关参数
     * */

    private float newX, newY, lastX, lastY, dx, dy, downX, downY;
    /*判断是否是滑动的阈值*/
    private int touchSlop = 5;
}

注意有一些属性并没有应用,是为以后升级所预留的。比如IndicatorStyle等。
然后是构造函数,自定义View时是要求自己写构造函数的,而在构造函数中,我们就可以将用户在布局xml文件中设置的属性拿到手,然后对我们的属性进行赋值或初始化工作。

    public TextViewPagerIndicator(Context context) {
        this(context, null);
    }

    public TextViewPagerIndicator(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TextViewPagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public TextViewPagerIndicator(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextViewPagerIndicator);
        textColor = a.getColor(R.styleable.TextViewPagerIndicator_textColor, textColor);
        backgroundColor = a.getColor(R.styleable.TextViewPagerIndicator_backgroundColor, backgroundColor);
        indicatorColor = a.getColor(R.styleable.TextViewPagerIndicator_indicatorColor, indicatorColor);
        textSize = a.getDimensionPixelSize(R.styleable.TextViewPagerIndicator_textSize, textSize);
        indicatorHeight = a.getDimensionPixelSize(R.styleable.TextViewPagerIndicator_indicatorHeight, indicatorHeight);
        interval = a.getDimensionPixelSize(R.styleable.TextViewPagerIndicator_intervalBetweenTags, interval);
        selectedTextColor = a.getColor(R.styleable.TextViewPagerIndicator_selectedTextColor, indicatorColor);
        balanceLayout = a.getBoolean(R.styleable.TextViewPagerIndicator_balanceLayout, balanceLayout);
        a.recycle();
        indicatorLine = new ImageView(context);
        indicatorLine.setBackgroundColor(indicatorColor);
        this.addView(indicatorLine);

        touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        this.setClickable(true);
        textOffset = getPaddingLeft();

        if(isInEditMode())
        {
            for(int i = 0; i < 5; i++)
            {
                addTag("item" + i);
            }
        }
    }

拿到布局文件中设置的属性,我们需要TypedArray对象。然后按照我们之前的attrs中定义的属性来获取属性值,R.styleable.TextViewPagerIndicator就是我们之前的attrs中定义的。然后就如同取普通的键值对存储一样,传入属性名称和默认值。如果在布局文件中设置了,那我们就能拿到,否则就返回我们传入的默认值。
isInEditMode()这是自定义View的一个预览方法。返回true代表当前是在布局的编辑模式中。自定义的View中如果没有正确处理这个流程,就无法在编辑的时候实时预览。我们在这里添加了5个tag,在编辑的时候就能够实时查看效果了。
touchSlop是一个阈值。在处理触摸事件时有用。之前已经说过,单指触摸事件是由ACTION_DOWN开始,中间有若干ACTION_MOVE,结尾是ACTION_UP。基本所有的触摸事件,哪怕是很快地点击屏幕,都会有ACTION_MOVE事件发生。所以不能单纯地以ACTION的类型来判断事件性质。因此设置一个阈值,当ACTION_MOVE发生时,我们判断移动距离是否超过阈值,如果是,则代表用户现在确实要进行滑动操作。否则我们就认为这个ACTION_MOVE是意外发生的,用户没有要滑动。而这个阈值不可过大也不可过小,过小会导致作用不明显,过大则会导致滑动时有卡顿感(尤其是在慢速滑动时)。因此一般设置为4或5即可。

3. Measure过程

measure过程的主要作用就是测量自己和子view的大小,而自身的大小又和子view的大小息息相关。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        log.v("onMeasure");
        /*如果这个View不进行任何绘制操作,则设置为true,以便系统进行优化*/
        setWillNotDraw(true);

        /*获取父view传递给我们的宽和高的SpecMode和SpecSize*/
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        /*设置用来测量TextView宽度的MeasureSpec,由于tag是一定要完整的单行显示,因此我们将宽度的SpecMode设置为UNSPECIFIED,即
        * 要多大给多大*/
        int unspecifiedWidthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.UNSPECIFIED);
        /*设置用来测量TextView高度的MeasureSpec,不同于宽度,高度上TextView不能比我们指示器的高度更大,还要减去padding值和预留给横线的空间*/
        int atMostHeightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.AT_MOST);

        /*宽度结果,要考虑paddingLeft和paddingRight*/
        int resultWidthSize = getPaddingLeft() + getPaddingRight();
        /*高度结果,要考虑paddingTop和paddingBottom,还有给横线预留的位置*/
        int resultHeightSize = getPaddingTop() + getPaddingBottom() + indicatorHeight;

        for(String tag : tags)
        {
            /*依次对每一个TextView进行布局,并将宽度累加到宽度结果里*/
            TextView child = tagMap.get(tag);
            ViewGroup.LayoutParams childLayoutParams = child.getLayoutParams();
            int childWidthSpec = getChildMeasureSpec(unspecifiedWidthMeasureSpec, resultWidthSize, childLayoutParams.width);
            int childHeightSpec = getChildMeasureSpec(atMostHeightMeasureSpec, resultHeightSize, childLayoutParams.height);
            child.measure(childWidthSpec, childHeightSpec);
            resultWidthSize += child.getMeasuredWidth();

        }

        /*最终完全确定宽度和高度结果。注意到如果我们不是平衡布局,那么宽度结果还要加上tag之间的距离。对于高度结果,我们只要随便
        * 取一个已经测量过的TextView将其高度加进去即可*/
        if(tags != null && tags.size() != 0)
        {
            resultHeightSize += tagMap.get(tags.get(0)).getMeasuredHeight();
            if(!balanceLayout)
            {
                resultWidthSize += (tags.size() - 1) * interval;
            }
        }

        /*结合父view传给我们的SpecMode来确定我们这个指示器layout的最终大小。如果是AT_MOST,那大小不能超过父View传给我们的SpecSize,
        * 如果是EXACTLY,那就直接将SpecSize作为我们的结果,而不管我们之前测量的宽度和高度结果*/
        if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST)
        {
            setMeasuredDimension(resultWidthSize > widthSize ? widthSize : resultWidthSize,
                    resultHeightSize > heightSize ? heightSize : resultHeightSize);
        }else if(widthMeasureSpec == MeasureSpec.AT_MOST)
        {
            setMeasuredDimension(resultWidthSize > widthSize ? widthSize : resultWidthSize, heightSize);
        }else if(heightMeasureSpec == MeasureSpec.AT_MOST)
        {
            setMeasuredDimension(widthSize, resultHeightSize > heightSize ? heightSize : resultHeightSize);
        }else
        {
            setMeasuredDimension(widthSize, heightSize);
        }


    }

测量的主要思路就是先测量所有子view的大小,并且计算所有子view的宽度和、tag之间的距离以及指示器的padding值等。由于我们指示器暂时只支持横向布局,因此高度上我们只要考虑任意一个子view的高、指示器横线的高度以及指示器本身的padding值即可。
需要注意的是getChildMeasureSpec(int spec, int padding, int childDimension)这个方法,它是ViewGroup的静态方法,可以依据父View传递给我们的MeasureSpec、padding值以及childDimension(子view的宽或高)来生成子view的MeasureSpec。此处我们通过伪造了父View传递给我们(就是指示器本身,要注意到在measure方法里会有两种MeasureSpec,一种是父View传给我们的,另一种是我们生产的用于测量子View的)的Spec来测量子view,宽度伪造成UNSPECIFIED的,而高度伪造成AT_MOST。其实对于UNSPECIFIED的MeasureSpec,SpecSize传入多少都没关系,因为它就代表子View想要多大要多大,不必理会父容器(也就是我们的指示器本身)的尺寸。padding值其实并不是单纯的padding值,而是父容器在这个方向上已经被用掉的尺寸,比如高度上,我们除了考虑paddingTop和paddingBottom,还要考虑到横线的高度。而childDimension是我们取自TextView的LayoutParams中的值,此时它并不是具体的尺寸,而是WRAP_CONTENT,即-2,因为我们在new一个TextView时给它设置的就是WRAP_CONTENT。这个在后面会看到。关于getChildMeasureSpec(int spec, int padding, int childDimension)这个方法更多的说明以及它如何生产子View的MeasureSpec,可以看我的文章《Android自定义view之measure、layout、draw三大流程 》。
如果已经了解了Measure的详细流程,其实这里生产子View的MeasureSpec压根不用这么麻烦,也不用伪造指示器的MeasureSpec,只要直接制造子View的MeasureSpec即可。宽度上我们TextView一定要单行完整显示,那么可以直接写childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);高度上就比较麻烦了,因为我们对于TextView的高度是有限制的,即childHeight <= height - paddingTop - paddingBottom - indicatorLine.getHeight(),由于后面3个属于已知的值,因此我们主要要确定height值。这要根据父View传给指示器的heightMeasureSpec来确定。可分为两种情况:
1. SpecMode == UNSPECIFIED,此时我们也无法确定指示器的高度,所以直接构造TextView的spec为childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)即可。
2. SpecMode == AT_MOST 或 SpecMode == EXACTLY:这个时候我们可以知道指示器最大的高度值就是父View传递给指示器的MeasureSpec中的SpecSize,而TextView的高都是WRAP_CONTENT的,因此只要设置子View的SpecMode为AT_MOST即可。即childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(remainingSize, MeasureSpec.AT_MOST),其中remainingSize是指示器在高度上能留给TextView空间。在这里,可以设置为remainingSize = height - paddingTop - paddingBottom - indicatorLine.getHeight()

之后我们就可以使用刚制作的子view的宽度和高度的MeasureSpec来测量子view了,调用TextView的measure方法,并将MeasureSpec传入即可。

接下来TextView有了测量宽和高(getMeasuredHeight()和getMeasuredWidth()可以取到有效值了)。然后每测量一个TextView,我们就把它的measuredWidth累加到resultWidth中。直到测量完所有的TextView,现在resultWidth是所有TextView的宽度之和,另外再加上指示器的paddingLeft和paddingRight。然后就要看是否是平衡布局,如果不是平衡布局,那么我们还要加上tag间的间距。高度就简单得多,因为是横向布局的,所以高度就是paddingTop、paddingBottom、一个TextView的高和横线高度之和。

现在已经得到了resultWidth和resultHeight,接下来就是对指示器的大小做最终的决定,这时我们要结合指示器的SpecMode来决定。详细的流程已经在以前文章中measure那一节讲过了,代码里也很清楚。最后别忘了调用setMeasuredDimension将结果应用到View。

4. Layout过程

对于ViewGroup来说,最重要的就是layout过程,因为布局涉及到视图表现以及动画效果等。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        if(changed)
        {
            layoutChildren(textOffset, currentPosition);
            tagMap.get(tags.get(currentPosition)).setTextColor(selectedTextColor);
        }


    }

在layout函数里我们调用了layoutChildren(int textOffset, int index),这个函数是用于立马完成布局的,也就是根据textOffset和目前所选中的tag值的index立即完成布局,不存在中间状态。关于layout在前面的文章中也说过,layout函数在布局发生改变时是会调用的,而changed则只有在该view的位置或大小发生变化时才会为true,在第一次布局时是为true的。这里的结构表明了它只会在第一次布局时走if语句里的流程。
接下来是layoutChildren(int textOffset, int index)

    private void layoutChildren(int textOffset, int index)
    {
        if(tags.size() != 0)
        {
            /*计算TextView的顶部到指示器顶部的距离和底部到横线顶部的距离。由于我们的TextView是居中显示的(不),所以如下计算*/
            int padding = (getMeasuredHeight() - getPaddingBottom() - getPaddingTop() - indicatorHeight - tagMap.get(tags.get(0)).getMeasuredHeight()) / 2;
            if(padding < 0)
            {
                padding = 0;
            }
            if(balanceLayout)
            {
                int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
                /*如果是平衡布局,我们就要计算横向所剩余的空间,再将这些空间平分,作为tag之间的间距和tag与指示器前端和后端的距离*/
                int totalItemWidth = 0;
                for(int i = 0; i < tags.size(); i++)
                {
                    totalItemWidth += tagMap.get(tags.get(i)).getMeasuredWidth();
                }
                int space = (availableWidth - totalItemWidth) / (tags.size() + 1);
                space = space < 0 ? 0 : space;
                /*根据paddingLeft决定第一个TextView的偏离值*/
                textOffset = getPaddingLeft() + space;
                /*对子view进行布局*/
                for(int i = 0; i < tags.size(); i++)
                {
                    TextView child = tagMap.get(tags.get(i));
                    int left = textOffset;
                    int right = textOffset + child.getMeasuredWidth();
                    child.layout(left,
                            getPaddingTop() + padding, right,
                            getPaddingTop() + child.getMeasuredHeight() + padding);
                    /*更新offset*/
                    textOffset += space + child.getMeasuredWidth();
                }
            }else
            {
                /*如果不是平衡布局,直接按照传入的textOffset来布局,此时更新textOffset时使用tag之间的间距,即interval*/
                for(String s : tags)
                {
                    TextView child = tagMap.get(s);
                    child.layout(textOffset, getPaddingTop() + padding, textOffset + child.getMeasuredWidth(), getPaddingTop() + child.getMeasuredHeight() + padding);
                    textOffset += child.getMeasuredWidth() + interval;
//                log.v(s + ": left=" + child.getLeft() + ", right=" + child.getRight() + ", top=" + child.getTop() + ", bottom=" + child.getBottom());
                }
            }

            /*最后对横线进行布局*/
            ViewGroup.LayoutParams layoutParams = indicatorLine.getLayoutParams();
            layoutParams.width = tagMap.get(tags.get(index)).getMeasuredWidth();
            indicatorLine.setLayoutParams(layoutParams);
            int indicatorOffset = tagMap.get(tags.get(index)).getLeft();
            indicatorLine.layout(indicatorOffset, getMeasuredHeight() - getPaddingBottom() - indicatorHeight ,
                    indicatorOffset + layoutParams.width, getMeasuredHeight() - getPaddingBottom());
        }

    }

但是上面的只能用于布局确定状态,无法布局中间状态,就是说我原来对应的是tag1,接下来转换到tag2时,调用这个函数就会立马对应的tag2,不能对中间渐变的过程进行布局。接下来我们还需要一个能对中间状态布局的函数layoutChildren(int textOffset, int indicatorOffset, int indicatorLength)

    private void layoutChildren(int textOffset, int indicatorOffset, int indicatorLength)
    {

        log.v("layout children, textOffset = " + textOffset + ", indicatorOffset = " + indicatorOffset + ", indicatorLength = " + indicatorLength);
        if(tags.size() != 0)
        {
            /*计算TextView的顶部到指示器顶部的距离和底部到横线顶部的距离。由于我们的TextView是居中显示的(不),所以如下计算*/
            int padding = (getMeasuredHeight() - getPaddingBottom() - getPaddingTop() - indicatorHeight - tagMap.get(tags.get(0)).getMeasuredHeight()) / 2;
            if(padding < 0)
            {
                padding = 0;
            }
            if(balanceLayout)
            {
                /*如果是平衡布局,我们就要计算横向所剩余的空间,再将这些空间平分,作为tag之间的间距和tag与指示器前端和后端的距离*/
                int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
                int totalItemWidth = 0;
                for(int i = 0; i < tags.size(); i++)
                {
                    totalItemWidth += tagMap.get(tags.get(i)).getMeasuredWidth();
                }
                int space = (availableWidth - totalItemWidth) / (tags.size() + 1);
                space = space < 0 ? 0 : space;
                textOffset = getPaddingLeft() + space;
                /*布局子view*/
                for(int i = 0; i < tags.size(); i++)
                {
                    TextView child = tagMap.get(tags.get(i));
                    int left = textOffset;
                    int right = textOffset + child.getMeasuredWidth();
                    child.layout(left,
                            getPaddingTop() + padding, right,
                            getPaddingTop() + child.getMeasuredHeight() + padding);

                    textOffset += space + child.getMeasuredWidth();
                }
            }else
            {
                for(String s : tags)
                {
                    TextView child = tagMap.get(s);
                    child.layout(textOffset, getPaddingTop() + padding, textOffset + child.getMeasuredWidth(), getPaddingTop() + child.getMeasuredHeight() + padding);
                    textOffset += child.getMeasuredWidth() + interval;
//                log.v(s + ": left=" + child.getLeft() + ", right=" + child.getRight() + ", top=" + child.getTop() + ", bottom=" + child.getBottom());
                }
            }

        }


        /*布局指示器的横线*/
        ViewGroup.LayoutParams layoutParams = indicatorLine.getLayoutParams();
        layoutParams.width = indicatorLength;
        indicatorLine.setLayoutParams(layoutParams);
        indicatorLine.layout(indicatorOffset, getMeasuredHeight() - getPaddingBottom() - indicatorHeight,
                indicatorOffset + indicatorLength, getMeasuredHeight() - getPaddingBottom());

    }

这个函数和上面那个相比,其实大体差不多,只不过前者可以自动根据当前对应的tag来确定横线的布局,而后者则对横线的布局进行了更精准的控制,可以操作具体的位置和长度。

至此,布局工作就做完了。接下来就是事件响应了。由于本篇篇幅有些过长,接下来的事件相应和其他一些完善工作将会放在下篇去讲。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值