需要实现的效果图如下
知识点介绍
实现该种效果需要用到 Android SDK 中提供的 ViewFlipper 类。翻译它的注释如下:
可以在已添加到其中的两个或多个视图之间进行动画处理。 一次只显示一个孩子。 如果有要求,可以按固定的时间间隔自动在每个孩子之间切换。
再来看下该类的关系图
从上面的关系图可见该类是个 ViewGroup(存在感几乎为零)。那感觉我们只要将多个 TextView 放到该容器中便可实现效果图的样式了(这也太简单了)。
ViewFlipper 介绍
xml 属性
属性名称 | 解释 |
---|---|
android:autoStart | 为 true 时,自动开始动画 |
android:flipInterval | view 间切换的时间间隔 |
android:inAnimation | 进入动画 |
android:outAnimation | 离开动画 |
java 方法
方法名称 | 解释 |
---|---|
isFlipping | 判断View切换是否正在进行 |
setFilpInterval | 设置View之间切换的时间间隔 |
startFlipping | 开始View的切换,而且会循环进行 |
stopFlipping | 停止View的切换 |
setOutAnimation | 设置切换View的退出动画 |
setInAnimation | 设置切换View的进入动画 |
showNext | 显示ViewFlipper里的下一个View |
showPrevious | 显示ViewFlipper里的上一个View |
ViewFlipper 使用
1、ViewFlipper 布局
<ViewFlipper
android:id="@+id/viewflipper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:flipInterval="2000"
android:inAnimation="@anim/up_in"
android:outAnimation="@anim/up_out"
android:persistentDrawingCache="animation">
<Button
android:id="@+id/btn"
android:layout_width="match_parent"
android:layout_height="158dp"
android:background="@color/blue"
android:text="第一个" />
<Button
android:id="@+id/btn1"
android:layout_width="match_parent"
android:layout_height="158dp"
android:background="@color/red"
android:text="第二个" />
</ViewFlipper>
2、进入滑出动画
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromYDelta="100%p"
android:toYDelta="0%p"
android:duration="1000" />
</set>
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="1000"
android:fromYDelta="0%p"
android:toYDelta="-100%p" />
</set>
通过以上2步变实现了基础版的轮播小喇叭功能。如果本文仅止于此那就太没意思了,下面介绍进阶版
进阶
上面的使用主要问题在于直接在布局文件中添加交替显示布局,显然这种方式不够灵活、扩展性比较差。下面就对这种方式进行简单的封装,以达到快速构建显示内容,不需要在布局文件内写入大量子布局。 设计模式中有种“适配器”的设计模式大家应该都有所了解,在 Android 中最常见的就是 RecyclerView 的 Adapter 了。这里我们使用的也是“适配器”的设计模式对该组件进行封装。我们可以通过定义一个 Adapter 来把需要显示的数据进行转化,转换成需要在视图上显示的View,然后在 ViewFlipper 中通过 Adapter 来获取转化后的View,将其动态添加到 ViewFlipper 中。
实现
1、定义一个数据适配器 MarqueeViewAdapter,模仿 RecyclerView 的 Adapter 定义如下
public abstract class MarqueeViewAdapter<T> {
protected List<T> mDatas;
public MarqueeViewAdapter(List<T> datas) {
this.mDatas = datas;
if (datas == null) {
throw new RuntimeException("MarqueeView datas is Null");
}
}
/**
* 设置数据
*/
public void setData(List<T> datas) {
this.mDatas = datas;
}
public int getItemCount() {
return this.mDatas == null ? 0 : this.mDatas.size();
}
/**
* 创建一个item view
**/
public abstract View onCreateView(XMarqueeView parent);
/**
* item view 绑定数据
**/
public abstract void onBindView(View container, View view, int position);
}
这里还有点问题,当我们设置了 Data 后并没有方法通知布局刷新数据,所以我们要定义一个数据刷新的接口
public interface OnDataChangedListener {
void onChanged();
}
完善后的 Adapter 类如下
public abstract class MarqueeViewAdapter<T> {
protected List<T> mDatas;
private OnDataChangedListener mOnDataChangedListener;
public MarqueeViewAdapter(List<T> datas) {
this.mDatas = datas;
if (datas == null) {
throw new RuntimeException("MarqueeView datas is Null");
}
}
/**
* 设置数据
*/
public void setData(List<T> datas) {
this.mDatas = datas;
this.notifyDataChanged();
}
public int getItemCount() {
return this.mDatas == null ? 0 : this.mDatas.size();
}
/**
* 创建一个item view
**/
public abstract View onCreateView(XMarqueeView parent);
/**
* item view 绑定数据
**/
public abstract void onBindView(View container, View view, int position);
public void setOnDataChangedListener(OnDataChangedListener onDataChangedListener) {
this.mOnDataChangedListener = onDataChangedListener;
}
public void notifyDataChanged() {
if (this.mOnDataChangedListener != null) {
this.mOnDataChangedListener.onChanged();
}
}
}
2、接着定义我们的主角 MarqueeView,其继承 ViewFlipper,主要功能即是对 ViewFlipper 功能的封装,下面列举出其中的属性和提供的功能
private boolean enableAnimDuration;//是否使用交替动画
private int interval;//交替间隔时间
private int animDuration;//动画时长
private int textSize;//字体大小
private int textColor;//字体颜色
设置基本属性
Animation animIn = AnimationUtils.loadAnimation(context, R.anim.up_in);
Animation animOut = AnimationUtils.loadAnimation(context, R.anim.up_out);
if (this.enableAnimDuration) {
animIn.setDuration((long)this.animDuration);
animOut.setDuration((long)this.animDuration);
}
this.setInAnimation(animIn);
this.setOutAnimation(animOut);
this.setFlipInterval(this.interval);
this.setMeasureAllChildren(false);
通过上面的设置 ViewFlipper 的功能基本配置完成,下面就是对数据的封装,首先定义一个配置 适配器的方法
public void setAdapter(MarqueeViewAdapter adapter) {
if (adapter == null) {
throw new RuntimeException("adapter must not be null");
} else if (this.mMarqueeViewAdapter != null) {
throw new RuntimeException("you have already set an Adapter");
} else {
this.mMarqueeViewAdapter = adapter;
this.mMarqueeViewAdapter.setOnDataChangedListener(this);
this.setData();
}
}
然后我们获取到 adapter, 通过 View view = adapter.onCreateView(this); 创建出 view 对象在调用 adapter.onBindView(view, view, currentIndex);绑定视图。然后将该view 添加到 MarqueeView 内。
具体实现
public class MarqueeView extends ViewFlipper implements OnDataChangedListener {
private boolean enableAnimDuration = false;
private int interval = 3000;
private int animDuration = 1000;
private int textSize = 14;
private int textColor = Color.parseColor("#888888");
private MarqueeViewAdapter mMarqueeViewAdapter;
private boolean isFlippingLessCount = true;
public MarqueeView(Context context, AttributeSet attrs) {
super(context, attrs);
this.init(context, attrs, 0);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, styleable.XMarqueeView, defStyleAttr, 0);
if (typedArray != null) {
this.enableAnimDuration = typedArray.getBoolean(styleable.MarqueeView_isSetAnimDuration, false);
this.isSingleLine = typedArray.getBoolean(styleable.MarqueeView_isSingleLine, true);
this.isFlippingLessCount = typedArray.getBoolean(styleable.MarqueeView_isFlippingLessCount, true);
this.interval = typedArray.getInteger(styleable.MarqueeView_marquee_interval, this.interval);
this.animDuration = typedArray.getInteger(styleable.MarqueeView_marquee_animDuration, this.animDuration);
if (typedArray.hasValue(styleable.MarqueeView_marquee_textSize)) {
this.textSize = (int)typedArray.getDimension(styleable.MarqueeView_marquee_textSize, (float)this.textSize);
this.textSize = Utils.px2sp(context, (float)this.textSize);
}
this.textColor = typedArray.getColor(styleable.MarqueeView_marquee_textColor, this.textColor);
this.itemCount = typedArray.getInt(styleable.MarqueeView_marquee_count, this.itemCount);
typedArray.recycle();
}
Animation animIn = AnimationUtils.loadAnimation(context, R.anim.up_in);
Animation animOut = AnimationUtils.loadAnimation(context, R.anim.up_out);
if (this.enableAnimDuration) {
animIn.setDuration((long)this.animDuration);
animOut.setDuration((long)this.animDuration);
}
this.setInAnimation(animIn);
this.setOutAnimation(animOut);
this.setFlipInterval(this.interval);
this.setMeasureAllChildren(false);
}
public void setAdapter(MarqueeViewAdapter adapter) {
if (adapter == null) {
throw new RuntimeException("adapter must not be null");
} else if (this.mMarqueeViewAdapter != null) {
throw new RuntimeException("you have already set an Adapter");
} else {
this.mMarqueeViewAdapter = adapter;
this.mMarqueeViewAdapter.setOnDataChangedListener(this);
this.setData();
}
}
private void setData() {
this.removeAllViews();
int currentIndex = 0;
int loopconunt = this.mMarqueeViewAdapter.getItemCount()
for(int i = 0; i < loopconunt; ++i) {
View view = this.mMarqueeViewAdapter.onCreateView(this);
if (currentIndex < this.mMarqueeViewAdapter.getItemCount()) {
this.mMarqueeViewAdapter.onBindView(view, view, currentIndex);
}
++currentIndex;
this.addView(view);
}
if (this.isFlippingLessCount) {
this.startFlipping();
}
}
public void setItemCount(int itemCount) {
this.itemCount = itemCount;
}
public void setSingleLine(boolean singleLine) {
this.isSingleLine = singleLine;
}
public void setFlippingLessCount(boolean flippingLessCount) {
this.isFlippingLessCount = flippingLessCount;
}
public void onChanged() {
this.setData();
}
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (0 == visibility) {
this.startFlipping();
} else if (8 == visibility || 4 == visibility) {
this.stopFlipping();
}
}
protected void onAttachedToWindow() {
super.onAttachedToWindow();
this.startFlipping();
}
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
this.stopFlipping();
}
}
使用
1、定义显示的子布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textColor="@color/color_gray_5"
android:textSize="@dimen/dimen_13"
/>
</LinearLayout>
2、定义 adapter
public class MarqueeViewAdapter extends MarqueeViewAdapter<HeadLineBean.HeadLineItemBean> {
private Context mContext;
public MarqueeViewAdapter(List<HeadLineBean.HeadLineItemBean> datas, Context context) {
super(datas);
mContext = context;
}
@Override
public View onCreateView(MarqueeView parent) {
return LayoutInflater.from(parent.getContext()).inflate(R.layout.home_headline_marqueeview_item, null);
}
@Override
public void onBindView(View parent, View view, final int position) {
HeadLineBean.HeadLineItemBean bean = mDatas.get(position);
TextView tvOne = view.findViewById(R.id.textView);
tvOne.setText(bean.getTitle());
tvOne.setGravity(Gravity.CENTER_VERTICAL|Gravity.LEFT);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
}
}
3、定义 MarqueeView 布局
<com.sakuqi.MarqueeView
android:id="@+id/notice_conatiner"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginLeft="@dimen/dimen_6"
app:isSetAnimDuration="true"
app:marquee_animDuration="800"
app:marquee_interval="3000"
app:marquee_textColor="@color/white"
app:marquee_textSize="@dimen/dimen_12" />
4、设置 adapter
MarqueeView marqueeView = getView().findViewById(R.id.notice_conatiner);
marqueeView.setVisibility(VISIBLE);
headLineItemBeanList.clear();
headLineItemBeanList.addAll(headLineBean.getData());
if (marqueeViewAdapter2 == null) {
marqueeViewAdapter2 = new MarqueeViewAdapter(headLineItemBeanList, context);
marqueeView.setAdapter(marqueeViewAdapter2);
} else {
marqueeViewAdapter2.setData(headLineItemBeanList);
}
总结
该自定义 View 主要使用了适配器的设计模式将数据转换成View,然后动态的添加到 ViewFlipper 中。如果不了解适配器设计模式可以自行 Google 百度一下,这里就不做展开。
如果你觉得本文对你有帮助不妨点个赞,或者你觉得本文哪里有写的不对的地方可以在下方评论。冰冻三尺非一日之寒,每天进步一点点。