前段时间b站直播新出了个功能,叫醒目留言。app端的显示效果大概像这个样子:
看了一下觉得挺有意思,于是自己写了一个控件实现这种效果。话不多说先看效果:
演示完效果之后正片开始。核心组件就一个:EyesCatchingMessageView。上代码:
public class EyesCatchingMessageView extends RelativeLayout {
/**
* 宽度模式,
*
* MODE_FIXED为固定长度模式
* MODE_WRAP为自适应模式
*/
public static final int MODE_FIXED = 0x1001;
public static final int MODE_WRAP = 0x1002;
private Context context;
// 背景图层
private View backgroundView;
// 计时时变化的图层
private View timingView;
// 留言内容图层
private LinearLayout messageLayout;
// 头像
private ImageView portraitImageView;
// 留言文字
private TextView messageTextView;
/**
* 可设置参数
*/
// 宽度模式,其值为MODE_FIXED或MODE_WRAP
private int widthMode;
// 视图识别id,在创建时由创建者给定
private int viewId;
// 控件宽
private int width;
// 控件高
private int height;
// 头像半径
private int portraitRadius;
// 背景图层颜色
private int backgroundViewColor;
// 计时图层颜色
private int timingViewColor;
// 留言内容
private String message;
// 留言字体大小
private int messageTextSize;
// 留言字体颜色
private int messageTextColor;
// 留言文字左间距
private int messageLeftMargin;
// 留言文字右间距
private int messageRightMargin;
// 留言显示长度限制
private int messageLengthLimit;
// 是否显示留言文字,默认为显示
private boolean showMessageText;
// 计时总时长
private float totalTime;
// 当前计时时长
private float currentTime = 0;
// 是否正在计时状态标识
private boolean isTiming = false;
// 剪裁路径,用于把视图显示区域剪裁成需要的形状
private Path reoundPath;
// 属性动画,用于计时
private ValueAnimator timingAnimator;
// 头像加载接口
private PortraitLoader portraitLoader;
// 计时监听接口
private OnTimingListenerAdapter timingListener;
public EyesCatchingMessageView(Context context) {
this(context, null);
}
public EyesCatchingMessageView(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public EyesCatchingMessageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**
* 初始化方法,给定可设置参数的默认值
*
* @param context
*/
private void init(Context context) {
this.context = context;
widthMode = MODE_WRAP;
width = dp2px(80);
height = dp2px(40);
portraitRadius = dp2px(15);
backgroundViewColor = Color.parseColor("#70FF6347");
timingViewColor = Color.parseColor("#FFFF6347");
messageTextSize = 14;
messageTextColor = Color.parseColor("#FFFFFF");
messageLeftMargin = dp2px(10);
messageRightMargin = dp2px(10);
messageLengthLimit = 8;
showMessageText = true;
setWillNotDraw(false);
}
/**
* 视图创建方法
*/
private void create() {
backgroundView = new View(context);
timingView = new View(context);
backgroundView.setBackgroundColor(backgroundViewColor);
timingView.setBackgroundColor(timingViewColor);
RelativeLayout.LayoutParams backgroundViewParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
RelativeLayout.LayoutParams timingViewParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
backgroundViewParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
timingViewParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
addView(backgroundView, backgroundViewParams);
addView(timingView, timingViewParams);
messageLayout = new LinearLayout(context);
messageLayout.setGravity(Gravity.CENTER_VERTICAL);
messageLayout.setOrientation(LinearLayout.HORIZONTAL);
addView(messageLayout, new RelativeLayout.LayoutParams
(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
portraitImageView = new RoundImageView(context);
if (portraitLoader != null) {
portraitLoader.onLoad(viewId, portraitImageView);
}
LinearLayout.LayoutParams portraitParams = new LinearLayout.LayoutParams(2 * portraitRadius, 2 * portraitRadius);
int portraitMargin = height / 2 - portraitRadius;
portraitParams.setMargins(portraitMargin, portraitMargin, 0, portraitMargin);
messageLayout.addView(portraitImageView, portraitParams);
messageTextView = new TextView(context);
messageTextView.setText(cutMessage(message));
messageTextView.setTextColor(messageTextColor);
messageTextView.setTextSize(messageTextSize);
messageTextView.setMaxLines(1);
messageTextView.setVisibility(showMessageText ? VISIBLE : INVISIBLE);
LinearLayout.LayoutParams messageParams = new LinearLayout.LayoutParams
(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
messageParams.setMargins(messageLeftMargin, 0, messageRightMargin, 0);
messageLayout.addView(messageTextView, messageParams);
messageLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
messageLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
if (widthMode == MODE_WRAP) {
width = messageLayout.getWidth();
}
getLayoutParams().width = width;
getLayoutParams().height = height;
// 创建一个圆角方形的剪裁区域
reoundPath = new Path();
reoundPath.addRoundRect(
new RectF(0, 0, width, height),
new float[]{
width / 2f, width / 2f,
width / 2f, width / 2f,
width / 2f, width / 2f,
width / 2f, width / 2f},
Path.Direction.CW);
}
});
}
@Override
protected void onDraw(Canvas canvas) {
if (reoundPath != null) {
// 剪裁视图显示区域
canvas.clipPath(reoundPath);
}
super.onDraw(canvas);
}
/**
* 开始计时方法
*
* @param timingTotalTime 计时总时长,单位秒
*/
public void startTiming(float timingTotalTime) {
startTiming(timingTotalTime, timingTotalTime);
}
/**
* 开始计时方法
*
* @param timingTotalTime 计时总时长,单位秒
* @param initialTime timingTotalTime=100s,initialTime=60s,则表示从剩余进度60%处开始计时
*/
public void startTiming(float timingTotalTime, float initialTime) {
if (isTiming) {
return;
}
if (timingTotalTime <= 0 || initialTime < 0 || initialTime > timingTotalTime) {
return;
}
this.totalTime = timingTotalTime;
timingAnimator = ValueAnimator.ofFloat(new float[]{initialTime * 1000, 0});
timingAnimator.setInterpolator(new LinearInterpolator());
timingAnimator.setDuration((long) (initialTime * 1000));
timingAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationCancel(Animator animation) {
currentTime = 0;
isTiming = false;
if (timingListener != null) {
timingListener.onCancel(viewId);
}
}
@Override
public void onAnimationEnd(Animator animation) {
currentTime = 0;
isTiming = false;
if (timingListener != null) {
timingListener.onFinish(viewId);
}
}
@Override
public void onAnimationStart(Animator animation) {
currentTime = totalTime;
isTiming = true;
if (timingListener != null) {
timingListener.onStart(viewId);
}
}
});
timingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentTime = (float) animation.getAnimatedValue() / 1000;
refreshTimingView(currentTime / totalTime);
if (timingListener != null) {
timingListener.onTiming(viewId, totalTime, currentTime, currentTime / totalTime);
}
}
});
timingAnimator.start();
}
/**
* 取消计时方法
*/
public void cancelTiming() {
if (!isTiming) {
return;
}
if (timingAnimator != null && timingAnimator.isRunning()) {
timingAnimator.cancel();
}
}
/**
* 更新计时图层
*
* @param percentage 剩余进度百分比
*/
private void refreshTimingView(float percentage) {
timingView.getLayoutParams().width = (int) (width * percentage);
timingView.requestLayout();
}
/**
* 限制留言显示
*
* @param message 留言内容
* @return
*/
private String cutMessage(String message) {
if (message == null) {
return null;
}
if (message.length() <= messageLengthLimit) {
return message;
} else {
return message.substring(0, messageLengthLimit) + "...";
}
}
/**
* 设置宽度模式
*
* @param widthMode 宽度模式,可选项:
* MODE_FIXED 固定长度模式
* MODE_WRAP 自适应模式
*/
public void setWidthMode(int widthMode) {
if (!(widthMode == MODE_FIXED || widthMode == MODE_FIXED)) {
return;
}
this.widthMode = widthMode;
}
/**
* 设置视图识别id
*
* @param viewId 识别id
*/
public void setViewId(int viewId) {
this.viewId = viewId;
}
/**
* 获取视图识别id
*
* @return
*/
public int getViewId() {
return viewId;
}
/**
* 设置视图宽度,仅在宽度模式(widthMode)为固定长度模式(MODE_FIXED)时有效
*
* @param width 宽度,单位dp
*/
public void setWidth(int width) {
this.width = dp2px(width);
}
/**
* 设置视图高度
*
* @param height 高度,单位dp
*/
public void setHeight(int height) {
this.height = dp2px(height);
}
/**
* 设置头像半径
*
* @param portraitRadius 半径,单位dp
*/
public void setPortraitRadius(int portraitRadius) {
this.portraitRadius = dp2px(portraitRadius);
}
/**
* 设置背景图层颜色
*
* @param backgroundViewColor 颜色值
*/
public void setBackgroundViewColor(int backgroundViewColor) {
this.backgroundViewColor = backgroundViewColor;
if (backgroundView != null) {
backgroundView.setBackgroundColor(backgroundViewColor);
}
}
/**
* 设置计时图层颜色
*
* @param timingViewColor 颜色值
*/
public void setTimingViewColor(int timingViewColor) {
this.timingViewColor = timingViewColor;
if (timingView != null) {
timingView.setBackgroundColor(timingViewColor);
}
}
/**
* 设置留言内容
*
* @param message 留言内容
*/
public void setMessage(String message) {
this.message = message;
if (messageTextView != null) {
messageTextView.setText(cutMessage(message));
}
}
/**
* 设置留言字体大小
*
* @param messageTextSize 字体大小,单位sp
*/
public void setMessageTextSize(int messageTextSize) {
this.messageTextSize = messageTextSize;
if (messageTextView != null) {
messageTextView.setTextSize(messageTextSize);
}
}
/**
* 设置留言字体颜色
*
* @param messageTextColor 颜色值
*/
public void setMessageTextColor(int messageTextColor) {
this.messageTextColor = messageTextColor;
if (messageTextView != null) {
messageTextView.setTextColor(messageTextColor);
}
}
/**
* 设置留言文字左间距
*
* @param messageLeftMargin 间距,单位dp
*/
public void setMessageLeftMargin(int messageLeftMargin) {
this.messageLeftMargin = dp2px(messageLeftMargin);
}
/**
* 设置留言文字右间距
*
* @param messageRightMargin 间距,单位dp
*/
public void setMessageRightMargin(int messageRightMargin) {
this.messageRightMargin = dp2px(messageRightMargin);
}
/**
* 设置留言显示长度限制
*
* @param messageLengthLimit 长度限制
*/
public void setMessageLengthLimit(int messageLengthLimit) {
this.messageLengthLimit = messageLengthLimit;
if (messageTextView != null) {
messageTextView.setText(cutMessage(message));
}
}
/**
* 设置是否显示留言文字
*
* @param showMessageText
*/
public void setShowMessageText(boolean showMessageText) {
this.showMessageText = showMessageText;
}
/**
* 获取剩余进度百分比
*
* @return
*/
public float getPercentage() {
if (isTiming && totalTime > 0) {
return currentTime / totalTime;
}
return 0;
}
/**
* 设置计时监听接口
*
* @param timingListener
*/
public void setTimingListener(OnTimingListenerAdapter timingListener) {
this.timingListener = timingListener;
}
/**
* 设置头像加载接口
*
* @param portraitLoader
*/
public void setPortraitLoader(PortraitLoader portraitLoader) {
this.portraitLoader = portraitLoader;
}
/**
* 头像加载接口
*/
public interface PortraitLoader {
void onLoad(int viewId, ImageView portraitImageView);
}
/**
* 计时监听接口
*/
private interface OnTimingListener {
/**
* 计时开始时回调
*
* @param viewId
*/
void onStart(int viewId);
/**
* 计时过程中回调
*
* @param viewId
* @param totalTime 计时总时长
* @param currentTime 当前计时时长
* @param percentage 剩余进度百分比
*/
void onTiming(int viewId, float totalTime, float currentTime, float percentage);
/**
* 计时结束时回调
*
* @param viewId
*/
void onFinish(int viewId);
/**
* 计时取消是回调
*
* @param viewId
*/
void onCancel(int viewId);
}
/**
* 计时监听接口适配器,作用是让调用者自助选择需要监听的回调方法
*/
public static class OnTimingListenerAdapter implements OnTimingListener {
@Override
public void onStart(int viewId) {
}
@Override
public void onTiming(int viewId, float totalTime, float currentTime, float percentage) {
}
@Override
public void onFinish(int viewId) {
}
@Override
public void onCancel(int viewId) {
}
}
private int dp2px(int dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, getResources().getDisplayMetrics());
}
/**
* 视图建造者
*
* 可以通过视图建造者方便的创建一个视图对象
*/
public static class EyesCatchingMessageViewBuilder {
private EyesCatchingMessageView eyesCatchingMessageView;
public EyesCatchingMessageViewBuilder(Context context) {
eyesCatchingMessageView = new EyesCatchingMessageView(context);
}
public EyesCatchingMessageViewBuilder setWidthMode(int widthMode) {
eyesCatchingMessageView.setWidthMode(widthMode);
return this;
}
public EyesCatchingMessageViewBuilder setViewId(int viewId) {
eyesCatchingMessageView.setViewId(viewId);
return this;
}
public EyesCatchingMessageViewBuilder setWidth(int width) {
eyesCatchingMessageView.setWidth(width);
return this;
}
public EyesCatchingMessageViewBuilder setHeight(int height) {
eyesCatchingMessageView.setHeight(height);
return this;
}
public EyesCatchingMessageViewBuilder setPortraitRadius(int portraitRadius) {
eyesCatchingMessageView.setPortraitRadius(portraitRadius);
return this;
}
public EyesCatchingMessageViewBuilder setBackgroundViewColor(int backgroundViewColor) {
eyesCatchingMessageView.setBackgroundViewColor(backgroundViewColor);
return this;
}
public EyesCatchingMessageViewBuilder setTimingViewColor(int timingViewColor) {
eyesCatchingMessageView.setTimingViewColor(timingViewColor);
return this;
}
public EyesCatchingMessageViewBuilder setMessage(String message) {
eyesCatchingMessageView.setMessage(message);
return this;
}
public EyesCatchingMessageViewBuilder setMessageTextSize(int messageTextSize) {
eyesCatchingMessageView.setMessageTextSize(messageTextSize);
return this;
}
public EyesCatchingMessageViewBuilder setMessageTextColor(int messageTextColor) {
eyesCatchingMessageView.setMessageTextColor(messageTextColor);
return this;
}
public EyesCatchingMessageViewBuilder setMessageLeftMargin(int messageLeftMargin) {
eyesCatchingMessageView.setMessageLeftMargin(messageLeftMargin);
return this;
}
public EyesCatchingMessageViewBuilder setMessageRightMargin(int messageRightMargin) {
eyesCatchingMessageView.setMessageRightMargin(messageRightMargin);
return this;
}
public EyesCatchingMessageViewBuilder setMessageLengthLimit(int messageLengthLimit) {
eyesCatchingMessageView.setMessageLengthLimit(messageLengthLimit);
return this;
}
public EyesCatchingMessageViewBuilder setShowMessageText(boolean showMessageText) {
eyesCatchingMessageView.setShowMessageText(showMessageText);
return this;
}
public EyesCatchingMessageViewBuilder setTimingListener(OnTimingListenerAdapter timingListener) {
eyesCatchingMessageView.setTimingListener(timingListener);
return this;
}
public EyesCatchingMessageViewBuilder setPortraitLoader(PortraitLoader portraitLoader) {
eyesCatchingMessageView.setPortraitLoader(portraitLoader);
return this;
}
/**
* 创建醒目留言视图对象
*
* @return 视图对象实例
*/
public EyesCatchingMessageView create() {
eyesCatchingMessageView.create();
return eyesCatchingMessageView;
}
}
}
简单讲一下实现原理。原理其实并不复杂,只需要知道明白两点,一是圆角矩形的图案是怎么实现的,通过下面这句话实现:
canvas.clipPath(reoundPath);
即在绘制图形时手动把画布剪裁成了圆角矩形。
二是计时动画如何实现。通过属性动画,也就是这句:
timingAnimator = ValueAnimator.ofFloat(new float[]{initialTime * 1000, 0});
然后在属性动画开启后,不断更新代表进度的View的宽度来实现计时效果,也就是这两句话:
timingView.getLayoutParams().width = (int) (width * percentage);
timingView.requestLayout();
这个控件里需要用到另一个圆形图片控件RoundImageView,这个在我之前的博客里介绍过,需要的戳这里查看,这里就不赘述了。
然后就可以使用了。控件提供了一个内部建造者类EyesCatchingMessageViewBuilder来让调用者快捷地创建一个EyesCatchingMessageView对象。当然你也可以new一个EyesCatchingMessageView对象,然后调用它自身的各种set方法设置参数来完成创建,我也不拦着你。好了简单写几句代码:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EyesCatchingMessageView eyesCatchingMessageView = new EyesCatchingMessageView.EyesCatchingMessageViewBuilder(this)
.setViewId(0)
.setPortraitLoader(new EyesCatchingMessageView.PortraitLoader() {
@Override
public void onLoad(int viewId, ImageView portraitImageView) {
portraitImageView.setImageResource(R.drawable.huaji);
}
})
.setMessage("哈哈哈")
.create();
addContentView(eyesCatchingMessageView,
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
eyesCatchingMessageView.startTiming(10);
}
运行一下看一下效果:
用法很简单,就是new一个EyesCatchingMessageViewBuilder对象,然后set各种参数,最后调用它的create()方法就可以创建一个EyesCatchingMessageView对象。最后调用EyesCatchingMessageView的startTiming方法来开启计时。
setViewId方法是给控件添加一个识别id,由调用者给定,只要保证它的唯一性让你能认出它来就可以了。
setPortraitLoader是设置头像加载器,因为具体项目中头像加载往往是异步的,具体加载方法也不一样,所以通过这个接口把加载过程抽象出来让调用者自己实现。
当然EyesCatchingMessageView还支持各种别的参数设置,你可以设置宽高、颜色、头像半径,文字颜色大小间距等等,还可以通过接口监听计时过程。
修改一下代码:
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EyesCatchingMessageView eyesCatchingMessageView = new EyesCatchingMessageView.EyesCatchingMessageViewBuilder(this)
.setWidthMode(EyesCatchingMessageView.MODE_WRAP)
.setViewId(0)
.setHeight(80)
.setPortraitRadius(30)
.setMessage("哈哈哈哈哈哈哈哈哈哈哈哈")
.setMessageLengthLimit(7)
.setBackgroundViewColor(Color.parseColor("#70FFD700"))
.setTimingViewColor(Color.parseColor("#FFD700"))
.setMessageTextColor(Color.parseColor("#000000"))
.setMessageTextSize(30)
.setMessageLeftMargin(20)
.setMessageRightMargin(20)
.setPortraitLoader(new EyesCatchingMessageView.PortraitLoader() {
@Override
public void onLoad(int viewId, ImageView portraitImageView) {
portraitImageView.setImageResource(R.drawable.huaji);
}
})
.setTimingListener(new EyesCatchingMessageView.OnTimingListenerAdapter() {
@Override
public void onFinish(int viewId) {
Toast.makeText(MainActivity.this, "Timing Finish", Toast.LENGTH_SHORT).show();
}
})
.create();
addContentView(eyesCatchingMessageView,
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
eyesCatchingMessageView.startTiming(10);
}
各种参数的意义注释里写得很清楚了,大家自己看注释就好。这里就特别提一嘴setWidthMode这个方法,这个方法是设置醒目留言的宽度显示模式。可选项有两种:MODE_FIXED和MODE_WRAP。MODE_FIXED是固定长度模式,该模式下可通过setWidth方法给定醒目留言的宽度;MODE_WRAP是自适应模式,无需给定宽度,其宽度会根据头像宽度及文字长度自适应。
好了,运行一下看一下效果:
到此为止,基本效果是实现了,但实际项目中使用起来肯定是不好用的,因为它功能集成地不够彻底,具体增加、删除、管理醒目留言的逻辑代码需要自己写。怎么办嘞?再写一个布局控件统一管理醒目留言对象。上代码:
public class EyesCatchingMessageLayout extends LinearLayout {
private Context context;
// 横向滚动容器
private HorizontalScrollView scrollView;
// 醒目留言视图容器
private LinearLayout parentLayout;
// 醒目留言视图对象集合
private ArrayList messageViews;
// 醒目留言数据集合
private ArrayList messageItems;
// 醒目留言配置集合
private ArrayList customConfigs;
/**
* 可设置参数
*/
// 醒目留言向上间距
private int paddingTop;
// 醒目留言向下间距
private int paddingBottom;
// 第一个醒目留言向左间距
private int paddingLeft;
// 最后一个醒目留言向右间距
private int paddingRight;
// 醒目留言向下间距
private int messageMargin;
// 醒目留言参数配置类
private EyesCatchingMessageConfig config;
// 排序比较器
private SortComparator sortComparator;
// 醒目留言点击监听接口
private OnMessageClickListener clickListener;
// 头像加载接口
private PortraitLoader portraitLoader;
// 计时监听接口
private OnTimingListenerAdapter timingListener;
public EyesCatchingMessageLayout(Context context) {
this(context, null);
}
public EyesCatchingMessageLayout(Context context, AttributeSet attrs) {
this(context, attrs, -1);
}
public EyesCatchingMessageLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
/**
* 初始化方法,给定可设置参数的默认值
*
* @param context
*/
private void init(Context context) {
this.context = context;
paddingTop = dp2px(10);
paddingBottom = dp2px(10);
paddingLeft = dp2px(15);
paddingRight = dp2px(15);
messageMargin = dp2px(10);
setOrientation(HORIZONTAL);
setGravity(Gravity.CENTER_VERTICAL);
messageViews = new ArrayList<>();
messageItems = new ArrayList<>();
customConfigs = new ArrayList<>();
sortComparator = new SortComparator() {
@Override
public boolean onCompare(MessageItem item1, MessageItem item2) {
return true;
}
};
create();
}
/**
* 创建方法,创建视图
*/
private void create() {
scrollView = new HorizontalScrollView(context);
parentLayout = new LinearLayout(context);
parentLayout.setOrientation(HORIZONTAL);
parentLayout.setGravity(Gravity.CENTER_VERTICAL);
scrollView.setHorizontalScrollBarEnabled(false);
scrollView.addView(parentLayout, new HorizontalScrollView.LayoutParams
(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1);
addView(scrollView, scrollViewParams);
}
/**
* 添加一个醒目留言
*
* @param messageItem 数据对象
*/
public void addMessageView(MessageItem messageItem) {
doAddMessageView(messageItem, null, null, null);
}
/**
* 添加一个醒目留言
*
* @param messageItem 数据对象
* @param width 指定醒目留言宽度,单位dp
*/
public void addMessageView(MessageItem messageItem, int width) {
doAddMessageView(messageItem, width, null, null);
}
/**
* 添加一个醒目留言
*
* @param messageItem 数据对象
* @param backgroundViewColor 指定背景图层颜色
* @param timingViewColor 指定计时图层颜色
*/
public void addMessageView(MessageItem messageItem, int backgroundViewColor, int timingViewColor) {
doAddMessageView(messageItem, null, backgroundViewColor, timingViewColor);
}
/**
* 添加一个醒目留言
*
* @param messageItem 数据对象
* @param width 指定醒目留言宽度,单位dp
* @param backgroundViewColor 指定背景图层颜色
* @param timingViewColor 指定计时图层颜色
*/
public void addMessageView(MessageItem messageItem, int width, int backgroundViewColor, int timingViewColor) {
doAddMessageView(messageItem, width, backgroundViewColor, timingViewColor);
}
/**
* 添加多个醒目留言
*
* @param messageList 数据对象集合
*/
public void addMessageViews(List messageList) {
if (messageList == null) {
return;
}
for (int i = 0; i < messageList.size(); i++) {
addMessageView(messageList.get(i));
}
}
/**
* 执行添加醒目留言方法
*
* @param messageItem
* @param width
* @param backgroundViewColor
* @param timingViewColor
*/
private void doAddMessageView(MessageItem messageItem, Integer width, Integer backgroundViewColor, Integer timingViewColor) {
if (messageItem == null) {
return;
}
EyesCatchingMessageConfig customConfig = new EyesCatchingMessageConfig.EyesCatchingMessageConfigBuilder()
.setWidth(width)
.setBackgroundViewColor(backgroundViewColor)
.setTimingViewColor(timingViewColor)
.create();
ArrayList tempItems = new ArrayList<>();
ArrayList tempConfigs = new ArrayList<>();
tempItems.add(messageItem);
tempConfigs.add(customConfig);
tempItems.addAll(messageItems);
tempConfigs.addAll(customConfigs);
for (int i = tempItems.size() - 1; i > 0; i--) {
for (int j = 0; j < i; j++) {
if (!sortComparator.onCompare(tempItems.get(j), tempItems.get(j + 1))) {
MessageItem tempItem = tempItems.get(j + 1);
tempItems.set(j + 1, tempItems.get(j));
tempItems.set(j, tempItem);
EyesCatchingMessageConfig tempConfig = tempConfigs.get(j + 1);
tempConfigs.set(j + 1, tempConfigs.get(j));
tempConfigs.set(j, tempConfig);
}
}
}
cancelAllTiming();
messageItems.clear();
customConfigs.clear();
messageItems.addAll(tempItems);
customConfigs.addAll(tempConfigs);
for (int i = 0; i < messageItems.size(); i++) {
addEyesCatchingMessageView(messageItems.get(i), customConfigs.get(i));
}
}
/**
* 添加醒目留言方法
*
* @param messageItem 数据对象
* @param customConfig 自定义配置对象
*/
private void addEyesCatchingMessageView(MessageItem messageItem, EyesCatchingMessageConfig customConfig) {
EyesCatchingMessageView.EyesCatchingMessageViewBuilder builder = new EyesCatchingMessageView.EyesCatchingMessageViewBuilder(context);
writeMessageConfig(builder, config);
writeMessageConfig(builder, customConfig);
EyesCatchingMessageView view = builder
.setViewId(messageItem.getViewId())
.setMessage(messageItem.getMessage())
.setTimingListener(new EyesCatchingMessageView.OnTimingListenerAdapter() {
@Override
public void onStart(int viewId) {
if (timingListener != null) {
int position = findPostionByViewId(viewId);
if (position != -1) {
timingListener.onStart(position, messageItems.get(position));
}
}
}
@Override
public void onTiming(int viewId, float totalTime, float currentTime, float percentage) {
int position = findPostionByViewId(viewId);
if (position != -1) {
messageItems.get(position).setInitialTime(currentTime);
if (timingListener != null) {
timingListener.onTiming(position, messageItems.get(position), currentTime, percentage);
}
}
}
@Override
public void onFinish(int viewId) {
if (timingListener != null) {
int position = findPostionByViewId(viewId);
if (position != -1) {
timingListener.onFinish(position, messageItems.get(position));
}
}
removeEyesCatchingMessageView(viewId);
}
@Override
public void onCancel(int viewId) {
if (timingListener != null) {
int position = findPostionByViewId(viewId);
if (position != -1) {
timingListener.onCancel(position, messageItems.get(position));
}
}
}
})
.setPortraitLoader(new EyesCatchingMessageView.PortraitLoader() {
@Override
public void onLoad(int viewId, ImageView portraitImageView) {
if (portraitLoader != null) {
int position = findPostionByViewId(viewId);
portraitLoader.onLoad(position, messageItems.get(position), portraitImageView);
}
}
})
.create();
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(messageViews.size() == 0 ? paddingLeft : messageMargin, paddingTop, paddingRight, paddingBottom);
if (messageViews.size() != 0) {
((LinearLayout.LayoutParams) messageViews.get(messageViews.size() - 1).getLayoutParams())
.setMargins(messageViews.size() == 1 ? paddingLeft : messageMargin, paddingTop, 0, paddingBottom);
}
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (clickListener != null) {
EyesCatchingMessageView currentView = (EyesCatchingMessageView) v;
int position = findPostionByViewId(currentView.getViewId());
clickListener.onMessageClick(position, currentView.getPercentage(), messageItems.get(position));
}
}
});
messageViews.add(view);
parentLayout.addView(view, params);
view.startTiming(messageItem.getTotalTime(), messageItem.getInitialTime());
}
/**
* 手动取消一个醒目留言
*
* @param viewId 识别id
*/
public void cancelTimingByViewId(int viewId) {
int position = findPostionByViewId(viewId);
if (position != -1) {
messageViews.get(position).cancelTiming();
}
}
/**
* 手动取消所有醒目留言
*/
public void cancelAllTiming() {
OnTimingListenerAdapter temp = timingListener;
timingListener = null;
while (messageViews.size() > 0) {
messageViews.get(0).cancelTiming();
}
timingListener = temp;
}
/**
* 获取醒目留言条数
*
* @return
*/
public int getMessageViewCount() {
return messageViews.size();
}
/**
* 移除指定醒目留言
*
* @param viewId 识别id
*/
private void removeEyesCatchingMessageView(int viewId) {
int position = findPostionByViewId(viewId);
if (position != -1) {
parentLayout.removeView(messageViews.get(position));
messageViews.remove(position);
messageItems.remove(position);
customConfigs.remove(position);
}
}
/**
* 写入配置对象中的数据
*
* @param builder
* @param config
*/
private void writeMessageConfig(EyesCatchingMessageView.EyesCatchingMessageViewBuilder builder, EyesCatchingMessageConfig config) {
if (config == null) {
return;
}
if (config.widthMode != null) {
builder.setWidthMode(config.widthMode);
}
if (config.width != null) {
builder.setWidth(config.width);
}
if (config.height != null) {
builder.setHeight(config.height);
}
if (config.portraitRadius != null) {
builder.setPortraitRadius(config.portraitRadius);
}
if (config.backgroundViewColor != null) {
builder.setBackgroundViewColor(config.backgroundViewColor);
}
if (config.timingViewColor != null) {
builder.setTimingViewColor(config.timingViewColor);
}
if (config.messageTextSize != null) {
builder.setMessageTextSize(config.messageTextSize);
}
if (config.messageTextColor != null) {
builder.setMessageTextColor(config.messageTextColor);
}
if (config.messageLengthLimit != null) {
builder.setMessageLengthLimit(config.messageLengthLimit);
}
if (config.messageLeftMargin != null) {
builder.setMessageLeftMargin(config.messageLeftMargin);
}
if (config.messageRightMargin != null) {
builder.setMessageRightMargin(config.messageRightMargin);
}
if (config.showMessageText != null) {
builder.setShowMessageText(config.showMessageText);
}
}
/**
* 根据viewId查找醒目留言位置
*
* @param viewId
* @return
*/
private int findPostionByViewId(int viewId) {
for (int i = 0; i < messageItems.size(); i++) {
if (messageItems.get(i).getViewId() == viewId) {
return i;
}
}
return -1;
}
/**
* 这只配置对象
*
* @param config
*/
public void setConfig(EyesCatchingMessageConfig config) {
this.config = config;
}
/**
* 设置醒目留言上下左右间距,单位dp
*
* @param paddingLeft 第一个醒目留言向左间距
* @param paddingTop 醒目留言向上间距
* @param paddingRight 最后一个醒目留言向右间距
* @param paddingBottom 醒目留言向下间距
*/
public void setMessagePadding(int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) {
this.paddingLeft = dp2px(paddingLeft);
this.paddingTop = dp2px(paddingTop);
this.paddingRight = dp2px(paddingRight);
this.paddingBottom = dp2px(paddingBottom);
}
/**
* 设置醒目留言间距
*
* @param messageMargin 间距,单位dp
*/
public void setMessageMargin(int messageMargin) {
this.messageMargin = dp2px(messageMargin);
}
/**
* 设置排序比较器
*
* @param sortComparator
*/
public void setSortComparator(SortComparator sortComparator) {
if (sortComparator == null) {
return;
}
this.sortComparator = sortComparator;
}
/**
* 设置点击监听接口
*
* @param clickListener
*/
public void setClickListener(OnMessageClickListener clickListener) {
this.clickListener = clickListener;
}
/**
* 设置头像加载接口
*
* 注:此方法需在调用addMessageView方法添加醒目留言之前调用,否则会导致头像无法加载
*
* @param portraitLoader
*/
public void setPortraitLoader(PortraitLoader portraitLoader) {
this.portraitLoader = portraitLoader;
}
/**
* 设置计时监听接口
*
* @param timingListener
*/
public void setTimingListener(OnTimingListenerAdapter timingListener) {
this.timingListener = timingListener;
}
/**
* 排序比较器
*
* 创建醒目留言时会根据该接口进行排序
*/
public interface SortComparator {
/**
* 比较方法
* 返回true则item1排在前面
* 返回false则item2排在前面
*
* @param item1
* @param item2
* @return
*/
boolean onCompare(MessageItem item1, MessageItem item2);
}
/**
* 点击监听接口
*/
public interface OnMessageClickListener {
/**
* 点击回调方法
*
* @param position 醒目留言位置
* @param percentage 剩余计时进度百分比
* @param messageItem 数据对象
*/
void onMessageClick(int position, float percentage, MessageItem messageItem);
}
/**
* 头像加载接口
*/
public interface PortraitLoader {
/**
* 头像加载回调
*
* @param position 醒目留言位置
* @param messageItem 数据对象
* @param portraitImageView 头像
*/
void onLoad(int position, MessageItem messageItem, ImageView portraitImageView);
}
/**
* 计时监听接口
*/
private interface OnTimingListener {
/**
* 计时开始时回调
*
* @param position 醒目留言位置
* @param messageItem 数据对象
*/
void onStart(int position, MessageItem messageItem);
/**
* 计时过程中回调
*
* @param position 醒目留言位置
* @param messageItem 数据对象
* @param currentTime 当前计时时间
* @param percentage 计时剩余进度百分比
*/
void onTiming(int position, MessageItem messageItem, float currentTime, float percentage);
/**
* 计时结束时回调
*
* @param position 醒目留言位置
* @param messageItem 数据对象
*/
void onFinish(int position, MessageItem messageItem);
/**
* 计时取消时回调
*
* @param position 醒目留言位置
* @param messageItem 数据对象
*/
void onCancel(int position, MessageItem messageItem);
}
/**
* 计时监听接口适配器,作用是让调用者自助选择需要监听的回调方法
*/
public static class OnTimingListenerAdapter implements OnTimingListener {
@Override
public void onStart(int position, MessageItem messageItem) {
}
@Override
public void onTiming(int position, MessageItem messageItem, float currentTime, float percentage) {
}
@Override
public void onFinish(int position, MessageItem messageItem) {
}
@Override
public void onCancel(int position, MessageItem messageItem) {
}
}
private int dp2px(int dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, getResources().getDisplayMetrics());
}
/**
* 数据对象,用于封装醒目留言相关数据
*/
public static class MessageItem {
// 识别id
private int viewId;
// 留言内容
private String message;
// 计时总时间
private float totalTime;
// 计时起始时间,如totalTime=100s,initialTime=60s,则表示从剩余进度60%处开始计时
private float initialTime;
// 自定义存储对象,调用者可根据需要自行存储相关数据
private Object extra;
public MessageItem(int viewId, String message, float totalTime) {
this(viewId, message, totalTime, totalTime, null);
}
public MessageItem(int viewId, String message, float totalTime, Object extra) {
this(viewId, message, totalTime, totalTime, extra);
}
public MessageItem(int viewId, String message, float totalTime, float initialTime) {
this(viewId, message, totalTime, initialTime, null);
}
public MessageItem(int viewId, String message, float totalTime, float initialTime, Object extra) {
this.viewId = viewId;
this.message = message;
this.totalTime = totalTime;
this.initialTime = initialTime;
this.extra = extra;
}
public int getViewId() {
return viewId;
}
public String getMessage() {
return message;
}
public float getTotalTime() {
return totalTime;
}
public float getInitialTime() {
return initialTime;
}
public Object getExtra() {
return extra;
}
void setInitialTime(float initialTime) {
this.initialTime = initialTime;
}
}
/**
* 醒目留言可设置参数配置对象,用于封装所有醒目留言统一的可设置参数
* 其中各个参数的意义具体参见EyesCatchingMessageView中的说明
*/
public static class EyesCatchingMessageConfig {
private Integer widthMode;
private Integer width;
private Integer height;
private Integer portraitRadius;
private Integer backgroundViewColor;
private Integer timingViewColor;
private Integer messageTextSize;
private Integer messageTextColor;
private Integer messageLeftMargin;
private Integer messageRightMargin;
private Integer messageLengthLimit;
private Boolean showMessageText;
public void setWidthMode(Integer widthMode) {
this.widthMode = widthMode;
}
public void setWidth(Integer width) {
this.width = width;
}
public void setHeight(Integer height) {
this.height = height;
}
public void setPortraitRadius(Integer portraitRadius) {
this.portraitRadius = portraitRadius;
}
public void setBackgroundViewColor(Integer backgroundViewColor) {
this.backgroundViewColor = backgroundViewColor;
}
public void setTimingViewColor(Integer timingViewColor) {
this.timingViewColor = timingViewColor;
}
public void setMessageTextSize(Integer messageTextSize) {
this.messageTextSize = messageTextSize;
}
public void setMessageTextColor(Integer messageTextColor) {
this.messageTextColor = messageTextColor;
}
public void setMessageLengthLimit(Integer messageLengthLimit) {
this.messageLengthLimit = messageLengthLimit;
}
public void setMessageLeftMargin(Integer messageLeftMargin) {
this.messageLeftMargin = messageLeftMargin;
}
public void setMessageRightMargin(Integer messageRightMargin) {
this.messageRightMargin = messageRightMargin;
}
public void setShowMessageText(Boolean showMessageText) {
this.showMessageText = showMessageText;
}
/**
* 配置对象建造者,用于让调用者方便地创建一个配置对象实例
*/
public static class EyesCatchingMessageConfigBuilder {
private EyesCatchingMessageConfig config;
public EyesCatchingMessageConfigBuilder() {
config = new EyesCatchingMessageConfig();
}
public EyesCatchingMessageConfigBuilder setWidthMode(Integer widthMode) {
config.setWidthMode(widthMode);
return this;
}
public EyesCatchingMessageConfigBuilder setWidth(Integer width) {
config.setWidth(width);
return this;
}
public EyesCatchingMessageConfigBuilder setHeight(Integer height) {
config.setHeight(height);
return this;
}
public EyesCatchingMessageConfigBuilder setPortraitRadius(Integer portraitRadius) {
config.setPortraitRadius(portraitRadius);
return this;
}
public EyesCatchingMessageConfigBuilder setBackgroundViewColor(Integer backgroundViewColor) {
config.setBackgroundViewColor(backgroundViewColor);
return this;
}
public EyesCatchingMessageConfigBuilder setTimingViewColor(Integer timingViewColor) {
config.setTimingViewColor(timingViewColor);
return this;
}
public EyesCatchingMessageConfigBuilder setMessageTextSize(Integer messageTextSize) {
config.setMessageTextSize(messageTextSize);
return this;
}
public EyesCatchingMessageConfigBuilder setMessageTextColor(Integer messageTextColor) {
config.setMessageTextColor(messageTextColor);
return this;
}
public EyesCatchingMessageConfigBuilder setMessageLengthLimit(Integer messageLengthLimit) {
config.setMessageLengthLimit(messageLengthLimit);
return this;
}
public EyesCatchingMessageConfigBuilder setMessageLeftMargin(Integer messageLeftMargin) {
config.setMessageLeftMargin(messageLeftMargin);
return this;
}
public EyesCatchingMessageConfigBuilder setMessageRightMargin(Integer messageRightMargin) {
config.setMessageRightMargin(messageRightMargin);
return this;
}
public EyesCatchingMessageConfigBuilder setShowMessageText(Boolean showMessageText) {
config.setShowMessageText(showMessageText);
return this;
}
/**
* 返回创建好的配置对象
*
* @return
*/
public EyesCatchingMessageConfig create() {
return config;
}
}
}
}
EyesCatchingMessageLayout继承自LinearLayout,是一个布局存放所有的醒目留言EyesCatchingMessageView对象,并提供了一系列方法实现对这些对象的增加、删除、管理等等功能。
又废话了那么多不如实际演练一下。先在布局里加上这个控件:
然后在Activity里写点简单的交互逻辑:
private EyesCatchingMessageLayout messageLayout;
private EditText moneyEditText, totalTimeEditText, initialTimeEditText;
private Button sendButton;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intiView();
initMessageLayout();
}
private void intiView() {
messageLayout = findViewById(R.id.message_layout);
moneyEditText = findViewById(R.id.money_edittext);
totalTimeEditText = findViewById(R.id.totaltime_edittext);
initialTimeEditText = findViewById(R.id.initialtime_edittext);
sendButton = findViewById(R.id.send_button);
sendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int money;
int totalTime;
Integer initialTime = null;
if (!moneyEditText.getText().toString().isEmpty()
&& !totalTimeEditText.getText().toString().isEmpty()) {
money = Integer.parseInt(moneyEditText.getText().toString());
totalTime = Integer.parseInt(totalTimeEditText.getText().toString());
if (!initialTimeEditText.getText().toString().isEmpty()) {
initialTime = Integer.parseInt(initialTimeEditText.getText().toString());
}
messageLayout.addMessageView(new EyesCatchingMessageLayout.MessageItem(
messageLayout.getMessageViewCount(),
"¥" + money,
totalTime,
initialTime == null ? totalTime : initialTime.intValue(),
money
));
moneyEditText.setText("");
totalTimeEditText.setText("");
initialTimeEditText.setText("");
} else {
Toast.makeText(MainActivity.this, "INVALID INPUT!", Toast.LENGTH_SHORT).show();
}
}
});
}
private void initMessageLayout() {
messageLayout.setPortraitLoader(new EyesCatchingMessageLayout.PortraitLoader() {
@Override
public void onLoad(int position, EyesCatchingMessageLayout.MessageItem messageItem, ImageView portraitImageView) {
portraitImageView.setImageResource(R.drawable.huaji);
}
});
messageLayout.setTimingListener(new EyesCatchingMessageLayout.OnTimingListenerAdapter() {
@Override
public void onFinish(int position, EyesCatchingMessageLayout.MessageItem messageItem) {
Toast.makeText(MainActivity.this, "第" + position + "条醒目留言:" + messageItem.getMessage() + " 显示时间结束了!", Toast.LENGTH_SHORT).show();
}
});
messageLayout.setClickListener(new EyesCatchingMessageLayout.OnMessageClickListener() {
@Override
public void onMessageClick(int position, float percentage, EyesCatchingMessageLayout.MessageItem messageItem) {
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
if (messageLayout != null) {
messageLayout.cancelAllTiming();
}
}
EyesCatchingMessageLayout提供的各种操作醒目留言对象的方法,源码注释都写得比较清楚了,这里就提几个比较重要的:
addMessageView():添加一个醒目留言
addMessageViews():批量添加一组醒目留言
cancelTimingByViewId():取消指定的醒目留言的计时
cancelAllTiming():取消所有的醒目留言的计时,一般在页面退出时执行此方法。
setPortraitLoader():设置头像加载器,用法同EyesCatchingMessageView,注意此方法需在调用addMessageView方法添加醒目留言之前调用,否则会导致头像无法加载
setClickListener():设置醒目留言的点击事件。
好了终于可以看一下效果了,运行一下:
EyesCatchingMessageLayout同样支持对醒目留言的各种参数设定,通过参数配置类EyesCatchingMessageConfig实现,可以通过内部建造器EyesCatchingMessageConfigBuilder快速创建一个Config对象,添加几行代码:
private void initMessageLayout() {
EyesCatchingMessageLayout.EyesCatchingMessageConfig config = new EyesCatchingMessageLayout.EyesCatchingMessageConfig.EyesCatchingMessageConfigBuilder()
.setHeight(80)
.setPortraitRadius(30)
.setBackgroundViewColor(Color.parseColor("#70FFD700"))
.setTimingViewColor(Color.parseColor("#FFD700"))
.setMessageTextSize(20)
.setMessageTextColor(Color.BLACK)
.setMessageLengthLimit(8)
.setMessageLeftMargin(10)
.setMessageRightMargin(10)
.create();
messageLayout.setConfig(config);
messageLayout.setMessagePadding(30, 30, 30, 30);
messageLayout.setMessageMargin(20);
......
}
用法很简单,就是new一个EyesCatchingMessageConfigBuilder对象,然后set各种参数,然后调用create方法创建一个Config对象,最后调用EyesCatchingMessageLayout的setConfig方法完成参数配置。EyesCatchingMessageConfigBuilder下各种set方法的意义和EyesCatchingMessageView中的一样,大家看注释就好。
看看效果:
控件支持对醒目留言排序的功能。通过排序比较器接口SortComparator实现,其默认实现为越新的醒目留言排序越靠前。可以通过setSortComparator方法自定义排序规则。比如说,现在的规则是,金额越大的显示越靠前,金额相同越新的显示越靠前,就可以这样写:
private void initMessageLayout() {
......
messageLayout.setSortComparator(new EyesCatchingMessageLayout.SortComparator() {
@Override
public boolean onCompare(EyesCatchingMessageLayout.MessageItem item1, EyesCatchingMessageLayout.MessageItem item2) {
int money1 = (int) item1.getExtra();
int money2 = (int) item2.getExtra();
return money1 >= money2;
}
});
......
}
看看效果:
好了,到此为止所有的内容都介绍完了,最后来总结一下:
● EyesCatchingMessageView实现了醒目留言的基本功能,可以通过EyesCatchingMessageViewBuilder来快速构建。
● EyesCatchingMessageLayout实现了对EyesCatchingMessageView的集成管理,包括增加、删除、排序、过程监听等等。
● 通过配置类EyesCatchingMessageConfig完成对醒目留言的统一参数配置,
EyesCatchingMessageConfig可通过建造器EyesCatchingMessageConfigBuilder来快速构造。
最后的最后,附上源码地址:https://download.csdn.net/download/Sure_Min/12579376
这次的内容就到这里,我们下次再见。