实践背景
在即时通讯类应用里,很常见各种气泡布局包裹消息,通常我们采用.9图实现。但是使用气泡图片面临着间距不可控,如果是图片消息,此方法就无法实现气泡。本文将介绍如何更加用优雅的方式去实现自定义气泡布局。
PS前置知识: 如何自定义view、XFermode混合图层、path概念以及贝赛尔曲线。
惯例,我们先看下最终要实现的效果图,如下图,总共有5种类型,基本满足日常需要,可以根据需要再进行扩展。
自定义气泡View思路分析
1.图形基本分析
以上四种常见气泡,从外形上看是圆角带犄角,文字内容在气泡的矩形内,图片被裁剪部分。 图片类型的气泡上,犄角部分是带有图片的一部分,而且在图片的左右下角有一个提示类型的图片(特殊UI需要),上图中未体现该效果。文字类型气泡特殊一些,在单个字的时候,文字是居中的,然后左右内间距和多文字下的间距不一样(UI要求),但是从整体上也符合气泡的通用裁剪规则。容器类型气泡,内部子view可随意布置,但是最终显示区域只有气泡部分,这样可扩展度搞。
2.实现思路分析
那么我们如何去形成这种View呢?最开始我接触到的代码是用drawable加载.9图的方式,但是新UI效果图片类型气泡就无法下手了。可供采用的方案有2种,canvas的clipPath和XFermode图层混合。
PS:目前任何布局类型都是四边形的,而那些各种形状的布局,其实只是四边形布局只显示其中部分区域而已。
- clipPath: 通过对canvas的裁剪形成气泡布局,先用描绘出一个气泡模样的path,然后按照这个path把画布裁剪,然后再这个画布上绘制内容。具体操作下面会介绍。
- XFermode图层混合:XFermode可以通过多个图层进行叠加按照一定规则保留部分图形区域。利用这个特性,我们先绘制原始图形,然后在这个执行之后,依然先描绘出气泡path,我们可以给paint画笔设置PorterDuffXfermode,然后用画笔带上DST_IN模式进行图层叠加。
具体代码实践分析
1.简单介绍下本文所需的自定义View知识
首先我们要知道自定义View需要做哪些代码准备,一般来说,onDraw是必然需要的。根据需要onMeasure,onSizeChange,onLayout,dispatchDraw等方法有时候也需要。本文是实现一个气泡view,有单一显示视图的自定义view以及容器类型的气泡。所以大家需要了解onDraw、onSizeChange、dispatchDraw等需要重写的方法。此外,了解invalidate和postInvalidate等刷新View视图方法。
onDraw
这个方法是核心,主要是用来描绘出你展示的视图界面。比如你想画一个花,那么这个地方进行最终的描绘工作。在部分情况下,这里会在继承某个View情况下,增加一些绘制,此时会继续调用super.onDraw(canvas); ,这样保留父view的图形。
onSizeChange
在进行ondraw之前会有若干次调用onSizeChange,这里可以用来提前获取当前view的最新高宽,比如下面代码。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mHeight = h;
mWidth = w;
}
这个方法在本次自定义view中主要是给容器布局使用,调用顺序在ondraw之后,重写这个用来在分发绘制内部子view时,绘制需要的背景色以及进行图层裁剪形成气泡。
2.ClipPath方式实现策略
以文本气泡View为例,我们先思考气泡的图形path如何形成。首先我们先继承TextView,因为我们要在这个基础上实现文字气泡。图形大致上拆分为一个圆角矩形,然后再左侧或者右侧画一个犄角。犄角是带有一定弧度的,这个和UI沟通过,我当初是通过px一点点调整最终给到UI满意的弧度。圆角矩形的绘制就不多说了,看API。犄角,是通过2条二阶贝塞尔曲线形成的,上面一条,下面一条(关于path的介绍很多,看上面的预学习链接,或者自行搜索)。
protected void onDraw(Canvas canvas) {
canvas.setDrawFilter(mPaintFlagsDrawFilter);
// LogUtil.i(TAG, getText() + " getPaddingLeft" + getPaddingLeft() + " getPaddingRight" + getPaddingRight());
mSrcPath.reset();
if (mIsRightPop) {
mRoundRect.set(0, 0, mWidth - mWidthDiff, mHeight);
mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW);
//给path增加右侧的犄角,形成气泡效果
mSrcPath.moveTo(mWidth - mWidthDiff, mRoundRadius);
mSrcPath.quadTo(mTopControl.x, mTopControl.y, mWidth, mRoundRadius - mDefaultCornerPadding);
mSrcPath.quadTo(mBottomControl.x, mBottomControl.y, mWidth - mWidthDiff,
mRoundRadius + mWidthDiff);
} else {
mRoundRect.set(mWidthDiff, 0, mWidth, mHeight);
mSrcPath.addRoundRect(mRoundRect, mRoundRadius, mRoundRadius, Path.Direction.CW);
//给path增加右侧的犄角,形成气泡效果
mSrcPath.moveTo(mWidthDiff, mRoundRadius);
mSrcPath.quadTo(mTopControl.x, mTopControl.y, 0, mRoundRadius - mDefaultCornerPadding);
mSrcPath.quadTo(mBottomControl.x, mBottomControl.y, mWidthDiff, mRoundRadius + mWidthDiff);
}