通俗易懂的自定义view详解

自定义View的分类

1、继承View

当我们需要实现的效果是一个不规则效果的时候,那么这时就需要继承 View 来实现了,我们需要重写 onDraw 方法,在该方法里实现各种不规则的图形和效果。当我们使用这种方式的时候,需要自己去处理 warp_content 和 padding。

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
 
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
 
        int width = 0;
        int height =0;
        if(widthMode == MeasureSpec.EXACTLY) {
            width = widthSize;
        }else {
            //widthMode == MeasureSpec.AT_MOST模式 自己设置控件宽度
            //当是wrap_content或者给具体dp的时候会走这里
            width = mRadius * 2 +  getPaddingRight() + getPaddingLeft();
        }
        if(heightMode == MeasureSpec.EXACTLY) {
            height = heightSize;
        }else {
            height = mRadius * 2 + getPaddingTop() + getPaddingBottom();
        }
        //注意最后 调用这个方法 让属性生效
        setMeasuredDimension(width,height);
    }

2、继承ViewGroup

当系统所提供的 LinearLayout、FrameLayout 等布局控件无法满足我们的需求时,这时我们就需要使用这种方式来实现自己想要的布局效果了。当我们使用这种方式的时候,需要重写 onLayout 方法来对子 View 进行布局,以及测量本身和子 View 宽高,还需要处理本身的 padding 和子 View 的 margin。

3、继承已有View

当我们需要基于已有的 View 进行扩展或修改的时候,那么就可以使用这种方式。比如说,我们需要一个圆角的 ImageView,那么这时就可以继承 ImageView 进行修改了。当我们使用这种方式的时候,一般不需要自己去处理 wrap_content 和 padding 等,因为系统控件已经帮我们做好了。

4、继承已有布局

这种方式也叫做:自定义组合 View。该方式比较简单,当我们需要将一组 View 组合在一起,方便后期复用的时候,就可以使用该方法。当我们使用这种方式的时候,不需要去处理 ViewGroup 的测量和布局流程,因为系统控件已经帮我们做好了。

view的绘制流程

每一个视图的绘制过程都必须经历三个最主要的阶段,即onMeasure()、onLayout()和onDraw()

1)、Activity的attach 方法里创建PhoneWindow。

2)、onCreate方法里的 setContentView 会调用PhoneWindow的setContentView方法,创建DecorView并且把xml布局解析然后添加到DecorView中。

3)、在onResume方法执行后,会创建ViewRootImpl,它是最顶级的View,是DecorView的parent,创建之后会调用setView方法。

4)、ViewRootImpl 的 setView方法,会将PhoneWindow添加到WMS中,通过 Session作为媒介。setView方法里面会调用requestLayout,发起绘制请求。

5)、requestLayout 一旦发起,最终会调用 performTraversals 方法,里面将会调用View的三个measure、layout、draw方法,其中View的draw 方法需要一个传一个Canvas参数。

6)、最后通过relayoutWindow 方法将Surface跟当前Window绑定,通过Surface的lockCanvas方法获取Surface的的Canvas,然后View的绘制就通过这个Canvas,最后通过Surface的unlockCanvasAndPost 方法提交绘制的数据,最终将绘制的数据交给SurfaceFlinger去提交给屏幕显示。

一、onMeasure()

onMeasure()是用于测量视图的大小的。

1、View系统的绘制流程会从ViewRootImpl的performTraversals()方法中开始,在其内部调用performMeasure()方法,
2、它里面又调用的View的measure()方法。

measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec【它俩是int类型32位,前2位表示specMode,后30位表示specSize,这么做是为了减少对象的创建】,这两个值分别用于确定视图的宽度和高度的规格和大小。

MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格。

specMode一共有三种类型

  1. EXACTLY==MATCH_PARENT
  2. AT_MOST==WRAP_CONTENT
  3. UNSPECIFIED

widthMeasureSpec和heightMeasureSpec这两个值,通常情况下,都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。但是最外层的根视图,它的widthMeasureSpec和heightMeasureSpec又是从哪里得到的呢?

/*****desiredWindowWidth是window的宽,lp是WindowManager.LayoutParams***/
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
/*****getRootMeasureSpec()*****/
private int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

由以上代码可以看出
1)我们需要自己去处理 warp_content 和 padding。因为无论是match_parent还是warp_content ,size都是windowSize。总是充满全屏的。

2)根视图的widthMeasureSpec和heightMeasureSpec就是window的宽高

3、measure中调用onMeasure(widthMeasureSpec, heightMeasureSpec)方法

这里才是真正去测量并设置View大小的地方,默认会调用getDefaultSize()方法来获取视图的大小

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

可以看到mBackground == null 为没有设置背景,那么返回mMinWidth ,也就是android:minWidth 这个属性所指定的值,这个值可以是0 ;如果View 设置了背景,则返回mMinWidth 与背景的最小宽度这两者的最大值。

getSuggestedMinimumWidth() 的返回值就是View 在UNSPECIFIED 情况下的测量宽。

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }

调用setMeasuredDimension()方法来设定测量出的大小,这样一次measure过程就结束了。

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
    case MeasureSpec.UNSPECIFIED:
        result = size;
        break;
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

如果是ViewGroup,ViewGroup中定义了一个measureChildren()方法来去测量子视图的大小

首先会去遍历当前布局下的所有子视图,然后逐个调用measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)方法来测量相应子视图的大小

然后分别调用了getChildMeasureSpec()方法来去计算子视图的width/heightMeasureSpec

最后调用子视图的measure()方法,并把计算出的width/heightMeasureSpec传递进去,之后的流程就和前面所介绍的一样了

需要注意的是,在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。

二、onLayout()

measure过程结束后,视图的大小就已经测量好了,接下来就是layout的过程了。这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewRootImpl的performTraversals()方法会在measure结束后继续执行,并调用View的layout()方法来执行此过程

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);

layout()方法接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。可以看到,这里还把刚才测量出的宽度和高度传到了layout()方法中。

在layout()方法中,首先会调用setFrame()方法来判断视图的大小是否发生过变化,以确定有没有必要对当前的视图进行重绘,同时还会在这里把传递过来的四个参数分别赋值给mLeft、mTop、mRight和mBottom这几个变量。接下来会调用onLayout()方法

View中的onLayout()方法就是一个空方法,因为onLayout()过程是为了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图(ViewGroup)决定子视图的显示位置

ViewGroup中的onLayout()方法竟然是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法

/**如果你想改变View显示的位置,只需要改变childView.layout()方法的四个参数就行了**/
@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		if (getChildCount() > 0) {
			View childView = getChildAt(0);
			childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
		}
	}

问:getWidth()方法和getMeasureWidth()方法到底有什么区别?
答:首先getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。

//getWidth()=childView.getMeasuredWidth()-0 == childView.getMeasuredWidth()
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
//getWidth()=200-0 != childView.getMeasuredWidth()
childView.layout(0, 0, 200, 200);

三、onDraw()

measure和layout的过程都结束后,接下来就进入到draw的过程了。这里才真正地开始对视图进行绘制。ViewRootImpl中会继续执行并创建出一个Canvas对象,然后调用View的draw()方法并传递过来Canvas,来执行具体的绘制工作

在draw()中去调用了一下onDraw()方法,那么onDraw()方法里又写了什么代码呢?进去一看你会发现,原来又是个空方法啊。其实也可以理解,因为每个视图的内容部分肯定都是各不相同的,这部分的功能交给子类来去实现也是理所当然的。

然后对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码。

view重绘invalidate()

view的invalidate()—>view的invalidateInternal()—>ViewParent接口的invalidateChild()—>ViewGroup的invalidateChild()【因为ViewGroup实现了ViewParent接口】—>ViewRootImpl的invalidateChildInParent()—>最终在scheduleTraversals()方法中postCallback()

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

执行doTraversal();

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

又重新执行了performTraversals();

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
			//重点在这
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }
invalidate与postinvalidate、requestLayout区别

invalidate
view调用invalidate将导致当前view的重绘(draw调用),view的父类将不会执行draw方法;viewGroup调用invalidate会使viewGroup的子view调用draw,也就是viewGroup内部的子view进行重绘;

postinvalidate
在非UI线程调用,将非UI线程切换到UI线程,最后也是调用invalidate

requestLayout
只会导致当前view的measure和layout,而draw不一定被执行,只有当view的位置发生改变才会执行draw方法,因此如果要使当前view重绘需要调用invalidate

自定义view支付输入框

1、继承EditView,首先画一个圆角矩形
2、再画五个竖线
3、在画圆点,按输入的字符长度画
在这里插入图片描述

@SuppressLint("AppCompatCustomView")
public class PayEditTextView extends EditText {

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

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

    private int maxCount = 6;
    private int borderColor = Color.GRAY;
    private int circleColor = Color.BLACK;
    private Paint mPaint;
    private Paint borderPaint;
    private Paint divideLinePaint;
    private Paint circlePaint;
    private RectF rectF = new RectF();

    private void initView() {
        setBackgroundColor(Color.TRANSPARENT);//将背景变透明,去掉EditView的下横线
        setCursorVisible(false);//隐藏光标

        setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxCount)});//设置输入过滤器,输入的最大长度
    }


    private void initPaint() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);

        borderPaint = getPaint(3, Paint.Style.STROKE, borderColor);
        divideLinePaint = getPaint(3, Paint.Style.FILL, borderColor);
        circlePaint = getPaint(5, Paint.Style.FILL, circleColor);

    }

    /**
     * 设置画笔
     *
     * @param strokeWidth 画笔宽度
     * @param style       画笔风格
     * @param color       画笔颜色
     * @return
     */
    private Paint getPaint(int strokeWidth, Paint.Style style, int color) {
        Paint paint = new Paint(ANTI_ALIAS_FLAG);
        paint.setStrokeWidth(strokeWidth);
        paint.setStyle(style);
        paint.setColor(color);
        paint.setAntiAlias(true);

        return paint;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //super.onDraw(canvas);//禁止EditView的默认实现

        rectF.left = 0;
        rectF.top = 0;
        rectF.right = getWidth();
        rectF.bottom = getHeight();
        canvas.drawRoundRect(rectF, 12, 12, borderPaint);

        int divideLineWStartX = getWidth() / maxCount;
        for (int i = 0; i < maxCount - 1; i++) {
            canvas.drawLine((i + 1) * divideLineWStartX, 0, (i + 1) * divideLineWStartX, getHeight(), divideLinePaint);
        }

        int startX = getWidth() / maxCount / 2;
        LogU.Companion.d("textLength==" + textLength);
        for (int i = 1; i <= textLength; i++) {
            canvas.drawCircle(getWidth() / maxCount * i - startX, getHeight() / 2, getHeight() / 4, circlePaint);
        }

    }

    private int textLength = 0;

    @Override
    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);
        textLength = text.toString().length();
    }
}

自定义view,button提交动画

主要是通过ValueAnimator.addUpdateListener监听动画执行过程改变值来重绘view的大小、样式

ValueAnimator.addUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                two_circle_distance = (int) animation.getAnimatedValue();
                //default_two_circle_distance=(w-h)/2,意思是椭圆变成圆的所需的总值
                int alpha = 255 - (two_circle_distance * 255) / default_two_circle_distance;
                textPaint.setAlpha(alpha);

                invalidate();
            }
        });

在这里插入图片描述

public class AnimationButton extends View {

    /**
     * view的宽度
     */
    private int width;
    /**
     * view的高度
     */
    private int height;
    /**
     * 圆角半径
     */
    private int circleAngle;
    /**
     * 默认两圆圆心之间的距离=需要移动的距离
     */
    private int default_two_circle_distance;
    /**
     * 两圆圆心之间的距离
     */
    private int two_circle_distance;
    /**
     * 背景颜色
     */
    private int bg_color = 0xffbc7d53;
    /**
     * 按钮文字字符串
     */
    private String buttonString = "确认完成";
    /**
     * 动画执行时间
     */
    private int duration = 1000;
    /**
     * view向上移动距离
     */
    private int move_distance = 300;

    /**
     * 圆角矩形画笔
     */
    private Paint paint;
    /**
     * 文字画笔
     */
    private Paint textPaint;
    /**
     * 对勾(√)画笔
     */
    private Paint okPaint;
    /**
     * 文字绘制所在矩形
     */
    private Rect textRect = new Rect();

    /**
     * 动画集
     */
    private AnimatorSet animatorSet = new AnimatorSet();

    /**
     * 矩形到圆角矩形过度的动画
     */
    private ValueAnimator animator_rect_to_angle;
    /**
     * 矩形到正方形过度的动画
     */
    private ValueAnimator animator_rect_to_square;
    /**
     * view上移的动画
     */
    private ObjectAnimator animator_move_to_up;
    /**
     * 绘制对勾(√)的动画
     */
    private ValueAnimator animator_draw_ok;

    /**
     * 是否开始绘制对勾
     */
    private boolean startDrawOk = false;

    /**
     * 根据view的大小设置成矩形
     */
    private RectF rectf = new RectF();

    /**
     * 路径--用来获取对勾的路径
     */
    private Path path = new Path();
    /**
     * 取路径的长度
     */
    private PathMeasure pathMeasure;
    /**
     * 对路径处理实现绘制动画效果
     */
    private PathEffect effect;
   

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

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

    public AnimationButton(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        LogU.Companion.d("AnimationButton");
        initPaint();

        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                start();
            }
        });
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                reset();
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
    }

    /**
     * 初始化所有动画
     */
    private void initAnimation() {
        set_rect_to_angle_animation();
        set_rect_to_circle_animation();
        set_move_to_up_animation();
        set_draw_ok_animation();

        animatorSet
                .play(animator_move_to_up)
                .before(animator_draw_ok)//在play动画之后执行
                .after(animator_rect_to_square)//在play动画之前执行
                .after(animator_rect_to_angle);//在play动画之前执行

    }


    /**
     * 设置矩形过度到圆角矩形的动画
     */
    private void set_rect_to_angle_animation() {
        animator_rect_to_angle = ValueAnimator.ofInt(0, height / 2);
        animator_rect_to_angle.setDuration(duration);
        animator_rect_to_angle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                circleAngle = (int) animation.getAnimatedValue();
                invalidate();
            }

        });

    }


    /**
     * 设置圆角矩形过度到圆的动画
     */
    private void set_rect_to_circle_animation() {
        animator_rect_to_square = ValueAnimator.ofInt(0, default_two_circle_distance);
        animator_rect_to_square.setDuration(duration);
        animator_rect_to_square.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                two_circle_distance = (int) animation.getAnimatedValue();
                //default_two_circle_distance=(w-h)/2,意思是椭圆变成圆的所需的总值
                int alpha = 255 - (two_circle_distance * 255) / default_two_circle_distance;
                textPaint.setAlpha(alpha);

                invalidate();
            }
        });

    }


    /**
     * 设置view上移的动画
     */
    private void set_move_to_up_animation() {
        final float curTranslationY = this.getTranslationY();
        animator_move_to_up = ObjectAnimator.ofFloat(this, "translationY", curTranslationY, curTranslationY - move_distance);
        animator_move_to_up.setDuration(duration);
        animator_move_to_up.setInterpolator(new AccelerateDecelerateInterpolator());
    }

    /**
     * 绘制对勾的动画
     */
    private void set_draw_ok_animation() {
        animator_draw_ok = ValueAnimator.ofFloat(1, 0);
        animator_draw_ok.setDuration(duration);
        animator_draw_ok.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                startDrawOk = true;
                float value = (Float) animation.getAnimatedValue();

                effect = new DashPathEffect(new float[]{pathMeasure.getLength(), pathMeasure.getLength()}, value * pathMeasure.getLength());
                okPaint.setPathEffect(effect);
                invalidate();
            }
        });
    }

    private void initPaint() {

        paint = new Paint();
        paint.setStrokeWidth(4);
        paint.setStyle(Paint.Style.FILL);
        paint.setAntiAlias(true);
        paint.setColor(bg_color);

        textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        textPaint.setTextSize(40);
        textPaint.setColor(Color.WHITE);
        textPaint.setTextAlign(Paint.Align.CENTER);
        textPaint.setAntiAlias(true);

        okPaint = new Paint();
        okPaint.setStrokeWidth(10);
        okPaint.setStyle(Paint.Style.STROKE);
        okPaint.setAntiAlias(true);
        okPaint.setColor(Color.WHITE);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        LogU.Companion.d("onSizeChanged");
        width = w;
        height = h;

        default_two_circle_distance = (w - h) / 2;

        initOk();
        initAnimation();

    }
    private int mWidth;
    private int mHeight;
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode=MeasureSpec.getMode(widthMeasureSpec);
        int heightMode=MeasureSpec.getMode(heightMeasureSpec);
        int widthSize=MeasureSpec.getSize(widthMeasureSpec);
        int heightSize=MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode == MeasureSpec.EXACTLY) {
            mWidth=widthSize;
        }else {
            mWidth=80*2+getPaddingLeft()+getPaddingRight();
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            mHeight=heightSize;
        }else {
            mHeight=80*2+getPaddingTop()+getPaddingBottom();
        }

        setMeasuredDimension(mWidth,mHeight);
    }

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

        draw_oval_to_circle(canvas);
        drawText(canvas);

        if (startDrawOk) {
            canvas.drawPath(path, okPaint);
        }

    }


    /**
     * 绘制长方形变成圆形
     *
     * @param canvas 画布
     */
    private void draw_oval_to_circle(Canvas canvas) {
        rectf.left = two_circle_distance;
        rectf.top = 0;
        rectf.right = width - two_circle_distance;
        rectf.bottom = height;

        //画圆角矩形
        canvas.drawRoundRect(rectf, circleAngle, circleAngle, paint);

    }


    /**
     * 绘制文字
     *
     * @param canvas 画布
     */
    private void drawText(Canvas canvas) {
        textRect.left = 0;
        textRect.top = 0;
        textRect.right = width;
        textRect.bottom = height;
        Paint.FontMetricsInt fontMetrics = textPaint.getFontMetricsInt();
        int baseline = (textRect.bottom + textRect.top - fontMetrics.bottom - fontMetrics.top) / 2;
        //文字绘制到整个布局的中心位置
        canvas.drawText(buttonString, textRect.centerX(), baseline, textPaint);
    }

    /**
     * 绘制对勾
     */
    private void initOk() {
        //对勾的路径
        path.moveTo(default_two_circle_distance + height / 8 * 3, height / 2);
        path.lineTo(default_two_circle_distance + height / 2, height / 5 * 3);
        path.lineTo(default_two_circle_distance + height / 3 * 2, height / 5 * 2);

        pathMeasure = new PathMeasure(path, true);

    }

    /**
     * 启动动画
     */
    public void start() {
        animatorSet.start();
    }

    /**
     * 动画还原
     */
    public void reset() {
//        startDrawOk = false;
//        circleAngle = 0;
//        two_circle_distance = 0;
//        default_two_circle_distance = (width - height) / 2;
//        textPaint.setAlpha(255);
//        setTranslationY(getTranslationY() + move_distance);
//        invalidate();
    }
    
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
支持向量机(Support Vector Machine,SVM)是一种常用的机器学习算法,常用于分类和回归问题。其原理相对来说较为复杂,但可以通过以下通俗易懂的方式进行解释: SVM的基本思想是找到一个最佳的超平面将不同类别的数据完全分开。换句话说,我们要找到一个决策边界,使得属于某一类的样本点都在一个侧面,而属于另一类的样本点则在另一侧面。 为了找到这个最佳的超平面,SVM引入了支持向量的概念。支持向量是离决策边界最近的那些样本点,它们对于决策边界的确定起到关键作用。这些支持向量点到决策边界的距离被称为间隔,我们的目标是找到最大化间隔的决策边界。 但是,在实际问题中,数据往往无法完全线性分开。为解决这个问题,SVM引入了核函数的概念。核函数将数据从原始空间映射到更高维的特征空间,在特征空间中,原本线性不可分的问题可能变得线性可分。 基于核函数的SVM即通过在特征空间中寻找最佳的超平面来解决非线性问题。通过设置不同的核函数,我们可以将数据映射到不同的特征空间,并得到不同的决策边界。 SVM的训练过程可以通过求解一个凸优化问题来实现。优化问题的目标是最大化间隔,同时要求决策边界的分类结果尽量准确。通过使用相关的数学方法,可以求解出最优的超平面以及对应的支持向量。 总之,SVM是一种通过找到最佳的超平面来实现分类的机器学习算法。它的原理简单概括为找到一条决策边界,使得离这条边界最近的样本点都被正确分类,并且间隔最大化。通过引入核函数,SVM也可以解决非线性分类问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值