package com.smartandroid.sa.floatextview;
import java.util.ArrayList;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
public class FlowTextView extends RelativeLayout {
// FIELDS
private final PaintHelper mPaintHelper = new PaintHelper();
private final SpanParser mSpanParser = new SpanParser(this, mPaintHelper);
private final ClickHandler mClickHandler = new ClickHandler(mSpanParser);
private int mColor = Color.BLACK;
private int pageHeight = 0;
private TextPaint mTextPaint;
private TextPaint mLinkPaint;
private float mTextsize = 20.0f;
private Typeface typeFace;
private int mDesiredHeight = 100; // height of the whole view
private boolean needsMeasure = true;
private final ArrayList<Obstacle> obstacles = new ArrayList<Obstacle>();
private CharSequence mText = "";
private boolean mIsHtml = false;
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 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(mClickHandler);
}
// INTERESTING DRAWING STUFF
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float mViewWidth = this.getWidth();
obstacles.clear(); // clear old data, boxes stores an array of
// "obstacles" that we need to paint the text around
int lowestYCoord = findBoxesAndReturnLowestObstacleYCoord(); // find the
// "obstacles"
// within
// the
// view
// and
// get
// the
// lowest
// obstacle
// coordinate
// at
// the
// same
// time
String[] blocks = mText.toString().split("\n"); // split the text into
// its natural blocks
// set up some counter and helper variables we will us to traverse
// through the string to be rendered
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; // left margin off a given line
float maxWidth; // how far to the right it can strectch
float yOffset = 0;
String thisLineStr; // the current line we are trying to render
int chunkSize;
int lineHeight = getLineHeight(); // get the height in pixels of a line
// for our current TextPaint
ArrayList<HtmlObject> lineObjects = new ArrayList<HtmlObject>(); // this
// will
// get
// populated
// with
// special
// html
// objects
// we
// need
// to
// render
Object[] spans;
HtmlObject htmlLine;// reuse for single plain lines
mSpanParser.reset();
for (int block_no = 0; block_no <= blocks.length - 1; block_no++) // at
// the
// highest
// level
// we
// iterate
// through
// each
// 'block'
// of
// text
{
String thisBlock = blocks[block_no];
if (thisBlock.length() <= 0) { // is a line break
lineIndex++; // we need a new line
charOffsetEnd += 2;
charOffsetStart = charOffsetEnd;
} else { // is some actual text
while (thisBlock.length() > 0) { // churn through the block
// spitting it out onto
// seperate lines until
// there is nothing left to
// render
lineIndex++; // we need a new line
yOffset = lineIndex * lineHeight; // calculate our new y
// position based on
// number of lines *
// line height
Line thisLine = CollisionHelper
.calculateLineSpaceForGivenYOffset(yOffset,
lineHeight, mViewWidth, obstacles); // calculate
// a
// theoretical
// "line"
// space
// that
// we
// have
// to
// paint
// into
// based
// on
// the
// "obstacles"
// that
// exist
// at
// this
// yOffset
// and
// this
// line
// height
// -
// collision
// detection
// essentially
xOffset = thisLine.leftBound;
maxWidth = thisLine.rightBound - thisLine.leftBound;
float actualWidth;
// now we have a line of known maximum width that we can
// render to, figure out how many characters we can use to
// get that width taking into account html funkyness
do {
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 = mSpanParser.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
}
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;
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);
mSpanParser.addLink(thisLink, yOffset,
thisLinkWidth, lineHeight);
}
paintObject(canvas, thisHtmlObject.content,
thisHtmlObject.xOffset, yOffset,
thisHtmlObject.paint);
if (thisHtmlObject.recycle) {
mPaintHelper.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 < obstacles.get(obstacles.size() - 1).topLefty
- getLineHeight()) {
child.setVisibility(View.GONE);
} else {
child.setVisibility(View.VISIBLE);
}
} else {
child.setVisibility(View.GONE);
}
}
}
mDesiredHeight = Math.max(lowestYCoord, (int) yOffset);
if (needsMeasure) {
needsMeasure = false;
requestLayout();
}
}
private int findBoxesAndReturnLowestObstacleYCoord() {
int lowestYCoord = 0;
int childCount = this.getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
Obstacle obstacle = new Obstacle();
obstacle.topLeftx = child.getLeft();
obstacle.topLefty = child.getTop();
obstacle.bottomRightx = obstacle.topLeftx + child.getWidth();
obstacle.bottomRighty = obstacle.topLefty + child.getHeight();
obstacles.add(obstacle);
if (obstacle.bottomRighty > lowestYCoord)
lowestYCoord = obstacle.bottomRighty;
}
}
return lowestYCoord;
}
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) != ' ') {
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
}
return tempLength + 1; // return the nicer break length which doesn't
// split a word up
}
private void paintObject(Canvas canvas, String thisLineStr, float xOffset,
float yOffset, Paint paint) {
canvas.drawText(thisLineStr, xOffset, yOffset, paint);
}
// MINOR VIEW EVENTS
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
this.invalidate();
}
@Override
public void invalidate() {
this.needsMeasure = true;
super.invalidate();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
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;
int height;
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());
}
// GETTERS AND SETTERS
// text size
public float getTextsize() {
return mTextsize;
}
public void setTextSize(float textSize) {
this.mTextsize = textSize;
mTextPaint.setTextSize(mTextsize);
mLinkPaint.setTextSize(mTextsize);
invalidate();
}
// typeface
public Typeface getTypeFace() {
return typeFace;
}
public void setTypeface(Typeface type) {
this.typeFace = type;
mTextPaint.setTypeface(typeFace);
mLinkPaint.setTypeface(typeFace);
invalidate();
}
// text paint
public TextPaint getTextPaint() {
return mTextPaint;
}
public void setTextPaint(TextPaint mTextPaint) {
this.mTextPaint = mTextPaint;
invalidate();
}
// link paint
public TextPaint getLinkPaint() {
return mLinkPaint;
}
public void setLinkPaint(TextPaint mLinkPaint) {
this.mLinkPaint = mLinkPaint;
invalidate();
}
// text content
public CharSequence getText() {
return mText;
}
public void setText(CharSequence text) {
mText = text;
if (text instanceof Spannable) {
mIsHtml = true;
mSpanParser.setSpannable((Spannable) text);
} else {
mIsHtml = false;
}
this.invalidate();
}
// text colour
public int getColor() {
return mColor;
}
public void setColor(int color) {
this.mColor = color;
if (mTextPaint != null) {
mTextPaint.setColor(mColor);
}
mPaintHelper.setColor(mColor);
this.invalidate();
}
// link click listener
public OnLinkClickListener getOnLinkClickListener() {
return mClickHandler.getOnLinkClickListener();
}
public void setOnLinkClickListener(OnLinkClickListener onLinkClickListener) {
mClickHandler.setOnLinkClickListener(onLinkClickListener);
}
// line height
public int getLineHeight() {
float mSpacingMult = 1.0f;
float mSpacingAdd = 0.0f;
return Math.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult
+ mSpacingAdd);
}
// page height
public void setPageHeight(int pageHeight) {
this.pageHeight = pageHeight;
invalidate();
}
}