Last active
November 11, 2015 09:25
-
-
Save Kane-Shih/c056482faf82718a5692 to your computer and use it in GitHub Desktop.
A View contains both text and image. Note: 1. Relies on UIL; 2. Needs to improve: use invalidate(Rect) and canvas.getClipBounds(), auto line break, WRAP_CONTENT
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package tw.kaneshih.view; | |
import android.content.Context; | |
import android.graphics.Bitmap; | |
import android.graphics.BitmapFactory; | |
import android.graphics.Canvas; | |
import android.graphics.Color; | |
import android.graphics.Paint; | |
import android.graphics.RectF; | |
import android.graphics.Typeface; | |
import android.support.annotation.CallSuper; | |
import android.support.annotation.ColorInt; | |
import android.support.annotation.DrawableRes; | |
import android.util.AttributeSet; | |
import android.util.Log; | |
import android.util.TypedValue; | |
import android.view.MotionEvent; | |
import android.view.View; | |
import com.nostra13.universalimageloader.core.ImageLoader; | |
import com.nostra13.universalimageloader.core.assist.FailReason; | |
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener; | |
import java.lang.ref.WeakReference; | |
import java.util.ArrayList; | |
import java.util.List; | |
/** | |
* Created by kane on 2015/11/4. | |
* | |
* Usage example: | |
* TextImageView textImageView = (TextImageView) findViewById(R.id.content_view); | |
* textImageView.setSupportContentClick(true); | |
* List<TextImageView.Content> contentList = new ArrayList<>(); | |
* contentList.add(new TextImageView.Text(textImageView, "Some text ...")); | |
* contentList.add(new TextImageView.Image(textImageView, BitmapFactory.decodeResource(getResources(), R.drawable.some_image))); | |
* contentList.add(textImageView.getNewLine()); | |
* contentList.add(new TextImageView.Image(textImageView, "http://somewhere/some_remote_image.png").setOnClickListener(new View.OnClickListener() { | |
* @Override | |
* public void onClick(View v) { | |
* // on remote image clicked | |
* } | |
* })); | |
* contentList.add(new TextImageView.Text(textImageView, "Click on me", 72.f, Color.GREEN, true, true).setOnClickListener(new View.OnClickListener() { | |
* @Override | |
* public void onClick(View v) { | |
* // big text clicked | |
* } | |
* })); | |
* textImageView.setContentList(contentList); | |
*/ | |
public final class TextImageView extends View { | |
private static final String TAG = "TextImageView"; | |
public static final int DEFAULT_TEXT_COLOR = Color.BLACK; | |
public static final float DEFAULT_TEXT_SIZE_IN_SP = 12.f; | |
public static final float DEFAULT_LINE_SPACING_IN_SP = 4.f; | |
public static final float DEFAULT_IMAGE_SPACING_HORIZONTAL_IN_DIP = 6.f; | |
public static final float DEFAULT_EMPTY_IMAGE_RECT_LENGTH_IN_DIP = 12.f; | |
public static final int DEFAULT_ONCLICK_COLOR = Color.argb(50, 50, 50, 255); | |
private static final int DEBUG_TOP_LINE_COLOR = Color.RED; | |
private static final int DEBUG_BOTTOM_LINE_COLOR = Color.GREEN; | |
private static final int DEBUG_BASE_LINE_COLOR = Color.BLUE; | |
private Paint paint; | |
private float fontHeight; | |
private float fontTop; | |
private float fontBottom; | |
private List<Content> content; | |
private float lineSpacing; | |
private float imageSpacingHorizontal; | |
private float emptyImageRectLength; | |
private boolean isSupportContentClick = false; | |
private Content touchDownContent = null; | |
private RectF touchedRect = null; | |
private Paint touchPaint; | |
private int onClickColor = DEFAULT_ONCLICK_COLOR; | |
private boolean isDebug = false; | |
private Paint debugPaint = null; | |
private final NewLine newLine = new NewLine(this); | |
public TextImageView(Context context) { | |
super(context); | |
init(); | |
} | |
public TextImageView(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
init(); | |
} | |
public TextImageView(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
init(); | |
} | |
private void init() { | |
paint = new Paint(); | |
paint.setColor(DEFAULT_TEXT_COLOR); | |
paint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, DEFAULT_TEXT_SIZE_IN_SP, getResources().getDisplayMetrics())); | |
updateFontVariables(); | |
lineSpacing = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, DEFAULT_LINE_SPACING_IN_SP, getResources().getDisplayMetrics()); | |
imageSpacingHorizontal = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_IMAGE_SPACING_HORIZONTAL_IN_DIP, getResources().getDisplayMetrics()); | |
emptyImageRectLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_EMPTY_IMAGE_RECT_LENGTH_IN_DIP, getResources().getDisplayMetrics()); | |
touchPaint = new Paint(); | |
touchPaint.setColor(onClickColor); | |
} | |
private void updateFontVariables() { | |
Paint.FontMetrics fontMetrics = paint.getFontMetrics(); | |
fontBottom = fontMetrics.bottom; | |
fontTop = fontMetrics.top; | |
fontHeight = fontBottom - fontTop; | |
} | |
/** | |
* @param isDebug if true, show logcat and draw lines(line top/line bottom/default text base) in onDraw | |
*/ | |
public void enableDebug(boolean isDebug) { | |
if (!isDebug) { | |
debugPaint = null; | |
} else { | |
debugPaint = new Paint(); | |
} | |
this.isDebug = isDebug; | |
} | |
/** | |
* Default text size is ({@link #DEFAULT_TEXT_SIZE_IN_SP}) | |
*/ | |
public void setTextSize(int textSizeInPx) { | |
paint.setTextSize(textSizeInPx); | |
updateFontVariables(); | |
reDrawIfNeeded(); | |
} | |
/** | |
* Default color is {@link #DEFAULT_TEXT_COLOR} | |
*/ | |
public void setTextColor(@ColorInt int color) { | |
paint.setColor(color); | |
updateFontVariables(); | |
reDrawIfNeeded(); | |
} | |
public void setTextStyle(Typeface typeface) { | |
paint.setTypeface(typeface); | |
updateFontVariables(); | |
reDrawIfNeeded(); | |
} | |
private void reDrawIfNeeded() { | |
if (content != null && !content.isEmpty()) { | |
invalidate(); | |
} | |
} | |
/** | |
* Default is {@link #DEFAULT_LINE_SPACING_IN_SP} | |
* | |
* @param lineSpacing unit is px | |
*/ | |
public void setLineSpacing(float lineSpacing) { | |
this.lineSpacing = lineSpacing; | |
} | |
/** | |
* Default is {@link #DEFAULT_IMAGE_SPACING_HORIZONTAL_IN_DIP} | |
* | |
* @param imageSpacingHorizontal unit is px | |
*/ | |
public void setImageSpacingHorizontal(float imageSpacingHorizontal) { | |
this.imageSpacingHorizontal = imageSpacingHorizontal; | |
} | |
/** | |
* View.onClickListener in each Content works if set to true, default is false. | |
* | |
* @param isSupport | |
*/ | |
public void setSupportContentClick(boolean isSupport) { | |
boolean needInvalidate = !isSupportContentClick && isSupport; | |
isSupportContentClick = isSupport; | |
setClickable(isSupport); | |
if (isSupport) { | |
touchedRect = new RectF(); | |
} else { | |
touchedRect = null; | |
} | |
if (needInvalidate) { | |
invalidate(); | |
} | |
} | |
public boolean isSupportContentClick() { | |
return isSupportContentClick; | |
} | |
public void setContentOnClickColor(@ColorInt int color) { | |
onClickColor = color; | |
} | |
/** | |
* @param content We'll use this instance, and we'll clear these Content (including recycling bitmaps, clearing this list) when detached from window | |
*/ | |
public void setContentList(List<Content> content) { | |
clearContent(); | |
this.content = content; | |
invalidate(); | |
} | |
/** | |
* Clear current Content, then set Content according to text(\n and \r\n will be changed to NewLineContent) | |
* | |
* @param text | |
*/ | |
public void setText(String text) { | |
clearContent(); | |
appendText(text); | |
} | |
/** | |
* append Content accoring to text(\n and \r\n will be changed to NewLineContent) | |
* | |
* @param text | |
*/ | |
public void appendText(String text) { | |
if (content == null) { | |
content = new ArrayList<>(); | |
} | |
if (text != null && !text.isEmpty()) { | |
addTextToContentList(text); | |
} | |
} | |
private void addTextToContentList(String text) { | |
String[] lines = text.split("\\r?\\n"); | |
for (int i = 0; i < lines.length; i++) { | |
content.add(new Text(this, lines[i])); | |
if (i != lines.length - 1) { | |
content.add(newLine); | |
} | |
} | |
if (text.endsWith("\n") || text.endsWith("\r\n")) { | |
content.add(newLine); | |
} | |
} | |
public NewLine getNewLine() { | |
return newLine; | |
} | |
/** | |
* Clear Content (recycle bitmaps and clear list) | |
*/ | |
public void clear() { | |
clearContent(); | |
invalidate(); | |
} | |
private void clearContent() { | |
touchDownContent = null; | |
if (touchedRect != null) { | |
touchedRect.setEmpty(); | |
} | |
if (content != null && !content.isEmpty()) { | |
for (Content c : content) { | |
c.notifyOnDetached(); | |
} | |
content.clear(); | |
} | |
content = null; | |
} | |
public static abstract class Content { | |
protected TextImageView textImageView; | |
private View.OnClickListener onClickListener; | |
float left; | |
float right; | |
float top; | |
float bottom; | |
Content(TextImageView textImageView) { | |
this.textImageView = textImageView; | |
} | |
@CallSuper | |
protected void notifyOnDetached() { | |
textImageView = null; | |
onClickListener = null; | |
} | |
public final Content setOnClickListener(View.OnClickListener onClickListener) { | |
this.onClickListener = onClickListener; | |
return this; | |
} | |
boolean isInside(float x, float y) { | |
return textImageView != null && textImageView.isSupportContentClick() | |
&& onClickListener != null | |
&& (bottom > top && right > left) | |
&& (x >= left && x <= right && y >= top && y <= bottom); | |
} | |
protected void setRect(float l, float r, float t, float b) { | |
left = l; | |
right = r; | |
top = t; | |
bottom = b; | |
} | |
public final void fillRectF(RectF rect) { | |
rect.set(left, top, right, bottom); | |
} | |
public final void drawRect(Canvas canvas, Paint paint) { | |
canvas.drawRect(left, top, right, bottom, paint); | |
} | |
protected abstract float getHeight(); | |
protected abstract float getWidth(); | |
protected abstract void draw(Canvas canvas, Paint paint, float x, float top, float bottom, float base, float xBoundaryStart, float xBoundaryEnd, float yBoundaryEnd); | |
protected abstract float getNextX(); | |
protected abstract float getNextY(); | |
} | |
/** | |
* Use NewLineContent instead of \n | |
*/ | |
public static final class Text extends Content { | |
private String text; | |
private Paint styledPaint; | |
private float styledDescent; | |
private float height; | |
private float width; | |
public Text(TextImageView textImageView, String text) { | |
super(textImageView); | |
this.text = text; | |
measure(); | |
} | |
public Text(TextImageView textImageView, String text, boolean isBold, boolean isItalic) { | |
super(textImageView); | |
this.text = text; | |
setupStyle(textImageView.paint.getTextSize(), textImageView.paint.getColor(), isBold, isItalic); | |
measure(); | |
} | |
public Text(TextImageView textImageView, String text, float textSizeInPixel, @ColorInt int rgbColor, boolean isBold, boolean isItalic) { | |
super(textImageView); | |
this.text = text; | |
setupStyle(textSizeInPixel, rgbColor, isBold, isItalic); | |
measure(); | |
} | |
private void setupStyle(float textSize, int color, boolean isBold, boolean isItalic) { | |
styledPaint = new Paint(); | |
styledPaint.setTextSize(textSize); | |
styledPaint.setColor(color); | |
if (isBold && isItalic) { | |
styledPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC)); | |
} else if (isBold) { | |
styledPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); | |
} else if (isItalic) { | |
styledPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)); | |
} | |
styledDescent = styledPaint.getFontMetrics().descent; | |
} | |
private void measure() { | |
if (styledPaint != null) { | |
Paint.FontMetrics fm = styledPaint.getFontMetrics(); | |
height = fm.bottom - fm.top; | |
} else { | |
height = textImageView.fontHeight; | |
} | |
if (styledPaint != null) { | |
width = styledPaint.measureText(text); | |
} else { | |
width = textImageView.paint.measureText(text); | |
} | |
} | |
@Override | |
protected void notifyOnDetached() { | |
super.notifyOnDetached(); | |
text = null; | |
} | |
@Override | |
protected float getHeight() { | |
return height; | |
} | |
@Override | |
protected float getWidth() { | |
return width; | |
} | |
@Override | |
protected void draw(Canvas canvas, Paint paint, float x, float top, float bottom, float base, float xBoundaryStart, float xBoundaryEnd, float yBoundaryEnd) { | |
if (styledPaint != null) { | |
canvas.drawText(text, x, bottom - styledDescent, styledPaint); | |
} else { | |
canvas.drawText(text, x, base, paint); | |
} | |
setRect(x, x + width, bottom - height, bottom); | |
} | |
@Override | |
protected float getNextX() { | |
return 0; | |
} | |
@Override | |
protected float getNextY() { | |
return 0; | |
} | |
} | |
public static final class NewLine extends Content { | |
private float height; | |
private NewLine(TextImageView textImageView) { | |
super(textImageView); | |
height = textImageView.fontHeight; | |
} | |
@Override | |
protected float getHeight() { | |
return height; | |
} | |
@Override | |
protected float getWidth() { | |
return 0; | |
} | |
@Override | |
protected void draw(Canvas canvas, Paint paint, float x, float top, float bottom, float base, float xBoundaryStart, float xBoundaryEnd, float yBoundaryEnd) { | |
} | |
@Override | |
protected float getNextX() { | |
return 0; | |
} | |
@Override | |
protected float getNextY() { | |
return 0; | |
} | |
} | |
public static final class Image extends Content { | |
private Bitmap bitmap; | |
private Listener listener; | |
private float width; | |
private float height; | |
private static class Listener implements ImageLoadingListener { | |
private WeakReference<Image> contentRef; | |
private Listener(Image content) { | |
this.contentRef = new WeakReference<>(content); | |
} | |
@Override | |
public void onLoadingStarted(String imageUri, View view) { | |
} | |
@Override | |
public void onLoadingFailed(String imageUri, View view, FailReason failReason) { | |
} | |
@Override | |
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) { | |
Image image = contentRef.get(); | |
if (image != null) { | |
image.notifyLoadingComplete(loadedImage); | |
} | |
} | |
@Override | |
public void onLoadingCancelled(String imageUri, View view) { | |
} | |
} | |
public Image(TextImageView textImageView, Bitmap bitmap) { | |
super(textImageView); | |
this.bitmap = bitmap; | |
measure(); | |
} | |
public Image(TextImageView textImageView, @DrawableRes int drawableResId) { | |
super(textImageView); | |
this.bitmap = BitmapFactory.decodeResource(textImageView.getResources(), drawableResId); | |
measure(); | |
} | |
public Image(TextImageView textImageView, String url) { | |
super(textImageView); | |
listener = new Listener(this); | |
ImageLoader.getInstance().loadImage(url, listener); | |
measure(); | |
} | |
private void measure() { | |
if (bitmap != null && !bitmap.isRecycled()) { | |
width = bitmap.getWidth() + textImageView.imageSpacingHorizontal; | |
height = bitmap.getHeight(); | |
} else { | |
width = textImageView.emptyImageRectLength; | |
height = width; | |
} | |
} | |
private void notifyLoadingComplete(Bitmap loadedImage) { | |
bitmap = loadedImage; | |
measure(); | |
textImageView.invalidate(); | |
} | |
@Override | |
protected void notifyOnDetached() { | |
super.notifyOnDetached(); | |
if (bitmap != null && !bitmap.isRecycled()) { | |
bitmap.recycle(); | |
bitmap = null; | |
} | |
listener = null; | |
} | |
@Override | |
protected float getHeight() { | |
return height; | |
} | |
@Override | |
protected float getWidth() { | |
return width; | |
} | |
@Override | |
protected void draw(Canvas canvas, Paint paint, float x, float top, float bottom, float base, float xBoundaryStart, float xBoundaryEnd, float yBoundaryEnd) { | |
x += textImageView.imageSpacingHorizontal / 2.f; | |
if (bitmap != null && !bitmap.isRecycled()) { | |
canvas.drawBitmap(bitmap, x, bottom - height, paint); | |
} | |
setRect(x, x + width - textImageView.imageSpacingHorizontal, bottom - height, bottom); | |
} | |
@Override | |
protected float getNextX() { | |
return 0; | |
} | |
@Override | |
protected float getNextY() { | |
return 0; | |
} | |
} | |
@Override | |
protected void onDraw(Canvas canvas) { | |
long t0 = System.nanoTime(); | |
if (isDebug) { | |
Log.d(TAG, "onDraw START " + (content != null ? "content size: " + content.size() : "content null")); | |
} | |
if (content != null && !content.isEmpty()) { | |
float xBoundaryStart = getPaddingLeft(); | |
float xBoundaryEnd = canvas.getWidth() - getPaddingRight(); | |
float yBoundaryStart = getPaddingTop(); | |
float yBoundaryEnd = canvas.getHeight() - getPaddingBottom(); | |
float x = xBoundaryStart; | |
float defaultTextBase = 0; | |
float lineTop = yBoundaryStart; | |
float lineBottom = 0; | |
float lineHeight = fontHeight; | |
int startContentIndexOfLine = -1; | |
int lineCount = 1; | |
Content c; | |
for (int i = 0; i < content.size(); i++) { | |
c = content.get(i); | |
if (startContentIndexOfLine == -1) { | |
startContentIndexOfLine = i; | |
} | |
lineHeight = Math.max(lineHeight, c.getHeight()); | |
if (c.getClass() == NewLine.class || i == content.size() - 1) { | |
if (lineCount > 1) { | |
lineTop = lineBottom + lineSpacing; | |
lineBottom += lineHeight + lineSpacing; | |
defaultTextBase += lineHeight + lineSpacing; | |
} else { | |
lineBottom = yBoundaryStart + lineHeight; | |
defaultTextBase = lineBottom - fontBottom; | |
} | |
if (isDebug) { | |
drawLineForLine(canvas, lineTop, defaultTextBase, lineBottom); | |
} | |
for (int j = startContentIndexOfLine; j <= i; j++) { | |
c = content.get(j); | |
c.draw(canvas, paint, x, lineTop, lineBottom, defaultTextBase, xBoundaryStart, xBoundaryEnd, yBoundaryEnd); | |
x += c.getWidth(); | |
if (isDebug) { | |
debugPaint.setColor(Color.argb(50, 50, 255, 50)); | |
c.drawRect(canvas, debugPaint); | |
} | |
} | |
x = xBoundaryStart; | |
lineHeight = fontHeight; | |
startContentIndexOfLine = -1; | |
lineCount++; | |
} | |
} | |
if (touchedRect != null && !touchedRect.isEmpty()) { | |
canvas.drawRect(touchedRect, touchPaint); | |
} | |
} | |
if (isDebug) { | |
Log.d(TAG, "onDraw END, spent(nanoseconds) " + (System.nanoTime() - t0)); | |
} | |
} | |
private void drawLineForLine(Canvas canvas, float top, float base, float bottom) { | |
debugPaint.setColor(DEBUG_TOP_LINE_COLOR); | |
canvas.drawLine(0, top, 3000, top, debugPaint); | |
debugPaint.setColor(DEBUG_BASE_LINE_COLOR); | |
canvas.drawLine(0, base, 3000, base, debugPaint); | |
debugPaint.setColor(DEBUG_BOTTOM_LINE_COLOR); | |
canvas.drawLine(0, bottom, 3000, bottom, debugPaint); | |
} | |
@Override | |
protected void onDetachedFromWindow() { | |
clearContent(); | |
super.onDetachedFromWindow(); | |
} | |
@Override | |
public boolean onTouchEvent(MotionEvent event) { | |
if (isSupportContentClick && content != null && !content.isEmpty()) { | |
switch (event.getAction()) { | |
case MotionEvent.ACTION_DOWN: | |
for (Content c : content) { | |
if (c.isInside(event.getX(), event.getY())) { | |
c.fillRectF(touchedRect); | |
touchPaint.setColor(onClickColor); | |
invalidate(); | |
touchDownContent = c; | |
return true; | |
} | |
} | |
touchedRect.setEmpty(); | |
touchDownContent = null; | |
break; | |
case MotionEvent.ACTION_UP: | |
if (touchDownContent != null && touchDownContent.isInside(event.getX(), event.getY())) { | |
touchDownContent.onClickListener.onClick(this); | |
touchedRect.setEmpty(); | |
touchPaint.setColor(Color.TRANSPARENT); | |
touchDownContent = null; | |
invalidate(); | |
return true; | |
} | |
break; | |
} | |
} | |
return super.onTouchEvent(event); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment