Android自定义view之下载控件,ProgressBar


请尊重别人的劳动成果,转发文章请注明出处


概述


在开发过程中我们总会遇到一些不同于安卓自带的控件,业内称之为自定义控件,一直没有深入了解自定义VIEW,总觉得好像很厉害的样子,最近公司业务需求(做一个APK文件的下载)需要个性化的展示下载进度条。于是尝试着写一个下载进度条的自定义控件

为了不浪费大家的时间,先上效果图,对于赶时间的哥们来说在这里就是一个分水岭了,如果大家奔着学习自定义控件来的,那你不妨接着看下去




效果如图所示,只是小白不会制作动态图,只能随机截取一张示例


自定义VIEW


在网上看了一些博客,这里抛砖引玉,先总结下自定义View的步骤:

1、自定义View的属性

2、在View的构造方法中获得我们自定义的属性

3、#重写onMesure #

4、重写onDraw

第三点使用了不同的符号,想必有特殊的地方,别急,等一下会解释。现在结合我们的需求:下载进度条  最简单的进度条无非两个部分组成

  • 未下载部分
  • 已下载部分

这下简单了,我们使用两种不同的颜色来绘制 已下载部分 和 未下载部分 不就可以了吗?然后再根据下载进度实时重绘已经下载部分 。但是仔细想想一个完整的控件不应该只有两种不同的颜色,而是需要具备:
  • 下载状态文字
  • 文字颜色
  • 文字大小
  • 未下载部分颜色
  • 已经下载部分颜色
 基本的就这几个,但是再仔细想想,我们在应用商店上面看到的下载控件不仅仅就一个状态吧,而且这个下载的进度条要什么形状的呢?所以又有了下面的需求:
  • 矩形进度条  /  圆角矩形进度条
  • 控件其他状态的默认颜色


基本的需求已经分析完了,接下来就按照这自定义VIEW的步骤走吧
1、自定义View的属性首先在res/values/  下建立一个attrs.xml , 在里面定义我们的属性和声明我们的整个样式。
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 四周圆弧度 -->
    <attr name="cornerRadius" format="dimension" />
    <attr name="text" format="string" />
    <attr name="textColor" format="color" />
    <attr name="textSize" format="dimension" />
    <!-- 默认颜色 -->
    <attr name="defaultColor" format="color" />
    <!-- 未下载部分颜色 -->
    <attr name="undownloadColor" format="color" />
    <!-- 已经下载部分颜色 -->
    <attr name="downloadedColor" format="color" />
    
    <!-- RuffianProgressBarLine -->
    <declare-styleable name="RuffianProgressBarLine">
        <attr name="cornerRadius" />
        <attr name="text" />
        <attr name="textColor" />
        <attr name="textSize" />
        <attr name="defaultColor" />
        <attr name="undownloadColor" />
        <attr name="downloadedColor" />
    </declare-styleable>
</resources>
根据需求,我们定义了字体,字体颜色,字体大小,控件默认颜色,已经下载部分颜色,未下载部分颜色,控件的形状[矩形,圆角矩形],一共7个属性,format是值该属性的取值类型:

一共有:string,color,demension,integer,enum,reference,float,boolean,fraction,flag;不清楚的可以google一把。

然后在布局中声明我们的自定义View

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.ruffian.android.MainActivity$PlaceholderFragment" >

    <com.ruffian.android.view.RuffianProgressBarLine
        xmlns:custom="http://schemas.android.com/apk/res-auto"
        android:id="@+id/progressBarLine1"
        android:layout_width="100dp"
        android:layout_height="30dp"
        android:padding="10dp"
        custom:cornerRadius="20dp"
        custom:defaultColor="#9ACF51"
        custom:downloadedColor="#ec7883"
        custom:text="下载"
        custom:textColor="@android:color/white"
        custom:textSize="16sp"
        custom:undownloadColor="#cdcdcd" />

    <com.ruffian.android.view.RuffianProgressBarLine
        xmlns:custom="http://schemas.android.com/apk/res-auto"
        android:id="@+id/progressBarLine2"
        android:layout_width="100dp"
        android:layout_height="30dp"
        android:layout_alignParentRight="true"
        android:padding="10dp"
        custom:cornerRadius="0dp"
        custom:defaultColor="#f29b76"
        custom:downloadedColor="#e55a7f"
        custom:text="下载"
        custom:textColor="@android:color/white"
        custom:textSize="16sp"
        custom:undownloadColor="#fb9090" />

    <TextView
        android:id="@+id/progressText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/progressBarLine1"
        android:layout_centerHorizontal="true"
        android:padding="10dp"
        android:text="下载进度 " />

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="再玩一次" />

</RelativeLayout>
布局中展示不同的控件形状,同时展示下载进度百分比

注意:一定要引入 xmlns:custom="http://schemas.android.com/apk/res/res-auto"我们的命名空间,后面也可以是包路径:com.ruffian.android.view

2、在View的构造方法中,获得我们的自定义的样式

// 默认
	public static final String STATE_DEFAULT = "DEFAULT";
	// 安装
	public static final String STATE_INSTALL = "INSTALL";
	// 暂停
	public static final String STATE_STOP = "STOP";
	// 下载
	public static final String STATE_DOWNLOAD = "DOWNLOAD";
	// 打开
	public static final String STATE_OPEN = "OPEN";
	// 最大值100
	private static final float MAX_PROGRESS = 100;

	/**
	 * 控件四周圆弧角度,0:矩形<br/>
	 * 不设置或者设置为0的情况是矩形,其他情况是圆角矩形
	 * 
	 */
	private float mCornerRadius;
	/**
	 * 文字
	 */
	private String mText = "";
	/**
	 * 字体颜色
	 */
	private int mTextColor;
	/**
	 * 字体大小
	 */
	private int mTextSize;
	/**
	 * 控件默认颜色
	 */
	private int mDefaultColor;
	/**
	 * 默认颜色
	 */
	private final String DEF_DEFAULTCOLOR = "#9ACF51";
	/**
	 * 未下载部分颜色
	 */
	private int mUnDownloadColor;
	/**
	 * 默认颜色-下载进度条背景
	 */
	private final String DEF_BACKGROUDCOLOR = "#cdcdcd";
	/**
	 * 已经下载部分颜色
	 */
	private int mDownloadedColor;
	/**
	 * 默认颜色-下载进度
	 */
	private final String DEF_DOWNLOADCOLOR = "#ec7883";

	/**
	 * 矩形,绘制文字需要用
	 */
	private Rect mRect;

	/**
	 * 圆角矩形
	 */
	private RectF mRectF;

	/**
	 * 画笔,属性值可能改变
	 */
	private Paint mPaint;
	/**
	 * 文字画笔,初始化之后属性不再改变
	 */
	private Paint mTextPaint;

	/**
	 * 控件状态
	 */
	private String mState = STATE_DEFAULT;

	/**
	 * 下载进度{这里根据需求定基础类型,也可以是float[0.0f,1.0f]}
	 */
	private int mProgress;

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

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

	public RuffianProgressBarLine(Context context, AttributeSet attrs,
			int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		// 获取自定义的控件
		TypedArray typedArray = getContext().obtainStyledAttributes(attrs,
				R.styleable.RuffianProgressBarLine, defStyleAttr, 0);
		int parameterCount = typedArray.getIndexCount();
		for (int i = 0; i < parameterCount; i++) {

			int attr = typedArray.getIndex(i);
			switch (attr) {
			case R.styleable.RuffianProgressBarLine_cornerRadius:
				mCornerRadius = typedArray.getDimensionPixelSize(attr, 0);
				break;
			case R.styleable.RuffianProgressBarLine_text:
				mText = typedArray.getString(attr);
				break;
			case R.styleable.RuffianProgressBarLine_textColor:
				mTextColor = typedArray.getColor(attr, 0);
				break;
			case R.styleable.RuffianProgressBarLine_textSize:
				mTextSize = typedArray.getDimensionPixelSize(attr, 12);
				break;
			case R.styleable.RuffianProgressBarLine_defaultColor:
				mDefaultColor = typedArray.getColor(attr,
						Color.parseColor(DEF_DEFAULTCOLOR));
				break;
			case R.styleable.RuffianProgressBarLine_undownloadColor:
				mUnDownloadColor = typedArray.getColor(attr,
						Color.parseColor(DEF_BACKGROUDCOLOR));
				break;
			case R.styleable.RuffianProgressBarLine_downloadedColor:
				mDownloadedColor = typedArray.getColor(attr,
						Color.parseColor(DEF_DOWNLOADCOLOR));
				break;
			}

		}

		typedArray.recycle();

		mPaint = new Paint();
		mRect = new Rect();
		mRectF = new RectF();

		// 初始化之后不再改变,直接设置属性
		mTextPaint = new Paint();
		// 设置抗锯齿,圆滑处理
		mTextPaint.setAntiAlias(true);
		// 设置画笔类型
		mTextPaint.setStyle(Style.FILL);
		// 设置画笔颜色
		mTextPaint.setColor(mTextColor);
		// 设置字体大小
		mTextPaint.setTextSize(mTextSize);

	}
我们重写了3个构造方法,默认的布局文件调用的是两个参数的构造方法,所以记得让所有的构造调用我们的三个参数的构造,我们在三个参数的构造中获得自定义属性。

3、我们重写 onDraw,onMesure 调用系统提供的:

/**
	 * 重写计算控件宽高函数
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

		// 获取宽高的设置模式
		int withMode = MeasureSpec.getMode(widthMeasureSpec);
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		// 获取宽高的大小
		int withSize = MeasureSpec.getSize(widthMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);
		// 最终宽高
		int height = getSizeInMode(heightSize, heightMode, 1);
		int width = getSizeInMode(withSize, withMode, 0);

		// 最终设置宽高
		setMeasuredDimension(width, height);

	}

	/**
	 * 获取不同mode下宽高的实际值<br/>
	 * type[0:宽,1:高]
	 * 
	 * @param size初始值
	 * @param mode设置类型
	 * @param type
	 * @return
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	private int getSizeInMode(int size, int mode, int type) {
		// 返回值
		int sizeValue = 0;

		switch (mode) {
		case MeasureSpec.EXACTLY:

			// 设置了明确的值,直接使用
			sizeValue = size;

			break;
		case MeasureSpec.AT_MOST:

			// WARP_CONTENT时候,先计算绘制文本的大小
			mTextPaint.setTextSize(mTextSize);
			mTextPaint.getTextBounds(mText, 0, mText.length(), mRect);

			// 再计算[左右,上下]的padding值
			int desired = 0;
			if (type == 0) {

				// 文本宽度+左右padding
				float textWidth = mRect.width();
				desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());

			} else if (type == 1) {

				// 文本宽度+上下padding
				float textHeight = mRect.height();
				desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
			}

			sizeValue = desired;

			break;
		case MeasureSpec.UNSPECIFIED:
			// 不处理
			break;
		}

		return sizeValue;

	}

	/**
	 * 重写绘制函数onDraw
	 */
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		// 设置抗锯齿,圆滑处理
		mPaint.setAntiAlias(true);
		// 设置画笔类型
		mPaint.setStyle(Style.FILL);

		// 绘制控件
		canvasViewOnLogic(canvas);

	}

	/**
	 * 根据业务逻辑绘制控件
	 * 
	 * @param canvas
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	private void canvasViewOnLogic(Canvas canvas) {

		/**
		 * 下载中和暂停状态是特殊情况,需要画两层视图,其他情况只需要一层
		 */
		if (mState.equals(STATE_DOWNLOAD) || mState.equals(STATE_STOP)) {
			// 暂停状态--下载中状态

			// 绘制时mProgress要转化成float类型,区间[0.0f,1.0f]
			drawDownloadView(canvas, mDownloadedColor, 0,
					(int) ((mProgress / MAX_PROGRESS) * getWidth()));

			drawDownloadView(canvas, mUnDownloadColor,
					(int) ((mProgress / MAX_PROGRESS) * getWidth()), getWidth());

		} else {
			// 其他状态

			// 设置默认画笔颜色
			mPaint.setColor(mDefaultColor);
			// 设置矩形,宽度是控件大小
			mRectF = new RectF(0, 0, getWidth(), getHeight());
			// 画底部矩形
			canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);

		}

		// 计算文字
		mTextPaint.getTextBounds(mText, 0, mText.length(), mRect);
		// 绘制文字居中
		canvas.drawText(mText, getWidth() / 2 - mRect.width() / 2, getHeight()
				/ 2 + mRect.height() / 2, mTextPaint);

	}

	/**
	 * 绘制下载状态的view<br/>
	 * 理解:绘制两次相同的view,不同颜色区分,一个绘制前半部分,一部分绘制后半部分
	 * 
	 * @param canvas
	 * @param color
	 * @param startX开始绘制的X
	 * @param endX结束绘制的X
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	private void drawDownloadView(Canvas canvas, int color, int startX, int endX) {

		mPaint.setColor(color);
		// 设置矩形,宽度是控件大小
		mRectF = new RectF(0, 0, getWidth(), getHeight());

		canvas.save(Canvas.CLIP_SAVE_FLAG);
		canvas.clipRect(startX, 0, endX, getMeasuredHeight());
		canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);
		canvas.restore();

	}

文章开头说 onMeasure 函数存在特殊情况。这种特殊情况是针对,布局文件中 layout_width , layout_height。

当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果,当我们设置为WRAP_CONTENT,或者MATCH_PARENT系统帮我们测量的结果就是MATCH_PARENT的长度。

所以,当设置了WRAP_CONTENT时,我们需要自己进行测量,即重写onMesure方法”:

重写之前先了解MeasureSpec的specMode,一共三种类型:

EXACTLY:一般是设置了明确的值或者是MATCH_PARENT

AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT

UNSPECIFIED:表示子布局想要多大就多大,很少使用


/**
	 * 重写计算控件宽高函数
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

		// 获取宽高的设置模式
		int withMode = MeasureSpec.getMode(widthMeasureSpec);
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		// 获取宽高的大小
		int withSize = MeasureSpec.getSize(widthMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);
		// 最终宽高
		int height = getSizeInMode(heightSize, heightMode, 1);
		int width = getSizeInMode(withSize, withMode, 0);

		// 最终设置宽高
		setMeasuredDimension(width, height);

	}

	/**
	 * 获取不同mode下宽高的实际值<br/>
	 * type[0:宽,1:高]
	 * 
	 * @param size初始值
	 * @param mode设置类型
	 * @param type
	 * @return
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	private int getSizeInMode(int size, int mode, int type) {
		// 返回值
		int sizeValue = 0;

		switch (mode) {
		case MeasureSpec.EXACTLY:

			// 设置了明确的值,直接使用
			sizeValue = size;

			break;
		case MeasureSpec.AT_MOST:

			// WARP_CONTENT时候,先计算绘制文本的大小
			mTextPaint.setTextSize(mTextSize);
			mTextPaint.getTextBounds(mText, 0, mText.length(), mRect);

			// 再计算[左右,上下]的padding值
			int desired = 0;
			if (type == 0) {

				// 文本宽度+左右padding
				float textWidth = mRect.width();
				desired = (int) (getPaddingLeft() + textWidth + getPaddingRight());

			} else if (type == 1) {

				// 文本宽度+上下padding
				float textHeight = mRect.height();
				desired = (int) (getPaddingTop() + textHeight + getPaddingBottom());
			}

			sizeValue = desired;

			break;
		case MeasureSpec.UNSPECIFIED:
			// 不处理
			break;
		}

		return sizeValue;

	}


这里特别说明一下 onDraw方法

如果是在矩形的情况下是很简答的一种实现:

先画一个底部的矩形(表示未下载),然后再重新设置画笔颜色再画一个(表示进度)矩形。看起来就能达到下载进度的效果

但是当我们设置属性为 圆角矩形(cornerRadius>0)的时候,我发现效果不是我想要的

运行结果是:这样的,这样的



但是我们想要的是:这样的,这样的



由于刚开始自定义控件,很多属性和用法都不知道怎么用,折腾了好久,后来在网上看到说  canvas 有个 clipRect 的方法,good ,那么修改一下绘制部分的代码就可以了

起初代码

	// 暂停状态--下载中状态
		
					// 设置底部矩形颜色
					mPaint.setColor(mBackgroudColor);
					// 设置矩形,宽度是控件大小
					mRectF = new RectF(0, 0, getWidth(), getHeight());
					// 画底部矩形
					canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);

					// 设置进度矩形颜色
					mPaint.setColor(mDownloadColor);

					// 设置矩形,宽度是实际进度
					mRectF = new RectF(0, 0, (mProgress / MAX_PROGRESS) * getWidth(),
							getHeight());
					// 画进度矩形
					canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);


 
 修改关键方法 

/**
	 * 绘制下载状态的view<br/>
	 * 理解:绘制两次相同的view,不同颜色区分,一个绘制前半部分,一部分绘制后半部分
	 * 
	 * @param canvas
	 * @param color
	 * @param startX开始绘制的X
	 * @param endX结束绘制的X
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	private void drawDownloadView(Canvas canvas, int color, int startX, int endX) {

		mPaint.setColor(color);
		// 设置矩形,宽度是控件大小
		mRectF = new RectF(0, 0, getWidth(), getHeight());

		canvas.save(Canvas.CLIP_SAVE_FLAG);
		canvas.clipRect(startX, 0, endX, getMeasuredHeight());
		canvas.drawRoundRect(mRectF, mCornerRadius, mCornerRadius, mPaint);
		canvas.restore();

	}
// 暂停状态--下载中状态

			// 绘制时mProgress要转化成float类型,区间[0.0f,1.0f]
			drawDownloadView(canvas, mDownloadedColor, 0,
					(int) ((mProgress / MAX_PROGRESS) * getWidth()));

			drawDownloadView(canvas, mUnDownloadColor,
					(int) ((mProgress / MAX_PROGRESS) * getWidth()), getWidth());

恩恩,这下好了,终于是我们想要的效果了,接下来看看activity模拟下载整个流程,使用自定义的下载控件

activity代码

package com.ruffian.android;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

import com.ruffian.android.view.RuffianProgressBarLine;

@SuppressLint("HandlerLeak")
public class MainActivity extends Activity {

	private RuffianProgressBarLine mProgressBarLine;
	private Button mButton;
	private TextView mProgressText;// 下载进度

	int mProgress = 0;
	boolean isLoading = false;
	private String viewState;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mProgressBarLine = (RuffianProgressBarLine) findViewById(R.id.progressBarLine1);
		mProgressText = (TextView) findViewById(R.id.progressText);
		mButton = (Button) findViewById(R.id.button);

		mProgressBarLine.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View arg0) {

				viewState = mProgressBarLine.getState();

				if (viewState.equals(RuffianProgressBarLine.STATE_DEFAULT)) {
					// 下载中
					mProgressBarLine
							.setState(RuffianProgressBarLine.STATE_DOWNLOAD);
					mProgressBarLine.setText("暂停");
					isLoading = true;
					download();

				} else if (viewState
						.equals(RuffianProgressBarLine.STATE_DOWNLOAD)) {
					// 暂停
					mProgressBarLine
							.setState(RuffianProgressBarLine.STATE_STOP);
					mProgressBarLine.setText("继续");
					isLoading = false;
					// download();

				} else if (viewState.equals(RuffianProgressBarLine.STATE_STOP)) {
					// 继续
					mProgressBarLine
							.setState(RuffianProgressBarLine.STATE_DOWNLOAD);
					mProgressBarLine.setText("暂停");
					isLoading = true;
					// download();

				} else if (viewState
						.equals(RuffianProgressBarLine.STATE_INSTALL)) {
					// 安装
					mProgressBarLine
							.setState(RuffianProgressBarLine.STATE_INSTALL);
					mProgressBarLine.setText("安装");

				} else if (viewState.equals(RuffianProgressBarLine.STATE_OPEN)) {
					// 运行
					mProgressBarLine
							.setState(RuffianProgressBarLine.STATE_DOWNLOAD);
					mProgressBarLine.setText("运行");
				}

			}
		});

		mButton.setOnClickListener(new OnClickListener() {

			@Override
			public void onClick(View arg0) {
				mProgress = 0;
				isLoading = false;
				mProgressBarLine.setState(RuffianProgressBarLine.STATE_DEFAULT);
				mProgressBarLine.setText("下载");
				mProgressText.setText("下载进度 ");
			}
		});

	}

	/**
	 * 下载,暂停
	 * 
	 * @author Ruffian
	 * @date 2015年12月11日
	 */
	public void download() {
		new Thread() {
			public void run() {

				while (mProgress <= 100) {

					if (mProgress == 100) {
						// 进度满100,状态改为安装
						mProgressBarLine
								.setState(RuffianProgressBarLine.STATE_INSTALL);
						mProgressBarLine.setText("安装");
					}

					// 是否正在下载
					if (isLoading) {
						// 更新UI
						uiHandler.sendMessage(uiHandler.obtainMessage(1001,
								mProgress));
						mProgressBarLine.setProgress(mProgress);
						mProgress++;
					}
					// Log.w("sss", "" + mProgress);
					try {

						Thread.sleep(80);// 进度改变速度
					} catch (InterruptedException e) {
						e.printStackTrace();
					}

				}

			};
		}.start();
	}

	/**
	 * 更新UI
	 */
	Handler uiHandler = new Handler() {
		public void handleMessage(android.os.Message msg) {
			switch (msg.what) {
			case 1001:
				int progress = (int) msg.obj;
				if (progress == 100) {
					mProgressText.setText("下载完成");
				} else {
					mProgressText.setText(String.valueOf(progress) + "%");
				}
				break;
			}
		};
	};

}


小弟第一次尝试自定义控件的实现,前辈们要是看见哪里有不正确的地方,还恳请耐心指正,同时小伙伴们在学习过程中遇到什么问题可以留言讨论, 紧急情况发送邮件:632835821@qq.com 
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值