声明:本文中使用的Demo的Git地址:https://github.com/NoClay/TestView.git

1.自定义View中需要知道的几个类

1.MeasureSpec


简述:作为一个尺寸类,将测量模式和尺寸合而为一,在我们自定义的控件,MeasureSpce的四种测量模式分别对应如下:


UNSPECIFIED --> 系统内部使用,要多大给多大,父容器对子View没有任何影响,要多大给多大


EXACTLY -->精确值,对应例如34dp,或者math_parent


AT_MOST -->父容器给定子View了一个可用大小的SpecSize, View的大小相当于wrap_content


public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
		
		//父容器不对View有任何限制,要多大给多大,一般用于系统内部
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;
		
		//父容器已经检测出View所需要的精确大小,这个时候view的最终大小就是指定的SpecSize的值
		//对应于使用match_parent和具体的数值如37dp这种
        public static final int EXACTLY     = 1 << MODE_SHIFT;
		
		//父容器指定了一个可用大小的SpecSize,View的大小不能大于这个值,相当于wrap_content
        public static final int AT_MOST     = 2 << MODE_SHIFT;
		
		//获取一个SpecSize的模式
        @MeasureSpecMode
        public static int getMode(int measureSpec) ;
		//获取一个SpecSize的大小
        public static int getSize(int measureSpec) ;
		//对一个SpecSize的大小进行调整
        static int adjust(int measureSpec, int delta);
    }





2.Paint


简述:Paint即画笔,在绘图过程中起到了极其重要的作用,画笔主要保存了颜色, 样式等绘制信息,指定了如何绘制文本和图形,画笔对象有很多设置方法,  大体上可以分为两类,一类与图形绘制相关Paint,一类与文本绘制相关TextPaint。相当于我们常用的ps软件中的画笔,我们可以对其设置许多东西,比如画笔颜色、抗锯齿、画笔粗细、画笔线形等



/**  

     * Paint类介绍  

     *   

     * Paint即画笔,在绘图过程中起到了极其重要的作用,画笔主要保存了颜色,  

     * 样式等绘制信息,指定了如何绘制文本和图形,画笔对象有很多设置方法,  

     * 大体上可以分为两类,一类与图形绘制相关,一类与文本绘制相关。         

     *   

     * 1.图形绘制  

     * setARGB(int a,int r,int g,int b);  

     * 设置绘制的颜色,a代表透明度,r,g,b代表颜色值。  

     *   

     * setAlpha(int a);  

     * 设置绘制图形的透明度。  

     *   

     * setColor(int color);  

     * 设置绘制的颜色,使用颜色值来表示,该颜色值包括透明度和RGB颜色。  

     *   

    * setAntiAlias(boolean aa);  

     * 设置是否使用抗锯齿功能,会消耗较大资源,绘制图形速度会变慢。  

     *   

     * setDither(boolean dither);  

     * 设定是否使用图像抖动处理,会使绘制出来的图片颜色更加平滑和饱满,图像更加清晰  

     *   

     * setFilterBitmap(boolean filter);  

     * 如果该项设置为true,则图像在动画进行中会滤掉对Bitmap图像的优化操作,加快显示  

     * 速度,本设置项依赖于dither和xfermode的设置  

     *   

     * setMaskFilter(MaskFilter maskfilter);  

     * 设置MaskFilter,可以用不同的MaskFilter实现滤镜的效果,如滤化,立体等       *   

     * setColorFilter(ColorFilter colorfilter);  

     * 设置颜色过滤器,可以在绘制颜色时实现不用颜色的变换效果  

     *   

     * setPathEffect(PathEffect effect);  

     * 设置绘制路径的效果,如点画线等  

     *   

     * setShader(Shader shader);  

     * 设置图像效果,使用Shader可以绘制出各种渐变效果  

     *  

     * setShadowLayer(float radius ,float dx,float dy,int color);  

     * 在图形下面设置阴影层,产生阴影效果,radius为阴影的角度,dx和dy为阴影在x轴和y轴上的距离,color为阴影的颜色  

     *   

     * setStyle(Paint.Style style);  

     * 设置画笔的样式,为FILL,FILL_OR_STROKE,或STROKE  

     *   

     * setStrokeCap(Paint.Cap cap);  

     * 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式  

     * Cap.ROUND,或方形样式Cap.SQUARE  

     *   

     * setSrokeJoin(Paint.Join join);  

     * 设置绘制时各图形的结合方式,如平滑效果等  

     *   

     * setStrokeWidth(float width);  

     * 当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度  

     *   

     * setXfermode(Xfermode xfermode);  

     * 设置图形重叠时的处理方式,如合并,取交集或并集,经常用来制作橡皮的擦除效果  

     *   

     * 2.文本绘制  

     * setFakeBoldText(boolean fakeBoldText);  

     * 模拟实现粗体文字,设置在小字体上效果会非常差  

     *   

     * setSubpixelText(boolean subpixelText);  

     * 设置该项为true,将有助于文本在LCD屏幕上的显示效果  

     *   

     * setTextAlign(Paint.Align align);  

     * 设置绘制文字的对齐方向  

     *   

   * setTextScaleX(float scaleX);  

    * 设置绘制文字x轴的缩放比例,可以实现文字的拉伸的效果  

     *   

     * setTextSize(float textSize);  

     * 设置绘制文字的字号大小  

     *   

     * setTextSkewX(float skewX);  

     * 设置斜体文字,skewX为倾斜弧度  

     *   

     * setTypeface(Typeface typeface);  

     * 设置Typeface对象,即字体风格,包括粗体,斜体以及衬线体,非衬线体等  

     *   

     * setUnderlineText(boolean underlineText);  

     * 设置带有下划线的文字效果  

     *   

     * setStrikeThruText(boolean strikeThruText);  

     * 设置带有删除线的效果  

     *   

     */



3.Canvas


简述:相当于我们的ps软件,设置好画笔,即可通过代码在画布上画圆等


public class Canvas{
		弧线(arcs)、填充颜色(argb和color)、
		Bitmap、圆(circle和oval)、点(point)、
		线(line)、矩形(Rect)、图片(Picture)、
		圆角矩形 (RoundRect)、文本(text)、
		顶点(Vertices)、路径(path)。
		Canvas位置转换的方法:
		rorate(旋转)、
		scale(缩放)、
		translate(变换)、
		skew(扭曲)等
	}



2.View绘制的三大流程


首先先看各种View的继承关系。




在一个Activity手机屏幕的View树,顶层View即我们看到的View。






1.View绘制的三大流程--measure



主要作用:为整个View树计算实际的大小,即设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:

  mMeasureWidth),每个View的控件的实际宽高都是由父视图和本身视图决定的。

 

     具体的调用链如下:

         ViewRoot根对象地属性mView(其类型一般为ViewGroup类型)调用measure()方法去计算View树的大小,回调View/ViewGroup对象的onMeasure()方法,该方法实现的功能如下:

   

         1、设置本View视图的最终大小,该功能的实现通过调用setMeasuredDimension()方法去设置实际的高(对应属性:mMeasuredHeight)和宽(对应属性:mMeasureWidth)  ;

         2 、如果该View对象是个ViewGroup类型,需要重写该onMeasure()方法,对其子视图进行遍历的measure()过程。对每个子视图的measure()过程,是通过调用父类ViewGroup.java类里的measureChildWithMargins()方法去实现,该方法内部只是简单地调用了View对象的measure()方法。(由于measureChildWithMargins()方法只是一个过渡层更简单的做法是直接调用View对象的measure()方法)。

              

     整个measure调用流程就是个树形的递归过程

看流程图如下,如果View树申请重新测量,则进行重新测量,从顶层View依次向下递归测量整个View树







伪代码如下:



public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
    //....  
  
    //回调onMeasure()方法    
    onMeasure(widthMeasureSpec, heightMeasureSpec);  
     
    //more  
}  

//回调View视图里的onMeasure过程  
private void onMeasure(int height , int width){  
 //设置该view的实际宽(mMeasuredWidth)高(mMeasuredHeight)  
 //1、该方法必须在onMeasure调用,否者报异常。  
 setMeasuredDimension(h , l) ;  
   
 //2、如果该View是ViewGroup类型,则对它的每个子View进行measure()过程  
 int childCount = getChildCount() ;  
   
 for(int i=0 ;i<childCount ;i++){  
  //2.1、获得每个子View对象引用  
  View child = getChildAt(i) ;  
    
  //整个measure()过程就是个递归过程  
  //该方法只是一个过滤器,最后会调用measure()过程 ;或者 measureChild(child , h, i)方法都  
  measureChildWithMargins(child , h, i) ;   
    
  //其实,对于我们自己写的应用来说,最好的办法是去掉框架里的该方法,直接调用view.measure(),如下:  
  //child.measure(h, l)  
 }  
}  
  
//该方法具体实现在ViewGroup.java里 。  
protected  void measureChildWithMargins(View v, int height , int width){  
 v.measure(h,l)     
}



2.View绘制的三大流程--layout


主要作用 :为将整个根据子视图的大小以及布局参数将View树放到合适的位置上。

 

     具体的调用链如下:

       host.layout()开始View树的布局,继而回调给View/ViewGroup类中的layout()方法。具体流程如下

  

        1 、layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现)接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ;

       

       2、如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的layout()方法去设置它的坐标值。




流程图如下:如果请求View树重新布局,则进行重新布局,与测量流程类似,依次自顶层View递归向下测量





伪代码:



/* final 标识符 , 不能被重载 , 参数为每个视图位于父视图的坐标轴 
 * @param l Left position, relative to parent 
 * @param t Top position, relative to parent 
 * @param r Right position, relative to parent 
 * @param b Bottom position, relative to parent 
 */  
public final void layout(int l, int t, int r, int b) {  
    boolean changed = setFrame(l, t, r, b); //设置每个视图位于父视图的坐标轴  
    if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
        if (ViewDebug.TRACE_HIERARCHY) {  
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
        }  
  
        onLayout(changed, l, t, r, b);//回调onLayout函数 ,设置每个子视图的布局  
        mPrivateFlags &= ~LAYOUT_REQUIRED;  
    }  
    mPrivateFlags &= ~FORCE_LAYOUT;  
}  



// layout()过程  ViewRoot.java  
// 发起layout()的"发号者"在ViewRoot.java里的performTraversals()方法, mView.layout()  
  
private void  performTraversals(){  
   
    //...  
      
    View mView  ;  
       mView.layout(left,top,right,bottom) ;  
      
    //....  
}  
  
//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现  
private void onLayout(int left , int top , right , bottom){  
  
 //如果该View不是ViewGroup类型  
 //调用setFrame()方法设置该控件的在父视图上的坐标轴  
   
 setFrame(l ,t , r ,b) ;  
   
 //--------------------------  
   
 //如果该View是ViewGroup类型,则对它的每个子View进行layout()过程  
 int childCount = getChildCount() ;  
   
 for(int i=0 ;i<childCount ;i++){  
  //2.1、获得每个子View对象引用  
  View child = getChildAt(i) ;  
  //整个layout()过程就是个递归过程  
  child.layout(l, t, r, b) ;  
 }  
}



3.View绘制的三大流程--draw


由ViewRoot对象的performTraversals()方法调用draw()方法发起绘制该View树,值得注意的是每次发起绘图时,并不会重新绘制每个View树的视图,而只会重新绘制那些“需要重绘”的视图,View类内部变量包含了一个标志位DRAWN,当该视图需要重绘时,就会为该View添加该标志位。

 

   调用流程 :

     mView.draw()开始绘制,draw()方法实现的功能如下:

          1 、绘制该View的背景

          2 、为显示渐变框做一些准备操作(见5,大多数情况下,不需要改渐变框)          

          3、调用onDraw()方法绘制视图本身  (每个View都需要重载该方法,ViewGroup不需要实现该方法)

          4、调用dispatchDraw ()方法绘制子视图(如果该View类型不为ViewGroup,即不包含子视图,不需要重载该方法)值得说明的是,ViewGroup类已经为我们重写了dispatchDraw ()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。

  dispatchDraw()方法内部会遍历每个子视图,调用drawChild()去重新回调每个子视图的draw()方法(注意,这个 地方“需要重绘”的视图才会调用draw()方法)。值得说明的是,ViewGroup类已经为我们重写了dispatchDraw()的功能实现,应用程序一般不需要重写该方法,但可以重载父类函数实现具体的功能。




伪代码:


// draw()过程     ViewRoot.java  
// 发起draw()的"发号者"在ViewRoot.java里的performTraversals()方法, 该方法会继续调用draw()方法开始绘图  
private void  draw(){  
   
    //...  
 View mView  ;  
    mView.draw(canvas) ;    
      
    //....  
}  
  
//回调View视图里的onLayout过程 ,该方法只由ViewGroup类型实现  
private void draw(Canvas canvas){  
 //该方法会做如下事情  
 //1 、绘制该View的背景  
 //2、为绘制渐变框做一些准备操作  
 //3、调用onDraw()方法绘制视图本身  
 //4、调用dispatchDraw()方法绘制每个子视图,dispatchDraw()已经在Android框架中实现了,在ViewGroup方法中。  
      // 应用程序程序一般不需要重写该方法,但可以捕获该方法的发生,做一些特别的事情。  
 //5、绘制渐变框    
}  
  
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法  
@Override  
protected void dispatchDraw(Canvas canvas) {  
 //   
 //其实现方法类似如下:  
 int childCount = getChildCount() ;  
   
 for(int i=0 ;i<childCount ;i++){  
  View child = getChildAt(i) ;  
  //调用drawChild完成  
  drawChild(child,canvas) ;  
 }       
}  
//ViewGroup.java中的dispatchDraw()方法,应用程序一般不需要重写该方法  
protected void drawChild(View child,Canvas canvas) {  
 // ....  
 //简单的回调View对象的draw()方法,递归就这么产生了。  
 child.draw(canvas) ;  
   
 //.........  
}





3.自定义View的一些重要方法


 1.invalidate()方法 :

  

 

     一般引起invalidate()操作的函数如下:

           1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。

           2、setSelection()方法:请求重新draw(),但只会绘制调用者本身。

           3、setVisibility()方法 :当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,

                    继而绘制该View。

           4 、setEnabled()方法 :请求重新draw(),但不会重新绘制任何视图包括该调用者本身。





2.requestLayout()方法 :会导致调用measure()过程 和 layout()过程 。

 

           说明:只是对View树重新布局layout过程包括measure()和layout()过程,不会调用draw()过程,但不会重新绘制任何视图包括该调用者本身。

 

    一般引起操作的方法如下:

         setVisibility()方法:

            当View的可视状态在INVISIBLE/VISIBLE 转换为GONE状态时,会间接调用requestLayout() 和invalidate方法。 同时,由于整个个View树大小发生了变化,会请求measure()过程以及draw()过程,同样地,只绘制需要“重新绘制”的视图。

3.requestFocus()函数说明:

 

          说明:请求View树的draw()过程,但只绘制“需要重绘”的视图。

4.实例

1.继承自View重写onDraw



继承自View制作一个心电图的自定义View



public class HeartWavesView extends View {

    private int mTableLineColor = Color.RED;
    private int mWavesLineColor = Color.BLACK;
    private int mTitleColor = Color.BLACK;
    private int mTitleSize = 30;
    private int mXYTextSize = 20;

    private Context context;
    private static final String TAG = "HeartWavesView";

    private Paint paintWavesLine;
    private Paint paintTableLine;
    private TextPaint paintTitle;
    private TextPaint paintXYText;

    private boolean isFirstDrawPoint = true;
    private boolean isFirstDrawBackground = true;
    private int height;
    private int width;
    private int leftPadding;
    private int rightPadding;
    private int topPadding;
    private int bottomPadding;

    private int maxY = 2100;
    private int minY = -2100;
    private int maxX = 120;
    private int x_num = 25;
    private int y_num;
    private int grid_width;

    //x轴每个小格子对应的秒
    //y轴每个小格子对应的指数
    private int grid_second = 5;
    private float grid_num;
    private int zeroCurY;
    private int yStartNum;


    private int workWidth;
    private int workHeight;

    //几秒钟一次数据,默认为1秒1次
    private float dataHz = 1;

    private List<PointXY> pointList;

    private String title = "心电图";


    public HeartWavesView(Context context) {
        super(context);
        this.context = context;
        initView();
    }

    public HeartWavesView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        resolveAttrs(attrs);
        initView();
    }

    private void resolveAttrs(AttributeSet attrs) {
        TypedArray typeArray = context.obtainStyledAttributes(attrs, R.styleable.HeartWavesView);
        mTableLineColor = typeArray.getColor(R.styleable.HeartWavesView_tableLineColor, Color.RED);
        mTitleColor = typeArray.getColor(R.styleable.HeartWavesView_titleColor, Color.BLACK);
        mWavesLineColor = typeArray.getColor(R.styleable.HeartWavesView_wavesLineColor, Color.BLACK);
        mTitleSize = typeArray.getDimensionPixelSize(R.styleable.HeartWavesView_titleSize, 30);
        mXYTextSize = typeArray.getDimensionPixelSize(R.styleable.HeartWavesView_xyTextSize, 20);
        typeArray.recycle();
    }

    public HeartWavesView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        resolveAttrs(attrs);
        initView();
    }

    private void initView() {
        //生成抗锯齿的画笔
        pointList = new ArrayList<>();
        paintWavesLine = new Paint(Paint.ANTI_ALIAS_FLAG);
        //设置画笔粗细
        paintWavesLine.setStrokeWidth(2.5f);
        //设置画笔颜色
        paintWavesLine.setColor(mWavesLineColor);

        paintTableLine = new Paint();
        paintTableLine.setColor(mTableLineColor);
        paintTableLine.setAntiAlias(true);
        paintWavesLine.setStrokeWidth(4);

        paintTitle = new TextPaint();
        paintTitle.setTextSize(mTitleSize);
        paintTitle.setColor(mTitleColor);

        paintXYText = new TextPaint();
        paintXYText.setColor(mTitleColor);
        paintXYText.setTextSize(mXYTextSize);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (isFirstDrawBackground) {
            height = getHeight();
            width = getWidth();
            leftPadding = rightPadding = bottomPadding = topPadding = 100;
        }
        drawBackground(canvas);
        drawWaves(canvas);
    }

    private void drawWaves(Canvas canvas) {
        PointXY start = new PointXY();
        PointXY end = new PointXY();
        for (int i = 0; i < pointList.size() - 1; i++) {
            start = pointList.get(i);
            end = pointList.get(i + 1);
            canvas.drawLine(start.getX(), start.getY(), end.getX(), end.getY(), paintWavesLine);
        }
    }

    private void drawBackground(Canvas canvas) {
        if (isFirstDrawBackground) {
            isFirstDrawBackground = false;
            x_num = maxX / grid_second;
            x_num = x_num % 5 == 0 ? x_num : (x_num % 5 > 3 ? (x_num / 5 + 1) * 5 : x_num / 5 * 5);
            grid_width = (width - leftPadding - rightPadding) / x_num;
            y_num = (height - topPadding - rightPadding) / grid_width;
            y_num = y_num % 5 == 0 ? y_num : (y_num % 5 > 3 ? (y_num / 5 + 1) * 5 : y_num / 5 * 5);
            //获取工作区的宽和高
            workWidth = grid_width * x_num;
            workHeight = grid_width * y_num;
            //获取xy轴比例尺
            //获得y轴0标识位的位置
            if (maxY > 0 && minY >= 0) {
                yStartNum = maxY;
                grid_num = maxY / y_num;
                zeroCurY = y_num;
            } else if (maxY <= 0 && minY < 0) {
                yStartNum = 0;
                grid_num = -minY / y_num;
                zeroCurY = 0;
            } else {
                zeroCurY = y_num / 2;
                zeroCurY = zeroCurY % 5 == 0 ? zeroCurY :
                        (zeroCurY % 5 > 3) ? (zeroCurY / 5 + 1) * 5 : (zeroCurY / 5 * 5);
                grid_num = Math.max(maxY, minY) / Math.min(y_num - zeroCurY, zeroCurY);
                yStartNum = (int) (zeroCurY * grid_num);
            }
        }
        for (int i = 0; i <= x_num; i++) {
            paintTableLine.setStrokeWidth(1f);
            if (i % 5 == 0) {
                paintTableLine.setStrokeWidth(3f);
                String label = grid_second * i + "";
                canvas.drawText(label,
                        leftPadding + i * grid_width - mXYTextSize / 2,
                        workHeight + bottomPadding / 2 + topPadding,
                        paintXYText);
            }
            canvas.drawLine(leftPadding + i * grid_width, topPadding,
                    leftPadding + i * grid_width, topPadding + workHeight, paintTableLine);
        }
        for (int i = 0; i <= y_num; i++) {
            paintTableLine.setStrokeWidth(1f);
            if (i % 5 == 0) {
                paintTableLine.setStrokeWidth(3f);
                String label = yStartNum - i * grid_num + "";
                canvas.drawText(label, leftPadding / 5, topPadding + i * grid_width, paintXYText);
            }
            canvas.drawLine(leftPadding, topPadding + i * grid_width,
                    leftPadding + workWidth, topPadding + i * grid_width, paintTableLine);
        }
        canvas.drawText(title, width / 2 - mTitleSize * title.length() / 2,
                topPadding / 2, paintTitle);
    }

    public void drawNextPoint(float y) {
        if (!isFirstDrawBackground) {
            if (isFirstDrawPoint) {
                isFirstDrawPoint = false;
                PointXY point = new PointXY();
                point.setX(leftPadding);
                point.setY(zeroCurY * grid_width + topPadding);
                pointList.add(point);
            }
            PointXY lastPoint = pointList.get(pointList.size() - 1);
            Log.d(TAG, "drawNextPoint: size" + pointList.size());
            if (pointList.size() == maxX- 1) {
                pointList.clear();
                lastPoint.setX(leftPadding);
            }
            PointXY nowPoint = new PointXY();
            nowPoint.setX(dataHz / grid_second * grid_width + lastPoint.getX());
            nowPoint.setY((yStartNum - y) / grid_num * grid_width + topPadding);
            pointList.add(nowPoint);
            invalidate();

        }

    }

    public Paint getPaintWavesLine() {
        return paintWavesLine;
    }

    public void setPaintWavesLine(Paint paintWavesLine) {
        this.paintWavesLine = paintWavesLine;
    }

    public Paint getPaintTableLine() {
        return paintTableLine;
    }

    public void setPaintTableLine(Paint paintTableLine) {
        this.paintTableLine = paintTableLine;
    }

    public TextPaint getPaintTitle() {
        return paintTitle;
    }

    public void setPaintTitle(TextPaint paintTitle) {
        this.paintTitle = paintTitle;
    }

    public TextPaint getPaintXYText() {
        return paintXYText;
    }

    public void setPaintXYText(TextPaint paintXYText) {
        this.paintXYText = paintXYText;
    }

    public int getLeftPadding() {
        return leftPadding;
    }

    public void setLeftPadding(int leftPadding) {
        this.leftPadding = leftPadding;
    }

    public int getRightPadding() {
        return rightPadding;
    }

    public void setRightPadding(int rightPadding) {
        this.rightPadding = rightPadding;
    }

    public int getTopPadding() {
        return topPadding;
    }

    public void setTopPadding(int topPadding) {
        this.topPadding = topPadding;
    }

    public int getBottomPadding() {
        return bottomPadding;
    }

    public void setBottomPadding(int bottomPadding) {
        this.bottomPadding = bottomPadding;
    }

    public int getMaxY() {
        return maxY;
    }

    public void setMaxY(int maxY) {
        this.maxY = maxY;
    }

    public int getMinY() {
        return minY;
    }

    public void setMinY(int minY) {
        this.minY = minY;
    }

    public int getMaxX() {
        return maxX;
    }

    public void setMaxX(int maxX) {
        this.maxX = maxX;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public class PointXY {
        private float x;
        private float 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;
        }
    }
}

2.继承自ViewGroup实现特定的layout





实现一个水平的滑动栏容器



public class HorizontalScrollViewEx extends ViewGroup {
    private static final String TAG = "HorizontalScrollViewEx";
    private int mChildrenSize;
    private int mChildWidth;
    private int mChildIndex;
    //记录上次滑动的坐标
    private int mLastX = 0;
    private int mLastY = 0;
    //记录上次滑动的坐标(onInterceptTouchEvent)
    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;


    public HorizontalScrollViewEx(Context context) {
        super(context);
        init();
    }

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

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

    private void init() {
        if (mScroller == null) {
            mScroller = new Scroller(getContext());
            mVelocityTracker = VelocityTracker.obtain();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                    //动画滚动到最终位置结束动画,这里代表本身HorizontalScrollViewEx处理触摸事件
                    intercepted = true;
                }
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastXIntercept;
                int deltaY = y - mLastYIntercept;
                if (Math.abs(deltaX) > Math.abs(deltaY)) {
                    //水平滑动则处理此事件
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        Log.d(TAG, "onInterceptTouchEvent: intercepted = " + intercepted);
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }


    /**
     * 本身在处理触摸事件
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:{
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                break;
            }
            case MotionEvent.ACTION_MOVE:{
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);
                break;
            }
            case MotionEvent.ACTION_UP:{
                int scrollX = getScrollX();
                mVelocityTracker.computeCurrentVelocity(1000);
                float xVelocity = mVelocityTracker.getXVelocity();
                if(Math.abs(xVelocity) >= 50){
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                }else{
                    mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
                }
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
                int dx = mChildIndex * mChildWidth - scrollX;
                smoothScrollBy(dx, 0);
                mVelocityTracker.clear();
                break;
            }
            default:break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }


    /**
     * 重写onMeasure方法,对容器内的测量进行修正
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /**
         * super.onMeasure方法内部调用了setMeasuredDimension(getDefaultSize(
         * getSuggestedMinimumWidth(), widthMeasureSpec),
         * getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
         */
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
        if(childCount == 0){
            //no child -- no width, no height
            setMeasuredDimension(0, 0);
        }else if(widthSpaceMode == MeasureSpec.AT_MOST &&
                heightSpaceMode == MeasureSpec.AT_MOST){
            //width is wrap and height is wrap
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        }else if(heightSpaceMode == MeasureSpec.AT_MOST){
            //height is wrap but width not
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, measuredHeight);
        }else if(widthMeasureSpec == MeasureSpec.AT_MOST){
            //width is wrap but height not
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth();
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;
        for(int i = 0; i < childCount; i ++){
            final View childView = getChildAt(i);
            if(childView.getVisibility() != View.GONE){
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth,
                        childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

    /**
     * 两个startScrollBy方法,一个加上了时间间隔,一个并没有
     * 之前整错了,用错了方法
     * @param dx
     * @param dy
     */
    private void smoothScrollBy(int dx, int dy) {
        mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
        invalidate();
    }

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

    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }


}



3.继承自特定的ViewGroup





布局文件如下:



<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_gravity="center"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="3dp">

    <ImageView
        android:id="@+id/image_view_image"
        android:layout_gravity="center_horizontal"
        android:src="@drawable/image"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:padding="5dp"/>

    <TextView
        android:textSize="10sp"
        android:text="测试"
        android:id="@+id/image_view_title"
        android:layout_width="match_parent"
        android:layout_height="10dp"
        android:gravity="center_horizontal" />

</LinearLayout>



实现代码:





package noclay.testview;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;

/**
 * Created by 82661 on 2016/10/18.
 */

public class MyImageView extends LinearLayout {
    private static final String TAG = "MyImageView";
    private TextView textView;
    private ImageView imageView;
    private LinearLayout linearLayout;
    private static Context context;

    public MyImageView(Context context) {
        super(context);
        this.context = context;
        init();
    }

    public MyImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.context = context;
        init();
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyImageView);
        textView.setText(array.getText(R.styleable.MyImageView_title));
        imageView.setImageDrawable(getResources().
                getDrawable(array.getResourceId(R.styleable.MyImageView_image, R.drawable.image)));
        array.recycle();
    }

    public MyImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        init();
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.MyImageView);
        textView.setText(array.getText(R.styleable.MyImageView_title));
        imageView.setImageDrawable(getResources().
                getDrawable(array.getResourceId(R.styleable.MyImageView_image, R.drawable.image)));
        array.recycle();
    }

    public void init(){
        LayoutInflater.from(getContext()).inflate(R.layout.my_iamge_view, this, true);
        textView = (TextView) findViewById(R.id.image_view_title);
        imageView = (ImageView) findViewById(R.id.image_view_image);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);
        Log.d(TAG, "onMeasure: width = " + widthSpaceSize);
        Log.d(TAG, "onMeasure: height = " + heightSpaceSize);
        if(widthSpaceMode == MeasureSpec.EXACTLY && heightSpaceMode == MeasureSpec.EXACTLY){
            //在有具体数值的时候调整子项的大小
            if(7 * widthSpaceSize <= 5 * heightSpaceSize){//规定长宽比例为5:7
                setMeasure(widthSpaceSize);
            }else{
                setMeasure(heightSpaceSize);
            }
        }
    }

    private void setMeasure(int value){
        Log.d(TAG, "onMeasure: true");
        LayoutParams lp = (LayoutParams) imageView.getLayoutParams();
        lp.width = value * 4 / 5 ;
        lp.height = value * 4 / 5;
        lp.bottomMargin = lp.topMargin = value * 1 / 10;
        imageView.setLayoutParams(lp);

        lp = (LayoutParams) textView.getLayoutParams();
        lp.width = value;
        lp.height = value * 2 / 5;
        textView.setLayoutParams(lp);
        textView.setTextSize(px2sp(lp.height / 2));
    }

    public static int px2sp(float value) {
        final float scale =context.getResources().getDisplayMetrics().scaledDensity;
        return (int) (value / scale + 0.5f);
    }
}

4.继承自特定的View



一个圆形图片的View



package noclay.testview;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;

/**
 * Created by 82661 on 2016/10/25.
 */

public class MyCircleImageView extends ImageView {
    private Paint paint;

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

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

    public MyCircleImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        paint = new Paint();

    }

    /**
     * 绘制圆形图片
     *
     * @author caizhiming
     */
    @Override
    protected void onDraw(Canvas canvas) {

        Drawable drawable = getDrawable();
        if (drawable != null) {
            Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            Bitmap b = getCircleBitmap(bitmap);
            final Rect rectSrc = new Rect(0, 0, b.getWidth(), b.getHeight());
            final Rect rectDest = new Rect(0, 0, getWidth(), getHeight());
            paint.reset();
            canvas.drawBitmap(b, rectSrc, rectDest, paint);

        } else {
            super.onDraw(canvas);
        }
    }

    /**
     * 获取圆形图片方法
     *
     * @param bitmap
     * @return Bitmap
     * @author caizhiming
     */
    private Bitmap getCircleBitmap(Bitmap bitmap) {
        Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
                bitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(output);
        final int color = 0xff424242;
        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        paint.setAntiAlias(true);
        //设置背景色
        canvas.drawARGB(0, 0, 0, 0);
        paint.setColor(color);
        int x = bitmap.getWidth();
		
        canvas.drawCircle(x / 2, x / 2, x / 2, paint);
		//设置遮罩
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(bitmap, rect, rect, paint);
        return output;
    }
}