在工作中难免遇到自定义
View
的相关需求,本身这方面比较薄弱,因此做个记录,也是自己学习和成长的积累。自定义View实战.
前言
这个版本主要的任务就是完成环信客服系统的集成,上一篇文章 仿IOS下载View 也是这个版本开发需求中的一小部分,那今天介绍一下另一个小需求 客服好评。 客服好评 的功能在于用户对客服服务态度和质量的评价,也是作为考核客服服务的标准。相关代码已上传 EvaluationCardView
看一下预览效果:
需求简要说明
- 默认状态为0星,不可提交
- 星星数量小于等于3,展示差评理由
- 差评理由云控,数量可变
- 差评理由可不选,可多选
我将分为3部分进行介绍。
- 评级的
RatingBar
- 差评理由
TagView
- 整体评价的
CardView
EvaluationRatingBar
介绍
Android
原生就有这个空间 RatingBar
,定制型不是很高,所以需要通过自定义来满足特定的产品需求。其实 RatingBar
的主要用处就在于 评级,基本就是对服务进行等级评价,来决定服务的质量如何。
需求分析
有需求才会有对应的实现,那么有哪些需要控制的属性呢。
属性名称 | 属性介绍 |
---|---|
mStarTotal | 评级的总数 |
mSelectedCount | 评级选中的数量 |
mStarResId | 星星的资源文件 |
mHeight | 星星的高度 |
mIntervalWidth | 星星之间间隔的宽度 |
mEditable | 是否可被点击 |
具体实现
既然星星有两种状态可供选择,那么单个 View
就使用 CheckBox
代替,首先初始化的时候,需要根据 mStarTotal
来控制添加多少个 CheckBox
,并根据 mHeight
高度和 mIntervalWidth
间隔来控制摆放的位置。
for (int i = 0; i < mStarTotal; i++) {
CheckBox cb = new CheckBox(getContext());
LayoutParams layoutParams;
if (mHeight == 0) {
layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
} else {
layoutParams = new LayoutParams((int) mHeight, (int) mHeight);
}
layoutParams.gravity = Gravity.CENTER_VERTICAL;
if (i != 0 && i != mStarTotal - 1) {
layoutParams.leftMargin = (int) mIntervalWidth;
layoutParams.rightMargin = (int) mIntervalWidth;
} else if (i == 0) {
layoutParams.rightMargin = (int) mIntervalWidth;
} else if (i == mStarTotal - 1) {
layoutParams.leftMargin = (int) mIntervalWidth;
}
addView(cb, layoutParams);
}
最后在父布局 LinearLayout
中添加 所有的 CheckBox
。
至于点击事件的回调,可以在每次点击的时候进行遍历,获取 CheckBox
的选中状态,并通过 callback
回调出来。
for (int i = 0; i < mStarTotal; i++) {
CheckBox cb = (CheckBox) getChildAt(i);
if (i <= position) {
cb.setChecked(true);
} else if (i > position) {
cb.setChecked(false);
}
}
if (mOnRatingChangeListener != null) {
mOnRatingChangeListener.onChange(mSelectedCount);
}
最后的效果:
EvaluationNegReasonsLayout
需求分析
当用户给出差评的时候,需要展示对应的差评理由选择。理由云控,数量可变,内容可变。可单选,可不选,可多选。
主要的难点和重点在于根据理由内容的长短进行展示,如果内容长则显示一条,如果内容短可以显示多条。
具体实现
我们都知道 View
的测量工作主要是在 onMeasure
里进行。 宽度计算,可以先测量出每个子 View
的宽度,每次叠加,如果超过父布局限制的宽度则换行。 高度计算,每次换行叠加高度,每一行的高度取子 View
高度的最大值。
//遍历每个子元素
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
View childView = getChildAt(i);
//测量每一个子view的宽和高
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
//获取到测量的宽和高
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
//因为子View可能设置margin,这里要加上margin的距离
MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();
int realChildWidth = childWidth + mlp.leftMargin + mlp.rightMargin;
int realChildHeight = childHeight + mlp.topMargin + mlp.bottomMargin;
//如果当前一行的宽度加上要加入的子view的宽度大于父容器给的宽度,就换行
if ((lineWidth + realChildWidth) > sizeWidth) {
//换行
resultWidth = Math.max(lineWidth, realChildWidth);
resultHeight += realChildHeight;
//换行了,lineWidth和lineHeight重新算
lineWidth = realChildWidth;
lineHeight = realChildHeight;
} else {
//不换行,直接相加
lineWidth += realChildWidth;
//每一行的高度取二者最大值
lineHeight = Math.max(lineHeight, realChildHeight);
}
//遍历到最后一个的时候,肯定走的是不换行
if (i == childCount - 1) {
resultWidth = Math.max(lineWidth, resultWidth);
resultHeight += lineHeight;
}
}
setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth : resultWidth,modeHeight == MeasureSpec.EXACTLY ? sizeHeight : resultHeight);
既然 宽高 计算完了,剩下就是子 View
的摆放了,自然是在在 onLayout()
中实现。摆放就比较简单了,同样需要遍历所有的子 View
,最终调用 layout(left, top, right, bottom)
方法进行位置的摆放。宽度不断叠加,当超过父布局的宽度,则将 left
置为 0,高度记上一行子 View
的最大高度,以此类推。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int realWidht = getWidth();
int childLeft = 0;
int childTop = 0;
//遍历子控件,记录每个子view的位置
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
View childView = getChildAt(i);
//跳过View.GONE的子View
if (childView.getVisibility() == View.GONE) {
continue;
}
//获取到测量的宽和高
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
//因为子View可能设置margin,这里要加上margin的距离
MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();
if (childLeft + mlp.leftMargin + childWidth + mlp.rightMargin > realWidht) {
//换行处理
childTop += (mlp.topMargin + childHeight + mlp.bottomMargin);
childLeft = 0;
}
//布局
int left = childLeft + mlp.leftMargin;
int top = childTop + mlp.topMargin;
int right = childLeft + mlp.leftMargin + childWidth;
int bottom = childTop + mlp.topMargin + childHeight;
childView.layout(left, top, right, bottom);
childLeft += (mlp.leftMargin + childWidth + mlp.rightMargin);
}
}
来看一下最终的效果:
EvaluationCardView
这个就简单了,配合着 AlertDialog
弹窗显示,将之前介绍的 EvaluationRatingBar
和 EvaluationNegReasonsLayout
结合在一块,并根据自己特殊的产品需求来定制对应的效果。最后在点击提交的时候通过接口回调的方式,将最终的结果回调出来并处理。
public void setOnEvaluationCallback(OnEvaluationCallback callback) {
this.mCallback = callback;
}
public interface OnEvaluationCallback {
void onEvaluationCommitClick(int starCount, Set reasons);
}
starCount: 即为评级的等级。reasons:即为选择的差评理由
最终调用
EvaluationCardView cardView = new EvaluationCardView(this);
List reasonsData = new ArrayList<>();
reasonsData.add("回复太慢");
reasonsData.add("对业务不了解");
reasonsData.add("服务态度差");
reasonsData.add("问题没有得到解决");
cardView.setReasonsData(reasonsData);
cardView.show();
cardView.setOnEvaluationCallback(new EvaluationCardView.OnEvaluationCallback() {
@Override
public void onEvaluationCommitClick(int starCount, Set reasons) {
StringBuilder sb = new StringBuilder();
for (String reason : reasons) {
sb.append("\n").append(reason);
}
Toasty.success(EvaluationCardViewActivity.this, "评价成功\n" + "星星数量:" + starCount + "\n差评理由:" + sb.toString(), Toast.LENGTH_LONG, true).show();
}
});
具体的实现代码请查看 EvaluationCardView。
感谢
FlowTag
原文地址