Android SDK: Membuat Dialer berputar
Indonesian (Bahasa Indonesia) translation by Dika Budiaji (you can also view the original English article)
Android SDK menawarkan berbagai macam komponen interface, termasuk TextViews, tombol, dan EditText. Namun, menambahkan elemen kustom interface untuk aplikasi Anda adalah cara yang bagus untuk berdiri keluar di pasar App. Tutorial ini akan menyelam ke dalam pembuatan komponen kustom interface dengan mengajar Anda bagaimana membangun dialer rotary ramping.
Tutorial ini mengasumsikan beberapa pengetahuan dasar Java dan Android. Selain itu, saya tidak akan memperkenalkan sebuah API yang lengkap. Sebaliknya, kelas-kelas tertentu akan digunakan dan dijelaskan. Saya juga akan menjelaskan, langkah demi langkah, mengapa saya telah memilih sebuah implementasi tertentu untuk komponen interface ini.
Langkah 1: Konsep
Kami memiliki lingkaran dan ingin memutar di sekitar pusatnya. Pendekatan paling sederhana adalah untuk mengambil sebuah sistem koordinat Kartesius dua dimensi sebagai template.



Anda menyentuh lingkaran, memutar itu, dan kemudian membiarkannya pergi. Selain itu, lingkaran harus mendaftar "fling" ketika hal ini terjadi.
Android menyediakan interface yang sangat sederhana untuk mengubah gambar dengan bantuan Bitmap kelas dan kelas Matrix. Lingkaran akan ditampilkan dalam ImageView. Dasar matematika unit lingkaran dapat dilaksanakan dengan menggunakan Math kelas. Android juga menawarkan API yang baik untuk mengenali touch event dan gesture. Seperti yang Anda lihat, sebagian besar pekerjaan sudah tersedia bagi kita dengan SDK!
Langkah 2: Setup proyek
Membuat proyek Android baru dan tambahkan Activity. Kemudian tambahkan grafis dialer ke folder "drawable". Kita tidak perlu versi yang berbeda untuk density tampilan yang berbeda, karena nanti kita akan scale gambar secara programmatically. Satu gambar dengan resolusi tinggi, yang mencakup semua ukuran layar, harus cukup dan akan menghemat ruang penyimpanan pada pengguna telepon.

Langkah 3: Layout
Untuk tutorial ini, kami akan membuat user interface sangat sederhana. Kami hanya menambah satu ImageView tata letak. Untuk konten ImageView, kami memilih kami grafis dialer. Hal ini tidak ada masalah untuk menambahkan View lebih kemudian karena melakukan sehingga tidak mempengaruhi dialer kami berputar.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="https://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#FFCCCCCC"> <ImageView android:src="@drawable/graphic_ring" android:id="@+id/imageView_ring" android:layout_height="fill_parent" android:layout_width="fill_parent"></ImageView> </LinearLayout>
Langkah 4: Activity
Seperti dijelaskan dalam langkah-langkah di atas, kita perlu memuat gambar sebagai Bitmap sekaligus membuat activity. Selain itu, kami membutuhkan matriks untuk transformasi, seperti rotasi. Kami melakukan semua ini dalam metode onCreate.
Kita juga perlu scale copy image secara benar. Karena kita hanya tahu setelah measuring tata letak berapa banyak ruang kami mengisi ImageView, kami menambahkan OnGlobalLayoutListener. Dalam metode onGlobalLayout, kita bisa intercept event bahwa tata telah di drawn dan query ukuran view kami.
private static Bitmap imageOriginal, imageScaled; private static Matrix matrix; private ImageView dialer; private int dialerHeight, dialerWidth; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // load the image only once if (imageOriginal == null) { imageOriginal = BitmapFactory.decodeResource(getResources(), R.drawable.graphic_ring); } // initialize the matrix only once if (matrix == null) { matrix = new Matrix(); } else { // not needed, you can also post the matrix immediately to restore the old state matrix.reset(); } dialer = (ImageView) findViewById(R.id.imageView_ring); dialer.setOnTouchListener(new MyOnTouchListener()); dialer.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() { @Override public void onGlobalLayout() { // method called more than once, but the values only need to be initialized one time if (dialerHeight == 0 || dialerWidth == 0) { dialerHeight = dialer.getHeight(); dialerWidth = dialer.getWidth(); // resize Matrix resize = new Matrix(); resize.postScale((float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getWidth(), (float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getHeight()); imageScaled = Bitmap.createBitmap(imageOriginal, 0, 0, imageOriginal.getWidth(), imageOriginal.getHeight(), resize, false); } } }); }
OnTouchListener ini tetap sangat sederhana. Dalam event ACTION_DOWN kami menginisialisasi sudut (yaitu unit circle). Dengan setiap gerakan, perbedaan antara sudut lama dan baru mendapat additively bertambah dengan dialer.
Kami menambahkan OnTouchListener sebagai private inner kelas. Melakukannya akan menyelamatkan kita dari tidak perlu melewati parameter yang dibutuhkan (misalnya ukuran view).
/** * Simple implementation of an {@link OnTouchListener} for registering the dialer's touch events. */ private class MyOnTouchListener implements OnTouchListener { private double startAngle; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startAngle = getAngle(event.getX(), event.getY()); break; case MotionEvent.ACTION_MOVE: double currentAngle = getAngle(event.getX(), event.getY()); rotateDialer((float) (startAngle - currentAngle)); startAngle = currentAngle; break; case MotionEvent.ACTION_UP: break; } return true; } }
Perhitungan sudut dapat diselesaikan dengan cepat dengan bantuan dari unit circle. Namun, touch event koordinat perlu dikonversi ke koordinat sistem koordinat Kartesius dua dimensi pertama. Mungkin akan membantu Anda untuk mengatur formula ini sendiri sekali lagi untuk memahami mereka sepenuhnya.
/** * @return The angle of the unit circle with the image view's center */ private double getAngle(double xTouch, double yTouch) { double x = xTouch - (dialerWidth / 2d); double y = dialerHeight - yTouch - (dialerHeight / 2d); switch (getQuadrant(x, y)) { case 1: return Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI; case 2: return 180 - Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI; case 3: return 180 + (-1 * Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI); case 4: return 360 + Math.asin(y / Math.hypot(x, y)) * 180 / Math.PI; default: return 0; } } /** * @return The selected quadrant. */ private static int getQuadrant(double x, double y) { if (x >= 0) { return y >= 0 ? 1 : 4; } else { return y >= 0 ? 2 : 3; } }
Metode untuk berputar dialer sangat sederhana. Kami mengganti ImageView yang lama konten setiap kali dengan yang Bitmap baru yang dirotasi.
/** * Rotate the dialer. * * @param degrees The degrees, the dialer should get rotated. */ private void rotateDialer(float degrees) { matrix.postRotate(degrees); dialer.setImageBitmap(Bitmap.createBitmap(imageScaled, 0, 0, imageScaled.getWidth(), imageScaled.getHeight(), matrix, true)); }
Perhatikan bahwa, terlepas dari beberapa masalah kecil, rotasi dialer sudah bekerja. Perhatikan juga bahwa jika bitmap diputar tidak cocok ke tampilan, bitmap mendapat diperkecil secara otomatis (Lihat jenis skala ImageView). Hal ini menyebabkan berbagai ukuran.
Langkah 5: Algoritma koreksi
Beberapa bug telah diperkenalkan di langkah di atas, misalnya dengan skala yang berbeda-beda, tetapi mereka dapat dengan mudah diperbaiki. Apakah Anda melihat mana masalahnya adalah dengan metode yang diterapkan di atas?
Jika Anda melihat lebih dalam ke dalam implementasi perhitungan rotasi gambar, Anda akan segera menemukan bahwa sebuah instance Bitmap yang baru dibuat setiap kali. Ini membuang-buang banyak RAM dan hasil dalam sering menjalankan garbage collector di background (Anda melihat masalah ini dalam output LogCat). Hal ini menyebabkan animasi untuk melihat berombak.
Masalahnya hanya dapat dihindari dengan mengubah pendekatan. Daripada berputar bitmap dan menerapkannya pada ImageView, kita harus langsung memutar konten ImageView. Untuk itu, kita dapat mengubah jenis skala ImageView untuk "Matrix" dan menerapkan matriks untuk ImageView setiap kali. Ini juga menambah perubahan kecil lain: sekarang kita perlu menambahkan bitmap skala ImageView langsung setelah inisialisasi.
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#FFCCCCCC"> <ImageView android:src="@drawable/graphic_ring" android:id="@+id/imageView_ring" android:scaleType="matrix" android:layout_height="fill_parent" android:layout_width="fill_parent"></ImageView> </LinearLayout>
... @Override public void onGlobalLayout() { // method called more than once, but the values only need to be initialized one time if (dialerHeight == 0 || dialerWidth == 0) { dialerHeight = dialer.getHeight(); dialerWidth = dialer.getWidth(); // resize Matrix resize = new Matrix(); resize.postScale((float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getWidth(), (float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getHeight()); imageScaled = Bitmap.createBitmap(imageOriginal, 0, 0, imageOriginal.getWidth(), imageOriginal.getHeight(), resize, false); dialer.setImageBitmap(imageScaled); dialer.setImageMatrix(matrix); } } ... /** * Rotate the dialer. * * @param degrees The degrees, the dialer should get rotated. */ private void rotateDialer(float degrees) { matrix.postRotate(degrees); dialer.setImageMatrix(matrix); }
Sekarang bug lain terjadi. Perhitungan sudut bekerja dengan baik dengan ImageView pusat. Namun, gambar yang berputar di sekitar koordinat [0, 0]. Oleh karena itu, kita perlu untuk memindahkan gambar di inisialisasi ke posisi yang benar. Sama berlaku untuk rotasi. Pertama bitmap harus bergeser, sehingga pusat terletak di original ImageView, maka bitmap dapat diputar dan kembali lagi.
... @Override public void onGlobalLayout() { // method called more than once, but the values only need to be initialized one time if (dialerHeight == 0 || dialerWidth == 0) { dialerHeight = dialer.getHeight(); dialerWidth = dialer.getWidth(); // resize Matrix resize = new Matrix(); resize.postScale((float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getWidth(), (float)Math.min(dialerWidth, dialerHeight) / (float)imageOriginal.getHeight()); imageScaled = Bitmap.createBitmap(imageOriginal, 0, 0, imageOriginal.getWidth(), imageOriginal.getHeight(), resize, false); // translate to the image view's center float translateX = dialerWidth / 2 - imageScaled.getWidth() / 2; float translateY = dialerHeight / 2 - imageScaled.getHeight() / 2; matrix.postTranslate(translateX, translateY); dialer.setImageBitmap(imageScaled); dialer.setImageMatrix(matrix); } } ... /** * Rotate the dialer. * * @param degrees The degrees, the dialer should get rotated. */ private void rotateDialer(float degrees) { matrix.postRotate(degrees, dialerWidth / 2, dialerHeight / 2); dialer.setImageMatrix(matrix); }
Sekarang animasi kami bekerja jauh lebih baik dan sangat halus.
Langkah 6: Fling
Android menyediakan easy-to-use API untuk mengenali gesture. Misalnya sebuah scroll, long press dan fling dapat dideteksi. Untuk tujuan kita, kita extend kelas SimpleOnGestureListener dan menimpa onFling metode. Kelas ini direkomendasikan oleh dokumentasi.
Selain itu, kita perlu menghidupkan rotasi dan dengan demikian mengurangi kecepatan. Karena ini adalah hanya kecil perhitungan, hal ini tidak diperlukan untuk melakukan outsourcing fungsi ekstra thread. Sebaliknya, Anda dapat mentransfer action ke Handler yang dapat memanipulasi GUI di thread utama. Kita bahkan dapat menghindari contoh handler dengan passing action untuk ImageView yang menggunakan Handler sendiri untuk itu.
... private int dialerHeight, dialerWidth; private GestureDetector detector; @Override public void onCreate(Bundle savedInstanceState) { ... detector = new GestureDetector(this, new MyGestureDetector()); dialer = (ImageView) findViewById(R.id.imageView_ring); dialer.setOnTouchListener(new MyOnTouchListener()); ... /** * Simple implementation of an {@link OnTouchListener} for registering the dialer's touch events. */ private class MyOnTouchListener implements OnTouchListener { private double startAngle; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: startAngle = getAngle(event.getX(), event.getY()); break; case MotionEvent.ACTION_MOVE: double currentAngle = getAngle(event.getX(), event.getY()); rotateDialer((float) (startAngle - currentAngle)); startAngle = currentAngle; break; case MotionEvent.ACTION_UP: break; } detector.onTouchEvent(event); return true; } } /** * Simple implementation of a {@link SimpleOnGestureListener} for detecting a fling event. */ private class MyGestureDetector extends SimpleOnGestureListener { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { dialer.post(new FlingRunnable(velocityX + velocityY)); return true; } } /** * A {@link Runnable} for animating the the dialer's fling. */ private class FlingRunnable implements Runnable { private float velocity; public FlingRunnable(float velocity) { this.velocity = velocity; } @Override public void run() { if (Math.abs(velocity) > 5) { rotateDialer(velocity / 75); velocity /= 1.0666F; // post this instance again dialer.post(this); } } }
Langkah 7: Memperbaiki rotasi Inversed
Jika Anda secara ekstensif menguji melemparkan, dengan cepat menjadi jelas bahwa posisi dialer berputar dalam arah yang salah. Sebagai contoh, ini adalah kasus ketika mulai dan titik akhir hanya dalam kuadran yang ketiga. Namun ada juga kasus-kasus kesalahan yang lebih sulit (misalnya ketika menyeka dari kuadran dua sampai empat atas kuadran tiga).
Solusi yang paling sederhana adalah untuk mengingat kuadran yang disentuh. Kasus kesalahan tertangkap dalam metode onFling dan kecepatan mendapat terbalik sesuai.
... private GestureDetector detector; // needed for detecting the inversed rotations private boolean[] quadrantTouched; ... detector = new GestureDetector(this, new MyGestureDetector()); // there is no 0th quadrant, to keep it simple the first value gets ignored quadrantTouched = new boolean[] { false, false, false, false, false }; dialer = (ImageView) findViewById(R.id.imageView_ring); ... /** * Simple implementation of an {@link OnTouchListener} for registering the dialer's touch events. */ private class MyOnTouchListener implements OnTouchListener { private double startAngle; @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // reset the touched quadrants for (int i = 0; i < quadrantTouched.length; i++) { quadrantTouched[i] = false; } startAngle = getAngle(event.getX(), event.getY()); break; case MotionEvent.ACTION_MOVE: double currentAngle = getAngle(event.getX(), event.getY()); rotateDialer((float) (startAngle - currentAngle)); startAngle = currentAngle; break; case MotionEvent.ACTION_UP: break; } // set the touched quadrant to true quadrantTouched[getQuadrant(event.getX() - (dialerWidth / 2), dialerHeight - event.getY() - (dialerHeight / 2))] = true; detector.onTouchEvent(event); return true; } } /** * Simple implementation of a {@link SimpleOnGestureListener} for detecting a fling event. */ private class MyGestureDetector extends SimpleOnGestureListener { @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // get the quadrant of the start and the end of the fling int q1 = getQuadrant(e1.getX() - (dialerWidth / 2), dialerHeight - e1.getY() - (dialerHeight / 2)); int q2 = getQuadrant(e2.getX() - (dialerWidth / 2), dialerHeight - e2.getY() - (dialerHeight / 2)); // the inversed rotations if ((q1 == 2 && q2 == 2 && Math.abs(velocityX) < Math.abs(velocityY)) || (q1 == 3 && q2 == 3) || (q1 == 1 && q2 == 3) || (q1 == 4 && q2 == 4 && Math.abs(velocityX) > Math.abs(velocityY)) || ((q1 == 2 && q2 == 3) || (q1 == 3 && q2 == 2)) || ((q1 == 3 && q2 == 4) || (q1 == 4 && q2 == 3)) || (q1 == 2 && q2 == 4 && quadrantTouched[3]) || (q1 == 4 && q2 == 2 && quadrantTouched[3])) { dialer.post(new FlingRunnable(-1 * (velocityX + velocityY))); } else { // the normal rotation dialer.post(new FlingRunnable(velocityX + velocityY)); } return true; } }
Langkah 8: Stop Fling
Tentu saja fling animasi telah berhenti jika Anda menyentuh dialer sementara animasi berjalan. Ini hal ini hanya diperlukan untuk menambahkan nilai boolean untuk memeriksa apakah Animasi diperbolehkan untuk dimainkan atau tidak.
Dalam acara ACTION_DOWN Anda ingin menghentikannya. Setelah Anda membiarkan dialer pergi (ACTION_UP), animasi harus dimainkan.
... // needed for detecting the inversed rotations private boolean[] quadrantTouched; private boolean allowRotating; ... allowRotating = true; dialer = (ImageView) findViewById(R.id.imageView_ring); ... switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // reset the touched quadrants for (int i = 0; i < quadrantTouched.length; i++) { quadrantTouched[i] = false; } allowRotating = false; startAngle = getAngle(event.getX(), event.getY()); break; case MotionEvent.ACTION_MOVE: double currentAngle = getAngle(event.getX(), event.getY()); rotateDialer((float) (startAngle - currentAngle)); startAngle = currentAngle; break; case MotionEvent.ACTION_UP: allowRotating = true; break; } ... @Override public void run() { if (Math.abs(velocity) > 5 && allowRotating) { rotateDialer(velocity / 75); velocity /= 1.0666F; // post this instance again dialer.post(this); } }
Langkah 9: Apakah berikutnya
Semua fitur yang diinginkan termasuk dalam dialer. Namun, ini hanya contoh dan dapat di extend indefinitively.
Beberapa perangkat tambahan tambahan yang datang ke pikiran termasuk: Anda bisa melacak sudut dan membalikkan atau mengurangi nilai-nilai tertentu (Lihat app Shutdown Remote saya untuk contoh ini). Juga, setelah merilis dialer Anda bisa meng-animasikan kembali ke posisi semula untuk meniru fungsi mekanis nyata, rotary antik cepat. Lebih lanjut, dialer dapat diekstraksi ke kelas lihat kustom, yang akan meningkatkan usabilitas komponen. Kemungkinan untuk peningkatan sangat bisa!
Kesimpulan
Dalam tutorial ini, saya telah menunjukkan kepada Anda bagaimana menerapkan dialer rotary sederhana. Tentu saja, fungsi dapat di extend dan ditingkatkan seperti yang dibahas diatas. Android SDK menawarkan API yang luar biasa dan sangat berguna dengan banyak kelas. Ada tidak perlu reinvent banyak, Anda biasanya hanya perlu untuk menerapkan apa yang sudah ada. Jangan ragu untuk memberi komentar atau meninggalkan saran!
Sumber
http://en.wikipedia.org/wiki/file:Cartesian_coordinates_2D.SVG
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weekly