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.android.setupwizardlib.view; 18 19 import android.content.Context; 20 import android.text.Annotation; 21 import android.text.SpannableString; 22 import android.text.Spanned; 23 import android.text.method.MovementMethod; 24 import android.text.style.ClickableSpan; 25 import android.text.style.TextAppearanceSpan; 26 import android.util.AttributeSet; 27 import android.util.Log; 28 import android.view.MotionEvent; 29 import android.widget.TextView; 30 import com.android.setupwizardlib.span.LinkSpan; 31 import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener; 32 import com.android.setupwizardlib.span.SpanHelper; 33 import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod; 34 35 /** 36 * An extension of TextView that automatically replaces the annotation tags as specified in {@link 37 * SpanHelper#replaceSpan(android.text.Spannable, Object, Object)} 38 * 39 * <p>Note: The accessibility interaction for ClickableSpans (and therefore LinkSpans) are built 40 * into platform in O, although the interaction paradigm is different. (See b/17726921). In this 41 * platform version, the links are exposed in the Local Context Menu of TalkBack instead of 42 * accessible directly through swiping. 43 */ 44 public class RichTextView extends TextView implements OnLinkClickListener { 45 46 /* static section */ 47 48 private static final String TAG = "RichTextView"; 49 50 private static final String ANNOTATION_LINK = "link"; 51 private static final String ANNOTATION_TEXT_APPEARANCE = "textAppearance"; 52 53 /** 54 * Replace <annotation> tags in strings to become their respective types. Currently 2 types 55 * are supported: 56 * 57 * <ol> 58 * <li><annotation link="foobar"> will create a {@link 59 * com.android.setupwizardlib.span.LinkSpan} that broadcasts with the key "foobar" 60 * <li><annotation textAppearance="TextAppearance.FooBar"> will create a {@link 61 * android.text.style.TextAppearanceSpan} with @style/TextAppearance.FooBar 62 * </ol> 63 */ getRichText(Context context, CharSequence text)64 public static CharSequence getRichText(Context context, CharSequence text) { 65 if (text instanceof Spanned) { 66 final SpannableString spannable = new SpannableString(text); 67 final Annotation[] spans = spannable.getSpans(0, spannable.length(), Annotation.class); 68 for (Annotation span : spans) { 69 final String key = span.getKey(); 70 if (ANNOTATION_TEXT_APPEARANCE.equals(key)) { 71 String textAppearance = span.getValue(); 72 final int style = 73 context 74 .getResources() 75 .getIdentifier(textAppearance, "style", context.getPackageName()); 76 if (style == 0) { 77 Log.w(TAG, "Cannot find resource: " + style); 78 } 79 final TextAppearanceSpan textAppearanceSpan = new TextAppearanceSpan(context, style); 80 SpanHelper.replaceSpan(spannable, span, textAppearanceSpan); 81 } else if (ANNOTATION_LINK.equals(key)) { 82 LinkSpan link = new LinkSpan(span.getValue()); 83 SpanHelper.replaceSpan(spannable, span, link); 84 } 85 } 86 return spannable; 87 } 88 return text; 89 } 90 91 /* non-static section */ 92 93 private OnLinkClickListener mOnLinkClickListener; 94 RichTextView(Context context)95 public RichTextView(Context context) { 96 super(context); 97 } 98 RichTextView(Context context, AttributeSet attrs)99 public RichTextView(Context context, AttributeSet attrs) { 100 super(context, attrs); 101 } 102 103 @Override setText(CharSequence text, BufferType type)104 public void setText(CharSequence text, BufferType type) { 105 text = getRichText(getContext(), text); 106 // Set text first before doing anything else because setMovementMethod internally calls 107 // setText. This in turn ends up calling this method with mText as the first parameter 108 super.setText(text, type); 109 boolean hasLinks = hasLinks(text); 110 111 if (hasLinks) { 112 // When a TextView has a movement method, it will set the view to clickable. This makes 113 // View.onTouchEvent always return true and consumes the touch event, essentially 114 // nullifying any return values of MovementMethod.onTouchEvent. 115 // To still allow propagating touch events to the parent when this view doesn't have 116 // links, we only set the movement method here if the text contains links. 117 setMovementMethod(TouchableLinkMovementMethod.getInstance()); 118 } else { 119 setMovementMethod(null); 120 } 121 // ExploreByTouchHelper automatically enables focus for RichTextView 122 // even though it may not have any links. Causes problems during talkback 123 // as individual TextViews consume touch events and thereby reducing the focus window 124 // shown by Talkback. Disable focus if there are no links 125 setFocusable(hasLinks); 126 // Do not "reveal" (i.e. scroll to) this view when this view is focused. Since this view is 127 // focusable in touch mode, we may be focused when the screen is first shown, and starting 128 // a screen halfway scrolled down is confusing to the user. 129 setRevealOnFocusHint(false); 130 setFocusableInTouchMode(hasLinks); 131 } 132 hasLinks(CharSequence text)133 private boolean hasLinks(CharSequence text) { 134 if (text instanceof Spanned) { 135 final ClickableSpan[] spans = 136 ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class); 137 return spans.length > 0; 138 } 139 return false; 140 } 141 142 @Override 143 @SuppressWarnings("ClickableViewAccessibility") // super.onTouchEvent is called onTouchEvent(MotionEvent event)144 public boolean onTouchEvent(MotionEvent event) { 145 // Since View#onTouchEvent always return true if the view is clickable (which is the case 146 // when a TextView has a movement method), override the implementation to allow the movement 147 // method, if it implements TouchableMovementMethod, to say that the touch is not handled, 148 // allowing the event to bubble up to the parent view. 149 boolean superResult = super.onTouchEvent(event); 150 MovementMethod movementMethod = getMovementMethod(); 151 if (movementMethod instanceof TouchableMovementMethod) { 152 TouchableMovementMethod touchableMovementMethod = (TouchableMovementMethod) movementMethod; 153 if (touchableMovementMethod.getLastTouchEvent() == event) { 154 return touchableMovementMethod.isLastTouchEventHandled(); 155 } 156 } 157 return superResult; 158 } 159 setOnLinkClickListener(OnLinkClickListener listener)160 public void setOnLinkClickListener(OnLinkClickListener listener) { 161 mOnLinkClickListener = listener; 162 } 163 getOnLinkClickListener()164 public OnLinkClickListener getOnLinkClickListener() { 165 return mOnLinkClickListener; 166 } 167 168 @Override onLinkClick(LinkSpan span)169 public boolean onLinkClick(LinkSpan span) { 170 if (mOnLinkClickListener != null) { 171 return mOnLinkClickListener.onLinkClick(span); 172 } 173 return false; 174 } 175 } 176