13 ตุลาคม 2555

[Android Code] Circle Selector การเลือกค่าแบบหมุน


วันนี้วันหยุดอยากผ่อนคลายบ้างก็เลยบ้าจี้นั่งเขียนแอพตัวนี้เล่นๆ
สำหรับปุ่มหรือ Spinner หรือ Number Picker อาจจะดูธรรมดาๆไป
เจ้าของบล็อกก็เลยบ้าจี้ลองเขียนปุ่มเลือกค่าแบบใหม่ดู
โดยจะทำเป็นปุ่มรูปเฟืองแล้วมีค่าแสดงอยู่ตรงกลาง
เวลาที่หมุนปุ่มเป็นวงกลมค่าที่เลือกก็จะเปลี่ยนไปเรื่อยๆ
อธิบายยากแฮะ ไม่รู้ว่าเค้าเรียกว่าอะไรกันซะด้วยสิ ดูวีดีโอละกัน



ก็จะเห็นว่าจากปุ่มธรรมดาๆ กลายมาเป็นปุ่มแบบหมุนเพื่อเลือกค่าแล้ว
โดยเจ้าของบล็อกจะเขียนเป็นคลาสไว้นะ จะได้เอาไปใช้ง่ายๆ
เจ้าของบล็อกขอเรียกว่า Circle Selector ไปก่อนละกันนะ
สำหรับ Circle Selector จะต้องใช้ Widget สองตัวคือ ImageView กับ TextView
โดยที่ ImageView เป็นภาพที่ต้องการให้แสดงเป็นปุ่มนั่นแหละ
และ TextView สำหรับแสดงค่าที่เลือก สำหรับโค๊ดก็จะมีดังนี้


CircleSelector.java
package app.akexorcist.circleselector;

import android.content.Context;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.animation.Animation;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.TextView;

public class CircleSelector {
    public int SCROLL_SPEED = 50;
    public int MAX_VALUE = 100;
    public int MIN_VALUE = 0;
    public float VALUE = 50;
    private ImageView mImageView;
    private TextView mTextView;
    private Context mContext;
    private int animAngle = 0;

    public CircleSelector(Context context, TextView tv, ImageView iv) {
        mContext = context;
        mTextView = tv;
        mImageView = iv;
        setListener();
    }
    
    public CircleSelector(Context context, TextView tv, ImageView iv
            , int speed) {
        mContext = context;
        mTextView = tv;
        mImageView = iv;
        setScrollSpeed(speed);
        setListener();
    }
    
    public CircleSelector(Context context, TextView tv, ImageView iv
            , int min, int max, int init) {
        mContext = context;
        mTextView = tv;
        mImageView = iv;
        setInitialValue(min, max, init);
        setListener();
    }
    
    public CircleSelector(Context context, TextView tv, ImageView iv
            , int min, int max, int init, int speed) {
        mContext = context;
        mTextView = tv;
        mImageView = iv;
        setScrollSpeed(speed);
        setInitialValue(min, max, init);
        setListener();
    }
    
    public void setValue(int value) {
        if(VALUE > MAX_VALUE)
            VALUE = MAX_VALUE;
        else if(VALUE < MIN_VALUE)
            VALUE = MIN_VALUE;
        else 
            VALUE = value;
    }
    
    public int getValue() {
        return (int)VALUE;
    }
    
    public void setScrollSpeed(int speed) {
        if(speed > 100)
        SCROLL_SPEED = speed;
    }
    
    public int getScrollSpeed() {
        return SCROLL_SPEED;
    }
    
    public void setInitialValue(int min, int max, int init) {
        if(min <= max) 
            MIN_VALUE = min;
        else 
            MIN_VALUE = max;
        MAX_VALUE = max;    
        VALUE = init;
        if(VALUE > MAX_VALUE)
            VALUE = MAX_VALUE;
        else if(VALUE < MIN_VALUE)
            VALUE = MIN_VALUE;
    }
    
    public void setImageView(ImageView iv) {
        mImageView = iv;
        setListener();
    }
    
    public ImageView getImageView() {
        return mImageView;
    }
    
    public void setTextView(TextView tv) {
        mTextView = tv;
    }
    
    public TextView getTextView() {
        return mTextView;
    }
    
    public double cal_angle(float x, float y) {
        if(x >= 0 && y >= 0)
            return Math.toDegrees(Math.atan(y / x));
        else if(x < 0 && y >= 0)
            return Math.toDegrees(Math.atan(y / x)) + 180;
        else if(x < 0 && y < 0)
            return Math.toDegrees(Math.atan(y / x)) + 180;
        else if(x >= 0 && y < 0) 
            return Math.toDegrees(Math.atan(y / x)) + 360;
        return 0;
    }
    
    private void setListener() {
        mImageView.setOnTouchListener(new OnTouchListener() {
            double angle;
            public boolean onTouch(View arg0, MotionEvent arg1) {
                float x = arg1.getX() - (mImageView.getWidth() / 2);
                float y = arg1.getY() - (mImageView.getHeight() / 2);
                if(arg1.getAction() == MotionEvent.ACTION_DOWN) {
                    angle = cal_angle(y, x);
                } else if(arg1.getAction() == MotionEvent.ACTION_MOVE 
                        || arg1.getAction() == MotionEvent.ACTION_UP) {
                    if(angle - cal_angle(y, x) > 300) {
                        VALUE += ((angle - cal_angle(y, x)) - 360) 
                                * SCROLL_SPEED / 1000;
                        if(arg1.getAction() == MotionEvent.ACTION_MOVE)
                            animate((angle - cal_angle(y, x)) - 360);
                    } else if(angle - cal_angle(y, x) < -300) {
                        VALUE += ((angle - cal_angle(y, x)) + 360) 
                                * SCROLL_SPEED / 1000;
                        if(arg1.getAction() == MotionEvent.ACTION_MOVE)
                            animate((angle - cal_angle(y, x)) + 360);
                    } else {
                        VALUE += (angle - cal_angle(y, x)) 
                                * SCROLL_SPEED / 1000;
                        if(arg1.getAction() == MotionEvent.ACTION_MOVE)
                            animate(angle - cal_angle(y, x));
                    }
                    
                    if(VALUE < MIN_VALUE)
                        VALUE = MIN_VALUE;
                    else if(VALUE > MAX_VALUE)
                        VALUE = MAX_VALUE;
                    angle = cal_angle(y, x);
                }
                mTextView.setText(String.valueOf((int)VALUE));
                return true;
            }
        });
    }
    
    public void animate(double offset) {
        Animation animation = new RotateAnimation(animAngle
                , animAngle + (float)offset
                , mImageView.getWidth() / 2
                , mImageView.getHeight() / 2);
        animation.setDuration(50);
        animation.setInterpolator(new DecelerateInterpolator());
        animation.setFillEnabled(true);
        animation.setFillBefore(true);
        animAngle += offset;
        mImageView.startAnimation(animation);
    }
}

สำหรับตัวแปรเบื้องต้นก็จะมีดังนี้
SCROLL_SPEED ความเร็วในการหมุนปุ่ม ยิ่งเยอะค่าก็จะเปลี่ยนไปไว
MAX_VALUE ค่าสูงสุดที่สามารถเลือกได้
MIN_VALUE ค่าต่ำสุดที่สามารถเลือกได้
VALUE ค่าที่เลือกอยู่
mImageView สำหรับเก็บ ImageView ที่ส่งเข้ามาตอนเรียกใช้คลาสนี้
mTextView สำหรับเก็บ TextView ที่ส่งเข้ามาตอนเรียกใช้คลาสนี้
animAngle ใช้สำหรับการหมุนปุ่มว่าจะหมุนไปกี่องศา (อ้างอิงจากนิ้ว)

การสร้าง Object ของคล่าส CircleSelector เจ้าของบล็อกทำไว้ 4 แบบ
โดยเจ้าของบล็อกเขียนเผื่อไว้ 4 แบบเพื่อผู้ที่หลงเอาไปใช้จะเลือกได้

แบบแรก Context, TextView, ImageView
แบบที่สอง Context, TextView, ImageView, Speed
แบบที่สาม Context, TextView, ImageView, Min, Max, Value
แบบที่สี่ Context, TextView, ImageView, Min, Max, Value, Speed

สำหรับฟังก์ชัน setValue เอาไว้กำหนดค่าที่เลือกว่าจะให้เป็นค่าอะไร
ถ้ามีการใส่ค่าเกินค่าสูงสุดหรือต่ำสุดก็จะมีการปรับให้ค่าไม่เกินที่กำหนด

ฟังก์ชัน getValue เอาไว้อ่านค่าที่เลือก หรือก็คือค่าที่แสดงบน TextView 

ฟังก์ชัน setScrollSpeed เอาไว้กำหนดความเร็วในการหมุนปุ่ม

ฟังก์ชัน getScrollSpeed เอาไว้อ่านค่าความเร็วในการหมุนปุ่ม

ฟังก์ชัน setInitialValue เอาไว้กำหนดค่า Max, Min และค่า Value เริ่มต้น

ฟังก์ชัน setImageView เอาไว้กำหนด ImageView ใหม่

ฟังก์ชัน getImageView สำหรับส่ง ImageView ที่ถูกใช้ในคลาสนี้

ฟังก์ชัน setTextView สำหรับกำหนด TextView ใหม่

ฟังก์ชัน getTextView สำหรับส่ง TextView ที่ถูกใช้ในคลาสนี้

ฟังก์ชัน cal_angle อันนี้ใช้สำหรับคำนวณองศาระหว่าง
จุดที่นิ้วสัมผัส เทียบกับกลางปุ่ม โดยคำนวณออกมาเป็นองศา Degree
ซึ่งจะเป็นการคำนวณหาองศาจาก arctan โดยใช้ค่าพิกัดที่ได้มาคำนวณ
และค่าที่ได้จะเป็น Radian จึงต้องแปลงให้เป็น Degree เพื่อให้ได้องศา Degree

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



แต่สำหรับ Quadrant ปกติค่า Y+ จะชี้ไปข้างบน และ X+ จะชี้ไปทางขวา
ที่ใช้กันทั่วไปในกราฟ หรือก็คือจุด 0, 0 จะเริ่มจากทางซ้ายล่าง
แต่สำหรับพิกัดของเหล่าโปรแกรมทั้งหลายจะมีจุด 0, 0 เริ่มจากซ้ายบน
ทำให้ Quadrant ที่ใช้ในคลาสนี้ต้องหมุนตามเข็ม 90 องศาจากปกติ เป็นแบบนี้



ทีนี้มาดู Quadrant อันที่ใช้ในคลาสนี้แทน แบบปกติทิ้งไป
ในฟังก์ชัน cal_angle ก็จะแบ่ง if ตามช่วงต่างๆ เป็น 4 ช่วง
โดยที่ Quadrant ที่ 1 จะเป็นเงื่อนไข x >= 0 && y >= 0
ซึ่งที่ฝั่งนี้ก็จะเป็นค่าปกติ มีตั้งแต่ 0 ถึง 90 ก็คำนวณปกติไป
สำหรับ Quadrant ที่ 2 จะเป็นเงื่อนไข x < 0 && y >= 0
ซึ่งจะเป็นช่วงที่คำนวณแล้วจะได้ค่าตั้งแต่ -90 ถึง 0
ค่าจะติดลบ แต่เจ้าของบล็อกจะให้มีค่าตั้งแต่ 90 ถึง 180
ก็จะให้บวกเพิ่มเข้าไปอีก 180 เพื่อให้ค่าอยู่ในช่วงที่ต้องการ
สำหรับ Quadrant ที่ 3 จะเป็นเงื่อนไข x < 0 && y < 0
ค่าที่ได้จะเป็นบวก แต่ก็จะมีค่าตั้งแต่ 0 ถึง 90 
ซึ่งเจ้าของบล็อกจะให้มันมีค่าตั้งแต่ 180 ถึง 270
ก็จะให้บวกเพิ่มเข้าไปอีก 180 เพื่อให้ค่าอยู่ในช่วงที่ต้องการ
สำหรับ Quadrant ที่ 4 จะเป็นเงื่อนไข x >= 0 && y < 0
ค่าที่ได้จะติดลบ และมีค่าตั้งแต่ -90 ถึง 0 เหมือน Quadrant ที่ 2
แต่ใน Quadrant นี้ เจ้าของบล็อกจะให้มีค่าตั้งแต่ 270 ถึง 360
ก็เลยต้องบวกเพิ่มข้าไปอีก 360 เพื่อให้ค่าอยู่ในช่วงที่ต้องการ

ก็จะเห็นว่าฟังก์ชันนี้คำนวณองศาออกมาในช่วง 0 ถึง 360
โดยที่จุด 0, 0 คือตำแหน่งตรงกลางของปุ่มหมุน
ถ้าจุดที่นิ้วแตะลงไปอยู่ฝั่งขวาล่างของจุดกึ่งกลางปุ่ม
ก็จะเป็น Quadrant ที่ 1 ซึ่งมีค่าตั้งแต่ 0 ถึง 90
ถ้าแตะที่ซ้ายล่างก็จะเป็น Quarant ที่ 2 มีค่าตั้งแต่ 90 ถึง 180
ถ้าแตะที่ซ้ายบนก็จะเป็น Quadrant ที่ 3 มีค่าตั้งแต่ 180 ถึง 270
และถ้าแตะที่ขวาบนก็จะเป็น Quadrant ที่ 4 มีค่าตั้งแต่ 270 ถึง 360

ต่อไปก็จะเป็นฟังก์ชัน setListener อันนี้สำหรับกำหนด Event Listener 
แบบ OnTouchListener ให้กับ mImageView เพื่อรับพิกัดเวลาที่แตะปุ่ม
โดยเจ้าของบล็อกก็จะให้คำนวณ X และ Y ออกมาว่าแตะพิกัดใด
แต่อย่าลืมว่าพิกัดของปุ่มจะเริ่ม 0, 0 ที่ ซ้ายบนของภาพ
ซึ่งเจ้าของบล็อกจะให้จุด 0, 0 อยู่ที่กลางภาพ ก็จะใช้การคำนวณ
โดยลบด้วยครึ่งหนึ่งขนาดภาพ ทำให้จุด 0, 0 อยู่ตรงกลางปุ่มพอดี
แล้วก็จะเข้าสู่ If สำหรับตรวจสอบเงื่อนไขว่าการแตะปุ่ม
เป็นแบบ ACTION_DOWN, ACTION_MOVE หรือ ACTION_UP
โดยที่ ACTION_DOWN จะเป็นกรณีที่เริ่มทำการแตะนิ้วลงบนปุ่ม
สำหรับ ACTION_MOVE จะเป็นกรณีที่แตะปุ่มแล้วเลื่อนตำแหน่งไปมา
และสำหรับ ACTION_UP จะเป็นกรณีที่เอาขึ้นออกจากจอ
สำหรับ If ที่เจ้าของบล็อกใช้ กรณีของ ACTION_MOVE กับ ACTION_UP
จะมีคำสั่งที่คล้ายกันก็เลยให้อยู่รวมกันเลย เพื่อลดความยาวของโปรแกรม
ซึ่ง ACTION_DOWN จะมีการเพิ่ม Animation เข้าไป ซึ่งอันนี้ก็ใช้ If ย่อยอีกที
สำหรับเงื่อนไข ACTION_DOWN ก็จะให้เก็บค่าพิกัดที่เริ่มแตะปุ่ม

angle = cal_angle(y, x);

และสำหรับเงื่อนไข ACTION_MOVE กับ ACTION_UP 
จะให้คำนวณ Delta ของมุมที่เคลื่อนที่ไป  จะอ้างอิงจากองศาโดยตรงไม่ได้
อย่าลืมว่าองศามีค่าแค่ 0 ถึง 360 ทำให้หมุนปุ่มหลายๆรอบก็ได้ค่าช่วงเดิม
เจ้าของบล็อกจึงคำนวณหาว่า จากจุดที่เริ่มเคลื่อนที่และปลายทางที่เคลื่อนไป
ต่างกันกี่องศา แล้วนำ Delta ที่ได้ไปคำนวณเป็นค่าจริงอีกทีหนึ่ง
และก็จะมีการใช้ If ย่อยอีกชุดเพื่อป้องกันกรณีที่เคลื่อนนิ้วในช่วง 360 องศา
เพราะค่าที่จะได้กลับมาเป็น 0 ใหม่ ถ้าคำนวณหา Delta เลยก็จะเพี้ยน
จึงต้องชดเชยค่า Delta เข้าไปด้วย ถ้า Delta มากกว่า 300 ก็จะให้ลบค่าออก 360 
และถ้า Delta น้อยกว่า 300 ก็จะให้บวกค่าเพิ่ม 360 ที่ใช้ 300 ก็เพื่อเผื่อช่วง Delta ไว้
เพราะมันไม่ได้ค่า Delta ออกมา 360 เป๊ะๆแน่ แต่จะออกมาประมาณ 350 ถึง 370
ซึ่งในการหมุนปุ่มปกติค่า Delta จะอยู่ประมาณ -8 ถึง 8 ต่อ ACTION_MOVE หนึ่งครั้ง
เพราะ ACTION_MOVE จะเรียกใช้ตลอดเวลาที่เคลื่อนตำแหน่งที่แตะ
ก็จะมีการคำนวณค่า Delta ไปเรื่อยๆแล้วเอาไปบวกลบกับค่าจริง
และมีการคำนวณ Speed ด้วยเพื่อให้กำหนดความเร็วในการเปลี่ยนแปลงของค่า
(ค่า Speed มาก เวลาหมุนปุ่ม ค่าก็จะเพิ่มขึ้นหรือลดลงอย่างรวดเร็ว
และค่า Speed น้อย เวลาหมุนปุ่ม ค่าก็จะเพิ่มขึ้นหรือลดลงอย่างช้าๆ)

if(angle - cal_angle(y, x) > 300) { VALUE += ((angle - cal_angle(y, x)) - 360) * SCROLL_SPEED / 1000; if(arg1.getAction() == MotionEvent.ACTION_MOVE) animate((angle - cal_angle(y, x)) - 360); } else if(angle - cal_angle(y, x) < -300) { VALUE += ((angle - cal_angle(y, x)) + 360) * SCROLL_SPEED / 1000; if(arg1.getAction() == MotionEvent.ACTION_MOVE) animate((angle - cal_angle(y, x)) + 360); } else { VALUE += (angle - cal_angle(y, x)) * SCROLL_SPEED / 1000; if(arg1.getAction() == MotionEvent.ACTION_MOVE) (angle - cal_angle(y, x)); }

ซึ่ง ACTION_UP ก็จะใช้คำสั่งเดียวกับ ACTION_MOVE เลย
แต่สำหรับ ACTION_MOVE จะใช้ Animation เพื่อให้ปุ่มหมุนไปตามที่เลื่อน

if(arg1.getAction() == MotionEvent.ACTION_MOVE) animate(angle - cal_angle(y, x)); }

และสุดท้าย ถ้าค่าจริงมากกว่าค่าสูงสุดก็จะให้หยุดเพิ่มค่าจริง
และถ้าน้อยกว่าค่าต่ำสุดก็จะให้หยุดลดค่าจริง ด้วยคำสั่ง

if(VALUE < MIN_VALUE) VALUE = MIN_VALUE; else if(VALUE > MAX_VALUE) VALUE = MAX_VALUE;

จากนั้นก็ให้เก็บค่ามุมล่าสุดไว้เพื่อใช้คำนวณต่อ 
และแสดงค่าจริงบน mTextView โดยปัดเป็นจำนวณเต็ม

angle = cal_angle(y, x);

mTextView.setText(String.valueOf((int)VALUE));


ต่อมาก็เป็นฟังก์ชัน animate อันนี้เอาไว้สร้าง Animation ให้ปุ่มหมุนตามที่เลื่อน
ก็จะใช้ Animation แบบ Rotate Animation โดยกำหนดจุดหมุนที่ตรงกลางปุ่ม 
และการหมุนก็จะหมุนจากมุมเดิมไปตามค่า Delta ที่คำนวณในฟังก์ชัน setListener
ใช้ระยะเวลาในการหมุน 50 มิลลิวินาที และคงสภาพล่าสุดไว้ ด้วยคำสั่ง

animation.setDuration(50);
animation.setFillEnabled(true);
animation.setFillBefore(true);

สำหรับการใช้ Rotate Animation อ่านเพิ่มเติมได้ที่ 

แล้วก็เก็บค่ามุมที่หมุนล่าสุดไว้เพื่อใช้คำนวณองศาที่หมุนในรอบต่อไป
และทำให้ Animation เริ่มทำงาน ด้วยคำสั่ง

animAngle += offset;

mImageView.startAnimation(animation);


เพียงเท่านี้ก็ได้ล่ะ คลาสสำหรับปุ่มแบบหมุน


Main.java
package app.akexorcist.circleselector;

import android.os.Bundle;
import android.app.Activity;
import android.widget.ImageView;
import android.widget.TextView;

public class Main extends Activity {
    TextView textView1;
    ImageView imageView;
    
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        textView1 = (TextView)findViewById(R.id.textView1);
        imageView = (ImageView)findViewById(R.id.imageView1);
        
        CircleSelector cs = new CircleSelector(getApplicationContext()
                , textView1, imageView);
        cs.setScrollSpeed(4);
    }
}

เวลาเรียกใช้ก็ง่ายๆ คำสั่งสั้นๆ เพราะเจ้าของบล็อกเขียนใน CircleSelector.java แล้ว
เวลาเรียกใช้คลาสนี้จะต้องสร้าง Object สำหรับ TextView และ ImageView ทุกครั้ง

textView1 = (TextView)findViewById(R.id.textView1); imageView = (ImageView)findViewById(R.id.imageView1);

จากนั้นก็สร้าง Object จากคลาส CircleSelector ขึ้นมา ด้วยคำสั่ง

CircleSelector cs = new CircleSelector(getApplicationContext(), textView1, imageView);


อันนี้เจ้าของบล็อกกำหนดค่าให้คลาสในแบบแรกนะ
คือกำหนดแค่ Context, TextView แล้วก็ ImageView
อยากจะกำหนดค่าเริ่มต้นให้กับคลาสแบบไหนก็ได้ ทำไว้ให้ 4 แบบ
แล้วก็เจ้าของบล็อกได้กำหนดคุณสมบัติให้กับคลาสเพิ่มเข้าไป
คือกำหนดความเร็วในการหมุนปุ่ม โดยให้มีความเร็วแค่ 4 
เพราะค่าในตัวอย่างนี้มีค่าตั้งแต่ 0 ถึง 100
ใช้ Speed ที่ 4 ก็พอ ถ้ามากกว่านั้นเวลาหมุนปุ่ม ค่าจะเปลี่ยนไวเกินไป


main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >
    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:text="50"
        android:textSize="30dp" />
    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="150dp"
        android:layout_height="150dp"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:src="@drawable/selector" />
</RelativeLayout>

อันนี้เจ้าของบล็อกใส่ภาพปุ่มที่ใช้เข้าไปด้วยนะ ใช้รูปนี้



AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="app.akexorcist.circleselector"
    android:versionCode="1"
    android:versionName="1.0" >
    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="15" />
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".Main"
            android:label="@string/title_activity_main" 
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

เพียงเท่านี้ก็ได้ละ ปุ่มแบบหมุนเพื่อกำหนดค่า
สำหรับผู้ที่หลงเข้ามาอ่านคนใดต้องการดาวน์โหลดไฟล์ตัวอย่าง
สามารถดาวน์โหลดได้จากที่นี่เลย CircleSelector [Google Drive]



เสร็จแล้วววว




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium