/* * Copyright (C) 2013 Chen Hui <calmer91@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package master.flame.danmaku.danmaku.model.android; import android.annotation.SuppressLint; import android.graphics.Camera; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Typeface; import android.os.Build; import android.text.TextPaint; import master.flame.danmaku.danmaku.model.AbsDisplayer; import master.flame.danmaku.danmaku.model.AlphaValue; import master.flame.danmaku.danmaku.model.BaseDanmaku; import master.flame.danmaku.danmaku.parser.DanmakuFactory; import master.flame.danmaku.danmaku.renderer.IRenderer; import java.util.HashMap; import java.util.Map; /** * Created by MoiTempete. */ public class AndroidDisplayer extends AbsDisplayer<Canvas> { private Camera camera = new Camera(); private Matrix matrix = new Matrix(); private final static Map<Float,Float> sTextHeightCache = new HashMap<Float,Float>(); private static float sLastScaleTextSize; private final static Map<Float,Float> sCachedScaleSize = new HashMap<Float, Float>(10); @SuppressWarnings("unused") private int HIT_CACHE_COUNT = 0; @SuppressWarnings("unused") private int NO_CACHE_COUNT = 0; public static TextPaint PAINT, PAINT_DUPLICATE; private static Paint ALPHA_PAINT; private static Paint UNDERLINE_PAINT; private static Paint BORDER_PAINT; /** * 下划线高度 */ public static int UNDERLINE_HEIGHT = 4; /** * 边框厚度 */ public static final int BORDER_WIDTH = 4; /** * 阴影半径 */ private static float SHADOW_RADIUS = 4.0f; /** * 描边宽度 */ private static float STROKE_WIDTH = 3.5f; /** * 投影参数 */ private static float sProjectionOffsetX = 1.0f; private static float sProjectionOffsetY = 1.0f; private static int sProjectionAlpha = 0xCC; /** * 开启阴影,可动态改变 */ public static boolean CONFIG_HAS_SHADOW = false; private static boolean HAS_SHADOW = CONFIG_HAS_SHADOW; /** * 开启描边,可动态改变 */ public static boolean CONFIG_HAS_STROKE = true; private static boolean HAS_STROKE = CONFIG_HAS_STROKE; /** * 开启投影,可动态改变 */ public static boolean CONFIG_HAS_PROJECTION = false; private static boolean HAS_PROJECTION = CONFIG_HAS_PROJECTION; /** * 开启抗锯齿,可动态改变 */ public static boolean CONFIG_ANTI_ALIAS = true; private static boolean ANTI_ALIAS = CONFIG_ANTI_ALIAS; static { PAINT = new TextPaint(); PAINT.setStrokeWidth(STROKE_WIDTH); PAINT_DUPLICATE = new TextPaint(PAINT); ALPHA_PAINT = new Paint(); UNDERLINE_PAINT = new Paint(); UNDERLINE_PAINT.setStrokeWidth(UNDERLINE_HEIGHT); UNDERLINE_PAINT.setStyle(Style.STROKE); BORDER_PAINT = new Paint(); BORDER_PAINT.setStyle(Style.STROKE); BORDER_PAINT.setStrokeWidth(BORDER_WIDTH); } @SuppressLint("NewApi") private static final int getMaximumBitmapWidth(Canvas c) { if(Build.VERSION.SDK_INT >= 14) { return c.getMaximumBitmapWidth(); } else { return c.getWidth(); } } @SuppressLint("NewApi") private static final int getMaximumBitmapHeight(Canvas c) { if(Build.VERSION.SDK_INT >= 14) { return c.getMaximumBitmapHeight(); } else { return c.getHeight(); } } public static void setTypeFace(Typeface font){ if(PAINT!=null) PAINT.setTypeface(font); } public static void setShadowRadius(float s){ SHADOW_RADIUS = s; } public static void setPaintStorkeWidth(float s){ PAINT.setStrokeWidth(s); STROKE_WIDTH = s; } public static void setProjectionConfig(float offsetX, float offsetY, int alpha) { if (sProjectionOffsetX != offsetX || sProjectionOffsetY != offsetY || sProjectionAlpha != alpha) { sProjectionOffsetX = (offsetX > 1.0f) ? offsetX : 1.0f; sProjectionOffsetY = (offsetY > 1.0f) ? offsetY : 1.0f; sProjectionAlpha = (alpha < 0) ? 0 : ((alpha > 255) ? 255 : alpha); } } public static void setFakeBoldText(boolean fakeBoldText){ PAINT.setFakeBoldText(fakeBoldText); } public Canvas canvas; private int width; private int height; private float density = 1; private int densityDpi = 160; private float scaledDensity = 1; private int mSlopPixel = 0; private boolean mIsHardwareAccelerated = true; private int mMaximumBitmapWidth = 2048; private int mMaximumBitmapHeight = 2048; private void update(Canvas c) { canvas = c; if (c != null) { width = c.getWidth(); height = c.getHeight(); if (mIsHardwareAccelerated) { mMaximumBitmapWidth = getMaximumBitmapWidth(c); mMaximumBitmapHeight = getMaximumBitmapHeight(c); } } } @Override public int getWidth() { return width; } @Override public int getHeight() { return height; } @Override public float getDensity() { return density; } @Override public int getDensityDpi() { return densityDpi; } @Override public int draw(BaseDanmaku danmaku) { float top = danmaku.getTop(); float left = danmaku.getLeft(); if (canvas != null) { Paint alphaPaint = null; boolean needRestore = false; if (danmaku.getType() == BaseDanmaku.TYPE_SPECIAL) { if (danmaku.getAlpha() == AlphaValue.TRANSPARENT) { return IRenderer.NOTHING_RENDERING; } if (danmaku.rotationZ != 0 || danmaku.rotationY != 0) { saveCanvas(danmaku, canvas, left, top); needRestore = true; } int alpha = danmaku.getAlpha(); if ( alpha != AlphaValue.MAX) { alphaPaint = ALPHA_PAINT; alphaPaint.setAlpha(danmaku.getAlpha()); } } // skip drawing when danmaku is transparent if(alphaPaint!=null && alphaPaint.getAlpha()== AlphaValue.TRANSPARENT){ return IRenderer.NOTHING_RENDERING; } // drawing cache boolean cacheDrawn = false; int result = IRenderer.CACHE_RENDERING; if (danmaku.hasDrawingCache()) { DrawingCacheHolder holder = ((DrawingCache) danmaku.cache).get(); if (holder != null) { cacheDrawn = holder.draw(canvas, left, top, alphaPaint); } } if (!cacheDrawn) { if (alphaPaint != null) { PAINT.setAlpha(alphaPaint.getAlpha()); } else { resetPaintAlpha(PAINT); } drawDanmaku(danmaku, canvas, left, top, true); result = IRenderer.TEXT_RENDERING; } if (needRestore) { restoreCanvas(canvas); } return result; } return IRenderer.NOTHING_RENDERING; } private void resetPaintAlpha(Paint paint) { if (paint.getAlpha() != AlphaValue.MAX) { paint.setAlpha(AlphaValue.MAX); } } private void restoreCanvas(Canvas canvas) { canvas.restore(); } private int saveCanvas(BaseDanmaku danmaku, Canvas canvas, float left, float top) { camera.save(); camera.rotateY(-danmaku.rotationY); camera.rotateZ(-danmaku.rotationZ); camera.getMatrix(matrix); matrix.preTranslate(-left, -top); matrix.postTranslate(left, top); camera.restore(); int count = canvas.save(); canvas.concat(matrix); return count; } public static void drawDanmaku(BaseDanmaku danmaku, Canvas canvas, float left, float top, boolean quick) { float _left = left; float _top = top; left += danmaku.padding; top += danmaku.padding; if (danmaku.borderColor != 0) { left += BORDER_WIDTH; top += BORDER_WIDTH; } HAS_STROKE = CONFIG_HAS_STROKE; HAS_SHADOW = CONFIG_HAS_SHADOW; HAS_PROJECTION = CONFIG_HAS_PROJECTION; ANTI_ALIAS = !quick && CONFIG_ANTI_ALIAS; TextPaint paint = getPaint(danmaku, quick); if (danmaku.lines != null) { String[] lines = danmaku.lines; if (lines.length == 1) { if (hasStroke(danmaku)) { applyPaintConfig(danmaku, paint, true); float strokeLeft = left; float strokeTop = top - paint.ascent(); if (HAS_PROJECTION) { strokeLeft += sProjectionOffsetX; strokeTop += sProjectionOffsetY; } canvas.drawText(lines[0], strokeLeft, strokeTop, paint); } applyPaintConfig(danmaku, paint, false); canvas.drawText(lines[0], left, top - paint.ascent(), paint); } else { float textHeight = (danmaku.paintHeight - 2 * danmaku.padding) / lines.length; for (int t = 0; t < lines.length; t++) { if (lines[t] == null || lines[t].length() == 0) { continue; } if (hasStroke(danmaku)) { applyPaintConfig(danmaku, paint, true); float strokeLeft = left; float strokeTop = t * textHeight + top - paint.ascent(); if (HAS_PROJECTION) { strokeLeft += sProjectionOffsetX; strokeTop += sProjectionOffsetY; } canvas.drawText(lines[t], strokeLeft, strokeTop, paint); } applyPaintConfig(danmaku, paint, false); canvas.drawText(lines[t], left, t * textHeight + top - paint.ascent(), paint); } } } else { if (hasStroke(danmaku)) { applyPaintConfig(danmaku, paint, true); float strokeLeft = left; float strokeTop = top - paint.ascent(); if (HAS_PROJECTION) { strokeLeft += sProjectionOffsetX; strokeTop += sProjectionOffsetY; } canvas.drawText(danmaku.text, strokeLeft, strokeTop, paint); } applyPaintConfig(danmaku, paint, false); canvas.drawText(danmaku.text, left, top - paint.ascent(), paint); } // draw underline if (danmaku.underlineColor != 0) { Paint linePaint = getUnderlinePaint(danmaku); float bottom = _top + danmaku.paintHeight - UNDERLINE_HEIGHT; canvas.drawLine(_left, bottom, _left + danmaku.paintWidth, bottom, linePaint); } //draw border if (danmaku.borderColor != 0) { Paint borderPaint = getBorderPaint(danmaku); canvas.drawRect(_left, _top, _left + danmaku.paintWidth, _top + danmaku.paintHeight, borderPaint); } } private static boolean hasStroke(BaseDanmaku danmaku) { return (HAS_STROKE || HAS_PROJECTION) && STROKE_WIDTH > 0 && danmaku.textShadowColor != 0; } public static Paint getBorderPaint(BaseDanmaku danmaku) { BORDER_PAINT.setColor(danmaku.borderColor); return BORDER_PAINT; } public static Paint getUnderlinePaint(BaseDanmaku danmaku){ UNDERLINE_PAINT.setColor(danmaku.underlineColor); return UNDERLINE_PAINT; } private static TextPaint getPaint(BaseDanmaku danmaku, boolean quick) { TextPaint paint; if (quick) { paint = PAINT_DUPLICATE; paint.set(PAINT); } else { paint = PAINT; } paint.setTextSize(danmaku.textSize); applyTextScaleConfig(danmaku, paint); //ignore the transparent textShadowColor if (!HAS_SHADOW || SHADOW_RADIUS <= 0 || danmaku.textShadowColor == 0) { paint.clearShadowLayer(); } else { paint.setShadowLayer(SHADOW_RADIUS, 0, 0, danmaku.textShadowColor); } paint.setAntiAlias(ANTI_ALIAS); return paint; } public static TextPaint getPaint(BaseDanmaku danmaku) { return getPaint(danmaku, false); } private static void applyPaintConfig(BaseDanmaku danmaku, Paint paint,boolean stroke) { if (DanmakuGlobalConfig.DEFAULT.isTranslucent) { if(stroke){ paint.setStyle(HAS_PROJECTION ? Style.FILL : Style.STROKE); paint.setColor(danmaku.textShadowColor & 0x00FFFFFF); int alpha = HAS_PROJECTION ? (int)(sProjectionAlpha * ((float)DanmakuGlobalConfig.DEFAULT.transparency / AlphaValue.MAX)) : DanmakuGlobalConfig.DEFAULT.transparency; paint.setAlpha(alpha); }else{ paint.setStyle(Style.FILL); paint.setColor(danmaku.textColor & 0x00FFFFFF); paint.setAlpha(DanmakuGlobalConfig.DEFAULT.transparency); } } else { if(stroke){ paint.setStyle(HAS_PROJECTION ? Style.FILL : Style.STROKE); paint.setColor(danmaku.textShadowColor & 0x00FFFFFF); int alpha = HAS_PROJECTION ? sProjectionAlpha : AlphaValue.MAX; paint.setAlpha(alpha); }else{ paint.setStyle(Style.FILL); paint.setColor(danmaku.textColor & 0x00FFFFFF); paint.setAlpha(AlphaValue.MAX); } } } private static void applyTextScaleConfig(BaseDanmaku danmaku, Paint paint) { if (!DanmakuGlobalConfig.DEFAULT.isTextScaled) { return; } Float size = sCachedScaleSize.get(danmaku.textSize); if (size == null || sLastScaleTextSize != DanmakuGlobalConfig.DEFAULT.scaleTextSize) { sLastScaleTextSize = DanmakuGlobalConfig.DEFAULT.scaleTextSize; size = danmaku.textSize * DanmakuGlobalConfig.DEFAULT.scaleTextSize; sCachedScaleSize.put(danmaku.textSize, size); } paint.setTextSize(size); } @Override public void measure(BaseDanmaku danmaku) { TextPaint paint = getPaint(danmaku); if (HAS_STROKE) { applyPaintConfig(danmaku, paint, true); } calcPaintWH(danmaku, paint); if (HAS_STROKE) { applyPaintConfig(danmaku, paint, false); } } private void calcPaintWH(BaseDanmaku danmaku, TextPaint paint) { float w = 0; Float textHeight = getTextHeight(paint); if (danmaku.lines == null) { w = danmaku.text == null ? 0 : paint.measureText(danmaku.text); setDanmakuPaintWidthAndHeight(danmaku,w,textHeight); return; } for(String tempStr : danmaku.lines){ if (tempStr.length() > 0) { float tr = paint.measureText(tempStr); w = Math.max(tr, w); } } setDanmakuPaintWidthAndHeight(danmaku,w,danmaku.lines.length * textHeight); } private void setDanmakuPaintWidthAndHeight(BaseDanmaku danmaku, float w, float h) { float pw = w + 2 * danmaku.padding; float ph = h + 2 * danmaku.padding; if (danmaku.borderColor != 0) { pw += 2 * BORDER_WIDTH; ph += 2 * BORDER_WIDTH; } danmaku.paintWidth = pw + getStrokeWidth(); danmaku.paintHeight = ph; } private static float getTextHeight(TextPaint paint) { Float textSize = paint.getTextSize(); Float textHeight = sTextHeightCache.get(textSize); if(textHeight == null){ Paint.FontMetrics fontMetrics = paint.getFontMetrics(); textHeight = fontMetrics.descent - fontMetrics.ascent + fontMetrics.leading; sTextHeightCache.put(textSize, textHeight); } return textHeight; } public static void clearTextHeightCache(){ sTextHeightCache.clear(); sCachedScaleSize.clear(); } @Override public float getScaledDensity() { return scaledDensity; } @Override public void resetSlopPixel(float factor) { float d = Math.max(density, scaledDensity); d = Math.max(factor, getWidth() / (float) DanmakuFactory.BILI_PLAYER_WIDTH); //correct for low density and high resolution float slop = d * DanmakuFactory.DANMAKU_MEDIUM_TEXTSIZE; mSlopPixel = (int) slop; if (factor > 1f) mSlopPixel = (int) (slop * factor); } @Override public int getSlopPixel() { return mSlopPixel; } @Override public void setDensities(float density, int densityDpi, float scaledDensity) { this.density = density; this.densityDpi = densityDpi; this.scaledDensity = scaledDensity; } @Override public void setSize(int width, int height) { this.width = width; this.height = height; } @Override public void setExtraData(Canvas data) { update(data); } @Override public Canvas getExtraData() { return this.canvas; } @Override public float getStrokeWidth() { if (HAS_SHADOW && HAS_STROKE) { return Math.max(SHADOW_RADIUS, STROKE_WIDTH); } if (HAS_SHADOW) { return SHADOW_RADIUS; } if (HAS_STROKE) { return STROKE_WIDTH; } return 0f; } @Override public void setHardwareAccelerated(boolean enable) { mIsHardwareAccelerated = enable; } @Override public boolean isHardwareAccelerated() { return mIsHardwareAccelerated ; } @Override public int getMaximumCacheWidth() { return mMaximumBitmapWidth; } @Override public int getMaximumCacheHeight() { return mMaximumBitmapHeight; } }