Android 与 View体系

84 阅读8分钟

对于一个App来说,与用户的交互、将内容展示给用户,既是十分重要的,也是十分必要的,而这些就是一个个View通过拓展实现的。View就如同现实世界的原子一般,是实现界面展示和交互的最小“微粒”。

View与ViewGroup

  • View是Android所有控件的基类
  • ViewGroup是View的组合,它可以包含许多View以及ViewGroup,ViewGroup又可以包含许多View和ViewGroup,从而形成一个View树

1.PNG

  • ViewGroup也继承自View,ViewGroup作为View或者ViewGroup这些组件的容器,派生了多种布局控件子类,如LinearLayout,RalativeLayout等。一般我们不会直接使用View或ViewGroup,而是使用他们的派生类。下图列出了View的部分继承关系:

2.PNG

坐标系

Android坐标系

3.PNG

  • getRawX()
  • getRawY()

View坐标系

4.PNG 1.View的宽和高

width = getRight()-getLeft();
height = getBottom()-getTop();

width = getWidth();
height = getHeight();

2.View自身的坐标

  • getTop()
  • getLeft()
  • getRight()
  • getBottom()

3.MotionEvent提供的方法

  • 视图坐标

    • getX()
    • getY()
  • 绝对坐标

    • getRawX()
    • getRawY()

View的滑动

1.layout()方法

自定义一个View,在onTouchEvent()方法中获取触摸点的位置,在Action_MOVE事件中计算偏移量,再调用layout()方法重新繁殖View的位置。

public class CustomView extends View {
    private int lastX;
    private int lastY;
​
    public CustomView(Context context, AttributeSet attrs){
        super(context, attrs);
        mScroller = new Scroller(context);
    }
​
    public CustomView(Context context){
        super(context);
    }
​
    public boolean onTouchEvent(MotionEvent event){
        //获取手指触摸点的横坐标和纵坐标
        int x = (int) event.getX();
        int y = (int) event.getY();
​
        switch(event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //计算移动的距离
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                //调用layout方法来重新放置它的位置
                layout(getLeft()+offsetX, getTop()+offsetY,getRight()+offsetX, getBottom()+offsetY);
        }
        return true;
    }
}

最后在布局中引用自定义View即可

<com.example.myapplication.CustomView
        android:id="@+id/customview"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_margin="50dp"
        android:background="@android:color/holo_red_light" />

2.offsetLeftAndRight()与offsetAndBottom()

与layout()方法类似,即将AcTION_MOVE中的代码替换如下:

case MotionEvent.ACTION_MOVE:
    //计算移动的距离
    int offsetX = x - lastX;
    int offsetY = y - lastY;
    //对left和right偏移
    offsetLeftAndRight(offsetX);
    //对top和bottom偏移
    offsetTopAndBottom(offsetY);

2.LayoutParams

LayoutParams主要保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参数从而达到改变View位置的效果。

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

如果父控件是RelativeLayout, 则要使用RelativeLayout.LayoutParams。除了使用布局的LayoutParams外,我们还可以用 ViewGroup.MarginLayoutParams来实现:

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

3.动画

可以采用View动画来移动,在res目录新建anim文件夹并创建translate.xml:

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

注意:必须加上fillAfter="true"移动后才不会回到原来位置,View动画并不能改变View的位置参数。

在java代码中调用

mCustomView.setAnimation(AnimationUtils.loadAnimation(this,R.anim.translate));

4.scrollTo 与 scrollBy

  • scrollTo(x,y): 移动到一个具体的坐标点,
  • scrollBy(dx,dy): 移动的增量为dx、dy

我们将 ACTION_MOVE中的代码替换成如下代码:

((View)getParent()).scrollBy(-offsetX,-offsetY)

ps:若要实现 CustomView 随手指移动的效果,就需要将偏移量设置为负值。 因为scrollBy移动的是手机屏幕,而不是画布的内容。

5.Scroller

用scrollTo和scrollBy方法完成滑动是瞬时完成的,所以体验不太好,可以用Scroller实现有过度效果的滑动。 Scroller本身是不能实现View的滑动的,它需要与View的computeScroll()方法配合才能实现弹性滑动的效果。

初始化scroller

public CustomView(Context context, AttributeSet attrs){
        super(context, attrs);
        mScroller = new Scroller(context);
}

重写computeScroll()方法,调用父类的crollTo()方法并通过scroller获取当前滚动值,每滑动一小段距离就调用invalidate()方法重绘,重绘就会调用computeScroll()方法,这样通过不断移动一小段距离实现平滑移动。

//绘制view时在draw()方法中调用该方法
@Override
public void computeScroll() {
super.computeScroll();
    if(mScroller.computeScrollOffset()){ 
        ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        //重绘
        invalidate();
    }
}

在CustomView中写一个 个 smoothScrollTo 方法,调用 Scroller 的 startScroll()方法,在2000ms内沿X轴平移delta像素

public void smoothscrollTo(int destX,int destY){
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        //在2000ms内沿x轴平移delta像素
        mScroller.startScroll(scrollX,0,delta,0,2000);
        invalidate();
    }

最后在activity中调用,设定延x轴向右平移400像素

mCustomView.smoothScrollTo(-400,0);

属性动画

1.ObjectAnimator

实现一个平移动画

ObjectAnimator mObjectAnimator = ObjectAnimator.ofFloat(view,"translationX",200);
mObjectAnimator.setDuration(200);
mObjectAnimator.start();

常用属性值

  • translationXtranslationY:用来沿着X轴或者Y轴进行平移
  • rotationrotationXrotationY:用来围绕View的支点进行旋转
  • PrivotXPrivotY:控制View对象的支点位置,默认该支点 位置就是View对象的中心点。
  • alpha:透明度,默认是1(不透明),0代表完全透明。
  • x和y:描述View对象在其容器中的最终位置。

2.ValueAnimator

ValueAnimator不提供任何动画效果,它更像一个数值发生器,用来产生有一定规律的数字,从而让调 用者控制动画的实现过程。

ValueAnimatorAnimatorUpdateListener中监听数值的变化,从 而完成动画的变换

ValueAnimator mValueAnimator = ValueAnimator.ofFloat(0,100);
mValueAnimator.setTarget(view);
mValueAnimator.setDuration(1000).start();
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator valueAnimator) {
        Float mFloat = (Float)valueAnimator.getAnimatedValue();
    }
});

3.动画的监听

完整的动画具有start、Repeat、End、Cancel这4个过程

ObjectAnimator animator = ObjectAnimator.ofFloat(view,"alpha",1.5f);
    animator.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animator) {
        }
        @Override
        public void onAnimationEnd(Animator animator) {
        }
        @Override
        public void onAnimationCancel(Animator animator) {
        }
        @Override
        public void onAnimationRepeat(Animator animator) {
        }
    });

大部分时候我们只关心 onAnimationEnd 事件,Android 也提供了 AnimatorListenterAdaper来让我们选择必要的事件进行监听:

ObjectAnimator animator = ObjectAnimator.ofFloat(mCustomView,"translationX",0,200);
animator.setDuration(1000).start();
animator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
        super.onAnimationEnd(animation);
        Toast.makeText(MainActivity.this, "动画结束", Toast.LENGTH_SHORT).show();
    }
});

4.组合动画—— AnimatorSet

AnimatorSet 类提供了一个 play()方法,如果我们向这个方法中传入一个 Animator 对象 (ValueAnimatorObjectAnimator),将会返回一个AnimatorSet.Builder的实例。

Builder 类采用了建造者模式,每次调用方法时都返回 Builder 自身用于继续构建。 AnimatorSet.Builder中包括以下4个方法。

  • after(Animator anim):将现有动画插入到传入的动画之后执行。
  • after(long delay):将现有动画延迟指定毫秒后执行。
  • before(Animator anim):将现有动画插入到传入的动画之前执行。
  • with(Animator anim):将现有动画和传入的动画同时执行。

示例:

ObjectAnimator animator1 = ObjectAnimator.ofFloat(mCustomView,"translationX",0.0f,200.0f,0f);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(mCustomView,"scaleX",1.0f,2.0f);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(mCustomView,"rotationX",0.0f,90.0f,0.0f);
AnimatorSet set = new AnimatorSet();
set.setDuration(1000);
set.play(animator1).with(animator2).after(animator3);

在 这里先执行 animator3,然后同时执行 animator1 和 animator2。

5.组合动画—— PropertyValuesHolder

除了上面的AnimatorSet类,还可以使用PropertyValuesHolder类来实现组合动画,使用PropertyValuesHolder类只能是多个动画一起执行:

PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("translationX",0.0f,200.0f);
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("scaleX",1.0f,2.0f);
PropertyValuesHolder holder3 = PropertyValuesHolder.ofFloat("rotationY",0.0f,30.0f);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mCustomView,holder1,holder2,holder3);
animator.setDuration(2000).start();

在XML中使用属性动画

在res文件中新建animator文件,在里面新建一个 scale.xml

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:duration="3000"
    android:propertyName="scaleX"
    android:valueFrom="1.0"
    android:valueTo="2.0"
    android:valueType="floatType">
</objectAnimator>

在程序中引用属性动画

Animator animator = AnimatorInflater.loadAnimator(this,R.animator.scale);
animator.setTarget(view);
animator.start();

3.5解析Scroller

我们先来看看Scroller的构造方法:

    /**
     * Create a Scroller with the default duration and interpolator.
     */
    public Scroller(Context context) {
        this(context, null);
    }

    /**
     * Create a Scroller with the specified interpolator. If the interpolator is
     * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
     * be in effect for apps targeting Honeycomb or newer.
     */
    public Scroller(Context context, Interpolator interpolator) {
        this(context, interpolator,
                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    }

    /**
     * Create a Scroller with the specified interpolator. If the interpolator is
     * null, the default (viscous) interpolator will be used. Specify whether or
     * not to support progressive "flywheel" behavior in flinging.
     */
    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
        mFinished = true;
        if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }
        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;

        mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
    }

Scroller有三个构造方法,通常情况下我们都用第一个;第二个需要传进去一个插值器Interpolator,如果不传则采用默认的插值器ViscousFluidInterpolator

接下来看看ScrollerstartScroll()方法:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
    //设置模式为滑动
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAnimationTimeMillis();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    //计算每一毫秒时间的大小
    mDurationReciprocal = 1.0f / (float) mDuration;
}

startScroll()方法中并没有调用类似开启滑动的方法,而是保存了传进来的各种参数,因此该方法并不能使 View 滑动,关键是我们在startScroll()方法后调用了invalidate()方法,这个方法会导致View的重绘,而View的重绘会调用View的draw()方法,draw()方法又会调用View的computeScroll()方法:

//绘制view时在draw()方法中调用该方法
@Override
public void computeScroll() {
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        ((View) getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        //重绘
        invalidate();
    }
}

computeScroll()方法中通过 Scroller 来获取当前的 ScrollX 和 ScrollY,然后调用scrollTo()方法进行View的滑动,接着调用invalidate 方法来让View进行重绘,重绘就会调用computeScroll()方法来实现View的滑动。

该方法还会调用 computeScrollOffset() 方法:

/**
 * Call this when you want to know the new location.  If it returns true,
 * the animation is not yet finished.
 */ 
public boolean computeScrollOffset() {
    //判断滑动是否结束
    if (mFinished) {
        return false;
    }

    int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

    //如果动画运行时间小于设置的滑动持续时间则执行switch
    if (timePassed < mDuration) {
        switch (mMode) {
        case SCROLL_MODE:
            //根据插值器计算在该时间段内移动的距离
            final float x = mInterpolator.getInterpolation(timePassed *mDurationReciprocal);
            mCurrX = mStartX + Math.round(x * mDeltaX);
            mCurrY = mStartY + Math.round(x * mDeltaY);
            break;
        case FLING_MODE:
            final float t = (float) timePassed / mDuration;
            final int index = (int) (NB_SAMPLES * t);
            float distanceCoef = 1.f;
            float velocityCoef = 0.f;
            if (index < NB_SAMPLES) {
                final float t_inf = (float) index / NB_SAMPLES;
                final float t_sup = (float) (index + 1) / NB_SAMPLES;
                final float d_inf = SPLINE_POSITION[index];
                final float d_sup = SPLINE_POSITION[index + 1];
                velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                distanceCoef = d_inf + (t - t_inf) * velocityCoef;
            }

            mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;

            mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
            // Pin to mMinX <= mCurrX <= mMaxX
            mCurrX = Math.min(mCurrX, mMaxX);
            mCurrX = Math.max(mCurrX, mMinX);

            mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
            // Pin to mMinY <= mCurrY <= mMaxY
            mCurrY = Math.min(mCurrY, mMaxY);
            mCurrY = Math.max(mCurrY, mMinY);

            if (mCurrX == mFinalX && mCurrY == mFinalY) {
                mFinished = true;
            }
            break;
        }
    }
    else {
        mCurrX = mFinalX;
        mCurrY = mFinalY;
        //设置结束标志
        mFinished = true;
    }
    return true;
}

因为在startScroll()方法中的mMode值为SCROLL_MODE,所以执行分支语句SCROLL_MODE。

总结: Scroller 并不能直接实现View的滑动,它需要配合View的computeScroll()方法。在computeScroll()中不断让View进行重绘,每次重绘都会计算滑动持续的时间,根据这个持续时间就能算出这次View滑动的位置,我们根据每次滑动的位置调用 scrollTo() 方法进行滑动,这样不断地重复上述过程就形成了弹性滑动。