
Halo, Habr! Belum lama ini, dalam mencari petualangan, proyek dan teknologi baru, saya menjadi robot menetap di Redmadrobot. Saya mendapatkan kursi, monitor dan macbook, dan proyek internal kecil untuk pemanasan. Itu perlu untuk menyelesaikan dan menerbitkan perpustakaan yang ditulis sendiri untuk melihat konten media yang kami gunakan dalam proyek kami. Dalam artikel ini, saya akan memberi tahu Anda cara memahami acara sentuh dalam seminggu, menjadi sumber terbuka, menemukan bug di Android SDK dan menerbitkan perpustakaan.
Mulai
Salah satu fitur penting dari aplikasi toko kami adalah kemampuan untuk melihat video dan foto barang dan jasa di dekat dan dari semua sisi. Kami tidak ingin menemukan kembali roda dan mencari perpustakaan yang sudah jadi yang cocok untuk kami.
Kami berencana untuk menemukan solusi seperti itu sehingga pengguna dapat:
- lihat foto;
- Skala foto menggunakan pinch to zoom dan double tap;
- Tonton video
- membalik konten media;
- tutup kartu foto dengan sapuan vertikal (gesek untuk menghilangkan).
Inilah yang kami temukan:
- FrescoImageViewer - mendukung melihat dan menggulir foto dan gerakan dasar, tetapi tidak mendukung menonton video dan ditujukan untuk perpustakaan Fresco .
- PhotoView - mendukung melihat foto, sebagian besar gerakan kontrol utama, kecuali menggulir, menggesek untuk mengabaikan, tidak mendukung menonton video.
- PhotoDraweeView - fungsionalitasnya mirip dengan PhotoView, tetapi ditujukan untuk Fresco.
Karena tidak ada perpustakaan yang sepenuhnya memenuhi persyaratan, kami harus menulis sendiri.
Kami menyadari perpustakaan
Untuk mendapatkan fungsionalitas yang diperlukan, kami menyelesaikan solusi yang ada dari perpustakaan lain. Mereka memutuskan untuk memberikan nama sederhana Galeri Android untuk apa yang terjadi.
Kami menerapkan fungsionalitas
Lihat dan perbesar foto
Untuk melihat foto, kami mengambil perpustakaan PhotoView, yang mendukung penskalaan di luar kotak.
Tonton video
Untuk menonton video, mereka mengambil ExoPlayer , yang digunakan kembali di MediaPagerAdapter . Saat pengguna membuka video untuk pertama kalinya, ExoPlayer dibuat. Saat beralih ke elemen lain, ia di-antri, jadi saat Anda memulai video berikutnya, instance ExoPlayer yang sudah dibuat akan digunakan. Ini membuat transisi antar elemen lebih lancar.
Menggulir Konten Media
Di sini kami menggunakan MultiTouchViewPager dari FrescoImageViewer, yang tidak mencegat acara multi-sentuh, jadi kami dapat menambahkan gerakan untuk skala gambar.
Geser untuk mengabaikan
PhotoView tidak mendukung gesek untuk menghilangkan dan melonggarkan (mengembalikan ukuran gambar asli ketika gambar ditingkatkan atau turun).
Beginilah cara kami menanganinya.
Mempelajari acara sentuh untuk menerapkan gesek untuk menghilangkan
Sebelum melanjutkan untuk mendukung swipe untuk diberhentikan, Anda harus memahami cara kerja sentuh. Ketika pengguna menyentuh layar, metode dispatchTouchEvent(motionEvent: MotionEvent)
dipanggil dalam Aktivitas saat ini, di mana MotionEvent.ACTION_DOWN
dapat MotionEvent.ACTION_DOWN
. Metode ini menentukan nasib acara. Anda dapat meneruskan motionEvent
ke onTouchEvent(motionEvent: MotionEvent)
untuk pemrosesan sentuh, atau membiarkannya melangkah lebih jauh, dari atas ke bawah, dalam hierarki tampilan. Lihat, yang tertarik pada suatu acara dan / atau acara berikutnya sebelum ACTION_UP
, mengembalikan true.
Setelah itu, semua peristiwa gerakan saat ini akan jatuh ke tampilan ini sampai gerakan berakhir dengan acara ACTION_UP
atau orang tua yang ViewGroup mengambil kendali (maka acara ACTION_CANCELED
akan datang ke ACTION_CANCELED
). Jika acara berjalan di sekitar seluruh hierarki Tampilan dan tidak ada yang tertarik, itu kembali ke Activity di onTouchEvent(motionEvent: MotionEvent)
.

Di perpustakaan Galeri Android kami, acara ACTION_DOWN
pertama mencapai dispatchTouchEvent()
di PhotoView, di mana motionEvent
diteruskan ke implementasi onTouch()
, yang mengembalikan true. Selanjutnya, semua acara melalui rantai yang sama sampai salah satu dari:
ACTION_UP
;- ViewPager akan mencoba mencegat acara untuk bergulir;
- VerticalDragLayout akan mencoba untuk mencegat acara agar swipe untuk diberhentikan
Hanya ViewGroup di metode onInterceptTouchEvent(motionEvent: MotionEvent)
yang dapat mencegat acara. Bahkan jika View tertarik pada MotionEvent apa pun, acara itu sendiri akan melalui dispatchTouchEvent(motionEvent: MotionEvent)
seluruh ViewGroup sebelumnya. Karenanya, orang tua selalu "mengawasi" anak-anak mereka. Setiap induk ViewGroup dapat mencegat acara dan mengembalikan true dalam metode onInterceptTouchEvent(motionEvent: MotionEvent)
, maka semua MotionEvent.ACTION_CANCEL
anak akan menerima MotionEvent.ACTION_CANCEL
di onTouchEvent(motionEvent: MotionEvent)
.
Contoh: pengguna memegang jari pada elemen dalam RecyclerView, lalu acara diproses dalam elemen yang sama. Tetapi begitu dia mulai menggerakkan jarinya ke atas / bawah, RecyclerView akan mencegat acara, dan gulir akan dimulai, dan Tampilan akan menerima acara ACTION_CANCEL
.

Di Galeri Android, VerticalDragLayout dapat mencegat acara untuk dihapus atau ViewPager untuk digulir. Tetapi View dapat mencegah orang tua dari requestDisallowInterceptTouchEvent(true)
peristiwa dengan memanggil metode requestDisallowInterceptTouchEvent(true)
. Ini mungkin diperlukan jika Tampilan perlu melakukan tindakan yang intersepsi oleh orang tua tidak diinginkan untuk kita.
Misalnya, ketika pengguna dalam pemain melompati trek ke waktu tertentu. Jika ViewPager induk mencegat gulir horizontal, akan ada transisi ke trek berikutnya.
Untuk menangani gesek untuk menghilangkan, kami menulis VerticalDragLayout, tetapi tidak menerima acara sentuh dari PhotoView. Untuk memahami mengapa ini terjadi, saya harus mencari tahu bagaimana acara sentuh diproses dalam PhotoView.
Memproses pesanan:
- Ketika MotionEvent.ACTION_DOWN di VerticalDragLayout,
interceptTouchEvent()
dipicu, yang mengembalikan false, karena ViewGroup ini hanya tertarik pada ACTION_MOVE vertikal. Arah ACTION_MOVE didefinisikan dalam dispatchTouchEvent()
, setelah peristiwa dilewatkan ke metode super.dispatchTouchEvent()
di ViewGroup, di mana acara diteruskan ke implementasi interceptTouchEvent()
di VerticalDragLayout.

- Ketika acara
ACTION_DOWN
mencapai metode onTouch()
di PhotoView, tampilan menghilangkan kemampuan untuk mencegat manajemen acara. Semua peristiwa gerakan berikutnya tidak termasuk dalam metode interceptTouchEvent()
. Kemampuan untuk mencegat kontrol diberikan kepada orang tua hanya jika gerakan selesai atau jika ACTION_MOVE
horizontal ACTION_MOVE
di batas kanan / kiri gambar.
if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { if (mScrollEdge == EDGE_BOTH || (mScrollEdge == EDGE_LEFT && dx >= 1f) || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) { if (parent != null) { parent.requestDisallowInterceptTouchEvent(false); } } } else { if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } }

Karena PhotoView memungkinkan orang tua untuk mengambil kendali hanya dalam kasus horisontal ACTION_MOVE
, dan gesek untuk mengabaikan adalah ACTION_MOVE
vertikal, maka VerticalDragLayout tidak dapat mencegat kontrol acara untuk menerapkan gerakan. Untuk memperbaikinya, Anda perlu menambahkan kemampuan untuk mencegat kontrol jika ACTION_MOVE
vertikal.
if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f) || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f) || mVerticalScrollEdge == VERTICAL_EDGE_BOTH || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f) || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) { if (parent != null) { parent.requestDisallowInterceptTouchEvent(false); } } } else { if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } }
Sekarang, dalam kasus vertikal ACTION_MOVE
pertama akan memberikan kesempatan untuk mencegat induk:

ACTION_MOVE
berikut akan dicegat di VerticalDragLyout, dan acara ACTION_CANCEL
akan terbang ke tampilan anak:

Semua ACTION_MOVE
lainnya akan terbang ke VerticalDragLayout di sepanjang rantai standar. Adalah penting bahwa setelah ViewGroup mengendalikan acara dari Tampilan anak, Tampilan anak tidak dapat mendapatkan kembali kontrol.

Jadi kami menerapkan gesek untuk mengabaikan dukungan untuk perpustakaan PhotoView. Di perpustakaan kami, kami menggunakan sumber PhotoView yang dimodifikasi yang diambil dalam modul terpisah, dan membuat permintaan penggabungan dalam repositori PhotoView asli.
Kami menerapkan debind di PhotoView
Ingat bahwa debounce adalah pemulihan animasi dari skala yang dapat diterima ketika gambar diskalakan melampaui batasnya.

Tidak ada kemungkinan seperti itu di PhotoView. Tetapi karena kami mulai menggali sumber terbuka orang lain, mengapa berhenti di situ? Di PhotoView, Anda dapat mengatur batas zoom. Awalnya, ini adalah minimum - x1 dan maksimum - x3. Gambar tidak dapat melampaui batas ini.
@Override public void onScale(float scaleFactor, float focusX, float focusY) { if ((getScale() < mMaxScale || scaleFactor < 1f) && (getScale() > mMinScale || scaleFactor > 1f)) { if (mScaleChangeListener != null) { mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); } mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
Untuk mulai dengan, kami memutuskan untuk menghapus kondisi "larangan penskalaan setelah mencapai minimum": kami cukup membuang kondisi getScale() > mMinScale || scaleFactor > 1f
getScale() > mMinScale || scaleFactor > 1f
. Lalu tiba-tiba ...

Debun diterima! Rupanya, ini terjadi karena fakta bahwa pembuat perpustakaan memutuskan untuk memainkannya dengan aman dua kali, membuat debounce dan pembatasan penskalaan. Dalam implementasi acara onTouch, yaitu dalam kasus MotionEvent.ACTION_UP
, jika pengguna telah menskala lebih / kurang dari maksimum / minimum, AnimatedZoomRunnable diluncurkan, yang mengembalikan gambar ke ukuran aslinya.
@Override public boolean onTouch(View v, MotionEvent ev) { boolean handled = false; switch (ev.getAction()) { case MotionEvent.ACTION_UP:
Seperti halnya dengan swipe untuk mengabaikan, kami menyelesaikan PhotoView di sumber perpustakaan kami dan membuat permintaan tarik dengan "penambahan" debounce ke PhotoView asli.
Perbaiki bug yang tiba-tiba di PhotoView
PhotoView memiliki bug yang sangat jahat. Saat pengguna ingin memperbesar gambar dengan ketuk dua kali dan dia memiliki serangan epilepsi gambar mulai skala, dapat membalik 180 derajat secara vertikal. Bug ini dapat ditemukan bahkan di aplikasi populer dari Google Play, misalnya, di Cyan.

Setelah pencarian yang panjang, kami masih melokalkan bug ini: kadang-kadang scaleFactor negatif dimasukkan ke dalam transformasi gambar matriks untuk penskalaan, yang menyebabkan gambar terbalik.
CustomGestureDetector
@Override public boolean onScale(ScaleGestureDetector detector) {
Untuk skala dari Android ScaleGestureDetector, kami mendapatkan scaleFactor, yang dihitung sebagai berikut:
public float getScaleFactor() { if (inAnchoredScaleMode()) {
Jika Anda menimpa metode ini dengan log debug, Anda dapat melacak di mana nilai-nilai tertentu dari variabel scaleFactor negatif diperoleh:
mEventBeforeOrAboveStartingGestureEvent is true; SCALE_FACTOR is 0.5; mCurrSpan: 1075.4398; mPrevSpan 38.867798; scaleUp: false; spanDiff: 13.334586; eval result is -12.334586
Ada kecurigaan bahwa mereka mencoba menyelesaikan masalah ini dengan mengalikan spanDiff dengan SCALE_FACTOR == 0,5. Tetapi solusi ini tidak akan membantu jika perbedaan antara mCurrSpan dan mPrevSpan lebih dari tiga kali. Tiket sudah dimulai untuk bug ini, tetapi belum diperbaiki.
Kruk Solusi termudah untuk masalah ini adalah dengan hanya melewatkan nilai scaleFactor negatif. Dalam praktiknya, pengguna tidak akan melihat bahwa gambar terkadang diperbesar dengan sedikit kurang lancar dari biasanya.
Alih-alih sebuah kesimpulan
Nasib permintaan tarik
Kami membuat perbaikan lokal dan membuat Permintaan Tarik terakhir di PhotoView. Terlepas dari kenyataan bahwa beberapa PR telah bertahan di sana selama setahun, PR kami telah ditambahkan ke cabang utama dan bahkan rilis baru PhotoView telah dirilis. Setelah itu kami memutuskan untuk memotong modul lokal dari Galeri Android dan menarik sumber PhotoView resmi. Untuk melakukan ini, saya harus menambahkan dukungan untuk AndroidX, yang telah ditambahkan ke PhotoView di versi 2.1.3 .
Di mana menemukan perpustakaan
Cari kode sumber pustaka Galeri Android di sini - https://github.com/redmadrobot-spb/android-gallery , bersama dengan instruksi untuk digunakan. Dan untuk mendukung proyek yang masih menggunakan Perpustakaan Dukungan, kami membuat versi terpisah dari galeri-android yang sudah tidak digunakan lagi . Tapi hati-hati, karena dalam setahun Perpustakaan Dukungan akan berubah menjadi labu!
Apa selanjutnya
Sekarang perpustakaan benar-benar cocok untuk kita, tetapi dalam proses pengembangan ide-ide baru muncul. Inilah beberapa di antaranya:
- kemampuan untuk menggunakan perpustakaan dalam tata letak apa pun, dan bukan hanya FragmentDialog yang terpisah;
- kemampuan untuk menyesuaikan UI;
- kemampuan untuk mengganti Gilde dan ExoPlayer;
- kemampuan untuk menggunakan sesuatu, bukan ViewPager.
Referensi
UPD
Saat menulis artikel, perpustakaan serupa dari pengembang FrescoImageViewer keluar . Mereka menambahkan dukungan untuk animasi transisi , tetapi sejauh ini kami hanya memiliki dukungan video. :)