一、前言
最近一直巩固 Android 自定义 View 相关知识,以前都是阅读一些理论性的文章,很少抽时间自己去实现一个自定义 View,项目中遇到问题就上 github 上去找效果。其实自定义 View 涉及到很多内容,只有亲自动手完成几个案例,才能对相关知识点有深入了解。
本文是对上篇文章的一个补充,股票 APP 列表底部有一个实时更新交易的跑马灯效果,纵观市面上很多产品都应用到这个效果,决定自己动手实现一下。
二、开发准备工作
1、先看效果图
2、案例源码下载
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 对象不为空。
@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 路,与你同在!