18 July 2015

วิธีการทำให้ TextView สามารถกดเลือกที่ข้อความบางส่วนได้

Updated on


        เคยมั้ย? อยากจะทำให้ Text View กดแล้วทำอะไรบางอย่างได้? ใช่ มันเป็นเรื่องง่ายมาก ก็แค่ใช้ OnClickListener มากำหนดให้กับ Text View ก็ทำได้แล้ว

        แต่เคยมั้ย? อยากจะทำให้กดได้เฉพาะบางข้อความเท่านั้น? ทำได้นะเออ

        เนื้อหาในบทความนี้เจ้าของบล็อกอ้างอิงจาก Advance Android TextView ของ Chiuki นะครับ

        ในการทำให้ Text View กดได้เฉพาะบางข้อความนั้น ทำได้ตั้งแต่แรกแล้ว เพียงแต่ว่าจะมีขั้นตอนที่ยุ่งยากเล็กน้อย

เตรียมข้อความไว้ให้พร้อม

        เพื่อความถูกต้องของตัวอย่างนี้ เจ้าของบล็อกจึงสร้างข้อความไว้ใน String Resource นะครับ โดยข้อความที่จะต้องกำหนดมีอยู่ 2 ข้อความด้วยกันคือ

        • ข้อความทั้งหมดที่ต้องการแสดงใน Text View
        • ข้อความที่ต้องการให้กดได้

        ยกตัวอย่างเช่น

strings.xml
<resources>
    <string name="app_name">ClickableTextView</string>

    <string name="msg_sample">Hey! You can click here.</string>
    <string name="msg_linkable">here</string>
    <string name="yeah">Yeah!</string>

</resources>

        เจ้าของบล็อกต้องการให้ Text View แสดงข้อความว่า Hey! You can click here. โดยคำว่า here จะสามารถกดได้

กำหนดสีของข้อความตามต้องการ

        โดยปกติแล้วข้อความที่กำหนดได้จะมีสีแตกต่างจากข้อความปกติและมีขีดเส้นใต้ ซึ่งสีจะขึ้นอยู่กับว่าใช้แอนดรอยด์เวอร์ชันอะไร แต่เพื่อความดัดจริตของชีวิตนิดหน่อย เจ้าของบล็อกก็เลยอยากจะเปลี่ยนสีให้เป็นสีที่ต้องการ

        โดยสามารถกำหนดสีได้สองอย่างคือ

        • สีของข้อความที่กดได้
        • สีพื้นหลังของข้อความเมื่อถูกกด (Highlight)

        เจ้าของบล็อกก็เลยสร้าง Color Resource ขึ้นมาเพื่อกำหนดค่าสีส้มลงไป ส่วนตัวหนังสือปกติเป็นสีเทา และพื้นหลังตอนกดเป็นสีออกเหลือง

colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <color name="text">#434343</color>
    <color name="text_link">#ff6c00</color>
    <color name="text_link_highlight">#ffcc00</color>

</resources>

เตรียม Text View ให้พร้อม

        จริงๆก็ไม่มีอะไรมากแค่แปะ Text View ที่ต้องการไว้ใน Layout นั่นเอง แล้วให้กำหนดค่าสีของตัวหนังสือด้วย ส่วนข้อความที่แสดงในตอนนี้จะไม่มีผลอะไร จะเป็นข้อความอะไรก็ได้ เพราะเดี๋ยวจะถูกกำหนดใหม่ผ่านโค๊ดอีกที

activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center">

    <TextView
        android:id="@+id/text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/msg_sample"
        android:textColor="@color/text"
        android:textColorHighlight="@color/text_link_highlight"
        android:textColorLink="@color/text_link"
        android:textSize="@dimen/text_size" />

</RelativeLayout>


เพิ่มคำสั่งเพื่อทำให้คำนั้นๆสามารถกดได้

        หลักการคือใช้คลาส SpannableString เพื่อกำหนดว่าข้อความช่วงไหนที่กดได้ แล้วค่อยเอาไปกำหนดให้กับ TextView ใหม่

        อย่างแรกสุดจะต้องประกาศคลาสที่จะให้ทำงานเมื่อข้อความถูกกด โดยให้สืบทอดคลาสมาจาก ClickableSpan ดังนั้นเจ้าของบล็อกก็ยกตัวอย่างประมาณนี้

private class CallToast extends ClickableSpan {
    @Override
    public void onClick(View widget) {
        Toast.makeText(MainActivity.this, "Yeah!", Toast.LENGTH_SHORT).show();
    }
}

        เอาแบบง่ายๆเลยคลาสชื่อ CallToast เวลาทำงานก็จะแสดง Toast ขึ้นมาเป็นคำว่า Yeah!

        แต่ความยุ่งยากจริงๆคือตอนกำหนดจะต้องกำหนดเป็นช่วง Index ของคำที่กดได้ ว่าคำนั้นอยู่ตรงไหนในข้อความ ดังนั้นเจ้าของบล็อกจึงใช้ String Resource ที่เตรียมไว้นี่ล่ะ เพื่อหาช่วง Index ของคำที่ต้องการให้กดได้

        ก่อนอื่นดึงข้อความจาก String Resource มาเก็บไว้ใน String ซะ

String text = getString(R.string.msg_sample);
String linkText = getString(R.string.msg_linkable);


        และเพื่อไม่ให้สับสน เจ้าของบล็อกจึงหาก่อนว่าไอคำที่ต้องการเนี่ย มันอยู่ใน Index ที่เท่าไรของข้อความทั้งหมด

String text = getString(R.string.msg_sample);
String linkText = getString(R.string.msg_linkable);
int start = text.indexOf(linkText);
int end = start + linkText.length();

        ถ้ายังนึกไม่ออกว่า start กับ end มาจากไหนให้ลองดูภาพนี้


        คำสั่ง indexOf จะหาลำดับของคำว่าคำๆนั้นอยู่ที่ลำดับเท่าไร ดังนั้นตัวแปร start ก็จะมีค่าเป็น 19 เพราะว่าคำว่า here อยู่ลำดับที่ 19 จากข้อความทั้งหมด

        ส่วนตัวแปร end จะเป็นการเอาตัวแปร start มาบวกกับจำนวนตัวอักษร ก็จะได้เป็น 19 + 4 หรือก็คือ 23 นั่นเอง

        เมื่อพร้อมแล้วก็สร้าง SpanableString ขึ้นมา

SpannableString spannableString = new SpannableString(text);

        โดยตอนสร้าง Instance ตัวนี้ก็กำหนดข้อความทั้งหมดลงไปด้วย (เอา text ที่เก็บข้อความทั้งหมดไว้ มากำหนดนั่นเอง)

        จากนั้นก็เริ่มทำการกำหนดช่วงคำที่อยากจะให้กดได้ซะ โดยใช้คำสั่ง setSpan แล้วเอาตัวแปร start กับ end ที่เตรียมไว้มากำหนดลงไป

SpannableString spannableString = new SpannableString(text);
spannableString.setSpan(new CallToast(), start, end, 0);

        จะเห็นว่าในคำสั่ง setSpan เจ้าของบล็อกมีการสร้าง Instance ของ CallToast ขึ้นมาด้วย จากนั้นก็กำหนดตัวแปร start และ end ลงไป ส่วน 0 คือ Flag กำหนดการทำงาน อันนี้ไม่ต้องสนใจอะไร

        ทำไม end ถึงเป็น 23 ทั้งๆที่ตัวอักษรตัวสุดท้ายของคำว่า here คือ 22?

        คำสั่ง setSpan จะกำหนดช่วง Index ด้วยการกำหนด Index เริ่มต้น (นั่นก็คือ start) และกำหนดว่าจะสิ้นสุดที่เท่าไร (นั่นก็คือ end) เพียงแต่ว่าตัวที่ 23 จะไม่มีผล แค่บอกว่าจะสิ้นสุดที่ตัวที่เท่าไร ดังนั้นช่วง Index ที่มีผลจริงๆคือตัวที่ 19 ถึง 22

        เอ้า!! พร้อมแล้ว เอา SpannableString มากำหนดใน Text VIew ได้เลย

TextView textView = (TextView) findViewById(R.id.text_view);
textView.setText(spannableString);
textView.setMovementMethod(new LinkMovementMethod());
        อย่าลืมคำสั่ง setMovementMethod เพราะคำสั่งนี้จะทำให้ Text View สามารถกดที่ข้อความบางส่วนได้ ดังนั้นถ้าลืมคำสั่งนี้ไปก็จะทำให้ Text View กดเลือกคำที่กำหนดไว้ใน SpannableString ไม่ได้

         สรุปโค๊ด

MainActivity.java
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        String text = getString(R.string.msg_sample);
        String linkText = getString(R.string.msg_linkable);
        int start = text.indexOf(linkText);
        int end = start + linkText.length();

        SpannableString spannableString = new SpannableString(text);
        spannableString.setSpan(new CallToast(), start, end, 0);

        TextView textView = (TextView) findViewById(R.id.text_view);
        textView.setText(spannableString);
        textView.setMovementMethod(new LinkMovementMethod());
    }

    private class CallToast extends ClickableSpan {
        @Override
        public void onClick(View widget) {
            Toast.makeText(MainActivity.this, R.string.yeah, Toast.LENGTH_SHORT).show();
        }
    }
}

ทดสอบการทำงาน

         เมื่อลองทดสอบดูก็จะเห็นคำว่า here เป็นตัวอักษรสีส้ม และเมื่อกดที่คำนั้นก็จะแสดง Toast ขึ้นมาตามที่กำหนดไว้




ข้อควรรู้

        ข้อความที่สามารถกดได้ สามารถมีได้มากกว่าหนึ่งคำ เพราะคำสั่ง setSpan สามารถกำหนดเพิ่มเข้าไปได้เรื่อยๆ ดังนั้นจึงทำให้ Text View หนึ่งตัวสามารถกดเลือกที่ข้อความได้หลายที่ (แต่กำหนดสีแยกกันไม่ได้นะ)



        สิ่งที่ควรระวังคือ "ข้อความซ้ำ" เพราะว่าในตัวอย่างนี้ใช้วิธีกำหนดข้อความโดยใช้วิธีค้นหา Index ของคำนั้นๆ นั่นก็หมายความว่าถ้ามีคำที่เหมือนกันอยู่ในข้อความทั้งหมด ก็จะไม่มีผลอะไร จะมีผลแค่คำแรกสุดเท่านั้น



        ซึ่งจริงๆแล้วก็สามารถแก้ไขได้ เพราะคำสั่ง indexOf ที่ให้ค้นหาคำสามารถกำหนดได้ว่าจะเริ่มค้นหาคำตั้งแต่ Index ที่เท่าไร แต่เอาเข้าจริงเวลาเขียนโค๊ดก็จะค่อนข้างวุ่นวายพอสมควร

String text = getString(R.string.msg_sample);
String linkText = getString(R.string.msg_linkable);
int start = text.indexOf(linkText);
int end = start + linkText.length();

int start2 = text.indexOf(linkText, end);
int end2 = start2 + linkText.length();

SpannableString spannableString = new SpannableString(text);
spannableString.setSpan(new CallToast(), start, end, 0);
spannableString.setSpan(new CallSomething(), start2, end2, 0);

TextView textView = (TextView) findViewById(R.id.text_view);
textView.setText(spannableString);
textView.setMovementMethod(new LinkMovementMethod());


        แต่ถ้าเป็นไปได้ก็แนะนำให้ใช้คนละคำดีกว่านะ

แหล่งอ้างอิงข้อมูล

        • Advance Android TextView [Chiuki]