如何使复选框文本的部分可点击?

67

我想在文本框旁边的文本中创建一个链接。该链接不是URL,而是应该充当按钮,以便我可以在onItemClick事件中执行一些任务。我基本上将其连接到显示我们最终用户许可协议(硬编码)的视图。

我怎样才能做到这一点?

提前致谢。

11个回答

75

你可能只想让文本的一部分成为可点击的链接,而复选框的其他部分则像往常一样工作,即你可以单击其他文本来切换状态。

你可以这样设置你的复选框:

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());

1
太棒了,谢谢Daniel,这正是我在寻找的!有没有办法在API <19上实现cancelPendingInputEvents()? - Flyview
13
应该接受这个答案。widget.cancelPendingInputEvents() 做了魔法! - Ramachandra Reddy Avula
2
运行良好,但即使点击了ClickableSpan,CheckBox drawable的按下状态也会被选中,即使它没有切换。有什么建议可以避免这种情况吗? - devrocca
1
@devrocca 我的解决方案是使用此链接中的代码 https://gist.github.com/Jeevuz/b775e0d5f0dabdcaefe4 将涟漪效果从复选框中删除。这不是完美的解决方案,但它很好地工作,因为复选框已经有了它们的“选中/取消选中”动画。 - Mattia Ruggiero
1
这应该是被接受的答案。此外,这是@Mattia Ruggiero方法的Kotlin版本,可以去除涟漪效果:checkBox.apply { background = (background as? RippleDrawable)?.findDrawableByLayerId(0) } - Hampel Előd
显示剩余5条评论

53

以下的代码在我的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());

有人可以在低版本上尝试吗?这个解决方案非常完美! - X.X_Mass_Developer
2
@Gopinath 是的,我可以确认这个解决方案适用于 Android 2.2.2(Froyo,三星 GT-7510)及更早版本。我喜欢这个解决方案,它非常简单和优雅,确实是完美的(正如 X.X_Mass_Developer 所说)! - Mike
将此内容添加到我的应用程序中。它尝试使用您的URL字符串名称启动一个活动。 - Lou Morda
10
我在这个问题上遇到了一个 bug:按下复选框的下部分会启动 URL,而不是切换复选框。 - Xiao
2
@Xiao 这是与LinkMovementMethod有关的问题,您可以使用以下方法来解决:https://github.com/saket/Better-Link-Movement-Method - pauminku

37

实际上有一种优雅的解决方案,使用CheckBox和单个TextView。结合TextView.setClickable()、Intent Filter和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。我使用AndroidManifest.xml中的intent-filter来实现这一点。

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

它捕获 VIEW 命令,仅过滤以 id.web.freelancer.example.TCActivity:// 开头的 URL。

这里提供了一个包供您尝试这是 github 存储库。希望这可以帮到您。


抱歉,但要实现真正的优雅,您不能错过@Gopinath在下面提供的三行解决方案 - 它只使用一个控件并完成了所有所需的操作。 - Mike
@mike:在内部,它执行相同的三行解决方案。但是,我的解决方案不会打开网页,而是打开另一个Activity。 - ariefbayu
在文本框中添加android:gravity="top"以使复选框与文本对齐。 - M. Usman Khan
多行文本在底部会被裁剪,除非您添加 android:gravity="fill"。同时考虑添加 style="?android:checkboxStyle" - gladed
1
当您在<data>中传递大写字母时,您将收到警告“方案匹配区分大小写,应仅使用小写字符”。您可以使用id.web.freelancer.example.termsactivity并将其传递给<a href=''>中的相同内容。 - Pratik Butani
显示剩余5条评论

12

使用 Kotlin (通过扩展函数)翻译自 Daniel Schuler 的回答:

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()
}

伙计,这个扩展太棒了! - Muzafferus
2
我尝试过这个方法,它会使整个复选框可点击,并忽略 Spannable 部分。 - Jono

6
我遇到了同样的问题,希望能在复选框文本中拥有多个可点击链接,而不会失去在文本中任何位置(没有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的movement method设置为LinkMovementMethod:

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

接下来是比较棘手的部分。目前,当按下TextView时,CheckBox并不会被选中。为了实现这一点,我向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()正如Dheeraj V.S.指出的那样。。由于我的目标是API级别8+,因此我使用了我在这里找到的实现:确定在Android TextView中单击哪个单词

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);
}

5

要求:

只有部分文本作为可点击链接,而复选框的其余部分则像通常一样工作:

  1. 在单击链接时防止复选框状态被切换
  2. 从复选框中删除涟漪效果

这是 Kotlin 版本:

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()
        }
    })
}

这是无法访问的。Talkback用户无法切换复选框,链接每次都会打开。请注意,我还没有检查其他解决方案。我怀疑这个问题可能也适用于它们。 - undefined

3
创建一个没有文本的 CheckBox,并在其旁边添加两个 TextView。第一个视图是不可点击的,并带有像“我已阅读并同意”这样的文本。第二个是可点击的,并带有像“条款和条件”这样的文本。将这两个 TextView 并排放置,不要留有任何间距。请注意第一个视图末尾的额外空间,以实现自然文本对齐。这样你就可以根据自己的喜好来设计这两个文本了。
示例 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() 处理程序。Voilá。
public class SignUpActivity extends Activity {

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

这是个好主意。但理想情况下,只有相邻文本的一部分应该可点击。例如:“我已阅读并同意条款和条件”。 - PaulG
如何将两个“TextView”并排添加:一个不可点击的“我已阅读并同意”和紧挨着它的一个可点击的“条款和条件”。请注意第一个视图末尾的额外空间,以便进行自然文本对齐。通过这种方式,您可以根据自己的喜好设置两个文本的样式。 - Jarno Argillander
是的,你需要自己处理边距。这在很大程度上取决于应用程序。 - Jarno Argillander
你可以从CheckBox继承自己的类并尝试这种方式,或者下载Android源代码并直接修改CheckBox类。不过我认为这两种方法都需要太多时间了 :) - Jarno Argillander
1
这对于国际化(i18n)来说是一场噩梦。 - Joey
显示剩余5条评论

1
我不喜欢使用checkBox + textView的解决方案,因为你的自定义视图将扩展ViewGroup而不是CheckBox,从而迫使你包装CheckBox行为。
对我来说很重要的是,自定义CheckBox可以在xml中像普通的CheckBox一样使用。
对我来说,可接受的行为是,只有在按下它的框时才会切换此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();
}
}

它依赖于 performClick 方法在 TextView 机制内部被调用,而 CheckBox 扩展了此方法,ClickableSpan 也是由 TextView 机制调用的。 因此,当您触摸 CheckBox 的文本时,它将同时调用两者。
所以,我所做的就是检测是否点击了文本区域,如果是,则禁用 perfomClick,从而禁用切换。但 clickable span 仍将被调用。
用法:
您仍然需要添加 clickable span 并像常规 TextView 一样设置 setMovementMethod。

0

这里是一个简单的代码片段,用于在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()

编程愉快:)


0

请试试这个

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());

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接