Android自定义View(LimitScrollerView-仿天猫广告栏上下滚动效果)

时间:2022-08-27 09:46:50

转载请标明出处:

http://blog.csdn.net/xmxkf/article/details/53303872

本文出自:【openXu的博客】

  最近项目中需要在首页做一个跑马灯类型的广告栏,最后上面决定仿照天猫的广告栏效果做(中间部位),效果图如下(右边是我们的效果):

    Android自定义View(LimitScrollerView-仿天猫广告栏上下滚动效果)Android自定义View(LimitScrollerView-仿天猫广告栏上下滚动效果)

  天猫上抢购那一栏的广告条可以向上滚动,每次展示一条广告,展示一定时间后,第二条广告从下往上顶起。但项目经理说我们需要一次展示两条广告,广告每次停留5秒,然后向上滚动,滚动的过程持续1.5秒。要求还真多,想着这么多要求说不定什么时候又得改了,每次展示三条广告,需要停留8秒,滚动持续3秒,那就死球了。所以干脆自己封装一个通用的,你爱咋改咋改…

1、分析

  遇到这种展示效果,我们第一反应就会想到两个控件:ListViewScrollerViewListView可以展示条目,只需要重写下onMeasure就能达到一次只显示n条的效果,但是要自动滚动、滚动时间限制貌似有点困难;ScrollerView可以动态的往里面添加指定数量的条目,可以实现自动滚动,但是滚动持续时间不可控制。想到这里,顿时绝望、一头雾水,既然系统自带的控件实现起来有困难,那就自己造。

  经过一小阵思索,突然灵光一现,如下:

    Android自定义View(LimitScrollerView-仿天猫广告栏上下滚动效果)

  既然要实现滚动的效果,肯定有一个容器容纳当前展示的条目,还有一个容器在下面作为预备展示的容器,需要展示几条就动态的向容器中添加指定数量的子条目;最外层是一个大的容器,如果将他的高度设置为小容器的高度,即可实现遮挡预备容器的目的;滚动可使用动画集合,让两个容器同时向上滚动;滚动结束后,马上让被顶上去的容器复位到预备位置;这里需要两个引用指向当前展示的容器和预备容器,当动画结束之后,这两个引用需要互换。经过一段时间停留后重复上述步骤即可。

  思路是有了,要实现起来得考虑细节了。最外层用什么包裹?继承ViewGroup?太麻烦(得重写onLayout计算麻烦),我要实现的效果就是里面的两个容器在开始的时候能够垂直向下排列好即可,所以最简单的就是LinearLayout,里面的容器就不用说了,子条目都是垂直向下排列,肯定也是LinearLayout。是直接继承LinearLayout后动态向里面添加两个LinearLayout?还是使用组合控件?考虑到之前博客中自定义控件系列没有讲到组合控件,就这个机会写个小demo填充空白。那下面就开始了(不要嫌我啰嗦,大神们如果觉得太easy请口下留人,这些实现思路我想对很多人还是有帮助的)

2、定义组合控件布局

  组合控件,顾名思义就是由很多个控件组合而成,这里第一步就是定义好这些控件组合:

<?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>

3、继承最外层控件

  上面的控件组合定义好之后,下面就需要用一个类去形容他,那这个类就是组合控件了。用什么形容他合适呢?那就看控件组合最外层用的是什么,这里最外层是LinearLayout,那就定义一个类继承LinearLayout,然后覆盖其构造方法,使用LayoutInflater将控件组合挂在自己身上,并完成容器内控件的初始化:

public class LimitScrollerView extends LinearLayout{
    private LinearLayout ll_content1, ll_content2;  //展示容器 和 预备容器
    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);
    }
}

4、自定义属性

  为了达到通用的效果,自定义属性是必不可少的(自定义属性详解请参见: Android自定义View(二、深入解析自定义属性))。这里需要定义的是:一次显示的条目数量、滚动动画持续时间、停留时间,如下:

<?xml version="1.0" encoding="utf-8"?>
<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>

  然后就是使用这个自定义的控件了,在使用的时候可以指定属性值:

<com.openxu.lc.LimitScrollerView
    android:id="@+id/limitScroll"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    openxu:limit="2"
    openxu:durationTime="200"
    openxu:periodTime="5000"/>

  最后需要在控件初始化的时候,获取到属性值:

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);
    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);
    }
}

5、重写onMeasure

  由于每次只能显示需要展示的容器,遮盖预备容器,所以只能设置整个高度的一半,这里使用一个小技巧,由于最外层是LinearLayout,并且是竖直向下的,自带的LinearLayoutonMeasure()方法完成之后组合控件的高度就是两个子容器的高度了,所以直接调用super.onMeasuer()之后,再设置高度为getMeasureHeight()/2即可(onMeasure()详解请移步: Android自定义View(三、深入解析控件测量onMeasure)

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

6、数据适配器

  上面的步骤完成之后,展示的框架已经搭好了,但是运行之后是看不到控件的,因为容器中还没有子条目,整个控件的高度是0,下面就开始绑定数据、动态添加子条目。由于大家对ListView的数据填充模式已经很熟练,所以这里模仿Adapter的方式:

/**数据适配器*/
interface LimitScrllAdapter{
    public int getCount();
    public View getView(int index);
}
private LimitScrllAdapter adapter;

public void setDataAdapter(LimitScrllAdapter adapter){
    this.adapter = adapter;
    handler.sendEmptyMessage(MSG_SETDATA);
}

  在Activity请求数据完毕后,为适配器添加数据,这里需要实现LimitScrollAdapter的两个抽象方法,使用方式和ListView一样,这里就不赘述:

class MyLimitScrllAdapter implements LimitScrollerView.LimitScrllAdapter{

    private List<DataBean> datas;
    public void setDatas(List<DataBean> datas){
        this.datas = datas;
        //API:2、开始滚动
        limitScroll.startScroll();
    }
    @Override
    public int getCount() {
        return datas==null?0:datas.size();
    }

    @Override
    public View getView(int index) {
        View itemView = LayoutInflater.from(MainActivity.this).inflate(R.layout.limit_scroller_item, null, false);
        ImageView iv_icon = (ImageView)itemView.findViewById(R.id.iv_icon);
        TextView tv_text = (TextView)itemView.findViewById(R.id.tv_text);

        //绑定数据
        DataBean data = datas.get(index);
        itemView.setTag(data);
        iv_icon.setImageResource(data.getIcon());
        tv_text.setText(data.getText());
        return itemView;
    }
}

7、动态添加子条目

  数据有了,子条目通过adapter.getView()获取,那什么时候向容器中添加条目呢?第一次肯定是两个容器中都得添加,向上滚动之后,有一个容器被定到上面,然后复位到预备位置了,但是他的数据还是之前的数据,所以每次动画结束之后得为预备容器更新新的子条目:

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);
            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);
        ll_down.addView(view);
        dataIndex ++;
    }
}

8、滚动动画

  什么时候开始动画?这是个需要考虑的问题,没有数据的时候肯定不需要吧?有数据之后,activity不可见了也不需要动画,所以这里需要提供接口让activity中控制,Activity中请求完数据之后调用此接口开始动画,在onStart()中也需要调用开启动画,在onStop()中调用停止动画的接口。动画开启之后会无限循环的执行,每次动画执行完毕后通过Handler发送一个延迟指定时间的消息,停留指定时间后,handler收到消息后又调用startAnimation()方法:

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){
            //继续动画
            startAnimation();
        }
    }
};
private void startAnimation(){
    if(isCancel)
        return;
    //当前展示的容器,从当前位置(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) {
        }
        @Override
        public void onAnimationEnd(Animator animation) {
            //滚动结束后,now的位置变成了-scrollHeight,这时将他移动到最底下
            ll_now.setY(scrollHeight);
            //down的位置变为0,也就是当前看见的
            ll_down.setY(0);
            //引用交换
            LinearLayout temp = ll_now;
            ll_now = ll_down;
            ll_down = temp;
            //给不可见的控件绑定新数据
            boundData(false);
            //停留指定时间后,重复动画
            handler.sendEmptyMessageDelayed(MSG_SCROL, periodTime);
        }
        @Override
        public void onAnimationCancel(Animator animation) {
        }
        @Override
        public void onAnimationRepeat(Animator animation) {
        }
    });
    animSet.start();
}
/**
 * 2、开始滚动
 * 应该在两处调用此方法:
 * ①、Activity.onStart()
 * ②、MyLimitScrllAdapter.setDatas()
 */
public void startScroll(){
    if(adapter==null||adapter.getCount()<=0)
        return;
    if(!boundData){
        handler.sendEmptyMessage(MSG_SETDATA);
    }
    isCancel = false;
    handler.sendEmptyMessageDelayed(MSG_SCROL, periodTime);
}
/**
 * 3、停止滚动
 * 当在Activity不可见时,在Activity.onStop()中调用
 */
public void cancel(){
    isCancel = true;
}

9、条目点击事件

  在组合控件中写一个条目点击事件的接口,在动态添加子条目时,为子条目添加点击事件,通过view.getTag()(数据适配器绑定数据时,将数据对象设置给子条目view)将当前点击的子条目对应的数据对象返回即可:

interface OnItemClickListener{
    public void onItemClick(Object obj);
}
private OnItemClickListener clickListener;
/**
 * 向容器中添加子条目
 * @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);
    }
}

  好了,该考虑的基本上都有了,看看最终的效果:

        Android自定义View(LimitScrollerView-仿天猫广告栏上下滚动效果)

注意:修复一处bug,生命周期方法可能导致消息反复发送,所以在发送滚动消息时应该移除handler中滚动的消息,否则会出现滚动动画错乱。

handler.sendEmptyMessageDelayed(MSG_SCROL, periodTime);

改为

handler.removeMessages(MSG_SCROL);   //先清空所有滚动消息,避免滚动错乱
handler.sendEmptyMessageDelayed(MSG_SCROL, periodTime);

喜欢请点赞,no爱请勿喷~O(∩_∩)O谢谢

源码下载:

注:没有积分的童鞋 请留言索要代码喔

http://download.csdn.net/detail/u010163442/9690822

Android自定义View(LimitScrollerView-仿天猫广告栏上下滚动效果)的更多相关文章

  1. Android自定义View实现仿QQ实现运动步数效果

    效果图: 1.attrs.xml中 <declare-styleable name="QQStepView"> <attr name="outerCol ...

  2. Android自定义View 画弧形&comma;文字&comma;并增加动画效果

    一个简单的Android自定义View的demo,画弧形,文字,开启一个多线程更新ui界面,在子线程更新ui是不允许的,但是View提供了方法,让我们来了解下吧. 1.封装一个抽象的View类   B ...

  3. android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检索

    我们的手机通讯录一般都有这样的效果,如下图: OK,这种效果大家都见得多了,基本上所有的android手机通讯录都有这样的效果.那我们今天就来看看这个效果该怎么实现. 一.概述 1.页面功能分析 整体 ...

  4. Android 自定义View,仿微信视频播放按钮

    闲着,尝试实现了新版微信视频播放按钮,使用的是自定义View,先来个简单的效果图...真的很简单哈. 由于暂时用不到,加上时间原因,加上实在是没意思,加上……,本控件就没有实现自定义属性,有兴趣的朋友 ...

  5. Android 自定义View修炼-仿QQ5&period;0 的侧滑菜单效果的实现

    有一段时间没有写博客了,最近比较忙,没什么时间写,刚好今天有点时间, 我就分享下,侧滑菜单的实现原理,一般android侧滑的实现原理和步骤如下:(源码下载在下面最后给出哈) 1.使用ViewGrou ...

  6. Android自定义view之仿微信录制视频按钮

    本文章只写了个类似微信的录制视频的按钮,效果图如下:             一.主要的功能: 1.长按显示进度条,单击事件,录制完成回调 2.最大时间和最小时间控制 3.进度条宽度,颜色设置 二.实 ...

  7. Android 自定义View修炼-仿360手机卫士波浪球进度的实现

    像360卫士的波浪球进度的效果,一般最常用的方法就是 画线的方式,先绘sin线或贝塞尔曲线,然后从左到右绘制竖线,然后再裁剪圆区域. 今天我这用图片bitmap的方式,大概的方法原理是: (1)首先用 ...

  8. Android自定义View之ProgressBar出场记

    关于自定义View,我们前面已经有三篇文章在介绍了,如果筒子们还没阅读,建议先看一下,分别是android自定义View之钟表诞生记.android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检 ...

  9. android自定义View之NotePad出鞘记

    现在我们的手机上基本都会有一个记事本,用起来倒也还算方便,记事本这种东东,如果我想要自己实现,该怎么做呢?今天我们就通过自定义View的方式来自定义一个记事本.OK,废话不多说,先来看看效果图. 整个 ...

随机推荐

  1. Bash Shell字符串操作

    转自:http://my.oschina.net/aiguozhe/blog/41557,并对内容作了验证修改. 1. 取长度 str="abcd" 2.查找子串的位置 貌似也只有 ...

  2. 词频junit测试

    package search; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; impor ...

  3. php的几个版本的区别?

    1. VC6与VC9的区别:VC6版本是使用Visual Studio 6编译器编译的,如果你的PHP是用Apache来架设的,那你就选择VC6版本.VC9版本是使用Visual Studio 200 ...

  4. POJ 2289&Tab;Jamie&&num;39&semi;s Contact Groups(多重匹配&plus;二分)

    题意: Jamie有很多联系人,但是很不方便管理,他想把这些联系人分成组,已知这些联系人可以被分到哪个组中去,而且要求每个组的联系人上限最小,即有一整数k,使每个组的联系人数都不大于k,问这个k最小是 ...

  5. javascript and dom1

    <script type="text/javascript"> //数组 var beatles=Array(4); var beatles=Array(); cons ...

  6. 《Python自然语言处理》第二章 学习笔记

    import nltk from nltk.book import * nltk.corpus.gutenberg.fileids() emma = nltk.corpus.gutenberg.wor ...

  7. 苹果手机input有圆角阴影的解决方法

    input[type=button], input[type=submit], input[type=file], button { cursor: pointer; -webkit-appearan ...

  8. java中的重载(overload)和重写&lpar;override&rpar;区别

    方法重载(overload): 方法重载就是在一个类中可以创建多个方法,它们具有相同的名字,但是具有不同的参数和不同定义,调用方法时通过传递给他们的不同参数个数和参数列表决定具体使用     哪   ...

  9. linux环境中&comma;如何使用tar来创建压缩包&quest;解压缩&quest;

    需求说明: 今天需要将一个tomcat目录打成压缩包,使用zip感觉有点慢,所以就想用tar来试试,之前一直使用tar的解压缩命令, 今天试试tar的压缩命令 操作过程: 1.通过tar的zcf选项进 ...

  10. PHP获取照片exif信息

    在这个没图说个屁的年代,照片还是很重要的.如果照片上传后会自动加上 照片相关信息,那用户体验的确会好很多,本着这个想法,今天尝试了下 PHP获取照片exif信息,重要的是 获取图片的GPS信息,如果再 ...