网上搜索看到自定义的公告栏,采用ViewFlipper实现。之所以采用ViewFlipper,是因为它可以实现:子类有规律的间隔“跳动”。而ViewFlipper和ViewSwitcher都是ViewAnimator的子类。不同的是,ViewSwitcher只能有两个子view,而ViewFlipper可以有多个子view.
考虑到,公告栏是一些文字在切换,所以采用ViewSwitcher包含两个textview实现。而恰好,ViewSwitcher有一个子类TextSwitcher。看看TextSwitcher的介绍:
- A TextSwitcher is useful to animate a label on screen.Whenever setText(CharSequence) is called, TextSwitcher animates the current text out and animates the new text in.
TextSwitcher 与文本类型的公告栏,简直是绝配。
TextSwitcher ----继承自---->ViewSwitcher----继承自----->ViewAnimator
以下是具体实现过程
1.自定义NotiveView,继承自TextSwitcher ,实现相应构造方法。
2.我们要给NotiveView添加两个Textview,一个是公告栏进入 时的TextView,一个是公告栏退出 时的Textview.
ViewSwitcher早已为我们铺垫好了,ViewSwitcher中有一个ViewFactory的接口,负责为ViewSwitcher创建子view.
看一下ViewSwitcher的源码:
public interface ViewFactory {
View makeView();
}
public void setFactory(ViewFactory factory) {
mFactory = factory;
obtainView();
obtainView();
}
private View obtainView() {
View child = mFactory.makeView();
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp == null) {
lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
}
addView(child, lp);
return child;
}
这也表明,我们在自定义的view中实现ViewFactory 这个接口,在 makeView()方法中返回一个TextView,每调用一次obtainView()就会 执行addView(child, lp),将view添加到viewgroup中。也就是说我们在自定义的view中只要调用setFactory(ViewFactory factory)就好了。
private void init() {
setFactory(this);
}
@Override
public View makeView() {
TextView t = new TextView(context);
t.setGravity(Gravity.CENTER);
t.setTextColor(Color.parseColor("#333333"));
t.setMaxLines(1);
float textSize=TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,16,context.getResources().getDisplayMetrics());
t.setTextSize(textSize);
t.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,240));
return t;
}
现在只是没有进入和退出的动画,我们可以先在activity中调用,看看效果:
在此之前,再提一下TextSwitcher的setText(CharSequence text)方法。
- Sets the text of the next view and switches to the next view. This can be used to animate the old text out
and animate the next text in.
只要我们调用此方法,正在show的TextView就会out,而后面的Textview就会in,并且设置传递的text.
而要实现隔几秒变换一次公告栏内容,我们就需要开启一个线程,在子线程中使用handler重复调用。
由于重复调用,采用取余的办法,来确定当前显示的是哪个String.
代码如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
stringList=getStringList();
mNotiveView=findViewById(R.id.auto_view);
mNotiveView.setFocusableInTouchMode(true);
handler.postDelayed(runnable,2000);
mNotiveView.setOnClickListener(v ->
Log.e(TAG, "onClick: "+stringList.get(mCount%stringList.size()) ));
}
模拟公告栏数据
private List getStringList() {
List list=new ArrayList<>();
list.add("hello");
list.add("world");
list.add("i");
list.add("miss");
list.add("you");
list.add("!");
return list;
}
private Handler handler=new Handler();
private int mCount=0;
public Runnable runnable=new Runnable() {
@Override
public void run() {
handler.postDelayed(runnable,3000);
if (stringList.size()==0){return;}
myAutoView.setText(stringList.get(mCount%stringList.size()));
mCount++;
}
};
注意在onDestroy中把runnable回收掉
@Override
protected void onDestroy() {
super.onDestroy();
if (runnable!=null){
handler.removeCallbacks(runnable);
}
}
现在运行后,可以看到公告栏文本每隔3秒切换一次,但并没有动画效果,显得不美,下面在自定义的view中设置进入和退出的动画。
一切都是那么巧合,ViewAnimator,也就是textSwitcher的父类的父类,已经定义好了两个方法:
//Specifies the animation used to animate a View that enters the screen.
public void setInAnimation(Animation inAnimation) {
mInAnimation = inAnimation;
}
//Specifies the animation used to animate a View that exit the screen.
public void setOutAnimation(Animation outAnimation) {
mOutAnimation = outAnimation;
}
美中不足的是,他们都需要一个Animation ,而不是Animator.属性动画就不能用了,只能设置Tween(补间动画)。
下面是定义的一个动画
class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private float mCenterX;
private float mCenterY;
private final boolean mTurnIn;
private final boolean mTurnUp;
private Camera mCamera;
public Rotate3dAnimation(float fromDegrees, float toDegrees, boolean turnIn, boolean turnUp) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mTurnIn = turnIn;
mTurnUp = turnUp;
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
mCenterY = getHeight();
mCenterX = getWidth() / 2;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
Log.e(TAG, "applyTransformation: interpolatedTime"+interpolatedTime );
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
final float centerX = mCenterX ;
final float centerY = mCenterY ;
final Camera camera = mCamera;
final int derection = mTurnUp ? 1: -1;
final Matrix matrix = t.getMatrix();
camera.save();
if (mTurnIn) {
camera.translate(0.0f, derection *mCenterY * (interpolatedTime - 1.0f), 0.0f);
} else {
camera.translate(0.0f, derection *mCenterY * (interpolatedTime), 0.0f);
}
camera.rotateX(degrees);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
在init方法中设置动画
private void init() {
setFactory(this);
//进入和出去时的动画
Animation mInUp = createAnim(0, 0, true, true);
Animation mOutUp = createAnim(0, 0, false, true);
setInAnimation(mInUp);
setOutAnimation(mOutUp);
}
private Animation createAnim(float start, float end, boolean turnIn, boolean turnUp) {
//平移动画
final Animation rotation = new Rotate3dAnimation(start, end, turnIn, turnUp);
rotation.setDuration(1000);
rotation.setFillAfter(true);
rotation.setInterpolator(new OvershootInterpolator());
return rotation;
}
我也在问自己,当在自定义view中设置动画后,并没有指定哪个TextView启动执行动画,那动画是怎么执行的呢?
结果在ViewAnimator中。
理一下思路:
当在activity中findviewbyId的时候,自定义的view已经初始化,init方法便会调用,setFactory(ViewFactory factory)执行之后,我们便在自定义的view里面添加了两个TextView.之后设置了进入退出的动画。
当activity开启的子线程执行后,自定义的view调用了setText(CharSequence text)方法
public void setText(CharSequence text) {
final TextView t = (TextView) getNextView();
t.setText(text);
showNext();------------------------>1
}
showNext()方法很关键。
@android.view.RemotableViewMethod
public void showNext() {
setDisplayedChild(mWhichChild + 1);-------->2
}
继续看setDisplayedChild(mWhichChild + 1)方法。
@android.view.RemotableViewMethod
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);--------------------》3
if (hasFocus) {
// Try to retake focus if we had it
requestFocus(FOCUS_FORWARD);
}
}
我大胆猜测一下,whichChild 是int类型的,作用是判断目前的子类是哪个,而hasFocus 是判断焦点,那么showOnly(mWhichChild),就是接近真相的地方了。
void showOnly(int childIndex) {
final boolean animate = (!mFirstTime || mAnimateFirstTime);
showOnly(childIndex, animate);
}
接着看 showOnly(childIndex, animate);
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);
}
}
}
终于在这里,我看到了我想看到的东西。child.startAnimation(mInAnimation);这就是启动动画的地方。
总结:虽然尽力想弄明白一些东西,但又岂是一朝一夕。还好TextSwitcher 源码简单,能看的下去。最后我在想,源码为什么这样封装继承,拐回头又看了一遍ViewAnimator----这个上游父类的注释:
- Base class for a FrameLayout container that will perform animations when switching between its views.