Android 自定义 View 实现公告小喇叭功能

728 阅读5分钟

需要实现的效果图如下

1620977133160189.gif

知识点介绍

实现该种效果需要用到 Android SDK 中提供的 ViewFlipper 类。翻译它的注释如下:

可以在已添加到其中的两个或多个视图之间进行动画处理。 一次只显示一个孩子。 如果有要求,可以按固定的时间间隔自动在每个孩子之间切换。

再来看下该类的关系图

20210514163946.jpg

从上面的关系图可见该类是个 ViewGroup(存在感几乎为零)。那感觉我们只要将多个 TextView 放到该容器中便可实现效果图的样式了(这也太简单了)。

ViewFlipper 介绍

xml 属性

属性名称解释
android:autoStart为 true 时,自动开始动画
android:flipIntervalview 间切换的时间间隔
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 百度一下,这里就不做展开。

如果你觉得本文对你有帮助不妨点个赞,或者你觉得本文哪里有写的不对的地方可以在下方评论。冰冻三尺非一日之寒,每天进步一点点。