web-dev-qa-db-ja.com

android ClickableSpanがクリックイベントをインターセプトします

レイアウトにTextViewがあります。とても簡単です。 OnClickListenerをレイアウトに配置し、TextViewの一部をClickableSpanに設定します。 ClickableSpanがクリックされたときにonClick関数で何かを実行し、TextViewの他の部分がクリックされたときに、レイアウトのOnClickListenerのonClick関数で何かを実行する必要があります。これが私のコードです。

    RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
    l.setOnClickListener(new OnClickListener(){
        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "whole layout", Toast.LENGTH_SHORT).show();
        }
    });

    TextView textView = (TextView)findViewById(R.id.t1);
    textView.setMovementMethod(LinkMovementMethod.getInstance());

    SpannableString spannableString = new SpannableString(textView.getText().toString());
    ClickableSpan span = new ClickableSpan() {
        @Override
        public void onClick(View widget) {
            Toast.makeText(MainActivity.this, "just Word", Toast.LENGTH_SHORT).show();
        }
    };
    spannableString.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);        
    textView.setText(spannableString);
28
강윤식

私もこの問題に遭遇しました。@ KMDevのソースコードのおかげで、よりクリーンなアプローチを思い付きました。

まず、部分的にクリック可能にするTextViewしかないので、実際には、キーの押下やスクロールなどを処理する機能を追加するLinkMovementMethod(およびそのスーパークラスScrollingMovementMethod)のほとんどの機能は必要ありません。

代わりに、MovementMethodOnTouch()コードを使用するカスタムLinkMovementMethodを作成します。

ClickableMovementMethod.Java

_package com.example.yourapplication;

import Android.text.Layout;
import Android.text.Selection;
import Android.text.Spannable;
import Android.text.method.BaseMovementMethod;
import Android.text.method.LinkMovementMethod;
import Android.text.style.ClickableSpan;
import Android.view.MotionEvent;
import Android.widget.TextView;

/**
 * A movement method that traverses links in the text buffer and fires clicks. Unlike
 * {@link LinkMovementMethod}, this will not consume touch events outside {@link ClickableSpan}s.
 */
public class ClickableMovementMethod extends BaseMovementMethod {

    private static ClickableMovementMethod sInstance;

    public static ClickableMovementMethod getInstance() {
        if (sInstance == null) {
            sInstance = new ClickableMovementMethod();
        }
        return sInstance;
    }

    @Override
    public boolean canSelectArbitrarily() {
        return false;
    }

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {

        int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {

            int x = (int) event.getX();
            int y = (int) event.getY();
            x -= widget.getTotalPaddingLeft();
            y -= widget.getTotalPaddingTop();
            x += widget.getScrollX();
            y += widget.getScrollY();

            Layout layout = widget.getLayout();
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);

            ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
            if (link.length > 0) {
                if (action == MotionEvent.ACTION_UP) {
                    link[0].onClick(widget);
                } else {
                    Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
                            buffer.getSpanEnd(link[0]));
                }
                return true;
            } else {
                Selection.removeSelection(buffer);
            }
        }

        return false;
    }

    @Override
    public void initialize(TextView widget, Spannable text) {
        Selection.removeSelection(text);
    }
}
_

次に、このClickableMovementMethodを使用すると、タッチイベントは移動メソッドによって消費されなくなります。ただし、TextView.setMovementMethod()を呼び出すTextView.fixFocusableAndClickableSettings()は、クリック可能、ロングクリック可能、フォーカス可能をtrueに設定し、View.onTouchEvent()がタッチイベントを消費するようにします。これを修正するには、3つの属性をリセットするだけです。

したがって、ClickableMovementMethodに付随する最後のユーティリティメソッドは次のとおりです。

_public static void setTextViewLinkClickable(TextView textView) {
    textView.setMovementMethod(ClickableMovementMethod.getInstance());
    // Reset for TextView.fixFocusableAndClickableSettings(). We don't want View.onTouchEvent()
    // to consume touch events.
    textView.setClickable(false);
    textView.setLongClickable(false);
}
_

これは私にとって魅力のように機能します。

ClickableSpansのクリックイベントが発生し、それらの外側のクリックが親レイアウトリスナーに渡されます。

TextViewを選択可能にする場合、そのケースについてはテストしていません。ソースを自分で調べる必要があるかもしれません:P

20
Hai Zhang

あなたの質問に対する最初の答えは、user2558882が指摘するようにクリックイベントを消費するTextViewにクリックリスナーを設定していないということです。 TextViewにクリックリスナーを設定すると、ClickableSpansのタッチ領域の外側の領域が期待どおりに機能することがわかります。ただし、ClickableSpansの1つをクリックすると、TextViewのonClickコールバックも発生することがわかります。両方の火事があなたにとって問題である場合、それは私たちを難しい問題に導きます。 user2558882の応答では、Textableの前にClickableSpanのonClickコールバックが発生することを保証できません。以下は、より適切に実装された 類似のスレッド からのソリューションと、ソースからの説明です。そのスレッドはほとんどのデバイスで機能するはずですが、その回答のコメントには、特定のデバイスに問題があることが記載されています。カスタムのキャリア/メーカーUIを備えた一部のデバイスが原因であるように見えますが、それは推測です。

では、なぜonClickコールバック順序を保証できないのでしょうか? TextView(Android 4.3)のソースを見ると、onTouchEventメソッドでは、boolean superResult = super.onTouchEvent(event);の前にhandled |= mMovement.onTouchEvent(this, (Spannable) mText, event);(super is View)が呼び出されていることがわかります。移動メソッドを呼び出し、ClickableSpanのonClickを呼び出します。スーパー(ビュー)のonTouchEvent(..)を見ると、次のことがわかります。

_    // Use a Runnable and post this rather than 
    // performClick directly. This lets other visual 
    // of the view update before click actions start.
    if (mPerformClick == null) {
        mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) { // <---- In the case that this won't post, 
        performClick();         //    it'll fallback to calling it directly
    }
_

performClick()は、クリックリスナーセット(この場合は、TextViewのクリックリスナー)を呼び出します。つまり、onClickコールバックが発生する順序がわかりません。あなたが知っていることは、あなたのClickableSpanとTextViewクリックリスナーが呼び出されるということです。前述のスレッドでの解決策は、フラグを使用できるように順序を確実にするのに役立ちます。

多くのデバイスとの互換性を確保することが優先事項である場合は、レイアウトを再検討して、この状況で行き詰まらないようにすることができるかどうかを確認することをお勧めします。このようなスカートケースには通常、多くのレイアウトオプションがあります。

コメント回答用に編集:

TextViewがonTouchEventを実行すると、LinkMovementMethodのonTouchEventを呼び出して、さまざまなClickableSpanのonClickメソッドの呼び出しを処理できるようにします。 LinkMovementMethodは、onTouchEventで次のことを行います。

_    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer,
                            MotionEvent event) {
        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP ||
            action == MotionEvent.ACTION_DOWN) {
            int x = (int) event.getX();
            int y = (int) event.getY();

            x -= widget.getTotalPaddingLeft();
            y -= widget.getTotalPaddingTop();

            x += widget.getScrollX();
            y += widget.getScrollY();

            Layout layout = widget.getLayout();
            int line = layout.getLineForVertical(y);
            int off = layout.getOffsetForHorizontal(line, x);

            ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);

            if (link.length != 0) {
                if (action == MotionEvent.ACTION_UP) {
                    link[0].onClick(widget);
                } else if (action == MotionEvent.ACTION_DOWN) {
                    Selection.setSelection(buffer,
                                           buffer.getSpanStart(link[0]),
                                           buffer.getSpanEnd(link[0]));
                }

                return true;
            } else {
                Selection.removeSelection(buffer);

            }
         }

        return super.onTouchEvent(widget, buffer, event);
    }
_

MotionEventを受け取り、アクション(ACTION_UP:指を離す、ACTION_DOWN:指を押し下げる)、タッチが発生した場所のx座標とy座標を取得し、どの行番号とオフセット(テキスト内の位置)を見つけるかがわかります。タッチヒット。最後に、そのポイントを含むClickableSpansがある場合、それらが取得され、それらのonClickメソッドが呼び出されます。親レイアウトへのタッチを渡したいので、タッチしたときに行うすべてのことを実行したい場合はレイアウトonTouchEventを呼び出すか、必要な機能を実装している場合はクリックリスナーを呼び出すことができます。これを行う場所は次のとおりです。

_         if (link.length != 0) {
                if (action == MotionEvent.ACTION_UP) {
                    link[0].onClick(widget);
                } else if (action == MotionEvent.ACTION_DOWN) {
                    Selection.setSelection(buffer,
                                           buffer.getSpanStart(link[0]),
                                           buffer.getSpanEnd(link[0]));
                }

                return true;
            } else {
                Selection.removeSelection(buffer);

                // Your call to your layout's onTouchEvent or it's 
                //onClick listener depending on your needs

            }
         }
_

確認するには、LinkMovementMethodを拡張する新しいクラスを作成し、onTouchEventメソッドをオーバーライドします。このソースをコピーして、私がコメントした正しい位置に呼び出しを貼り付け、TextViewの移動メソッドをこの新しいサブクラスに設定していることを確認します。設定する必要があります。

副作用を回避するために再度編集しましたScrollingMovementMethodのソース(LinkMovementMethodの親)を確認すると、静的メソッドを呼び出すデリゲートメソッドであることがわかりますreturn Touch.onTouchEvent(widget, buffer, event);つまり、これをメソッドの最後の行として追加するだけで、貼り付けたものと重複するスーパー(LinkMovementMethod's)のonTouchEvent実装を呼び出さずに済み、他のイベントが予想どおりに失敗する可能性があります。

17
KMDev

これは簡単な解決策であり、私にとってはうまくいきました

これは、TextviewクラスのgetSelectionStart()およびgetSelectionEnd()関数の回避策を使用して実現できます。

tv.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ClassroomLog.log(TAG, "Textview Click listener ");
        if (tv.getSelectionStart() == -1 && tv.getSelectionEnd() == -1) {
            //This condition will satisfy only when it is not an autolinked text
            //Fired only when you touch the part of the text that is not hyperlinked 
        }
    }
});
3
Lahiru Pinto