Android(安卓)自定义View(五)实现跑马灯垂直滚动效果
一、前言
最近一直巩固 Android 自定义 View 相关知识,以前都是阅读一些理论性的文章,很少抽时间自己去实现一个自定义 View,项目中遇到问题就上 github 上去找效果。其实自定义 View 涉及到很多内容,只有亲自动手完成几个案例,才能对相关知识点有深入了解。
本文是对上篇文章的一个补充,股票 APP 列表底部有一个实时更新交易的跑马灯效果,纵观市面上很多产品都应用到这个效果,决定自己动手实现一下。
二、开发准备工作
1、先看效果图
2、案例源码下载
GitHub下载地址
CSDN下载地址
3、案例应用知识点
- ViewFlipper 控件基础知识
- Android 动画基础知识
- 自定义 View 基础知识
- 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 之自定义属性方式,主要分以下几个步骤:
- 根据集合 Size 和每页显示条目取余“%”计算一共需要展示几页;
- 遍历步骤 1 中获取的页数;
- 根据单行/多行显示,遍历每页创建子 View 布局;
- 调用 Adapter.onBindView()方法完成每个子 View 数据绑定;
- 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 状态都会被调用,适合做最后的清理操作;
- onAttachedToWindow 被调用,即代表着 View 被添加到了一个绘制过的视图树中。
- onAttachedToWindow 和 onDetachedFromWindow 可以被调用多次。
- 当 View 被添加到已经绘制过的视图树上时,onAttachedToWindow 会被立即执行,接着 onVisibilityChanged 也会立即执行。
- 当 View 从视图上移除时,如果 onAttachedToWindow 方法曾经执行过,那么 onDetachedFromWindow 将会被执行。
- onVisibilityChanged 被调用的前提是 View 执行过 onAttachedToWindow 方法。
- 判断 View 是否执行过 onAttachedToWindow 的依据是 View 里的 mAttachInfo 对象不为空。
@Overrideprotected void onVisibilityChanged(@NonNull View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); if (VISIBLE == visibility) { startFlipping(); } else if (GONE == visibility || INVISIBLE == visibility) { stopFlipping(); }}@Overrideprotected void onAttachedToWindow() { super.onAttachedToWindow(); startFlipping();}@Overrideprotected 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 路,与你同在!
更多相关文章
- Android应用开发学习记录(一)ImageView 添加点击效果
- Android(安卓)自定义View实现打钩(签到)的动画
- Android(安卓)Dev Guide -> User Interface
- Android中常被利用的漏洞
- android 窗口管理框架解析
- 实现一个用于显示当前时间的Google Android(安卓)窗口小部件(AppW
- Notification通知栏图标5.0以后效果不同的细节补充和处理
- React-Native系列Android——Native与Javascript通信原理(二)
- Java及Android中常用链式调用写法简单示例