滑动折线图

网上找到自定义view

package com.beijingyiling.ninglingpolice.widget;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.v4.util.Pair;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewConfigurationCompat;
import android.support.v4.widget.ScrollerCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Size;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;

import com.beijingyiling.ninglingpolice.R;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by ccy on 2017-07-28.
 */

public class MiuiWeatherView extends View {

    private static int DEFAULT_BULE = 0XFF00BFFF;
    private static int DEFAULT_GRAY = Color.GRAY;

    private int backgroundColor;
    private int minViewHeight; //控件的最低高度
    private int minPointHeight;//折线最低点的高度
    private int lineInterval; //折线线段长度
    private float pointRadius; //折线点的半径
    private float textSize; //字体大小
    private float pointGap; //折线单位高度差
    private int defaultPadding; //折线坐标图四周留出来的偏移量
    private float iconWidth;  //天气图标的边长
    private int viewHeight;
    private int viewWidth;
    private int screenWidth;
    private int screenHeight;

    private Paint linePaint; //线画笔
    private Paint textPaint; //文字画笔
    private Paint circlePaint; //圆点画笔

    private List<WeatherBean> data = new ArrayList<>(); //元数据
    private List<Pair<Integer, String>> weatherDatas = new ArrayList<>();  //对元数据中天气分组后的集合
    private List<Float> dashDatas = new ArrayList<>(); //不同天气之间虚线的x坐标集合
    private List<PointF> points = new ArrayList<>(); //折线拐点的集合
    private Map<String, Bitmap> icons = new HashMap<>(); //天气图标集合
    private int maxTemperature;//元数据中的最高和最低温度
    private int minTemperature;

    private VelocityTracker velocityTracker;
    private Scroller scroller;
    private ViewConfiguration viewConfiguration;


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

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

    public MiuiWeatherView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        scroller = new Scroller(context);
        viewConfiguration = ViewConfiguration.get(context);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MiuiWeatherView);
        minPointHeight = (int) ta.getDimension(R.styleable.MiuiWeatherView_min_point_height, dp2pxF(context, 60));
        lineInterval = (int) ta.getDimension(R.styleable.MiuiWeatherView_line_interval, dp2pxF(context, 60));
        backgroundColor = ta.getColor(R.styleable.MiuiWeatherView_background_color, Color.WHITE);
        ta.recycle();

        setBackgroundColor(backgroundColor);

        initSize(context);

        initPaint(context);

//        initIcons();


    }

    /**
     * 初始化默认数据
     */
    private void initSize(Context c) {
        screenWidth = getResources().getDisplayMetrics().widthPixels;
        screenHeight = getResources().getDisplayMetrics().heightPixels;

        minViewHeight = 3 * minPointHeight;  //默认3倍
        pointRadius = dp2pxF(c, 2.5f);
        textSize = sp2pxF(c, 10);
//      TODO 设置padding值
        defaultPadding = (int) dp2pxF(getContext(), 28);  //默认0.5倍
        iconWidth = (1.0f / 3.0f) * lineInterval; //默认1/3倍
    }

    /**
     * 计算折线单位高度差
     */
    private void calculatePontGap() {
        int lastMaxTem = -Integer.MAX_VALUE;
        int lastMinTem = Integer.MAX_VALUE;
        for (WeatherBean bean : data) {
            if (bean.temperature > lastMaxTem) {
                maxTemperature = bean.temperature;
                lastMaxTem = bean.temperature;
            }
            if (bean.temperature < lastMinTem) {
                minTemperature = bean.temperature;
                lastMinTem = bean.temperature;
            }
        }
        float gap = (maxTemperature - minTemperature) * 1.0f;
        gap = (gap == 0.0f ? 1.0f : gap);  //保证分母不为0
        pointGap = (viewHeight - minPointHeight - 2 * defaultPadding) / gap;
    }

    private void initPaint(Context c) {
        linePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        linePaint.setStrokeWidth(dp2px(c, 1));

        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setTextSize(textSize);
        textPaint.setColor(Color.BLACK);
        textPaint.setTextAlign(Paint.Align.CENTER);

        circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        circlePaint.setStrokeWidth(dp2pxF(c, 1));
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        initSize(getContext());
        calculatePontGap();
    }

    /**
     * 公开方法,用于设置元数据
     *
     * @param data
     */
    public void setData(List<WeatherBean> data) {
        if (data == null) {
            return;
        }
        this.data = data;
        notifyDataSetChanged();
    }

    public List<WeatherBean> getData() {
        return data;
    }

    public void notifyDataSetChanged() {
        if (data == null) {
            return;
        }
        weatherDatas.clear();
        points.clear();
        dashDatas.clear();

//        initWeatherMap(); //初始化相邻的相同天气分组
        requestLayout();
        invalidate();
    }


    /**
     * 初始化天气图标集合
     * (涉及解析、缩放等耗时操作,故不要在ondraw里再去获取图片,提前解析好放在集合里)
     */
//    private void initIcons() {
//        icons.clear();
//        String[] weathers = WeatherBean.getAllWeathers();
//        for (int i = 0; i < weathers.length; i++) {
//            Bitmap bmp = getWeatherIcon(weathers[i], iconWidth, iconWidth);
//            icons.put(weathers[i], bmp);
//        }
//    }

    /**
     * 根据元数据中连续相同的天气数做分组,
     * pair中的first值为连续相同天气的数量,second值为对应天气
     */
//    private void initWeatherMap() {
//        weatherDatas.clear();
//        String lastWeather = "";
//        int count = 0;
//        for (int i = 0; i < data.size(); i++) {
//            WeatherBean bean = data.get(i);
//            if (i == 0) {
//                lastWeather = bean.weather;
//            }
//            if (bean.weather != lastWeather) {
//                Pair<Integer, String> pair = new Pair<>(count, lastWeather);
//                weatherDatas.add(pair);
//                count = 1;
//            } else {
//                count++;
//            }
//            lastWeather = bean.weather;
//
//            if (i == data.size() - 1) {
//                Pair<Integer, String> pair = new Pair<>(count, lastWeather);
//                weatherDatas.add(pair);
//            }
//        }
//
//        for (int i = 0; i < weatherDatas.size(); i++) {
//            int c = weatherDatas.get(i).first;
//            String w = weatherDatas.get(i).second;
//            Log.d("ccy", "weatherMap i =" + i + ";count = " + c + ";weather = " + w);
//        }
//    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        if (heightMode == MeasureSpec.EXACTLY) {
            viewHeight = Math.max(heightSize, minViewHeight);
        } else {
            viewHeight = minViewHeight;
        }

        int totalWidth = 0;
        if (data.size() > 1) {
            totalWidth = 2 * defaultPadding + lineInterval * (data.size() - 1);
        }
        viewWidth = Math.max(screenWidth, totalWidth);  //默认控件最小宽度为屏幕宽度

        setMeasuredDimension(viewWidth, viewHeight);
        calculatePontGap();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (data.isEmpty()) {
            return;
        }
        drawAxis(canvas);

        drawLinesAndPoints(canvas);

        drawTemperature(canvas);

//        drawWeatherDash(canvas);

//        drawWeatherIcon(canvas);

    }

    /**
     * 画时间轴
     *
     * @param canvas
     */
    private void drawAxis(Canvas canvas) {
        canvas.save();
        linePaint.setColor(DEFAULT_GRAY);
        linePaint.setStrokeWidth(dp2px(getContext(), 1));

        canvas.drawLine(defaultPadding,
                viewHeight - defaultPadding,
                viewWidth - defaultPadding,
                viewHeight - defaultPadding,
                linePaint);

        float centerY = viewHeight - defaultPadding + dp2pxF(getContext(), 15);
        float centerX;
        for (int i = 0; i < data.size(); i++) {
            String text = data.get(i).time;
            centerX = defaultPadding + i * lineInterval;
            Paint.FontMetrics m = textPaint.getFontMetrics();
            canvas.drawText(text, 0, text.length(), centerX, centerY - (m.ascent + m.descent) / 2, textPaint);
        }
        canvas.restore();
    }

    /**
     * 画折线和它拐点的园
     *
     * @param canvas
     */
    private void drawLinesAndPoints(Canvas canvas) {
        canvas.save();
        linePaint.setColor(DEFAULT_BULE);
        linePaint.setStrokeWidth(dp2pxF(getContext(), 1));
        linePaint.setStyle(Paint.Style.STROKE);

        Path linePath = new Path(); //用于绘制折线
        points.clear();
//        TODO 原本基准线高度为 defaultPadding + minPointHeight
//        defaultPadding为折线坐标图四周留出来的偏移量
//        minPointHeight为折线最低点的高度
//        为了美观,重新定义了最低点到view底部的距离
        int baseHeight = (int) dp2pxF(getContext(), 20) + defaultPadding;
        float centerX;
        float centerY;
        for (int i = 0; i < data.size(); i++) {
            int tem = data.get(i).temperature;
            tem = tem - minTemperature;
            centerY = (int) (viewHeight - (baseHeight + tem * pointGap));
            centerX = defaultPadding + i * lineInterval;
            points.add(new PointF(centerX, centerY));
            if (i == 0) {
                linePath.moveTo(centerX, centerY);
            } else {
                linePath.lineTo(centerX, centerY);
            }
        }
        canvas.drawPath(linePath, linePaint); //画出折线

        //接下来画折线拐点的园
        float x, y;
        for (int i = 0; i < points.size(); i++) {
            x = points.get(i).x;
            y = points.get(i).y;

            //先画一个颜色为背景颜色的实心园覆盖掉折线拐角
            circlePaint.setStyle(Paint.Style.FILL_AND_STROKE);
            circlePaint.setColor(backgroundColor);
            canvas.drawCircle(x, y,
                    pointRadius + dp2pxF(getContext(), 1),
                    circlePaint);
            //再画出正常的空心园
            circlePaint.setStyle(Paint.Style.STROKE);
            circlePaint.setColor(DEFAULT_BULE);
            canvas.drawCircle(x, y,
                    pointRadius,
                    circlePaint);
        }
        canvas.restore();
    }

    /**
     * 画温度描述值
     *
     * @param canvas
     */
    private void drawTemperature(Canvas canvas) {
        canvas.save();

        textPaint.setTextSize(1.2f * textSize); //字体放大一丢丢
        float centerX;
        float centerY;
        String text;
        for (int i = 0; i < points.size(); i++) {
            text = data.get(i).temperatureStr;
            centerX = points.get(i).x;
            centerY = points.get(i).y - dp2pxF(getContext(), 13);
            Paint.FontMetrics metrics = textPaint.getFontMetrics();
            canvas.drawText(text,
                    centerX,
                    centerY - (metrics.ascent + metrics.descent) / 2,
                    textPaint);
        }
        textPaint.setTextSize(textSize);
        canvas.restore();
    }

    /**
     * 画不同天气之间的虚线
     *
     * @param canvas
     */
//    private void drawWeatherDash(Canvas canvas) {
//        canvas.save();
//        linePaint.setColor(DEFAULT_GRAY);
//        linePaint.setStrokeWidth(dp2pxF(getContext(), 0.5f));
//        linePaint.setAlpha(0xcc);
//
//        //设置画笔画出虚线
//        float[] f = {dp2pxF(getContext(), 5), dp2pxF(getContext(), 1)};  //两个值分别为循环的实线长度、空白长度
//        PathEffect pathEffect = new DashPathEffect(f, 0);
//        linePaint.setPathEffect(pathEffect);
//
//        dashDatas.clear();
//        int interval = 0;
//        float startX, startY, endX, endY;
//        endY = viewHeight - defaultPadding;
//
//        //0坐标点的虚线手动画上
//        canvas.drawLine(defaultPadding,
//                points.get(0).y + pointRadius + dp2pxF(getContext(), 2),
//                defaultPadding,
//                endY,
//                linePaint);
//        dashDatas.add((float) defaultPadding);
//
//        for (int i = 0; i < weatherDatas.size(); i++) {
//            interval += weatherDatas.get(i).first;
//            if(interval > points.size()-1){
//                interval = points.size()-1;
//            }
//            startX = endX = defaultPadding + interval * lineInterval;
//            startY = points.get(interval).y + pointRadius + dp2pxF(getContext(), 2);
//            dashDatas.add(startX);
//            canvas.drawLine(startX, startY, endX, endY, linePaint);
//        }
//
//        //这里注意一下,当最后一组的连续天气数为1时,是不需要计入虚线集合的,否则会多画一个天气图标
//        //TODO 若不理解,可尝试去掉下面这块代码并观察运行效果
        if(weatherDatas.get(weatherDatas.size()-1).first == 1
                && dashDatas.size() > 1){
            dashDatas.remove(dashDatas.get(dashDatas.size()-1));
        }
//
//        linePaint.setPathEffect(null);
//        linePaint.setAlpha(0xff);
//        canvas.restore();
//    }

    /**
     * 画天气图标和它下方文字
     * 若相邻虚线都在屏幕内,图标的x位置即在两虚线的中间
     * 若有一条虚线在屏幕外,图标的x位置即在屏幕边沿到另一条虚线的中间
     * 若两条都在屏幕外,图标x位置紧贴某一条虚线或屏幕中间
     *
     * @param canvas
     */
//    private void drawWeatherIcon(Canvas canvas) {
//        canvas.save();
//        textPaint.setTextSize(0.9f * textSize); //字体缩小一丢丢
//
//        boolean leftUsedScreenLeft = false;
//        boolean rightUsedScreenRight = false;
//
//        int scrollX = getScrollX();  //范围控制在0 ~ viewWidth-screenWidth
//        float left, right;
//        float iconX, iconY;
//        float textY;     //文字的x坐标跟图标是一样的,无需额外声明
//        iconY = viewHeight - (defaultPadding + minPointHeight / 2.0f);
//        textY = iconY + iconWidth / 2.0f + dp2pxF(getContext(), 10);
//        Paint.FontMetrics metrics = textPaint.getFontMetrics();
//        for (int i = 0; i < dashDatas.size() - 1; i++) {
//            left = dashDatas.get(i);
//            right = dashDatas.get(i + 1);
//
//            //以下校正的情况为:两条虚线都在屏幕内或只有一条在屏幕内
//
//            if (left < scrollX &&    //仅左虚线在屏幕外
//                    right < scrollX + screenWidth) {
//                left = scrollX;
//                leftUsedScreenLeft = true;
//            }
//            if (right > scrollX + screenWidth &&  //仅右虚线在屏幕外
//                    left > scrollX) {
//                right = scrollX + screenWidth;
//                rightUsedScreenRight = true;
//            }
//
//            if (right - left > iconWidth) {    //经过上述校正之后左右距离还大于图标宽度
//                iconX = left + (right - left) / 2.0f;
//            } else {                          //经过上述校正之后左右距离小于图标宽度,则贴着在屏幕内的虚线
//                if (leftUsedScreenLeft) {
//                    iconX = right - iconWidth / 2.0f;
//                } else {
//                    iconX = left + iconWidth / 2.0f;
//                }
//            }
//
//            //以下校正的情况为:两条虚线都在屏幕之外
//
//            if (right < scrollX) {  //两条都在屏幕左侧,图标紧贴右虚线
//                iconX = right - iconWidth / 2.0f;
//            } else if (left > scrollX + screenWidth) {   //两条都在屏幕右侧,图标紧贴左虚线
//                iconX = left + iconWidth / 2.0f;
//            } else if (left < scrollX && right > scrollX + screenWidth) {  //一条在屏幕左一条在屏幕右,图标居中
//                iconX = scrollX + (screenWidth / 2.0f);
//            }
//
//
//            Bitmap icon = icons.get(weatherDatas.get(i).second);
//
//            //经过上述校正之后可以得到图标和文字的绘制区域
//            RectF iconRect = new RectF(iconX - iconWidth / 2.0f,
//                    iconY - iconWidth / 2.0f,
//                    iconX + iconWidth / 2.0f,
//                    iconY + iconWidth / 2.0f);
//
//            canvas.drawBitmap(icon, null, iconRect, null);  //画图标
//
//            canvas.drawText(weatherDatas.get(i).second, //画图标下方文字
//                    iconX,
//                    textY - (metrics.ascent+metrics.descent)/2,
//                    textPaint);
//
//            leftUsedScreenLeft = rightUsedScreenRight = false; //重置标志位
//        }
//
//        textPaint.setTextSize(textSize);
//        canvas.restore();
//    }


    /**
     * 根据天气获取对应的图标,并且缩放到指定大小
     *
     * @param weather
     * @param requestW
     * @param requestH
     * @return
     */
//    private Bitmap getWeatherIcon(String weather, float requestW, float requestH) {
//        int resId = getIconResId(weather);
//        Bitmap bmp;
//        int outWdith, outHeight;
//        BitmapFactory.Options options = new BitmapFactory.Options();
//        options.inJustDecodeBounds = true;
//        BitmapFactory.decodeResource(getResources(), resId, options);
//        outWdith = options.outWidth;
//        outHeight = options.outHeight;
//        options.inSampleSize = 1;
//        if (outWdith > requestW || outHeight > requestH) {
//            int ratioW = Math.round(outWdith / requestW);
//            int ratioH = Math.round(outHeight / requestH);
//            options.inSampleSize = Math.max(ratioW, ratioH);
//        }
//        options.inJustDecodeBounds = false;
//        bmp = BitmapFactory.decodeResource(getResources(), resId, options);
//        return bmp;
//    }


//    private int getIconResId(String weather) {
//        int resId;
//        switch (weather) {
//            case WeatherBean.SUN:
//                resId = R.drawable.sun;
//                break;
//            case WeatherBean.CLOUDY:
//                resId = R.drawable.cloudy;
//                break;
//            case WeatherBean.RAIN:
//                resId = R.drawable.rain;
//                break;
//            case WeatherBean.SNOW:
//                resId = R.drawable.snow;
//                break;
//            case WeatherBean.SUN_CLOUD:
//                resId = R.drawable.sun_cloud;
//                break;
//            case WeatherBean.THUNDER:
//            default:
//                resId = R.drawable.thunder;
//                break;
//        }
//        return resId;
//    }


    private float lastX = 0;
    private float x = 0;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        velocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!scroller.isFinished()) {  //fling还没结束
                    scroller.abortAnimation();
                }
                lastX = x = event.getX();
                return true;
            case MotionEvent.ACTION_MOVE:
                x = event.getX();
                int deltaX = (int) (lastX - x);
                if (getScrollX() + deltaX < 0) {    //越界恢复
                    scrollTo(0, 0);
                    return true;
                } else if (getScrollX() + deltaX > viewWidth - screenWidth) {
                    scrollTo(viewWidth - screenWidth, 0);
                    return true;
                }
                scrollBy(deltaX, 0);
                lastX = x;
                break;
            case MotionEvent.ACTION_UP:
                x = event.getX();
                velocityTracker.computeCurrentVelocity(1000);  //计算1秒内滑动过多少像素
                int xVelocity = (int) velocityTracker.getXVelocity();
                if (Math.abs(xVelocity) > viewConfiguration.getScaledMinimumFlingVelocity()) {  //滑动速度可被判定为抛动
                    scroller.fling(getScrollX(), 0, -xVelocity, 0, 0, viewWidth - screenWidth, 0, 0);
                    invalidate();
                }
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            invalidate();
        }
    }

    //工具类
    public static int dp2px(Context c, float dp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, c.getResources().getDisplayMetrics());
    }

    public static int sp2px(Context c, float sp) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, c.getResources().getDisplayMetrics());
    }

    public static float dp2pxF(Context c, float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, c.getResources().getDisplayMetrics());
    }

    public static float sp2pxF(Context c, float sp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, c.getResources().getDisplayMetrics());
    }
}

对应的bean对象

package com.beijingyiling.ninglingpolice.widget;

/**
 * Created by ccy on 2017-07-28.
 */

public class WeatherBean {

//    public static final String SUN = "晴";
//    public static final String CLOUDY ="阴";
//    public static final String SNOW = "雪";
//    public static final String RAIN = "雨";
//    public static final String SUN_CLOUD = "多云";
//    public static final String THUNDER = "雷";



//    public String weather;  //天气,取值为上面6种
    public int temperature; //温度值
    public String temperatureStr; //温度的描述值
    public String time; //时间值

    public WeatherBean(int temperature,String time) {
//    public WeatherBean(String weather, int temperature,String time) {
//        this.weather = weather;
        this.temperature = temperature;
        this.time = time;
        this.temperatureStr = temperature + "°";
    }

    public WeatherBean(String weather, int temperature, String temperatureStr, String time) {
//        this.weather = weather;
        this.temperature = temperature;
        this.temperatureStr = temperatureStr;
        this.time = time;
    }

//    public static String[] getAllWeathers(){
//        String[] str = {SUN,RAIN,CLOUDY,SUN_CLOUD,SNOW,THUNDER};
//        return str;
//    }




}

value/attr文件添加

    <declare-styleable name="MiuiWeatherView">
        <attr name="line_interval" format="dimension"/>
        <attr name="min_point_height" format="dimension"/>
        <attr name="background_color" format="color"/>
    </declare-styleable>

使用方法

        MiuiWeatherView miuiWeatherView = findViewById(R.id.lineview);
        ArrayList<WeatherBean> lists = new ArrayList<WeatherBean>();
        for (int i = 0; i < 20; i++) {
            int temperature = new Random().nextInt(101);
            WeatherBean weatherBean = new WeatherBean(temperature, "1/8");
            lists.add(weatherBean);
        }
        miuiWeatherView.setData(lists);

参考文章链接https://blog.csdn.net/ccy0122/article/details/76464825

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值