之前我也写过一篇关于Android竖直跑马灯效果的控件,不过这个控件是基于子Item是纯文本的情况,详情请移步:Android 自定义View实现竖直跑马灯效果,不过后面项目需求发生了变化,必须要整个Item包括图片啊文本啥的一起上下滚动,这个控件顿时就傻眼了,旧的设计架构是不行了,但是旧的思路依然可行。本文采取得思路和之前的是一样的,只是实现方式不同。放上效果图,DEMO在最下面
首先这里将大概的一些重点讲解一下:
//滚动间隔时间 和滚动动画时间 public static final int DURATION_SCROLL = 3000; public static final int DURATION_ANIMATOR = 1000; //实体集合和子控件集合 private List<T> beans = new ArrayList<T>(); private List<View> views = new ArrayList<View>(2); private int itemLayoutId; private Handler handler = new Handler(); //宽度和高度(包括padding) private int width; private int height; //第一个子View的中点Y坐标 private int centerY; //是否结束滚动 private boolean isStopScroll = true; //当前的索引 private int current; private OnItemClickListener listener; private OnItemBuilder builder; public VerticalMarqueeLayout(Context context) { super(context); } public VerticalMarqueeLayout(Context context, AttributeSet attrs) { super(context, attrs); } public VerticalMarqueeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public int getCurrentIndex(){ return current; } public VerticalMarqueeLayout listener(OnItemClickListener listener){ this.listener = listener; return this; } public VerticalMarqueeLayout builder(OnItemBuilder builder){ this.builder = builder; return this; } /** * 设置实体集合和item布局id */ public VerticalMarqueeLayout datas(List<T> beans, int itemLayoutId){ this.beans.clear(); this.beans.addAll(beans); this.itemLayoutId = itemLayoutId; return this; }
public interface OnItemClickListener{ void onItemClick(int position); } public abstract class OnItemBuilder{ public abstract void assemble(View view, T t); private void measure(View view){ view.measure(MeasureSpec.makeMeasureSpec(width - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); } public void builder(View view, T t){ //先装配数据 assemble(view, t); //重新测量 measure(view); } }
public void commit(){ if(builder == null){ throw new IllegalStateException("must invoke the method [builder(OnItemBuilder)]"); } this.views.clear(); if(beans != null && beans.size() != 0){ View view = View.inflate(getContext(), itemLayoutId, null); //在这里填充布局参数 if(builder != null){ builder.builder(view, beans.get(0)); } this.views.add(view); //这里通过手动设置全屏宽度的方式add addViewWidthMatchParent(view); //如果大于等于2个,初始化第二个View if(beans.size() > 1){ View view1 = View.inflate(getContext(), itemLayoutId, null); if(builder != null){ builder.builder(view1, beans.get(1)); } this.views.add(view1); addViewWidthMatchParent(view1); } //手动触发onMeasure和onDraw LayoutParams params = getLayoutParams(); if(params != null){ setLayoutParams(params); invalidate(); } current = 0; setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if(listener != null){ listener.onItemClick(current); } } }); } } private void addViewWidthMatchParent(View view){ LayoutParams params = new LayoutParams(view.getMeasuredWidth(), view.getMeasuredHeight()); addView(view, params); }
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if(beans.size() == 0 || views.size() == 0){ super.onMeasure(widthMeasureSpec, heightMeasureSpec); }else{ width = MeasureSpec.getSize(widthMeasureSpec); //这里必须再次measure一次,因为此时width才真正意义上有值 views.get(0).measure(MeasureSpec.makeMeasureSpec(width - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); height = views.get(0).getMeasuredHeight() + getPaddingTop() + getPaddingBottom(); centerY = height / 2; views.get(1).measure(MeasureSpec.makeMeasureSpec(width - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); } }
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //第一次布局时 if(changed){ change(l + getPaddingLeft(), t + getPaddingTop(), r - getPaddingRight(), b - getPaddingBottom()); } } private void change(int l, int t, int r, int b) { if(views.size() != 0){ //布局第一个View int dy = height / 2 - centerY; views.get(0).layout(l, t - dy, r, b - dy); //布局第二个View if(views.size() > 1){ views.get(1).layout(l, t + height - dy, r, b + height - dy); } } }
public void startScroll(){ stopScroll(); if(views.size() > 1){ isStopScroll = false; if(!isStopScroll){ handler.postDelayed(new Runnable() { @Override public void run() { scroll(); if(!isStopScroll){ handler.postDelayed(this, DURATION_SCROLL); } } }, DURATION_SCROLL); } } } private void scroll() { ValueAnimator animator = ValueAnimator.ofPropertyValuesHolder( PropertyValuesHolder.ofInt("centerY", height / 2, - height / 2)).setDuration(DURATION_ANIMATOR); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { centerY = (Integer) animation.getAnimatedValue("centerY"); //手动布局 change(getPaddingLeft(), getPaddingTop(), width - getPaddingRight(), height - getPaddingBottom()); invalidate(); } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) {} @Override public void onAnimationEnd(Animator animation) { //动画结束,进行迭代 current ++; //删除第一个View,然后加入到最后一个的位置 removeViewAt(0); View view = views.remove(0); views.add(view); addViewWidthMatchParent(view); //边界检查 if(current == beans.size() - 1){ if(builder != null){ builder.builder(views.get(0), beans.get(current)); builder.builder(view, beans.get(0)); } }else if(current == beans.size()){ current = 0; if(builder != null){ builder.builder(views.get(0), beans.get(current)); builder.builder(view, beans.get(current + 1)); } }else{ if(builder != null){ builder.builder(views.get(0), beans.get(current)); builder.builder(view, beans.get(current + 1)); } } //每一次动画结束,都触发onMeasure和onDraw,防止每一个Item的高度发生变化而出现错位 LayoutParams params = getLayoutParams(); setLayoutParams(params); invalidate(); } @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} }); animator.start(); } public void stopScroll(){ isStopScroll = true; handler.removeCallbacksAndMessages(null); }
package cc.wxf.component; import android.animation.Animator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; import android.content.Context; import android.os.Handler; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.List; /** * Created by ccwxf on 2016/8/1. */ public class VerticalMarqueeLayout<T> extends ViewGroup { //滚动间隔时间 和滚动动画时间 public static final int DURATION_SCROLL = 3000; public static final int DURATION_ANIMATOR = 1000; //实体集合和子控件集合 private List<T> beans = new ArrayList<T>(); private List<View> views = new ArrayList<View>(2); private int itemLayoutId; private Handler handler = new Handler(); //宽度和高度(包括padding) private int width; private int height; //第一个子View的中点Y坐标 private int centerY; //是否结束滚动 private boolean isStopScroll = true; //当前的索引 private int current; private OnItemClickListener listener; private OnItemBuilder builder; public VerticalMarqueeLayout(Context context) { super(context); } public VerticalMarqueeLayout(Context context, AttributeSet attrs) { super(context, attrs); } public VerticalMarqueeLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public int getCurrentIndex(){ return current; } public VerticalMarqueeLayout listener(OnItemClickListener listener){ this.listener = listener; return this; } public VerticalMarqueeLayout builder(OnItemBuilder builder){ this.builder = builder; return this; } /** * 设置实体集合和item布局id */ public VerticalMarqueeLayout datas(List<T> beans, int itemLayoutId){ this.beans.clear(); this.beans.addAll(beans); this.itemLayoutId = itemLayoutId; return this; } public void commit(){ if(builder == null){ throw new IllegalStateException("must invoke the method [builder(OnItemBuilder)]"); } this.views.clear(); if(beans != null && beans.size() != 0){ View view = View.inflate(getContext(), itemLayoutId, null); //在这里填充布局参数 if(builder != null){ builder.builder(view, beans.get(0)); } this.views.add(view); //这里通过手动设置全屏宽度的方式add addViewWidthMatchParent(view); //如果大于等于2个,初始化第二个View if(beans.size() > 1){ View view1 = View.inflate(getContext(), itemLayoutId, null); if(builder != null){ builder.builder(view1, beans.get(1)); } this.views.add(view1); addViewWidthMatchParent(view1); } //手动触发onMeasure和onDraw LayoutParams params = getLayoutParams(); if(params != null){ setLayoutParams(params); invalidate(); } current = 0; setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if(listener != null){ listener.onItemClick(current); } } }); } } private void addViewWidthMatchParent(View view){ LayoutParams params = new LayoutParams(view.getMeasuredWidth(), view.getMeasuredHeight()); addView(view, params); } public void startScroll(){ stopScroll(); if(views.size() > 1){ isStopScroll = false; if(!isStopScroll){ handler.postDelayed(new Runnable() { @Override public void run() { scroll(); if(!isStopScroll){ handler.postDelayed(this, DURATION_SCROLL); } } }, DURATION_SCROLL); } } } private void scroll() { ValueAnimator animator = ValueAnimator.ofPropertyValuesHolder( PropertyValuesHolder.ofInt("centerY", height / 2, - height / 2)).setDuration(DURATION_ANIMATOR); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { centerY = (Integer) animation.getAnimatedValue("centerY"); //手动布局 change(getPaddingLeft(), getPaddingTop(), width - getPaddingRight(), height - getPaddingBottom()); invalidate(); } }); animator.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) {} @Override public void onAnimationEnd(Animator animation) { //动画结束,进行迭代 current ++; //删除第一个View,然后加入到最后一个的位置 removeViewAt(0); View view = views.remove(0); views.add(view); addViewWidthMatchParent(view); //边界检查 if(current == beans.size() - 1){ if(builder != null){ builder.builder(views.get(0), beans.get(current)); builder.builder(view, beans.get(0)); } }else if(current == beans.size()){ current = 0; if(builder != null){ builder.builder(views.get(0), beans.get(current)); builder.builder(view, beans.get(current + 1)); } }else{ if(builder != null){ builder.builder(views.get(0), beans.get(current)); builder.builder(view, beans.get(current + 1)); } } //每一次动画结束,都触发onMeasure和onDraw,防止每一个Item的高度发生变化而出现错位 LayoutParams params = getLayoutParams(); setLayoutParams(params); invalidate(); } @Override public void onAnimationCancel(Animator animation) {} @Override public void onAnimationRepeat(Animator animation) {} }); animator.start(); } public void stopScroll(){ isStopScroll = true; handler.removeCallbacksAndMessages(null); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if(beans.size() == 0 || views.size() == 0){ super.onMeasure(widthMeasureSpec, heightMeasureSpec); }else{ width = MeasureSpec.getSize(widthMeasureSpec); //这里必须再次measure一次,因为此时width才真正意义上有值 views.get(0).measure(MeasureSpec.makeMeasureSpec(width - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); height = views.get(0).getMeasuredHeight() + getPaddingTop() + getPaddingBottom(); centerY = height / 2; views.get(1).measure(MeasureSpec.makeMeasureSpec(width - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); } } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { //第一次布局时 if(changed){ change(l + getPaddingLeft(), t + getPaddingTop(), r - getPaddingRight(), b - getPaddingBottom()); } } private void change(int l, int t, int r, int b) { if(views.size() != 0){ //布局第一个View int dy = height / 2 - centerY; views.get(0).layout(l, t - dy, r, b - dy); //布局第二个View if(views.size() > 1){ views.get(1).layout(l, t + height - dy, r, b + height - dy); } } } public interface OnItemClickListener{ void onItemClick(int position); } public abstract class OnItemBuilder{ public abstract void assemble(View view, T t); private void measure(View view){ view.measure(MeasureSpec.makeMeasureSpec(width - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); } public void builder(View view, T t){ //先装配数据 assemble(view, t); //重新测量 measure(view); } } }
package cc.wxf.androiddemo; /** * Created by ccwxf on 2016/8/1. */ public class Bean { private int icon; private String time; private String title; private String summary; public Bean() { } public Bean(int icon, String time, String title, String summary) { this.icon = icon; this.time = time; this.title = title; this.summary = summary; } public int getIcon() { return icon; } public void setIcon(int icon) { this.icon = icon; } public String getTime() { return time; } public void setTime(String time) { this.time = time; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getSummary() { return summary; } public void setSummary(String summary) { this.summary = summary; } @Override public String toString() { return "Bean{" + "icon=" + icon + ", time='" + time + '\'' + ", title='" + title + '\'' + ", summary='" + summary + '\'' + '}'; } }
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" android:padding="5dp" > <ImageView android:id="@+id/icon" android:layout_width="50dp" android:layout_height="50dp" android:scaleType="centerCrop" android:src="@mipmap/ic_launcher" /> <TextView android:id="@+id/time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:text="2016-8-1 14:42" android:textColor="@android:color/holo_blue_light" android:textSize="13sp" /> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" android:layout_toLeftOf="@id/time" android:layout_toRightOf="@id/icon" android:text="南海仲裁不过为一张废纸" android:textColor="@android:color/black" android:lines="1" android:ellipsize="end" android:textSize="15sp" /> <TextView android:id="@+id/summary" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignLeft="@id/title" android:layout_below="@id/title" android:layout_marginTop="5dp" android:ellipsize="end" android:lines="2" android:text="南海仲裁不过为一张废纸南海仲裁不过为一张废纸南海仲裁不过为一张废纸南海仲裁不过为一张废纸" android:textColor="@android:color/darker_gray" android:textSize="13sp" /> </RelativeLayout>
<?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" android:background="#e3e3e3" android:orientation="vertical" > <cc.wxf.component.VerticalMarqueeLayout android:id="@+id/vmLayout" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="10dp" /> </LinearLayout>
package cc.wxf.androiddemo; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import java.util.ArrayList; import java.util.List; import cc.wxf.component.VerticalMarqueeLayout; public class MainActivity extends Activity { private VerticalMarqueeLayout<Bean> vmLayout; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); List<Bean> beans = new ArrayList<Bean>(); for(int i = 0; i < 6; i++){ beans.add(new Bean(R.mipmap.ic_launcher, "2016/8/1 15:51" , "南海仲裁一张废纸" + i ,"南海仲裁不过为一张废纸南海仲裁不过为一张废纸南海仲裁不过为一张废纸南海仲裁不过为一张废纸" + i)); } vmLayout = (VerticalMarqueeLayout<Bean>) findViewById(R.id.vmLayout); vmLayout.datas(beans, R.layout.item).builder(vmLayout.new OnItemBuilder(){ @Override public void assemble(View view, Bean bean) { ImageView icon = (ImageView) view.findViewById(R.id.icon); TextView time = (TextView) view.findViewById(R.id.time); TextView title = (TextView) view.findViewById(R.id.title); TextView summary = (TextView) view.findViewById(R.id.summary); icon.setImageResource(bean.getIcon()); time.setText(bean.getTime()); title.setText(bean.getTitle()); summary.setText(bean.getSummary()); } }).listener(new VerticalMarqueeLayout.OnItemClickListener() { @Override public void onItemClick(int position) { Toast.makeText(MainActivity.this, "当前选择:" + position, Toast.LENGTH_SHORT).show(); } }).commit(); vmLayout.startScroll(); } @Override protected void onDestroy() { super.onDestroy(); if(vmLayout != null){ vmLayout.stopScroll(); } } }