- Как мы сделали свою библиотеку Android Gallery для просмотра медиаконтента
- Начало
- Реализуем библиотеку
- Реализуем функциональность
- Изучаем touch events для реализации swipe to dismiss
- Реализуем дебаунс в PhotoView
- Исправляем внезапный баг в PhotoView
- Вместо заключения
- Судьба пулл реквестов
- Где найти библиотеку
- Что дальше
- Ссылки
- What to Do If I Cannot View Pictures in Android Gallery?
- Why Can’t I View Pictures in Android Gallery?
- What to Do If I Cannot See the Photos on Gallery?
Как мы сделали свою библиотеку Android Gallery для просмотра медиаконтента
Привет, Хабр! Не так давно в поисках приключений, новых проектов и технологий я стал роботом устроился в Redmadrobot. Получил стул, монитор и макбук, а для разогрева — небольшой внутренний проект. Нужно было допилить и опубликовать самописную библиотеку для просмотра медиаконтента, которую мы используем в наших проектах. В статье я расскажу, как за неделю разобраться в touch events, стать опенсурсером, найти багу в Android sdk и опубликовать библиотеку.
Начало
Одна из важных фич наших приложений-магазинов — возможность просматривать видео и фото товаров и услуг вблизи и со всех сторон. Мы не хотели изобретать велосипед и отправились на поиски готовой библиотеки, которая бы нас устроила.
Планировали найти такое решение, чтобы пользователь мог:
- просматривать фотографии;
- масштабировать фото при помощи pinch to zoom и double tap;
- просматривать видео;
- листать медиаконтент;
- закрывать карточку с фото вертикальным свайпом (swipe to dismiss).
Вот, что мы нашли:
- FrescoImageViewer — поддерживает просмотр и пролистывание фото и основные жесты, однако не поддерживает просмотр видео и предназначен для библиотеки Fresco.
- PhotoView — поддерживает просмотр фото, большинство основных жестов управления, кроме пролистывания, swipe to dismiss, не поддерживает просмотр видео.
- PhotoDraweeView — аналогичная по функциональности PhotoView, но предназначена для Fresco.
Так как ни одна из найденных библиотек полностью не соотвествовала требованиям, нам пришлось написать свою.
Реализуем библиотеку
Чтобы получить нужную функциональность, мы доработали существующие решения из других библиотек. Тому, что получилось, решили дать скромное название Android Gallery.
Реализуем функциональность
Просмотр и масштабирование фотографий
Для просмотра фотографий взяли библиотеку PhotoView, которая из коробки поддерживает масштабирование.
Просмотр видео
Для просмотра видео взяли ExoPlayer, который переиспользутеся в MediaPagerAdapter. Когда пользователь открывает видео впервые, создаётся ExoPlayer. При переходе к другому элементу он ставится в очередь, так что при следующем запуске видео использоваться будет уже созданный экземпляр ExoPlayer. Это делает переход между элементами более плавным.
Перелистывание медиаконтента
Здесь мы использовали MultiTouchViewPager из FrescoImageViewer, который не перехватывает multi touch events, поэтому мы смогли добавить к нему жесты для масштабирования изображения.
Swipe to dismiss
В PhotoView не было поддержки swipe to dismiss и дебаунса (восстановления исходного размера картинки, когда картинка масштабируется в большую или меньшую сторону).
Вот как нам удалось с этим справиться.
Изучаем touch events для реализации swipe to dismiss
Прежде, чем перейти к поддержке swipe to dismiss, нужно разобраться, как работают touch events. Когда пользователь касается экрана, в текущей Activity вызывается метод dispatchTouchEvent(motionEvent: MotionEvent) , куда попадает MotionEvent.ACTION_DOWN . Этот метод решает дальнейшую судьбу события. Можно передать motionEvent в onTouchEvent(motionEvent: MotionEvent) на обработку касания или пустить дальше, сверху вниз по иерархии View. View, которая заинтересована в событии и/или в последующих событиях до ACTION_UP , возвращает true.
После все события текущего жеста (gesture) будут попадать в это View, пока жест не завершится событием ACTION_UP или родительский ViewGroup не перехватит управление (тогда во View придет событие ACTION_CANCELED ). Если событие обошло всю иерархию View и никого не заинтересовало, оно возвращается обратно в Activity в onTouchEvent(motionEvent: MotionEvent) .
В нашей библиотеке Android Gallery первое событие ACTION_DOWN доходит до dispatchTouchEvent() в PhotoView, где motionEvent передаётся в реализацию onTouch() , которая возвращает true. Дальше все события проходят такую же цепочку, пока не произойдёт одно из:
- ACTION_UP ;
- ViewPager попытается перехватить событие для пролистывания;
- VerticalDragLayout попытается перехватить событие для swipe to dismiss.
Перехват событий может осуществлять только ViewGroup в методе onInterceptTouchEvent(motionEvent: MotionEvent) . Даже если View заинтересована в каком-либо MotionEvent, само событие будет проходить через dispatchTouchEvent(motionEvent: MotionEvent) всей предшествующей цепочки ViewGroup. Соответственно родители всегда «наблюдают» за своими детьми. Любой родительский ViewGroup может перехватить событие и вернуть true в методе onInterceptTouchEvent(motionEvent: MotionEvent) , тогда все дочерние View получат MotionEvent.ACTION_CANCEL в onTouchEvent(motionEvent: MotionEvent) .
Пример: пользователь удерживает палец на некотором элементе в RecyclerView, тогда события обрабатываются в этом же элементе. Но как только он начнёт двигать пальцем вверх/вниз, RecyclerView перехватит события, и начнётся скролл, а View получит событие ACTION_CANCEL .
В Android Gallery VerticalDragLayout может перехватывать события для swipe to dismiss или ViewPager — для перелистывания. Но View может запретить родителю перехватывать события, вызвав метод requestDisallowInterceptTouchEvent(true) . Это может понадобиться, если View нужно совершить такие действия, перехват родителем которых для нас не желателен.
Например, когда пользователь в плеере проматывает трек к конкретному времени. Если бы родительский ViewPager перехватил горизонтальный скролл, произошёл бы переход к следующему треку.
Для обработки swipe to dismiss мы написали VerticalDragLayout, но он не получал touch событий от PhotoView. Чтобы понять почему так происходит, пришлось разобраться, как обрабатываются touch события в PhotoView.
- При MotionEvent.ACTION_DOWN в VerticalDragLayout срабатывает interceptTouchEvent() , который возвращает false, т.к. данный ViewGroup интересуют только вертикальные ACTION_MOVE. Направление ACTION_MOVE определяется в dispatchTouchEvent() , после чего событие передаётся в метод super.dispatchTouchEvent() во ViewGroup, где происходит передача события в реализацию interceptTouchEvent() в VerticalDragLayout.
Когда событие ACTION_DOWN доходит до метода onTouch() в PhotoView, то вьюха отбирает возможность перехватывать управление событиями. Все последующие события жеста не попадают в метод interceptTouchEvent() . Возможность перехватывать управление отдаётся родителю только в случае завершения жеста или если происходит горизонтальный ACTION_MOVE у правой/левой границы изображения.
Так как PhotoView разрешает родителю перехватывать управление только в случае горизонтального ACTION_MOVE , а swipe to dismiss — это вертикальный ACTION_MOVE , то VerticalDragLayout не может перехватить управление событиями для осуществления жеста. Для исправления нужно добавить возможность перехватывать управления в случае вертикального ACTION_MOVE .
Теперь в случае первого вертикального ACTION_MOVE PhotoView будет отдавать возможность перехвата родителю:
Следующий ACTION_MOVE будет перехвачен в VerticalDragLyout, при этом в дочерние View прилетит событие ACTION_CANCEL :
Все остальные ACTION_MOVE будут прилетать в VerticalDragLayout по стандартной цепочке. Важно, что после того как ViewGroup перехватывает управление событиями у дочернего View, дочерние View никак не могут вернуть себе управление.
Так мы реализовали поддержку swipe to dismiss для библиотеки PhotoView. В нашей библиотеке мы использовали вынесенные в отдельный модуль доработанные исходники PhotoView, а в оригинальный репозиторий PhotoView создали merge request.
Реализуем дебаунс в PhotoView
Напомним, что дебаунс — это анимация-восстановление допустимого масштаба, когда изображение масштабируется за его пределы.
В PhotoView такой возможности не было. Но раз уж мы начали копать чужой опенсорс, зачем останавливаться на достигнутом? В PhotoView можно задать ограничение на зум. Изначально это минимальный — х1 и максимальный — х3. За эти пределы изображение выйти не может.
Для начала мы решили убрать условие «запрет масштабирования по достижении минимума»: просто выкинули условие getScale() > mMinScale || scaleFactor > 1f . И тут внезапно…
Дебаунс заработал! Видимо, так произошло из-за того, что создатели библиотеки решили дважды подстраховаться, сделав и дебаунс, и ограничение на масштабирование. В реализации события onTouch, а именно в случае MotionEvent.ACTION_UP , если пользователь отмасштабировался больше/меньше максимума/минимума, запускается AnimatedZoomRunnable, который возвращает картинку к исходному размеру.
Также как и с swipe to dismiss, мы доработали PhotoView в исходниках нашей библиотеки и создали pull request с «добавлением» дебаунса в оригинальный PhotoView.
Исправляем внезапный баг в PhotoView
В PhotoView есть очень неприятный баг. Когда пользователь хочет увеличить изображение двойным тапом и у него случается приступ эпилепсии изображение начинает масштабироваться, оно может перевернуться на 180 градусов по вертикали. Этот баг можно встретить даже в популярных приложениях из Google Play, например, в ЦИАНе.
После долгого поиска мы всё-таки локализовали этот баг: иногда в матричное преобразование изображения для масштабирования на вход подаётся отрицательный scaleFactor, он-то и вызывает переворот изображения.
Для масштабирования из андроидовского ScaleGestureDetector достаём scaleFactor, который вычисляется следующим образом:
Если обложить данный метод дебаг-логами, можно отследить, при каких именно значениях переменных получается отрицательный scaleFactor:
Есть подозрение, что эту проблему пытались решить путём домножения spanDiff на SCALE_FACTOR == 0.5. Но это решение не поможет, если разница между mCurrSpan и mPrevSpan больше, чем в три раза. На этот баг уже даже заведён тикет, однако он до сих пор не исправлен.
Костыльное Самое простое решение этой проблемы — просто пропускать отрицательные значения scaleFactor. На практике пользователь не заметит, что изображение иногда зумируется чуть менее плавно, чем обычно.
Вместо заключения
Судьба пулл реквестов
Мы сделали локальное исправление и создали последний Pull Request в PhotoView. Несмотря на то, что некоторые PR висят там уже год, наши PR были добавлены в master-ветку и даже был выпущен новый релиз PhotoView. После чего мы решили выпилить локальный модуль из Android Gallery и подтянуть официальные исходники PhotoView. Для этого пришлось добавить поддержку AndroidX, который был добавлен в PhotoView в версии 2.1.3.
Где найти библиотеку
Исходный код библиотеки Android Gallery ищите тут — https://github.com/redmadrobot-spb/android-gallery, вместе с инструкцией по использованию. А для поддержки проектов, которые всё ещё используют Support Library, мы создали отдельную версию android-gallery-deprecated. Но будьте осторожны, ведь через год Support Library превратится в тыкву!
Что дальше
Сейчас библиотека полностью нас устраивает, но в процессе разработки возникли новые идеи. Вот некоторые из них:
- возможность использовать библиотеку в любой вёрстке, а не только отдельным FragmentDialog;
- возможность кастомизации UI;
- возможность подмены Gilde и ExoPlayer;
- возможность использовать что-то вместо ViewPager.
Ссылки
Пока писали статью, вышла похожая библиотека от разработчиков FrescoImageViewer. Они добавили поддержку transition animation, однако поддержка видео пока что есть только у нас. 🙂
Источник
What to Do If I Cannot View Pictures in Android Gallery?
Q: Unable to view photos from Gallery
I was able to view photos from Gallery in my Android phone while it was connected to PC through USB. But I am not able to see them in my phone. How can I view the pictures on my phone directly?
Sometimes users would meet such a problem that they cannot view pictures in Android Gallery after taking or downloading photos with their devices. If you are also one of these users, you must want to know the reasons for this issue and the solutions to solve this problem, right?
You can also click to learn:
Just take it easy and read on. This article will give you all the answers.
Why Can’t I View Pictures in Android Gallery?
Why the issue that pictures disappear from Gallery happens? The reasons are various but there two main causes actually:
1. The caches cause the malfunction
Once you open and manage the images in Gallery, there must be some caches created. And certainly, the caches that the Gallery App generates one time are very small. But if you don’t clear them regularly, these caches will become a huge trash which would take up a lot of space of the device. What’s worse, the caches will cause some issues also. For example, you will be unable to open and use the Gallery application smoothly and fail to view the pictures that you have saved in it.
2. The images are saved as in Nomedia file
A .nomedia file is basically a blank file placed inside a folder with «.nomedia» as extension. When the device detects such kind of file, it would not scan the folder so the files like photos in that folder won’t appear in the Gallery App. Thus, you need to make sure whether you have put the wanted pictures in the Nomedia file when you cannot see them in Gallery.
You can also read:
What to Do If I Cannot See the Photos on Gallery?
Normally, to make the pictures visible in this case, you have two choices:
Option 1. Clear the caches of Gallery
The most common situation is that the pictures in Gallery are corrupted due to the App caches so you are unable to open and view them successfully. In order to solve this problem, you need to delete the caches and useless data of the Gallery App. This would be easy:
Step 1. Go to the Home screen of your device and click on the Settings icon on it to open the application.
Step 2. In the Settings section, you need to find out the option Apps or Application Management and tap on it to display all the installed Apps on your device.
Step 3. Now you need to find the Gallery option from the listed applications and click on it to enter its App info page. On this page, you can see several options. Just tap on Clear cache and Clear data to remove the useless data from your device.
Option 2. Rename the Nomedia folder
It is possible that you have saved the photos in a .nomedia folder unwittingly or designedly to hide the image. So now, if you want to see them in the Gallery as usual, you need to delete them from the Nomedia folder or rename the folder to make them visible in Gallery. Then how to achieve this goal? You need to turn to an application- ES File Explorer. The whole process could be:
Step 1. Download and install the ES File Explorer on your Android phone.
Step 2. Next, you need to launch the file manager and find out the location of the images you want to display.
Step 3. Now just navigate to the folder and remove or rename the «.nomedia» file to make them visable.
Step 4. After that, you can restart your Android phone and go to the Gallery to see the pictures as usual.
If you want to hide some images or other files on your phone, you can also add a folder and name it «.nomedia» file again and move the files you wish to hide into the folder so that others cannot find the data on your phone.
Источник