android中实现更溜的字母导航索引

现在讲一下今天要完成和前面android中字母导航和PinnedHeaderListView(listview头部固定)功能差不多,今天实现的功能是上篇的另一种实现方式,有兴趣的可以看一下

千言万语抵不过一张效果图:



上面右侧字母效果图有没有眼前一亮,如果没接触过这方面的童鞋,都不知道从哪里入手,下面一起来看一下,上面右侧字母的效果图是怎样实现的

讲一下滑动字母里面相关效果

1、有明显字体大小变化

2、字母有明显渐变效果

3、字母在指定的区域内左右滑动,字母有明显伸缩变化


相关技术

主要使用贝塞尔曲线二阶函数,运行效果图如下:


二阶贝塞尔曲线公式:


解释一下公式里面变量相关含义

B(t)表示t时间时点的坐标值(比如x值 或 y值)

P0为起点

P1为控制点

P2为终点


结合贝塞尔曲线二阶函数和下面那张图一起来看代码,这个就不难理解了



上图描述是右侧字母效果图运动的整个轨迹,里面都标的比较清楚了,如果上图看不清楚,可以下载下来再看,这个就比较清楚了。结合工程中LetterIndexer这个类理解起来比较容易了,下面是LetterIndexer类代码

public class LetterIndexer extends View {


    public interface OnTouchLetterChangedListener {
        void onTouchLetterChanged(String s, int index);

        void onTouchActionUp(String s);
    }

    private Context mContext;

    // 向右偏移多少画字符, default 30
    private float mWidthOffset = 30.0f;

    // 最小字体大小
    private int mMinFontSize = 24;

    // 最大字体大小
    private int mMaxFontSize = 48;

    // 提示字体大小
    private int mTipFontSize = 52;

    // 提示字符的额外偏移
    private float mAdditionalTipOffset = 20.0f;

    // 贝塞尔曲线控制的高度
    private float mMaxBezierHeight = 150.0f;

    // 贝塞尔曲线单侧宽度
    private float mMaxBezierWidth = 240.0f;

    // 贝塞尔曲线单侧模拟线量
    private int mMaxBezierLines = 32;

    // 列表字符颜色
    private int mFontColor = 0xffffffff;  //白色

    // 提示字符颜色
//	int  mTipFontColor = 0xff3399ff;
    int mTipFontColor = 0xffd33e48; //金


    private OnTouchLetterChangedListener mListener;

    private String[] constChar = {"#", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L"
            , "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};

    private int mConLength = 0;

    private int mChooseIndex = -1;
    private Paint mPaint = new Paint();
    private PointF mTouch = new PointF();

    private PointF[] mBezier1;
    private PointF[] mBezier2;

    private float mLastOffset[]; // 记录每一个字母的x方向偏移量, 数字<=0

    private Scroller mScroller;
    //正在动画
    private boolean mAnimating = false;
    //动画的偏移量
    private float mAnimationOffset;

    //动画隐藏
    private boolean mHideAnimation = false;

    //手指是否抬起
    private boolean isUp = false;

    private int mAlpha = 255;

    /**
     * 控制距离顶部的距离、底部距离
     */
    private int paddingTop = 0;
    private int paddingBottom = 0;

    Handler mHideWaitingHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
//				mScroller.startScroll(0, 0, 255, 0, 1000);
                mHideAnimation = true;
                mAnimating = false;                                                        //动画mAnimating=false onDraw触发
                LetterIndexer.this.invalidate();
                return;
            }
            super.handleMessage(msg);
        }
    };

    public LetterIndexer(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initData(context, attrs);
    }

    public LetterIndexer(Context context, AttributeSet attrs) {
        super(context, attrs);
        initData(context, attrs);
    }

    public LetterIndexer(Context context) {
        super(context);
        initData(null, null);
    }

    private void initData(Context context, AttributeSet attrs) {
        if (context != null && attrs != null) {

            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LetterIndexer, 0, 0);

            mWidthOffset = a.getDimension(R.styleable.LetterIndexer_widthOffset, mWidthOffset);
            mMinFontSize = a.getInteger(R.styleable.LetterIndexer_minFontSize, mMinFontSize);
            mMaxFontSize = a.getInteger(R.styleable.LetterIndexer_maxFontSize, mMaxFontSize);
            mTipFontSize = a.getInteger(R.styleable.LetterIndexer_tipFontSize, mTipFontSize);
            mMaxBezierHeight = a.getDimension(R.styleable.LetterIndexer_maxBezierHeight, mMaxBezierHeight);
            mMaxBezierWidth = a.getDimension(R.styleable.LetterIndexer_maxBezierWidth, mMaxBezierWidth);
            mMaxBezierLines = a.getInteger(R.styleable.LetterIndexer_maxBezierLines, mMaxBezierLines);
            mAdditionalTipOffset = a.getDimension(R.styleable.LetterIndexer_additionalTipOffset, mAdditionalTipOffset);
            mFontColor = a.getColor(R.styleable.LetterIndexer_fontColor, mFontColor);
            mTipFontColor = a.getColor(R.styleable.LetterIndexer_tipFontColor, mTipFontColor);
            a.recycle();
        }


        this.mContext = context;
        mScroller = new Scroller(getContext());
        mTouch.x = 0;
        mTouch.y = -10 * mMaxBezierWidth;

        mBezier1 = new PointF[mMaxBezierLines];
        mBezier2 = new PointF[mMaxBezierLines];

        commonData(0, 0);
    }

    /**
     * 需 注意的是,传值单位是sp
     * @param top   距离顶部的距离
     * @param bottom 距离底部的距离
     */
    private void commonData(int top, int bottom) {
        paddingTop = DisplayUtils.convertDIP2PX(mContext,top);
        paddingBottom = DisplayUtils.convertDIP2PX(mContext,bottom);
        mConLength = constChar.length;
        mLastOffset = new float[mConLength];
        calculateBezierPoints();
    }

    public void setConstChar(String[] constChar,int top, int bottom) {
        this.constChar = constChar;
        commonData(top,bottom);
    }

    @Override
    protected void onDraw(Canvas canvas) {

        // 控件宽高
        int height = getHeight() - paddingTop - paddingBottom;
        int width = getWidth();

        // 单个字母高度
        float singleHeight = height / (float) constChar.length;

        int workHeight = paddingTop;

        if (mAlpha == 0)
            return;

        //恢复画笔的默认设置。
        mPaint.reset();

        /**
         * 遍历所以字母内容
         */
        for (int i = 0; i < mConLength; i++) {

            mPaint.setColor(mFontColor);
            mPaint.setAntiAlias(true);

            float xPos = width - mWidthOffset;           // 字母在 x 轴的位置      基本保持不变
            float yPos = workHeight + singleHeight / 2;  //字母在 y 轴的位置     该值一直在变化

            // 根据当前字母y的位置计算得到字体大小
            int fontSize = adjustFontSize(i, yPos);
            mPaint.setTextSize(fontSize);
            mAlpha = 255 - fontSize*4;
            mPaint.setAlpha(mAlpha);
            if (i == mChooseIndex){
                mPaint.setColor(Color.parseColor("#F50527"));
            }

            // 添加一个字母的高度
            workHeight += singleHeight;
            // 绘制字母
            drawTextInCenter(canvas, constChar[i], xPos + ajustXPosAnimation(i, yPos), yPos);

            // 如果手指抬起
            if (isUp) {
                mListener.onTouchActionUp(constChar[mChooseIndex]);
                isUp = false;
            }
            mPaint.reset();
        }
    }

    /**
     * @param canvas  画板
     * @param string  被绘制的字母
     * @param xCenter 字母的中心x方向位置
     * @param yCenter 字母的中心y方向位置
     */
    private void drawTextInCenter(Canvas canvas, String string, float xCenter, float yCenter) {

        FontMetrics fm = mPaint.getFontMetrics();
        float fontHeight = mPaint.getFontSpacing();

        float drawY = yCenter + fontHeight / 2 - fm.descent;

        if (drawY < -fm.ascent - fm.descent)
            drawY = -fm.ascent - fm.descent;

        if (drawY > getHeight())
            drawY = getHeight();

        mPaint.setTextAlign(Align.CENTER);

        canvas.drawText(string, xCenter, drawY, mPaint);
    }

    private int adjustFontSize(int i, float yPos) {

        // 根据水平方向偏移量计算出一个放大的字号
        float adjustX = Math.abs(ajustXPosAnimation(i, yPos));
        int adjustSize = (int) ((mMaxFontSize - mMinFontSize) * adjustX / mMaxBezierHeight) + mMinFontSize;

        return adjustSize;
    }

    /**
     * x 方向的向左偏移量
     *
     * @param i    当前字母的索引
     * @param yPos y方向的初始位置  会变化
     * @return
     */
    private float ajustXPosAnimation(int i, float yPos) {

        float offset;
        if (this.mAnimating || this.mHideAnimation) {
            // 正在动画中或在做隐藏动画
            offset = mLastOffset[i];
            if (offset != 0.0f) {
                offset += this.mAnimationOffset;
                if (offset > 0)
                    offset = 0;
            }
        } else {

            // 根据当前字母y方向位置, 计算水平方向偏移量
            offset = adjustXPos(yPos);

            // 当前触摸的x方向位置
            float xPos = mTouch.x;

            float width = getWidth() - mWidthOffset;
            width = width - 60;

            // 字母绘制时向左偏移量 进行修正, offset需要是<=0的值
            if (offset != 0.0f && xPos > width){
                offset += (xPos - width);
            }
            if (offset > 0){
                offset = 0;
            }

            mLastOffset[i] = offset;
        }
        return offset;
    }

    private float adjustXPos(float yPos) {

        float dis = yPos - mTouch.y; // 字母y方向位置和触摸时y值坐标的差值, 距离越小, 得到的水平方向偏差越大
        if (dis > -mMaxBezierWidth && dis < mMaxBezierWidth) {
            // 在2个贝赛尔曲线宽度范围以内 (一个贝赛尔曲线宽度是指一个山峰的一边)

            // 第一段 曲线
            if (dis > mMaxBezierWidth / 4) {
                for (int i = mMaxBezierLines - 1; i > 0; i--) {
                    // 从下到上, 逐个计算

                    if (dis == -mBezier1[i].y) // 落在点上
                        return mBezier1[i].x;

                    // 如果距离dis落在两个贝塞尔曲线模拟点之间, 通过三角函数计算得到当前dis对应的x方向偏移量
                    if (dis > -mBezier1[i].y && dis < -mBezier1[i - 1].y) {
                        return (dis + mBezier1[i].y) * (mBezier1[i - 1].x - mBezier1[i].x) / (-mBezier1[i - 1].y + mBezier1[i].y) + mBezier1[i].x;
                    }
                }
                return mBezier1[0].x;
            }

            // 第三段 曲线, 和第一段曲线对称
            if (dis < -mMaxBezierWidth / 4) {
                for (int i = 0; i < mMaxBezierLines - 1; i++) {
                    // 从上到下

                    if (dis == mBezier1[i].y) // 落在点上
                        return mBezier1[i].x;

                    // 如果距离dis落在两个贝塞尔曲线模拟点之间, 通过三角函数计算得到当前dis对应的x方向偏移量
                    if (dis > mBezier1[i].y && dis < mBezier1[i + 1].y) {
                        return (dis - mBezier1[i].y) * (mBezier1[i + 1].x - mBezier1[i].x) / (mBezier1[i + 1].y - mBezier1[i].y) + mBezier1[i].x;
                    }
                }
                return mBezier1[mMaxBezierLines - 1].x;
            }

            // 第二段 峰顶曲线
            for (int i = 0; i < mMaxBezierLines - 1; i++) {

                if (dis == mBezier2[i].y)
                    return mBezier2[i].x;

                // 如果距离dis落在两个贝塞尔曲线模拟点之间, 通过三角函数计算得到当前dis对应的x方向偏移量
                if (dis > mBezier2[i].y && dis < mBezier2[i + 1].y) {
                    return (dis - mBezier2[i].y) * (mBezier2[i + 1].x - mBezier2[i].x) / (mBezier2[i + 1].y - mBezier2[i].y) + mBezier2[i].x;
                }
            }
            return mBezier2[mMaxBezierLines - 1].x;

        }

        return 0.0f;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        final int action = event.getAction();
        final float y = event.getY();
        final int oldmChooseIndex = mChooseIndex;
        final OnTouchLetterChangedListener listener = mListener;
        /**
         * 计算除去paddingTop后,用户点击不同位置对应的字母索引
         */
        final int c = (int) ((y-paddingTop) / (getHeight()-paddingTop-paddingBottom) * constChar.length);


        switch (action) {
            case MotionEvent.ACTION_DOWN:

                if (this.getWidth() > mWidthOffset) {
                    if (event.getX() < this.getWidth() - mWidthOffset)
                        return false;
                }

                if (y < paddingTop || c<0 || y > getHeight()-paddingBottom){
                    return false;
                }

                mHideWaitingHandler.removeMessages(1);

                mScroller.abortAnimation();
                mAnimating = false;
                mHideAnimation = false;
                mAlpha = 255;

                mTouch.x = event.getX();
                mTouch.y = event.getY();

                if (oldmChooseIndex != c && listener != null) {
                    if (c > 0 && c < constChar.length) {
                        listener.onTouchLetterChanged(constChar[c],c);
                        mChooseIndex = c;
                    }
                }
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                mTouch.x = event.getX();
                mTouch.y = event.getY();
                invalidate();
                if (oldmChooseIndex != c && listener != null) {

                    if (c >= 0 && c < constChar.length) {
                        listener.onTouchLetterChanged(constChar[c],c);
                        mChooseIndex = c;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                mTouch.x = event.getX();
                mTouch.y = event.getY();

                isUp = true;
                mScroller.startScroll(0, 0, (int) mMaxBezierHeight, 0, 2000);
                mAnimating = true;
                postInvalidate();
                break;

        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()) {
            if (mAnimating) {
                float x = mScroller.getCurrX();
                mAnimationOffset = x;
            } else if (mHideAnimation) {
                mAlpha = 255 - (int) mScroller.getCurrX();
            }
            invalidate();
        } else if (mScroller.isFinished()) {
            if (mAnimating) {
                mHideWaitingHandler.sendEmptyMessage(1);
            } else if (mHideAnimation) {
                mHideAnimation = false;
                this.mChooseIndex = -1;
                mTouch.x = -10000;
                mTouch.y = -10000;
            }

        }
    }

    public void setOnTouchLetterChangedListener(OnTouchLetterChangedListener listener) {
        this.mListener = listener;
    }

    /**
     * 计算出所有贝塞尔曲线上的点
     * 个数为 mMaxBezierLines * 2 = 64
     */
    private void calculateBezierPoints() {

        PointF mStart = new PointF();   // 开始点
        PointF mEnd = new PointF();        // 结束点
        PointF mControl = new PointF(); // 控制点


        // 计算第一段红色部分 贝赛尔曲线的点
        // 开始点
        mStart.x = 0.0f;
        mStart.y = -mMaxBezierWidth;

        // 控制点
        mControl.x = 0.0f;
        mControl.y = -mMaxBezierWidth / 2;

        // 结束点
        mEnd.x = -mMaxBezierHeight / 2;
        mEnd.y = -mMaxBezierWidth / 4;

        mBezier1[0] = new PointF();
        mBezier1[mMaxBezierLines - 1] = new PointF();

        mBezier1[0].set(mStart);
        mBezier1[mMaxBezierLines - 1].set(mEnd);

        for (int i = 1; i < mMaxBezierLines - 1; i++) {

            mBezier1[i] = new PointF();

            mBezier1[i].x = calculateBezier(mStart.x, mEnd.x, mControl.x, i / (float) mMaxBezierLines);
            mBezier1[i].y = calculateBezier(mStart.y, mEnd.y, mControl.y, i / (float) mMaxBezierLines);

        }

        // 计算第二段蓝色部分 贝赛尔曲线的点
        mStart.y = -mMaxBezierWidth / 4;
        mStart.x = -mMaxBezierHeight / 2;

        mControl.y = 0.0f;
        mControl.x = -mMaxBezierHeight;

        mEnd.y = mMaxBezierWidth / 4;
        mEnd.x = -mMaxBezierHeight / 2;

        mBezier2[0] = new PointF();
        mBezier2[mMaxBezierLines - 1] = new PointF();

        mBezier2[0].set(mStart);
        mBezier2[mMaxBezierLines - 1].set(mEnd);

        for (int i = 1; i < mMaxBezierLines - 1; i++) {

            mBezier2[i] = new PointF();
            mBezier2[i].x = calculateBezier(mStart.x, mEnd.x, mControl.x, i / (float) mMaxBezierLines);
            mBezier2[i].y = calculateBezier(mStart.y, mEnd.y, mControl.y, i / (float) mMaxBezierLines);
        }
    }

    /**
     * 贝塞尔曲线核心算法
     *
     * @param start
     * @param end
     * @param control
     * @param val
     * @return 公式及动图, 维基百科: https://en.wikipedia.org/wiki/B%C3%A9zier_curve
     * 中文可参考此网站: http://blog.csdn.net/likendsl/article/details/7852658
     */
    private float calculateBezier(float start, float end, float control, float val) {

        float t = val;
        float s = 1 - t;

        float ret = start * s * s + 2 * control * s * t + end * t * t;

        return ret;
    }
}

上面代码是实现字母右侧滑动效果的核心类,里面都有注释了,记得这个类要结合上图来看,会发现哦原来是这样子,很溜吧。所以学好数学是很必要的


attrs.xml文件内容

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="LetterIndexer">
        <attr name="widthOffset" format="dimension" />
        <attr name="minFontSize" format="integer" />
        <attr name="maxFontSize" format="integer" />
        <attr name="tipFontSize" format="integer" />
        <attr name="maxBezierHeight" format="dimension" />
        <attr name="maxBezierWidth" format="dimension" />
        <attr name="maxBezierLines" format="integer" />
        <attr name="additionalTipOffset" format="dimension" />
        <attr name="fontColor" format="color" />
        <attr name="tipFontColor" format="color" />
    </declare-styleable>

</resources>


主MainActivity

public class MainActivity extends Activity {
    private PinnedHeaderListView pinnedHeaderListView;
    private ArrayList<Person> persons;                                //英雄好汉列表数据集 (除过分组标签,列表所有数据,无序)
    private LinkedHashMap<String, List<Person>> personMpas;           //英雄好汉列表 分组标签对应的数据集合(有序)
    private PinnedHeaderListViewAdapter<Person> adapter;              //英雄好汉列表适配器
    private LetterIndexer letterIndexer;
    private TextView tv_index_center;
    private Handler mHandler = new Handler();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        lister();
        //初始化数据
        initData();
    }

    private void initView() {
        pinnedHeaderListView = (PinnedHeaderListView)findViewById(R.id.pinnedheader_listview);
        pinnedHeaderListView.setPinnedHeaderView(this.getLayoutInflater().inflate(
                R.layout.pinnedheaderlistview_header_layout, pinnedHeaderListView, false));
        letterIndexer = (LetterIndexer) findViewById(R.id.letter_index);
        tv_index_center = (TextView) findViewById(R.id.tv_index_center);
    }

    private void lister() {
        letterIndexer.setOnTouchLetterChangedListener(new LetterIndexer.OnTouchLetterChangedListener() {

            @Override
            public void onTouchLetterChanged(String letter,int index) {

                // 从集合中查找第一个拼音首字母为letter的索引, 进行跳转
                for (int i = 0; i < persons.size(); i++) {
                    Person person = persons.get(i);
                    String s = person.getLetter();
                    if(TextUtils.equals(s, letter)){
                        // 匹配成功, 中断循环, 将列表移动到指定的位置
                        pinnedHeaderListView.setSelection(i);
                        break;
                    }
                }
            }

            @Override
            public void onTouchActionUp(String letter) {
                showLetter(letter);
            }
        });
    }

    /**
     * 显示字母提示
     *
     * @param letter
     */
    protected void showLetter(String letter) {
        tv_index_center.setVisibility(View.VISIBLE);
        tv_index_center.setText(letter);

        mHandler.removeCallbacksAndMessages(null);
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                // 隐藏
                tv_index_center.setVisibility(View.GONE);
            }
        }, 2000);

    }

    protected void initData() {
        InternalStorageUtils.asynReadInternalFile(this, "names.config", new AsyncResonseHandler() {
            @Override
            protected void onSuccess(String content) {
                super.onSuccess(content);
                try {
                    Gson gson = new Gson();
                    HeroPerson hero = gson.fromJson(content,
                            new TypeToken<HeroPerson>() {
                            }.getType());

                    List<PersonList> personList = hero.getSections();
                    persons = new ArrayList<Person>();
                    personMpas = new LinkedHashMap<String, List<Person>>();

                    //特殊字符
                    List<Person> specialChar = new ArrayList<Person>();
                    Person charPerson = null;
                    for (int i=0; i<5; i++){
                        charPerson = new Person();
                        charPerson.setName("#特殊字符"+i);
                        charPerson.setLetter("#");
                        specialChar.add(charPerson);
                    }
                    personMpas.put("特殊字符", specialChar);
                    persons.addAll(specialChar);

                    //得到右侧字母索引的内容
                    int letterLength = personList.size()+ personMpas.size();
                    String[] constChar = new String[letterLength];
                    constChar[0] = "#";


                    List<Person> personItems;
                    for (int i = 0; i < personList.size(); i++) {
                        personItems = personList.get(i).getPersons();
                        persons.addAll(personItems);
                        personMpas.put(personList.get(i).getIndex(), personItems);
                        constChar[i+1] = personList.get(i).getIndex();
                    }

                    adapter = new PinnedHeaderListViewAdapter<Person>(MainActivity.this, personMpas, pinnedHeaderListView,
                            letterIndexer,constChar, 20, 20);
                    pinnedHeaderListView.setOnScrollListener(adapter);
                    pinnedHeaderListView.setAdapter(adapter);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:huahua="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@color/bg_gray_main" >
    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center"
        android:background="#96B1B4"
        android:text="标题内容"/>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <include layout="@layout/person_pinned_header_listview"
            android:id="@+id/pinnedheader_listview"/>

        <cn.com.huahua.pinnedheaderlistview.ui.LetterIndexer
            android:id="@+id/letter_index"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            huahua:widthOffset="15dip"
            huahua:minFontSize="32"
            huahua:maxFontSize="77"
            huahua:tipFontSize="72"
            huahua:maxBezierHeight="150dip"
            huahua:maxBezierWidth="180dip"
            huahua:additionalTipOffset="40dip"
            huahua:fontColor="#2278BF" />

        <TextView
            android:id="@+id/tv_index_center"
            android:visibility="gone"
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:layout_centerInParent="true"
            android:textColor="#FFFFFF"
            android:gravity="center"
            android:textSize="36sp"
            android:background="@drawable/alpha_center_corner"
            android:text="A" />
    </RelativeLayout>
</LinearLayout>


下面提供一下源码

源码下载


参考资料

http://blog.csdn.net/likendsl/article/details/7852658

http://www.jcodecraeer.com/a/opensource/2015/1104/3656.html





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值