package org.goodev.discourse.widget;
//package com.pagesuite.flowtext;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.BoringLayout;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
public class FlowTextView extends RelativeLayout {
private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
private final float mSpacingMult = 1.0f;
private final float mSpacingAdd = 0.0f;
private final ArrayList<Box> mLineboxes = new ArrayList<FlowTextView.Box>();
private final ArrayList<Area> mAreas = new ArrayList<FlowTextView.Area>();
private final ArrayList<Box> boxes = new ArrayList<FlowTextView.Box>();
private final ArrayList<TextPaint> mPaintHeap = new ArrayList<TextPaint>();
private final ArrayList<HtmlLink> mLinks = new ArrayList<FlowTextView.HtmlLink>();
Area mLargestArea;
boolean needsMeasure = true;
int charFlagSize = 0;
int charFlagIndex = 0;
int spanStart = 0;
int spanEnd = 0;
int charCounter;
float objPixelwidth;
HashMap<Integer, HtmlObject> sorterMap = new HashMap<Integer, FlowTextView.HtmlObject>();
float tempFloat;
Object[] sorterKeys;
int[] sortedKeys;
String tempString;
int temp1;
int temp2;
int arrayIndex = 0;
int mTextLength = 0;
private int mColor = Color.BLACK;
private int pageHeight = 0;
private TextPaint mTextPaint;
private TextPaint mLinkPaint;
private int mTextsize = 20;
private Typeface typeFace;
private int mDesiredHeight = 100; // height of the whole view
private float mViewWidth;
private OnLinkClickListener mOnLinkClickListener;
private CharSequence mText = "";
private boolean mIsHtml = false;
private boolean[] charFlags;
private Spannable mSpannable;
private ArrayList<BitmapSpec> bitmaps = new ArrayList<FlowTextView.BitmapSpec>();
public FlowTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
public FlowTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public FlowTextView(Context context) {
super(context);
init(context);
}
// private URLSpan[] urls;
private static double getPointDistance(float x1, float y1, float x2, float y2) {
double dist = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
return dist;
}
public void setColor(int color) {
this.mColor = color;
if (mTextPaint != null) {
mTextPaint.setColor(mColor);
}
for (TextPaint paint : mPaintHeap) {
paint.setColor(mColor);
}
this.invalidate();
}
private void init(Context context) {
mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mTextPaint.density = getResources().getDisplayMetrics().density;
mTextPaint.setTextSize(mTextsize);
mTextPaint.setColor(Color.BLACK);
mLinkPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
mLinkPaint.density = getResources().getDisplayMetrics().density;
mLinkPaint.setTextSize(mTextsize);
mLinkPaint.setColor(Color.BLUE);
mLinkPaint.setUnderlineText(true);
this.setBackgroundColor(Color.TRANSPARENT);
this.setOnTouchListener(new OnTouchListener() {
double distance = 0;
float x1
,
y1
,
x2
,
y2;
@Override
public boolean onTouch(View v, MotionEvent event) {
int event_code = event.getAction();
if (event_code == MotionEvent.ACTION_DOWN) {
distance = 0;
x1 = event.getX();
y1 = event.getY();
}
if (event_code == MotionEvent.ACTION_MOVE) {
x2 = event.getX();
y2 = event.getY();
distance = getPointDistance(x1, y1, x2, y2);
}
if (distance < 10) {
if (event_code == MotionEvent.ACTION_UP) {
FlowTextView.this.onClick(event.getX(), event.getY());
}
return true;
} else {
return false;
}
}
});
}
public void setTextSize(int textSize) {
this.mTextsize = textSize;
mTextPaint.setTextSize(mTextsize);
mLinkPaint.setTextSize(mTextsize);
invalidate();
}
public void setTypeface(Typeface type) {
this.typeFace = type;
mTextPaint.setTypeface(typeFace);
mLinkPaint.setTypeface(typeFace);
invalidate();
}
private void onClick(float x, float y) {
for (HtmlLink link : mLinks) {
float tlX = link.xOffset;
float tlY = link.yOffset;
float brX = link.xOffset + link.width;
float brY = link.yOffset + link.height;
if (x > tlX && x < brX) {
if (y > tlY && y < brY) {
// collision
onLinkClick(link.url);
return;
}
}
}
}
public void setOnLinkClickListener(OnLinkClickListener onLinkClickListener) {
this.mOnLinkClickListener = onLinkClickListener;
}
private void onLinkClick(String url) {
if (mOnLinkClickListener != null)
mOnLinkClickListener.onLinkClick(url);
}
private Line getLine(float lineYbottom, int lineHeight) {
Line line = new Line();
line.leftBound = 0;
line.rightBound = mViewWidth;
float lineYtop = lineYbottom - lineHeight;
mAreas.clear();
mLineboxes.clear();
for (Box box : boxes) {
if (box.topLefty > lineYbottom || box.bottomRighty < lineYtop) {
} else {
Area leftArea = new Area();
leftArea.x1 = 0;
for (Box innerBox : boxes) {
if (innerBox.topLefty > lineYbottom || innerBox.bottomRighty < lineYtop) {
} else {
if (innerBox.topLeftx < box.topLeftx) {
leftArea.x1 = innerBox.bottomRightx;
}
}
}
leftArea.x2 = box.topLeftx;
leftArea.width = leftArea.x2 - leftArea.x1;
Area rightArea = new Area();
rightArea.x1 = box.bottomRightx;
rightArea.x2 = mViewWidth;
for (Box innerBox : boxes) {
if (innerBox.topLefty > lineYbottom || innerBox.bottomRighty < lineYtop) {
} else {
if (innerBox.bottomRightx > box.bottomRightx) {
rightArea.x2 = innerBox.topLeftx;
}
}
}
rightArea.width = rightArea.x2 - rightArea.x1;
mAreas.add(leftArea);
mAreas.add(rightArea);
}
}
mLargestArea = null;
if (mAreas.size() > 0) { // if there is no areas then the whole line is clear, if there is areas, return the largest (it means there
// is one or more boxes colliding with this line)
for (Area area : mAreas) {
if (mLargestArea == null) {
mLargestArea = area;
} else {
if (area.width > mLargestArea.width) {
mLargestArea = area;
}
}
}
line.leftBound = mLargestArea.x1;
line.rightBound = mLargestArea.x2;
}
return line;
}
private int getChunk(String text, float maxWidth) {
int length = mTextPaint.breakText(text, true, maxWidth, null);
if (length <= 0)
return length; // if its 0 or less, return it, can't fit any chars on this line
else if (length >= text.length())
return length; // we can fit the whole string in
else if (text.charAt(length - 1) == ' ')
return length; // if break char is a space -- return
else {
if (text.length() > length)
if (text.charAt(length) == ' ')
return length + 1; // or if the following char is a space then return this length - it is fine
}
// otherwise, count back until we hit a space and return that as the break length
int tempLength = length - 1;
while (text.charAt(tempLength) != ' ') {
// char test = text.charAt(tempLength);
tempLength--;
if (tempLength <= 0)
return length; // if we count all the way back to 0 then this line cannot be broken, just return the original break length
}
// char test = text.charAt(tempLength);
return tempLength + 1; // return the nicer break length which doesn't split a word up
}
@Override
protected void onDraw(Canvas canvas) {
Log.i("flowText", "onDraw");
super.onDraw(canvas);
mViewWidth = this.getWidth();
int lowestYCoord = 0;
boxes.clear();
int childCount = this.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
Box box = new Box();
box.topLeftx = child.getLeft();
box.topLefty = child.getTop();
box.bottomRightx = box.topLeftx + child.getWidth();
box.bottomRighty = box.topLefty + child.getHeight();
boxes.add(box);
if (box.bottomRighty > lowestYCoord)
lowestYCoord = box.bottomRighty;
}
}
String[] blocks = mText.toString().split("\n");
int charOffsetStart = 0; // tells us where we are in the original string
int charOffsetEnd = 0; // tells us where we are in the original string
int lineIndex = 0;
float xOffset = 0; // left margin off a given line
float maxWidth = mViewWidth; // how far to the right it can strectch
float yOffset = 0;
String thisLineStr;
int chunkSize;
int lineHeight = getLineHeight();
ArrayList<HtmlObject> lineObjects = new ArrayList<FlowTextView.HtmlObject>();
Object[] spans = new Object[0];
HtmlObject htmlLine;// = new HtmlObject(); // reuse for single plain lines
mLinks.clear();
for (int block_no = 0; block_no <= blocks.length - 1; block_no++) {
String thisBlock = blocks[block_no];
if (thisBlock.length() <= 0) {
lineIndex++; // is a line break
charOffsetEnd += 2;
charOffsetStart = charOffsetEnd;
} else {
while (thisBlock.length() > 0) {
lineIndex++;
yOffset = lineIndex * lineHeight;
Line thisLine = getLine(yOffset, lineHeight);
xOffset = thisLine.leftBound;
maxWidth = thisLine.rightBound - thisLine.leftBound;
float actualWidth = 0;
do {
Log.i("tv", "maxWidth: " + maxWidth);
chunkSize = getChunk(thisBlock, maxWidth);
int thisCharOffset = charOffsetEnd + chunkSize;
if (chunkSize > 1) {
thisLineStr = thisBlock.substring(0, chunkSize);
} else {
thisLineStr = "";
}
lineObjects.clear();
if (mIsHtml) {
spans = ((Spanned) mText).getSpans(charOffsetStart, thisCharOffset, Object.class);
if (spans.length > 0) {
actualWidth = parseSpans(lineObjects, spans, charOffsetStart, thisCharOffset, xOffset);
} else {
actualWidth = maxWidth; // if no spans then the actual width will be <= maxwidth anyway
}
} else {
actualWidth = maxWidth;// if not html then the actual width will be <= maxwidth anyway
}
Log.i("tv", "actualWidth: " + actualWidth);
if (actualWidth > maxWidth) {
maxWidth -= 5; // if we end up looping - start slicing chars off till we get a suitable size
}
} while (actualWidth > maxWidth);
// chunk is ok
charOffsetEnd += chunkSize;
Log.i("tv", "charOffsetEnd: " + charOffsetEnd);
if (lineObjects.size() <= 0) { // no funky objects found, add the whole chunk as one object
htmlLine = new HtmlObject(thisLineStr, 0, 0, xOffset, mTextPaint);
lineObjects.add(htmlLine);
}
for (HtmlObject thisHtmlObject : lineObjects) {
if (thisHtmlObject instanceof HtmlLink) {
HtmlLink thisLink = (HtmlLink) thisHtmlObject;
float thisLinkWidth = thisLink.paint.measureText(thisHtmlObject.content);
addLink(thisLink, yOffset, thisLinkWidth, lineHeight);
}
paintObject(canvas, thisHtmlObject.content, thisHtmlObject.xOffset, yOffset, thisHtmlObject.paint);
if (thisHtmlObject.recycle) {
recyclePaint(thisHtmlObject.paint);
}
}
if (chunkSize >= 1)
thisBlock = thisBlock.substring(chunkSize, thisBlock.length());
charOffsetStart = charOffsetEnd;
}
}
}
yOffset += (lineHeight / 2);
View child = getChildAt(getChildCount() - 1);
if (child.getTag() != null) {
if (child.getTag().toString().equalsIgnoreCase("hideable")) {
if (yOffset > pageHeight) {
if (yOffset < boxes.get(boxes.size() - 1).topLefty - getLineHeight()) {
child.setVisibility(View.GONE);
// lowestYCoord = (int) yOffset;
} else {
// lowestYCoord = boxes.get(boxes.size()-1).bottomRighty + getLineHeight();
child.setVisibility(View.VISIBLE);
}
} else {
child.setVisibility(View.GONE);
// lowestYCoord = (int) yOffset;
}
}
}
mDesiredHeight = Math.max(lowestYCoord, (int) yOffset);
if (needsMeasure) {
needsMeasure = false;
requestLayout();
}
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
this.invalidate();
}
@Override
public void invalidate() {
this.needsMeasure = true;
super.invalidate();
}
private void paintObject(Canvas canvas, String thisLineStr, float xOffset, float yOffset, Paint paint) {
canvas.drawText(thisLineStr, xOffset, yOffset, paint);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.i("flowText", "onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width = 0;
int height = 0;
if (widthMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
width = widthSize;
} else {
width = this.getWidth();
}
if (heightMode == MeasureSpec.EXACTLY) {
// Parent has told us how big to be. So be it.
height = heightSize;
} else {
height = mDesiredHeight;
}
setMeasuredDimension(width, height + getLineHeight());
// setMeasuredDimension(800, 1400);
}
public int getLineHeight() {
return Math.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
}
private float parseSpans(ArrayList<HtmlObject> objects, Object[] spans, int lineStart, int lineEnd, float baseXOffset) {
sorterMap.clear();
charFlagSize = lineEnd - lineStart;
charFlags = new boolean[charFlagSize];
for (Object span : spans) {
spanStart = mSpannable.getSpanStart(span);
spanEnd = mSpannable.getSpanEnd(span);
if (spanStart < lineStart)
spanStart = lineStart;
if (spanEnd > lineEnd)
spanEnd = lineEnd;
for (charCounter = spanStart; charCounter < spanEnd; charCounter++) { // mark these characters as rendered
charFlagIndex = charCounter - lineStart;
charFlags[charFlagIndex] = true;
}
tempString = extractText(spanStart, spanEnd);
sorterMap.put(spanStart, parseSpan(span, tempString, spanStart, spanEnd));
// objects.add();
}
charCounter = 0;
while (!isArrayFull(charFlags)) {
while (true) {
if (charCounter >= charFlagSize)
break;
if (charFlags[charCounter] == true) {
charCounter++;
continue;
}
temp1 = charCounter;
while (true) {
if (charCounter > charFlagSize)
break;
if (charCounter < charFlagSize) {
if (charFlags[charCounter] == false) {
charFlags[charCounter] = true;// mark as filled
charCounter++;
continue;
}
}
temp2 = charCounter;
spanStart = lineStart + temp1;
spanEnd = lineStart + temp2;
tempString = extractText(spanStart, spanEnd);
sorterMap.put(spanStart, parseSpan(null, tempString, spanStart, spanEnd));
break;
}
}
}
sorterKeys = sorterMap.keySet().toArray();
Arrays.sort(sorterKeys);
float thisXoffset = baseXOffset;
for (charCounter = 0; charCounter < sorterKeys.length; charCounter++) {
HtmlObject thisObj = sorterMap.get(sorterKeys[charCounter]);
thisObj.xOffset = thisXoffset;
tempFloat = thisObj.paint.measureText(thisObj.content);
thisXoffset += tempFloat;
objects.add(thisObj);
}
return (thisXoffset - baseXOffset);
}
private boolean isArrayFull(boolean[] array) {
for (arrayIndex = 0; arrayIndex < array.length; arrayIndex++) {
if (array[arrayIndex] == false)
return false;
}
return true;
}
private HtmlObject parseSpan(Object span, String content, int start, int end) {
if (span instanceof URLSpan) {
return getHtmlLink((URLSpan) span, content, start, end, 0);
} else if (span instanceof StyleSpan) {
return getStyledObject((StyleSpan) span, content, start, end, 0);
} else {
return getHtmlObject(content, start, end, 0);
}
}
private String extractText(int start, int end) {
if (start < 0)
start = 0;
if (end > mTextLength - 1)
end = mTextLength - 1;
return mSpannable.subSequence(start, end).toString();
}
private TextPaint getPaintFromHeap() {
if (mPaintHeap.size() > 0) {
return mPaintHeap.remove(0);
} else {
return new TextPaint(Paint.ANTI_ALIAS_FLAG);
}
}
private void recyclePaint(TextPaint paint) {
mPaintHeap.add(paint);
}
private HtmlObject getStyledObject(StyleSpan span, String content, int start, int end, float thisXOffset) {
TextPaint paint = getPaintFromHeap();
paint.setTypeface(Typeface.defaultFromStyle(span.getStyle()));
paint.setTextSize(mTextsize);
paint.setColor(mColor);
span.updateDrawState(paint);
span.updateMeasureState(paint);
HtmlObject obj = new HtmlObject(content, start, end, thisXOffset, paint);
obj.recycle = true;
return obj;
}
private HtmlObject getHtmlObject(String content, int start, int end, float thisXOffset) {
HtmlObject obj = new HtmlObject(content, start, end, thisXOffset, mTextPaint);
return obj;
}
private HtmlLink getHtmlLink(URLSpan span, String content, int start, int end, float thisXOffset) {
HtmlLink obj = new HtmlLink(content, start, end, thisXOffset, mLinkPaint, span.getURL());
mLinks.add(obj);
return obj;
}
private void addLink(HtmlLink thisLink, float yOffset, float width, float height) {
thisLink.yOffset = yOffset - 20;
;
thisLink.width = width;
thisLink.height = height + 20;
mLinks.add(thisLink);
}
public void setText(CharSequence text) {
mText = text;
if (text instanceof Spannable) {
mIsHtml = true;
mSpannable = (Spannable) text;
Object[] urls = mSpannable.getSpans(0, mSpannable.length(), Object.class);
} else {
mIsHtml = false;
}
mTextLength = mText.length();
this.invalidate();
}
public BitmapSpec addImage(Bitmap bitmap, int xOffset, int yOffset, int padding) {
BitmapSpec spec = new BitmapSpec(bitmap, xOffset, yOffset, padding);
bitmaps.add(spec);
return spec;
}
public ArrayList<BitmapSpec> getBitmaps() {
return bitmaps;
}
public void setBitmaps(ArrayList<BitmapSpec> bitmaps) {
this.bitmaps = bitmaps;
}
public void setPageHeight(int pageHeight) {
this.pageHeight = pageHeight;
}
public interface OnLinkClickListener {
public void onLinkClick(String url);
}
private class Area {
float x1;
float x2;
float width;
}
private class Box {
public int topLeftx;
public int topLefty;
public int bottomRightx;
public int bottomRighty;
}
private class Line {
public float leftBound;
public float rightBound;
}
class HtmlObject {
public String content;
public int start;
public int end;
public float xOffset;
public TextPaint paint;
public boolean recycle = false;
public HtmlObject(String content, int start, int end, float xOffset, TextPaint paint) {
super();
this.content = content;
this.start = start;
this.end = end;
this.xOffset = xOffset;
this.paint = paint;
}
}
class HtmlLink extends HtmlObject {
public float width;
public float height;
public float yOffset;
public String url;
public HtmlLink(String content, int start, int end, float xOffset, TextPaint paint, String url) {
super(content, start, end, xOffset, paint);
this.url = url;
}
}
public class BitmapSpec {
public Bitmap bitmap;
public int xOffset;
public int yOffset;
public int mPadding = 10;
public BitmapSpec(Bitmap bitmap, int xOffset, int yOffset, int mPadding) {
super();
this.bitmap = bitmap;
this.xOffset = xOffset;
this.yOffset = yOffset;
this.mPadding = mPadding;
}
}
}