自定义view的基本知识

这篇文章介绍了 自定义view的一些基础知识…个人总结的…

先给个效果图(没啥别的意思…本来就是练手用的…)
效果图

package com.diandou.demo41_cycle;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

/**
 * Created by baiya on 2018/2/24.
 */

public class MyView extends View {

    private static final String TAG = "mm_MyView";

    /** 当前控件的宽 */
    private int mWidthSize = 1000;
    /** 当前控件的高 */
    private int mHeightSize = 1000;
    /** 普通画笔 */
    private Paint mPaint;
    /** 文字画笔 */
    private TextPaint mTextPaint;
    /** 路径path对象(很强大) */
    private Path mPath;

    /**
     * 当我们 new MyView() 的时候调用...
     * @param context
     */
    public MyView(Context context) {
        this(context, null);
    }

    /**
     * 当我们 在xml中写这个自定义view的话会调用
     * @param context
     * @param attrs
     */
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs, 0);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        Log.d(TAG, "init");

        /** 画笔(一般都是在构造方法中初始化滴....) */
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);/**设置画画笔颜色*/
        mPaint.setStrokeWidth(10);/**设置画笔宽*/
        /**
         * Paint.Style
         * 1, Paint.Style.STROKE 描边
         * 2, Paint.Style.FILL   填充
         * 3, Paint.Style.FILL_AND_STROKE 描边+填充
         * */
        mPaint.setStyle(Paint.Style.STROKE);

        /**
         * 这个是阴影对象, (很强大, 可以实现渐变等多种效果)
         */
//        Shader shader = new Shader();
//        mPaint.setShader(shader);


        /** 文字画笔 */
        mTextPaint = new TextPaint();
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setStrokeWidth(2);
        mTextPaint.setStyle(Paint.Style.STROKE);/** 这个也有问题 */
        mTextPaint.setTextSize(100);/** 文字大小, 文字的大小是按像素的 */
        /**
         * 设置文字的偏移量
         * 文字的偏移量默认是left 就是 文本 左下角 的坐标点....
         * */
        mTextPaint.setTextAlign(Paint.Align.LEFT);


        /**
         * path是一个很强大的对象 + 上paint 可以绘制出任意图形:
         * 1, 贝塞尔曲线
         * 2, 圆
         * 3, 矩形
         * 4, 不规则图像
         */
        mPath = new Path();
        mPath.moveTo(100, 100);/** 第一个点 */
        mPath.lineTo(400, 400);/** 第二个 */
        mPath.lineTo(600, 200);/** 第三个 */


    }


    /**
     * 在子view中重写onLayout方法是没有大用的其实
     *
     * 因为子view中没有子view了, 它不是一个父容器..
     *
     * onLayout 方法在viewGroup中详解...
     *
     * @param changed
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.d(TAG, "onLayout");
//        super.onLayout(changed, left, top, right, bottom);
    }

    /**
     * onMeasure方法被调用的时机:
     *
     *
     * 在View里,有个mParent的变量。这个变量其实就是ViewRootImpl. 所以在调用View的requestFitSystemWindows, requestLayout, invalidateChildInParent时候,都会调用measure方法。
     * (也就是说onMeasure会被它的父布局调用)
     * 好吧, 具体的我也没弄清楚...
     *
     *
     *
     *
     * onMeasure方法... 测量
     * MeasureSpce的mode有三种:EXACTLY, AT_MOST,UNSPECIFIED
     * 当父布局是EXACTLY时,子控件确定大小或者match_parent,mode都是EXACTLY,子控件是wrap_content时,mode为AT_MOST;
     * 当父布局是AT_MOST时,子控件确定大小,mode为EXACTLY,子控件wrap_content或者match_parent时,mode为AT_MOST.
     * 所以在确定控件大小时,需要判断MeasureSpec的mode,不能直接用MeasureSpec的size。
     * 在进行一些逻辑处理以后,调用setMeasureDimension()方法,将测量得到的宽高传进去供layout使用。
     *
     * EXACTLY : view确定大小, 有明确的值
     *      有明确的值情况: 父控件大小确定, 子控件match_parent
     *                    父控件大小也不确定, 但是子控件宽高 有明确值,
     *
     * AT_MOST : view不确定大小, 没有明确值
     *      没有明确值的情况: 父控件大小不确定 , 子控件match_parent
     *                      父控件大小确定, 但是子控件是wrap_content 也没有明确值
     *
     * UNSPECIFIED: 这个不知道有什么用-_-///(一般不会是这种模式)
     *
     * 需要注意的是:
     *
     *      测量所得的宽高不一定是最后展示的宽高,最后宽高确定是在onLayout方法里,
     *      layou(left,top,right,bottom),不过一般都是一样的。
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.d(TAG, "onMeasure");
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        Log.d(TAG, "onMeasure" + " widthSize--" + widthSize + " heightSize--" + heightSize);

        int measuredHeight, measuredWidth;

        if (widthMode == MeasureSpec.EXACTLY){
            measuredWidth = mWidthSize;
        } else {
            //如果能用到这种情况, 那就是不确定大小咯
            measuredWidth = mHeightSize;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            measuredHeight = heightSize;
        } else {
            measuredHeight = mWidthSize;
        }

        /**
         * 在 setMeasuredDimension 这个方法执行之后,
         * 调用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,
         * 否则得到的结果是0
         */
        setMeasuredDimension(measuredWidth, measuredHeight);
    }


    /**
     * onDraw 方法... 绘制
     *
     * 使用Paint画笔在canvas画布上绘制想要实现的内容...
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        Log.d(TAG, "onDraw");
//        super.onDraw(canvas);
        /**
         * 画一条线  参数: x1, y1, x2, y2, 画笔
         *                  坐标   坐标
         */
        canvas.drawLine(0,0,1000,1000, mPaint);
        /**
         * 平移画布...
         */
//        canvas.translate(500,500);
        /**
         * 画一个圆 参数: x1, y1, r, 画笔
         *                 圆心  半径
         */
        canvas.drawCircle(500, 500, 500, mPaint);

        /**
         * 画一段文字 参数: 文本 x1, y1, 文本画笔
         *                      文本左下角(和文字偏移量有关)
         */
        canvas.drawText("这是一段文字", 100, 100, mTextPaint);

        /**
         * 矩形的类
         * 构造参数: left 100, top 100, right 700, bottom 700
         */
        Rect rect = new Rect(100, 100, 700, 700);
        canvas.drawRect(rect, mPaint);

        /**
         * 和上个一样
         */
        RectF rectF = new RectF(200, 200, 600, 600);
        mPaint.setColor(Color.WHITE);
        canvas.drawRect(rectF, mPaint);

        /**
         * path 很强大就是了 具体在搜博客
         */
        canvas.drawPath(mPath, mPaint);



    }



}

ViewGroup下的代码

package com.diandou.demo41_cycle;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by baiya on 2018/2/24.
 */

public class MyViewGroup extends ViewGroup {

    private static final String TAG = "mm_MyViewGroup";

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

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

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        Log.d(TAG, "init");
    }

    /**
     * 给子view排版用到的方法..
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     * 以上4个参数是在父容器的宽高, 同时也是给子view的宽高
     *
     *
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "onLayout " + l+" "+ t+" "+ r+" "+ b+" ");
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            /** 子view */
            View childView = getChildAt(i);
            /**
             * 发现了一个神奇的现象,
             * 当我重写了onLayout用来排版子view的话, 此时, childView的onMeasure就不执行了
             * */
            childView.layout(r/2-500, 100, r/2+500, 1100);

        }



    }

    @Override
    protected void onDraw(Canvas canvas) {
        Log.d(TAG, "onDraw");
        super.onDraw(canvas);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.d(TAG, "onMeasure");
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    }
}

activity中的代码:

package com.diandou.demo41_cycle;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "mm_MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "setContentView before");
        setContentView(R.layout.activity_main);
        Log.d(TAG, "setContentView after");


    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }
}

打印的log信息

02-24 18:06:36.517 7596-7596/com.diandou.demo41_cycle D/mm_MainActivity: setContentView before
02-24 18:06:36.583 7596-7596/com.diandou.demo41_cycle D/mm_MyViewGroup: init
02-24 18:06:36.587 7596-7596/com.diandou.demo41_cycle D/mm_MyView: init
02-24 18:06:36.587 7596-7596/com.diandou.demo41_cycle D/mm_MainActivity: setContentView after
02-24 18:06:36.587 7596-7596/com.diandou.demo41_cycle D/mm_MainActivity: onStart
02-24 18:06:36.588 7596-7596/com.diandou.demo41_cycle D/mm_MainActivity: onResume
02-24 18:06:36.632 7596-7596/com.diandou.demo41_cycle D/mm_MyViewGroup: onMeasure
02-24 18:06:36.662 7596-7596/com.diandou.demo41_cycle D/mm_MyViewGroup: onMeasure
02-24 18:06:36.663 7596-7596/com.diandou.demo41_cycle D/mm_MyViewGroup: onLayout 0 0 1080 1536 
02-24 18:06:36.663 7596-7596/com.diandou.demo41_cycle D/mm_MyView: onLayout
02-24 18:06:36.686 7596-7596/com.diandou.demo41_cycle D/mm_MyViewGroup: onDraw
02-24 18:06:36.686 7596-7596/com.diandou.demo41_cycle D/mm_MyView: onDraw

如果用到自定义属性的话(我个人一般不用, 写起来麻烦, 直接在代码里写不好么…):

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyView">
        <attr name="background_color" format="color"/>
        <attr name="size" format="dimension"/>
    </declare-styleable>
</resources>
  <com.diandou.demo41_cycle.MyView
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:background="@color/colorAccent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:background_color="@color/colorPrimary"
        app:size="24dp"
        />
 public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView,defStyleAttr, R.style.AppTheme);

        custom_size = a.getDimensionPixelSize(R.styleable.MyView_size, 5);
        custon_background = a.getColor(R.styleable.MyView_background_color, Color.BLACK);

        a.recycle();
        init();
    }

一个简单的动画:
小圆点可滑动(滑动是一条直线...复杂的我暂时没做出来...)

package com.diandou.demo41_cycle;

import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.DecelerateInterpolator;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * Created by baiya on 2018/2/24.
 */

public class MyView extends View {

    private static final String TAG = "mm_MyView";

    /**
     * 当前控件的宽
     */
    private int mWidthSize = 1000;
    /**
     * 当前控件的高
     */
    private int mHeightSize = 1000;
    /**
     * 普通画笔
     */
    private Paint mPaint;
    /**
     * 文字画笔
     */
    private TextPaint mTextPaint;
    /**
     * 路径path对象(很强大)
     */
    private Path mPath;
    private int custom_size;
    private int custon_background;

    private ValueAnimator animator;

    /**
     * 当我们 new MyView() 的时候调用...
     *
     * @param context
     */
    public MyView(Context context) {
        this(context, null);
    }

    /**
     * 当我们 在xml中写这个自定义view的话会调用
     *
     * @param context
     * @param attrs
     */
    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, R.style.AppTheme);

        custom_size = a.getDimensionPixelSize(R.styleable.MyView_size, 5);
        custon_background = a.getColor(R.styleable.MyView_background_color, Color.BLACK);

        a.recycle();
        init();
    }


    /**
     * 初始化
     */
    private void init() {
        Log.d(TAG, "init");

        /** 画笔(一般都是在构造方法中初始化滴....) */
        mPaint = new Paint();
        mPaint.setColor(custon_background);/**设置画画笔颜色*/
        mPaint.setStrokeWidth(custom_size);/**设置画笔宽*/
        /**
         * Paint.Style
         * 1, Paint.Style.STROKE 描边
         * 2, Paint.Style.FILL   填充
         * 3, Paint.Style.FILL_AND_STROKE 描边+填充
         * */
        mPaint.setStyle(Paint.Style.STROKE);

        /**
         * 这个是阴影对象, (很强大, 可以实现渐变等多种效果)
         */
//        Shader shader = new Shader();
//        mPaint.setShader(shader);


        /** 文字画笔 */
        mTextPaint = new TextPaint();
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setStrokeWidth(2);
        mTextPaint.setStyle(Paint.Style.STROKE);/** 这个也有问题 */
        mTextPaint.setTextSize(100);/** 文字大小, 文字的大小是按像素的 */
        /**
         * 设置文字的偏移量
         * 文字的偏移量默认是left 就是 文本 左下角 的坐标点....
         * */
        mTextPaint.setTextAlign(Paint.Align.LEFT);


        /**
         * path是一个很强大的对象 + 上paint 可以绘制出任意图形:
         * 1, 贝塞尔曲线
         * 2, 圆
         * 3, 矩形
         * 4, 不规则图像
         */
        mPath = new Path();
        mPath.moveTo(100, 100);/** 第一个点 */
        mPath.lineTo(400, 400);/** 第二个 */
        mPath.lineTo(700, 100);/** 第三个 */
        mPath.lineTo(700, 700);/** 第四个 */


//        Point startPoint = new Point(100, 400, 700, 700, 100, 400, 100, 700, 1);
//        startPoint.setType(1);
//        Point endPoint = new Point(100, 400, 700, 700, 100, 400, 100, 700, 1);
//        endPoint.setType(2);


        Point startPoint = new Point(100, 100);
        Point endPoint = new Point(800, 800);

        animator = ValueAnimator
                .ofObject(new PathEvaluator(), startPoint, endPoint);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                mPoint = ((Point) animation.getAnimatedValue());
//                LogUtil.i(mPoint.getX()+"------"+mPoint.getY());
                invalidate();
            }
        });
        animator.setDuration(5000);
        animator.start();
//
//        ObjectAnimator anim = ObjectAnimator.ofObject(this, "fab", new PathEvaluator(), path.getPoints().toArray());
//        anim.setInterpolator(new DecelerateInterpolator());//动画插值器
//        anim.setDuration(3000);
//        anim.start();


    }


    /**
     * 在子view中重写onLayout方法是没有大用的其实
     * <p>
     * 因为子view中没有子view了, 它不是一个父容器..
     * <p>
     * onLayout 方法在viewGroup中详解...
     *
     * @param changed
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        Log.d(TAG, "onLayout");
//        super.onLayout(changed, left, top, right, bottom);
    }

    /**
     * onMeasure方法被调用的时机:
     * <p>
     * <p>
     * 在View里,有个mParent的变量。这个变量其实就是ViewRootImpl. 所以在调用View的requestFitSystemWindows, requestLayout, invalidateChildInParent时候,都会调用measure方法。
     * (也就是说onMeasure会被它的父布局调用)
     * 好吧, 具体的我也没弄清楚...
     * <p>
     * <p>
     * <p>
     * <p>
     * onMeasure方法... 测量
     * MeasureSpce的mode有三种:EXACTLY, AT_MOST,UNSPECIFIED
     * 当父布局是EXACTLY时,子控件确定大小或者match_parent,mode都是EXACTLY,子控件是wrap_content时,mode为AT_MOST;
     * 当父布局是AT_MOST时,子控件确定大小,mode为EXACTLY,子控件wrap_content或者match_parent时,mode为AT_MOST.
     * 所以在确定控件大小时,需要判断MeasureSpec的mode,不能直接用MeasureSpec的size。
     * 在进行一些逻辑处理以后,调用setMeasureDimension()方法,将测量得到的宽高传进去供layout使用。
     * <p>
     * EXACTLY : view确定大小, 有明确的值
     * 有明确的值情况: 父控件大小确定, 子控件match_parent
     * 父控件大小也不确定, 但是子控件宽高 有明确值,
     * <p>
     * AT_MOST : view不确定大小, 没有明确值
     * 没有明确值的情况: 父控件大小不确定 , 子控件match_parent
     * 父控件大小确定, 但是子控件是wrap_content 也没有明确值
     * <p>
     * UNSPECIFIED: 这个不知道有什么用-_-///(一般不会是这种模式)
     * <p>
     * 需要注意的是:
     * <p>
     * 测量所得的宽高不一定是最后展示的宽高,最后宽高确定是在onLayout方法里,
     * layou(left,top,right,bottom),不过一般都是一样的。
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.d(TAG, "onMeasure");
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        Log.d(TAG, "onMeasure" + " widthSize--" + widthSize + " heightSize--" + heightSize);

        int measuredHeight, measuredWidth;

        if (widthMode == MeasureSpec.EXACTLY) {
            measuredWidth = mWidthSize;
        } else {
            //如果能用到这种情况, 那就是不确定大小咯
            measuredWidth = mHeightSize;
        }

        if (heightMode == MeasureSpec.EXACTLY) {
            measuredHeight = heightSize;
        } else {
            measuredHeight = mWidthSize;
        }

        /**
         * 在 setMeasuredDimension 这个方法执行之后,
         * 调用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,
         * 否则得到的结果是0
         */
        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    /**
     * onDraw 方法... 绘制
     * <p>
     * 使用Paint画笔在canvas画布上绘制想要实现的内容...
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        Log.d(TAG, "onDraw");
//        /**
//         * 画一条线  参数: x1, y1, x2, y2, 画笔
//         *                  坐标   坐标
//         */
//        canvas.drawLine(0,0,1000,1000, mPaint);
//        /**
//         * 平移画布...
//         */
        canvas.translate(500,500);
//        /**
//         * 画一个圆 参数: x1, y1, r, 画笔
//         *                 圆心  半径
//         */
//        canvas.drawCircle(500, 500, 500, mPaint);
//
//        /**
//         * 画一段文字 参数: 文本 x1, y1, 文本画笔
//         *                      文本左下角(和文字偏移量有关)
//         */
//        canvas.drawText("这是一段文字", 100, 100, mTextPaint);
//
//        /**
//         * 矩形的类
//         * 构造参数: left 100, top 100, right 700, bottom 700
//         */
//        Rect rect = new Rect(100, 100, 700, 700);
//        canvas.drawRect(rect, mPaint);
//
//        /**
//         * 和上个一样
//         */
//        RectF rectF = new RectF(200, 200, 600, 600);
//        mPaint.setColor(Color.WHITE);
//        canvas.drawRect(rectF, mPaint);
//
//        /**
//         * path 很强大就是了 具体在搜博客
//         */


        canvas.drawPath(mPath, mPaint);

//        mPath.moveTo(100, 100);/** 第一个点 */
//        mPath.lineTo(400, 400);/** 第二个 */
//        mPath.lineTo(700, 100);/** 第三个 */
//        mPath.lineTo(700, 700);/

        canvas.drawCircle(mPoint.getX(), mPoint.getY(), 50, mPaint);


    }


    private Point mPoint;

    class Point {
        float x;
        float y;

        public Point(float x, float y) {
            this.x = x;
            this.y = y;
        }

        public float getX() {
            return x;
        }

        public void setX(float x) {
            this.x = x;
        }

        public float getY() {
            return y;
        }

        public void setY(float y) {
            this.y = y;
        }
    }

    class PathEvaluator implements TypeEvaluator<Point> {

        @Override
        public Point evaluate(float fraction, Point startValue, Point endValue) {

            float x = fraction * (endValue.getX() - startValue.getX());
            float y = fraction * (endValue.getY() - startValue.getY());

            return new Point(x, y);
        }
    }

}

自己写的贝塞尔曲线代码:
https://www.jianshu.com/p/5ad720c77053
自定义view需要用到渐变颜色的时候(Paint.setShader):
http://blog.csdn.net/iispring/article/details/50500106
圆环上的圆点可随指尖移动
https://www.jianshu.com/p/94d858130e71
自定义view的事件分发机制
https://www.jianshu.com/p/17e3df1a1d6a
requestLayout, invalidate和postInvalidate的异同
https://www.jianshu.com/p/5c11b42ad7d0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源】:包含前端、后端、移动开发、操作系统、人工智能、物联网、信息化管理、数据库、硬件开发、大数据、课程资源、音视频、网站开发等各种技术项目的源码。包括STM32、ESP8266、PHP、QT、Linux、iOS、C++、Java、MATLAB、python、web、C#、EDA、proteus、RTOS等项目的源码。 【项目质量】:所有源码都经过严格测试,可以直接运行。功能在确认正常工作后才上传。 【适用人群】:适用于希望学习不同技术领域的小白或进阶学习者。可作为毕设项目、课程设计、大作业、工程实训或初期项目立项。 【附加价值】:项目具有较高的学习借鉴价值,也可直接拿来修改复刻。对于有一定基础或热衷于研究的人来说,可以在这些基础代码上进行修改和扩展,实现其他功能。 【沟通交流】:有任何使用上的问题,欢迎随时与博主沟通,博主及时解答。鼓励下载和使用,并欢迎大家互相学习,共同进步。【项目资源
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值