最近发现了一个比较好玩的效果,android实现的LED点阵屏幕效果,挺有意思的,于是花了点时间实现了一下,这个用在演唱会上的粉丝当成牌子举是不是挺好的呢,或者是送给妹子?哈哈~
实现思路比较简单,主要是计算汉字对应的点阵矩阵,汉字通过GB2312编码,每个汉字对用两个byte来表示,而一个汉字被存储为点阵时,以16*16表示,需要16*16=256bit,也就是32byte,GB2312编码也是它在字库文件中的区码和位码,通过这两个值可以计算到这个汉字在字库文件中的相对位置,根据这个位置读取接下来的32位,就对应着这个汉字对应的字模信息,字模信息 其实就是一个byte数组,对于16*16的汉字,对应着长度为32的byte数组。
比如要在手机屏幕上显示两个“我”字,也就是上面的样子,其实对应一个矩阵,称之为点阵矩阵,在存储的时候就是需要保存一个点阵矩阵,如上图的样子,黑色点为true,空心点为false,这个点阵矩阵很容易根据每个汉字的字模信息局算出来,其实就是将字模信息中的每个byte处理一下它的每个bit。
比如“我”这个字,它的字模信息为
[4, -128, 14, -96, 120, -112, 8, -112, 8, -124, -1, -2, 8, -128, 8, -112, 10, -112, 12, 96, 24, 64, 104, -96, 9, 32, 10, 20, 40, 20, 16, 12,]
两个”我”得到就是
[4, -128, 14, -96, 120, -112, 8, -112, 8, -124, -1, -2, 8, -128, 8, -112, 10, -112, 12, 96, 24, 64, 104, -96, 9, 32, 10, 20, 40, 20, 16, 12,
4, -128, 14, -96, 120, -112, 8, -112, 8, -124, -1, -2, 8, -128, 8, -112, 10, -112, 12, 96, 24, 64, 104, -96, 9, 32, 10, 20, 40, 20, 16, 12]
接下来就是将其对应成下面的byte矩阵,当然,对于每个字节,我们保存它的位信息,即保存成boolean值。
0x04,0x80 | 0x04,0x80
0x0E,0xA0 | 0x0E,0xA0
0x78,0x90 | 0x78,0x90
0x08,0x90 | 0x08,0x90
0x08,0x84 | 0x08,0x84
0xFF,0xFE | 0xFF,0xFE
0x08,0x80 | 0x08,0x80
0x08,0x90 | 0x08,0x90
0x0A,0x90 | 0x0A,0x90
0x0C,0x60 | 0x0C,0x60
0x18,0x40 | 0x18,0x40
0x68,0xA0 | 0x68,0xA0
0x09,0x20 | 0x09,0x20
0x0A,0x14 | 0x0A,0x14
0x28,0x14 | 0x28,0x14
0x10,0x0C | 0x10,0x0C
在绘图时,最好还是用SurfaceView,因为滚动时,绘制任务较多,需要注意的几个参数
控件宽度w,高度h,s为点的半径,高度方向上的点的个数为num,于是
点的半径r=(h – (num + 1) * s ) / (2 * num)
第(i,j)个点的坐标为 { (s + r + (s + 2 * r) * j), (s + r + (s + 2 * r) * i)}
当画每个点的时候和点阵矩阵对比,如果对应位置在点阵矩阵中为true就画成实心点,否则为空心点。
如何让启动起来呢?其实也很简单,只需要定时的将这个点阵矩阵的列循环左右移动,然后重绘即可,移动的速度只需要调整定时重绘的间隔。
此外,支持设置颜色textColor,默认绿色
设置点间隔spacing,一般不用设置
设置是否滚动scroll,默认是
设置滚动方向scrollDirection,默认向左
设置滚动速度speed,可选normal和slow,默认normal
效果图
代码:
LedView.java
public class LedView extends TextView { private FontUtils utils; /* * 一个字用16*16的点阵表示 */ private int dots = 16; /* * 点阵之间的距离 */ private float spacing = 10; /* * 点阵中点的半径 */ private float radius; private Paint normalPaint; private Paint selectPaint; private String text; /* * 汉字对应的点阵矩阵 */ private boolean[][] matrix; /* * 是否开启滚动 */ private boolean scroll; /* * 默认颜色绿色 */ private int paintColor = Color.GREEN; private Thread thread; /* * 滚动的text */ private volatile boolean scrollText = true; /* * 用来调整滚动速度 */ private int sleepTime = 100; /* * 滚动方向,默认0向左 */ private int scrollDirection = 0; public LedView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LedTextView); int n = typedArray.getIndexCount(); for (int i = 0; i < n; i++) { int attr = typedArray.getIndex(i); switch (attr) { case R.styleable.LedTextView_textColor: paintColor = typedArray.getColor(R.styleable.LedTextView_textColor, Color.GREEN); break; case R.styleable.LedTextView_spacing: spacing = typedArray.getDimension(R.styleable.LedTextView_spacing, 10); break; case R.styleable.LedTextView_scroll: scroll = typedArray.getBoolean(R.styleable.LedTextView_scroll, true); break; case R.styleable.LedTextView_speed: int speed = typedArray.getInt(R.styleable.LedTextView_speed, 0); if (0 == speed) { sleepTime = 100; } else { sleepTime = 300; } break; case R.styleable.LedTextView_scrollDirection: scrollDirection = typedArray.getInt(R.styleable.LedTextView_scrollDirection, 0); break; } } typedArray.recycle(); selectPaint = new Paint(); selectPaint.setStyle(Style.FILL); selectPaint.setColor(paintColor); normalPaint = new Paint(); normalPaint.setStyle(Style.STROKE); normalPaint.setColor(paintColor); text = getText().toString(); if (1 == scrollDirection) { text = reverseString(text); } utils = new FontUtils(context); matrix = utils.getWordsInfo(text); if (scroll) { thread = new ScrollThread(); thread.start(); } } public LedView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public LedView(Context context) { this(context, null, 0); } /** * 翻转字符串,当设置享有滚动时调用 * @param str * @return */ private String reverseString(String str){ StringBuffer sb = new StringBuffer(str); sb=sb.reverse(); return sb.toString(); } /* * 用于控制滚动,仅在开启滚动时启动。 */ private class ScrollThread extends Thread { @Override public void run() { while (scrollText) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } if (0 == scrollDirection) { matrixLeftMove(matrix); } else { matrixRightMove(matrix); } postInvalidate(); } } } /** * 向左滚动时调用,列循环左移 * @param matrix */ private void matrixLeftMove(boolean [][] matrix){ for (int i = 0; i < matrix.length; i++) { boolean tmp = matrix[i][0]; System.arraycopy(matrix[i], 1, matrix[i], 0, matrix[0].length - 1); matrix[i][matrix[0].length - 1] = tmp; } } /** * 向右滚动时调用,列循环右移 * @param matrix */ private void matrixRightMove(boolean [][] matrix){ for (int i = 0; i < matrix.length; i++) { boolean tmp = matrix[i][matrix[0].length - 1]; System.arraycopy(matrix[i], 0, matrix[i], 1, matrix[0].length - 1); matrix[i][0] = tmp; } } /** * 主要是想处理AT_MOST的情况,我觉得View默认的情况就挺好的,由于继承自TextView,而TextView重 * 写了onMeasure,因此这里参考View#onMeasure函数的写法即可 */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); } private void drawText(Canvas canvas) { radius = (getHeight() - (dots + 1) * spacing) / (2 * dots); // 行 int row = 0; // 列 int column = 0; while (getYPosition(row) < getHeight()) { while (getXPosition(column) < getWidth()) { // just draw if (row < matrix.length && column < matrix[0].length && matrix[row][column]) { canvas.drawCircle(getXPosition(column), getYPosition(row), radius, selectPaint); } else { canvas.drawCircle(getXPosition(column), getYPosition(row), radius, normalPaint); } column++; } row++; column = 0; } } /** * 获取绘制第column列的点的X坐标 * @param column * @return */ private float getXPosition(int column) { return spacing + radius + (spacing + 2 * radius) * column; } /** * 获取绘制第row行的点的Y坐标 * @param row * @return */ private float getYPosition(int row) { return spacing + radius + (spacing + 2 * radius) * row; } private void stopScroll() { scrollText = false; } @Override protected void onDetachedFromWindow() { stopScroll(); super.onDetachedFromWindow(); } @Override protected void onDraw(Canvas canvas) {// super.onDraw(canvas); drawText(canvas); }}
FontUtils.java
public class FontUtils { private Context context; /* * 字库名 */ private static String dotMatrixFont = "HZK16";; /* * 编码 GB2312 */ private final static String ENCODE = "GB2312"; /* * 16X16的字库用16个点表示 */ private int dots = 16; /* * 一个字用点表示需要多少字节,16X16的字体需要32个字节 */ private int wordByteByDots = 32; public FontUtils(Context context) { this.context = context; } /** * 获取字符串的点阵矩阵 * @param str * @return */ public boolean[][] getWordsInfo(String str) { byte[] dataBytes = null; try { dataBytes = str.getBytes(ENCODE); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } // 汉字对应的byte数组 int[] byteCode = new int[dataBytes.length]; // 当成无符号对待 for (int i = 0; i < dataBytes.length; i++) { // 根据每两个byte数组计算一个汉字的相对位置 byteCode[i] = dataBytes[i] < 0 ? 256 + dataBytes[i] : dataBytes[i]; } // 用来存放所有汉字对应的字模信息 // 一个汉字32 byte int wordNums = byteCode.length/2; boolean[][] matrix = new boolean[dots][wordNums * dots]; byte [] dataResult = new byte[wordNums * wordByteByDots]; for (int i = 0, numIndex = 0; i < byteCode.length; i += 2, numIndex++) { // 依次读取到这个汉字对应的32位的字模信息 byte[] data = read(byteCode[i], byteCode[i+1]); System.arraycopy(data, 0, dataResult, numIndex * data.length, data.length); } for (int num = 0; num < wordNums; num++) { for (int i = 0; i < dots; i++) { for (int j1 = 0; j1 < 2; j1++) { // 对每个字节进行解析 byte tmp = dataResult[num * wordByteByDots + i * 2 + j1]; for (int j2 = 0; j2 < 8; j2++) { if (((tmp >> (7 - j2)) & 1) == 1) { matrix[i][num*dots + j1 * 8 + j2] = true; } else { matrix[i][num*dots + j1 * 8 + j2] = false; } } } } } return matrix; } /** * 获取一个汉字的点阵数组,开始测试代码时用。。 * @see getWordsInfo * @param str * @param font * @return */ @SuppressWarnings("unused") private boolean[][] getWordInfo(String str, String font) { boolean[][] matrix = new boolean[16][16]; int[] byteCode = new int[2]; try { byte[] data = str.getBytes(ENCODE); // 当成无符号对待 byteCode[0] = data[0] < 0 ? 256 + data[0] : data[0]; byteCode[1] = data[1] < 0 ? 256 + data[1] : data[1]; } catch (UnsupportedEncodingException e) { e.printStackTrace(); } // 读取到这个汉子对应的32位的字模信息 byte[] data = read(byteCode[0], byteCode[1]); // 填充16x16的矩阵 for (int i = 0; i < dots; i++) { for (int j1 = 0; j1 < 2; j1++) { // 对每个字节进行解析 byte tmp = data[i * 2 + j1]; for (int j2 = 0; j2 < 8; j2++) { if (((tmp >> (7 - j2)) & 1) == 1) { matrix[i][j1 * 8 + j2] = true; } else { matrix[i][j1 * 8 + j2] = false; } } } } return matrix; } /** * 从字库中找到这个汉字的字模信息 * @param areaCode 区码,对应编码的第一个字节 * @param posCode 位码,对应编码的第二个字节 * @return */ protected byte[] read(int areaCode, int posCode) { byte[] data = null; try { int area = areaCode - 0xa0; int pos = posCode - 0xa0; InputStream in = context.getResources().getAssets().open(dotMatrixFont); long offset = wordByteByDots * ((area - 1) * 94 + pos - 1); in.skip(offset); data = new byte[wordByteByDots]; in.read(data, 0, wordByteByDots); in.close(); } catch (Exception ex) { } return data; }}
自定义参数文件attrs.xml
使用时需要将16*16的点阵字库放到assets文件夹中。
查看原文: