1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.android.setupdesign.span; 18 19 import android.content.Context; 20 import android.content.ContextWrapper; 21 import android.os.Build; 22 import androidx.annotation.Nullable; 23 import android.text.Selection; 24 import android.text.Spannable; 25 import android.text.TextPaint; 26 import android.text.style.ClickableSpan; 27 import android.util.Log; 28 import android.view.View; 29 import android.widget.TextView; 30 31 /** 32 * A clickable span that will listen for click events and send it back to the context. To use this 33 * class, implement {@link OnLinkClickListener} in your TextView, or use {@link 34 * com.google.android.setupdesign.view.RichTextView#setOnClickListener(View.OnClickListener)}. 35 * 36 * <p>Note on accessibility: For TalkBack to be able to traverse and interact with the links, you 37 * should use {@code LinkAccessibilityHelper} in your {@code TextView} subclass. Optionally you can 38 * also use {@code RichTextView}, which includes link support. 39 */ 40 public class LinkSpan extends ClickableSpan { 41 42 /* 43 * Implementation note: When the orientation changes, TextView retains a reference to this span 44 * instead of writing it to a parcel (ClickableSpan is not Parcelable). If this class has any 45 * reference to the containing Activity (i.e. the activity context, or any views in the 46 * activity), it will cause memory leak. 47 */ 48 49 /* static section */ 50 51 private static final String TAG = "LinkSpan"; 52 53 /** @deprecated Use {@link OnLinkClickListener} */ 54 @Deprecated 55 public interface OnClickListener { onClick(LinkSpan span)56 void onClick(LinkSpan span); 57 } 58 59 /** 60 * Listener that is invoked when a link span is clicked. If the containing view of this span 61 * implements this interface, this will be invoked when the link is clicked. 62 */ 63 public interface OnLinkClickListener { 64 65 /** 66 * Called when a link has been clicked. 67 * 68 * @param span The span that was clicked. 69 * @return True if the click was handled, stopping further propagation of the click event. 70 */ onLinkClick(LinkSpan span)71 boolean onLinkClick(LinkSpan span); 72 } 73 74 /* non-static section */ 75 76 private final String id; 77 LinkSpan(String id)78 public LinkSpan(String id) { 79 this.id = id; 80 } 81 82 @Override onClick(View view)83 public void onClick(View view) { 84 if (dispatchClick(view)) { 85 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 86 // Prevent the touch event from bubbling up to the parent views. 87 view.cancelPendingInputEvents(); 88 } 89 } else { 90 Log.w(TAG, "Dropping click event. No listener attached."); 91 } 92 if (view instanceof TextView) { 93 // Remove the highlight effect when the click happens by clearing the selection 94 CharSequence text = ((TextView) view).getText(); 95 if (text instanceof Spannable) { 96 Selection.setSelection((Spannable) text, 0); 97 } 98 } 99 } 100 dispatchClick(View view)101 private boolean dispatchClick(View view) { 102 boolean handled = false; 103 if (view instanceof OnLinkClickListener) { 104 handled = ((OnLinkClickListener) view).onLinkClick(this); 105 } 106 if (!handled) { 107 final OnClickListener listener = getLegacyListenerFromContext(view.getContext()); 108 if (listener != null) { 109 listener.onClick(this); 110 handled = true; 111 } 112 } 113 return handled; 114 } 115 116 /** @deprecated Deprecated together with {@link OnClickListener} */ 117 @Nullable 118 @Deprecated getLegacyListenerFromContext(@ullable Context context)119 private OnClickListener getLegacyListenerFromContext(@Nullable Context context) { 120 while (true) { 121 if (context instanceof OnClickListener) { 122 return (OnClickListener) context; 123 } else if (context instanceof ContextWrapper) { 124 // Unwrap any context wrapper, in base the base context implements onClickListener. 125 // ContextWrappers cannot have circular base contexts, so at some point this will 126 // reach the one of the other cases and return. 127 context = ((ContextWrapper) context).getBaseContext(); 128 } else { 129 return null; 130 } 131 } 132 } 133 134 @Override updateDrawState(TextPaint drawState)135 public void updateDrawState(TextPaint drawState) { 136 super.updateDrawState(drawState); 137 drawState.setUnderlineText(false); 138 } 139 getId()140 public String getId() { 141 return id; 142 } 143 } 144