Skip to content

Instantly share code, notes, and snippets.

@ultraon
Last active June 15, 2016 13:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ultraon/cdbea5d1ed81893a0e279efe31f59cb0 to your computer and use it in GitHub Desktop.
Save ultraon/cdbea5d1ed81893a0e279efe31f59cb0 to your computer and use it in GitHub Desktop.
The HandledClickUrlTextView is an extended TextView for supporting custom handler for link clicks
package presentation.view;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.method.LinkMovementMethod;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import timber.log.Timber;
/**
* Version of TextView which can give a chance to handle clicks on web url with {@link HandledClickDrawStateUrlListener}.
* Set text with Html.from("Some text <a href='my_tag'>custom link</a>").
*/
public class HandledClickUrlTextView extends TextView {
@Nullable
private HandledClickDrawStateUrlListener mHandledClickUrlListener;
/**
* Instantiates {@link HandledClickUrlTextView} object.
*/
public HandledClickUrlTextView(final Context context) {
this(context, null);
}
/**
* Instantiates {@link HandledClickUrlTextView} object.
*/
public HandledClickUrlTextView(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* Instantiates {@link HandledClickUrlTextView} object.
*/
public HandledClickUrlTextView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
this.init();
}
/**
* Instantiates {@link HandledClickUrlTextView} object.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public HandledClickUrlTextView(final Context context, final AttributeSet attrs, final int defStyleAttr, final int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
this.init();
}
@Override
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
try {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} catch (final IndexOutOfBoundsException exception) {
Timber.v(exception, "Ignore this exception, known bug for Android 4.1.x, trying fix it...");
this.fixOnMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
/**
* Sets text with {@link HandledClickDrawStateUrlListener}.
*/
public void setText(@Nullable final CharSequence text, @Nullable final HandledClickDrawStateUrlListener handledClickUrlListener) {
this.setHandledClickUrlListener(handledClickUrlListener);
setText(text);
}
@Override
public void setText(final CharSequence text, final BufferType type) {
this.prepareLinkedText(text);
super.setText(text, type);
}
/**
* Sets {@link HandledClickDrawStateUrlListener}.
*
* @param handledClickUrlListener {@link HandledClickDrawStateUrlListener} or null
*/
public void setHandledClickUrlListener(@Nullable final HandledClickDrawStateUrlListener handledClickUrlListener) {
mHandledClickUrlListener = handledClickUrlListener;
}
/**
* Returns current {@link HandledClickDrawStateUrlListener} or null.
*/
@Nullable
public HandledClickDrawStateUrlListener getHandledClickUrlListener() {
return mHandledClickUrlListener;
}
private void init() {
setAutoLinkMask(0);
setLinksClickable(true);
setMovementMethod(LinkMovementMethod.getInstance());
}
private void prepareLinkedText(@Nullable final CharSequence text) {
if (!(text instanceof SpannableStringBuilder)) {
return;
}
final SpannableStringBuilder ssb = (SpannableStringBuilder) text;
final URLSpan[] spans = ssb.getSpans(0, ssb.length(), URLSpan.class);
if (null == spans || spans.length == 0) {
return;
}
for (final URLSpan urlSpan: spans) {
ssb.setSpan(new URLSpanWrapper(urlSpan,
new HandledClickDrawStateUrlListener() {
@Override
public void onUrlLinkClick(@NonNull final View widget, @NonNull final String urlHref) {
if (null == mHandledClickUrlListener) {
new URLSpan(urlHref).onClick(widget);
} else {
mHandledClickUrlListener.onUrlLinkClick(widget, urlHref);
}
}
@Override
public void updateDrawState(@NonNull final TextPaint ds) {
if (null != mHandledClickUrlListener) {
mHandledClickUrlListener.updateDrawState(ds);
}
}
}),
ssb.getSpanStart(urlSpan), ssb.getSpanEnd(urlSpan), ssb.getSpanFlags(urlSpan));
ssb.removeSpan(urlSpan);
}
}
// If possible, fixes the Spanned text by adding spaces around spans when needed.
private void fixOnMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
final CharSequence text = getText();
if (text instanceof Spanned) {
final SpannableStringBuilder builder = new SpannableStringBuilder(text);
this.fixSpannedWithSpaces(builder, widthMeasureSpec, heightMeasureSpec);
} else {
this.fallbackToString(widthMeasureSpec, heightMeasureSpec);
}
}
// Add spaces around spans until the text is fixed, and then removes the unneeded spaces.
private void fixSpannedWithSpaces(final SpannableStringBuilder builder, final int widthMeasureSpec, final int heightMeasureSpec) {
final SpansFixingResult result = this.addSpacesAroundSpansUntilFixed(builder, widthMeasureSpec, heightMeasureSpec);
if (result.isFixed()) {
this.removeUnneededSpaces(widthMeasureSpec, heightMeasureSpec, builder, result);
} else {
this.fallbackToString(widthMeasureSpec, heightMeasureSpec);
}
}
private SpansFixingResult addSpacesAroundSpansUntilFixed(final SpannableStringBuilder spannableBuilder, final int widthMeasureSpec, final int heightMeasureSpec) {
final Object[] spans = spannableBuilder.getSpans(0, spannableBuilder.length(), Object.class);
final List<Object> spansWithSpacesBeforeList = new ArrayList<>(spans.length);
final List<Object> spansWithSpacesAfterList = new ArrayList<>(spans.length);
for (final Object span : spans) {
final int spanStart = spannableBuilder.getSpanStart(span);
if (this.isNotSpace(spannableBuilder, spanStart - 1)) {
spannableBuilder.insert(spanStart, " ");
spansWithSpacesBeforeList.add(span);
}
final int spanEnd = spannableBuilder.getSpanEnd(span);
if (this.isNotSpace(spannableBuilder, spanEnd)) {
spannableBuilder.insert(spanEnd, " ");
spansWithSpacesAfterList.add(span);
}
try {
this.setTextAndMeasure(spannableBuilder, widthMeasureSpec, heightMeasureSpec);
return SpansFixingResult.newFixedInstance(spansWithSpacesBeforeList, spansWithSpacesAfterList);
} catch (final IndexOutOfBoundsException exception) {
Timber.w(exception, "Can't fix spannable text: %s, known bug in Android 4.1.x", getText().toString());
}
}
return SpansFixingResult.newUnfixedInstance();
}
private boolean isNotSpace(final CharSequence text, final int where) {
return where < 0 || where >= text.length() || text.charAt(where) != ' ';
}
@SuppressLint("WrongCall")
private void setTextAndMeasure(final CharSequence text, final int widthMeasureSpec, final int heightMeasureSpec) {
setText(text);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@SuppressLint("WrongCall")
private void removeUnneededSpaces(final int widthMeasureSpec, final int heightMeasureSpec, final SpannableStringBuilder builder, final SpansFixingResult result) {
for (final Object span: result.getSpansWithSpacesAfterList()) {
final int spanEnd = builder.getSpanEnd(span);
builder.delete(spanEnd, spanEnd + 1);
try {
this.setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec);
} catch (final IndexOutOfBoundsException exception) {
Timber.v(exception, "Ignore this exception, known bug for Android 4.1.x");
builder.insert(spanEnd, " ");
}
}
boolean needReset = true;
for (final Object span: result.getSpansWithSpacesBeforeList()) {
final int spanStart = builder.getSpanStart(span);
builder.delete(spanStart - 1, spanStart);
try {
this.setTextAndMeasure(builder, widthMeasureSpec, heightMeasureSpec);
needReset = false;
} catch (final IndexOutOfBoundsException ignored) {
needReset = true;
final int newSpanStart = spanStart - 1;
builder.insert(newSpanStart, " ");
}
}
if (needReset) {
setText(builder);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
private void fallbackToString(final int widthMeasureSpec, final int heightMeasureSpec) {
final String fallbackText = getText().toString();
this.setTextAndMeasure(fallbackText, widthMeasureSpec, heightMeasureSpec);
}
/**
* Interface for handling url clicks and updating drawing state.
*/
public interface HandledClickDrawStateUrlListener {
/**
* Triggers after user clicks on url link.
*
* @param widget current view
* @param urlHref string url in href attr in '&lt;a&gt;' html element
*/
void onUrlLinkClick(@NonNull View widget, @NonNull String urlHref);
/**
* Triggers while drawing current view text.
*
* @param ds text paint for modifying
*/
void updateDrawState(@NonNull TextPaint ds);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment