最近一直巩固 Android 自定义 View 相关知识,以前都是阅读一些理论性的文章,很少抽时间自己去实现一个自定义 View,项目中遇到问题就上 github 上去找效果。其实自定义 View 涉及到很多内容,只有亲自动手完成几个案例,才能对相关知识点有深入了解。
本文是对上篇文章的一个补充,股票 APP 列表底部有一个实时更新交易的跑马灯效果,纵观市面上很多产品都应用到这个效果,决定自己动手实现一下。
GitHub下载地址
CSDN下载地址
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 的方式实现跑马灯垂直滚动效果。
设置以下属性,建议使用自定义属性方式,便于后期修改和 XML 中使用。
/**
* 是否单行显示
*/
private boolean isSingleLine;
/**
* 轮播间隔
*/
private int interval = 3000;
/**
* 动画时间
*/
private int animDuration = 1000;
/**
* 一次性显示item数目
*/
private int itemCount = 1;
<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 退出动画:
<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>
完成上面 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);
}
因为跑马灯数据基本都是集合形式存在,所以采用 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) {
}
根据 List 集合设置 View 数据,这里主要使用自定义 View 之自定义属性方式,主要分以下几个步骤:
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);
}
}
}
有的朋友会很好奇这跟 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();
}
只需要在 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 路,与你同在!