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

本文主要是介绍Android自定义View精品(LimitScrollerView-仿天猫广告栏上下滚动效果),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

版权声明:本文为openXu原创文章【openXu的博客】,未经博主允许不得以任何形式转载

文章目录

    • 1、分析
    • 2、定义组合控件布局
    • 3、继承最外层控件
    • 4、自定义属性
    • 5、重写onMeasure
    • 6、数据适配器
    • 7、动态添加子条目
    • 8、滚动动画
    • 9、条目点击事件

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

    这里写图片描述 这里写图片描述

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

1、分析

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

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

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

  思路是有了,要实现起来得考虑细节了。最外层用什么包裹?继承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"><LinearLayoutandroid:id="@+id/ll_content1"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"/><LinearLayoutandroid: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.LimitScrollerViewandroid: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();}@Overridepublic int getCount() {return datas==null?0:datas.size();}@Overridepublic 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(){@Overridepublic 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),向上滚动scrollHeightObjectAnimator anim1 = ObjectAnimator.ofFloat(ll_now, "Y",ll_now.getY(), ll_now.getY()-scrollHeight);//预备容器,从当前位置,向上滚动scrollHeightObjectAnimator 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() {@Overridepublic void onAnimationStart(Animator animation) {}@Overridepublic 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);}@Overridepublic void onAnimationCancel(Animator animation) {}@Overridepublic 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);}
}

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

        这里写图片描述

注意:修复一处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 CSDN下载平台太流氓

https://github.com/openXu/LimitScrollerView

这篇关于Android自定义View精品(LimitScrollerView-仿天猫广告栏上下滚动效果)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/968205

相关文章

基于Python实现PDF动画翻页效果的阅读器

《基于Python实现PDF动画翻页效果的阅读器》在这篇博客中,我们将深入分析一个基于wxPython实现的PDF阅读器程序,该程序支持加载PDF文件并显示页面内容,同时支持页面切换动画效果,文中有详... 目录全部代码代码结构初始化 UI 界面加载 PDF 文件显示 PDF 页面页面切换动画运行效果总结主

React实现原生APP切换效果

《React实现原生APP切换效果》最近需要使用Hybrid的方式开发一个APP,交互和原生APP相似并且需要IM通信,本文给大家介绍了使用React实现原生APP切换效果,文中通过代码示例讲解的非常... 目录背景需求概览技术栈实现步骤根据 react-router-dom 文档配置好路由添加过渡动画使用

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO

SpringBoot 自定义消息转换器使用详解

《SpringBoot自定义消息转换器使用详解》本文详细介绍了SpringBoot消息转换器的知识,并通过案例操作演示了如何进行自定义消息转换器的定制开发和使用,感兴趣的朋友一起看看吧... 目录一、前言二、SpringBoot 内容协商介绍2.1 什么是内容协商2.2 内容协商机制深入理解2.2.1 内容

使用Python实现生命之轮Wheel of life效果

《使用Python实现生命之轮Wheeloflife效果》生命之轮Wheeloflife这一概念最初由SuccessMotivation®Institute,Inc.的创始人PaulJ.Meyer... 最近看一个生命之轮的视频,让我们珍惜时间,因为一生是有限的。使用python创建生命倒计时图表,珍惜时间

Android WebView的加载超时处理方案

《AndroidWebView的加载超时处理方案》在Android开发中,WebView是一个常用的组件,用于在应用中嵌入网页,然而,当网络状况不佳或页面加载过慢时,用户可能会遇到加载超时的问题,本... 目录引言一、WebView加载超时的原因二、加载超时处理方案1. 使用Handler和Timer进行超

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

防近视护眼台灯什么牌子好?五款防近视效果好的护眼台灯推荐

在家里,灯具是属于离不开的家具,每个大大小小的地方都需要的照亮,所以一盏好灯是必不可少的,每个发挥着作用。而护眼台灯就起了一个保护眼睛,预防近视的作用。可以保护我们在学习,阅读的时候提供一个合适的光线环境,保护我们的眼睛。防近视护眼台灯什么牌子好?那我们怎么选择一个优秀的护眼台灯也是很重要,才能起到最大的护眼效果。下面五款防近视效果好的护眼台灯推荐: 一:六个推荐防近视效果好的护眼台灯的