这是我参与「第四届青训营 」笔记创作活动的的第1天
View
体系是较为复杂的,但是又非常重要的一个知识点。我们把这部分知识吃透吃熟是十分必要的,打卡第一天,我把View体系的第一部分知识整理出来,快来和我一起学习吧。
View树结构
官方给出我们使用的各种布局和各种 View
都是继承自 ViewGroup
、 View
或者他们的派生类。所以,了解View体系是极其重要的任务
如下图的 View部分继承关系
,我们可以看到常用的 View
组件、布局组件是如何继承的。
坐标系
学习 View
,首先需要知道 View
的位置在 Android
中是如何定义和测量的。
上图之中的蓝色和绿色是有着不同作用含义,我们平时使用也是在不同的地方调用
绿色:在
View
中获得View
到其父控件之间的距离蓝色:来自于点击事件
MotionEvent
内部的方法,可以在重写View
事件分发体系的的三大方法的时候,利用传入的事件调用上图的蓝色方法,获取点击的位置坐标
获取坐标绘制View的滑动
//自定义一个View,点击该View可以随意滑动其位置 //下面有5个方法可以实现,其中两个由于理解为移动的是屏幕框,会使得其他元素一起偏移 public class CoutomView extends View { private int lastX; private int lastY; Scroller mScroller; public CoutomView(Context context) { super(context); } public CoutomView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); mScroller = new Scroller(context); } public CoutomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public CoutomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public void computeScroll() { super.computeScroll(); if (mScroller.computeScrollOffset()){ ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); invalidate(); } } @Override 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; // M1 layout(getLeft()+offsetX,getTop()+offsetY, getRight()+offsetX,getBottom()+offsetY); // M2 // offsetLeftAndRight(offsetX); // offsetTopAndBottom(offsetY); // M3 // ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams)getLayoutParams(); // layoutParams.leftMargin = getLeft()+offsetX; // layoutParams.topMargin = getTop()+offsetY; // setLayoutParams(layoutParams); // M4 会使得其他元素一起偏移 // ((View)getParent()).scrollBy(-offsetX,-offsetY); break; } return true; } /** * M5 * 提供给Activity调用滑动,也会使得其他元素一起偏移 * * @param destX * @param destY */ public void smoothScrollTo(int destX,int destY){ int scrollX = getScrollX(); int delta = destX - scrollX; mScroller.startScroll(scrollX,0,delta,0,2000); invalidate(); } } 复制代码
属性动画
区别
View动画:只展示普通动画效果,响应的点击时间的位置依旧在原来的地方。所以无法左交互效果
属性动画:利用属性和对象来控制 View ,同时使得动画执行后可交互
属性动画的执行,可以带上属性以及属性参数
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) //使用传入 ObjectAnimator.ofFloat(binding.coutomView,"translationX",0f,300f).setDuration(1000).start() 复制代码
ObjectAnimator
常用属性
translationX
和translantionY
沿轴平移rotatian
、rotatianX
和rotatianY
沿着某支点进行旋转PrivotX
和PrivotY
可以控制支点位置,围绕支点旋转和缩放,支点默认为中心位置、alpha
透明度,默认为1,1不透明 0 全透明x
,y
View的终点位置
使用
ObjectAnimator
时,要调用某个属性,该属性需要有对应的get()
和set()
方法。若是没有,我们就需要自定义一个属性类或者包装类添加该方法
//MainActivity val myView = MyView(binding.button) ObjectAnimator.ofInt(myView,"width",500).setDuration(500).start() //MyView,给MyView里面的 width 添加一个 set() 和 get() 功能 class MyView(private val mTarget: View) { var width: Int get() = mTarget.layoutParams.width set(width) { mTarget.layoutParams.width = width mTarget.requestLayout() } } 复制代码
ValueAnimator
这个方法不提供动画效果,类似数值发生器,你需要根据里面的 AnimatorUpdateListener
来监听数值,设置动画变化
//传入的值被 a.animatedValue 获取到,根据该值设置做动画 val animator = ValueAnimator.ofFloat(0f,100f).apply { setTarget(binding.button2) duration = 1000 start() addUpdateListener { a -> val mFloat = a.animatedValue as Float binding.button2.rotation = mFloat binding.button2.translationX = 100f } } //复杂些的动画 binding.button8.setOnClickListener { val anim = ValueAnimator.ofFloat(0f, 360f) anim.addUpdateListener { animation -> val angle = animation.animatedValue as Float binding.layer.rotation = angle binding.layer.scaleX = 1 + (180 - Math.abs(angle - 180)) / 20f binding.layer.scaleY = 1 + (180 - Math.abs(angle - 180)) / 20f var shift_x = 500 * Math.sin(Math.toRadians((angle * 5).toDouble())).toFloat() var shift_y = 500 * Math.sin(Math.toRadians((angle * 7).toDouble())).toFloat() binding.layer.translationX = shift_x binding.layer.translationY = shift_y } anim.duration = 4000 anim.start() } 复制代码
动画的监听
//完整的监听,四个过程 ObjectAnimator.ofFloat(binding.layer,"alpha",1.5f).addListener(object : Animator.AnimatorListener{ override fun onAnimationStart(p0: Animator?) { TODO("Not yet implemented") } override fun onAnimationEnd(p0: Animator?) { TODO("Not yet implemented") } override fun onAnimationCancel(p0: Animator?) { TODO("Not yet implemented") } override fun onAnimationRepeat(p0: Animator?) { TODO("Not yet implemented") } }) //不完整的监听,匿名类中,重写其中的一个方法 ObjectAnimator.ofFloat(binding.layer,"alpha",0f,1f,0f,1f,0f,1f).apply { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) } }) duration = 10000 start() } 复制代码
AnimatorSet 组合动画
我们可以调用 AnimatorSet
里面的 play()
方法,以及该方法内部的类,就可以完成多个动画的组合展示功能
//eg,下面的执行顺序是 a3 -> a2 -> a1 val a1 = ObjectAnimator.ofFloat(binding.coutomView,"translationX",0f,300f,0f) val a2 = ObjectAnimator.ofFloat(binding.coutomView,"scaleX",1.0f,2.0f) val a3 = ObjectAnimator.ofFloat(binding.coutomView,"rotationX",0.0f,90f,0.0f) val set = AnimatorSet().apply { duration = 1000 play(a1).with(a2).after(a3) start() } 复制代码
//简单展示下对应方法的结构 //play() public AnimatorSet.Builder play(Animator anim){ if(anim != null) return new Builder(anim); return null; } //Builder 结构,对应源码可自行查看 public class Builder { Builder() { throw new RuntimeException("Stub!"); } public AnimatorSet.Builder with(Animator anim) { throw new RuntimeException("Stub!"); } public AnimatorSet.Builder before(Animator anim) { throw new RuntimeException("Stub!"); } public AnimatorSet.Builder after(Animator anim) { throw new RuntimeException("Stub!"); } public AnimatorSet.Builder after(long delay) { throw new RuntimeException("Stub!"); } } 复制代码
after(Animator anim)
当下Builder
的动画放到传入的动画之后执行
after(long delay)
当下Builder
的动画延迟指定的毫秒执行
before(Animator anim)
当下Builder
的动画放到传入的动画之前执行
with(Animator anim)
当下Builder
的动画与传入的动画并行执行
根据这个属性,浅析一下这段代码的逻辑。play(a1).with(a2).after(a3)
,
- 首先传入
play()
的是a1
,会返回一个含有a1
的Builder
对象,我们简称这个对象为b1
- 再次调用
with()
传入a2
,其实就是传入b1
的with()
中。那当前动画就是a1
,传入的是a2
,两个并行执行。最后会返回this
即为b1
- 最后再调用
after()
传入a3
,也还是b1
内部的after()
中。即当前动画还是a1
,传入的是a3
,a1
在a3
后面执行。最后会返回this
即为b1
所以最终的顺序是 :
a3
->a1
/a2
由于这几个方法都是同一个对象内的,所以当前动画
currentNode
是不变的,一直都是a1
。那么其他需要组合的动画,都还会是以a1
为主题,看是插入到他的前或者后。如果有两个动画是放置与同一个位置,即
play(a1).after(a2).after(a3)
。那么a2
和a3
是并行执行的,即顺序为a2
/a3
->a1