Android SwitchButton(滑动开关)

@RemoteView
public class Button extends TextView {  public Button(Context context) {   this(context, null);  }  public Button(Context context, AttributeSet attrs) {   this(context, attrs, com.android.internal.R.attr.buttonStyle);  }  public Button(Context context, AttributeSet attrs, int defStyle) {   super(context, attrs, defStyle);  }  @Override  public void onInitializeAccessibilityEvent(AccessibilityEvent event) {   super.onInitializeAccessibilityEvent(event);   event.setClassName(Button. class.getName());  }  @Override  public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {   super.onInitializeAccessibilityNodeInfo(info);   info.setClassName(Button. class.getName());  } }

  是直接继承于TextView,所不同的是在构造方法中添加了Button的样式,并且在初始化可见性方面交由Button类自己来处理。虽然Button的实现比较简单,但是它的子类并不是这样。看一下:


  直接子类只有有一个,CompoundButton。它是一个抽象类,而实现这个类的控件正是 CheckBoxRadioButtonSwitchToggleButton 这四个,所以先重点说一下它。源码如下:

  
/**
 * <p>
 * A button with two states, checked and unchecked. When the button is pressed
 * or clicked, the state changes automatically.
 * </p>
 *
 * <p><strong>XML attributes </strong></p>
 * <p>
 * See {@link android.R.styleable#CompoundButton
 * CompoundButton Attributes}, {@link android.R.styleable#Button Button
 * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link * android.R.styleable #View View Attributes} * </p> */ public abstract class CompoundButton extends Button implements Checkable {  private boolean mChecked ;  private int mButtonResource ;  private boolean mBroadcasting ;  private Drawable mButtonDrawable;  private OnCheckedChangeListener mOnCheckedChangeListener;  private OnCheckedChangeListener mOnCheckedChangeWidgetListener ;  private static final int[] CHECKED_STATE_SET = {   R.attr.state_checked  };  public CompoundButton(Context context) {   this(context, null);  }  public CompoundButton(Context context, AttributeSet attrs) {   this(context, attrs, 0);  }  public CompoundButton(Context context, AttributeSet attrs, int defStyle) {   super(context, attrs, defStyle);   TypedArray a =     context.obtainStyledAttributes(       attrs, com.android.internal.R.styleable.CompoundButton, defStyle, 0);   Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button);   if (d != null ) {    setButtonDrawable(d);   }   boolean checked = a     .getBoolean(com.android.internal.R.styleable.CompoundButton_checked, false);   setChecked(checked);   a.recycle();  }  public void toggle() {   setChecked(! mChecked);  }  @Override  public boolean performClick() {   /*   * XXX: These are tiny, need some surrounding 'expanded touch area',   * which will need to be implemented in Button if we only override   * performClick()   */   /* When clicked, toggle the state */   toggle();   return super .performClick();  }  @ViewDebug.ExportedProperty  public boolean isChecked() {   return mChecked ;  }  /**  * <p>Changes the checked state of this button.</p>  *  * @param checked true to check the button, false to uncheck it  */  public void setChecked(boolean checked) {   if (mChecked != checked) {    mChecked = checked;    refreshDrawableState();    notifyViewAccessibilityStateChangedIfNeeded(      AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED );    // Avoid infinite recursions if setChecked() is called from a listener    if (mBroadcasting ) {     return;    }    mBroadcasting = true ;    if (mOnCheckedChangeListener != null) {     mOnCheckedChangeListener.onCheckedChanged(this, mChecked);    }   if (mOnCheckedChangeWidgetListener != null) { mOnCheckedChangeWidgetListener .onCheckedChanged(this, mChecked); } mBroadcasting = false ; } } /** * Register a callback to be invoked when the checked state of this button * changes. * * @param listener the callback to call on checked state change */ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { mOnCheckedChangeListener = listener; } /** * Register a callback to be invoked when the checked state of this button * changes. This callback is used for internal purpose only. * * @param listener the callback to call on checked state change * @hide */ void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) { mOnCheckedChangeWidgetListener = listener; } /** * Interface definition for a callback to be invoked when the checked state * of a compound button changed. */ public static interface OnCheckedChangeListener { /** * Called when the checked state of a compound button has changed. * * @param buttonView The compound button view whose state has changed. * @param isChecked The new checked state of buttonView. */ void onCheckedChanged(CompoundButton buttonView, boolean isChecked); } /** * Set the background to a given Drawable, identified by its resource id. * * @param resid the resource id of the drawable to use as the background */ public void setButtonDrawable(int resid) { if (resid != 0 && resid == mButtonResource ) { return; } mButtonResource = resid; Drawable d = null; if (mButtonResource != 0) { d = getResources().getDrawable(mButtonResource ); } setButtonDrawable(d); } /** * Set the background to a given Drawable * * @param d The Drawable to use as the background */ public void setButtonDrawable(Drawable d) { if (d != null ) { if (mButtonDrawable != null) { mButtonDrawable.setCallback(null); unscheduleDrawable( mButtonDrawable); } d.setCallback( this); d.setVisible(getVisibility() == VISIBLE, false); mButtonDrawable = d; setMinHeight(mButtonDrawable .getIntrinsicHeight()); } refreshDrawableState(); } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); event.setClassName(CompoundButton.class .getName()); event.setChecked( mChecked); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(info); info.setClassName(CompoundButton.class .getName()); info.setCheckable( true); info.setChecked( mChecked); } @Override public int getCompoundPaddingLeft() { int padding = super.getCompoundPaddingLeft(); if (!isLayoutRtl()) { final Drawable buttonDrawable = mButtonDrawable; if (buttonDrawable != null) { padding += buttonDrawable.getIntrinsicWidth(); } } return padding; } @Override public int getCompoundPaddingRight() { int padding = super.getCompoundPaddingRight(); if (isLayoutRtl()) { final Drawable buttonDrawable = mButtonDrawable; if (buttonDrawable != null) { padding += buttonDrawable.getIntrinsicWidth(); } } return padding; } /** * @hide */ @Override public int getHorizontalOffsetForDrawables() { final Drawable buttonDrawable = mButtonDrawable ; return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); final Drawable buttonDrawable = mButtonDrawable ; if (buttonDrawable != null) { final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK ; final int drawableHeight = buttonDrawable.getIntrinsicHeight(); final int drawableWidth = buttonDrawable.getIntrinsicWidth(); int top = 0; switch (verticalGravity) { case Gravity.BOTTOM : top = getHeight() - drawableHeight; break; case Gravity.CENTER_VERTICAL : top = (getHeight() - drawableHeight) / 2; break; } int bottom = top + drawableHeight; int left = isLayoutRtl() ? getWidth() - drawableWidth : 0; int right = isLayoutRtl() ? getWidth() : drawableWidth; buttonDrawable.setBounds(left, top, right, bottom); buttonDrawable.draw(canvas); } } @Override protected int[] onCreateDrawableState(int extraSpace) { final int [] drawableState = super.onCreateDrawableState(extraSpace + 1); if (isChecked()) { mergeDrawableStates(drawableState, CHECKED_STATE_SET); } return drawableState; } @Override protected void drawableStateChanged() { super.drawableStateChanged(); if (mButtonDrawable != null) { int[] myDrawableState = getDrawableState(); // Set the state of the Drawable mButtonDrawable.setState(myDrawableState); invalidate(); } } @Override protected boolean verifyDrawable(Drawable who) { return super .verifyDrawable(who) || who == mButtonDrawable; } @Override public void jumpDrawablesToCurrentState() { super.jumpDrawablesToCurrentState(); if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState(); } static class SavedState extends BaseSavedState { boolean checked ; /** * Constructor called from {@link CompoundButton#onSaveInstanceState()} */ SavedState(Parcelable superState) { super(superState); } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); checked = (Boolean)in.readValue( null); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeValue( checked); } @Override public String toString() { return "CompoundButton.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " checked=" + checked + "}" ; } public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } @Override public Parcelable onSaveInstanceState() { // Force our ancestor class to save its state setFreezesText( true); Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss. checked = isChecked(); return ss; } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); setChecked(ss. checked); requestLayout(); } }
   先从构造方法开始,在构造方法中,
public CompoundButton(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);

  TypedArray a =
   context.obtainStyledAttributes(    attrs, com.android.internal.R.styleable.CompoundButton, defStyle, 0);  Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button);  if (d != null ) {  setButtonDrawable(d);  }  boolean checked = a   .getBoolean(com.android.internal.R.styleable.CompoundButton_checked, false);  setChecked(checked);  a.recycle(); }
   先是从attrs中读取定义的属性,一个是Drawable用于设置背景;一个是布尔类型变量用于判断是否check过。设置背景使用的是setButtonDrawable()方法,代码如下:
/**
   * Set the background to a given Drawable
   *
   * @param d The Drawable to use as the background  */  public void setButtonDrawable(Drawable d) {   if (d != null ) {    if (mButtonDrawable != null) {     mButtonDrawable.setCallback(null);     unscheduleDrawable( mButtonDrawable);    }    d.setCallback( this);    d.setVisible(getVisibility() == VISIBLE, false);    mButtonDrawable = d;    setMinHeight(mButtonDrawable .getIntrinsicHeight());   }   refreshDrawableState();  }
  这个方法写的就比较完善,可以作为一个学习的典范。首先判断传递过来的Drawable是否为空,如果不为空并且默认的Drawable也不为空,那么取消默认Drawable的callback,然后调用 unscheduleDrawable 方法。这个方法代码如下:
/**
  * Unschedule any events associated with the given Drawable.  This can be
  * used when selecting a new Drawable into a view, so that the previous
  * one is completely unscheduled.
 *  * @param who The Drawable to unschedule.  *  * @see #drawableStateChanged  */ public void unscheduleDrawable(Drawable who) {  if (mAttachInfo != null && who != null) {   mAttachInfo.mViewRootImpl .mChoreographer.removeCallbacks(     Choreographer.CALLBACK_ANIMATION, null, who);  } }

   从方法注释中可以看出它的用途,正是更换Drawable时候使用的。接下来开始重新设置Drawable,包括回调、可见性、最小高度。最后调用 refreshDrawableState() 方法,这个是View类的方法,用于更新Drawable状态。

  然后再回过头看一下 setChecked (checked) 方法,这个用于设置check,也就是button的点击状态。代码如下:

/**
   * <p>Changes the checked state of this button.</p>
   *
   * @param checked true to check the button, false to uncheck it  */  public void setChecked( boolean checked) {   if (mChecked != checked) {    mChecked = checked;    refreshDrawableState();    notifyViewAccessibilityStateChangedIfNeeded(      AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED );    // Avoid infinite recursions if setChecked() is called from a listener    if (mBroadcasting ) {     return;    }    mBroadcasting = true ;    if (mOnCheckedChangeListener != null) {     mOnCheckedChangeListener.onCheckedChanged(this, mChecked);    }    if (mOnCheckedChangeWidgetListener != null) {     mOnCheckedChangeWidgetListener .onCheckedChanged(this, mChecked);    }    mBroadcasting = false ;   }  }

  在这个方法中多出了一个接口,这个接口真是check的一个回调接口,代码如下:

/**
     * Interface definition for a callback to be invoked when the checked state
     * of a compound button changed.
     */
    public static interface OnCheckedChangeListener { /** * Called when the checked state of a compound button has changed. * * @param buttonView The compound button view whose state has changed. * @param isChecked The new checked state of buttonView. */ void onCheckedChanged(CompoundButton buttonView, boolean isChecked); }

  这种回调接口在Android中处处可见,之前的文章也有介绍过。但是在上面的方法,它使用了一个mBroadcasting变量,进而巧妙地避免了重复递归的问题,大家自己感受一下。

  然后就是ondraw()方法了,把之前的drawable画出来。代码如下:

@Override
  protected void onDraw(Canvas canvas) {
  super.onDraw(canvas);   final Drawable buttonDrawable = mButtonDrawable ;   if (buttonDrawable != null) {    final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK ;    final int drawableHeight = buttonDrawable.getIntrinsicHeight();    final int drawableWidth = buttonDrawable.getIntrinsicWidth();    int top = 0;    switch (verticalGravity) {     case Gravity.BOTTOM :      top = getHeight() - drawableHeight;      break;     case Gravity.CENTER_VERTICAL :      top = (getHeight() - drawableHeight) / 2;      break;    }    int bottom = top + drawableHeight;    int left = isLayoutRtl() ? getWidth() - drawableWidth : 0;    int right = isLayoutRtl() ? getWidth() : drawableWidth;    buttonDrawable.setBounds(left, top, right, bottom);    buttonDrawable.draw(canvas);   }  }

  看得出来,在onDrawable()方法中,最主要的部分还是如何确定上下左右四个参数。确定完后就可以画出来了。但是,CompoundButton是一个抽象类,并不能直接使用,那看一下它的子类是如何实现的:

1、CheckBox
public class CheckBox extends CompoundButton {  public CheckBox(Context context) {   this(context, null);  }  public CheckBox(Context context, AttributeSet attrs) {   this(context, attrs, com.android.internal.R.attr.checkboxStyle);  }  public CheckBox(Context context, AttributeSet attrs, int defStyle) {   super(context, attrs, defStyle);  }  @Override  public void onInitializeAccessibilityEvent(AccessibilityEvent event) {   super.onInitializeAccessibilityEvent(event);   event.setClassName(CheckBox. class.getName());  }  @Override  public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {   super.onInitializeAccessibilityNodeInfo(info);   info.setClassName(CheckBox. class.getName());  } }

  和Button的实现差不多,使用了一个自己的样式。并且也是重写了那两个方法。再来看一下RadioButton,

public class RadioButton extends CompoundButton {  public RadioButton(Context context) {   this(context, null);  }  public RadioButton(Context context, AttributeSet attrs) {   this(context, attrs, com.android.internal.R.attr.radioButtonStyle);  }  public RadioButton(Context context, AttributeSet attrs, int defStyle) {   super(context, attrs, defStyle);  }  /**  * {@inheritDoc}  * <p>  * If the radio button is already checked, this method will not toggle the radio button.  */  @Override  public void toggle() {   // we override to prevent toggle when the radio is already   // checked (as opposed to check boxes widgets)   if (!isChecked()) {    super.toggle();   }  }  @Override  public void onInitializeAccessibilityEvent(AccessibilityEvent event) {   super.onInitializeAccessibilityEvent(event);   event.setClassName(RadioButton. class.getName());  }  @Override  public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {   super.onInitializeAccessibilityNodeInfo(info);   info.setClassName(RadioButton. class.getName());  } }

   和CheckBox实现差不多,区别在于多重写了一个方法,用于防止按钮被重复点击。另外还有ToggleButton以及Switch,前者实现也比较简单,后者稍微麻烦了一些,感兴趣可以自己分析。

  最后切入正题,看看滑动Button要如何实现呢?首先看一下效果图:

 图1-1
  
 图1-2

  图1-1所示的滑动Button实现的思路是这样的,背景图片有开和关的文字,一个按钮在其上面左右滑动,遮住相应的部分,使其在一个位置时候只能看到一个开关。


  如图1-3,在实现的时候,先画一个开关背景图片只,然后在其上面画一个按钮,滑动开关的时候对上面的按钮进行处理即可。

  准备:
    1、按钮图片
       
      

    2、背景图片       

       
 编码:

    在自定义滑动按钮控件的时候,可以有多种选择,可以继承于Button,也可以继承于Button的子类,也可以继承于View类等。我们知道滑动按钮是 一个很简单的控件,就是左右滑动改变显示内容,不需要其他的额外东西在里面,所以直接继承于View来实现即可。如果继承于系统的一些控件,那么有很多东 西用不到,会造成浪费。

    1、定义一个类继承于View,初始化构造方法,在构造方法中加载图片及其信息。

    2、重写onMeasure()方法,计算控件的大小。

    3、重写onTouchEvent()方法,对滑动事件进行判别处理。

    4、定义接口,实现回调。

    5、重写onDraw()方法,动态画出按钮。

代码如下:
/**
*
*/
package com.kince.slidebutton;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.View; /** * @author kince * @category 左右手势滑动button * @serial 1.0.0 * @since 2014.5.17 * @see http://blog.csdn.net/wangjinyu501 * */ public class SlideButton extends View {  private Bitmap slideBitMap;// 滑动图片  private Bitmap switchBitMap;// 背景图片  private int slideBitMapWidth;// 滑动图片宽度  private int switchBitMapWidth;// 背景图片宽度  private int switchBitMapHeight;// 背景图片高度  private boolean currentState;// 开关状态  private boolean isSliding = false; // 是否正在滑动中  private int currentX; // 当前开关的位置  private OnToggleStateChangedListener mChangedListener;// 回调接口  /**  * @param context  * 在java代码中直接调用使用此构造方法  */  public SlideButton(Context context) {   this(context, null);   // TODO Auto-generated constructor stub  }  /**  * @param context  * @param attrs  * 在xml中使用要用到这个方法  */  public SlideButton(Context context, AttributeSet attrs) {   this(context, attrs, 0);   // TODO Auto-generated constructor stub  }  /**  * @param context  * @param attrs  * @param defStyleAttr  * 指定一个样式  */  public SlideButton(Context context, AttributeSet attrs, int defStyleAttr) {   super(context, attrs, defStyleAttr);   initBitmap();  }  /**  * @category 加载背景图片以及开关图片 然后获取各自的宽高  *  */  private void initBitmap() {   // TODO Auto-generated method stub   slideBitMap = BitmapFactory.decodeResource(getResources(),     R.drawable.slide_button_background);   switchBitMap = BitmapFactory.decodeResource(getResources(),     R.drawable.switch_background);   slideBitMapWidth = slideBitMap.getWidth();   switchBitMapWidth = switchBitMap.getWidth();   switchBitMapHeight = switchBitMap.getHeight();   Log.i("switchBitMapWidth", switchBitMapWidth + "");  }  @Override  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {   // TODO Auto-generated method stub   super.onMeasure(widthMeasureSpec, heightMeasureSpec);   setMeasuredDimension(switchBitMapWidth, switchBitMapHeight);// 设置控件的宽高  }  @Override  protected void onDraw(Canvas canvas) {   // 绘制button背景图片   canvas.drawBitmap(switchBitMap, 0, 0, null);   // 绘制滑动开关   if (isSliding) {// 如果当前状态是滑动中 则动态绘制开关    int dis = currentX - slideBitMapWidth / 2;    if (dis < 0) {     dis = 0;   } else if (dis > switchBitMapWidth - slideBitMapWidth) { dis = switchBitMapWidth - slideBitMapWidth; } canvas.drawBitmap(slideBitMap, dis, 0, null); } else { if (currentState) { // 绘制开关为开的状态 canvas.drawBitmap(slideBitMap, switchBitMapWidth - slideBitMapWidth, 0, null); } else { // 绘制开关为关的状态 canvas.drawBitmap(slideBitMap, 0, 0, null); } } super.onDraw(canvas); } @Override public boolean onTouchEvent(MotionEvent event) { // 手势识别 判断滑动方向 int action = event.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: isSliding = true; currentX = (int) event.getX(); break; case MotionEvent.ACTION_MOVE: currentX = (int) event.getX(); Log.i("currentX", currentX + ""); break; case MotionEvent.ACTION_UP: isSliding = false; int bgCenter = switchBitMapWidth / 2; boolean state = currentX > bgCenter; // 改变后的状态 if (state != currentState && mChangedListener != null) {// 添加回调 mChangedListener.onToggleStateChanged(state); } currentState = state; break; default: break; } invalidate(); return true; } public OnToggleStateChangedListener getmChangedListener() { return mChangedListener; } public void setmChangedListener( OnToggleStateChangedListener mChangedListener) { this.mChangedListener = mChangedListener; } public boolean isToggleState() { return currentState; } public void setToggleState(boolean currentState) { this.currentState = currentState; } }
  回调接口,
package com.kince.slidebutton;

/**
 * @author kince
 *
 */
public interface OnToggleStateChangedListener { /** * @category * @param state */ public void onToggleStateChanged(boolean state); }
  Activity代码,
package com.kince.slidebutton;

import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.ActionBar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import android.os.Build; public class MainActivity extends ActionBarActivity {  @Override  protected void onCreate(Bundle savedInstanceState) {   super.onCreate(savedInstanceState);   setContentView(R.layout.activity_main);   if (savedInstanceState == null) {    getSupportFragmentManager().beginTransaction()      .add(R.id.container, new PlaceholderFragment()).commit();   }  }  @Override  public boolean onCreateOptionsMenu(Menu menu) {   // Inflate the menu; this adds items to the action bar if it is present.   getMenuInflater().inflate(R.menu.main, menu);   return true;  }  @Override  public boolean onOptionsItemSelected(MenuItem item) {   // Handle action bar item clicks here. The action bar will   // automatically handle clicks on the Home/Up button, so long   // as you specify a parent activity in AndroidManifest.xml.   int id = item.getItemId();   if (id == R.id.action_settings) {    return true;   }   return super.onOptionsItemSelected(item);  }  /**  * A placeholder fragment containing a simple view.  */  public static class PlaceholderFragment extends Fragment implements    OnToggleStateChangedListener {   private SlideButton slidebutton;   public PlaceholderFragment() {   }   @Override   public View onCreateView(LayoutInflater inflater, ViewGroup container,     Bundle savedInstanceState) {    View rootView = inflater.inflate(R.layout.fragment_main, container,      false);    slidebutton = (SlideButton) rootView.findViewById(R.id.slidebutton1);    // 设置一下开关的状态    slidebutton.setToggleState(true); // 设置开关的状态为打开    slidebutton.setmChangedListener(this);    return rootView;   }   @Override   public void onToggleStateChanged(boolean state) {    // TODO Auto-generated method stub    FragmentActivity activity = getActivity();    if (state) {     Toast.makeText(activity, "开关打开", 0).show();    } else {     Toast.makeText(activity, "开关关闭", 0).show();    }   }  } }

  未完待续。

转载于:https://www.cnblogs.com/Free-Thinker/p/4013826.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值