Last active
June 15, 2016 13:56
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 '<a>' 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