最近公司要做的项目中要求实现一个简单的视频录制功能的组件,我简单设计了一个,主要功能就是开始,暂停,停止和显示录制时间长度。首先看一下效果图:
可以看到是一个非常简单的动画效果,为了方便使用,我把他做成了aar并发布到了jCenter,集成方式:
1
|
compile 'com.rangaofei:sakarecordview:0.0.2' |
组件里用到的库也非常简单,包括databinding,属性动画和layouttransition。通过这个简单的库简单的介绍一下LayoutTransition的用法,其中也会插入一些简单的databinding和属性动画的知识点,遇到困难请自行解决。
使用方法: 在xml文件中添加自定义控件:
1
2
3
4
5
6
7
|
< com.hanlinbode.sakarecordview.RecordView android:id = "@+id/rv_saka" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_alignParentBottom = "true" android:layout_margin = "30dp" app:record_view_time_string = "HHMMSS" /> |
record_view_time_string
属性是枚举类型,用来表示时间表示形式:
HHMMSS 00:00:00
MMSS 00:00
HH_MM_SS 00-00-00
MM_SS 00-00
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//更新时间 void updateTime( long ) /*设置监听器, void onInitial(); void onStartRecord(); void onPauseRecord(); void onResumeRecord(); void onStopRecord();*/ void setRecordListener(RecordListener) void setDebug( boolean ) |
LayoutTransition简介
来源于官方文档
LayoutTransition能够在viewgroup的布局发生变化时产生一个动画效果。可以通过 ViewGroup.setLayoutTransition(LayoutTransition transition)
来设置过度效果。调用这个方法将会使用内置的过渡动画(alpha值变化,xy位置变化等),开发者可用通过`LayoutTransition.setAnimator(int transitionType,Animator animator)来设置自己的过渡效果。能够出发动画的情况有两种:
- item添加(设置View.VISIBLE也可)
- item移除(设置View.GON也可)
当viewgroup中发生上述两种行为时,或者由于添加删除而引起其他item变化,都会触发动画。
过渡动画的触发种类
这个种类指的是在发生某种行为时(例如item添加或者删除),共有5种: CHANGE_APPEARING,CHANGE_DISAPPERING,APPEARING,DISAPPEARING,CHANGING
。每种状态有自己的一个位标记。
CHANGE_APPEARING
指示动画将会在新的控件添加到viewgroup中的时候引起其他view变化触发。它的标志位是0x01。也就是当addview或者将非VISIBLE状态的view设置为VISIBILE状态时其他的view被影响到时也会触发。
CHANGE_DISAPPEARING
指示动画将会在viewgroup删除控件的时候引起其他view变化触发,它的标志位是0x02。也就是当removeview或者将VISIBLE状态的view设置为非VISIBLE状态时其他的view被影响到也会触发。
APPEARING
当新的view添加到viewgroup中的时候触发。它的标志位是0x04。也就是当addview或者将非VISIBLE状态的view设置为VISIBILE状态时会触发。
DISAPPERAING
指示动画将会在viewgroup删除控件时触发,它的标志位是0x08。也就是当removeview或者将VISIBLE状态的view设置为非VISIBLE状态时会触发。
CHANGING
出去前边的四种,当布局发生变化时会触发动画。它的标志位是0x10。这个标志位默认是不激活的,但是可以通过enableTransitonType(int)来激活。
了解了这些,这个库基本就能实现了。
RecordView分析
左边的开始和暂停按钮是一个checkbox实现的,通过一个简单的selector来切换图片,并在右侧布局出现和消失的时候有一个缩放动画。我们可以通过设置一个简单的ObjectAnimator监听器来实现这个缩放:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
ObjectAnimator animShow = ObjectAnimator.ofFloat( null , "scaleX" , 0 , 1 ); animShow.setInterpolator( new OvershootInterpolator()); animShow.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (isDebug()) { Log.e(TAG, "show anim value=" + ( float ) animation.getAnimatedValue()); } recordState.setPlayScale( 1 + ( float ) animation.getAnimatedValue() / 5 ); } }); layoutTransition.setAnimator(LayoutTransition.APPEARING, animShow); ObjectAnimator animHide = ObjectAnimator.ofFloat( null , "alpha" , 1 , 0 ); animHide.addUpdateListener( new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (isDebug()) { Log.e(TAG, "hide anim value=" + ( float ) animation.getAnimatedValue()); } recordState.setPlayScale( 1 + ( float ) animation.getAnimatedValue() / 5 ); } }); layoutTransition.addTransitionListener( this ); layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animHide); binding.rootView.setLayoutTransition(layoutTransition); binding.rootContainer.setLayoutTransition(layoutTransition); |
record是自定一个一个类,用来设置显示的图片和时间,并保存缩放的状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
public class RecordState extends BaseObservable implements Parcelable { private boolean recording; private String time = "00:00:00" ; private float playScale = 1 ; @DrawableRes private int playDrawable; @DrawableRes private int stopDrawable; public RecordState( int playDrawable, int stopDrawable) { this .playDrawable = playDrawable; this .stopDrawable = stopDrawable; } @Bindable public boolean isRecording() { return recording; } public void setRecording( boolean recording) { this .recording = recording; notifyPropertyChanged(BR.recording); } //省略其他的getter和setter @Bindable public float getPlayScale() { return playScale; } public void setPlayScale( float playScale) { this .playScale = playScale; notifyPropertyChanged(BR.playScale); } //省略parcelable代码 } |
这里需要提一个view的局限性,就是只能改变x或者y的缩放,不能同时改变,所以这里做了一个双向绑定并写了一个adapter来设置同时更改X和Y的scale值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class CheckboxAttrAdapter { @BindingAdapter ( "checkListener" ) public static void setCheckBoxListener(CheckBox view, CompoundButton.OnCheckedChangeListener listener) { view.setOnCheckedChangeListener(listener); } @BindingAdapter ( "android:button" ) public static void setButton(CheckBox view, @DrawableRes int drawableId) { view.setButtonDrawable(drawableId); } @BindingAdapter ( "recordScale" ) public static void setRecordScale(CheckBox view, float scale) { view.setScaleX(scale); view.setScaleY(scale); } } |
然后在xml文件中可以直接饮用属性:
1
2
3
4
5
6
7
8
9
10
|
< CheckBox android:id = "@+id/start" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:layout_centerVertical = "true" android:layout_marginLeft = "30dp" android:button = "@{state.playDrawable}" android:checked = "@{state.recording}" app:checkListener = "@{checkListener}" app:recordScale = "@{state.playScale}" /> |
这样就基本完成了动画操作,然后暴露一些接口即可:
1
2
3
4
5
6
7
8
9
10
11
12
|
public interface RecordListener { void onInitial(); void onStartRecord(); void onPauseRecord(); void onResumeRecord(); void onStopRecord(); } |
这样就完成了一个最简单的RecordView了。
原理探究
本人水平有限,这里只进行最简单的一些分析。
LayoutTransition设置了一系列的默认值,这些默认值有默认的animator,animator的duration,动画开始的延迟时间,动画的错开间隔,插值器,等待执行view的动画map关系,正在显示或者消失的view动画的map关系,view和view的onlayoutchangelistenr对应关系等等。
默认的方法和变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public LayoutTransition() { if (defaultChangeIn == null ) { PropertyValuesHolder pvhLeft = PropertyValuesHolder.ofInt( "left" , 0 , 1 ); PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt( "top" , 0 , 1 ); PropertyValuesHolder pvhRight = PropertyValuesHolder.ofInt( "right" , 0 , 1 ); PropertyValuesHolder pvhBottom = PropertyValuesHolder.ofInt( "bottom" , 0 , 1 ); PropertyValuesHolder pvhScrollX = PropertyValuesHolder.ofInt( "scrollX" , 0 , 1 ); PropertyValuesHolder pvhScrollY = PropertyValuesHolder.ofInt( "scrollY" , 0 , 1 ); defaultChangeIn = ObjectAnimator.ofPropertyValuesHolder((Object) null , pvhLeft, pvhTop, pvhRight, pvhBottom, pvhScrollX, pvhScrollY); defaultChangeIn.setDuration(DEFAULT_DURATION); defaultChangeIn.setStartDelay(mChangingAppearingDelay); defaultChangeIn.setInterpolator(mChangingAppearingInterpolator); defaultChangeOut = defaultChangeIn.clone(); defaultChangeOut.setStartDelay(mChangingDisappearingDelay); defaultChangeOut.setInterpolator(mChangingDisappearingInterpolator); defaultChange = defaultChangeIn.clone(); defaultChange.setStartDelay(mChangingDelay); defaultChange.setInterpolator(mChangingInterpolator); defaultFadeIn = ObjectAnimator.ofFloat( null , "alpha" , 0f, 1f); defaultFadeIn.setDuration(DEFAULT_DURATION); defaultFadeIn.setStartDelay(mAppearingDelay); defaultFadeIn.setInterpolator(mAppearingInterpolator); defaultFadeOut = ObjectAnimator.ofFloat( null , "alpha" , 1f, 0f); defaultFadeOut.setDuration(DEFAULT_DURATION); defaultFadeOut.setStartDelay(mDisappearingDelay); defaultFadeOut.setInterpolator(mDisappearingInterpolator); } mChangingAppearingAnim = defaultChangeIn; mChangingDisappearingAnim = defaultChangeOut; mChangingAnim = defaultChange; mAppearingAnim = defaultFadeIn; mDisappearingAnim = defaultFadeOut; } |
可以看到,默认动画持有的属性有left、top、right、bottom、scrollY和scrollX,这里注意一下startDelay这个方法,可以看到其实这个启动的延迟时间是不一样的,对应的关系为:
1
2
3
4
5
|
private long mAppearingDelay = DEFAULT_DURATION; private long mDisappearingDelay = 0 ; private long mChangingAppearingDelay = 0 ; private long mChangingDisappearingDelay = DEFAULT_DURATION; private long mChangingDelay = 0 ; |
官方文档中特别说明了:
By default, the DISAPPEARING animation begins immediately, as does the CHANGE_APPEARING animation. The other animations begin after a delay that is set to the default duration of the animations.
DISAPPEARING和CHANGE_APPEARING没有延迟时间,其他的动画都会有延迟300ms。这样做的目的是为了在动画展示的时候有一个顺序展示的视觉效果,看起来更符合逻辑:
当一个item添加到viewgroup的时候,其他阿德item首先要移动来调整出一块空白区域供新添加的item显示,然后执行新添加的item的显示动画。当移除一个item时,是一个逆向的过程。
看另个一有用的变量
1
2
|
private int mTransitionTypes = FLAG_CHANGE_APPEARING | FLAG_CHANGE_DISAPPEARING | FLAG_APPEARING | FLAG_DISAPPEARING; |
这个mTransitionTypes就是在后边的执行动画中必须使用的一个变量,它默认激活了四种种类,只有前边提到的FLAG_CHAGE未激活.
开发者可控的变量
这里集中讲几个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
//设置所有的动画持续时间 public void setDuration( long duration) //设置指定种类的动画持续时间:CHANGE_APPEARING,CHANGE_DISAPPEARING,APPEARING,DISAPPEARRING,CHANGING public void setDuration( int transitionType, long duration) //获取指定种类动画的持续时间 public long getDuration( int transitionType) //设置在CHANGEINGXX状态下时间的间隔 public void setStagger( int transitionType, long duration) //获取在CHANGEINGXX状态下时间的间隔 public long getStagger( int transitionType) //为指定的种类添加动画插值器 public void setInterpolator( int transitionType, TimeInterpolator interpolator) //获取指定的种类添加动画插值器 public TimeInterpolator getInterpolator( int transitionType) //为指定的种类添加动画 public void setAnimator( int transitionType, Animator animator) //设置viewgroup的属性是否随着view的变化而变化,比如viewgroup使用的是wrapcontent,添加view时会有一个扩张动画 public void setAnimateParentHierarchy( boolean animateParentHierarchy) //是否正在执行引起布局改变动画 public boolean isChangingLayout() //是否有正在执行的动画 public boolean isRunning() //添加item public void addChild(ViewGroup parent, View child) //移除item public void removeChild(ViewGroup parent, View child) //显示item public void showChild(ViewGroup parent, View child, int oldVisibility) //隐藏item public void hideChild(ViewGroup parent, View child, int newVisibility) //添加监听器 public void addTransitionListener(TransitionListener listener) //移除监听器 public void removeTransitionListener(TransitionListener listener) //获取监听器 public List<TransitionListener> getTransitionListeners() |
这些方法都比较简单。
执行流程
先看一张简单的图:
从上面的方法中可以看到,flag全都没有激活的话,那就没有任何显示或者隐藏的动画了。 CHANGE_DISAPPEARING
和 CHANGE_APPEARING
控制的是父view和非新添加view的动画, APPEARING
和 DISAPPEARING
控制的是新添加view的动画。
mAnimateParentHierarchy这个变量控制的是是否显示父布局的改变动画,所以这个必须设置为true后父布局的 CHANGE_DISAPPEARING
和 CHANGE_APPEARING
才能有作用,设置为false后只有父布局没有动画,而子控件中非新添加的view还是用动画效果。
viewgroup中调用
addview()用来为viewroup添加一个没有父控件的view,这个方法最终调用的是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout){ //省略代码 if (mTransition != null ) { // Don't prevent other add transitions from completing, but cancel remove // transitions to let them complete the process before we add to the container mTransition.cancel(LayoutTransition.DISAPPEARING); } //省略代码 if (mTransition != null ) { mTransition.addChild( this , child); } //省略代码 //省略代码 } |
设置view的显示或者隐藏时会调用以下方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) { if (mTransition != null ) { if (newVisibility == VISIBLE) { mTransition.showChild( this , child, oldVisibility); } else { mTransition.hideChild( this , child, newVisibility); if (mTransitioningViews != null && mTransitioningViews.contains(child)) { // Only track this on disappearing views - appearing views are already visible // and don't need special handling during drawChild() if (mVisibilityChangingChildren == null ) { mVisibilityChangingChildren = new ArrayList<View>(); } mVisibilityChangingChildren.add(child); addDisappearingView(child); } } } // in all cases, for drags if (newVisibility == VISIBLE && mCurrentDragStartEvent != null ) { if (!mChildrenInterestedInDrag.contains(child)) { notifyChildOfDragStart(child); } } } |
可以看到在viewgroup中与上面图中提到的方法调用是吻合的。
在调用ViewGroup.setLayoutTransition(LayoutTransition transition)的时候为自身设置了一个TransitionListener,这个地方加入的目的是为了缓存正在进行动画的view,暂不分析。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。
原文链接:https://juejin.im/post/5b231e18f265da59601958bf