前段时间b站直播新出了个功能,叫醒目留言。app端的显示效果大概像这个样子:
看了一下觉得挺有意思,于是自己写了一个控件实现这种效果。话不多说先看效果:
演示完效果之后正片开始。核心组件就一个:EyesCatchingMessageView。上代码:
public class EyesCatchingMessageView extends RelativeLayout {
/**
* 宽度模式,
* <p>
* 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 timing