见我上篇 iOS版本介绍 http://blog.csdn.net/chenee543216/article/details/9305391
-前言:
既然放过话,说要实现android版本就硬着头皮凑一个吧(这玩意,我的项目目前压根没有需求,纯义务劳动了。。。)
话说Android实现的比较蛋疼。。。。 真正要写的时候发现连Java语法也忘记了(可能原先就没有学好,对不起“体育老师”),更别说Android下面那几个绘图相关函数了,发现居然一个都不熟悉。。。所以就一边学一边写。。。不说了,都是泪水。。。
二 中言
1 介绍思路。
2 贴代码
3 分析问题
1、实现原理上篇已经说过了,Android绘制LabelTTF就是通过JNI调用Java canvas drawText() 到一个Bitmap里面,然后把bitmap中的pixel取出来。
既然原先采用HTML标记的方式实现,那么理所当然的就沿用了。我看到cocos2d-x 3.0 预览版本里面是直接通过调用参数来设置属性,这种方式实现相对简单一点,但是扩展性不好。(说错了请轻轻拍)。 而HTML标记,只要实现端添加相应解析代码,可以很容易扩展(臭屁:)
iOS里面通过调用CoreText可以直接对一段文本设置属性,android这两天看了一下,其实textview的spannable 方式也可以实现类似coreText的功能。(详见:http://developer.android.com/reference/android/text/Spannable.html),但是这里没有采用,因为:我不会,也懒得去学。。。
所以,俺就把
String s = "Hllo,<font face='HelveticaNeue-CondensedBold' size=60 color='#00CC00'>先生</font>早上好,<a href='action=show'><font color='#FF00FF'>要吃点什么呢?</font>你到底\n吃不吃?</a>\n您的金钱:<a href='item=ID10086'><font color='#FF0000'>2000</font></a>"
这玩意根据不同的属性(android Paint)拆分成一段一段,然后计算位置,一段一段的去drawText(s,x,y,paint);
分2步,第一步解析HTML <> </> 对,生成component,第二步解析component 生产textInfo,textInfo就是直接提供给canvas 去draw text的信息。
具体,见代码吧。
2、代码在这里。。。。。
md,为毛贴不上来??? 擦,一直点取消。。。。怨念啊 。。。
主要就是修改Android项目proj.android\src\org\cocos2dx\lib\Cocos2dxBitmap.java 文件(win下面,linux改\\为 //);
/**************************************************************************** Copyright (c) 2010-2011 cocos2d-x.org http://www.cocos2d-x.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ****************************************************************************/ package org.cocos2dx.lib; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Iterator; import java.util.LinkedList; import java.util.Vector; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.FontMetricsInt; import android.graphics.Rect; import android.graphics.Typeface; import android.text.TextPaint; import android.text.TextUtils; import android.util.FloatMath; import android.util.Log; public class Cocos2dxBitmap { // =========================================================== // Constants // =========================================================== /* The values are the same as cocos2dx/platform/CCImage.h. */ private static final int HORIZONTALALIGN_LEFT = 1; private static final int HORIZONTALALIGN_RIGHT = 2; private static final int HORIZONTALALIGN_CENTER = 3; private static final int VERTICALALIGN_TOP = 1; private static final int VERTICALALIGN_BOTTOM = 2; private static final int VERTICALALIGN_CENTER = 3; // =========================================================== // Fields // =========================================================== private static Context sContext; // =========================================================== // Constructors // =========================================================== // =========================================================== // Getter & Setter // =========================================================== public static void setContext(final Context pContext) { Cocos2dxBitmap.sContext = pContext; } // =========================================================== // Methods for/from SuperClass/Interfaces // =========================================================== // =========================================================== // Methods // =========================================================== private static native void nativeInitBitmapDC(final int pWidth, final int pHeight, final byte[] pPixels); /** * @param pWidth * the width to draw, it can be 0 * @param pHeight * the height to draw, it can be 0 */ public static void createTextBitmap(String pString, final String pFontName, final int pFontSize, final int pAlignment, final int pWidth, final int pHeight) { pString = Cocos2dxBitmap.refactorString(pString); final int horizontalAlignment = pAlignment & 0x0F; final int verticalAlignment = (pAlignment >> 4) & 0x0F; int il = pString.indexOf('<'); int ir = pString.indexOf('>'); if(il != -1 && ir != -1){// FIXME: too simple sometime naive //1 get components Vector<Component> components = Cocos2dxBitmap.getComponent(pString); //2 get text info for drawing int ln = 0; Vector<TextInfo> textinfos = new Vector<TextInfo>(); Vector<Integer> lineY = new Vector<Integer>(); int maxW = 0; //max line width for creating BMP int totalH= 0; // same as above int currentW = 0; //... above int currentH = 0; //... above int currentTop = 0; // for draw font Iterator<Component> it = components.iterator(); while(it.hasNext()){ Component c = it.next();// ++ final Paint p= Cocos2dxBitmap.newPaint2(c,pFontName, pFontSize); final FontMetricsInt fm = p.getFontMetricsInt(); final int h = fm.bottom - fm.top; if(currentH < h) { currentH = h; currentTop = -fm.top; } final String[] lines = c.mText.split("\\n"); for(int i=0;i<lines.length-1;i++){ final int tw = (int)FloatMath.ceil(p.measureText(lines[i])); TextInfo tf = new TextInfo(ln, lines[i], tw,h,currentW,p); textinfos.add(tf); currentW += tw; if(maxW < currentW){ maxW = currentW; } //new line currentW = 0; lineY.add(totalH + currentTop); totalH += currentH; currentH = h; currentTop = -fm.top; ln ++; } String s = lines[lines.length-1]; final int tw = (int)FloatMath.ceil(p.measureText(s)); TextInfo tf = new TextInfo(ln, s, tw,h,currentW,p); textinfos.add(tf); currentW += tw; } lineY.add(totalH + currentTop); totalH += currentH; if(maxW < currentW){ maxW = currentW; } // 3 create BMP according to (maxW & totalH) final Bitmap bitmap = Bitmap.createBitmap(maxW, totalH, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); //4 for test /* final Paint p0= Cocos2dxBitmap.newPaint(pFontName, pFontSize, horizontalAlignment); p0.setColor(Color.BLACK); Rect r = new Rect(0, 0, maxW, totalH ); canvas.drawRect(r, p0); */ // 5 use text info to draw text Iterator<TextInfo> it2 = textinfos.iterator(); while(it2.hasNext()){ TextInfo tf = it2.next(); canvas.drawText(tf.mText, tf.x, lineY.get(tf.lineIndex), tf.paint); } Cocos2dxBitmap.initNativeObject(bitmap); }else{ final Paint paint = Cocos2dxBitmap.newPaint(pFontName, pFontSize, horizontalAlignment); final TextProperty textProperty = Cocos2dxBitmap.computeTextProperty(pString, pWidth, pHeight, paint); final int bitmapTotalHeight = (pHeight == 0 ? textProperty.mTotalHeight : pHeight); final Bitmap bitmap = Bitmap.createBitmap(textProperty.mMaxWidth, bitmapTotalHeight, Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); /* Draw string. */ final FontMetricsInt fontMetricsInt = paint.getFontMetricsInt(); int x = 0; int y = Cocos2dxBitmap.computeY(fontMetricsInt, pHeight, textProperty.mTotalHeight, verticalAlignment); final String[] lines = textProperty.mLines; for (final String line : lines) { x = Cocos2dxBitmap.computeX(line, textProperty.mMaxWidth, horizontalAlignment); canvas.drawText(line, x, y, paint); y += textProperty.mHeightPerLine; } Cocos2dxBitmap.initNativeObject(bitmap); } } private static Paint newPaint2(Component c ,final String pFontName, final int pFontSize) { final Paint paint = Cocos2dxBitmap.newPaint(pFontName, pFontSize, HORIZONTALALIGN_LEFT); Vector<String> fs = c.mFontStyle; for(int i=0;i<fs.size();i++){ String s = fs.get(i); String sarray[] = s.split(" "); if("<font".compareToIgnoreCase(sarray[0])==0){ for(int j=1;j<sarray.length;j++){ String[] s2array = sarray[j].split("="); s2array[1] = s2array[1].replaceAll("'", ""); if("face".compareToIgnoreCase(s2array[0])==0){ paint.setTypeface(Typeface.create(s2array[1], Typeface.NORMAL)); } else if("size".compareToIgnoreCase(s2array[0])==0){ paint.setTextSize(Integer.parseInt(s2array[1])); } else if("color".compareToIgnoreCase(s2array[0])==0){ try{ paint.setColor(Color.parseColor(s2array[1])); }catch(Exception e){ //some thing error YOU should deal it! Log.v("newPaint2::", "set font color err!!!"); paint.setColor(Color.WHITE); } } } }else if("<a".compareToIgnoreCase(sarray[0]) == 0){ // paint.setColor(0x0000FF); paint.setColor(Color.BLUE); paint.setUnderlineText(true); }else if("<i".compareToIgnoreCase(sarray[0]) == 0){ }else if("<b".compareToIgnoreCase(sarray[0]) == 0){ }else if("<bi".compareToIgnoreCase(sarray[0]) == 0){ }else if("<u".compareToIgnoreCase(sarray[0]) == 0){ }else if("<p".compareToIgnoreCase(sarray[0]) == 0){ } } return paint; } private static Paint newPaint(final String pFontName, final int pFontSize, final int pHorizontalAlignment) { final Paint paint = new Paint(); paint.setColor(Color.WHITE); paint.setTextSize(pFontSize); paint.setAntiAlias(true); /* Set type face for paint, now it support .ttf file. */ if (pFontName.endsWith(".ttf")) { try { final Typeface typeFace = Cocos2dxTypefaces.get(Cocos2dxBitmap.sContext, pFontName); paint.setTypeface(typeFace); } catch (final Exception e) { Log.e("Cocos2dxBitmap", "error to create ttf type face: " + pFontName); /* The file may not find, use system font. */ paint.setTypeface(Typeface.create(pFontName, Typeface.NORMAL)); } } else { paint.setTypeface(Typeface.create(pFontName, Typeface.NORMAL)); } switch (pHorizontalAlignment) { case HORIZONTALALIGN_CENTER: paint.setTextAlign(Align.CENTER); break; case HORIZONTALALIGN_RIGHT: paint.setTextAlign(Align.RIGHT); break; case HORIZONTALALIGN_LEFT: default: paint.setTextAlign(Align.LEFT); break; } return paint; } private static TextProperty computeTextProperty(final String pString, final int pWidth, final int pHeight, final Paint pPaint) { final FontMetricsInt fm = pPaint.getFontMetricsInt(); final int h = (int) FloatMath.ceil(fm.bottom - fm.top); int maxContentWidth = 0; final String[] lines = Cocos2dxBitmap.splitString(pString, pWidth, pHeight, pPaint); if (pWidth != 0) { maxContentWidth = pWidth; } else { /* Compute the max width. */ int temp = 0; for (final String line : lines) { temp = (int) FloatMath.ceil(pPaint.measureText(line, 0, line.length())); if (temp > maxContentWidth) { maxContentWidth = temp; } } } return new TextProperty(maxContentWidth, h, lines); } private static int computeX(final String pText, final int pMaxWidth, final int pHorizontalAlignment) { int ret = 0; switch (pHorizontalAlignment) { case HORIZONTALALIGN_CENTER: ret = pMaxWidth / 2; break; case HORIZONTALALIGN_RIGHT: ret = pMaxWidth; break; case HORIZONTALALIGN_LEFT: default: break; } return ret; } private static int computeY(final FontMetricsInt pFontMetricsInt, final int pConstrainHeight, final int pTotalHeight, final int pVerticalAlignment) { int y = -pFontMetricsInt.top; if (pConstrainHeight > pTotalHeight) { switch (pVerticalAlignment) { case VERTICALALIGN_TOP: y = -pFontMetricsInt.top; break; case VERTICALALIGN_CENTER: y = -pFontMetricsInt.top + (pConstrainHeight - pTotalHeight) / 2; break; case VERTICALALIGN_BOTTOM: y = -pFontMetricsInt.top + (pConstrainHeight - pTotalHeight); break; default: break; } } return y; } /* * If maxWidth or maxHeight is not 0, split the string to fix the maxWidth and maxHeight. */ private static String[] splitString(final String pString, final int pMaxWidth, final int pMaxHeight, final Paint pPaint) { final String[] lines = pString.split("\\n"); String[] ret = null; final FontMetricsInt fm = pPaint.getFontMetricsInt(); final int heightPerLine = (int) FloatMath.ceil(fm.bottom - fm.top); final int maxLines = pMaxHeight / heightPerLine; if (pMaxWidth != 0) { final LinkedList<String> strList = new LinkedList<String>(); for (final String line : lines) { /* The width of line is exceed maxWidth, should divide it into two or more lines. */ final int lineWidth = (int) FloatMath.ceil(pPaint.measureText(line)); if (lineWidth > pMaxWidth) { strList.addAll(Cocos2dxBitmap.divideStringWithMaxWidth(line, pMaxWidth, pPaint)); } else { strList.add(line); } /* Should not exceed the max height. */ if (maxLines > 0 && strList.size() >= maxLines) { break; } } /* Remove exceeding lines. */ if (maxLines > 0 && strList.size() > maxLines) { while (strList.size() > maxLines) { strList.removeLast(); } } ret = new String[strList.size()]; strList.toArray(ret); } else if (pMaxHeight != 0 && lines.length > maxLines) { /* Remove exceeding lines. */ final LinkedList<String> strList = new LinkedList<String>(); for (int i = 0; i < maxLines; i++) { strList.add(lines[i]); } ret = new String[strList.size()]; strList.toArray(ret); } else { ret = lines; } return ret; } private static LinkedList<String> divideStringWithMaxWidth(final String pString, final int pMaxWidth, final Paint pPaint) { final int charLength = pString.length(); int start = 0; int tempWidth = 0; final LinkedList<String> strList = new LinkedList<String>(); /* Break a String into String[] by the width & should wrap the word. */ for (int i = 1; i <= charLength; ++i) { tempWidth = (int) FloatMath.ceil(pPaint.measureText(pString, start, i)); if (tempWidth >= pMaxWidth) { final int lastIndexOfSpace = pString.substring(0, i).lastIndexOf(" "); if (lastIndexOfSpace != -1 && lastIndexOfSpace > start) { /* Should wrap the word. */ strList.add(pString.substring(start, lastIndexOfSpace)); i = lastIndexOfSpace; } else { /* Should not exceed the width. */ if (tempWidth > pMaxWidth) { strList.add(pString.substring(start, i - 1)); /* Compute from previous char. */ --i; } else { strList.add(pString.substring(start, i)); } } /* Remove spaces at the beginning of a new line. */ while (pString.indexOf(i) == ' ') { ++i; } start = i; } } /* Add the last chars. */ if (start < charLength) { strList.add(pString.substring(start)); } return strList; } private static Vector<Component> getComponent(final String pString) { //String str = "Hello,<font face='HelveticaNeue-CondensedBold' size=60 color='#00CC00'>先生</font>早上好,<a href='action=show'><font color='#FF00FF'>要吃点什么呢?</font>你到底\n吃不吃?</a>\n您的金钱:<a href='item=ID10086'><font color='#FF0000'>2000</font></a>"; String str = pString; Vector<Component> components = new Vector<Component>(); Vector<String> fontStyle = new Vector<String>(); String tag; int il = str.indexOf('<'); int ir = str.indexOf('>'); String delimeter; String text; int lastPosition = 0; while(il != -1){ delimeter = str.substring(il, ir+1); text = str.substring(il, ir); int position = str.indexOf(delimeter); if(position != -1){ if(delimeter.indexOf("<p") == 0){//paragraph str = str.replaceFirst(delimeter, " "); }else{ str = str.replaceFirst(delimeter, ""); } //replace "<" ">" } if(text.indexOf("</") == 0){//end of tag //1 end of current text tag = text.substring(2); if(position != -1){ for(int i=(int)(components.size()-1);i>=0;i--){ Component comp = components.get(i); if( null != tag && null != comp.mTag && comp.mTag.compareTo(tag) == 0){ Component compPrev = new Component(str.substring(lastPosition, position),tag,position,fontStyle); components.add(compPrev); lastPosition = position; break; } } } //2 pop current fontStyle; if(fontStyle.size() > 0){ fontStyle.removeElementAt(fontStyle.size()-1); } }else{ //1 end of previous text if(lastPosition != position){//<font><a> xxx </a></font> Component compPrev = new Component(str.substring(lastPosition, position),null,position,fontStyle); components.add(compPrev); lastPosition = position; } //tag begin int iSpace = text.indexOf(' '); tag = text.substring(1, iSpace); Component comp= new Component(null,tag,position,fontStyle); components.add(comp); //2 new fontStyle fontStyle.add(text); } il = str.indexOf('<'); ir = str.indexOf('>'); } if(lastPosition < str.length()){ Component comp= new Component(str.substring(lastPosition),null,lastPosition,fontStyle); components.add(comp); } Iterator<Component> it = components.iterator(); while(it.hasNext()){ String s = it.next().mText;// ++ if(s == null || s.compareTo("")==0 ){ it.remove(); } } // Log.d("chenee =======", str); // Log.d("chenee =======", "size:"+components.size()); return components; } private static String refactorString(final String pString) { /* Avoid error when content is "". */ if (pString.compareTo("") == 0) { return " "; } /* * If the font of "\n" is "" or "\n", insert " " in front of it. For example: "\nabc" -> " \nabc" "\nabc\n\n" -> " \nabc\n \n". */ final StringBuilder strBuilder = new StringBuilder(pString); int start = 0; int index = strBuilder.indexOf("\n"); while (index != -1) { if (index == 0 || strBuilder.charAt(index - 1) == '\n') { strBuilder.insert(start, " "); start = index + 2; } else { start = index + 1; } if (start > strBuilder.length() || index == strBuilder.length()) { break; } index = strBuilder.indexOf("\n", start); } return strBuilder.toString(); } private static void initNativeObject(final Bitmap pBitmap) { final byte[] pixels = Cocos2dxBitmap.getPixels(pBitmap); if (pixels == null) { return; } Cocos2dxBitmap.nativeInitBitmapDC(pBitmap.getWidth(), pBitmap.getHeight(), pixels); } private static byte[] getPixels(final Bitmap pBitmap) { if (pBitmap != null) { final byte[] pixels = new byte[pBitmap.getWidth() * pBitmap.getHeight() * 4]; final ByteBuffer buf = ByteBuffer.wrap(pixels); buf.order(ByteOrder.nativeOrder()); pBitmap.copyPixelsToBuffer(buf); return pixels; } return null; } private static int getFontSizeAccordingHeight(int height) { Paint paint = new Paint(); Rect bounds = new Rect(); paint.setTypeface(Typeface.DEFAULT); int incr_text_size = 1; boolean found_desired_size = false; while (!found_desired_size) { paint.setTextSize(incr_text_size); String text = "SghMNy"; paint.getTextBounds(text, 0, text.length(), bounds); incr_text_size++; if (height - bounds.height() <= 2) { found_desired_size = true; } Log.d("font size", "incr size:" + incr_text_size); } return incr_text_size; } private static String getStringWithEllipsis(String pString, float width, float fontSize) { if (TextUtils.isEmpty(pString)) { return ""; } TextPaint paint = new TextPaint(); paint.setTypeface(Typeface.DEFAULT); paint.setTextSize(fontSize); return TextUtils.ellipsize(pString, paint, width, TextUtils.TruncateAt.END).toString(); } // =========================================================== // Inner and Anonymous Classes // =========================================================== private static class TextProperty { /** The max width of lines. */ private final int mMaxWidth; /** The height of all lines. */ private final int mTotalHeight; private final int mHeightPerLine; private final String[] mLines; TextProperty(final int pMaxWidth, final int pHeightPerLine, final String[] pLines) { this.mMaxWidth = pMaxWidth; this.mHeightPerLine = pHeightPerLine; this.mTotalHeight = pHeightPerLine * pLines.length; this.mLines = pLines; } } private static class Component { private final String mText; private final String mTag; private final int mPosition; private final Vector<String> mFontStyle = new Vector<String>(); public Component(final String pText,final String pTag, final int pPosition, final Vector<String> pFontStyle){ this.mText = pText; this.mTag = pTag; this.mPosition = pPosition; if(pFontStyle.size()>0){ this.mFontStyle.addAll(pFontStyle); } } } private static class TextInfo{ public int lineIndex; public Paint paint; public final String mText; public int w,h; public int x; public TextInfo(final int li,final String pText,final int pw,final int ph,final int px,final Paint p){ this.lineIndex = li; this.mText = pText; this.w = pw; this.h = ph; this.x = px; this.paint = p; } } }
ok ,稍微注意一点里面的currentTop,因为实际上研究过TTF的童鞋都知道,对于TrueType或者FreeType的字体来说有很多位置属性,这些位置属性来控制一个字体的绘制。比如AaJjGg对齐的基线都是不一样的,具体去google TTF字体吧。这里的currentTop就是保存每行的绘制Y点。而TotalH则用来保存每行的Height,2者是有区别的。
3、
我这个代码是基于 cocos2d-2.1rc0-x-2.1.2,如果您的版本和我使用的有差别“你他妈来打我啊”;
另外一个操蛋的地方是!!! cocos2d-x的bug,发现3.0预览版还存在。。。
cocos2d-2.1rc0-x-2.1.2/cocos2dx/platform/android/CCImage.cpp 文件中
void Java_org_cocos2dx_lib_Cocos2dxBitmap_nativeInitBitmapDC(JNIEnv* env, jobject thiz, int width, int height, jbyteArray pixels) { int size = width * height * 4; cocos2d::sharedBitmapDC().m_nWidth = width; cocos2d::sharedBitmapDC().m_nHeight = height; cocos2d::sharedBitmapDC().m_pData = new unsigned char[size]; env->GetByteArrayRegion(pixels, 0, size, (jbyte*)cocos2d::sharedBitmapDC().m_pData); return;//下面的代码实现的是所谓的将ARGB转换成RGBA,但,这个步骤是错误的!!!得到的颜色空间是错位了,直接跳过即可。 // swap data unsigned int *tempPtr = (unsigned int*)cocos2d::sharedBitmapDC().m_pData; unsigned int tempdata = 0; for (int i = 0; i < height; ++i) { for (int j = 0; j < width; ++j) { tempdata = *tempPtr; *tempPtr++ = cocos2d::sharedBitmapDC().swapAlpha(tempdata); } } }
具体现象就是无论怎么设置,颜色就是显示不正常,设置Paint.SetColor(Color.Black),居然显示成红色。
通过把一模一样的BitMap直接在Android里面显示,发现Android层面是正确的。那么只可能是Cocos2dx端的问题。
分析 黑色FF 00 00 00 显示成红色 FF FF 00 00 ,前面的FF是Alpha。说明 >>8 了,那么在Java层面把Paint.setColor(Color.BLACK << 8);进行测试,发现显示正常。说明猜测的正确。又试验了几个发现果然猜测正确,这也充分说明了,小时候不看书,直接带只筛子(自制)去考场也能门门高分的天赋~~
4、说了那么多废话饿了。回家吃饭。代码就不上传了吧,就2个文件改动而已,比iOS简单多了。
有需要工程的童鞋留言吧。
5 、 另外,代码就实现了<a > <font>的样式而已,回调函数没有写 :囧。这个这个这个。。。。。是因为。。。。。懒!