Skip to content

Instantly share code, notes, and snippets.

@Kane-Shih
Last active November 11, 2015 09:25
Show Gist options
  • Save Kane-Shih/c056482faf82718a5692 to your computer and use it in GitHub Desktop.
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
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