自定义view如何分类

  1. 自定义View:只需要重写onMeasure()和onDraw(),在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View,SurfaceView或其他的View
  2. 自定义ViewGroup:只需要重写onMeasure()和onLayout(),一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout

view的构造函数:共有4个

// 如果View是在Java代码里面new的,则调用第一个构造函数
    public CustomView(Context context) {
        super(context);
    }

    // 如果View是在.xml里声明的,则调用第二个构造函数
    // 自定义属性是从AttributeSet参数传进来的
    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // 不会自动调用 
    // 一般是在第二个构造函数里主动调用 
    // 如View有style属性时 
    public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    //API21之后才使用 
    // 不会自动调用 
    // 一般是在第二个构造函数里主动调用 
    // 如View有style属性时 
    public CustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

LayoutInflater 的基本用法


LayoutInflater 其实就是使用 Android 提供的 pull 解析方式 来解析布局文件的



LayoutInflater.from(this).inflate()


inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)


inflate()方法一般接收两个参数,第一个参数就是要加载的布局 id,第二个参数


是指给该布局的外部再嵌套一层父布局,如果不需要就直接传 null。接收三个参数时:



  • 如果 root 为 null,attachToRoot 将失去作用,设置任何值都没有意义。
  • 如果 root 不为 null,attachToRoot 设为 true,则会给加载的布局文件的指定一个父布局,即 root。
  • 如果 root 不为 null,attachToRoot 设为 false,则会将布局文件最外层的所有 layout 属性进行设置,当该 view 被添加到父 view 当中时,这些 layout 属性会自动生效。
  • 在不设置 attachToRoot 参数的情况下,如果 root 不为 null,attachToRoot 参数默认为 true。


Android两种坐标系





iOS collectionview 自定义 自定义view wrapcontent_Android


 

iOS collectionview 自定义 自定义view wrapcontent_子视图_02


  • View的静态坐标方法

View的静态坐标方法

解释

getLeft()

返回View自身左边到父布局左边的距离

getTop()

返回View自身顶边到父布局顶边的距离

getRight()

返回View自身右边到父布局左边的距离

getBottom()

返回View自身底边到父布局顶边的距离

getX()

返回值为getLeft()+getTranslationX(),当setTranslationX()时getLeft()不变,getX()变。

getY()

返回值为getTop()+getTranslationY(),当setTranslationY()时getTop()不变,getY()变。

  • 手指触摸屏幕时MotionEvent

MotionEvent坐标方法

解释

getX()

当前触摸事件距离当前View左边的距离

getY()

当前触摸事件距离当前View顶边的距离

getRawX()

当前触摸事件距离整个屏幕左边的距离

getRawY()

当前触摸事件距离整个屏幕顶边的距离

  • 获取宽高

View宽高方法

解释

getWidth()

layout后有效,返回值是mRight-mLeft,一般会参考measure的宽度(measure可能没用),但不是必须的。

getHeight()

layout后有效,返回值是mBottom-mTop,一般会参考measure的高度(measure可能没用),但不是必须的。

getMeasuredWidth()

返回measure过程得到的mMeasuredWidth值,供layout参考,或许没用。

getMeasuredHeight()

返回measure过程得到的mMeasuredHeight值,供layout参考,或许没用。

  • 获取view位置

View的方法

结论描述

getLocalVisibleRect()

获取View自身可见的坐标区域,坐标以自己的左上角为原点(0,0),另一点为可见区域右下角相对自己(0,0)点的坐标,其实View2当前height为550,可见height为470。

getGlobalVisibleRect()

获取View在屏幕绝对坐标系中的可视区域,坐标以屏幕左上角为原点(0,0),另一个点为可见区域右下角相对屏幕原点(0,0)点的坐标。

getLocationOnScreen()

坐标是相对整个屏幕而言,Y坐标为View左上角到屏幕顶部的距离。

getLocationInWindow()

如果为普通Activity则Y坐标为View左上角到屏幕顶部(此时Window与屏幕一样大);如果为对话框式的Activity则Y坐标为当前Dialog模式Activity的标题栏顶部到View左上角的距离。

  • View滑动相关坐标系

View的scrollTo()和scrollBy()是用于滑动View中的内容,而不是改变View的位置;改变View在屏幕中的位置可以使用offsetLeftAndRight()和offsetTopAndBottom()方法,他会导致getLeft()等值改变

View的滑动方法

效果及描述

offsetLeftAndRight(int offset)

水平方向挪动View,offset为正则x轴正向移动,移动的是整个View,getLeft()会变的,自定义View很有用

offsetTopAndBottom(int offset)

垂直方向挪动View,offset为正则y轴正向移动,移动的是整个View,getTop()会变的,自定义View很有用

scrollTo(int x, int y)

将**View中内容(不是整个View)**滑动到相应的位置,参考坐标原点为ParentView左上角,x,y为正则向xy轴反方向移动,反之同理。

scrollBy(int x, int y)

在scrollTo()的基础上继续滑动xy。

setScrollX(int value)

实质为scrollTo(),只是只改变Y轴滑动。

setScrollY(int value)

实质为scrollTo(),只是只改变X轴滑动。

getScrollX()/getScrollY()

获取当前滑动位置偏移量。


iOS collectionview 自定义 自定义view wrapcontent_自定义_03





Android 视图绘制流程

iOS collectionview 自定义 自定义view wrapcontent_自定义_04

view的层级结构

 

iOS collectionview 自定义 自定义view wrapcontent_Android_05

一. onMeasure()  决定View的大小;

源码:

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


用于测量视图的大小。View 系统的绘制流程会从 ViewRoot 的 performTraversals()方法中开始,在其内部调用 View 的 measure()方法。measure()方法接收两个参数,widthMeasureSpec 和 heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小。


MeasureSpec 的值由 specSize 和 specMode 共同组成的,其中 specSize 记


录的是大小,specMode 记录的是规格。specMode 一共有三种类型,如下所


示:


1. EXACTLY(确切的大小,如:100dp)


表示父视图希望子视图的大小应该是由 specSize 的值来决定的,系统默认会按


照这个规则来设置子视图的大小,开发人员当然也可以按照自己的意愿设置成任


意的大小。


2. AT_MOST(大小不可超过某数值,如:matchParent, 最大不能超过父布局大小)


表示子视图最多只能是 specSize 中指定的大小,开发人员应该尽可能小得去设


置这个视图,并且保证不会超过 specSize。系统默认会按照这个规则来设置子


视图的大小,开发人员当然也可以按照自己的意愿设置成任意的大小。


3. UNSPECIFIED(不对View大小做限制,系统使用)


表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这


种情况比较少见,不太会用到。


public class MyView extends View {  

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(200, 200);
        }
    }


上面代码中把 View 默认的测量流程覆盖掉了,不管在布局文件中定义 MyView 这个视图的大小是多少,最终在界面上显示的大小都将会是 200*200。


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


由此可见,视图大小的控制是由父视图、布局文件、以及视图本身共同完成的,父视图会提供给子视图参考的大小,而开发人员可以在 XML 文件中指定视图的大小,然后视图本身会对最终的大小进行拍板。


getMeasureWidth与getWidth的区别

  1. getMeasureWidth:在measure()过程结束后就可以获取到对应的值; 通过setMeasuredDimension()方法来进行设置的.
  2. getWidth:在layout()过程结束后才能获取到; 通过视图右边的坐标减去左边的坐标计算出来的.

二. onLayout() 决定View在ViewGroup中的位置;

源码:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }


这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewRoot 的 performTraversals()方法会在 measure 结束后继续执行,并调用 View 的 layout()方法来执行此过程。


View 中的 onLayout()方法就是一个空方法,因为 onLayout()过程是为

了确定视图在布局中所在的位置,而这个操作应该是由布局来完成的,即父视图 决定子视图的显示位置。既然如此,我们来看下 ViewGroup 中的 onLayout() 方法是怎么写的吧,代码如下:


ViewGroup中的源码:
  @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);


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


在 onLayout()过程结束后,我们就可以调用 getWidth()方法和 getHeight()方 法来获取视图的宽高了。




三. onDraw()  决定绘制这个View。


在这里才真正地开始对视图进行绘制。ViewRoot 中的代码会继续执行并创建出一个 Canvas 对象,然后调用 View 的 draw()方法来 执行具体的绘制工作。draw()方法内部的绘制过程总共可以分为六步,其中第二步和第五步在一般情况下很少用到,因此这里我们只分析简化后的绘制过程。代


码如下所示:


public void draw(Canvas canvas) {
        if (ViewDebug.TRACE_HIERARCHY) {
            ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
        }
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & DIRTY_MASK) == DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~DIRTY_MASK) | DRAWN;
        // Step 1, draw the background, if needed 
        int saveCount;
        if (!dirtyOpaque) {
            final Drawable background = mBGDrawable;
            if (background != null) {
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;
                if (mBackgroundSizeChanged) {
                    background.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
                    mBackgroundSizeChanged = false;
                }
                if ((scrollX | scrollY) == 0) {
                    background.draw(canvas);
                } else {
                    canvas.translate(scrollX, scrollY);
                    background.draw(canvas);
                    canvas.translate(-scrollX, -scrollY);
                }
            }
        }
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content 
            if (!dirtyOpaque) onDraw(canvas);
            // Step 4, draw the children 
            dispatchDraw(canvas);
            // Step 6, draw decorations (scrollbars) 
            onDrawScrollBars(canvas);
            // we're done...
            return;
        }
    }


第一步的作用是对视图的背景进行绘制。这里会先得到一个 mBGDrawable 对象,然后根据 layout 过程确定的视图位置来设置背景的绘制区域,之后再调用 Drawable 的draw()方法来完成背景的绘制工作。那么这个 mBGDrawable 对象是从哪里来的呢?其实就是在 XML中通过 android:background 属性设置的图片或颜色。当然你也可以在代码中通过 setBackgroundColor()、setBackgroundResource()等方法进行赋值。


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


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


第六步的作用是对视图的滚动条进行绘制。那么你可能会奇怪,当前的视图又不一定是 ListView 或者ScrollView,为什么要绘制滚动条呢?其实不管是 Button 也好,TextView 也 好,任何一个视图都是有滚动条的,只是一般情况下我们都没有让它显示出来而已。绘制滚动条的代码逻辑也比较复杂,这里就不再贴出来了,因为我们的重点是第三步过程。


自定义绘制API

Canvas常用方法

  • 绘制图形(点、线、矩形、椭圆、圆等)
  • 绘制文本(文本的居中问题,需要Paint知识)
  • 画布的基本变化(平移、缩放、旋转、倾斜)
  • 画布的裁剪
  • 画布的保存

Paint类主要用于设置绘制风格:包括画笔的颜色画笔触笔粗细、填充风格及文字的特征

  • Paint常用方法
  • 颜色
  • 类型(填充、描边)
  • 字体大小
  • 宽度
  • 对齐方式
  • 文字位置属性测量
  • 文字宽度测量

iOS collectionview 自定义 自定义view wrapcontent_Android_06

3.Path常用方法

  • 添加路径
  • 移动起点
  • 贝塞尔(二阶、三阶)
  • 逻辑运算
  • 重置路径
  • PathEffect
  • Matrix
  • PathMeasure
  • PorterDuffXfermode 
  • Matrix
  • iOS collectionview 自定义 自定义view wrapcontent_水波纹扩散_07


  • 平移矩阵
  • iOS collectionview 自定义 自定义view wrapcontent_水波纹扩散_08


  • 缩放矩阵

  • 旋转矩阵

iOS collectionview 自定义 自定义view wrapcontent_自定义_09

  • ColorMatrix

类别

API

描述

旋转

setRotate

设置(非输入轴颜色的)色调

饱和度

setSaturation

设置饱和度

缩放

setScale

三原色的取值的比例

设置

set、setConcat

设置颜色矩阵、两个颜色矩阵的乘积

重置

reset

重置颜色矩阵为初始状态

矩阵运算

preConcat、postConcat

颜色矩阵的前乘、后乘

4.动画

  • ObjectAnimator
  • ValueAnimator
  • AnimatorSet
  • 差值器
  • 估值器


自定义View示例:实现类似水波扩散效果一共分为六步

1.

public SpreadView(Context context) {
    this(context,null,0);
}

public SpreadView(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs,0);
}

public SpreadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initPaints();
}

private void initPaints() {

}

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

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

2.实现画笔paint类

本文一共两只画笔

private void initPaints() {
    //画笔1:
    centerPaint = new Paint();
    centerPaint.setColor(Color.YELLOW);
    centerPaint.setAntiAlias(true);//抗锯齿效果
    //最开始不透明且扩散距离为0
    alphas.add(255);
    spreadRadius.add(0);
    //画笔2:
    spreadPaint = new Paint();
    spreadPaint.setAntiAlias(true);
    spreadPaint.setAlpha(255);
    spreadPaint.setColor(Color.RED);
}

3.覆写onMeasure(…)方法

实现这个方法告诉了母容器如何放弃自定义View,可以通过提供的measureSpecs来决定你的View的高和宽,以下是一个正方形,确认它的宽和高是一样的。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int w = MeasureSpec.getSize(widthMeasureSpec);
    int h = MeasureSpec.getSize(heightMeasureSpec);
    int size = Math.min(w, h);
    setMeasuredDimension(size, size);
}

注意:

这个方法需要至少保证一个setMeasuredDimension(..)调用,否则会报IllegalStateException错误。

4.实现onSizeChanged(…)方法

这个方法是你获取View现在的宽和高. 这里我们计算的是中心和半径

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    //圆心位置
    centerX = w / 2;
    centerY = h / 2;
}

5.实现onDraw(…)方法

这个方法提供了如何绘制view,它提供的Canvas类可以进行绘制。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    for (int i = 0; i < spreadRadius.size(); i++) {
        int alpha = alphas.get(i);
        spreadPaint.setAlpha(alpha);
        int width = spreadRadius.get(i);
        //绘制扩散的圆
        canvas.drawCircle(centerX, centerY, radius + width, spreadPaint);
        //每次扩散圆半径递增,圆透明度递减
        if (alpha > 0 && width < 300) {
            alpha = alpha - distance > 0 ? alpha - distance : 1;
            alphas.set(i, alpha);
            spreadRadius.set(i, width + distance);
        }
    }
    //当最外层扩散圆半径达到最大半径时添加新扩散圆
    if (spreadRadius.get(spreadRadius.size() - 1) > maxRadius) {
        spreadRadius.add(0);
        alphas.add(255);
    }
    //超过8个扩散圆,删除最先绘制的圆,即最外层的圆
    if (spreadRadius.size() >= 8) {
        alphas.remove(0);
        spreadRadius.remove(0);
    }
    //中间的圆
    canvas.drawCircle(centerX, centerY, radius, centerPaint);
    //TODO 可以在中间圆绘制文字或者图片
    //延迟更新,达到扩散视觉差效果
    postInvalidateDelayed(delayMilliseconds);
}

6.添加你的View

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


    <com.qinqu.spreadviewdemo.SpreadView
        android:id="@+id/spreadView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:spread_center_color="@color/colorAccent"
        app:spread_delay_milliseconds="35"
        app:spread_distance="5"
        app:spread_max_radius="90"
        app:spread_radius="100"
        app:spread_spread_color="@color/colorAccent" />


</LinearLayout>

问题


自定义 View 执行 invalidate() 方法 , 为什么有时候不会回调 onDraw()?


自定义一个view时,重写onDraw。调用view.invalidate(),会触发onDraw和computeScroll()。前提


是该view被附加在当前窗口.


view.postInvalidate(); //是在非UI线程上调用的


自定义一个ViewGroup,重写onDraw。onDraw可能不会被调用,原因是需要先设置一个背景(颜色或图)。


表示这个group有东西需要绘制了,才会触发draw,之后是onDraw。因此,一般直接重写dispatchDraw来绘制viewGroup.自定义一个ViewGroup,dispatchDraw会调用drawChild.