TextSwitcher 实现 android 公告栏

96
靳艳杰
0.6 2018.01.09 18:13 字数 1053

网上搜索看到自定义的公告栏,采用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<String> getStringList() {
        List<String> 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.
日记本