Как сделать часть текста флажка интерактивной?

Я пытаюсь создать ссылку в смежном тексте моего текстового поля. Эта ссылка, однако, не является URL-адресом, но должна действовать как кнопка, чтобы я мог выполнить несколько задач в событии onItemClick. Я в основном подключаю это к представлению, которое показывает наше лицензионное соглашение с конечным пользователем (жестко запрограммировано).

Как я могу сделать это?

Заранее спасибо.

11 ответов

Решение

Там на самом деле элегантное решение, используя CheckBox и один TextView, Наряду с комбинациями TextView.setClickable()Фильтр намерений и TextView.setMovementMethod(),

У вас есть основной вид (здесь я назвал его ClickableTextViewExample):

package id.web.freelancer.example;

import android.app.Activity;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.widget.CheckBox;
import android.widget.TextView;

public class ClickableTextViewExampleActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);


        CheckBox checkbox = (CheckBox)findViewById(R.id.checkBox1);
        TextView textView = (TextView)findViewById(R.id.textView2);

        checkbox.setText("");
        textView.setText(Html.fromHtml("I have read and agree to the " +
                "<a href='id.web.freelancer.example.TCActivity://Kode'>TERMS AND CONDITIONS</a>"));
        textView.setClickable(true);
        textView.setMovementMethod(LinkMovementMethod.getInstance());
    }
}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <LinearLayout
        android:id="@+id/linearLayout1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        <CheckBox
            android:id="@+id/checkBox1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="CheckBox" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="TextView"
            android:clickable="true" />

    </LinearLayout>

</LinearLayout>

TCActivity.java

package id.web.freelancer.example;

import android.app.Activity;
import android.os.Bundle;

public class TCActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tc);
    }

}

tc.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

        <TextView
        android:id="@+id/tcView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Terms and conditions" />


</LinearLayout>

и последний фрагмент кода, который склеивает все это, AndroidManifest.xml:

<activity android:name="TCActivity">
    <intent-filter>
        <category android:name="android.intent.category.DEFAULT" />
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="id.web.freelancer.example.TCActivity" />  
    </intent-filter>            
</activity>

Вот и объяснения:

textView.setText(Html.fromHtml("I have read and agree to the " +
                     "<a href='id.web.freelancer.example.TCActivity://Kode'>TERMS AND CONDITIONS</a>"));
textView.setClickable(true);
textView.setMovementMethod(LinkMovementMethod.getInstance());

setClickable позволит вам нажать на textView, Но не ссылка HREF. Для этого вам придется использовать setMovementMethod() и установите его LinkMovementMethod,

После этого вам нужно поймать URL. Я сделал это используя intent-filter в AndroidManifest.xml

<action android:name="android.intent.action.VIEW" />
<data android:scheme="id.web.freelancer.example.TCActivity" />  

Он ловит команду VIEW и фильтрует только URL, начиная с id.web.freelancer.example.TCActivity://

Вот пакет для вас, чтобы попробовать его, и вот репозиторий github. Надеюсь, это помогло

Возможно, вы захотите, чтобы только часть текста была кликабельной ссылкой, тогда как остальная часть флажка ведет себя как обычно, то есть вы можете щелкнуть другой текст, чтобы переключить состояние.

Вы можете установить свой флажок так:

CheckBox checkBox = (CheckBox) findViewById(R.id.my_check_box);

ClickableSpan clickableSpan = new ClickableSpan() {
    @Override
    public void onClick(View widget) {
        // Prevent CheckBox state from being toggled when link is clicked
        widget.cancelPendingInputEvents();
        // Do action for link text...
    }
    @Override
    public void updateDrawState(TextPaint ds) {
        super.updateDrawState(ds);
        // Show links with underlines (optional)
        ds.setUnderlineText(true);
    }
};

SpannableString linkText = new SpannableString("Link text");
linkText.setSpan(clickableSpan, 0, linkText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
CharSequence cs = TextUtils.expandTemplate(
    "CheckBox text with link: ^1 , and after link", linkText);

checkBox.setText(cs);
// Finally, make links clickable
checkBox.setMovementMethod(LinkMovementMethod.getInstance());

Следующий код работал для меня на KitKat. Я еще не тестировал Android-версии ниже.

String checkBoxText = "I agree to all the <a href='http://www.redbus.in/mob/mTerms.aspx' > Terms and Conditions</a>";

checkBoxView.setText(Html.fromHtml(checkBoxText));
checkBoxView.setMovementMethod(LinkMovementMethod.getInstance());

Котлинская версия (через расширение) ответа Даниэля Шулера:

fun CheckBox.addClickableLink(fullText: String, linkText: SpannableString, callback: () -> Unit) {
    val clickableSpan = object : ClickableSpan() {
        override fun onClick(widget: View) {
            widget.cancelPendingInputEvents() // Prevent CheckBox state from being toggled when link is clicked
            callback.invoke()
        }

        override fun updateDrawState(ds: TextPaint) {
            super.updateDrawState(ds)
            ds.isUnderlineText = true // Show links with underlines
        }
    }
    linkText.setSpan(clickableSpan, 0, linkText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    val fullTextWithTemplate = fullText.replace(linkText.toString(), "^1", false)
    val cs = TextUtils.expandTemplate(fullTextWithTemplate, linkText)

    text = cs
    movementMethod = LinkMovementMethod.getInstance() // Make link clickable
}

Применение:

yourCheckBox.addClickableLink(
    fullText = "This link must be clickable",
    linkText = SpannableString("This link")
) {
    // Do whatever you want when onClick()
}

У меня была та же проблема, и я хотел, чтобы в тексте флажка было несколько кликабельных ссылок без потери возможности щелкать в любом месте текста (там, где нет URL), чтобы установить / снять флажок.

Отличие от других ответов на этот вопрос состоит в том, что с этим решением вы можете иметь несколько кликабельных ссылок в тексте флажка, и эти ссылки не обязательно должны быть в конце текста.

Макет выглядит так же, как в ariefbayu:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="16dp">

    <CheckBox
        android:id="@+id/tosCheckBox"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:checked="false" />

    <TextView
        android:id="@+id/tosTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/tosCheckBox"
        android:layout_centerVertical="true"
        android:clickable="true" />

</RelativeLayout>

Я сейчас устанавливаю текст программно. Текст, который я хочу отобразить:

"I have read and accepted the <a href='https://www.anyurl.com/privacy'>privacy statement</a> and <a href='https://www.anyurl.com/tos'>terms of service.</a>"

Поскольку он содержит HTML, я сначала конвертирую его в Spanned. Чтобы сделать ссылки кликабельными, я дополнительно установил метод перемещения TextView на LinkMovementMethod:

mTosTextView = (TextView) findViewById(R.id.tosTextView);
mTosTextView.setText(Html.fromHtml(getString(R.string.TOSInfo)));
mTosTextView.setMovementMethod(LinkMovementMethod.getInstance());

И тут начинается более сложная часть. До сих пор CheckBox не выбирался при нажатии TextView. Чтобы добиться этого, я добавил сенсорный обработчик в TextView:

mTosCheckBox = (CheckBox) findViewById(R.id.tosCheckBox);
mTosTextView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        CharSequence text = mTosTextView.getText();

        // find out which character was touched
        int offset = getOffsetForPosition(mTosTextView, event.getX(), event.getY());

        // check if this character contains a URL
        URLSpan[] types = ((Spanned)text).getSpans(offset, offset, URLSpan.class);

        if (types.length > 0) {
            // a link was clicked, so don't handle the event
            Log.d("Some tag", "link clicked: " + types[0].getURL());
            return false;
        }

        // no link was touched, so handle the touch to change 
        // the pressed state of the CheckBox
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mTosCheckBox.setPressed(true);
                break;

            case MotionEvent.ACTION_UP:
                mTosCheckBox.setChecked(!mTosCheckBox.isChecked());
                mTosCheckBox.setPressed(false);
                break;

            default:
                mTosCheckBox.setPressed(false);
                break;
        }
        return true;
    }
});

Наконец, как вы, наверное, заметили, нет никакого метода getOffsetForPosition(...) еще. Если вы нацелены на уровень API 14+, вы можете просто использовать getOffsetForPosition(), как указал Deeraj VS. Поскольку я нацеливаюсь на уровень API 8+, я использовал реализацию, которую нашел здесь: Определение слова, которое нажимается в текстовом представлении Android.

public int getOffsetForPosition(TextView textView, float x, float y) {
    if (textView.getLayout() == null) {
        return -1;
    }
    final int line = getLineAtCoordinate(textView, y);
    final int offset = getOffsetAtCoordinate(textView, line, x);
    return offset;
}

private int getOffsetAtCoordinate(TextView textView2, int line, float x) {
    x = convertToLocalHorizontalCoordinate(textView2, x);
    return textView2.getLayout().getOffsetForHorizontal(line, x);
}

private float convertToLocalHorizontalCoordinate(TextView textView2, float x) {
    x -= textView2.getTotalPaddingLeft();
    // Clamp the position to inside of the view.
    x = Math.max(0.0f, x);
    x = Math.min(textView2.getWidth() - textView2.getTotalPaddingRight() - 1, x);
    x += textView2.getScrollX();
    return x;
}

private int getLineAtCoordinate(TextView textView2, float y) {
    y -= textView2.getTotalPaddingTop();
    // Clamp the position to inside of the view.
    y = Math.max(0.0f, y);
    y = Math.min(textView2.getHeight() - textView2.getTotalPaddingBottom() - 1, y);
    y += textView2.getScrollY();
    return textView2.getLayout().getLineForVertical((int) y);
}

Требования:

только часть текста является интерактивной ссылкой, тогда как остальная часть флажка ведет себя как обычно:

  1. Запретить переключение состояния CheckBox при нажатии ссылки
  2. Убрать эффект ряби из CheckBox

Вот версия Котлина:

interface HtmlAnchorClickListener {
    fun onHyperLinkClicked(name: String)
}

fun addClickableSpan(linkableTextView: TextView?, htmlString: String, listener: HtmlAnchorClickListener) {
    linkableTextView?.let {
        val sequence = HtmlCompat.fromHtml(htmlString, HtmlCompat.FROM_HTML_MODE_LEGACY)
        Log.d("addClickableSpan", "sequence = $sequence")
        val spannableString = SpannableStringBuilder(sequence)
        val urls = spannableString.getSpans(0, sequence.length, URLSpan::class.java)
        urls.forEach { span ->
            with(spannableString) {
                val start = getSpanStart(span)
                val end = getSpanEnd(span)
                val flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
                val linkColor = linkableTextView.context.getColor(R.color.light_blue)

                val clickable = object : ClickableSpan() {

                    override fun onClick(view: View) {
                        // Prevent CheckBox state from being toggled when link is clicked
                        linkableTextView.cancelPendingInputEvents()
                        removeRippleEffectFromCheckBox(linkableTextView)
                        listener.onHyperLinkClicked(span.url)
                    }

                    override fun updateDrawState(textPaint: TextPaint) {
                        textPaint.color = linkColor
                        textPaint.isUnderlineText = true
                    }
                }
                setSpan(clickable, start, end, flags)
                setSpan(ForegroundColorSpan(linkColor), start, end, flags)
                removeSpan(span)
            }

            with(it) {
                text = spannableString
                linksClickable = true
                movementMethod = LinkMovementMethod.getInstance()
            }
        }
    }
}

fun removeRippleEffectFromCheckBox(textView: TextView) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        var drawable = textView.background
        if (drawable is RippleDrawable) {
            drawable = drawable.findDrawableByLayerId(0)
            textView.background = drawable
        }
    }
}

Применение:

private fun setUpTermsOfUseHyperLink() {
    val checkBoxText =
        "I agree to all the <a href='http://www.redbus.in/mob/mTerms.aspx' > Terms and Conditions</a>"

    addClickableSpan(cbAccept, checkBoxText, object : HtmlAnchorClickListener {
        override fun onHyperLinkClicked(name: String) {
            Toast.makeText(context!!, name, Toast.LENGTH_LONG).show()
        }
    })
}

Создать CheckBox без текста и добавить два TextViewрядом с ним. Первый - это не кликабельное представление с текстом типа "Я прочитал и согласен с". Второе - это интерактивное представление с текстом типа "УСЛОВИЯ И УСЛОВИЯ". Поместите TextViewS бок о бок без каких-либо полей. Обратите внимание на дополнительное пространство в конце первого представления для естественного выравнивания текста. Таким образом, вы можете оформить оба текста так, как вам нравится.

Пример XML-кода:

<RelativeLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <CheckBox
        android:id="@+id/terms_check"
        android:text=""
        android:layout_marginLeft="10dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/terms_text"
        android:layout_toRightOf="@id/terms_check"
        android:text="I have read and agree to the "
        android:layout_marginLeft="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
    <TextView
        android:id="@+id/terms_link"
        android:layout_toRightOf="@id/terms_text"
        android:text="TERMS AND CONDITIONS"
        android:textColor="#00f"
        android:onClick="onClick"
        android:clickable="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</RelativeLayout>


Затем добавьте onClick() обработчик в коде. Вуаля.

public class SignUpActivity extends Activity {

    public void onClick(View v) {
        ...
    }  
}

Мне не понравилось решение с checkBox + textView, так как ваш пользовательский вид будет расширять ViewGroup, а не CheckBox, таким образом заставляя вас обернуть поведение CheckBox.

Для меня было важно, чтобы пользовательский CheckBox мог использоваться в xml точно так же, как обычный.

Приемлемым для меня было то, что этот CheckBox будет переключаться только когда вы нажимаете на его поле, а не на его текст.

Поэтому я расширил CheckBox, и для достижения такого поведения я использовал весь сенсорный механизм, полный код которого приведен ниже, и пояснение сразу после него для всех, кто хочет знать, как он работает.

public class CheckBoxWithLinks extends CheckBox {


public CheckBoxWithLinks(Context context) {
    super(context);
}

public CheckBoxWithLinks(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public CheckBoxWithLinks(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
}

@Override
public boolean performClick() {
    if ( !onTextClick)
        return super.performClick();
    return false;
}

private boolean onTextClick = false;
@Override
public boolean onTouchEvent(MotionEvent event) {
    onTextClick = !isLeftDrawableClick(event) && !isRightDrawableClick(event);
    return super.onTouchEvent(event);
}

private boolean isRightDrawableClick(MotionEvent event) {
    return event.getX() >= getRight() - getTotalPaddingRight();
}

private boolean isLeftDrawableClick(MotionEvent event) {
    return event.getX() <= getTotalPaddingLeft();
}
}

он основан на том факте, что метод executeClick вызывается изнутри механизмом TextView, который расширяет CheckBox, ClickableSpan также вызывается механизмом TextView. так что получается, что когда вы дотрагиваетесь до текста вашего CheckBox, он вызовет оба.

Итак, что я сделал, так это определил, был ли щелчок в текстовой области, и в этом случае мы отключим perfomClick, тем самым отключив переключатель. но кликабельный промежуток все равно будет вызываться.

Использование:

Вам все еще нужно добавить кликабельный диапазон и setMovementMethod, как и раньше, как обычный TextView.

Вот простой фрагмент кода, чтобы сделать строку флажка, охватываемую флажком, интерактивной в kotlin:

              val myText = ... // your string.
        val spannableStr = SpannableString(myText)

        val clickableText1 = object : ClickableSpan() {
            override fun onClick(widget: View) {
                widget.cancelPendingInputEvents()
                doMyWorkHere()
            }

            override fun updateDrawState(text: TextPaint) {
                super.updateDrawState(text)
                text.color = Color.RED
                text.isUnderlineText = true
            }
        }

        val clickableText2 = object : ClickableSpan() {
            override fun onClick(widget: View) {
                widget.cancelPendingInputEvents()
                doMySecondWork()
            }

            override fun updateDrawState(textPaint: TextPaint) {
                super.updateDrawState(textPaint)
                textPaint.color = COLOR.BLUE
                textPaint.isUnderlineText = false
            }
        }

        spannableStr1.setSpan(clickableText1, 10, 20, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        spannableStr2.setSpan(clickableText2, 30, 40, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)

        myCheckBox.text = spannableStr
        myCheckBox.movementMethod = LinkMovementMethod.getInstance()

Удачного кодирования :)

Если вы ищете решение с помощью URL, я предлагаю вам использовать следующее решение. С CheckBox а также TextView,

    final TextView tvTerms = (TextView)findViewById(R.id.tvTerms);

    Pattern pattern = Pattern.compile(getString(R.string.terms_and_conds));
    TransformFilter transFilter = new TransformFilter() {
    @Override
    public String transformUrl(Matcher match, String url) {
        return "";
    }}; 
    Linkify.addLinks(tvTerms, pattern, Constants.URL_TERMS_AND_CONDS, null, transFilter);

гдеURL_TERMS_AND_CONDS = "yourUrl.com"; а также R.string.terms_and_conds = идентификатор ресурса с интерактивной строкой.

Примерьте вот это

      String checkBoxText = "I agree to the Yunolearning <a href='https://blog.google/intl/en-in/' > Blogs</a> and <a href='https://about.google/stories/' > Stories</a>";

MaterialCheckBox singleCheckbox = new MaterialCheckBox(this);
singleCheckbox.setTag(formField.getName());
singleCheckbox.setText(Html.fromHtml(checkBoxText));
singleCheckbox.setMovementMethod(LinkMovementMethod.getInstance());
Другие вопросы по тегам