Android 自定义View(五)实现跑马灯垂直滚动效果

一、前言

最近一直巩固 Android 自定义 View 相关知识,以前都是阅读一些理论性的文章,很少抽时间自己去实现一个自定义 View,项目中遇到问题就上 github 上去找效果。其实自定义 View 涉及到很多内容,只有亲自动手完成几个案例,才能对相关知识点有深入了解。

本文是对上篇文章的一个补充,股票 APP 列表底部有一个实时更新交易的跑马灯效果,纵观市面上很多产品都应用到这个效果,决定自己动手实现一下。

二、开发准备工作

1、先看效果图

在这里插入图片描述

2、案例源码下载

GitHub下载地址

CSDN下载地址

3、案例应用知识点

  1. ViewFlipper 控件基础知识
  2. Android 动画基础知识
  3. 自定义 View 基础知识
  4. Activity 启动流程基础知识

三、ViewFlipper 介绍

ViewFlipper 是 Android 中的基础控件,可能在一般开发中很少有人用到,所以很多开发者感觉对这个控件很陌生,在控件圈里更远远没有 ViewPager 出名,但是 ViewFlipper 用法很简单,效果却很不错。

ViewFlipper 继承自 ViewAnimator,而 ViewAnimator 又是继承自 FrameLayout,而 FrameLayout 就是平时基本上只显示一个子视图的布局,由于 FrameLayout 下不好确定子视图的位置,所以很多情况下子视图之前存在相互遮挡,这样就造成了很多时候我们基本上只要求 FrameLayout 显示一个子视图,然后通过某些控制来实现切换。正好,ViewFlipper 帮我们实现了这个工作,我们需要做的就是,选择恰当的时机调用其恰当的方法即可实质上只是封装了一些 ViewAnimator 的方法来调用,真正执行操作的是 ViewAnimator。

ViewFlipper 相关属性介绍

方法 描述
isFlipping 判断 View 切换是否正在进行
setFilpInterval 设置 View 之间切换的时间间隔
startFlipping 开始 View 的切换,而且会循环进行
stopFlipping 停止 View 的切换
setOutAnimation 设置切换 View 的退出动画
setInAnimation 设置切换 View 的进入动画
showNext 显示 ViewFlipper 里的下一个 View
showPrevious 显示 ViewFlipper 里的上一个 View

四、代码实现

上面已经介绍了 ViewFlipper 控件基础知识,如果要实现跑马灯效果,建议自定义 ViewFlipper 实现自己的需求。本文使用自定义 ViewFlipper 的方式实现跑马灯垂直滚动效果。

1、自定义 ViewFlipper 属性

设置以下属性,建议使用自定义属性方式,便于后期修改和 XML 中使用。

/**
 * 是否单行显示
 */
private boolean isSingleLine;
/**
 * 轮播间隔
 */
private int interval = 3000;
/**
 * 动画时间
 */
private int animDuration = 1000;
/**
 * 一次性显示item数目
 */
private int itemCount = 1;

2、创建动画

  • anim_marquee_in.xml 进入动画:
    • Y 轴位置从下面 100%移动到位置 0,动画持续 300 毫秒
    • 渐变透明度动画效果由 0.0 到 1.0,动画持续 500 毫秒
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="300"
        android:fromYDelta="100%p"
        android:toYDelta="0"/>
    <alpha
        android:duration="500"
        android:fromAlpha="0.0"
        android:toAlpha="1.0"/>
</set>
  • anim_marquee_out.xml 退出动画:

    • Y 轴位置从下面 0 移动到位置-100%,动画持续 400 毫秒
    • 渐变透明度动画效果由 1.0 到 0.0,动画持续 500 毫秒
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="400"
        android:fromYDelta="0"
        android:toYDelta="-100%p"/>
    <alpha
        android:duration="500"
        android:fromAlpha="1.0"
        android:toAlpha="0.0"/>
</set>

3、初始化动画

完成上面 2 步骤后,在自定义 ViewFlipper 中,完成动画的初始化工作。

private void initView(Context context) {
    // 动画
    Animation animIn = AnimationUtils.loadAnimation(context, R.anim.anim_marquee_in);
    Animation animOut = AnimationUtils.loadAnimation(context, R.anim.anim_marquee_out);
    // 设置动画
    animIn.setDuration(animDuration);
    animOut.setDuration(animDuration);
    // 设置切换View的进入动画
    setInAnimation(animIn);
    // 设置切换View的退出动画
    setOutAnimation(animOut);
    // 设置View之间切换的时间间隔
    setFlipInterval(interval);
    // 设置在测量时是考虑所有子项,还是只考虑可见或不可见状态的子项。
    setMeasureAllChildren(false);
}

4、创建 Adapter

因为跑马灯数据基本都是集合形式存在,所以采用 Adapter 模式,定义数据刷新回调接口 OnDataChangedListener,在 CustomizeMarqueeView 中接收回调并刷新数据。

public void setOnDataChangedListener(OnDataChangedListener onDataChangedListener) {
    mOnDataChangedListener = onDataChangedListener;
}

public void notifyDataChanged() {
    if (mOnDataChangedListener != null) {
        mOnDataChangedListener.onChanged();
    }
}

public interface OnDataChangedListener {
    void onChanged();
}

定义创建子 View 布局方法和绑定数据方法

/**
 * @param parent
 * @return 自定义跑马灯的Item布局
 */
public View onCreateView(CustomizeMarqueeView parent) {
    return LayoutInflater.from(parent.getContext()).inflate(R.layout.marqueeview_item, null);
}

/**
 * 更新数据
 * @param view
 * @param position
 */
public void onBindView(View view, int position) {
}

5、创建布局和绑定数据

根据 List 集合设置 View 数据,这里主要使用自定义 View 之自定义属性方式,主要分以下几个步骤:

  1. 根据集合 Size 和每页显示条目取余“%”计算一共需要展示几页;
  2. 遍历步骤 1 中获取的页数;
  3. 根据单行/多行显示,遍历每页创建子 View 布局;
  4. 调用 Adapter.onBindView()方法完成每个子 View 数据绑定;
  5. addView()将所有子 View 添加到 ViewFlipper 中;
private void setData() {
    removeAllViews();
    int currentIndex = 0;
    // 计算数据展示完毕需要几页,根据总条目%每页条目计算得出
    int loopCount = mMarqueeViewBaseAdapter.getItemCount() % itemCount == 0 ?
            mMarqueeViewBaseAdapter.getItemCount() / itemCount :
            mMarqueeViewBaseAdapter.getItemCount() / itemCount + 1;
    // 遍历动态添加每页的View
    for (int i = 0; i < loopCount; i++) {
        // 每页单条展示
        if (isSingleLine) {
            LinearLayout parentView = new LinearLayout(getContext());
            parentView.setOrientation(LinearLayout.VERTICAL);
            parentView.setGravity(Gravity.CENTER);
            parentView.removeAllViews();
            View view = mMarqueeViewBaseAdapter.onCreateView(this);
            parentView.addView(view);
            if (currentIndex < mMarqueeViewBaseAdapter.getItemCount()) {// 绑定View
                mMarqueeViewBaseAdapter.onBindView(view, currentIndex);
            }
            currentIndex = currentIndex + 1;
            addView(parentView);
        } else {
            LinearLayout parentView = new LinearLayout(getContext());
            parentView.setOrientation(LinearLayout.VERTICAL);
            parentView.setGravity(Gravity.CENTER);
            parentView.removeAllViews();
            // 每页显示多少条,就遍历添加几个子View
            for (int j = 0; j < itemCount; j++) {
                View view = mMarqueeViewBaseAdapter.onCreateView(this);
                parentView.addView(view);
                currentIndex = getRealPosition(j, currentIndex);
                if (currentIndex < mMarqueeViewBaseAdapter.getItemCount()) {
                    mMarqueeViewBaseAdapter.onBindView(view, currentIndex);
                }
            }
            addView(parentView);
        }
    }
}

6、Activity 启动过程

有的朋友会很好奇这跟 Activity 启动过程有什么关系?

因为 ViewFlipper 属性看到需要手动调用 startFlipping()方法和 stopFlipping()完成 View 切换和循环执行。所以考虑到 View 性能和使用效果,我们重写了 View 的三个方法,实现开启和关闭。

  • onVisibilityChanged 是否调用,依赖于 View 是否执行过 onAttachedToWindow 方法。也就是 View 是否被添加到 Window 上。

  • onAttachedToWindow 方法是在 Activity resume 的时候被调用的,也就是 Activity 对应的 window 被添加的时候,且每个 view 只会被调用一次,父 view 的调用在前,不论 view 的 visibility 状态都会被调用,适合做些 view 特定的初始化操作;

  • onDetachedFromWindow 方法是在 Activity destroy 的时候被调用的,也就是 Activity 对应的 window 被删除的时候,且每个 view 只会被调用一次,父 view 的调用在后,也不论 view 的 visibility 状态都会被调用,适合做最后的清理操作;

  1. onAttachedToWindow 被调用,即代表着 View 被添加到了一个绘制过的视图树中。
  2. onAttachedToWindow 和 onDetachedFromWindow 可以被调用多次。
  3. 当 View 被添加到已经绘制过的视图树上时,onAttachedToWindow 会被立即执行,接着 onVisibilityChanged 也会立即执行。
  4. 当 View 从视图上移除时,如果 onAttachedToWindow 方法曾经执行过,那么 onDetachedFromWindow 将会被执行。
  5. onVisibilityChanged 被调用的前提是 View 执行过 onAttachedToWindow 方法。
  6. 判断 View 是否执行过 onAttachedToWindow 的依据是 View 里的 mAttachInfo 对象不为空。
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
    super.onVisibilityChanged(changedView, visibility);
    if (VISIBLE == visibility) {
        startFlipping();
    } else if (GONE == visibility || INVISIBLE == visibility) {
        stopFlipping();
    }
}

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    startFlipping();
}

@Override
protected void onDetachedFromWindow() {
    super.onDetachedFromWindow();
    stopFlipping();
}

7、Activity 中使用

只需要在 XML 中加载自定义 View 布局,然后在 Activity 中获取 View,加载数据集合即可。

marquessViewAdapter = new MarquessViewAdapter(this);
mMarqueeView.setItemCount(1);
mMarqueeView.setSingleLine(true);
mMarqueeView.setAdapter(marquessViewAdapter);
marquessViewAdapter.setMessageBeans(messageBeans);

结合上一篇博文的最终效果图至上:

在这里插入图片描述

五、总结

以上就完美实现了跑马灯效果,通过自定义 View 方式,结合动画属性。代码可以直接在项目中使用,只需要根据自己项目效果更改 item 的布局就好。本篇文章已经是自定义 View 实战案例的第五篇,虽然都是一些简单效果,但是能将自定义 View 相关知识:View 绘制流程、View 测量、View 事件分发做一个系统化的深入。希望本文能对初学自定义 View 的朋友有所帮助。

我是 Jaynm,一个再互联网苟且偷生的 Android 码农,漫漫 Android 路,与你同在!

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/jaynm/article/details/106624287