package com.antonionicolaspina.textimageview; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.widget.ImageView; import java.util.ArrayList; public class TextImageView extends ImageView implements ScaleGestureDetector.OnScaleGestureListener, RotationGestureDetector.OnRotationGestureListener { public interface OnTextMovedListener { void textMoved(PointF position); } protected static class TextProperties { private String text; public float scale; public float size; public String[] textLines; public Paint paint; public ArrayList<Rect> textRects; public PointF textPosition; public PointF rotationCenter; public float rotation; public TextProperties(String text, float size, int color) { this.scale = 1f; this.size = size; this.paint = new Paint(Paint.ANTI_ALIAS_FLAG); this.textRects = new ArrayList<>(); this.textPosition = new PointF(0f, 0f); this.rotationCenter = new PointF(); this.rotation = 0f; paint.setColor(color); paint.setTextSize(size); setText(text); } public void setText(String text) { this.text = text; this.textLines = null; if (null != text) { this.textLines = text.split("\n"); textRects.clear(); for(int i=0; i<textLines.length; i++) { Rect r = new Rect(); paint.getTextBounds(textLines[i], 0, textLines[i].length(), r); textRects.add(i, r); } } } public String getText() { return text; } } public enum ClampMode {UNLIMITED, ORIGIN_INSIDE, TEXT_INSIDE} private ScaleGestureDetector scaleDetector; private RotationGestureDetector rotateDetector; // region Global parameters private float minSize; private float maxSize; private boolean panEnabled; private boolean scaleEnabled; private boolean rotationEnabled; private ClampMode clampTextMode; private int interline; private RectF imageRect; // endregion // region Other members private PointF focalPoint; private OnTextMovedListener onTextMovedListener; private float previousRotation = 0f; private ArrayList<TextProperties> texts; private int currentSize; private int currentColor; // endregion public TextImageView(Context context) { super(context); init(context, null); } public TextImageView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); } public TextImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public TextImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs); } protected void init(Context context, AttributeSet attributeSet) { texts = new ArrayList<>(); imageRect = new RectF(); focalPoint = new PointF(); Resources resources = context.getResources(); if (null != attributeSet) { TypedArray attrs = context.getTheme().obtainStyledAttributes(attributeSet, R.styleable.TextImageView, 0, 0); currentSize = attrs.getDimensionPixelSize(R.styleable.TextImageView_android_textSize, resources.getDimensionPixelSize(R.dimen.default_text_size)); currentColor = attrs.getColor(R.styleable.TextImageView_android_textColor, Color.BLACK); panEnabled = attrs.getBoolean(R.styleable.TextImageView_tiv_panEnabled, false); scaleEnabled = attrs.getBoolean(R.styleable.TextImageView_tiv_scaleEnabled, false); rotationEnabled = attrs.getBoolean(R.styleable.TextImageView_tiv_rotationEnabled, false); interline = attrs.getDimensionPixelOffset(R.styleable.TextImageView_tiv_interline, 0); clampTextMode = ClampMode.values()[attrs.getInt(R.styleable.TextImageView_tiv_clampTextMode, 0)]; setText(attrs.getString(R.styleable.TextImageView_android_text)); minSize = attrs.getDimensionPixelSize(R.styleable.TextImageView_tiv_minTextSize, resources.getDimensionPixelSize(R.dimen.default_min_text_size)); maxSize = attrs.getDimensionPixelSize(R.styleable.TextImageView_tiv_maxTextSize, resources.getDimensionPixelSize(R.dimen.default_max_text_size)); attrs.recycle(); } scaleDetector = new ScaleGestureDetector(context, this); rotateDetector = new RotationGestureDetector(this); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if ( isInEditMode() && (0 == texts.size()) ) { setText("sample text"); } // Get rectangle of the drawable imageRect.top = 0; imageRect.left = 0; Drawable drawable = getDrawable(); if (null != drawable) { imageRect.right = drawable.getIntrinsicWidth(); imageRect.bottom = drawable.getIntrinsicHeight(); } // Translate and scale the rectangle getImageMatrix().mapRect(imageRect); for(TextProperties tp: texts) { canvas.save(); if (rotationEnabled) { canvas.rotate(-tp.rotation, tp.rotationCenter.x, tp.rotationCenter.y); } // Draw text float top = tp.textPosition.y + imageRect.top; for (int i = 0; i < tp.textLines.length; i++) { int h = tp.textRects.get(i).height(); canvas.save(); canvas.translate(tp.textPosition.x + imageRect.left, top + h); canvas.drawText(tp.textLines[i], 0, 0, tp.paint); canvas.restore(); top += h + interline * tp.scale; } canvas.restore(); } } protected void recalculateFocalPoint(MotionEvent event) { final int pointerCount = event.getPointerCount(); if (pointerCount <= 0) { return; } focalPoint.x = 0f; focalPoint.y = 0f; for(int i=0; i<pointerCount; i++) { focalPoint.x += event.getX(i); focalPoint.y += event.getY(i); } focalPoint.x /= pointerCount; focalPoint.y /= pointerCount; } protected static float between(float value, float min, float max) { return Math.max(Math.min(value, max), min); } @Override public boolean onTouchEvent(MotionEvent event) { scaleDetector.onTouchEvent(event); rotateDetector.onTouchEvent(event); super.onTouchEvent(event); final int action = event.getAction(); switch(action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: recalculateFocalPoint(event); return true; case MotionEvent.ACTION_MOVE: if (0 < texts.size()) { final float x = focalPoint.x; final float y = focalPoint.y; TextProperties tp = texts.get(texts.size() - 1); recalculateFocalPoint(event); if (panEnabled) { tp.textPosition.x += focalPoint.x - x; tp.textPosition.y += focalPoint.y - y; tp.rotationCenter.x += focalPoint.x - x; tp.rotationCenter.y += focalPoint.y - y; reclampText(); invalidate(); } } return true; } return false; } protected void reclampText() { if (0 == texts.size()) { return; } TextProperties tp = texts.get(texts.size()-1); switch (clampTextMode) { case UNLIMITED: break; case ORIGIN_INSIDE: { RectF enclosingRect = calculateEnclosingRect(); enclosingRect.offset(-imageRect.left, -imageRect.top); tp.textPosition.x -= enclosingRect.left-between(enclosingRect.left, 0, imageRect.width()); tp.textPosition.y -= enclosingRect.top-between(enclosingRect.top, 0, imageRect.height()); invalidate(); break; } case TEXT_INSIDE: { RectF enclosingRect = calculateEnclosingRect(); enclosingRect.offset(-imageRect.left, -imageRect.top); tp.textPosition.x -= enclosingRect.left - between(enclosingRect.left, 0, imageRect.width()-enclosingRect.width()); tp.textPosition.y -= enclosingRect.top - between(enclosingRect.top, 0, imageRect.height()-enclosingRect.height()); invalidate(); break; } } if (null != onTextMovedListener) { PointF position = getTextPosition(); if ( (!Float.isNaN(position.x)) && (!Float.isNaN(position.y)) ) { onTextMovedListener.textMoved(position); } } } protected RectF calculateEnclosingRect() { if (0 == texts.size()) { return null; } TextProperties tp = texts.get(texts.size()-1); Matrix mat = new Matrix(); RectF globalRect = new RectF(); float top = tp.textPosition.y; for(int i=0; i<tp.textLines.length; i++) { int h = tp.textRects.get(i).height(); RectF rect = new RectF(0, 0, tp.textRects.get(i).width(), h); rect.offset(imageRect.left, imageRect.top); mat.reset(); mat.preRotate(-tp.rotation, tp.rotationCenter.x, tp.rotationCenter.y); mat.preTranslate(tp.textPosition.x, top); mat.mapRect(rect); if (0 == i) { globalRect.set(rect); } else { globalRect.top = Math.min(globalRect.top, rect.top); globalRect.left = Math.min(globalRect.left, rect.left); globalRect.bottom = Math.max(globalRect.bottom, rect.bottom); globalRect.right = Math.max(globalRect.right, rect.right); } top += h + interline*tp.scale; } return globalRect; } @Override public boolean onScale(ScaleGestureDetector scaleGestureDetector) { if (scaleEnabled && (0<texts.size())) { TextProperties tp = texts.get(texts.size()-1); tp.scale *= scaleGestureDetector.getScaleFactor(); tp.paint.setTextSize(Math.max(minSize, Math.min(tp.scale * tp.size, maxSize))); tp.scale = tp.paint.getTextSize() / tp.size; tp.setText(tp.text); reclampText(); invalidate(); } return true; } @Override public boolean onScaleBegin(ScaleGestureDetector scaleGestureDetector) { return true; } @Override public void onScaleEnd(ScaleGestureDetector scaleGestureDetector) { } @Override public void OnRotation(RotationGestureDetector rotationDetector) { if (rotationEnabled && (0<texts.size())) { TextProperties tp = texts.get(texts.size()-1); tp.rotation += rotationDetector.getAngle() - previousRotation; previousRotation = rotationDetector.getAngle(); tp.rotationCenter.x = focalPoint.x; tp.rotationCenter.y = focalPoint.y; invalidate(); } } /************** *** Public *** **************/ /** * Set text to be drawn over the image. * @param text The text. */ public void setText(String text) { texts.clear(); addText(text); } /** * Adds a text to be drawn over the image, above existing texts. * @param text The text. */ public void addText(String text) { if (null != text) { texts.add(new TextProperties(text, currentSize, currentColor)); reclampText(); invalidate(); } } /** * Removes the text on the top of the stack. */ public void removeText() { if (0 < texts.size()) { texts.remove(texts.size()-1); invalidate(); } } /** * Set the typeface to use for the text. * @param typeface The typeface to be used. */ public void setTypeface(Typeface typeface) { TextProperties tp = texts.get(texts.size()-1); tp.paint.setTypeface(typeface); reclampText(); invalidate(); } /** * Enable or disable user-pan for the view. * @param enabled Whether panning should be enabled. */ public void setPanEnabled(boolean enabled) { panEnabled = enabled; } /** * Enable or disable user-scaling for the view. * @param enabled Whether scaling should be enabled. */ public void setScaleEnabled(boolean enabled) { scaleEnabled = enabled; } /** * Enable or disable user-rotation for the view. * @param enabled Whether rotation should be enabled. */ public void setRotationEnabled(boolean enabled) { rotationEnabled = enabled; } /** * Set the text color. * @param color Color in the format of <a href="http://developer.android.com/reference/android/graphics/Color.html">android.graphics.Color</a>. * * @see <a href="http://developer.android.com/reference/android/graphics/Color.html">android.graphics.Color</a> */ public void setTextColor(int color) { TextProperties tp = texts.get(texts.size()-1); tp.paint.setColor(color); invalidate(); } /** * Set the default text size to the given value, interpreted as "scaled pixel" units. * This size is adjusted based on the current density and user font size preference. * @param textSize The scaled pixel size. */ public void setTextSize(float textSize) { TextProperties tp = texts.get(texts.size()-1); tp.scale = 1f; tp.size = textSize; tp.paint.setTextSize(textSize); tp.setText(tp.text); reclampText(); invalidate(); } /** * Return offset position between the text and the image. Considers both top left corners to the the calculation. * @return Pointf containing x and y offsets, as a per-one value. Eg. (0,0)=top-left, (1,1)=bottom-right. */ public PointF getTextPosition() { RectF enclosingRect = calculateEnclosingRect(); enclosingRect.offset(-imageRect.left, -imageRect.top); return new PointF(enclosingRect.left / imageRect.width(), enclosingRect.top / imageRect.height()); } /** * Set the listener to be fired when the text changes its location. * @param listener the listener to be called, or null. */ public void setOnTextMovedListener(OnTextMovedListener listener) { this.onTextMovedListener = listener; } /** * Get the relative size between the image and the text. * @return Relative size. Eg. 0.5=text half the height of the image. */ public float getTextRelativeSize() { TextProperties tp = texts.get(texts.size()-1); return tp.paint.getTextSize() / imageRect.height(); } /** * Get the text rotation. * @return Rotation angle. */ public float getTextRotation() { TextProperties tp = texts.get(texts.size()-1); return tp.rotation; } /** * Return rotation center for the text on top. * @return Pointf containing x and y offsets, as a per-one value. Eg. (0,0)=top-left, (1,1)=bottom-right. */ public PointF getTextRelativeRotationCenter() { TextProperties tp = texts.get(texts.size()-1); return new PointF(tp.rotationCenter.x / imageRect.width(), tp.rotationCenter.y / imageRect.height()); } }