使用VerticalBannerView实现垂直轮播广告(仿淘宝头条)

本文介绍了一个开源项目VerticalBannerView,它是一个模仿淘宝APP首页轮播头条的自定义控件,支持自由定义内容,使用方式类似ListView/RecyclerView。通过详细步骤展示了如何添加依赖、布局、实现Adapter以及源码分析,提供了实现垂直轮播广告的完整流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

VerticalBannerView是一个仿淘宝APP首页轮播头条的自定义控件。
特性:
1.可自由定义展示的内容。
2.使用方式类似ListView/RecyclerView。
3.可为当前显示的内容添加各种事件,比如点击打开某个页面等。

VerticalBannerView开源项目地址:
https://github.com/guojunustb/VerticalBannerView

运行效果图:


一、项目使用

(1).添加项目依赖。
dependencies {
    compile 'com.github.Rowandjj:VerticalBannerView:1.0'
}

(2).添加布局。
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:orientation="horizontal">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="5dp"
        android:text="淘宝头条"
        android:textStyle="bold"/>

    <View
        android:layout_width="1dp"
        android:layout_height="40dp"
        android:layout_marginLeft="5dp"
        android:layout_marginRight="5dp"
        android:background="#cccccc"/>

    <com.taobao.library.VerticalBannerView
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/banner"
        android:layout_width="wrap_content"
        android:layout_height="36dp"
        app:animDuration="900"
        app:gap="2000"/>
</LinearLayout>

(3).实现Adapter。
public class SampleAdapter extends BaseBannerAdapter<Model> {
    private List<Model> mDatas;

    public SampleAdapter01(List<Model> datas) {
        super(datas);
    }

    @Override
    public View getView(VerticalBannerView parent) {
        return LayoutInflater.from(parent.getContext()).inflate(R.layout.your_item,null);
    }

    @Override
    public void setItem(final View view, final Model data) {
        TextView textView = (TextView) view.findViewById(R.id.text);
        textView.setText(data.title);
        // 你可以增加点击事件
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // TODO handle click event
            }
        });
    }
}

(4).为VerticalBannerView设置Adapter,并启动动画。
List<Model> datas = new ArrayList<>();
datas.add(new Model01("Note7发布了"));
datas.add(new Model01("Note7被召回了"));
SampleAdapter adapter = new SampleAdapter(datas);
VerticalBannerView banner = (VerticalBannerView) findViewById(R.id.banner);
banner.setAdapter(adapter);
banner.start();

二、源码分析

实现原理:
VerticalBannerView本质上是一个垂直的LinearLayout。定义一个Adapter类,向LinearLayout提供子View。初始状态下往LinearLayout中添加两个子View,子View的高度同LinearLayout的高度一致,这样一来只有第1个子View显示出来,第2个子View在底部不显示。然后使用属性动画ObjectAnimator同时修改两个子View的translationY属性,动画执行过程中translationY从默认值0渐变到负的LinearLayout的高度,显示出来的效果就是第1个子View逐渐向上退出,第2个子View从底部向上逐渐显示。动画执行完毕后,移除第1个子View,这样第2个子View的索引变成0,并完全显示出来占据LinearLayout的高度。再将已经移除的第1个子View,添加到索引为1的位置,此时该子View超出父视图之外完全不显示。一轮动画执行完毕,再调用postDelay()方法重复上述动画,一直循环下去。

下面进入代码部分,主要是两个类BaseBannerAdapter和VerticalBannerView。
(1).BaseBannerAdapter类
BaseBannerAdapter类负责为广告栏提供数据。我们在使用时,需要写一个Adapter类继承BaseBannerAdapter,实现getView()和setItem()方法。在getView()方法中,我们需要把要添加到广告栏中的item view创建出来并返回,setItem()方法则负责为创建的item view绑定数据。
public abstract class BaseBannerAdapter<T> {
    private List<T> mDatas;
    private OnDataChangedListener mOnDataChangedListener;

    public BaseBannerAdapter(List<T> datas) {
        mDatas = datas;
        if (datas == null || datas.isEmpty()) {
            throw new RuntimeException("nothing to show");
        }
    }

    public BaseBannerAdapter(T[] datas) {
        mDatas = new ArrayList<>(Arrays.asList(datas));
    }

    // 设置banner填充的数据
    public void setData(List<T> datas) {
        this.mDatas = datas;
        notifyDataChanged();
    }

    void setOnDataChangedListener(OnDataChangedListener listener) {
        mOnDataChangedListener = listener;
    }

    // 获取banner总数
    public int getCount() {
        return mDatas == null ? 0 : mDatas.size();
    }

    // 通知数据改变
    void notifyDataChanged() {
        mOnDataChangedListener.onChanged();
    }

    // 获取数据
    public T getItem(int position) {
        return mDatas.get(position);
    }

    // 设置banner的ItemView
    public abstract View getView(VerticalBannerView parent);

    // 设置banner的数据
    public abstract void setItem(View view, T data);

    // 数据变化的监听
    interface OnDataChangedListener {
        void onChanged();
    }
}

(2).VerticalBannerView类
VerticalBannerView类继承自LinearLayout,并在构造方法中设定方向为垂直。同时VerticalBannerView类实现了OnDataChangedListener接口,实现onChanged()方法,这样当改变数据后调用BaseBannerAdapter的notifyDataChanged()时,VerticalBannerView的onChanged()方法被回调,执行setupAdapter()重新初始化数据。
public class VerticalBannerView extends LinearLayout implements BaseBannerAdapter.OnDataChangedListener {
    public VerticalBannerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        setOrientation(VERTICAL);
	......
    }

    ......

    @Override
    public void onChanged() {
        setupAdapter();
    }

    ......
}

为VerticalBannerView添加item数据,需要调用setAdapter()方法,关键在于其中执行的setupAdapter()方法。
public void setAdapter(BaseBannerAdapter adapter) {
    if (adapter == null) {
        throw new RuntimeException("adapter must not be null");
    }
    if (mAdapter != null) {
        throw new RuntimeException("you have already set an Adapter");
    }
    this.mAdapter = adapter;
    mAdapter.setOnDataChangedListener(this);
    setupAdapter();
}

在setupAdapter()方法中,先移除所有的子View,然后调用Adapter的getView()方法创建两个子View,分别赋值给成员变量mFirstView和mSecondView,并为这两个子View绑定数据,最后再调用addView()添加进来。
// 初始化Child View
private void setupAdapter() {
    // 先移除所有的子View
    removeAllViews();

    if (mAdapter.getCount() == 1) {
        mFirstView = mAdapter.getView(this);
        mAdapter.setItem(mFirstView, mAdapter.getItem(0));
        addView(mFirstView);
    } else {
        // 调用Adapter的getView()方法,创建两个子View,分别赋值给mFirstView和mSecondView
        mFirstView = mAdapter.getView(this);
        mSecondView = mAdapter.getView(this);
        // 使用索引0和1的数据,为mFirstView和mSecondView设置数据
        mAdapter.setItem(mFirstView, mAdapter.getItem(0));
        mAdapter.setItem(mSecondView, mAdapter.getItem(1));
        // 将mFirstView和mSecondView添加到当前View
        addView(mFirstView);
        addView(mSecondView);

        mPosition = 1;
        isStarted = false;
    }
    setBackgroundDrawable(mFirstView.getBackground());
}

为了实现子View之间的切换,需要把上面添加进来的子View的高度修改为与VerticalBannerView高度一致。这个操作在onMeasure()方法中完成。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    // 成员变量mBannerHeight有一个默认值
    if (LayoutParams.WRAP_CONTENT == getLayoutParams().height) {
        // 如果当前View的高度设置为wrap_content,使用默认值
        getLayoutParams().height = (int) mBannerHeight;
    } else {
        // 如果当前View设定了固定高度,则使用设定的高度
        mBannerHeight = getHeight();
    }
    // 修改mFirstView和mSecondView的高度为其父视图的高度
    if (mFirstView != null) {
        mFirstView.getLayoutParams().height = (int) mBannerHeight;
    }
    if (mSecondView != null) {
        mSecondView.getLayoutParams().height = (int) mBannerHeight;
    }
}

上面的准备工作完成后,就可以进入动画的执行了。VerticalBannerView通过调用start()方法启动切换动画。在start()方法中,调用postDelayed()执行AnimRunnable任务,在AnimRunnable的run()方法中,先调用performSwitch(),然后再次调用postDelayed()使AnimRunnable任务一直循环执行下去。两个子View之间的切换工作由performSwitch()负责执行。
public void start() {
    if (mAdapter == null) {
        throw new RuntimeException("you must call setAdapter() before start");
    }

    if (!isStarted && mAdapter.getCount() > 1) {
        isStarted = true;
        postDelayed(mRunnable, mGap);
    }
}

private AnimRunnable mRunnable = new AnimRunnable();

private class AnimRunnable implements Runnable {
    @Override
    public void run() {
        performSwitch();
        // 调用postDelayed()延时再执行,一直循环下去
        postDelayed(this, mGap);
    }
}

下面再进入performSwitch()方法,该方法是Item View之间产生切换效果的核心。
// 执行切换
private void performSwitch() {
    // 动画在执行过程中,View的translationY属性从0一直减小到-mBannerHeight
    // 因为translationY属性一直是负值,所以View向上移动
    ObjectAnimator animator1 = ObjectAnimator.ofFloat(mFirstView, "translationY", -mBannerHeight);
    ObjectAnimator animator2 = ObjectAnimator.ofFloat(mSecondView, "translationY", -mBannerHeight);
    AnimatorSet set = new AnimatorSet();
    set.playTogether(animator1, animator2);
    set.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            // 动画执行完成,把mFirstView和mSecondView的translationY恢复到默认状态
            mFirstView.setTranslationY(0);
            mSecondView.setTranslationY(0);
            // 使用下一条数据,设置到第一个子View
            View removedView = getChildAt(0);
            mPosition++;
            mAdapter.setItem(removedView, mAdapter.getItem(mPosition % mAdapter.getCount()));
            // 移除第一个子View,此时当前LinearLayout的childCount==1
            removeView(removedView);
            // 移除的View,再添加到第2个位置
            addView(removedView, 1);
        }
    });
    set.setDuration(mAnimDuration);
    set.start();
}
在performSwitch()方法中,使用属性动画ObjectAnimator同时修改两个mFirstView和mSecondView的translationY属性,动画执行中translationY从默认值0一直减小到-mBannerHeight,在这个过程中mFirstView逐渐向上退出,mSecondView从底部逐渐显现。动画执行完毕后,移除mFirstView,mSecondView变成第1个子View并完全显示出来填充父视图的高度。再将移除的mFirstView添加到第2个位置,此时mFirstView未显示出来。由于performSwitch()方法一直循环被调用,mFirstView和mSecondView就这样一直循环切换。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值