我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第4篇文章,点击查看活动详情
我自己公司的业务需求上是基本不会涉及动画相关的,也可以说基本两年没怎么做过动画相关的开发,这次写这篇文章是因为读到一篇比较好的文章,想把它记录下来,以后有做动画效果的需求的话就能快速上手。 参考《Android 开发艺术探索》 7.3.4 对任意属性做动画
众所周知Android里面动画有3种,View动画、帧动画和属性动画。不同的场景使用不同的动画,对控件而言,你想实现一个自定义动画的效果,也就是随心,那就使用属性动画会比较好,因为View动画只提供了简单的平移、翻转、缩放、透明度。 当然这里也不会去介绍一些基础的内容。为什么单独说这一小节,因为我觉得这个地方是这章(第七章)最经典的地方。
一. 对任意属性做动画
按照书上的逻辑来讲。 一开始举了一个栗子,给Button加个动画,让这个Button的宽度增加500px。 直接写属性动画的代码:
ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();
这里是对这个Button的 width属性做动画,也就是对宽度这个属性做动画,但是并不能实现我们想要的宽度增加500px的效果。这时候就先引出了一个重要的结论: 属性动画要求动画作用的对象提供该属性的get和set方法 这个很重要,意思就是说,这个mButton对象,没有提供getWidth和setWidth方法。其实这说法有点那啥,因为Button是有getWidth和setWidth的方法,但是为什么没生效呢? 这是展示了setWidth的源码
public void setWidth(int pixels) {
mMaxWidth = mMinWidth = pixels;
mMaxWidthMode = mMinWidthMode = PIXELS;
requestLayout();
invalidate();
}
发现这里并不是设置Button的宽度,而是设置Button的最大宽度和最小宽度。 也就是说在上面的动画中:真的的是随着时间的改变去改变控件的最大最小宽度,所以视觉看不出效果,因为实际作用的属性不是你想要去让它作用的属性 所以想要实现效果,应该要提供该属性正确的get/set方法
书上又写出了这样的一个结论(原话): 针对上诉问题,官方文档上告诉我们有3种解决方法:
- 给你的对象加上get和set方法,如果你有权限的话
- 用一个类来包装原始对象,间接为其提供get和set方法
- 采用ValueAnimator,监听动画过程,自己实现属性的变化
第一个方案肯定不可行,系统的View不是我们说改就改的,然后用两个Demo分别来描述方法2和方法3(不能复制要手写,好不想写)
方法2:
private static class ViewWrapper{
private View mTarget;
public ViewWrapper(View target){
mTarget = target;
}
public int getWidth(){
return mTarget.getLayoutParams().width;
}
public void setWidth(int width){
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
}
调用的时候
ViewWrapper wrapper = new ViewWrapper(mButton);
ObjectAnimator.ofInt(wrapper , "width", 500).setDuration(5000).start();
这样就能正常展示我们想要的动画效果,对象是wrapper ,它确实为width属性提供了get/set方法。动画内部在执行过程中会拿到传给它的属性,然后用反射去调用它的get/sset方法。但是怎么说呢,具体的属性改变的算法还是要你去手动写。
方法3:
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
private IntEvaluator mEvaluator = new IntEvaluator();
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int currentValue = (Integer) animation.getAnimatedValue();
float fraction = animator.getAnimatedFraction();
view.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
view.requestLayout();
}
});
这个方法就是对动画进行监听,每监听到一帧的时候再做具体的操作。可以看到这里也没写get/set方法,大概就那么一个意思就得了,也不是非得说一定要这样写,或者不用这两种方法也还有其它方法能实现。 总之,属性的具体的变化,是需要我们自己去写逻辑(如果要实现自定义的效果),而ObjectAnimator、ValueAnimator这些属性广告的类,能告诉我们这个动画执行的整个过程,简单的说就是我们知道播放到哪一帧,就能做到在这一帧做什么效果。 这两个Demo也是我觉得这章最有意思的地方。
二. 自己实现一个自定义动画
光看肯定是3天忘,自己动手写个简单的自定义动画效果。 我弄一个圆形View,可以拖动它,点击下去的时候圆会放大并变成正方形,然后移动,松开时正方形会缩小并变回圆,而且我们要让圆变成正方形不是秒变,要有个渐变的效果(只实现了简单的渐变效果,因为懒得去计算)。 最终的效果也没有做成gif,想看效果的直接复制代码去试就行,不多。
####1. 绘制view的初始位置 我这Demo再补充一些细节的东西,还是《Android 开发艺术探索》3.1.2 View的位置参数 不是什么难点,就是有些细节要注意,这个view的位置参数列举了3套 (1) Left, Right, Top, Bottom , 当成一个矩形看(圆也是矩形),就是4条边距离父容器x\y轴的距离。 (2) x, y 左上点的坐标 (3)translationX,translationY 距离父View坐标的偏移量
我就直接贴代码讲吧。 先看Activity布局
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fl_content"
android:padding="100px"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
这里padding一个100px,是为了方便介绍这些方位,单位相同容易看,实际开发中肯定最好用dp做单位。 然后看Activity代码
public class RsActivity extends Activity {
private FrameLayout flContent;
private RoundSquareView roundSquareView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rs);
initView();
}
private void initView(){
flContent = findViewById(R.id.fl_content);
roundSquareView = new RoundSquareView(this);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(150, 150);
roundSquareView.setLayoutParams(lp);
flContent.addView(roundSquareView);
roundSquareView.setX(100);
roundSquareView.setY(100);
}
@Override
protected void onResume() {
super.onResume();
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
Log.v("mmp", "getLeft "+roundSquareView.getLeft());
Log.v("mmp", "getTranslationX "+roundSquareView.getTranslationX());
Log.v("mmp", "getX "+roundSquareView.getX());
roundSquareView.setX(0);
}
}, 1000);
}
}
RoundSquareView是一个自定义View, 先不用管它。 第一个问题来了,为什么要写个延迟,重点啊,因为getLeft ,就是获取第一套的那4个属性,要在view绘制完成之后才能获取到,不然获取到的会是0,加个延迟是为了保证绘制完成(这只是Demo,真实开发中判断绘制完成肯定不能用延时这样玩) 看看显示结果
再看看打印结果
距离顶部和左边都是200px,但是left打印是100,如果把父布局中的padding去掉的话,left会打印0,这就是第二个地方,使用Left的时候要注意padding,并且它不算translationX偏移的部分。
然后translationX也打印的是100,他也不算是padding的部分。
最后getX是200,他是正常的相对于父布局的部分距离,不受任何影响。
也证实了x = left + translationX
这里只是为了介绍一些细节上的东西而已,onResume里面的代码对我们要实现的功能没有任何意义,所以使用时请屏蔽掉。
2. 自定义View
看看代码
public class RoundSquareView extends View {
private float oldRawX = 0;
private float oldRawY = 0;
private int currentValue = 0;
private boolean isExpand;
public RoundSquareView(Context context) {
super(context);
}
public RoundSquareView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RoundSquareView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(1);
int w = getWidth() /2;
int h = getHeight() /2;
if (!isExpand) {
// 非扩大状态下的绘制
canvas.drawCircle(w, h, w + currentValue , paint);
}else {
// 扩大状态下的绘制
canvas.drawCircle(w, h, w + (100 - currentValue) , paint);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
isExpand = false;
expand();
oldRawX = event.getRawX();
oldRawY = event.getRawY();
return true;
case MotionEvent.ACTION_MOVE:
moveView(event.getRawX(), event.getRawY());
return true;
case MotionEvent.ACTION_UP:
oldRawX = 0;
oldRawX = 0;
isExpand = true;
narrow();
return true;
}
return super.onTouchEvent(event);
}
@Override
public boolean performClick() {
return super.performClick();
}
private void moveView(float rawX, float rawY){
// 计算偏移量
float offsetX = rawX - oldRawX;
float offsetY = rawY - oldRawY;
// 更改位置
setX(getX()+offsetX);
setY(getY()+offsetY);
// 更新位置
oldRawX = rawX;
oldRawY = rawY;
}
/**
* 扩大
*/
private void expand(){
int w = getWidth();
int h = getHeight();
ViewGroup.LayoutParams lp = this.getLayoutParams();
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentValue = (int) animation.getAnimatedValue();
Log.v("mmp", "动画进度 "+currentValue);
lp.width = w + (w/100 * currentValue);
lp.height = h + (h/100 * currentValue);
RoundSquareView.this.setLayoutParams(lp);
}
});
valueAnimator.setDuration(300).start();
}
/**
* 缩小
*/
private void narrow(){
int w = getWidth();
int h = getHeight();
ViewGroup.LayoutParams lp = this.getLayoutParams();
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentValue = (int) animation.getAnimatedValue();
Log.v("mmp", "动画进度 "+currentValue);
lp.width = w - (w/200 * currentValue);
lp.height = h - (h/200 * currentValue);
RoundSquareView.this.setLayoutParams(lp);
}
});
valueAnimator.setDuration(300).start();
}
}
自定义View,在draw中用Paint画个圆,这个,这个没什么好说的,都能看懂,最后
if (!isExpand) {
// 非扩大状态下的绘制
canvas.drawCircle(w, h, w + currentValue , paint);
}else {
// 扩大状态下的绘制
canvas.drawCircle(w, h, w + (100 - currentValue) , paint);
}
先不用管,先看成
canvas.drawCircle(w, h, w, paint);
这样就画成一个圆了。 然后写onTouchEvent让view随手指移动而移动。记录改变前的手指的点击位置oldRawX,oldRawY,去获取当前的位置减去旧的位置就能得到一个偏移量,然后再用这个偏移量去改变x和y属性,这是最简单的view随手指移动的方式。 然后在按下时调用expand()做放大的属性动画,再抬起时调用narrow()做缩小的属性动画。
这里是用了上面的方法3去实现属性动画,这里离就不多说了,定义了一个变量isExpand来记录当前是否处于放大的状态。 最后,这个Demo只是简单的实现动画的效果,会存在一些问题,比如说在放大动画的执行过程中抬起,这些都没做处理,时间问题,就不打算花太多时间在这个Demo上。
文章出自www.jianshu.com/p/ad80cea0a… 当然也是自己写的,是同一个号,只是最近打算转平台
这是很早之前写的文章,当时还是有点年轻,技术差点火候。这里更正一下,onResume那不用延时,用post就行