Android进阶---自定义控件的详解

自定义控件的学习流程:

View的测量->View的绘制->ViewGroup的测量->ViewGroup的绘制->自定义ViewGroup->自定义控件的三种方式说明->事件拦截机制说明

View的测量:
Android系统绘制View是需要我们精确地告诉它该如何去画,它才能绘制出你想要的图形。
那么Android在绘制View之前,我们必须对View进行测量,即告诉系统该画一个多大的View。这个过程是在onMeasure()方法中进行的。
Android系统有一个功能强大的类---MeasureSpec类,通过它来帮我们测量View,测量模式有以下三种:
EXACTLY:精确值模式,当我们将控件的layout_width属性或layout_height属性指定为具体数字时,例如:android:layout_width="100dp",或指定为match_parent属性(填充父窗体)时,系统就会使用EXACTLY模式。

AT_MOST:最大值模式,当控件的layout_width属性或layout_height属性指定为wrap_content时,控件大小一般随着子控件或内容变化而变化,此时控件的尺寸只要不超过父控件允许的最大尺寸即可。

UNSPECIFIED:这个属性比较奇怪---它不指定其大小测量模式,View想多大就多大,通常情况下在绘制自定义View时才会使用。

如果不重写onMeasure()方法来指定属性的大小,那么View默认的就只支持EXACTLY模式。
View的测量方式:

/**
 * Created by Layne_Yao on 2017-11-17 上午11:38:38.
 * CSDN:http://blog.csdn.net/Jsagacity
 */
public class DragView extends View {

	public DragView(Context context) {
		super(context);
		ininView();
	}

	public DragView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		ininView();
	}

	public DragView(Context context, AttributeSet attrs) {
		super(context, attrs);
		ininView();
	}

	private void ininView() {
		setBackgroundColor(Color.BLUE);
	}

	// View的测量
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		setMeasuredDimension(measuredWidth(widthMeasureSpec),
				measuredHeight(heightMeasureSpec));
	}

	private int measuredHeight(int heightMeasureSpec) {
		int result = 0;
		// 从MeasureSpec对象中提取出具体的测量模式和大小
		int specMode = MeasureSpec.getMode(heightMeasureSpec);
		int specSize = MeasureSpec.getSize(heightMeasureSpec);

		if (specMode == MeasureSpec.EXACTLY) {// 如果精确值模式,直接就是指定值
			result = specSize;
		} else {
			// 最大值模式给定一个不大的值
			result = 200;
			if (specMode == MeasureSpec.AT_MOST) {
				result = Math.min(result, specSize);
			}
		}
		return result;
	}

	private int measuredWidth(int widthMeasureSpec) {
		int result = 0;
		int specMode = MeasureSpec.getMode(widthMeasureSpec);
		int specSize = MeasureSpec.getSize(widthMeasureSpec);

		if (specMode == MeasureSpec.EXACTLY) {
			result = specSize;
		} else {
			result = 200;
			if (specMode == MeasureSpec.AT_MOST) {
				result = Math.min(result, specSize);
			}
		}
		return result;
	}

}

布局情况:

    <com.itman.customviewdemo.DragView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

如果layout_width属性或layout_height属性指定为wrap_content时那么就是最大值模式,我们给定了一个值。如果layout_width属性或layout_height属性指定为100dp,或者是match_parent属性时,就是精确值模式,那么就指定的显示大小。

显示结果:




View的绘制:
当测量好了一个View之后,我们就可以简单地重写onDraw()方法,并在Canvas对象上来绘制所需要的图形。
要想在Android的界面中绘制相应的图像,就必须在Canvas上进行绘制。Canvas就像是一个画板,使用Paint就可以在上面作画了。通常需要通过继承View并重写onDraw()方法来完成绘图。

ViewGroup的测量:
ViewGroup会去管理其子View,其中一个管理项目就是负责子View 的显示大小,ViewGroup大小为wrap_content时,就需要获取View的大小来决定自己的大小。其他情况就是指定值。

ViewGroup测量子View时,就是调用子View的onMeasure()来测量子View自身。
测量完了,就会调用子View的onLayout()来决定View的显示位置。


ViewGroup的绘制:
ViewGroup其实没有需要绘制的东西,onDraw()也只是修改一下背景。
但是ViewGroup使用dispatchDraw()方法可以绘制其子View,其过程同样是通过遍历所有子View,并调用View的绘制方法来完成绘制工作。


自定义View:
在自定义View时,我们通常会去重写onDraw()方法来绘制View的显示内容。
自定义View中,通常有以下一些比较重要的View的回调方法
1.onFinishInflate():从XML加载组建后回调
2.onSizeChanged():组件大小改变后回调
3.onMeasure():回调该方法来进行测量
4.onLayout():回调该方法来确定显示的位置
5.onTouchEvent():监听触摸事件时回调

自定义控件的三种方式:
1.对现有控件进行拓展
2.通过组合来实现新的控件
3.重写View来实现全新的控件


1、对现有控件进行拓展
一般来说,可以通过onDraw()方法对原生控件行为进行拓展,增加新的功能,修改显示的UI等。

先来个简单的:

/**
 * Created by Layne_Yao on 2017-11-17 上午11:50:38.
 * CSDN:http://blog.csdn.net/Jsagacity
 */
public class CustomTextView extends TextView {

    private Paint paint1, paint2;

	public CustomTextView(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
		 initPaint();
	}
    
    public CustomTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }
   
    public CustomTextView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		// TODO Auto-generated constructor stub
		 initPaint();
	}

	/**
     * 初始化画笔
     */
    private void initPaint() {
        paint1 = new Paint();
        paint1.setColor(Color.RED);
        paint1.setStyle(Paint.Style.FILL);
        paint2 = new Paint();
        paint2.setColor(Color.YELLOW);
        paint2.setStyle(Paint.Style.FILL);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //绘制外层矩形
        canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint1);
        //绘制内层矩形
        canvas.drawRect(10, 10, getMeasuredWidth() - 10, getMeasuredHeight() - 10, paint2);
        canvas.save();
        //绘制文字前平移10像素
        canvas.translate(10, 0);
        //父类完成的方法,即绘制文本
        super.onDraw(canvas);
        canvas.restore();
    }
}

来个复杂点的:

/**
 * Created by Layne_Yao on 2017-11-18 上午9:47:25.
 * CSDN:http://blog.csdn.net/Jsagacity
 */
public class DynamicsTextView extends TextView{

	 int mViewWidth = 0;
	    private Paint mPaint;
	    private LinearGradient mLinearGradient;
	    private Matrix matrix;
	    private int mTranslate;
	    
	public DynamicsTextView(Context context) {
		super(context);
		
	}
	
	public DynamicsTextView(Context context, AttributeSet attrs,
			int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		// TODO Auto-generated constructor stub
	}

	public DynamicsTextView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		// TODO Auto-generated method stub
		super.onSizeChanged(w, h, oldw, oldh);
        if (mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                mPaint = getPaint();
                mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{Color.BLUE, 0xffffffff, Color.BLUE},
                        null, Shader.TileMode.CLAMP);
                mPaint.setShader(mLinearGradient);
                matrix = new Matrix();
            }
        }
		
	}
	
	 @Override
	    protected void onDraw(Canvas canvas) {
	        super.onDraw(canvas);
	        if (matrix != null) {
	            mTranslate += mViewWidth + 5;
	            if (mTranslate > 2 * mViewWidth / 5) {
	                mTranslate = -mViewWidth;
	            }
	            matrix.setTranslate(mTranslate, 0);
	            mLinearGradient.setLocalMatrix(matrix);
	            postInvalidateDelayed(500);
	        }
	    }
}

运行结果:



2、创建复合控件
创建复合控件可以很好地创建出具有重用功能的控件集合。这种方式通常需要继承一个合适的ViewGroup,再添加指定功能的控件,从而组合成新的复合控件。通过这种方式创建的控件,我们一般会给它指定一些可配置的属性,让她具有更强的拓展性。

复合控件:

/**
 * Created by Layne_Yao on 2017-11-18 上午10:36:12.
 * CSDN:http://blog.csdn.net/Jsagacity
 */
public class ToolBar extends RelativeLayout {
	  // 包含topbar上的元素:左按钮、右按钮、标题
    private Button mLeftButton, mRightButton;
    private TextView mTitleView;

    // 布局属性,用来控制组件元素在ViewGroup中的位置
    private LayoutParams mLeftParams, mTitlepParams, mRightParams;

    // 左按钮的属性值,即我们在atts.xml文件中定义的属性
    private int mLeftTextColor;
    private Drawable mLeftBackground;
    private String mLeftText;
    // 右按钮的属性值,即我们在atts.xml文件中定义的属性
    private int mRightTextColor;
    private Drawable mRightBackground;
    private String mRightText;
    // 标题的属性值,即我们在atts.xml文件中定义的属性
    private float mTitleTextSize;
    private int mTitleTextColor;
    private String mTitle;

    // 映射传入的接口对象
    private topbarClickListener mListener;

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

    public ToolBar(Context context) {
        super(context);
    }

    public ToolBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 设置topbar的背景
        setBackgroundColor(0xFFF59563);
        // 通过这个方法,将你在atts.xml中定义的declare-styleable
        // 的所有属性的值存储到TypedArray中
        TypedArray ta = context.obtainStyledAttributes(attrs,
                R.styleable.TopBar);
        // 从TypedArray中取出对应的值来为要设置的属性赋值
        mLeftTextColor = ta.getColor(
                R.styleable.TopBar_leftTextColor, 0);
        mLeftBackground = ta.getDrawable(
                R.styleable.TopBar_leftBackground);
        mLeftText = ta.getString(R.styleable.TopBar_leftText);

        mRightTextColor = ta.getColor(
                R.styleable.TopBar_rightTextColor, 0);
        mRightBackground = ta.getDrawable(
                R.styleable.TopBar_rightBackground);
        mRightText = ta.getString(R.styleable.TopBar_rightText);

        mTitleTextSize = ta.getDimension(
                R.styleable.TopBar_titleTextSize, 10);
        mTitleTextColor = ta.getColor(
                R.styleable.TopBar_titleTextColor, 0);
        mTitle = ta.getString(R.styleable.TopBar_title_text);

        // 获取完TypedArray的值后,一般要调用
        // recyle方法来避免重新创建的时候的错误
        ta.recycle();

        mLeftButton = new Button(context);
        mRightButton = new Button(context);
        mTitleView = new TextView(context);

        // 为创建的组件元素赋值
        // 值就来源于我们在引用的xml文件中给对应属性的赋值
        mLeftButton.setTextColor(mLeftTextColor);
        mLeftButton.setBackgroundDrawable(mLeftBackground);
        mLeftButton.setText(mLeftText);

        mRightButton.setTextColor(mRightTextColor);
        mRightButton.setBackgroundDrawable(mRightBackground);
        mRightButton.setText(mRightText);

        mTitleView.setText(mTitle);
        mTitleView.setTextColor(mTitleTextColor);
        mTitleView.setTextSize(mTitleTextSize);
        mTitleView.setGravity(Gravity.CENTER);

        // 为组件元素设置相应的布局元素
        mLeftParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.MATCH_PARENT);
        mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, TRUE);
        // 添加到ViewGroup
        addView(mLeftButton, mLeftParams);

        mRightParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.MATCH_PARENT);
        mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, TRUE);
        addView(mRightButton, mRightParams);

        mTitlepParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.MATCH_PARENT);
        mTitlepParams.addRule(RelativeLayout.CENTER_IN_PARENT, TRUE);
        addView(mTitleView, mTitlepParams);
}

运行结果:



3、重写View来实现全新的控件

自定义一个View时通常需要继承View类,并重写它的onDraw(),onMeasure()等方法来实现绘制逻辑,同时通过重写onTouchEvent()等触控时间来实现交互逻辑。

案例一:

/**
 * Created by Layne_Yao on 2017-11-18 上午11:08:23.
 * CSDN:http://blog.csdn.net/Jsagacity
 */
public class CircleProgressView extends View {

	private int mCircleXY;
	private int length;
	private float mRadius;

	private Paint mCirclePaint;
	private Paint mArcPaint;
	private Paint mTextPaint;
	private String mShowText = "Android skill";

	private int mTextSize = 25;
	private float mSweepValue = 270;

	public CircleProgressView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
		// 获取屏幕高宽
		WindowManager wm = (WindowManager) getContext().getSystemService(
				Context.WINDOW_SERVICE);
		length = wm.getDefaultDisplay().getWidth();
		init();
	}

	private void init() {
		mCircleXY = length / 2;
		mRadius = (float) (length * 0.5 / 2);

		mCirclePaint = new Paint();
		mCirclePaint.setColor(Color.BLUE);

		mArcPaint = new Paint();
		mArcPaint.setStrokeWidth(50);
		mArcPaint.setStyle(Paint.Style.STROKE);
		mArcPaint.setColor(Color.BLUE);

		mTextPaint = new Paint();
		mTextPaint.setColor(Color.WHITE);
		mTextPaint.setTextSize(mTextSize);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		// 矩形
		RectF mArcRectF = new RectF((float) (length * 0.1),
				(float) (length * 0.1), (float) (length * 0.9),
				(float) (length * 0.9));
		// 绘制圆
		canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
		// 绘制弧线
		canvas.drawArc(mArcRectF, 270, mSweepValue, false, mArcPaint);
		// 绘制文字
		canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY
				+ (mTextSize / 4), mTextPaint);
	}

	public void setSweepValue(float sweepValue) {
		if (sweepValue != 0) {
			mSweepValue = sweepValue;
		} else {
			mSweepValue = 25;
		}
		invalidate();
	}

}

案例二:

/**
 * Created by Layne_Yao on 2017-11-20 上午11:29:02.
 * CSDN:http://blog.csdn.net/Jsagacity
 */
public class VolumeView extends View {
	private int mWidth;
	private int mRectWidth;
	private int mRectHeight;
	private Paint mPaint;
	private int mRectCount;
	private int offset = 5;
	private double mRandom;
	private LinearGradient mLinearGradient;

	public VolumeView(Context context) {
		super(context);
		initView();
	}

	public VolumeView(Context context, AttributeSet attrs) {
		super(context, attrs);
		initView();
	}

	public VolumeView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		initView();
	}

	private void initView() {
		mPaint = new Paint();
		mPaint.setColor(Color.BLUE);
		mPaint.setStyle(Paint.Style.FILL);
		mRectCount = 12;
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		mWidth = getWidth();
		mRectHeight = getHeight();
		mRectWidth = (int) (mWidth * 0.6 / mRectCount);
		mLinearGradient = new LinearGradient(0, 0, mRectWidth, mRectHeight,
				Color.YELLOW, Color.BLUE, Shader.TileMode.CLAMP);
		mPaint.setShader(mLinearGradient);
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		for (int i = 0; i < mRectCount; i++) {
			mRandom = Math.random();
			float currentHeight = (float) (mRectHeight * mRandom);
			canvas.drawRect(
					(float) (mWidth * 0.4 / 2 + mRectWidth * i + offset),
					currentHeight, (float) (mWidth * 0.4 / 2 + mRectWidth
							* (i + 1)), mRectHeight, mPaint);
		}
		postInvalidateDelayed(300);
	}
}


运行结果:

   

自定义ViewGroup:
ViewGroup存在的目的就是为了对其子View进行管理,为其子View添加显示、相应的规则。因此,自定义ViewGroup通常需要重写onMeasure()方法来对子View进行测量,重写onLayout()方法来确定子View的位置,重写onTouchEvent()方法增加相应事件,下面看看如何自定义ViewGroup。

/**
 * Created by Layne_Yao on 2017-11-18 下午3:59:32.
 * CSDN:http://blog.csdn.net/Jsagacity
 */
public class CustomScrollView extends ViewGroup {
	private int mScreenHeight;
	private Scroller mScroller;
	private int mLastY;
	private int mStart;
	private int mEnd;

	public CustomScrollView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// 获取屏幕高宽
		WindowManager wm = (WindowManager) getContext().getSystemService(
				Context.WINDOW_SERVICE);
		mScreenHeight = wm.getDefaultDisplay().getHeight();
		mScroller = new Scroller(getContext());
	}

	@Override
	protected void onLayout(boolean b, int i, int i1, int i2, int i3) {
		int childCount = getChildCount();
		MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
		mlp.height = mScreenHeight * childCount;
		setLayoutParams(mlp);

		for (int j = 0; j < childCount; j++) {
			View child = getChildAt(j);
			if (child.getVisibility() != View.GONE) {
				child.layout(i, j * mScreenHeight, i2, (j + 1) * mScreenHeight);
			}
		}
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		int count = getChildCount();
		for (int i = 0; i < count; ++i) {
			View childView = getChildAt(i);
			measureChild(childView, widthMeasureSpec, heightMeasureSpec);
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		int y = (int) event.getY();
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			mLastY = y;
			mStart = getScrollY();
			break;
		case MotionEvent.ACTION_MOVE:
			if (!mScroller.isFinished()) {
				mScroller.abortAnimation();
			}
			int dy = mLastY - y;
			if (getScrollY() < 0) {
				dy = 0;
			}
			if (getScrollY() > getHeight() - mScreenHeight) {
				dy = 0;
			}
			scrollBy(0, dy);
			mLastY = y;
			break;
		case MotionEvent.ACTION_UP:
			mEnd = getScrollY();
			int dScrollY = mEnd - mStart;
			if (dScrollY > 0) {
				if (dScrollY < mScreenHeight / 3) {
					mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
				} else {
					mScroller.startScroll(0, getScrollY(), 0, mScreenHeight
							- dScrollY);
				}
			} else {
				if (-dScrollY < mScreenHeight / 3) {
					mScroller.startScroll(0, getScrollY(), 0, -dScrollY);
				} else {
					mScroller.startScroll(0, getScrollY(), 0, -mScreenHeight
							- dScrollY);
				}
			}
			break;
		}
		postInvalidate();
		return true;
	}

	@Override
	public void computeScroll() {
		super.computeScroll();
		if (mScroller.computeScrollOffset()) {
			scrollTo(0, mScroller.getCurrY());
		}
	}
}

运行结果:



事件拦截机制分析
要了解事件拦截机制,首先要了解一下触摸事件。
触摸事件就是捕获触摸屏幕后产生的时间。当点击一个按钮时,通过会产生两个或者三个时间---按钮按下,这是事件一;如果不小心滑动了,这是事件二;当手抬起,这是事件三。
触摸事件其实就是一个动作类型加坐标而已,但是Android的View结构是属性结构的,也就是说,View可以放在ViewGroup里面,通过不同的组合来实现不同的样式。
那么问题来了,View放在一个ViewGroup里面,这个ViewGroup又放在另一个ViewGroup里面,甚至还有可能继续嵌套,一层层地叠起来。可是触摸事件就一个到底分给谁?同一个时间,子View和父ViewGroup都有可能想要进行处理。因此就产生了“事件拦截”这个“霸气”的称呼。
我们定义两个ViewGroup,两个都重写以下方法:

@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		Log.e("MyView", "MyViewGroupA-dispatchTouchEvent:"+ev.getAction());
		return super.dispatchTouchEvent(ev);
	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		Log.e("MyView", "MyViewGroupA-onInterceptTouchEvent:"+ev.getAction());
		return super.onInterceptTouchEvent(ev);
	}
	
	
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.e("MyView", "MyViewGroupA-onTouchEvent:"+event.getAction());
		return super.onTouchEvent(event);
	}

定义一个View,重写以下方法

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		Log.e("MyView", "MyView-onTouchEvent:"+event.getAction());
		return super.onTouchEvent(event);
	}
	
	
	@Override
	public boolean dispatchTouchEvent(MotionEvent event) {
		Log.e("MyView", "MyView-dispatchTouchEvent:"+event.getAction());
		return super.dispatchTouchEvent(event);
	}

布局情况:

<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"
    tools:context="com.itman.customviewdemo.intercept.MyViewActivity" >

    <com.itman.customviewdemo.intercept.MyViewGroupA
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <com.itman.customviewdemo.intercept.MyViewGroupB
            android:layout_width="300dp"
            android:layout_height="300dp" >

            <com.itman.customviewdemo.intercept.MyView
                android:layout_width="100dp"
                android:layout_height="100dp" />
        </com.itman.customviewdemo.intercept.MyViewGroupB>
    </com.itman.customviewdemo.intercept.MyViewGroupA>

</RelativeLayout>

正常的情况下,时间的传递顺序是:
ViewGroupA->ViewGroupB->View
事件传递的时候,先执行dispatchTouchEvent()方法,再执行onInterceptTouchEvent()方法。


事件的处理顺序是:
View->ViewGroupB->ViewGroupA
事件处理都是执行onTouchEvent()方法。


事件传递的返回值非常容易理解:true,拦截,不继续;false不拦截,继续流程。
时间处理的返回值也类似:true,处理了,不用审核;false,继续执行。
初始情况下,返回值都是false。

默认情况下运行,打印的Log如下:


把MyViewGroupA的onInterceptTouchEvent(MotionEvent ev)返回值改为true,打印的Log如下:


把MyViewGroupB的onTouchEvent(MotionEvent event)返回值修改为true,打印Log如下:


源码下载

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值