『自定义View实战』—— 客服好评View

在工作中难免遇到自定义 View 的相关需求,本身这方面比较薄弱,因此做个记录,也是自己学习和成长的积累。自定义View实战.

前言

这个版本主要的任务就是完成环信客服系统的集成,上一篇文章 仿IOS下载View 也是这个版本开发需求中的一小部分,那今天介绍一下另一个小需求 客服好评客服好评 的功能在于用户对客服服务态度和质量的评价,也是作为考核客服服务的标准。相关代码已上传 EvaluationCardView

看一下预览效果:

预览

需求简要说明

  1. 默认状态为0星,不可提交
  2. 星星数量小于等于3,展示差评理由
  3. 差评理由云控,数量可变
  4. 差评理由可不选,可多选

我将分为3部分进行介绍。

介绍
  1. 评级的 RatingBar
  2. 差评理由 TagView
  3. 整体评价的 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);
}

最后的效果:

EvaluationRatingBar

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);
    }
}

来看一下最终的效果:

reasonsLayout

EvaluationCardView

这个就简单了,配合着 AlertDialog 弹窗显示,将之前介绍的 EvaluationRatingBarEvaluationNegReasonsLayout 结合在一块,并根据自己特殊的产品需求来定制对应的效果。最后在点击提交的时候通过接口回调的方式,将最终的结果回调出来并处理。

public void setOnEvaluationCallback(OnEvaluationCallback callback) {
    this.mCallback = callback;
}

public interface OnEvaluationCallback {
    void onEvaluationCommitClick(int starCount, Set<String> reasons);
}

starCount: 即为评级的等级。reasons:即为选择的差评理由

最终调用

EvaluationCardView cardView = new EvaluationCardView(this);
List<String> 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<String> 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

原文地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容