package com.marshalchen.common.uimodule.flowtextview;
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;
import com.marshalchen.common.uimodule.flowtextview.helpers.ClickHandler;
import com.marshalchen.common.uimodule.flowtextview.helpers.CollisionHelper;
import com.marshalchen.common.uimodule.flowtextview.helpers.PaintHelper;
import com.marshalchen.common.uimodule.flowtextview.helpers.SpanParser;
import com.marshalchen.common.uimodule.flowtextview.listeners.OnLinkClickListener;
import com.marshalchen.common.uimodule.flowtextview.models.HtmlLink;
import com.marshalchen.common.uimodule.flowtextview.models.HtmlObject;
import com.marshalchen.common.uimodule.flowtextview.models.Line;
import com.marshalchen.common.uimodule.flowtextview.models.Obstacle;
import java.util.ArrayList;
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();
}
}