0

The problem:

To make a long story short, I would like to insert a custom clickable Drawable (or something that looks like a little button and acts like a single character) inside an EditText.

Research:

I've read some documentation as well as related questions and I almost achieved the result I want (see "Code" section). It's a little bit tricky, but I wasn't able to find another way out. I'm using Html.fromHtml(source, imageGetter, tagHandler) to insert the drawable I need with a link and then implementing a custom LinkMovementMethod to handle clicks on it.

But there are some things I would like to avoid:

  1. If there are no text after my drawable, it gets clicked even if I click anywhere right to it. So I'm not able to place a cursor next to it without moving it manually.
  2. On some devices the cursor appears at the very beginning of EditText every time I perform a click, except cases when I click drawable.

Code:

Inserting drawable with a link and setting the custom LinkMovementMethod:

Html.ImageGetter imgGetter = new Html.ImageGetter() {

    @Override
    public Drawable getDrawable(String source) {

        Drawable drawable = getResources().getDrawable(R.drawable.blip_icon_read);

        //Making it as small as a character
        drawable.setBounds(0, 0, (int)getTextSize(), (int)getTextSize());

        return drawable;
    }
};

String buttonSrc = "<a href='button://" + "somedata" + "'><img src=/></a>";

myEditText.append(Html.fromHtml(buttonSrc, imgGetter, null));
myEditText.setMovementMethod(MyLinkMovementMethod.getInstance(context));

Custom LinkMovementMethod:

public class MyLinkMovementMethod extends LinkMovementMethod {

    private static Context movementContext;

    private static MyLinkMovementMethod linkMovementMethod = new MyLinkMovementMethod();

    public boolean onTouchEvent(android.widget.TextView widget, android.text.Spannable buffer, android.view.MotionEvent event) {

        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP) {

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

            URLSpan[] link = buffer.getSpans(off, off, URLSpan.class);
            if (link.length != 0) {

                URI uri;

                try {
                    uri = new URI(link[0].getURL());
                } catch (URISyntaxException e) {
                    e.printStackTrace();
                    return true;
                }

                if (uri.getScheme().equals("button")) {
                    //Doing stuff here
                }
                return true;
            }
        }

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

    public static android.text.method.MovementMethod getInstance(Context c) {
        movementContext = c;
        return linkMovementMethod;
    }
}

Layout:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <EditText
        android:id="@+id/my_edit_text"

        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        android:textColor="@android:color/black"
        android:scrollbars="vertical"
        android:textCursorDrawable="@null"
        android:gravity="top" >

    </EditText>

</LinearLayout>

Questions:

  1. Is there any way to avoid things I described in the end of "Research" section using this approach?
  2. Is there another approach I should use?

Will be glad to read advices or any ideas. Thank you.

2 Answers2

0

this seems to work (unless i dont really understand your idea)

public class MyMovementMethod extends ArrowKeyMovementMethod {
    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
        int action = event.getAction();
        if (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);

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

            if (link.length != 0 && off != buffer.length()) {
                link[0].doSomething();
                return true;
            }
        }
        return super.onTouchEvent(widget, buffer, event);
    }
}

class MyClickableSpan extends ImageSpan {
    public MyClickableSpan(Bitmap b) {
        super(b);
    }
    public void doSomething() {
        Log.d(TAG, "doSomething ***********************************************");
    }
}

to test it add the following in Activity.onCreate:

LinearLayout ll = new LinearLayout(this);
EditText et = new EditText(this);
SpannableStringBuilder b = new SpannableStringBuilder();
b.append("Attach the specified markup object to the ");
int start = b.length();
b.append("x");
int end = b.length();
b.append(" range start end of the text, or move the object to that range if it was...");
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
b.setSpan(new MyClickableSpan(bitmap), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

et.setText(b);
et.setMovementMethod(new MyMovementMethod());
ll.addView(et);
setContentView(ll);
pskink
  • 23,874
  • 6
  • 66
  • 77
  • Thank you, kind sir. I will check it tomorrow. –  Oct 13 '13 at 14:51
  • Unfortunately, this approach has the same issues. Eventually, the problem is that when there is nothing in a line after my drawable, EditText places the cursor right next to it, even if I click in the end of EditText. I know, this is a normal behavior, but I wonder is there any way to prevent my button from being clicked. –  Oct 14 '13 at 05:25
  • Currently, I'm thinking about placing a space character right next to my button. This is a work around, but it will solve the "unwanted-click-issue". I accept your answer for 2 reasons: 1. It's more simple & obvious than mine. 2. Since you extend the `ArrowKeyMovementMethod`, it doesn't disable a longClick event, whereas `LinkMovementMethod` does. Thank you. –  Oct 14 '13 at 05:40
  • replace link[0].doSomething with if off != buffer.length() link[0].doSomething() – pskink Oct 14 '13 at 06:34
  • Nope, still the same. –  Oct 14 '13 at 07:02
  • log.d() the off and buffer.length() when clicking at the end, you should call doSomething if they are != – pskink Oct 14 '13 at 07:15
  • For some reasons buffer always has the same size. –  Oct 14 '13 at 07:26
  • I think I can deal with it. It's not a major problem, I faced another one: when there are more buttons than line can fit, some of them go to the next line and the rest of them turn invisible. –  Oct 14 '13 at 07:33
  • i dont understand, anyway in my orig answer there was a check if (link.length != 0), now replace it with if (link.length != 0 && off != buffer.length()) – pskink Oct 14 '13 at 07:37
  • I did so, and I also logged buffer.length() and it always has the same size. Doesn't matter where I click. As I said, I can deal with it. –  Oct 14 '13 at 07:44
  • I don't understand why some of my buttons become invisible, when EditText breaks the line to fit buttons in a two lines. –  Oct 14 '13 at 07:45
  • Ok, I figured out that it's better to use any character, except space. It seems to act a little bit different when EditText automatically breaks the line. Please edit your answer and thank you for helping. –  Oct 14 '13 at 08:09
  • ok see it now, added the check if button is at the end of text – pskink Oct 14 '13 at 08:23
  • Works fine. Approved. –  Oct 14 '13 at 10:17
0

just use a ScrollView as parentView will help you. Like this

<ScrollView
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:layout_marginBottom="45dp"
     android:layout_below="@id/header_container"
     >
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical"
         >
         <EditText
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:id="@+id/title"
             android:hint="输入标题"
             android:layout_marginStart="15dp"
             android:layout_marginLeft="15dp"
             android:layout_marginRight="15dp"
             android:layout_marginEnd="15dp"
             android:inputType="text"
             android:textSize="22sp"
             android:singleLine="true"
             android:textCursorDrawable="@color/cursor_white"
             android:background="@drawable/bg_edittext_title_white"
             android:padding="5dp"
             >
         </EditText>

         <EditText
             android:id="@+id/content"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:background="@null"
             android:gravity="start|top"
             android:layout_marginLeft="15dp"
             android:layout_marginStart="15dp"
             android:layout_marginRight="15dp"
             android:layout_marginEnd="15dp"
             android:layout_marginTop="10dp"
             android:inputType="textMultiLine"
             android:minHeight="220dp"
             android:singleLine="false"
             android:textCursorDrawable="@color/cursor_white"
             android:hint="输入内容"
             android:padding="5dp"
             />
     </LinearLayout>

 </ScrollView>