对于一个App来说,与用户的交互、将内容展示给用户,既是十分重要的,也是十分必要的,而这些就是一个个View通过拓展实现的。View就如同现实世界的原子一般,是实现界面展示和交互的最小“微粒”。
View与ViewGroup
- View是Android所有控件的基类
- ViewGroup是View的组合,它可以包含许多View以及ViewGroup,ViewGroup又可以包含许多View和ViewGroup,从而形成一个View树
- ViewGroup也继承自View,ViewGroup作为View或者ViewGroup这些组件的容器,派生了多种布局控件子类,如LinearLayout,RalativeLayout等。一般我们不会直接使用View或ViewGroup,而是使用他们的派生类。下图列出了View的部分继承关系:
坐标系
Android坐标系
getRawX()getRawY()
View坐标系
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();
常用属性值
translationX和translationY:用来沿着X轴或者Y轴进行平移rotation、rotationX、rotationY:用来围绕View的支点进行旋转PrivotX和PrivotY:控制View对象的支点位置,默认该支点 位置就是View对象的中心点。alpha:透明度,默认是1(不透明),0代表完全透明。- x和y:描述View对象在其容器中的最终位置。
2.ValueAnimator
ValueAnimator不提供任何动画效果,它更像一个数值发生器,用来产生有一定规律的数字,从而让调 用者控制动画的实现过程。
在ValueAnimator的AnimatorUpdateListener中监听数值的变化,从 而完成动画的变换
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 对象 (ValueAnimator或ObjectAnimator),将会返回一个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。
接下来看看Scroller的startScroll()方法:
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() 方法进行滑动,这样不断地重复上述过程就形成了弹性滑动。