TextSwitcher 实现 android 公告栏

网上搜索看到自定义的公告栏,采用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.
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容