Android基础之初识View

212 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

1. View与ViewGroup

View是所有控件的基类,ViewGroup是容纳这些控件的容器,它包含很多View以及ViewGroup。需要注意的是ViewGroup也是继承自View的。

view_1.jpg

2. Android中的坐标系

Android中有两种坐标系,分别为屏幕坐标系和View坐标系。

2.1 屏幕坐标系

在Android中,将左上角的顶点作为Android坐标系的原点,原点向右为X轴的正方向,向下为Y轴的正方向。在触摸事件中,使用getRawX()getRawY()获得的坐标便是Android坐标系的坐标。 view_2.png

2.2 View坐标系

以view的左上角为原点,水平向右为x轴正方向,竖直向下为y轴正方向。

Untitled1.jpg

由上图可以看出

2.2.1. 获取View自身的宽高

int wdith = getRight() - getLeft();
int height = getBottom() - getTop();

Android系统也提供了getWidth()和getHeight()获取View的宽高,其原理也是一致的。部分源码为

//获取View的宽度
public final int getWidth() {
    return mRight - mLeft;
}
//获取View的高度
public final int getHeight() {
    return mBottom - mTop;
}

2.2.2. 获取View距离父控件(ViewGroup)的距离

  • getTop() 获取View自身顶边到父控件顶边的距离。
  • getRight() 获取View自身右边到父控件左边的距离。
  • getBottom() 获取View自身底边到父控件顶边的距离。
  • getLeft() 获取View自身左边到父控件左边的距离。

2.2.3.MotionEvent提供的方法

当View或者ViewGroup获取点击事件的时候,最后都由onTouchEvent(MotionEvent e)方法来处理,MotionEvent提供了获取坐标的方法

  • getX() 获取点击位置距离View左边的距离。
  • getY() 获取点击位置距离View顶边的距离。
  • getRawX() 获取点击位置距离屏幕左边的距离。
  • getRawY() 获取点击位置距离屏幕顶边的距离。

3. View的滑动

3.1 layout()方法

View进行绘制的时候会调用onLayout()方法设置显示的位置。因此可以通过修改View的left,right,top,bottom四个属性来控制View的位置。

  • 首先在手指触摸的时候记录触摸点的坐标。
  • 然后在手指移动的时候重新获取触摸点的坐标,并计算与按下坐标时的偏移量。
  • 最后调用layout()方法重新布局,从而达到移动View的效果。

关键代码如下

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //获取按下的坐标
            downX = (int) event.getX();
            downY = (int) event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            //获取移动的偏移量
            int offsetX = (int) (event.getX() - downX);
            int offsetY = (int) (event.getY() - downY);
            //调用layout重新布局
            layout(getLeft() + offsetX, getTop() + offsetY, getRight() + offsetX, getBottom() + offsetY);
            break;
    }

    return true;
}

3.2 offsetLeftAndRight()方法与offsetTopAndBottom()方法。

offsetLeftAndRight()和offsetTopAndBottom()与layout()方法类似,简化了设置方式。 关键代码如下

 public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //获取按下的坐标
            downX = (int) event.getX();
            downY = (int) event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            //获取移动的偏移量
            int offsetX = (int) (event.getX() - downX);
            int offsetY = (int) (event.getY() - downY);
            //调用offsetxxx()重新布局
            offsetLeftAndRight(offsetX);
            offsetTopAndBottom(offsetY);
            break;
    }

    return true;
}

3.3 改变布局参数LayoutParams

LayoutParams保存了View的布局参数,所以可以通过改变LayoutParams去改变View的位置。

  • 先通过getLayoutParams()方法获取布局参数。
  • 再根据父布局类型强转为需要的类型
  • 计算偏移量
  • 重新设置LayoutParams参数

关键代码如下

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //获取按下的坐标
            downX = (int) event.getX();
            downY = (int) event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            //获取移动的偏移量
            int offsetX = (int) (event.getX() - downX);
            int offsetY = (int) (event.getY() - downY);

            //此处父布局是LinearLayout,所以强转为LinearLayout.LayoutParams
            //LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) getLayoutParams();
            //params.leftMargin = getLeft() + offsetX;
            //params.topMargin = getTop() + offsetY;

			//如果不想考虑父布局的类型,还可以使用ViewGroup.MarginLayoutParams来进行设置
            ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
            marginLayoutParams.leftMargin = getLeft() + offsetX;
            marginLayoutParams.topMargin = getTop() + offsetY;
            setLayoutParams(marginLayoutParams);
            break;
    }

    return true;
}

3.4 动画

通过平移动画也可以来移动View

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true">
    <translate
        android:duration="2000"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="400"
        android:toYDelta="400" />
</set>

Animation animation = AnimationUtils.loadAnimation(this, R.anim.trans);
offetView.setAnimation(animation);

不过View动画仅仅执行了动画,并没有真正改变View的位置参数。所以当View动画执行完成停留在当前位置时,点击此View,并不会触发点击事件,只有点击其原位置才会触发点击事件。为了解决这个问题,Android在3.0引入了属性动画,他不仅可以执行动画,还可以改变View的位置参数,使用也更加简单。

ObjectAnimator.ofFloat(offetView, "translationX", 0, 300)//X轴向右平移300px
            .setDuration(1000)//动画执行时间
            .start();

3.5 scrollBy和scrollTo

scrollBy(dx,dy)表示移动的增量是dx,dy;scrollTo(x,y)表示移动到(x,y)这个坐标点。scrollBy也是调用scrollTo实现的。scrollBy源码如下

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

scrollBy和scrollTo可以移动View和ViewGroup,如果移动的对象是View,则移动的是他的内容,比如TextView中文字的位置,如果移动的对象是ViewGroup,则是移动他的所有子孩子。 自定义一个View,继承TextView,重写onTouchEvent方法,关键代码如下

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //获取按下的坐标
            downX = (int) event.getX();
            downY = (int) event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            //获取移动的偏移量
            int offsetX = (int) (event.getX() - downX);
            int offsetY = (int) (event.getY() - downY);
            scrollTo(-offsetX,-offsetY);
            break;
    }
    return true;
}

需要注意的是此时的坐标方向与平常是相反,所以设置成了偏移量的相反数。

3.6 Scroller

scrollBy和scrollTo在滑动时,整个过程是瞬间完成的,而使用Scroller则可以实现有过度效果的滑动。Scroller是一个辅助类,本身不能实现View的滑动,需要和computeScroll()配合使用。

  • 初始化Scroller,Scroller构造函数可以指定插值器,此处指定为线性插值器,整个动画更加平滑。
public ScrollerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    scroller = new Scroller(context,new LinearInterpolator());

}
  • 重写computeScroll()方法,系统在绘制View的时候在调用draw()的时候会调用此方法,此处通过Scroller不断获取当前的滚动坐标,并通过scrollTo()方法移动的此位置。并且调用invalidate()方法重绘,进而不断调用computeScroll()方法,直至动画完成,即scroller.computeScrollOffset()返回false。
public void computeScroll() {
    super.computeScroll();
    if(scroller.computeScrollOffset()){//判断scroller的移动动画是否完成(true没有)
        ((View)getParent()).scrollTo(scroller.getCurrX(),scroller.getCurrY());
        invalidate();

    }
}
  • 调用startScroll(int startX, int startY, int dx, int dy, int duration)方法开始滑动。
    • startX表示当前视图的x坐标值
    • startY表示当前视图的y坐标值
    • dx表示在当前视图的x坐标基础上横向移动的距离
    • dy表示在当前视图的y坐标基础上纵向移动的距离
    • duration表示视图移动的操作在多少时间内执行完场,也就是动画的持续时间(单位:毫秒)
public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                scroller.startScroll(0,0,-200,-1000,2000);
                invalidate();
                break;
        }
        return true;
    }