自定义View前置基础

自定义View前需要了解的知识点

Posted by dks on February 11, 2019

本文是对自定义View所需掌握的前置知识点的总结,当然,这不包括 View 的绘制流程、事件分发等内容。更多的是偏向于概念的理解和常用 API 的使用。

坐标

在 Android 中,将屏幕的左上角顶点(或者父View)作为坐标系的原点,从这个原点往右为 X 轴的正方向,从这个点往下是 Y 轴的正方向。 Android坐标系

触控事件 – MotionEvent

MotionEvent 中封装了一些事件常量:ACTION_DOWN ;ACTION_MOVE ;ACTION_CANCEL ;ACTION_UP。 一般我们会在 onTouchEvent(MotionEvent event) 方法中通过传进来的 ` MotionEvent 引用的 getAction 方法来获取时间的类型,并用 switch-case `的方法来进行筛选,根据不同的时间进行不同的逻辑操作。

public boolean onTouchEvent(MotionEvent event) {
  switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
      break;
    case MotionEvent.ACTION_MOVE:
      break;
    case MotionEvent.ACTION_UP:
      break;
  }
  return true; 
}

获取位置和距离

在 Android 中提供了很多的方法来获取坐标值,相对距离等。这些方法可以分成如下两个类型:

  • View 提供的获取坐标方法
    • getTop: View 自身的顶边到父View顶边的距离
    • getLeft: View 自身的左边到父View左边的距离
    • getRight: View 自身的右边到父View右边的距离
    • getBottom: View 自身的底边到父View底边的距离
  • MotionEvent 提供的获取坐标方法
    • getX : 触摸点到当前控件左边缘的距离
    • getY : 触摸点当前控件顶边缘的距离
    • getRawX : 触摸点屏幕左边缘的距离
    • getRawY : 触摸点到屏幕顶边缘的距离

获取位置和距离

onLayout、layout方法

  • onLayout

onLayout 方法是 ViewGroup 中子 View 的布局方法,用于放置子 View的 位置。放置子 View 很简单,只需在重写 onLayout 方法,然后获取子 View 的实例,调用子 View 的 layout 方法实现布局。在实际开发中,一般要配合onMeasure测量方法一起使用。自定义 View 首先调用 onMeasure 进行测量,然后调用 onLayout 方法,动态获取子 View 和子 View 的测量大小,然后进行 layout 布局。

	@Override
	protected abstract void onLayout(boolean changed,
	            int l, int t, int r, int b);

该方法是 ViewGroup 中唯一的抽象函数,继承ViewGroup抽象类必须实现 onLayout 方法,而 onMeasure 并非必须重写。ViewGroup 是一个矩形空间,onLayout 传下来的 l,t,r,b 分别是放置父控件的矩形可用空间(除去 margin 和 padding 的空间)的左上角的 left、top 以及右下角 right、bottom 值,即矩形的四条边到父控件可用空间原点的距离。

典型代码

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            for (int i = 0; i < getChildCount(); i++) {
                View child      = getChildAt(i);
                int  childWidth = getMeasuredWidth();
                //水平排列
                child.layout(l, t, l + childWidth, child.getMeasuredHeight());
                l += childWidth;
            }
        }
   }
  • layout 该方法在 View 类中实现,其作用是将 View 放置在 ViewGroup 的相应位置上。调用该方法需要传入 ViewGroup 的矩形空间左上角 left、top 值和右下角 right、bottom 值。这四个值是相对于父控件而言的。例如传入的是(10, 10, 100, 100),则该 View 在距离父控件的左上角位置 (10, 10) 处显示,显示的大小是宽高是90(参数 r,b 是相对左上角的),这有点像绝对布局。

滑动

要想让 View 滑动,可通过以下几种方式实现。

layout

调用 layout() 方法来对 View 重新布局,直接设置 View 在其 ViewGroup 中的位置。

offsetLeftAndRight()与offsetTopAndBottom()

这两个方法相当于系统提供的一个对左右上下移动的 API 封装,得到偏移量之后使用如下代码就可以完成移动。

//使用 offsetLeftAndRight 和 offsetLeftAndRight 进行偏移,从而移动view
offsetLeftAndRight(offsetX);
offsetTopAndBottom(offsetY);

LayoutParams

LayoutParams 保存了一个 View 的布局参数,通过改变 LayoutParams 来动态的修改一个布局的位置参数,从而达到改变 View 位置的效果。我们可以很方便的在程序中使用 getLayoutParams 来获取一个 View 的 LayoutParams。得到偏移量后,就可以通过 setLayoutParams 来改变。

RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin =getLeft()+offsetX;
layoutParams.topMargin = getTop()+ offsetY;
setLayoutParams(layoutParams);

这里的 RelativeLayout.LayoutParams 是根据你的父布局而定的 如果是 LinearLayout 的话就用 LinearLayout 的 LayoutParams。 当然了 如果你连父布局都没有,当我没说,那样是不能用这个方法的。除了使用布局的 LayoutParam s外,我们还可以用 ViewGroup.MarginLayoutParams 来实现:

ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);

scrollTo 和 scrollBy

scrollTo 和 scrollBy 使用起来很简单,但是理解起来稍微复杂一点。scrollTo 是直接移动到指定的坐标,而 scrollBy 是根据偏移量进行相对移动。但是需要注意的是,这两个都不是直接移动 View ,而是移动 View 中的 content ,比如 textView 中移动的是文字,imagView 中移动的是图片,移动的是内容,而不是本体。所以,我们应该在想要移动的 View 的父布局中去使用它,用它来移动 ViewGroup 中的子 View。

上面说的只是其中一个难点,还有一个难点就是参考系不同。 这样理解吧,ViewGroup 是一个长方形的相框,在相框背后是一块巨大的幕布,那么我们看到的内容,就是相框中所能囊括下的内容,在使用 scrollBy 进行移动的时候,移动的是整个相框,而相框里的内容没动,但是因为相框移动了,所以内容的位置也发生了变化。我们按照 X 轴将相框左边移动的话,那相框中的内容是在往右移动,所以在使用 scrollBy 的时候,内容是往反方向运动的,这里如果需要改为符合我们预期的移动方式,那么只需要将 scrollBy 的参数设置为负数即可。

迷之坐标

((View) getParent()).scrollBy(-offsetX, -offsetY);
// 上图中,偏移量-offsetX, -offsetY,分别为-30,-50

使用 scrollTo 直接移动指定的坐标;使用 scrollBy 根据偏移量来进行移动,注意参数使用负数即可。

Scroller

前面我们使用的不管是 scrollBy 还是 scrollTo ,移动其实都是在一瞬间完成的。这里我们可以使用Scroller来实现有过度效果的滑动,这个过程不是瞬间完成的,而是在一定的时间间隔完成的。 Scroller 的内部其实也是用 scrollTo 方法来实现的,但是它可以根据需要移动的总距离,以及设置的移动时间,计算出每一次需要移动的距离,然后不断的进行移动,这样就实现了一个动画的效果。

Scroller本身是不能实现View的滑动的,它需要配合View的computeScroll()方法才能弹性滑动的效果。

  • 创建实例
    public CustomView(Context context, AttributeSet attrs) {
          super(context, attrs);
          mScroller = new Scroller(context);
      }
    
  • 重写computeScroll()方法 系统会在绘制 View 的时候在draw()方法中调用 computeScroll() 方法,这个方法中我们调用父类的scrollTo() 方法并通过 Scroller 来不断获取当前的滚动值,每滑动一小段距离我们就调用 invalidate()方法不断的进行重绘,重绘就会调用 computeScroll() 方法,这样我们就通过不断的移动一个小的距离并连贯起来就实现了平滑移动的效果:
      @Override
      public void computeScroll() {
          super.computeScroll();
          if(mScroller.computeScrollOffset()){
              ((View) getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
               //通过不断的重绘不断的调用computeScroll方法
               invalidate();
          }  
      }
    
  • 执行滚动并刷新
    //startScroll方法源码
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
      //...
    }
    

    startScroll()有两个重载方法,接收四或者五个参数,第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动。第三个参数是持续时间,表示滚动在多长时间内完成。startScroll()后,紧接着调用invalidate()方法来刷新界面。

mScroller.startScroll(0,0,delta,0,2000);
invalidate();

属性动画

毫无疑问,属性动画可以移动 View 并呈现绚丽的动作。本篇不作拓展。

参考

感谢: