水印绘制控件
/**
* 创建者:Tomcat0916
* 创建时间:2020/11/25
* 功能描述:水印控件
* 自动行列
* 示例:
* <pre>
* <com.*.WatermarkView
* android:id="@+id/tv_text_hint"
* android:layout_width="match_parent"
* android:layout_height="match_parent"
* android:alpha="0.5"
* android:rotation="-30"
* android:textColor="@color/color_gray_4"
* android:textSize="12sp"
* android:textStyle="bold"/>
* </pre>
*
* @see R.styleable#WatermarkView_column 列 默认: 自动计算 = 宽/字符长度
* @see R.styleable#WatermarkView_row 行 默认 : 自动计算 = 高/字符串高度
* @see R.styleable#WatermarkView_decorationX 最小列间距 固定列 默认1dp 否则 默认 50dp
* @see R.styleable#WatermarkView_decorationY 最小行间距 固定行 默认1dp 否则 默认 50dp
*/
public class WatermarkView extends AppCompatTextView {
private static final int DEF_DECORATION_Y = 50, DEF_DECORATION_X = 50;
private StaticLayout textLayout;
private CharSequence lastText = "";
private int row = -1, column = -1, realRow = -1, realColumn = -1, columnWith, rowHeight, width, height;
private boolean isCreated, isFixRows, isFixColumns, isVertical, isTextSizeChanged = false;
private float decorationX = -1,//横向间离
decorationY = -1, //纵向间离,
spacingX = -1,//横向间离
spacingY = -1, //纵向间离
offsetX, //偶数行横向偏移量
correctY = 0, correctX = 0,//因canvas默认旋转原点为左上角,所以旋转后需矫正偏移
rotation, //旋转角度
maxTextWidth,
textWidth;
@Override
protected void onDetachedFromWindow() {
textLayout = null;
super.onDetachedFromWindow();
}
public WatermarkView(Context context) {
this(context, null);
}
public WatermarkView(Context context, AttributeSet attrs) {
this(context, attrs, android.R.attr.textViewStyle);
}
public WatermarkView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.WatermarkView);
setColumn(array.getInt(R.styleable.WatermarkView_column, column));
setRow(array.getInt(R.styleable.WatermarkView_row, row));
decorationX = getDecorationFromArray(array, R.styleable.WatermarkView_decorationX, isFixColumns, DEF_DECORATION_X);
decorationY = getDecorationFromArray(array, R.styleable.WatermarkView_decorationY, isFixRows, DEF_DECORATION_Y);
offsetX = array.getDimension(R.styleable.WatermarkView_offsetX, -1);
array.recycle();
isCreated = true;
}
private float getDecorationFromArray(TypedArray array, @StyleableRes int id, boolean isFix, int def) {
return array.hasValue(id) ? array.getDimension(id, 0) : BaseUtils.dp2px(isFix ? 1 : def);
}
@Override
public void setRotation(float rotation) {
this.rotation = rotation % 360;//-359~359
if (rotation > 0) {
this.rotation -= 360;//-359~0
}
float abs = Math.abs(rotation % 180);
boolean b = (abs > 85 && abs < 95);
if (isVertical != b) {
isVertical = b;
isTextSizeChanged = true;
}
postInvalidateImpl();
}
public void setDecoration(@FloatRange(from = 0) float decorationX, @FloatRange(from = 0) float decorationY) {
setDecorationX(decorationX);
setDecorationY(decorationY);
}
private void postInvalidateImpl() {
if (isCreated) {
postInvalidate();
}
}
public float getDecorationX() {
return decorationX;
}
public void setDecorationX(@FloatRange(from = 0) float decorationX) {
this.decorationX = decorationX;
postInvalidateImpl();
}
public float getDecorationY() {
return decorationY;
}
public void setDecorationY(@FloatRange(from = 0) float decorationY) {
this.decorationY = decorationY;
postInvalidateImpl();
}
public void setMatrix(@IntRange(from = 0) int row, @IntRange(from = 0) int column) {
setRow(row);
setColumn(column);
}
public int getRow() {
return row;
}
public void setRow(@IntRange(from = 0) int row) {
this.row = row;
isFixRows = row > 0;
postInvalidateImpl();
}
public int getColumn() {
return column;
}
public void setColumn(@IntRange(from = 0) int column) {
this.column = column;
isFixColumns = column > 0;
postInvalidateImpl();
}
public float getMaxTextWidth() {
return maxTextWidth;
}
public void setMaxTextWidth(float maxTextWidth) {
if (this.maxTextWidth != maxTextWidth) {
this.maxTextWidth = maxTextWidth;
isTextSizeChanged = true;
}
postInvalidateImpl();
}
@Override
protected void onDraw(Canvas canvas) {
if (!TextUtils.isEmpty(getText())) {
initTextLayout();
if (textLayout != null) {
float realColumnWith = columnWith + spacingX;//真实列宽
float realRowHeight = rowHeight + spacingY;//真实行高
canvas.translate((width - realColumnWith * realColumn) / 2f + correctX + spacingX / 2, (height - realRowHeight * realRow) / 2f + correctY + spacingY / 2);//居中绘制
float offset = offsetX > -1 ? offsetX : realColumnWith / 2f;//计算偶数行所需偏移量
int column = isFixColumns ? this.realColumn : this.realColumn + 1;
int row = isFixRows ? this.realRow : this.realRow + 1;
float addColumn = column + (offset != 0 ? (offset / realColumnWith + 1) : 0);
for (int i = 0; i < row; i++) {//行
canvas.save();
boolean isEvenNum = i % 2 == 1;//是否偶数行
canvas.translate(isEvenNum ? -offset : 0f, realRowHeight * i);//平移至每行起点
for (int j = 0; j < (isEvenNum ? addColumn : column); j++) {//列 偶数行因偏移为美观需额外添加列
canvas.save();
canvas.translate(realColumnWith * j, 0);//平移至每列起点
canvas.rotate(rotation);//旋转坐标系
textLayout.draw(canvas);//绘制文字
canvas.restore();
}
canvas.restore();
}
}
}
}
private void initTextLayout() {
CharSequence source = getText();
if (!TextUtils.isEmpty(source)) {
if (!source.equals(lastText)) {
lastText = source;
isTextSizeChanged = true;
}
TextPaint paint = getPaint();
if (isTextSizeChanged && paint != null) {
paint.setColor(getCurrentTextColor());
width = getWidth();
height = getHeight();
textWidth = paint.measureText(source.toString());
if (maxTextWidth > 0 && textWidth > maxTextWidth) {
textWidth = maxTextWidth;
}
if (isVertical && textWidth > height) {
textWidth = height;
} else if (!isVertical && textWidth > width) {
textWidth = width;
}
isTextSizeChanged = false;
//1.需要分行的字符串
//4.画笔对象
//5.layout的宽度,字符串超出宽度时自动换行。
//6.layout的对其方式,有ALIGN_CENTER, ALIGN_NORMAL, ALIGN_OPPOSITE 三种。
//7.相对行间距,相对字体大小,1.5f表示行间距为1.5倍的字体高度。
//8.在基础行距上添加多少 实际行间距等于这两者的和。
//9.设置是否在字体的上升和下降之外包括额外的空间(避免在某些语言(例如阿拉伯语和卡纳达语)中进行剪切是必需的*)。 *默认值为{@code true}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
textLayout = StaticLayout.Builder
.obtain(source, 0, source.length(), paint, (int) textWidth)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.setLineSpacing(0f, 1f)
.build();
} else {
textLayout = new StaticLayout(
source,
paint,
(int) textWidth,
Layout.Alignment.ALIGN_CENTER,
1f,
0f,
true
);
}
}
if (textLayout != null) {
double rotation = Math.abs(this.rotation * (Math.PI / 180));
int textHeight = textLayout.getHeight();
columnWith = (int) (Math.abs(textWidth * Math.cos(rotation)) + Math.abs(textHeight * Math.cos(Math.PI / 2 - rotation)));
spacingX = decorationX;
spacingY = decorationY;
realRow = row;
realColumn = column;
if (isFixColumns) {
int w = width / column;
if (w < columnWith) {//固定列数,平均列宽小于文字绘制所需宽度,重新计算列数
realColumn = (int) (width / (columnWith + decorationX)) + 1;
} else {
spacingX = w - columnWith + decorationX;
}
} else {
realColumn = (int) (width / (columnWith + decorationX)) + 1;
}
float offsetY = (float) Math.abs(textWidth * Math.sin(rotation));
rowHeight = (int) (offsetY + Math.abs(textHeight * Math.cos(rotation)));
if (isFixRows) {
int h = height / row;
if (h < rowHeight) {//固定列数,平均列宽小于文字绘制所需宽度,重新计算列数
realRow = (int) (height / (rowHeight + decorationY)) + 1;
} else {
spacingY = h - rowHeight + decorationY;
}
} else {
realRow = (int) (height / (rowHeight + decorationY)) + 1;
}
boolean firstQuadrant = this.rotation < 0 && this.rotation >= -90;
boolean deltaQuadrant = this.rotation == 0 || (this.rotation >= -360 && this.rotation < -270);
int x = 0;
//根据canvas旋转后文字所在象限计算矫正偏移量
if (firstQuadrant) {
x = 1;
correctX = 0;
correctY = offsetY;
} else if (deltaQuadrant) {
correctX = (float) (Math.abs(Math.sin(rotation) * textHeight));
correctY = 0;
x = 4;
} else if (this.rotation < -90 && this.rotation >= -180) {//2
x = 2;
correctX = (float) Math.abs(textWidth * Math.sin(rotation - Math.PI / 2f));
correctY = rowHeight;
} else {//3
x = 3;
correctX = columnWith;
correctY = rowHeight - offsetY;
}
}
}
}
}
arrts资源
<declare-styleable name="WatermarkView">
<attr name="column" format="integer" />
<attr name="row" format="integer" />
<attr name="decorationX" format="dimension" />
<attr name="decorationY" format="dimension" />
</declare-styleable>
