ViewFlipper源码分析
背景
最近产品有个需求是给用户上下滚动播放运营消息,这个需求我之前搞过,实现方式很是复杂,采用的自定义view的方式绘制出来的,今天用一种简单的方式实现
ViewFlipper
这个就是我们今天的主角,通过ViewFlipper就是可以实现View之间的切换
效果展示
ok.gif效果展示可能有点卡顿,但是实际上是不卡顿的
代码示例
1.xml 描述View
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity">
<ViewFlipper
android:id="@+id/viewFlipper"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.constraint.ConstraintLayout>
2.动画文件
anim_push_in.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromYDelta="100%p"
android:toYDelta="0"/>
<alpha
android:duration="500"
android:fromAlpha="0.0"
android:toAlpha="1.0"/>
</set>
anim_push_out.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="500"
android:fromYDelta="0"
android:toYDelta="-100%p"/>
<alpha
android:duration="500"
android:fromAlpha="1.0"
android:toAlpha="0.0"/>
</set>
3.代码编写
package demo.nate.com.viewflipper;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Gravity;
import android.widget.TextView;
import android.widget.ViewFlipper;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private ViewFlipper mViewFlipper;
private String[] mContents = new String[]{"窗前明月光。", "疑是地上霜","举头望明月","低头思故乡"};
private List<TextView> mViews = new ArrayList<>(mContents.length);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewFlipper = findViewById(R.id.viewFlipper);
createView();
addView();
mViewFlipper.setInAnimation(this, R.anim.anim_push_in);
mViewFlipper.setOutAnimation(this, R.anim.anim_push_out);
mViewFlipper.setFlipInterval(2000);
mViewFlipper.startFlipping();
}
private void addView() {
for (TextView view : mViews) {
mViewFlipper.addView(view);
}
}
private void createView() {
for (int i = 0; i < mContents.length; i++) {
TextView textView = new TextView(this);
textView.setTextSize(24);
textView.setGravity(Gravity.CENTER);
textView.setTextColor(getResources().getColor(R.color.colorAccent));
textView.setText(mContents[i]);
mViews.add(textView);
}
}
}
就这么简单
ViewFlipper 源码分析
如果只是讲使用的化,根本没有必要,代码太简单了,我希望能从原理上了解ViewFlipper,所以我需要分析一下他的源代码
1.先分析一下继承关系
viewFlipper.png其实ViewFlipper就是一个FrameLayout,其核心代码就是ViewAnimator上
2.ViewAnimtor 方法概率
ViewAnimator 方法上.png上面这些方法比较关键,我们后续详细分析一下
ViewAnimator方法下.png总结主要是提供了一些动画设置接口
3.整个流程分析
启动动画
mViewFlipper.startFlipping();
/**
* Start a timer to cycle through child views
*/
public void startFlipping() {
mStarted = true;
updateRunning();
}
private void updateRunning() {
updateRunning(true);
}
mStarted 变量默认是false,updateRunning()主要作用是循环执行动画
private void updateRunning(boolean flipNow) {
boolean running = mVisible && mStarted && mUserPresent;
if (running != mRunning) {
if (running) {
showOnly(mWhichChild, flipNow);
postDelayed(mFlipRunnable, mFlipInterval);
} else {
removeCallbacks(mFlipRunnable);
}
mRunning = running;
}
if (LOGD) {
Log.d(TAG, "updateRunning() mVisible=" + mVisible + ", mStarted=" + mStarted
+ ", mUserPresent=" + mUserPresent + ", mRunning=" + mRunning);
}
}
这个地方有几个变量首先要识别一下,mStarted 这个变量我们之前已经看过了,默认值是false,启动动画的时候设置了true,mVisible 这个变量我们先看一下这个值赋值是在哪里?
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mVisible = false;
getContext().unregisterReceiver(mReceiver);
updateRunning();
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
mVisible = visibility == VISIBLE;
updateRunning(false);
}
其实就是在view显示的时候,把这个值设置为true
现在只有mUserPresent的值我们还没有分析了,通过分析代码我们可以发现这个值在初始化的时候就是true
public class ViewFlipper extends ViewAnimator {
private static final String TAG = "ViewFlipper";
private static final boolean LOGD = false;
private static final int DEFAULT_INTERVAL = 3000;
private int mFlipInterval = DEFAULT_INTERVAL;
private boolean mAutoStart = false;
private boolean mRunning = false;
private boolean mStarted = false;
private boolean mVisible = false;
private boolean mUserPresent = true;
对他的修改也只是在接收屏幕关闭的广播时候进行修改
public ViewFlipper(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.ViewFlipper);
mFlipInterval = a.getInt(
com.android.internal.R.styleable.ViewFlipper_flipInterval, DEFAULT_INTERVAL);
mAutoStart = a.getBoolean(
com.android.internal.R.styleable.ViewFlipper_autoStart, false);
a.recycle();
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (Intent.ACTION_SCREEN_OFF.equals(action)) {
mUserPresent = false;
updateRunning();
} else if (Intent.ACTION_USER_PRESENT.equals(action)) {
mUserPresent = true;
updateRunning(false);
}
}
};
还是回到我们主线代码中
private void updateRunning(boolean flipNow) {
boolean running = mVisible && mStarted && mUserPresent;
if (running != mRunning) {
if (running) {
showOnly(mWhichChild, flipNow);
postDelayed(mFlipRunnable, mFlipInterval);
} else {
removeCallbacks(mFlipRunnable);
}
mRunning = running;
}
if (LOGD) {
Log.d(TAG, "updateRunning() mVisible=" + mVisible + ", mStarted=" + mStarted
+ ", mUserPresent=" + mUserPresent + ", mRunning=" + mRunning);
}
}
变量running的值是true,mRunning 的值是false,也就走到了showOnly这个方法中,然后postDelayed(mFlipRunnable, mFlipInterval)这个方法就是定时切换view然后执行动画,我们先看看showOnly方法
void showOnly(int childIndex, boolean animate) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (i == childIndex) {
if (animate && mInAnimation != null) {
child.startAnimation(mInAnimation);
}
child.setVisibility(View.VISIBLE);
mFirstTime = false;
} else {
if (animate && mOutAnimation != null && child.getVisibility() == View.VISIBLE) {
child.startAnimation(mOutAnimation);
} else if (child.getAnimation() == mInAnimation)
child.clearAnimation();
child.setVisibility(View.GONE);
}
}
}
这个方法定义在ViewAnimator中,整体的逻辑也是比较简单的,就是根据childIndex的索引查找指定的view,执行进入动画,其他view执行移出动画,那么循环动画是如何执行的呢?
private final Runnable mFlipRunnable = new Runnable() {
@Override
public void run() {
if (mRunning) {
showNext();
postDelayed(mFlipRunnable, mFlipInterval);
}
}
};
public void showNext() {
setDisplayedChild(mWhichChild + 1);
}
public void setDisplayedChild(int whichChild) {
mWhichChild = whichChild;
if (whichChild >= getChildCount()) {
mWhichChild = 0;
} else if (whichChild < 0) {
mWhichChild = getChildCount() - 1;
}
boolean hasFocus = getFocusedChild() != null;
// This will clear old focus if we had it
showOnly(mWhichChild);
if (hasFocus) {
// Try to retake focus if we had it
requestFocus(FOCUS_FORWARD);
}
}
总体上就是控制childIndex的大小,不让其查过最大值和最小值,剩下逻辑跟之前一样,就是执行动画
总结
- 从使用角度来看,ViewFlipper可以实现两个View之间的任意动画,让开发正更加关注动画的编写而不需要关系动画的执行逻辑
- 从代码设计角度来看,这种用FrameLayout来父布局,同过两个view来不断执行动画来达到设计上的动画效果,这种实现方式很是简单,在工作中很有借鉴性