因为项目需要,做了一个自定义垂直跑马灯,分享给大家。

先上个效果图:

从图片中可以看到布局是由包含两个TextView的布局组成,一般的垂直跑马灯效果只支持单个TextView,水平方向的跑马灯更是不需要自定义,原生TextView就支持。

我的需求不只于此,里面的布局复杂,网上的方案已经不满足我的需求,所以我参考别人的垂直跑马灯,自己写了一个支持任意布局的跑马灯效果。代码已上传至GitHub

下面贴上View源码:

package com.verticalmarquee.maomao.verticalmarqueedemo;

import android.content.Context;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AnimationUtils;
import android.widget.ViewAnimator;

import java.util.List;

public class VerticalMarqueeLayout extends ViewAnimator {

    private static final long DEFAULT_TIMER = 2000L;
    private long delayTime = DEFAULT_TIMER;
    private int viewIndex;
    private List<View> views;
    private static Handler handler = new Handler();
    private boolean started;//是否已经开始轮播

    public VerticalMarqueeLayout(Context context) {
        super(context);
        this.init();
    }

    public VerticalMarqueeLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.init();
    }

    private void init() {
        this.setInAnimation(AnimationUtils.loadAnimation(this.getContext(), R.anim.vertical_marquee_in));
        this.setOutAnimation(AnimationUtils.loadAnimation(this.getContext(), R.anim.vertical_marquee_out));
    }

    protected void onFinishInflate() {
        super.onFinishInflate();
    }

    private void startMarquee() {
        if (this.views != null) {
            if (this.views.size() > 1) {
                handler.postDelayed(new Runnable() {
                    public void run() {
                        VerticalMarqueeLayout.this.viewIndex++;
                        if (VerticalMarqueeLayout.this.viewIndex >= VerticalMarqueeLayout.this.views.size()) {
                            VerticalMarqueeLayout.this.viewIndex = 0;
                        }
                        showNext();
                        VerticalMarqueeLayout.handler.postDelayed(this, delayTime);
                    }
                }, delayTime);
                started = true;
            } else if (this.views.size() > 0) {
                this.viewIndex = 0;
            } else {
                this.viewIndex = 0;
            }
        } else {
            this.viewIndex = 0;
        }

    }

    /**
     * 获取当前显示的View
     * 修改方法名,避免与父类方法重名
     *
     * @return View
     */
    public View getCurView() {
        if (this.views != null && this.viewIndex >= 0 && this.viewIndex < this.views.size()) {
            return this.views.get(this.viewIndex);
        }

        return null;
    }

    /**
     * 获取当前显示View的index
     *
     * @return index
     */
    public int getCurIndex() {
        return this.viewIndex;
    }

    /**
     * 设置轮播的View列表,该方法会自动轮播
     *
     * @param views view列表
     */
    public void setViewList(List<View> views) {
        setViewList(views, DEFAULT_TIMER);
    }

    /**
     * 设置轮播的View列表,该方法会自动轮播
     *
     * @param views     view列表
     * @param delayTime 间歇时间
     */
    public void setViewList(final List<View> views, long delayTime) {
        if (views == null || views.size() == 0) {
            return;
        }
        if (delayTime >= 100) {
            //最少100毫秒,否则为默认值
            this.delayTime = delayTime;
        }
        this.views = views;
        handler.removeCallbacksAndMessages(null);
        started = false;
        post(new Runnable() {
            @Override
            public void run() {
                for (View view : views) {
                    addView(view);
                }
                startMarquee();
            }
        });
    }


    //开始倒计时(轮播),在页面可见并且需要自动轮播的时候调用该方法
    public void startTimer() {
        if (started || views == null || views.size() <= 1) {
            return;
        }
        stopTimer();
        startMarquee();
        Log.d("VerticalMarqueeLayout", "VerticalMarqueeLayout startTimer!");
    }

    //停止倒计时(轮播),如果调用过startTimer();在页面不可见的时候调用该方法停止自动轮播
    public void stopTimer() {
        if (handler != null) {
            handler.removeCallbacksAndMessages(null);
            started = false;
            Log.d("VerticalMarqueeLayout", "VerticalMarqueeLayout stopTimer!");
        }
    }
}

源码解析:

  • init():初始化跑马灯效果(可以修改此处代码,改成水平跑马灯);
  • startMarquee():开始倒计时刷新页面;
  • getCurrentView():获取当前显示的View;
  • getCurrentIndex():获取当前显示的View对应的index;
  • setViewList(List views):设置跑马灯View列表,间歇时间为默认时间;
  • setViewList(List views, long delayTime):设置跑马灯View列表,间歇时间为传入的时间,单位为毫秒;
  • startTimer():开始倒计时,与stopTimer()配套使用;
  • stopTimer():停止倒计时,与startTimer()配套使用;

使用时直接调用setViewList方法,即可开始倒计时。
startTimer()和stopTimer()方法一般在activity的onResume和onPuase里面调用,为了Activity跳转之后能暂停动画;

原理

其实整个代码的核心点在于继承了ViewAnimator,ViewAnimator里面有自带的子view管理机制,通过调用showNext()可以直接显示下一个子View。这对于轮播效果很有帮助。

使用示例:

xml代码:

<?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"
    tools:context=".MainActivity">

    <com.verticalmarquee.maomao.verticalmarqueedemo.VerticalMarqueeLayout
        android:id="@+id/marquee_root"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</android.support.constraint.ConstraintLayout>

Activity代码:

package com.verticalmarquee.maomao.verticalmarqueedemo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private VerticalMarqueeLayout marqueeRoot;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        marqueeRoot = findViewById(R.id.marquee_root);
        initData();
    }


    private void initData() {
        int count = 5;
        List<View> views = new ArrayList<>();
        LayoutInflater inflater = LayoutInflater.from(this);
        for (int i = 0; i < count; i++) {
            views.add(inflateView(inflater, marqueeRoot, "这是标题" + i, "这里是第" + i + "条内容"));
        }
        marqueeRoot.setViewList(views);
    }

    private View inflateView(LayoutInflater inflater, VerticalMarqueeLayout marqueeRoot, String name, String desc) {
        if (inflater == null) {
            inflater = LayoutInflater.from(this);
        }
        View view = inflater.inflate(R.layout.marquee_item, marqueeRoot, false);
        TextView viewName = view.findViewById(R.id.marquee_name);
        TextView viewDesc = view.findViewById(R.id.marquee_desc);
        viewName.setText(name);
        viewDesc.setText(desc);
        return view;
    }
}

最后修改于2019年5月20日,修改点如下:

  1. 修复因getCurrentView方法与父类方法重名导致被调用时出现空指针异常。
  2. 开始启动轮播动画和addView操作添加到post的Runnable里面,为避免在页面未显示前调用导致的异常。这样可以放心在任意地点调用setViewList方法。

赶紧试试吧,祝你成功!!!

(标签:Android、跑马灯)