注册

Android 实现LED 展示效果

一、前言


LED以其卓越的亮度和醒目的文字和图案,已成为车水马龙的城市中充满烟火气息的象征,深层次的是您红灯的闪烁唤醒着人们的娱乐、怀旧、童年的记忆。当然对新时代来说这显然格格不入的,因此这种霓虹灯能存在多久显然还是个问题。


效果预览



二、实现原理


最初的设想是利用BitmapShader  + Shader 实现网格图片,但是最终是失败的,因此绘制出的网格不是纯色。


为什么是需要网格纯色呢 ,主要原因是LED等作为单独的实体,单个LED智能发出一种光,电视也是一样的道理,微小的发光单元不可能同时发出多种光源,这也是LED显示屏的制作原理。至于我们的自定义View,本身是细腻的屏幕上发出的,如果一个LED发出多种光,就会显得很假。但事实上,在绘制View时一个区域可能会出现多种颜色,如何平衡这种颜色也是个问题,优化方式当然是增加采样点;但是采样点多了也会带来新的副作用,一是性能问题,而是过多的全透明和alpha为0的情况,因为这种情况会过度稀释真是的颜色,造成模糊不清的问题,其次和View本身的背景穿透,形成较大范围的噪点,所以绘制过程中一定要控制采样点的数量,其次对alpha为0或者过小的的情况剔除,当然不用担心失真,因为过度的透明人眼会认为是全透明,没有太多意义,我们来做个总结:



  • LED 单元智能发出一种光,因此不适合BitampShader做风格渲染
  • 颜色逼真程度和采样点有关,采样点越多越逼近真色
  • 清晰程度和LED单元大小相关,LED单元越小越清晰
  • 剔除alpha通道过小和颜色值为0的采样点颜色 

三、核心逻辑


生成刷子纹理


     if (brushBitmap == null) {
brushBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
brushCanvas = new Canvas(brushBitmap);
}

for (int i = 0; i < drawers.size(); i++) {
int saveCount = brushCanvas.save();
drawers.get(i).draw(brushCanvas, width, height, mCommonPaint);
brushCanvas.restoreToCount(saveCount);
}

生成网格数据


        float blockWidth = (squareWidth + padding);
int w = width;
int h = height;
int columNum = (int) Math.ceil(w / blockWidth);
int rowNum = (int) Math.ceil(h / blockWidth);

if (gridRects.isEmpty() && squareWidth > 1f) {
//通过rowNum * columNum方式降低时间复杂度
for (int i = 0; i < rowNum * columNum; i++) {

int col = i % columNum;
int row = (i / columNum);

Rect rect = new Rect();
rect.left = (int) (col * blockWidth);
rect.top = (int) (row * blockWidth);
rect.right = (int) (col * blockWidth + squareWidth);
rect.bottom = (int) (row * blockWidth + squareWidth);
//记录网格点
gridRects.add(rect);
}

}

采样绘制


    //这里是重点 ,LED等可以看作一只灯泡,灯泡区域要么全亮,要么全不亮
for (int i = 0; i < gridRects.size(); i++) {
Rect rect = gridRects.get(i);

if (brushBitmap.getWidth() <= rect.right) {
continue;
}
if (brushBitmap.getHeight() <= rect.bottom) {
continue;
}

if (sampleColors == null) {
sampleColors = new int[9];
}

//取7个点采样,纯粹是为了性能考虑,如果想要更准确的颜色,可以多采样几个点

sampleColors[0] = brushBitmap.getPixel(rect.left, rect.top); // left-top
sampleColors[1] = brushBitmap.getPixel(rect.right, rect.top); // right-top
sampleColors[2] = brushBitmap.getPixel(rect.right, rect.bottom); // right-bottom
sampleColors[3] = brushBitmap.getPixel(rect.left, rect.bottom); // left-bottom
sampleColors[4] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 2); //center

sampleColors[5] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 4); //top line
sampleColors[6] = brushBitmap.getPixel(rect.left + rect.width() * 3 / 4, rect.top + rect.height() / 2); //right line
sampleColors[7] = brushBitmap.getPixel(rect.left + rect.width() / 4, rect.top + rect.height() / 2); // left line
sampleColors[8] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() * 3 / 4); // bottom line

int alpha = 0;
int red = 0;
int green = 0;
int blue = 0;
int num = 0;

for (int c : sampleColors) {
if (c == Color.TRANSPARENT) {
//剔除全透明的颜色,必须剔除
continue;
}
int alphaC = Color.alpha(c);
if (alphaC <= 0) {
//剔除alpha为0的颜色,当然可以改大一点,防止降低清晰度
continue; }
alpha += alphaC;
red += Color.red(c);
green += Color.green(c);
blue += Color.blue(c);
num++;
}

if (num < 1) {
continue;
}

//求出平均值
int rectColor = Color.argb(alpha / num, red / num, green / num, blue / num);
if (rectColor != Color.TRANSPARENT) {
mGridPaint.setColor(rectColor);
// canvas.drawRect(rect, mGridPaint); //绘制矩形
canvas.drawCircle(rect.centerX(), rect.centerY(), squareWidth / 2, mGridPaint); //绘制圆
}
}

如果不剔除颜色,那么就会有噪点和清晰度问题



全部代码


public class LedDisplayView extends View {
private final DisplayMetrics mDM;
private TextPaint mGridPaint;
private TextPaint mCommonPaint;
private List<IDrawer> drawers = new ArrayList<>();
private Bitmap brushBitmap = null;
private float padding = 2; //分界线大小
private float squareWidth = 5; //网格大小
private List<Rect> gridRects = new ArrayList<>();
int[] sampleColors = null;
private Canvas brushCanvas = null;

public LedDisplayView(Context context) {
this(context, null);
}

public LedDisplayView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public LedDisplayView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDM = getResources().getDisplayMetrics();
initPaint();

}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

if (widthMode != MeasureSpec.EXACTLY) {
widthSize = mDM.widthPixels / 2;
}

int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

if (heightMode != MeasureSpec.EXACTLY) {
heightSize = widthSize / 2;
}
setMeasuredDimension(widthSize, heightSize);
}


public float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
}

public float sp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
}


@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (brushBitmap != null && !brushBitmap.isRecycled()) {
brushBitmap.recycle();
}
brushBitmap = null;
}


@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

int width = getWidth();
int height = getHeight();
if (width <= padding || height <= padding) {
return;
}

if (brushBitmap == null) {
brushBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
brushCanvas = new Canvas(brushBitmap);
}

for (int i = 0; i < drawers.size(); i++) {
int saveCount = brushCanvas.save();
drawers.get(i).draw(brushCanvas, width, height, mCommonPaint);
brushCanvas.restoreToCount(saveCount);
}


float blockWidth = (squareWidth + padding);
int w = width;
int h = height;
int columNum = (int) Math.ceil(w / blockWidth);
int rowNum = (int) Math.ceil(h / blockWidth);

if (gridRects.isEmpty() && squareWidth > 1f) {
//通过rowNum * columNum方式降低时间复杂度
for (int i = 0; i < rowNum * columNum; i++) {

int col = i % columNum;
int row = (i / columNum);

Rect rect = new Rect();
rect.left = (int) (col * blockWidth);
rect.top = (int) (row * blockWidth);
rect.right = (int) (col * blockWidth + squareWidth);
rect.bottom = (int) (row * blockWidth + squareWidth);
//记录网格点
gridRects.add(rect);
}

}
int color = mGridPaint.getColor();

//这里是重点 ,LED等可以看作一只灯泡,灯泡区域要么全亮,要们全不亮
for (int i = 0; i < gridRects.size(); i++) {
Rect rect = gridRects.get(i);

if (brushBitmap.getWidth() <= rect.right) {
continue;
}
if (brushBitmap.getHeight() <= rect.bottom) {
continue;
}

if (sampleColors == null) {
sampleColors = new int[9];
}

//取7个点采样,纯粹是为了性能考虑,如果想要更准确的颜色,可以多采样几个点

sampleColors[0] = brushBitmap.getPixel(rect.left, rect.top); // left-top
sampleColors[1] = brushBitmap.getPixel(rect.right, rect.top); // right-top
sampleColors[2] = brushBitmap.getPixel(rect.right, rect.bottom); // right-bottom
sampleColors[3] = brushBitmap.getPixel(rect.left, rect.bottom); // left-bottom
sampleColors[4] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 2); //center

sampleColors[5] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() / 4); //top line
sampleColors[6] = brushBitmap.getPixel(rect.left + rect.width() * 3 / 4, rect.top + rect.height() / 2); //right line
sampleColors[7] = brushBitmap.getPixel(rect.left + rect.width() / 4, rect.top + rect.height() / 2); // left line
sampleColors[8] = brushBitmap.getPixel(rect.left + rect.width() / 2, rect.top + rect.height() * 3 / 4); // bottom line

int alpha = 0;
int red = 0;
int green = 0;
int blue = 0;
int num = 0;

for (int c : sampleColors) {
if (c == Color.TRANSPARENT) {
//剔除全透明的颜色,必须剔除
continue;
}
int alphaC = Color.alpha(c);
if (alphaC <= 0) {
//剔除alpha为0的颜色,当然可以改大一点,防止降低清晰度
continue;
}
alpha += alphaC;
red += Color.red(c);
green += Color.green(c);
blue += Color.blue(c);
num++;
}

if (num < 1) {
continue;
}

//求出平均值
int rectColor = Color.argb(alpha / num, red / num, green / num, blue / num);
if (rectColor != Color.TRANSPARENT) {
mGridPaint.setColor(rectColor);
// canvas.drawRect(rect, mGridPaint); //绘制矩形
canvas.drawCircle(rect.centerX(), rect.centerY(), squareWidth / 2, mGridPaint); //绘制圆
}
}
mGridPaint.setColor(color);

}


private void initPaint() {
// 实例化画笔并打开抗锯齿
mGridPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mGridPaint.setAntiAlias(true);
mGridPaint.setColor(Color.LTGRAY);
mGridPaint.setStyle(Paint.Style.FILL);
mGridPaint.setStrokeCap(Paint.Cap.ROUND); //否则网格绘制

//否则提供给外部纹理绘制
mCommonPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mCommonPaint.setAntiAlias(true);
mCommonPaint.setStyle(Paint.Style.FILL_AND_STROKE);
mCommonPaint.setStrokeCap(Paint.Cap.ROUND);

}

public void addDrawer(IDrawer drawer) {
if (drawer == null) return;
this.drawers.add(drawer);
gridRects.clear();
postInvalidate();
}

public void removeDrawer(IDrawer drawer) {
if (drawer == null) return;
this.drawers.remove(drawer);
gridRects.clear();
postInvalidate();
}

public void clearDrawer() {
this.drawers.clear();
gridRects.clear();
postInvalidate();
}

public List<IDrawer> getDrawers() {
return new ArrayList<>(drawers);
}

public interface IDrawer {
void draw(Canvas canvas, int width, int height, Paint paint);
}

}

使用方式


       LedDisplayView displayView = findViewById(R.id.ledview);
final BitmapDrawable bitmapDrawable1 = (BitmapDrawable)getResources().getDrawable(R.mipmap.mm_07);
final BitmapDrawable bitmapDrawable2 = (BitmapDrawable)getResources().getDrawable(R.mipmap.mm_08);
ledDisplayView.addDrawer(new LedDisplayView.IDrawer() {

Matrix matrix = new Matrix();
@Override
public void draw(Canvas canvas, int width, int height, Paint paint) {
canvas.translate(width/2,height/2);
matrix.preTranslate(-width/2,-height/4);
Bitmap bitmap1 = bitmapDrawable1.getBitmap();
canvas.drawBitmap(bitmap1,matrix,paint);

matrix.postTranslate(width/2,height/4);
Bitmap bitmap2 = bitmapDrawable2.getBitmap();
canvas.drawBitmap(bitmap2,matrix,paint);
}
});
ledDisplayView.addDrawer(new LedDisplayView.IDrawer() {
@Override
public void draw(Canvas canvas, int width, int height, Paint paint) {
paint.setColor(Color.CYAN);
float textSize = paint.getTextSize();
paint.setTextSize(sp2px(50));
canvas.drawText("你好,L E D", 100, 200, paint);
canvas.drawText("85%", 100, 350, paint);

paint.setColor(Color.YELLOW);
canvas.drawCircle(width*3 / 4, height / 4, 100, paint);

paint.setTextSize(textSize);
}
});

四、总结


这个本质上的核心就是采样,通过采样我们最终实现了纹理贴图,这点类似open gl中的光栅化,将图形分割成小三角形一样,最后着色,理解本篇也能帮助大家理解open gl和led显示原理。


作者:时光少年
来源:juejin.cn/post/7304973928039153705

0 个评论

要回复文章请先登录注册