Android自定义View(广告栏上下滚动效果)

需求中涉及到的广告栏变化千变万化,这里,我们综合取材,有了下面的这篇文章。

开始的时候,我们使用的是MarqueeView,继承的ViewFlipper,但是会有一些bug,比如刷新数据时的重叠阴影等等

后来,考虑到后期的开发可能出现的修改,采用了自定义View继承LinearLayout来展示,代码实现如下:

Step1:自定义LimitScrollerView
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;

public class LimitScrollerView extends LinearLayout implements View.OnClickListener{

    private String TAG = "LimitScrollerView";

    private LinearLayout ll_content1, ll_content2;
    private LinearLayout ll_now, ll_down;   //当前可见的,下面不可见的(切换)
    private int limit;          //可见条目数量
    private int durationTime;   //动画执行时间
    private int periodTime;     //间隔时间
    private int scrollHeight;   //滚动高度(控件高度)

    private int dataIndex;

    private boolean isCancel;      //是否停止滚动动画
    private boolean boundData;     //是否已经第一次绑定过数据

    private final int MSG_SETDATA = 1;
    private final int MSG_SCROL = 2;

    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            if(msg.what == MSG_SETDATA){
                boundData(true);
            }else if(msg.what == MSG_SCROL){
                if(isCancel)
                    return;
                startAnimation();
            }
        }
    };

    public LimitScrollerView(Context context) {
        this(context, null);
    }

    public LimitScrollerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public LimitScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs){
        LayoutInflater.from(context).inflate(R.layout.limit_scroller, this, true);
        ll_content1 = (LinearLayout) findViewById(R.id.ll_content1);
        ll_content2 = (LinearLayout) findViewById(R.id.ll_content2);
        ll_now = ll_content1;
        ll_down = ll_content2;
        if(attrs!=null){
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LimitScroller);
            limit = ta.getInt(R.styleable.LimitScroller_limit, 1);
            durationTime = ta.getInt(R.styleable.LimitScroller_durationTime, 1000);
            periodTime = ta.getInt(R.styleable.LimitScroller_periodTime, 1000);
            ta.recycle();  //注意回收
            Log.v(TAG, "limit="+limit);
            Log.v(TAG, "durationTime="+durationTime);
            Log.v(TAG, "periodTime="+periodTime);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //EXACTLY=1073741824
        //AT_MOST=-2147483648
/*        int specMode = MeasureSpec.getMode(heightMeasureSpec);
        int specSize = MeasureSpec.getSize(heightMeasureSpec);
        Log.w(TAG, "specMode="+specMode);
        Log.w(TAG, "specSize="+specSize);
        int newHeightSpec = MeasureSpec.makeMeasureSpec(specSize, MeasureSpec.EXACTLY);
        int childCount = ll_content1.getChildCount();
        if(childCount>0){
            View item = ll_content1.getChildAt(0);
            item.measure(widthMeasureSpec, newHeightSpec);
            Log.w(TAG, "条目高度="+   item.getMeasuredHeight());
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight()/2);
            scrollHeight = getMeasuredHeight();
            return;
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);*/

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //设置高度为整体高度的一般,以达到遮盖预备容器的效果
        setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight()/2);
        //此处记下控件的高度,此高度就是动画执行时向上滚动的高度
        scrollHeight = getMeasuredHeight();
//        Log.w(TAG, "getMeasuredWidth="+getMeasuredWidth());
//        Log.w(TAG, "getMeasuredHeight="+getMeasuredHeight());
//        Log.w(TAG, "scrollHeight="+scrollHeight);
    }

    private void startAnimation(){
        if(isCancel)
            return;
        Log.i(TAG, "滚动");
        //当前展示的容器,从当前位置(0),向上滚动scrollHeight
        ObjectAnimator anim1 = ObjectAnimator.ofFloat(ll_now, "Y",ll_now.getY(), ll_now.getY()-scrollHeight);
        //预备容器,从当前位置,向上滚动scrollHeight
        ObjectAnimator anim2 = ObjectAnimator.ofFloat(ll_down, "Y",ll_down.getY(), ll_down.getY()-scrollHeight);
        AnimatorSet animSet = new AnimatorSet();
        animSet.setDuration(durationTime);
        animSet.playTogether(anim1, anim2);
        animSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
//                Log.v(TAG, "ll_now动画开始前位置:"+ll_now.getX()+"*"+ll_now.getY());
//                Log.v(TAG, "ll_down动画开始前位置:"+ll_down.getX()+"*"+ll_down.getY());
            }
            @Override
            public void onAnimationEnd(Animator animation) {
                //滚动结束后,now的位置变成了-scrollHeight,这时将他移动到最底下
                ll_now.setY(scrollHeight);
                //down的位置变为0,也就是当前看见的
                ll_down.setY(0);
//                Log.v(TAG, "1调整之后ll_now位置:"+ll_now.getX()+"*"+ll_now.getY());
//                Log.v(TAG, "1调整之后ll_down位置:"+ll_down.getX()+"*"+ll_down.getY());
                LinearLayout temp = ll_now;
                ll_now = ll_down;
                ll_down = temp;
//                Log.v(TAG, "2调整之后ll_now位置:"+ll_now.getX()+"*"+ll_now.getY());
//                Log.v(TAG, "2调整之后ll_down位置:"+ll_down.getX()+"*"+ll_down.getY());
                //给不可见的控件绑定新数据
                boundData(false);

                handler.removeMessages(MSG_SCROL);
                if(isCancel) {
                    return;
                }
                handler.sendEmptyMessageDelayed(MSG_SCROL, periodTime);

            }
            @Override
            public void onAnimationCancel(Animator animation) {
            }
            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });
        animSet.start();
    }

    /**
     * 向容器中添加子条目
     * @param first
     */
    private void boundData(boolean first){
        if(adapter==null || adapter.getCount()<=0)
            return;
        if(first){
            //第一次绑定数据,需要为两个容器添加子条目
            boundData = true;
            ll_now.removeAllViews();
            for(int i = 0; i<limit; i++){
                if(dataIndex>=adapter.getCount())
                    dataIndex = 0;
                View view = adapter.getView(dataIndex);

                //设置点击监听
                view.setClickable(true);
                view.setOnClickListener(this);

                ll_now.addView(view);
                dataIndex ++;
            }
        }

        //每次动画结束之后,为预备容器添加新条目
        ll_down.removeAllViews();
        for(int i = 0; i<limit; i++){
            if(dataIndex>=adapter.getCount())
                dataIndex = 0;
            View view = adapter.getView(dataIndex);
            //设置点击监听
            view.setClickable(true);
            view.setOnClickListener(this);
            ll_down.addView(view);
            dataIndex ++;
        }
    }

    @Override
    public void onClick(View v) {
        if(clickListener!=null){
            Object obj = v.getTag();
            clickListener.onItemClick(obj);
        }
    }

    public interface LimitScrollAdapter{
        public int getCount();
        public View getView(int index);
    }
    private LimitScrollAdapter adapter;

    interface OnItemClickListener{
        public void onItemClick(Object obj);
    }
    private OnItemClickListener clickListener;
    /**********************public API 以下为暴露的接口***********************/

    /**
     * 1、设置数据适配器
     * @param adapter
     */
    public void setDataAdapter(LimitScrollAdapter adapter){
        this.adapter = adapter;
        handler.sendEmptyMessage(MSG_SETDATA);
    }

    /**
     * 2、开始滚动
     * 应该在两处调用此方法:
     * ①、Activity.onStart()
     * ②、MyLimitScrllAdapter.setDatas()
     */
    public void startScroll(){
        if(adapter==null||adapter.getCount()<=0)
            return;
        if(!boundData){
            handler.sendEmptyMessage(MSG_SETDATA);
        }
        isCancel = false;
        Log.e(TAG, "开始滚动");
        handler.removeMessages(MSG_SCROL);   //先清空所有滚动消息,避免滚动错乱
        handler.sendEmptyMessageDelayed(MSG_SCROL, periodTime);
    }
    /**
     * 3、停止滚动
     * 当在Activity不可见时,在Activity.onStop()中调用
     */
    public void cancel(){
        isCancel = true;
        Log.e(TAG, "停止滚动");
    }

    /**
     * 4、设置条目点击事件
     * @param listener
     */
    public void setOnItemClickListener(OnItemClickListener listener){
        this.clickListener = listener;
    }

}
Step2:limit_scroller.xml(设置了2组数据的滚动)——自定义LimitScrollerView的布局
<?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="wrap_content"
    android:orientation="vertical">
    <LinearLayout
        android:id="@+id/ll_content1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"/>
    <LinearLayout
        android:id="@+id/ll_content2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"/>
</LinearLayout>
Step3:自定义适配器 MyLimitScrollAdapter
public class MyLimitScrollAdapter implements LimitScrollerView.LimitScrollAdapter{

    private Context context;
    private List<String> mLists;
    public MyLimitScrollAdapter(List<String> mLists, Context context) {
        this.context = context;
        this.mLists = mLists;
    }

    @Override
    public int getCount() {
        return mLists == null ? 0:mLists.size();
    }

    @Override
    public View getView(int index) {
        View itemView = LayoutInflater.from(context).inflate(R.layout.limit_scroller_item, null, false);
        TextView tv_text = (TextView)itemView.findViewById(R.id.tv_text);
        tv_text.setText(mLists.get(index));
        return itemView;
    }
}
Step4:limit_scroller_item.xml(适配器的布局)——自定义MyLimitScrollAdapter的布局
<?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="wrap_content"
    android:orientation="horizontal"
    android:gravity="center_vertical">
    <ImageView
        android:visibility="gone"
        android:id="@+id/iv_icon"
        android:layout_width="30dip"
        android:layout_height="30dip"
        android:src="@mipmap/ic_launcher"/>
    <TextView
        android:id="@+id/tv_text"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingLeft="10dip"
        android:paddingTop="3dip"
        android:paddingBottom="3dip"
        android:textColor="@color/black"
        android:text="asdfasdfasdf"/>
</LinearLayout>
Step5:values/attrs.xml的代码
<resources>
    <declare-styleable name="LimitScroller">
        <!--显示的条目数量-->
        <attr name="limit" format="integer" />
        <!--滚动速度,比如3000,滚动时间会持续3秒钟-->
        <attr name="durationTime" format="integer" />
        <!--滚动间隔,比如5000,滚动完成后停留5秒继续滚动-->
        <attr name="periodTime" format="integer" />
    </declare-styleable>
</resources>
Step6:主布局中放入该布局(注意修改下面布局中LimitScrollerView的位置,不然会报错的(这里我们放在View的文件夹下))
<LinearLayout
        android:orientation="horizontal"
        android:layout_gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
      <views.LimitScrollerView
         android:id="@+id/limitScroll"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         openxu:limit="2"
         openxu:durationTime="1200"
         openxu:periodTime="2000"/>
</LinearLayout>
Step7:数据初始化
private Date d1,d2;
private long diff,year,days,hours,minutes,second;
private String mDiffTime="";//广告条与服务时间差
private String mTitle="";//广告条显示内容

private MyLimitScrollAdapter myScrollAdapter;

private List<String> info = new ArrayList<>();//广告条集合
private List<String> mNoticeTitle  = new ArrayList<>();//公告
private List<String> mNoticeTime  = new ArrayList<>();//公告时间

//这里的数据是假数据,如果请求接口了,记得每次add数据前先清空以上的集合
for(int i=0;i<10;i++){
mNoticeTitle.add("公告"+i);
mNoticeTime.add("2018-04-11 17:26:10");
}
由于实际需求的满足,我们的广告头部需要插入时间的判断,显示最新的几条数据(这里数据库第一次调用接口会返回一个serviceTime,即d1.getTime()),代码如下:
if (null!=info || info.size()>0){
            info.clear();
            initSetTimeData();
}else {
            initSetTimeData();
        }

//时间计算
    private void initSetTimeData() {
        if (mNoticeTitle.size()>0){
            for (int i = 0; i < mNoticeTitle.size(); i++) {
                try {
                    d1 = (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")).parse(mNoticeTime.get(i));
                    d2 = new Date(System.currentTimeMillis());//获取当前系统时间
                    diff = d2.getTime() - d1.getTime();//时间差
                    year = diff / (1000 * 60 * 60 * 24 * 365);
                    days = diff / (1000 * 60 * 60 * 24);
                    hours = (diff-days*(1000 * 60 * 60 * 24))/(1000* 60 * 60);
                    minutes = (diff-days*(1000 * 60 * 60 * 24)-hours*(1000* 60 * 60))/(1000* 60);
                    second = (diff-days*(1000 * 60 * 60 * 24)-hours*(1000* 60 * 60)-minutes*(1000* 60))/(1000);
                    if (year>=1){
                        mDiffTime = year +"年前";
                    }else if (days>=1){
                        mDiffTime = days +"天前";
                    }else if (hours>=1 && hours<=24){
                        mDiffTime =hours+"小时前";
                    }else if (minutes>=1 && minutes<=60){
                        mDiffTime = minutes+"分钟前";
                    }else if (second>0 && second<=60){
                        mDiffTime = second+"秒前";
                    }else {
                        mDiffTime = "";
                    }
                } catch (ParseException e) {
                    e.printStackTrace();
                }

                if (null!= mNoticeTitle.get(i)){
                    mTitle = mNoticeTitle.get(i);
                }else {
                    mTitle = "";
                }

                info.add(mDiffTime + "  "+ mTitle);
            }
        }else {
            info.add(mDiffTime + "  "+ mTitle);
        }
    }

//到这里,我们的info集合中数据已有,那么开始把数据匹配到adapter中
myScrollAdapter = new MyLimitScrollAdapter(info,getActivity());
limitScroll.setDataAdapter(myScrollAdapter);
limitScroll.startScroll();    //开始滚动

//到这里,基本功能已完成,但是,我们还需要做的是控制它的滚动效果,所以。。。
    @Override
    public void onStart() {
        super.onStart();
        limitScroll.startScroll();    //开始滚动
    }

    @Override
    public void onStop() {
        super.onStop();
        limitScroll.cancel();//停止滚动
    }
最后祝你顺利完成!
参考网址:https://github.com/openXu/LimitScrollerView

这里写图片描述

猜你喜欢

转载自blog.csdn.net/dota_wy/article/details/80018968
今日推荐