我尽量不打错别字,用词准确,不造成阅读障碍。
本文是自定义聊天时的图片和视频显示及发送控件。
效果:
代码:
自定义属性
<declare-styleable name="MyIMImageView">
<attr name="arrowTop" format="dimension|reference" />
<attr name="arrowWidth" format="dimension|reference" />
<attr name="arrowHeight" format="dimension|reference" />
<attr name="circular" format="dimension|reference" />
<attr name="direction" format="string" />
<attr name="showText" format="boolean" />
<attr name="showShadow" format="boolean" />
</declare-styleable>
View代码
public class MyIMImageView extends android.support.v7.widget.AppCompatImageView {
private float mAngleLength = dp2px(20); //圆弧半径
private float mArrowTop = dp2px(40); //箭头距离顶部的位置,有的需要箭头在正中间
private float mArrowWidth = dp2px(20); //箭头宽度
private float mArrowHeight = dp2px(20); //箭头高度
private Paint mBitmapPaint; //画Bitmap画笔
private Paint mTextPaint; //写进度的Bitmap
private String direction; //方向
private Bitmap mBitmap; //资源图片
private BitmapShader mBitmapShader; //bitmap着色器
private RectF mBitmapRectF;
private RectF mShadowRectF = new RectF();
private Rect mTextRect = new Rect();
private Path mPath;
private boolean isShowText; //是否显示文字
private boolean isShowShadow; //是否显示阴影
private int mProgressPercent = 69;
public MyIMImageView(Context context) {
this(context, null);
}
public MyIMImageView(Context context, AttributeSet attrs) {
super(context, attrs);
initAttr(attrs);
}
private void initAttr(AttributeSet attrs) {
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.MyIMImageView);
mArrowTop = typedArray.getDimension(R.styleable.MyIMImageView_arrowTop, mArrowTop);
mArrowWidth = typedArray.getDimension(R.styleable.MyIMImageView_arrowWidth, mArrowWidth);
mArrowHeight = typedArray.getDimension(R.styleable.MyIMImageView_arrowHeight, mArrowHeight);
mAngleLength = typedArray.getDimension(R.styleable.MyIMImageView_circular, mAngleLength);
direction = typedArray.getString(R.styleable.MyIMImageView_direction);
isShowText = typedArray.getBoolean(R.styleable.MyIMImageView_showText, false);
isShowShadow = typedArray.getBoolean(R.styleable.MyIMImageView_showShadow, false);
typedArray.recycle();
}
@Override
protected void onDraw(Canvas canvas) {
if (mBitmapPaint != null) {
canvas.drawPath(mPath, mBitmapPaint);
drawShadowAndProgress(canvas, mBitmapRectF); //画阴影和进度
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mBitmapRectF = new RectF(getPaddingLeft(), getPaddingTop(), getRight()
- getLeft() - getPaddingRight(), getBottom() - getTop()
- getPaddingBottom());
mPath = new Path();
setBitmap();
if ("right".equals(direction)) {
rightPath(mBitmapRectF, mPath);
} else if ("left".equals(direction)) {
leftPath(mBitmapRectF, mPath);
}
}
@Override
public void setImageBitmap(Bitmap bm) {
super.setImageBitmap(bm);
mBitmap = bm;
}
@Override
public void setImageDrawable(@Nullable Drawable drawable) {
super.setImageDrawable(drawable);
mBitmap = getBitmapFromDrawable(drawable);
}
@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
mBitmap = getBitmapFromDrawable(getDrawable());
}
//三角形在有右边
private void rightPath(RectF rectF, Path path) {
path.moveTo(rectF.left + mAngleLength, rectF.top); //path移动到上边直线左点
path.lineTo(rectF.right - mAngleLength - mArrowWidth, rectF.top); //划上边线
//画右上角弧线
path.arcTo(new RectF(rectF.right - mAngleLength - mArrowWidth, rectF.top, rectF.right - mArrowWidth, mAngleLength + rectF.top), 270, 90);
path.lineTo(rectF.right - mArrowWidth, rectF.top + mArrowTop);//画弧线和三角之间的直线
path.lineTo(rectF.right, rectF.top + mArrowTop + mArrowHeight / 2); //画三角的上边线
path.lineTo(rectF.right - mArrowWidth, rectF.top + mArrowTop + mArrowHeight); //画三角的下边线
path.lineTo(rectF.right - mArrowWidth, rectF.bottom - mAngleLength);//三角下边的线
//画右下角弧线
path.arcTo(new RectF(rectF.right - mAngleLength - mArrowWidth, rectF.bottom - mAngleLength, rectF.right - mArrowWidth, rectF.bottom), 0, 90);
path.lineTo(rectF.left - mAngleLength, rectF.bottom); //画底边线
//画左下角弧线
path.arcTo(new RectF(rectF.left, rectF.bottom - mAngleLength, mAngleLength + rectF.left, rectF.bottom), 90, 90);
path.lineTo(rectF.left, rectF.top - mAngleLength); //画左边线
//画左上角弧线
path.arcTo(new RectF(rectF.left, rectF.top, mAngleLength + rectF.left, mAngleLength + rectF.top), 180, 90);
path.close();
}
//三角形在左边
private void leftPath(RectF rectF, Path path) {
path.moveTo(rectF.left + mAngleLength + mArrowWidth, rectF.top);//path移动到上边直线左点
path.lineTo(rectF.right - mAngleLength, rectF.top); //划上边线
//画右上角弧线
path.arcTo(new RectF(rectF.right - mAngleLength, rectF.top, rectF.right, mAngleLength + rectF.top), 270, 90);
path.lineTo(rectF.right, rectF.bottom - mAngleLength);
//画右下角弧线
path.arcTo(new RectF(rectF.right - mAngleLength, rectF.bottom - mAngleLength, rectF.right, rectF.bottom), 0, 90);
path.lineTo(rectF.left - mAngleLength - mArrowWidth, rectF.bottom); //画底边线
//画左下角弧线
path.arcTo(new RectF(rectF.left + mArrowWidth, rectF.bottom - mAngleLength, mAngleLength + rectF.left + mArrowWidth, rectF.bottom), 90, 90);
path.lineTo(rectF.left + mArrowWidth, rectF.top + mArrowTop + mArrowHeight); //三角下边的线
path.lineTo(rectF.left, rectF.top + mArrowTop + mArrowHeight / 2); //画三角的下边线
path.lineTo(rectF.left + mArrowWidth, rectF.top + mArrowTop); //画三角的上边线
path.lineTo(rectF.left + mArrowWidth, rectF.top + mArrowTop); //三角上边的线
//画左上角弧线
path.arcTo(new RectF(rectF.left + mArrowWidth, rectF.top, mAngleLength + rectF.left + mArrowWidth, mAngleLength + rectF.top), 180, 90);
path.close();
}
private void drawShadowAndProgress(Canvas canvas, RectF rectF) {
if (isShowShadow) {
mTextPaint.setAntiAlias(true);
mTextPaint.setStyle(Paint.Style.FILL);
mTextPaint.setColor(Color.parseColor("#70000000"));
if ("right".equals(direction)) {
mShadowRectF.set(rectF.left, rectF.top, rectF.right - mArrowWidth, rectF.bottom);
} else {
mShadowRectF.set(rectF.left + mArrowWidth, rectF.top, rectF.right, rectF.bottom);
}
canvas.drawRoundRect(mShadowRectF, mAngleLength / 2, mAngleLength / 2, mTextPaint);
}
if (isShowText) {
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(getResources().getDimension(R.dimen.shibadp));
mTextPaint.getTextBounds(mProgressPercent + "%", 0, (mProgressPercent + "%").length(), mTextRect);
if ("right".equals(direction)) {
canvas.drawText(mProgressPercent + "%", (rectF.right - mArrowWidth - mTextRect.width()) / 2, rectF.bottom / 2, mTextPaint);
} else {
canvas.drawText(mProgressPercent + "%", (rectF.left + rectF.right - mTextRect.width()) / 2, rectF.bottom / 2, mTextPaint);
}
}
}
//从Drawable中获取bitmap
private Bitmap getBitmapFromDrawable(Drawable drawable) {
if (drawable == null) {
return null;
}
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
Bitmap bitmap;
if (drawable instanceof ColorDrawable) {
bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
} else {
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
private void setBitmap() {
mTextPaint = new Paint();
//设置着色器为拉伸,图片很小的时候可以用
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mBitmapPaint = new Paint();
mBitmapPaint.setAntiAlias(true);
mBitmapPaint.setShader(mBitmapShader);
}
private int dp2px(int dp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getContext().getResources().getDisplayMetrics());
}
public void setProgressPercent(int percent) {
this.mProgressPercent = percent;
postInvalidate();
}
public void setIsShowText(boolean isShowText) {
this.isShowText = isShowText;
}
public void setIsShowShadow(boolean isShowShadow) {
this.isShowShadow = isShowShadow;
}
}
原理很简单的,就是设置图片后先获取Bitmap,然后设置shader和paint,然后画图形的path,最后在onDraw方法里,根据paint和path来画图。
Bug
但是这时候有bug,就是图片大小和矩形大小不一致,图片需要处理,基本99%的图片都需要处理,比如图片太大,而我们的控件小,那么控件只能显示图片中,左上角位置控件大小的部分,其他部分放不下,就这样:
红色矩形代表图片,黑色部分为控件,虚线部分都看不见。解决方法也很简单,就是缩放,这里我们一般使用矩阵进行缩放操作,写在setBitmap()里。
代码如下:
private void setBitmap() {
mTextPaint = new Paint();
//设置着色器为拉伸
mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
mBitmapPaint = new Paint();
mBitmapPaint.setAntiAlias(true);
mBitmapPaint.setShader(mBitmapShader);
mBitmapHeight = mBitmap.getHeight();
mBitmapWidth = mBitmap.getWidth();
setMatrix();
}
setMatrix()方法:
private void setMatrix() {
float scaleX;
float scaleY;
Matrix mShaderMatrix = new Matrix(); //矩阵对象
mShaderMatrix.set(null); //设置成单位矩阵
scaleY = (mBitmapRectF.bottom - mBitmapRectF.top) / mBitmapHeight; //Y轴缩放比例
scaleX = (mBitmapRectF.right - mBitmapRectF.left) / mBitmapWidth; //X轴缩放比例
mShaderMatrix.setScale(scaleX, scaleY); //缩放
mBitmapShader.setLocalMatrix(mShaderMatrix);
}
到这里,理论是没问题了,但是实际是欠考虑了一个问题,如果你设置了padding属性,比如我设置了paddingStart和paddingTop,结果是这样的:
原因很简单,矩阵缩放是以左上角为原点进行缩放的,缩放完了以后还是以左上角为原点,所以与我们控件可显示区域对不上,解决也简单,移动一下就好了:
private void setMatrix() {
float scaleX;
float scaleY;
float dx;
float dy;
Matrix mShaderMatrix = new Matrix();
mShaderMatrix.set(null);
scaleY = (mBitmapRectF.bottom - mBitmapRectF.top) / mBitmapHeight;
dx = mBitmapRectF.left;
scaleX = (mBitmapRectF.right - mBitmapRectF.left) / mBitmapWidth;
dy = mBitmapRectF.top;
mShaderMatrix.setScale(scaleX, scaleY);
mShaderMatrix.postTranslate(dx, dy); //移动
mBitmapShader.setLocalMatrix(mShaderMatrix);
}
最后结果是这样:
完美!其实我感觉矩阵应该是可以设置缩放后的原点的,就是省去移动那步,但是我现在对矩阵还不是很了解,后期尝试着更改。同样的,在这个基础之上可以做视频发送的控件,将drawText()换成draw其他图形就好了。
集合了一些简单自定义View的github地址:
https://github.com/longlong-2l/MySelfViewDemo
很简单,没有太多高深的用法,适合学习入门。