安卓View体系

183 阅读7分钟

Android View详解

一、什么是View

### View是所有控件的基类,下到各种Button,TextView,ImageView,上到LinearLayout,RelateLayout,
甚至你自定义的控件,都是继承View这个基类,所以说,View是代表着界面层的一个抽象控件。除了View,
还有一种叫ViewGroup,也就是控件组。ViewGroup本身也继承了View,View可以理解为存放很多View的组合,
例如,LinearLayout本身是一个View,它也是一个ViewGroup。ViewGroup里面有子View,这个子View也是一个ViewGroup。

二、View的位置参数

View和位置主要由它的四个顶点来决定,分别对应View的四个属性:top、left、right、bottom,top是左上角纵坐标, left是左上角横坐标,right是右下角横坐标,bottom是右下角纵坐标;

View的宽高和坐标的关系:width = right - left;

hight = bottom - top;

如何得到View的这四个参数呢?

              left = getLeft();

              right = getRight();

              top = getTop();

              bottom = getBottom();

三、能够实现View滑动的方法:

使用 scrollTo/scrollBy

使用动画

使用LayoutParams

使用Layout

1.使用scrollTo/scrollBy

        public void scrollTo(int x, int y) {
           
        if (mScrollX != x || mScrollY != y) {
          
        int oldX = mScrollX;
        
        int oldY = mScrollY;
        
        mScrollX = x;
        
        mScrollY = y;
        
        invalidateParentCaches();
        
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        
        if (!awakenScrollBars()) {
        
            postInvalidateOnAnimation();
            
        }
        
        }
    
        }
        
          public void scrollBy(int x, int y) {
          
         scrollTo(mScrollX + x, mScrollY + y);
         
           }

2、动画

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

</set>
2.
        watchView.setAnimation(AnimationUtils.loadAnimation(this,R.anim.translate_view));
        view动画不能改变view的位置参数
        或者利用属性动画来移动
        ObjectAnimator.ofFloat(watchView,"translationX",0,300)
        .setDuration(1000)
        .start();

3、改变布局参数 在这里我们将介绍第三种实现View滑动的方法,那就是改变布局参数,即改变LayoutParams。 这个就比较好理解了,比如我们想把一个 Button 向右平移100px,我们只需要将这个 Button 的 LayoutParams 里的 marginLeft 参数的值增加 100px 即可,是不是很简单呢? 还有一种情形,为了达到移动 Button 的目的,我们还可以在 Button 的左边放置一个空的 View, 这个空 View 的默认宽度为 0,当我们需要向右滑动 Button 时,只需要重新设置空 View 的宽度即可, 当空 View 的宽度增大时(假设 Button 的父容器是水平方向的 LinearLayout), Button 就会被自动挤向右边,这样也间接的实现了 View 向右平移的效果。 如何重新设置一个 View 的 LayoutParams 呢? 很简单, 如下所示:

          scrollTo.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View v) {
            RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) scrollTo.getLayoutParams();
            layoutParams.leftMargin +=100;
      //                 scrollTo.requestLayout();
               scrollTo.setLayoutParams(layoutParams);
          }
         });

4、使用layout方法 在 View 进行绘制时,会调用 onLayout() 方法来设置显示的位置。同样我们可以通过修改 View 的 left、top、right、bottom 四个属性来控制 View 的坐标。

四、ViewPropertyAnimator

使用方式:View.animate() 后跟 translationX() 等方法,动画自动运行

      view.animate().translationX(500);

五、1ObjectAnimator

使用方式:

如果是自定义控件,需要添加 setter / getter 方法,并在setter方法的最后调用invalidate()方法,刷新绘制;

用 ObjectAnimator.ofXXX() 创建 ObjectAnimator 对象;

用 start() 方法执行动画。

    public class SportsView extends View {

     float progress = 0;

     ......

   // 创建 getter 方法
   public float getProgress() {
    return progress;
   }

   // 创建 setter 方法
  public void setProgress(float progress) {
      this.progress = progress;
      invalidate();
   }

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

      ......

        canvas.drawArc(arcRectF, 135, progress * 2.7f, false, paint);

      ......
   }
}

六、设置监听事器

给动画设置监听器,可以在关键时刻得到反馈,从而及时做出合适的操作,例如在动画的属性更新时同步更新其他数据,或者在动画结束后回收资源等。

设置监听器的方法, ViewPropertyAnimator 和 ObjectAnimator 略微不一样: ViewPropertyAnimator用的是 setListener() 和 setUpdateListener()方法,可以设置一个监听器,要移除监听器时通过 set[Update]Listener(null) 填 null 值来移除;而 ObjectAnimator则是用 addListener() 和 addUpdateListener() 来添加一个或多个监听器,移除监听器则是通过 remove[Update]Listener() 来指定移除对象。

另外,由于 ObjectAnimator 支持使用 pause()方法暂停,所以它还多了一个 addPauseListener() / removePauseListener() 的支持; 而 ViewPropertyAnimator 则独有 withStartAction() 和 withEndAction() 方法,可以设置一次性的动画开始或结束的监听,在动画执行结束后就自动丢弃,就算之后再重用 ViewPropertyAnimator 来做别的动画,用它们设置的回调也不会再被调用。而 set/addListener() 所设置的 AnimatorListener 是持续有效的,当动画重复执行时,回调总会被调用。

需要说明一下的是,就算动画被取消,onAnimationEnd() 也会被调用。所以当动画被取消时,如果设置了 AnimatorListener,那么 onAnimationCancel()和 onAnimationEnd() 都会被调用。onAnimationCancel() 会先于 onAnimationEnd() 被调用。

withEndAction() 设置的回调只有在动画正常结束时才会被调用,而在动画被取消时不会被执行。这点和 AnimatorListener.onAnimationEnd() 的行为是不一致的。

七 TypeEvaluator

关于 ObjectAnimator,上面讲到可以用 ofInt() 来做整数的属性动画和用ofFloat() 来做小数的属性动画。这两种属性类型是属性动画最常用的两种,不过在实际的开发中,可以做属相动画的类型还是有其他的一些类型。当需要对其他类型来做属性动画的时候,就需要用到 TypeEvaluator 了。 自定义 Evaluator 借助于 TypeEvaluator,属性动画就可以通过 ofObject()来对不限定类型的属性做动画了。

        private class PointFEvaluator implements TypeEvaluator<PointF> {
         PointF newPoint = new PointF();

          @Override
         public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
          float x = startValue.x + (fraction * (endValue.x - startValue.x));
         float y = startValue.y + (fraction * (endValue.y - startValue.y));

       newPoint.set(x, y);
 
         return newPoint;
         }
       }

       ObjectAnimator animator = ObjectAnimator.ofObject(view, "position",
       new PointFEvaluator(), new PointF(0, 0), new PointF(1, 1));
       animator.start();

八、自定义ViewGroup

一般自定义ViewGroup需要重新的方法

 void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
 
  void onSizeChanged(int w, int h, int oldw, int oldh)
  
  void onLayout(boolean changed, int left, int top, int right, int bottom)
  
  void onDraw(Canvas canvas)

注:ViewGroup的onLayout方法是必须重写的,而onDraw方法默认是不会调用的,如果想执行onDraw方法,可以通过下面两种方法: 1.设置透明背景: 在构造函数中:setBackgroundColor(Color.TRANSPARENT);

或者在xml中:android:background="@color/transparent"

2.或者可以在构造函数中添加setWillNotDraw(false);

ViewGroup常用重写方法:

onMeasure

measureChildren方法触发所有子View的onMeasure方法测量自己并把测量结果回传给ViewGroup(ViewGroup传递给子View建议宽高和测量模式,如果子View的宽高是wrap_content,那么只有子View测量出自己的宽和高),当所有子View测量完毕后,再调用setMeasuredDimension将ViewGroup自身的宽和高传给它的父View。

onSizeChanged

在onMeasure()后执行,只有大小发生了变化才会执行onSizeChange

onLayout

排列所有子View的位置

通过getChildCount()获取所有子view,getChildAt获取childview调用各自的layout(int, int, int, int)方法来排列自己。

onDraw 自定义ViewGroup默认不会触发onDraw方法,需要设置背景色或者setWillNotDraw(false)来手动触发。

     public class CustomFiveRings extends ViewGroup {
      private Context mContext;
      private TextPaint mPaint;
     private int startX;//圆环起始X轴
      private int startY;//圆环起始Y轴
     private int mWidth;//ViewGroup的宽
      private int mHeight;//ViewGroup的高
       public CustomFiveRings(Context context) {
    this(context, null);
}
public CustomFiveRings(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}
public CustomFiveRings(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    setBackgroundColor(Color.TRANSPARENT);
    mContext = context;
    mPaint = new TextPaint();
    mPaint.setAntiAlias(true);
    mPaint.setDither(true);
    mPaint.setTextSize(50);
    mPaint.setColor(Color.BLACK);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //触发所有子View的onMeasure函数去测量宽高
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    //MeasureSpec封装了父View传递给子View的布局要求
    int wMode = MeasureSpec.getMode(widthMeasureSpec);
    int wSize = MeasureSpec.getSize(widthMeasureSpec);
    int hMode = MeasureSpec.getMode(heightMeasureSpec);
    int hSize = MeasureSpec.getSize(heightMeasureSpec);
    switch (wMode) {
        case MeasureSpec.EXACTLY:
            mWidth = wSize;
            break;
        case MeasureSpec.AT_MOST:
            //这里应该先计算所有子view的宽度,暂时先写死
            mWidth = wSize;
            break;
        case MeasureSpec.UNSPECIFIED:
            break;
    }
    switch (hMode) {
        case MeasureSpec.EXACTLY:
            mHeight = hSize;
            break;
        case MeasureSpec.AT_MOST:
            //这里应该先计算所有子view的高度,暂时先写死
            mHeight = hSize;
            // mHeight = getCircleHeight() / 2 * 3;
            break;
        case MeasureSpec.UNSPECIFIED:
            break;
    }
    setMeasuredDimension(mWidth, mHeight);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childNum = getChildCount();
    startX = startY = 0;
    int gap = 10;//同一行圆圈之间的间隔
    int screenWidth = DpUtil.getScreenSizeWidth(mContext);
    int firstTotalWidth = 0;//第一行子View的总宽度
    int secondTotalWidth = 0;//第二行子View的总宽度
    for (int i = 0; i < childNum; i++) {
        View childView = getChildAt(i);
        int childWidth = childView.getMeasuredWidth();
        if (i <= 2) {
            //前三个总宽度
            firstTotalWidth += childWidth;
        } else {
            //后两个总宽度
            secondTotalWidth += childWidth;
        }
    }
    int leftFMargin = (screenWidth - firstTotalWidth - gap * 2) / 2;
    int leftSMargin = (screenWidth - secondTotalWidth - gap) / 2;
    for (int i = 0; i < childNum; i++) {
        View childView = getChildAt(i);
        int childWidth = childView.getMeasuredWidth();
        int childHeight = childView.getMeasuredHeight();
        if (i <= 2) {
            //排列前三个圆圈
            if (i == 0) {
                childView.layout(leftFMargin + startX, startY, leftFMargin + startX + childWidth, startY + childHeight);
                startX += childWidth;
            } else {
                childView.layout(leftFMargin + startX + gap, startY, leftFMargin + startX + gap + childWidth, startY + childHeight);                    startX += (childWidth + gap);
            }
            if (i == 2) {
                startX = 0;
                startY += childHeight / 2;
            }
        } else {
            //排列后两个圆圈
            if (i == 3) {
                childView.layout(leftSMargin + startX, startY, leftSMargin + startX + childWidth, startY + childHeight);
                startX += childWidth;
            } else {
                childView.layout(leftSMargin + startX + gap, startY, leftSMargin + startX + gap + childWidth, startY + childHeight);                    startX += (childWidth + gap);
            }
        }
    }
}
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    int screenWidth = DpUtil.getScreenSizeWidth(mContext);
    String upStr = "同一个世界,同一个梦想";
    String downStr = "One World,One Dream";
    canvas.drawText(upStr, (screenWidth - mPaint.measureText(upStr)) / 2, getCircleHeight() / 2 * 3 + 60, mPaint);
    canvas.drawText(downStr, (screenWidth - mPaint.measureText(downStr)) / 2, getCircleHeight() / 2 * 3 + 120, mPaint);
} 
       /**
    * 获得圆环高度
  *
 * @return 圆环高度
    */
  private int getCircleHeight() {
     //5个圆环大小是一样的,这里就直接取第一个了
           View childView = getChildAt(0);
          return childView.getMeasuredHeight();
                }}