Android开发之初识自定义控件

开发的时候,因为业务需求或者封装需要,我们会进行自定义控件。

说在前面,本篇涉及到一些东西

  • onMeasure
  • onLayout
  • onDraw
  • MeasureSpec (32位二进制数,头两位模式(Mode),后两位大小(Size))
  • onFinishInflate
    * ViewGroup的getViewAt 方法
  • MeasureSpec的makeMeasureSpec方法,getSize,getMode,和AT_MOST、EXACTLY、UNSPECIFIFD三个常量。
  • getMeasuredWidth和getMeasuredHeight

一、控件的简单分类

简单粗暴来说,控件可以分为2种,View和ViewGroup

View 单独的控件,里面不能存放控件,继承自View。比如Imageview Button TextView。

                                                        TextView继承自View.png

ViewGroup 能存放控件的容器,比如FrameLayout、RelativeLayout 和LinearLayout等。

                                                   FrameLayout继承自ViewGroup.png

二、自定义控件的分类

自定义控件,类型各有各的分法,本人的对其进行大概如下分类:

  • 自制控件 : 该自定义控件继承自View或者ViewGroup,自己绘制。
  • 组合控件 : 利用系统已提供的控件,组合成一个新的控件
  • 拓展控件 : 继承自系统已提供的控件并且加上新的功能或者特性。

      我们就开始从自制控件说起。

三、自制控件

如果用最简单接地气的语言来描述自制控件,那么就是

1、继承自View或者ViewGroup
2、利用 onMeasure(测量)、onLayout(摆放)和onDraw(绘制) 三大步骤弄来弄去把View搞出来。(三者不必同时用到)
3、在利用事件的触发机制等调一调onTouchEvent等方法监听一下,然后利用Scroller或者ViewDragHelper等做做动画之类,合适怎么整就怎么整。
4、写写接口,调调回调,该谁干活谁就干活。

说成一句话:继承自View或者ViewGroup然后测量摆放绘制,然后做做动画,最后写写接口给调用者调用。

小二,来一个最简单的自制控件吧

1、 需要先认识的简单知识

提示:如果觉得琐碎可以直接从三.2的实例看起,最后再回过头来看这个三.1的内容。效果可能更佳。

我们的ViewGroup继承自View,在View里面有这么几个重要方法

首先三个我们已经提到的

1.1、measure和onMeasure(),layout()和onLayout(),draw()和onDraw()

  • measure() View的测量的入口,辗转调用真正工作的onMeasure()
  • layout() View的摆放的入口,辗转调用真正工作的onLayout()
  • draw() View的绘制的入口,辗转调用真正工作的onDraw()

      其中,measure确定View的宽高,layout确定View的四个顶点的位置,而draw将View绘制在屏幕上。
      measure()方法是final类型的方法,子类无法复写,而layout()和draw()子类就可以复写

  • onMeasure() 回调该方法对控件进行测量
  • onLayout() 回调该方法对控件位置进行摆放
  • onDraw() 回调该方法对控件进行绘制

measure和onMeasure()

关于measure(),需要分两种情况讨论,
情况1,只是View,那么通过View的measure() 可以完成对View的测量,而measure() 会去调用 onMeasure方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 setMeasuredDimension(
 getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
 getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

情况2,是ViewGroup,measure()除了完成对容器自身的测量之外,接着还会遍历去调用子元素(子元素可以使View或者ViewGroup)的measure(),各个子元素再去递归调用自身子元素的measure。注意是递归,递归,递归,重要的说三遍。
在ViewGroup的measure()会去调用onMeasure(),然后我们需要在onMeasure()里面对容器里的孩子进行 孩子.measure() 对孩子进行测量。

layout()和onLayout()
对于layout
如果是View,View调用layout可以指定View的位置
如果是ViewGroup,那么onLayout() 里面进行 孩子.layout 可以精确摆放孩子的位置

draw()和onDraw()

对于draw

这个就 比较简单了,他是按照如下几个流程走的
1、绘制背景 backgroud.draw(canvas)
2、绘制本身( onDraw )
3、绘制孩子( dispatchDraw )
4、绘制装饰 ( onDrawScrollBars )

其实实际开发中我们常用的,关心的也就是onDraw方法。

其实说了这么多,大概就是需要明白的是:

一个View的从无到有需要经过measure(),layout(),draw()三个步骤,measure()会辗转调用measure(),layout()会辗转调用onLayout,draw会转转调用onDraw().

如果是View,那么就写写onDraw,onLayout,至于onMeasure一般不需要。
如果是ViewGroup,那么些在onMeasure里面测量自身后接着进行对孩子的测量,在onLayout里面进行孩子的位置摆放。

1.2、onFinishInflate方法

我们知道,我们在Xml写的布局文件最终会在通过Pull解析的方式转成代码的。

onFinishInflate的作用,就是在xml加载组件完成后调用的。这个方法一般在自制ViewGroup的时候调用。

 /**
 * Finalize inflating a view from XML. This is called as the last phase
 * of inflation, after all child views have been added.
 *
 * <p>Even if the subclass overrides onFinishInflate, they should always be
 * sure to call the super method, so that we get called.
 */
 @CallSuper
 protected void onFinishInflate() {
 }

1.3、ViewGroup的 getChildAt(int position) 方法

Returns the view at the specified position in the group.
返回该组中指定位置的视图。

这个位置按照我们include的顺序排列,索引从0开始。

 /**
 * Returns the view at the specified position in the group.
 *
 * @param index the position at which to get the view from
 * @return the view at the specified position or null if the position
 * does not exist within the group
 */
 public View getChildAt(int index) {
 if (index < 0 || index >= mChildrenCount) {
 return null;
 }
 return mChildren[index];
 }

1.4 onMeasure和measure的参数

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 孩子.measure(32位的MeasureSpec宽,32位的MeasureSpec高);

 }

但看起来他们是一个int值,这两个参数可以是宽高的意思,但是不完全是,这宽和高都是32由 32位的二进制码组成的,并不是随随便便穿进去一个int类型的值就完事了的。

既然不能随随便便,那我们应该传什么?——我们应该传一个32位的二进制数。
那么我们计算出或者拿到这个数?—— 使用View类里面的MeasureSpec这个静态内部类。

1.5、 View类里面的静态内部类 MeasureSpec

我们首先需要看一个类,MeasureSpec

                                                                     MeasureSpec.png

从上图我们可以看到,MeasureSpec这个类给我们提供了4个方法和3个常量

makeMeasureSpec和32位的二进制int值

先看看几个方法

  • makeMeasureSpec(int size,int mode)
    返回值是int,我们就是用MeasureSpec的makeMeasureSpec方法制造32位的二进制数给measure(宽,高)这个方法。

其中,size占30位,mode占2位

这个方法的2个参数
size(大小)怎么指定?
mode怎么确定?

利用LayoutParams得到size

我们的View有多大谁知道?——我们在写xml的布局文件的时候就知道了有多大了。
但是怎么获取文件的在xml布局时的大小呢?
利用View的getLayoutParams()方法可以得到一个LayoutParams类型的值,利用
layoutParams.width 和
layoutParams.height
就可以获得View在布局时的宽高。

怎么确定mode

mode有3种模式,分别是EXACTLY:精确模式;AT_MOST最大值模式,UNSPECIFIED : 未指定的模式,

MeasureSpec.EXACTLY,精确模式
当我们的 layout_width 和 layout_height 设定为
填充父窗体 match_parent 或者
指定具体数值 比如 android:layout_width="130dp" 时
就可以采用EXACTLY这种模式

MeasureSpec.AT_MOST,最大值模式
当我们的 layout_width 和 layout_height 设定为 warp_content 时,控件大小随着内容大小的变化而变化,就是采用AT_MOST这种模式。

MeasureSpec.UNSPECIFIED,未指定模式
父容器没有给子布局任何限制,子布局可以任意大小。比较奇怪,少用。

1.6、getMeasuredWidth和getMeasuredHeight

getMeasuredWidth 获取测量后的宽度
getMeasuredHeight 获取测量后的高度

这两个如果孩子没有被measure有效地测量过,那么返回的值是0。

getMeasuredWidth

 /**
 * Like {@link #getMeasuredWidthAndState()}, but only returns the
 * raw width component (that is the result is masked by
 * {@link #MEASURED_SIZE_MASK}).
 *
 * @return The raw measured width of this view.
 */
 public final int getMeasuredWidth() {
 return mMeasuredWidth & MEASURED_SIZE_MASK;
 }

.
.
getMeasuredHeight

 /**
 * Like {@link #getMeasuredHeightAndState()}, but only returns the
 * raw width component (that is the result is masked by
 * {@link #MEASURED_SIZE_MASK}).
 *
 * @return The raw measured height of this view.
 */
 public final int getMeasuredHeight() {
 return mMeasuredHeight & MEASURED_SIZE_MASK;
 }

.
.

2、继承自View的简单demo

我们新建一个名字为MyDiyView 的类,当我们继承自View,会被系统要求写上构造方法。

2.1、重载构造方法

我们一般重载2个或者3个即可


                                                                   系统提示.png

构造方法

public class MyDiyView extends View {

 // java代码创建视图的时候被调用
 public MyDiyView(Context context) {
 super(context);
 }

 // xml创建视图并且没有指定StyleAttr是调用 至于attrs,是默认指定了的
 public MyDiyView(Context context, AttributeSet attrs) {
 super(context, attrs);
 }


 //xml创建视图并且指定StyleAttr是调用,一般不需需要
 public MyDiyView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 }

 // 参数多指定了一个,这个方法是API 21以上才有的,所以一般不需要
 public MyDiyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
 super(context, attrs, defStyleAttr, defStyleRes);
 }
}

只要有一个构造方法就不会报错了,一般我们会写上头两个构造方法。
为什么系统或有这样要求呢,因为这几个构造方法是必不可少的。根据参数的不同,其应用场合的也不同。我们在代码备注里面已经说得很清楚。

现在这个自定义VIew就能用了。只是当前是一片空白。

2.2、给view,鲜艳上色

一篇空白的View我们不需要,我们还是画上一点东西吧

对于自制控件来说,有三个方法是必须掌握的,分别是
onMeasure (测量)
onLayout (摆放)
onDraw (绘制)

我们当前既然是最简单,直接调用一下用于绘制的onDraw就好

复写一下onDraw方法,发现onDraw里面一个Canvas类型的参数可以拿来用

 @Override
 protected void onDraw(Canvas canvas) {
 super.onDraw(canvas);
 }

利用Canvas,我们可以画 线条,文字,图形,bitmap等等。我们先来画一个线条。(关于Canvas的相关知识网上很多,不了解的可以自行参考)

Canvas可以画出来很多东西


                                     Canvas可以画出来很多东西.png

我们画一条线看看

MyDiyView

public class MyDiyView extends View {

 private Paint paint;

 // java代码创建视图的时候被调用
 public MyDiyView(Context context) {
 super(context);
 paint = new Paint(); // 此处创建可避免多次创建对象,浪费内存
 }

 // xml创建视图并且没有指定StyleAttr是调用 至于attrs,是默认指定了的
 public MyDiyView(Context context, AttributeSet attrs) {
 super(context, attrs);
 paint = new Paint();
 }


 //xml创建视图并且指定StyleAttr是调用,一般不需需要
 public MyDiyView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 }

 @Override
 protected void onDraw(Canvas canvas) {
 //super.onDraw(canvas);

 paint.setColor(Color.BLUE);
 paint.setStrokeWidth(20); // 笔划大小
 // canvas 是帆布的意思,也可以理解为绘画板
 // 我们现在来画一条线
 /**
 public void drawLine (float startX, float startY, float stopX, float stopY, Paint paint)
 两点连成线
 startX和startY确定开始的点
 stopX和stopY确定结束的点
 */
 canvas.drawLine(30, 30, 120, 70, paint); // 利用canvas画一条线
 super.onDraw(canvas);

 }
}

顺便佐证下构造函数的作用

佐证1、利用第一个构造函数直接在Java代码创建出来视图

public class MainActivity extends Activity {

 private TextView mTv;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 //setContentView(R.layout.activity_main);
 setContentView(new MyDiyView(MainActivity.this));
 }
}

运行界面:


                     运行界面.png

如上已证

佐证2、在xml代码里面调用我们的自定义控件

在MainActivity的activity_main的xml布局里面利用MyDiyView的全路径名,引用我们的自定义控件

activity_main

<?xml version="1.0" encoding="utf-8"?>
<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:background="#ffffff"
 tools:context="com.amqr.simpleviewtest.MainActivity">

 <com.amqr.simpleviewtest.MyDiyView
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="Hello World!" />
</RelativeLayout>

MainActivity

public class MainActivity extends Activity {

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 }
}

运行,发现效果如上图的运行效果。

3、继承自ViewGroup的简单demo

3.1、 重载构造方法和复写onLayout

我们新建一个类,继承ViewGroup
发现,系统不单单要需要弄出来几个构造方法,还要求实现复写onLayout方法。这就不报错了。
其实这也可以理解,毕竟是可以存放孩子的容器嘛。

像下面这样就不错报了

public class MyDiyViewGroup extends ViewGroup{
 public MyDiyViewGroup(Context context) {
 super(context);
 }

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

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {

 }

}

3.2、把View塞进去ViewGroup

接下来我们怎么把View塞进ViewGroup里面呢?假设我们要塞进去的是两个View

1、新建两个xml布局文件,一个放上一个TextView,一个放上我们刚刚写好的自定义控件MyDiyView,在MainActivity的xml中的MyViewGroup里面include这两个布局

item_text

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:id="@+id/mLlDelete"
 android:layout_width="130dp"
 android:layout_height="60dp"
 android:background="#ff0000"
 android:gravity="center"
 android:orientation="vertical">

 <TextView
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="文本"
 android:textSize="22dp"
 android:textColor="#ffffff"
 />
</LinearLayout>

item_mdv

<?xml version="1.0" encoding="utf-8"?>
<com.amqr.simpleviewtest.MyDiyView
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="100dp"
 android:layout_height="60dp">

</com.amqr.simpleviewtest.MyDiyView>

MyViewGroup里面include这两个布局

<?xml version="1.0" encoding="utf-8"?>
<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:background="#ffffff"
 tools:context="com.amqr.simpleviewtest.MainActivity">

 <com.amqr.simpleviewtest.MyDiyViewGroup
 android:id="@+id/mMyDiyViewGroup"
 android:layout_width="match_parent"
 android:layout_height="match_parent">
 <!--这个include的顺序很重要,因为需要和onFinishInflate相结合-->

 <!--TextView,系统自带的-->
 <include layout="@layout/item_text"/>

 <!--自定义控件-->
 <include layout="@layout/item_mdv"/>
 </com.amqr.simpleviewtest.MyDiyViewGroup>
</RelativeLayout>

2、在onFinishInflate方法中使用getChild(int position)方法加载获取这两个View

public class MyDiyViewGroup extends ViewGroup{
 private View myDiyView;
 private View mViewText;

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

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

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {

 }

 @Override
 protected void onFinishInflate() {
 super.onFinishInflate();

 mViewText = getChildAt(0);
 myDiyView = getChildAt(1);
 }
}

到了这里总算把View塞进ViewGroup了。但是此时如果直接运行程序,我们会发现还看到什么效果。

3.3、把让进ViewGroup的View显示出来

这时候,我们就需要利用到onMeasure和onLayout了。

首先进行复写onMeasure,在onMeasure里面进行 孩子.measure(measureSpec的32位的宽,measureSpec的32位的高)

这个32的值的size利用 LayoutParams 得到
至于mode,因为我们xml都是指定大小的,所以用MeasureSpec.EXACTLY

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec);

 LayoutParams layoutParams = mViewText.getLayoutParams();

 int mViewTextWidth = MeasureSpec.makeMeasureSpec(layoutParams.width,MeasureSpec.EXACTLY);
 int mViewTextHeight = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
 mViewText.measure(mViewTextWidth,mViewTextHeight);

 LayoutParams lp = myDiyView.getLayoutParams();

 int myDiyViewWidth = MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.EXACTLY);
 int myDiyViewHeight = MeasureSpec.makeMeasureSpec(lp.height,MeasureSpec.EXACTLY);

 myDiyView.measure(myDiyViewWidth, myDiyViewHeight);

 }

.
.
在onLayout里面进行 孩子.layout(l,t,r,b)

利用 孩子View.getMeasuredWidth和孩子View.getMeasuredHeight获取的孩子的宽和高

因为之前我们已经对孩子进行 孩子View.measure(宽,高)了,所以此时调用getMeasuredWidth和getMeasuredHeight能够顺利获得孩子的宽和高。(如果没进行measure那么这两个获取的值就是0)

为什么不用 孩子View.getWidth() 和 孩子View.getHeight()?

因为在haiziView还没进行 孩子View.layout(l,t,r,b)之前,如果使用 孩子View.getWidth(),那么获取出来的值是0。

摆放孩子的位置

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {

 int mViewTextWidth = mViewText.getMeasuredWidth();
 int mViewTextHeight = mViewText.getMeasuredHeight();

 Log.d("VG", "mViewText.getMeasuredWidth(): " + mViewText.getMeasuredWidth());
 Log.d("VG", "mViewText.getMeasuredHeight(): " + mViewText.getMeasuredHeight());


 mViewText.layout(0,0,mViewTextWidth,mViewTextHeight);


 int myDiyViewWidth = myDiyView.getMeasuredWidth();
 int myDiyViewHeight = myDiyView.getMeasuredHeight();


 myDiyView.layout(mViewTextWidth, 0, mViewTextWidth + myDiyViewWidth, myDiyViewHeight);
 Log.d("VG", "layout之后 mViewText.getWidth(): " + mViewText.getWidth());

 }

此时运行程序,发现我们这个ViewGroup已经成功显示在屏幕上了。


          ViewGroup显示.png

附上完整的MyViewGroup代码

MyViewGroup

public class MyDiyViewGroup extends ViewGroup{

 private View myDiyView;
 private View mViewText;

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

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



 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 super.onMeasure(widthMeasureSpec, heightMeasureSpec);

 LayoutParams layoutParams = mViewText.getLayoutParams();

 int mViewTextWidth = MeasureSpec.makeMeasureSpec(layoutParams.width,MeasureSpec.EXACTLY);
 int mViewTextHeight = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
 mViewText.measure(mViewTextWidth,mViewTextHeight);

 LayoutParams lp = myDiyView.getLayoutParams();

 int myDiyViewWidth = MeasureSpec.makeMeasureSpec(lp.width,MeasureSpec.EXACTLY);
 int myDiyViewHeight = MeasureSpec.makeMeasureSpec(lp.height,MeasureSpec.EXACTLY);

 myDiyView.measure(myDiyViewWidth, myDiyViewHeight);
 Log.d("VG", "layout之前 mViewText.getWidth(): " + mViewText.getWidth());

 }

 @Override
 protected void onLayout(boolean changed, int l, int t, int r, int b) {

 int mViewTextWidth = mViewText.getMeasuredWidth();
 int mViewTextHeight = mViewText.getMeasuredHeight();

 Log.d("VG", "mViewText.getMeasuredWidth(): " + mViewText.getMeasuredWidth());
 Log.d("VG", "mViewText.getMeasuredHeight(): " + mViewText.getMeasuredHeight());


 mViewText.layout(0,0,mViewTextWidth,mViewTextHeight);


 int myDiyViewWidth = myDiyView.getMeasuredWidth();
 int myDiyViewHeight = myDiyView.getMeasuredHeight();


 myDiyView.layout(mViewTextWidth, 0, mViewTextWidth + myDiyViewWidth, myDiyViewHeight);
 Log.d("VG", "layout之后 mViewText.getWidth(): " + mViewText.getWidth());

 }


 @Override
 protected void onFinishInflate() {
 super.onFinishInflate();

 mViewText = getChildAt(0);
 myDiyView = getChildAt(1);
 }
}

MainActivity

public class MainActivity extends Activity {

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 }
}

activity_main

<?xml version="1.0" encoding="utf-8"?>
<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:background="#ffffff"
 tools:context="com.amqr.simpleviewtest.MainActivity">

 <com.amqr.simpleviewtest.MyDiyViewGroup
 android:id="@+id/mMyDiyViewGroup"
 android:layout_width="match_parent"
 android:layout_height="match_parent">
 <!--这个include的顺序很重要,因为需要和onFinishInflate相结合-->

 <!--TextView,系统自带的-->
 <include layout="@layout/item_text"/>

 <!--自定义控件-->
 <include layout="@layout/item_mdv"/>
 </com.amqr.simpleviewtest.MyDiyViewGroup>
</RelativeLayout>

.
.
最后附上证明在layout之前孩子的getWidth是无法获取宽度的log

02-29 02:40:45.952 5073-5073/? D/VG: layout之前 mViewText.getWidth(): 0
02-29 02:40:45.952 5073-5073/? D/VG: layout之前 mViewText.getWidth(): 0
02-29 02:40:46.024 5073-5073/? D/VG: mViewText.getMeasuredWidth(): 260
02-29 02:40:46.024 5073-5073/? D/VG: mViewText.getMeasuredHeight(): 120
02-29 02:40:46.024 5073-5073/? D/VG: layout之后 mViewText.getWidth(): 260
02-29 02:40:46.032 5073-5073/? D/VG: layout之前 mViewText.getWidth(): 260
02-29 02:40:46.036 5073-5073/? D/VG: layout之前 mViewText.getWidth(): 260
02-29 02:40:46.036 5073-5073/? D/VG: mViewText.getMeasuredWidth(): 260
02-29 02:40:46.036 5073-5073/? D/VG: mViewText.getMeasuredHeight(): 120
02-29 02:40:46.036 5073-5073/? D/VG: layout之后 mViewText.getWidth(): 260

关于最简单的自制控件到此结束。

  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是你的春哥!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值