Android-WaveSideBarActivity

本文详细介绍了如何在Android应用中实现一个带有动态贝塞尔曲线效果的侧滑字母索引栏,包括侧栏视图的创建、触摸事件处理、数据排序以及ListView适配器的使用。主要涉及的技术点有自定义View、触摸事件处理、贝塞尔曲线算法以及数据结构的排序。
摘要由CSDN通过智能技术生成

效果图展示
在这里插入图片描述
MainActivity.xml

public class MainActivity extends AppCompatActivity implements SideBarView.OnTouchLetterChangedListener {
    
    private FriendAdapter adapter;
    private ListView listView;
    private SideBarView sideView;
    private List<FriendInfo> friends;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();//数据准备
        initView();
        setListener();//侧边索引暴露出的监听
    }
    private void initView() {
        listView = ((ListView) findViewById(R.id.lv_friends_listView));
        sideView = ((SideBarView) findViewById(R.id.sbv_friends_bar));
    }

    private void setListener() {
        sideView.setOnTouchLetterChangedListener(this);
    }
    private void initData() {
        friends = new ArrayList<>();
        adapter = new FriendAdapter(this, friends);
        listView.setAdapter(adapter);
        //联系人数据数组放在了AppProperties
        String[] names = AppProperties.names;
        for (String name:names) {
            FriendInfo friend = new FriendInfo();
            friend.setName(name);
            friend.setPinyin(PinyinUtils.getPinyin(name));
            String convert = friend.getPinyin().substring(0, 1).toUpperCase();
            //利用拼音获取首字母大写,特殊字符填入#
            if (convert.matches("[A-Z]")) {
                friend.setFirstLetter(convert);
            }else{
                friend.setFirstLetter("#");
            }
            friends.add(friend);
        }
        
        //通过首字母排序list,如果是以#开头就默认在最后
        Collections.sort(friends, new Comparator<FriendInfo>() {
            @Override
            public int compare(FriendInfo f1, FriendInfo f2) {
                if (f1.getFirstLetter().contains("#")) {
                    return 1;
                } else if (f2.getFirstLetter().contains("#")) {
                    return -1;
                }
                else{
                    return f1.getFirstLetter().compareTo(f2.getFirstLetter());

                }
            }
        });
        adapter.addAll(friends);
        adapter.notifyDataSetChanged();
    }


    @Override
    public void onTouchLetterChanged(String letter) {
        if (!friends.isEmpty()) {
            for (int i = 0; i < friends.size(); i++) {
                FriendInfo friend = friends.get(i);
                String s = String.valueOf(friend.getPinyin().charAt(0));
                if (s.equals(letter)) { // 匹配成功, 中断循环, 跳转到i位置
                    listView.setSelection(i);
                    break;
                }
            }
        }
    }
}

关于Collections.sort
SideBarView
使用了贝塞尔曲线动态设置,看不懂

public class SideBarView extends View {

    private OnTouchLetterChangedListener mListener;

    public interface OnTouchLetterChangedListener {
        void onTouchLetterChanged(String letter);
    }

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

    // 向右偏移多少画字符, default 30
    float offset = 30.0f;
    // 最小字体大小
    int minSize = 24;
    // 最大字体大小
    int MaxSize = 48;
    // 提示字体大小
    int flagSize = 52;
    // 提示字符的额外偏移
    float flagOffSet = 20.0f;
    // 贝塞尔曲线控制的高度
    float mMaxBezierHeight = 150.0f;
    // 贝塞尔曲线单侧宽度
    float mMaxBezierWidth = 240.0f;
    // 贝塞尔曲线单侧模拟线量
    int mMaxBezierLines = 32;
    // 列表字符颜色
    int fontColor = 0xffffffff;
    // 提示字符颜色
    int flagFontColor = 0xffd33e48;

    private final String[] ConstChar = AppProperties.SIDEBAR_CHAR_LIST;

    int choose = -1;
    Paint paint = new Paint();
    PointF t_paint = new PointF();

    PointF[] pf1;
    PointF[] pf2;

    float offSet[] = new float[ConstChar.length]; // 记录每一个字母的x方向偏移量, 数字<=0
    PointF pointF = new PointF();

    Scroller scroller;
    boolean animation = false;
    float animationOffset;

    boolean hideAnimation = false;
    int mAlpha = 255;

    Handler mHideWaitingHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                hideAnimation = true;
                animation = false;
                SideBarView.this.invalidate();
                return;
            }
            super.handleMessage(msg);
        }
    };

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

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

    public SideBarView(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.FancyIndexer, 0, 0);
            offset = a.getDimension(R.styleable.FancyIndexer_widthOffset, offset);
            minSize = a.getInteger(R.styleable.FancyIndexer_minFontSize, minSize);
            MaxSize = a.getInteger(R.styleable.FancyIndexer_maxFontSize, MaxSize);
            flagSize = a.getInteger(R.styleable.FancyIndexer_tipFontSize, flagSize);
            mMaxBezierHeight = a.getDimension(R.styleable.FancyIndexer_maxBezierHeight, mMaxBezierHeight);
            mMaxBezierWidth = a.getDimension(R.styleable.FancyIndexer_maxBezierWidth, mMaxBezierWidth);
            mMaxBezierLines = a.getInteger(R.styleable.FancyIndexer_maxBezierLines, mMaxBezierLines);
            flagOffSet = a.getDimension(R.styleable.FancyIndexer_additionalTipOffset, flagOffSet);

            // 颜色
            fontColor = a.getColor(R.styleable.FancyIndexer_fontColor, fontColor);

            // 提示颜色
            flagFontColor = a.getColor(R.styleable.FancyIndexer_tipFontColor, flagFontColor);
            a.recycle();
        }
        scroller = new Scroller(getContext());
        t_paint.x = 0;
        t_paint.y = -10 * mMaxBezierWidth;

        pf1 = new PointF[mMaxBezierLines];
        pf2 = new PointF[mMaxBezierLines];

        calculateBezierPoints();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        // 控件宽高
        int height = getHeight();
        int width = getWidth();
        // 单个字母高度
        float singleHeight = height / (float) ConstChar.length;
        int workHeight = 0;
        if (mAlpha == 0)
            return;
        paint.reset();
        int saveCount = 0;
        if (hideAnimation) {
            saveCount = canvas.save();
            canvas.saveLayerAlpha(0, 0, width, height, mAlpha, Canvas.ALL_SAVE_FLAG);
        }

        for (int i = 0; i < ConstChar.length; i++) {
            paint.setColor(fontColor);
            paint.setAntiAlias(true);

            float xPos = width - offset;
            float yPos = workHeight + singleHeight / 2;

            int fontSize = adjustFontSize(i, yPos);
            paint.setTextSize(fontSize);

            // 添加一个字母的高度
            workHeight += singleHeight;

            // 绘制字母
            drawTextInCenter(canvas, ConstChar[i], xPos + ajustXPosAnimation(i, yPos), yPos);

            // 绘制的字母和当前触摸到的一致, 绘制被选中字母
            if (i == choose) {
                paint.setColor(flagFontColor);
                paint.setFakeBoldText(true);
                paint.setTextSize(flagSize);
                yPos = t_paint.y;

                float pos = 0;

                if (animation || hideAnimation) {
                    pos = pointF.x;
                    yPos = pointF.y;
                } else {
                    pos = xPos + ajustXPosAnimation(i, yPos) - flagOffSet;
                    pointF.x = pos;
                    pointF.y = yPos;
                }
                drawTextInCenter(canvas, ConstChar[i], pos, yPos);
            }
            paint.reset();
        }

        if (hideAnimation) {
            canvas.restoreToCount(saveCount);
        }
    }

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

        Paint.FontMetrics fm = paint.getFontMetrics();

        float fontHeight = paint.getFontSpacing();
        float drawY = yCenter + fontHeight / 2 - fm.descent;

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

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

        paint.setTextAlign(Paint.Align.CENTER);
        canvas.drawText(string, xCenter, drawY, paint);
    }

    private int adjustFontSize(int i, float yPos) {

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

        int adjustSize = (int) ((MaxSize - minSize) * adjustX / mMaxBezierHeight) + minSize;

        return adjustSize;
    }

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

        float offset;
        if (this.animation || this.hideAnimation) {
            // 正在动画中或在做隐藏动画
            offset = offSet[i];
            if (offset != 0.0f) {
                offset += this.animationOffset;
                if (offset > 0)
                    offset = 0;
            }
        } else {
            // 根据当前字母y方向位置, 计算水平方向偏移量
            offset = adjustXPos(yPos);
            // 当前触摸的x方向位置
            float xPos = t_paint.x;

            float width = getWidth() - this.offset;
            width = width - 60;

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

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

    private float adjustXPos(float yPos) {

        float dis = yPos - t_paint.y; // 字母y方向位置和触摸时y值坐标的差值, 距离越小, 得到的水平方向偏差越大
        if (dis > -mMaxBezierWidth && dis < mMaxBezierWidth) {
            // 在2个贝赛尔曲线宽度范围以内 (一个贝赛尔曲线宽度是指一个山峰的一边)
            // 第一段 曲线
            if (dis > mMaxBezierWidth / 4) {
                for (int i = mMaxBezierLines - 1; i > 0; i--) {
                    // 从下到上, 逐个计算
                    if (dis == -pf1[i].y) // 落在点上
                        return pf1[i].x;

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

            // 第三段 曲线, 和第一段曲线对称
            if (dis < -mMaxBezierWidth / 4) {
                for (int i = 0; i < mMaxBezierLines - 1; i++) {
                    // 从上到下
                    if (dis == pf1[i].y) // 落在点上
                        return pf1[i].x;

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

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

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

                // 如果距离dis落在两个贝塞尔曲线模拟点之间, 通过三角函数计算得到当前dis对应的x方向偏移量
                if (dis > pf2[i].y && dis < pf2[i + 1].y) {
                    return (dis - pf2[i].y) * (pf2[i + 1].x - pf2[i].x) / (pf2[i + 1].y - pf2[i].y) + pf2[i].x;
                }
            }
            return pf2[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 = choose;
        final OnTouchLetterChangedListener listener = mListener;
        final int c = (int) (y / getHeight() * ConstChar.length);

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                if (this.getWidth() > offset) {
                    if (event.getX() < this.getWidth() - offset)
                        return false;
                }

                mHideWaitingHandler.removeMessages(1);

                scroller.abortAnimation();
                animation = false;
                hideAnimation = false;
                mAlpha = 255;

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

                if (oldmChooseIndex != c && listener != null) {

                    if (c > 0 && c < ConstChar.length) {
                        listener.onTouchLetterChanged(ConstChar[c]);
                        choose = c;
                    }
                }
                invalidate();
                break;

            case MotionEvent.ACTION_MOVE:
                t_paint.x = event.getX();
                t_paint.y = event.getY();
                invalidate();
                if (oldmChooseIndex != c && listener != null) {

                    if (c >= 0 && c < ConstChar.length) {
                        listener.onTouchLetterChanged(ConstChar[c]);
                        choose = c;
                    }
                }
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                t_paint.x = event.getX();
                t_paint.y = event.getY();

                scroller.startScroll(0, 0, (int) mMaxBezierHeight, 0, 500);
                animation = true;
                postInvalidate();
                break;
        }
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            if (animation) {
                float x = scroller.getCurrX();
                animationOffset = x;
            } else if (hideAnimation) {
                mAlpha = 255 - scroller.getCurrX();
            }
            invalidate();
        } else if (scroller.isFinished()) {
            if (animation) {
                mHideWaitingHandler.sendEmptyMessage(1);
            } else if (hideAnimation) {
                hideAnimation = false;
                this.choose = -1;
                t_paint.x = -10000;
                t_paint.y = -10000;
            }
        }
    }

    /**
     * 计算出所有贝塞尔曲线上的点
     * 个数为 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;

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

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

        for (int i = 1; i < mMaxBezierLines - 1; i++) {
            pf1[i] = new PointF();
            pf1[i].x = calculateBezier(mStart.x, mEnd.x, mControl.x, i / (float) mMaxBezierLines);
            pf1[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;

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

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

        for (int i = 1; i < mMaxBezierLines - 1; i++) {
            pf2[i] = new PointF();
            pf2[i].x = calculateBezier(mStart.x, mEnd.x, mControl.x, i / (float) mMaxBezierLines);
            pf2[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;
    }

左侧ListAdapter(FriendAdapter)

public class FriendAdapter extends BaseAdapter {
    private List<FriendInfo> list;
    private LayoutInflater inflater;// 填充布局
    public FriendAdapter(Context context, List<FriendInfo> list) {
        this.list = list;
        this.inflater = LayoutInflater.from(context);
    }

   class ViewHolder {
       TextView username, showLetter;
   }

    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return null;
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
   public View getView(int position, View convertView, ViewGroup parent) {
       ViewHolder holder;
       if (convertView == null) {
           convertView =inflater.inflate(R.layout.listview_item, null);
           holder = new ViewHolder();
           holder.showLetter = (TextView) convertView.findViewById(R.id.tv_friend_title);
           holder.username = (TextView) convertView.findViewById(R.id.tv_friend_name);
           convertView.setTag(holder);
       } else {
           holder = (ViewHolder) convertView.getTag();
       }
       FriendInfo user = list.get(position);
       holder.username.setText(user.getName());
       //获得当前position是属于哪个分组
       int sectionForPosition = getSectionForPosition(position);
       //获得该分组第一项的position
       int positionForSection = getPositionForSection(sectionForPosition);
       //查看当前的是否是该组的第一个,是的话显示showLetter
       if (position == positionForSection) {
           holder.showLetter.setVisibility(View.VISIBLE);
           holder.showLetter.setText(user.getFirstLetter());
       } else {
           holder.showLetter.setVisibility(View.GONE);
       }
       return convertView;
   }
    //传入一个分组值[A....Z],获得该分组的第一项的position
    public int getPositionForSection(int sectionIndex) {
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i).getFirstLetter().charAt(0) == sectionIndex) {
                return i;
                //list是排序后的,只要一遇到该分组值就直接返回
            }
        }
        return -1;
    }

    //传入一个position,获得该position所在的分组
    public int getSectionForPosition(int position) {
        return list.get(position).getFirstLetter().charAt(0);
    }

    public void addAll(List<FriendInfo> data) {
        this.list.addAll(data);
        notifyDataSetChanged();
    }

}

FriendInfo

public class FriendInfo {
    private String name;
    private String pinyin;
    private String firstLetter;

    public String getFirstLetter() {
        return firstLetter;
    }

    public void setFirstLetter(String firstLetter) {
        this.firstLetter = firstLetter;
    }

    public FriendInfo() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPinyin() {
        return pinyin;
    }

    public void setPinyin(String pinyin) {
        this.pinyin = pinyin;
    }

    @Override
    public String toString() {
        return "FriendEntity{" +
                "name='" + name + '\'' +
                ", pinyin='" + pinyin + '\'' +
                '}';
    }

}

PinyinUtils

public class PinyinUtils {
    public static String getPinyin(String str) {
        HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
        format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
        format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
        StringBuilder sb = new StringBuilder();
        char[] charArray = str.toCharArray();
        for (int i = 0; i < charArray.length; i++) {
            char c = charArray[i];
            if (Character.isWhitespace(c)) { //如果是空格
                continue;
            }
            if (c > 128 || c < -127) {  //汉字
                try {   //根据字符获取对应的拼音
                    String s = PinyinHelper.toHanyuPinyinStringArray(c, format)[0];
                    sb.append(s);
                } catch (BadHanyuPinyinOutputFormatCombination e) {
                    e.printStackTrace();
                }
            } else {//除汉字以外的直接添加至StringBuilder
                sb.append(c);
            }
        }
        return sb.toString();
    }
}

attrs

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="FancyIndexer">
        <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>

activity_main

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:poplar="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#d5d5d5">
    <ListView
        android:id="@+id/lv_friends_listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <com.weitext.wavesidebaractivity.activities.SideBarView
        android:id="@+id/sbv_friends_bar"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginBottom="5dp"
        android:layout_marginTop="5dp"
        poplar:additionalTipOffset="40dp"
        poplar:fontColor="#6e6e6e"
        poplar:maxBezierHeight="150dp"
        poplar:maxBezierWidth="180dp"
        poplar:maxFontSize="60"
        poplar:minFontSize="32"
        poplar:tipFontColor="#41c2fc"
        poplar:tipFontSize="72"
        poplar:widthOffset="15dp"/>



</RelativeLayout>

listview_item

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_friend_title"
        android:layout_width="match_parent"
        android:layout_height="20dp"
        android:background="#CFCECE"
        android:gravity="center_vertical"
        android:paddingLeft="20dp"
        android:text="A"
        android:textColor="#232323"
        android:textSize="14sp"/>

    <TextView
        android:id="@+id/tv_friend_name"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:gravity="center_vertical"
        android:paddingLeft="20dp"
        android:text="海绵宝宝"
        android:textColor="#090909"
        android:textSize="16sp"/>

</LinearLayout>

在这里插入图片描述
借鉴:https://blog.csdn.net/qq_35352552/article/details/64918980?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-4.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-4.control

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值