textview - 文字轮播

啥叫文字轮播,不废话看图:

20161008134913311.gif

这是我们 app 项目中常常遇到的需求了,一般大家都是找找别人写好的第三方控件来用的,我也是这样。但是随着时间长河的流逝,我们也必须相应的成长, 文字轮播这种常见的东西可是不能错过的

我找一些实现,总结下实现思路:

  1. ViewAnimator 思路
    使用 ViewAnimator 自身特性,对期中的子 view 实现动画切换

  2. 自定义 viewGroup 思路
    在这个思路下,我们自定义一个容器,继承 FrameLayout ,根据数据数量自己 new 相应数量的 itemView 出来加入 FrameLayout ,动画是通过对当前 itemView 做一个出去的佛纳甘话,同时对下一个 itemView 做一个进入动画,使用 handle 实现延迟轮换

  3. ViewFlipper 思路
    ViewFlipper 思路和 ViewAnimator 一样,不过 ViewFlipper 使用上更灵活,这里我们根据数据流量动态往 ViewFlipper 里添加 itemView

  4. TextSwitcher 思路
    这个不想写了,没啥意思,TextSwitcher 不熟的大家自己去看看好了,这里我贴一下参考实现得了:

TextSwitcher 不熟悉的看这个:

  1. 自定义 textView 思路
    其实这个思路也好理解,我们继承 textView ,然后在 onDraw 绘制中自己话文字,自己做动画,动画的思路是先把上一个文字上移到顶,然后再绘制下一个文字,从下面开始一直移动到中间

ViewAnimator 思路


ViewAnimator 是个 viewGroup ,可以实现动画切换其中子 view 的效果。在 xml 布局种,我们把 ViewAnimator 当一个容器,里面写轮播的 view,写多少个 view 就有多少个轮播,然后设置切换的动画,用 handle 做定时延迟轮播,调 ViewAnimator.onNext 就可以切换到下一个 view

推荐参考实现:

实现思路:
  1. 先在 layout xml 中声明布局层级结构:
    <ViewAnimator
        android:layout_width="match_parent"
        android:layout_height="200dp">
        
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="欢迎"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="测试"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="本程序"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="!!!!!"/>
        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="hello,world"/>
    </ViewAnimator>
  1. 代码种设置切换动画
viewAnimator.setOutAnimation(this, R.anim.slide_out_up);
viewAnimator.setInAnimation(this, R.anim.slide_in_down);
  1. handle 延迟循环显示下一个

    public void showNext() {
        viewAnimator.showNext();
    }

    public void showPrevious() {
        viewAnimator.showPrevious();
    }

    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (autoPlayFlag) {
                showNext();
            }
            handler.sendMessageDelayed(new Message(), TIME_INTERVAL);
        }
    };

我们在需要的位置发送 handle 事件就可以了

使用 ViewAnimator 有点和确定同样明显

  • 优点:使用简单,没有难度
  • 缺点:xml 中固定了 view个数,为了兼容不同数据量,我们需要再 ViewAnimator 基础上 2 次开发

这里有个例子就是继承 ViewAnimator 做了一个带切换动画的 button,思路不错

自定义 viewGroup 思路


在这个思路下,我们自定义一个容器,继承 FrameLayout ,根据数据数量自己 new 相应数量的 itemView 出来加入 FrameLayout ,动画是通过对当前 itemView 做一个出去的佛纳甘话,同时对下一个 itemView 做一个进入动画,使用 handle 实现延迟轮换

推荐参考实现:

思路如下
  1. 在设置数据时添加相应数量的 itemView 进去
    public void setNoticeList(List<String> list) {

        // 创建TextView
        for (int i = 0; i < list.size(); i++) {
            TextView textView = createTextView(list.get(i));
            mNoticeList.add(textView);
            addView(textView);
        }
        // 显示第一条公告
        mCurrentNotice = 0;
        mNoticeList.get(mCurrentNotice).setVisibility(VISIBLE);
        // 启动轮播
        start();
    }

    private TextView createTextView(String text) {
        if (mLayoutParams == null) {
            mLayoutParams = new LayoutParams(
                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            mLayoutParams.gravity = Gravity.CENTER_VERTICAL;
        }

        TextView textView = new TextView(getContext());
        textView.setLayoutParams(mLayoutParams);
        textView.setSingleLine();
        textView.setEllipsize(TextUtils.TruncateAt.END);
        textView.setTextColor(mTextColor);
        textView.setVisibility(GONE);
        textView.setText(text);
        // 如果有设置字体大小,如果字体大小为null。
        if (mTextSize > 0) {
            textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
        }
        return textView;
    }
  1. 在 handle 里面启动 itemView 切换的动画
    class NoticeRunnable implements Runnable {
        @Override
        public void run() {
            // 隐藏当前的textView
            TextView currentView = mNoticeList.get(mCurrentNotice);
            currentView.setVisibility(GONE);
            if(mExitAnimSet != null) {
                currentView.startAnimation(mExitAnimSet);
            }
            mCurrentNotice++;
            if(mCurrentNotice >= mNoticeList.size()) {
                mCurrentNotice = 0;
            }

            // 显示下一个TextView
            TextView nextView = mNoticeList.get(mCurrentNotice);
            nextView.setVisibility(VISIBLE);
            if(mEnterAnimSet != null) {
                nextView.startAnimation(mEnterAnimSet);
            }
            mHandler.postDelayed(this, mNoticeDuration);
        }
    }

    private void createEnterAnimation() {
        mEnterAnimSet = new AnimationSet(false);
        TranslateAnimation translateAnimation =
                new TranslateAnimation(0,0,0,0, TranslateAnimation.RELATIVE_TO_PARENT, 1f,
                        TranslateAnimation.RELATIVE_TO_SELF, 0f);
        AlphaAnimation alphaAnimation = new AlphaAnimation(0f,1f);
        mEnterAnimSet.addAnimation(translateAnimation);
        mEnterAnimSet.addAnimation(alphaAnimation);
        mEnterAnimSet.setDuration(DEFAULT_ANIMATION_DURATION);
    }

    private void createExitAnimation() {
        mExitAnimSet = new AnimationSet(false);
        TranslateAnimation translateAnimation =
                new TranslateAnimation(0,0,0,0, TranslateAnimation.RELATIVE_TO_SELF, 0f,
                        TranslateAnimation.RELATIVE_TO_PARENT, -1f);
        AlphaAnimation alphaAnimation = new AlphaAnimation(1f,0f);
        mExitAnimSet.addAnimation(translateAnimation);
        mExitAnimSet.addAnimation(alphaAnimation);
        mExitAnimSet.setDuration(DEFAULT_ANIMATION_DURATION);
    }

这样写最练手,但是我是不推荐这样干的,基础差一些的容易出问题,而且 google 给我们提供了一些实现,我们何必非的自己实现呢,反正这样写会花点时间

ViewFlipper 思路


ViewFlipper 思路像是上面 1 和 2 的结合,ViewFlipper 对动画的控制更优秀一些,我们往 ViewFlipper 里面动态添加 itemView ,基本都是这个思路,区别是使用的容器不同

这里推荐一个成熟的库:

这个库非常完善了,也能满足大家的常用需求,是可以拿来直接用的,大家看图就明白了


思路如下

他这里自定义了一个 ViewGroup 继承自 RelativeLayout,在 view 初始化时添加了一个 ViewFlipper 进来,之后操作的都是这个 ViewFlipper 了

  1. 自定义 ViewGroup 初始化时添加了 ViewFlipper
    /**初始化控件*/
    private void init(Context context, AttributeSet attrs, int defStyleAttr) {

        mViewFlipper = new ViewFlipper(getContext());//new 一个ViewAnimator
        mViewFlipper.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        addView(mViewFlipper);
        startViewAnimator();
        //设置点击事件
        mViewFlipper.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = mViewFlipper.getDisplayedChild();//当前显示的子视图的索引位置
                if (mListener!=null){
                    mListener.onItemClick(mDatas.get(position),position);
                }
            }
        });
  1. 根据数据添加 itemView
    /**设置数据集合*/
    public void setDatas(List<String> datas){
        this.mDatas = datas;
        if (DisplayUtils.notEmpty(mDatas)){
            mViewFlipper.removeAllViews();
            for (int i = 0; i < mDatas.size(); i++) {
                TextView textView = new TextView(getContext());
                textView.setText(mDatas.get(i));
                //任意设置你的文字样式,在这里
                textView.setSingleLine(isSingleLine);
                textView.setTextColor(mTextColor);
                textView.setTextSize(mTextSize);
                textView.setGravity(mGravity);

                mViewFlipper.addView(textView,i);//添加子view,并标识子view位置
            }
        }
    }
  1. 添加动画
    /**
     * 设置进入动画和离开动画
     *
     * @param inAnimResId  进入动画的resID
     * @param outAnimResID 离开动画的resID
     */
    private void setInAndOutAnimation(@AnimRes int inAnimResId, @AnimRes int outAnimResID) {
        Animation inAnim = AnimationUtils.loadAnimation(getContext(), inAnimResId);
        inAnim.setDuration(animDuration);
        mViewFlipper.setInAnimation(inAnim);

        Animation outAnim = AnimationUtils.loadAnimation(getContext(), outAnimResID);
        outAnim.setDuration(animDuration);
        mViewFlipper.setOutAnimation(outAnim);
    }

之后就是用 handle 来做延迟循环,上面复制好几遍了,这里是在不想再复制了,打个源码很简单,大家直接看。

吐槽下:这个库多了一道手,多加了一个视图层级出来,其实没必要在顶层加一个 viewGroup 了,直接继承 ViewFlipper 可好

自定义 textView 思路


不继承 textView 我们直接继承 view 都可以,只要不支持 wrap_content 就好办。 核心就是在 onDraw 中实现绘制的动画。

例子这里没有使用 ValueAnimator 动画,而是 1 个 px 变化就重绘一次,性能上欠考虑。

参考资料地址:

实现思路
  1. 根据文字,确定文字出屏幕的零界点
// 获取文字矩阵的尺寸
Rect indexBound = new Rect();
mPaint.getTextBounds(text, 0, text.length(), indexBound);

// 文字居中绘制 Y 的坐标
my = mHeight  / 2 - (bound.top + bound.bottom) / 2

// 文字移动到最顶部
mY == 0 - bound.bottom

// 文字移动到最下部
mY = mHeight  - indexBound.top;
  1. 在 onDraw 中实现绘制

        // 文字首先绘制在最底部,mY 初始时 = 0
        if (mY == 0) {
            mY = getMeasuredHeight() - indexBound.top;
        }

        // 文字移动到最顶部时,更换数据,把文字移动到最底部
        if (mY == 0 - indexBound.bottom) {
            Log.i(TAG, "onDraw: " + getMeasuredHeight());
            mY = getMeasuredHeight() - indexBound.top;//返回底部
            mIndex++;//换下一组数据
        }

        // 文字移动到中间时,停止 handle 的重绘任务,延迟标准时间后再开始
        if (mY == getMeasuredHeight() / 2 - (indexBound.top + indexBound.bottom) / 2) {
            isMove = false;//停止移动
            // handle 通知标准时间后开始重绘
        }

        // 处理完 Y 坐标,开始绘制文字
        canvas.drawText(font, 0, font.length(), 10, mY, mPaintFront);

        // 最后移动 1 个 px,以实现动画效果
        mY -= 1

作者这里强调每移动 1个 px 就重绘一遍是为了确保动画的连贯性,但是这样系统也是 16 ms 才绘制一帧的,这样高频率的重绘并不是最优选择哦

详细的去看作何的文章吧,我更喜欢用 ValueAnimator 实现,这样更贴合 UI 线程的刷新频率。

说下感想,纯自己 draw 绘制的话效率太低,代码容易出错不说,若是需求换个方向对于我们也是一个大麻烦

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,400评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,680评论 2 59
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生x阅读 15,967评论 3 119
  • 上次说了男士西服,这次说下男装高级定制。真正定制男装英国的savile row才是男装典范,一人一版是基本要求,还...
    郭弈翎阅读 540评论 2 2
  • 文/绿山野 笔者的话:世界本来就在消亡,这种消亡在我们身处的空间稍慢一些,要感知到,需要通过某些手段,但很遗憾,大...
    绿山野阅读 298评论 1 2