Commit c6397e8d authored by Vladislav Bogdashkin's avatar Vladislav Bogdashkin 🎣

integrated glide

add photo slider
fix photo adapter
semi-working functionaly for photo screen
parent d95a21a3
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<AndroidXmlCodeStyleSettings>
<option name="ARRANGEMENT_SETTINGS_MIGRATED_TO_191" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
......
......@@ -159,6 +159,11 @@ dependencies {
//Photo view
implementation "com.github.chrisbanes:PhotoView:$photoViewVersion"
//Glide
implementation "com.github.bumptech.glide:glide:$glideVersion"
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
//Tests
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
......
......@@ -10,8 +10,6 @@ import com.biganto.visual.roompark.base.RoomParkApplication
import com.biganto.visual.roompark.domain.model.AlbumSortedModel
import com.biganto.visual.roompark.domain.model.PhotoModel
import com.biganto.visual.roompark.domain.model.PhotoResolutionModel
import com.biganto.visual.roompark.presentation.screen.photo.util.PhotosAdapter
import com.biganto.visual.roompark.presentation.screen.photo.util.PhotosViewHolder
import com.biganto.visual.roompark.presentation.screen.settings.util.CommonRecyclerAdapter
import com.biganto.visual.roompark.presentation.screen.settings.util.CommonViewHolder
import com.biganto.visual.roompark.util.view_utils.image_view.RoundedImageView
......
......@@ -9,7 +9,8 @@ import com.biganto.visual.roompark.R
import com.biganto.visual.roompark.base.RoomParkApplication
import com.biganto.visual.roompark.base.RoomParkMainActivity
import com.biganto.visual.roompark.conductor.BigantoBaseController
import com.biganto.visual.roompark.presentation.screen.photo.util.PhotoViewerAdapter
import com.biganto.visual.roompark.presentation.screen.photo.util.PhotoPreviewSlider
import com.biganto.visual.roompark.presentation.screen.photo.util.PhotosAdapter
import com.google.android.material.textview.MaterialTextView
import com.squareup.picasso.Picasso
import timber.log.Timber
......@@ -39,11 +40,26 @@ class PhotoScreenController :
@BindView(R.id.photo_frame)
lateinit var photoViewPager: ViewPager2
@BindView(R.id.photosPreviewSlider)
lateinit var slider: PhotoPreviewSlider
private fun bindRecycler() {
photoViewPager.isNestedScrollingEnabled = false
photoViewPager.offscreenPageLimit = 2
photoViewPager.adapter = PhotoViewerAdapter()
photoViewPager.adapter = PhotosAdapter()
setVewPager()
}
private fun setVewPager(){
val ap = PhotosAdapter()
photoViewPager.adapter = ap
photoViewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
photoViewPager.offscreenPageLimit = 2
ap.notifyDataSetChanged()
photoViewPager.invalidate()
slider.setUpViewPager(photoViewPager)
}
override fun onViewBound(v: View) {
......@@ -94,11 +110,13 @@ class PhotoScreenController :
lateinit var picassoAsync:Picasso
private fun render(viewState: PhotoScreenViewState.PhotoListLoaded) {
(photoViewPager.adapter as PhotoViewerAdapter).setItems(
(photoViewPager.adapter as PhotosAdapter).setItems(
viewState.list.asSequence().sortedBy{it.sort}.toList()
)
photoViewPager.currentItem = (photoViewPager.adapter as PhotoViewerAdapter)
.getItemPosition(viewState.selectedId)
photoViewPager.currentItem = viewState.selectedId
slider.visibility= View.VISIBLE
slider.setUpViewPager(photoViewPager)
}
private fun render(viewState: PhotoScreenViewState.AldumFetched) {
......@@ -107,7 +125,7 @@ class PhotoScreenController :
private fun render(viewState: PhotoScreenViewState.PhotoFetched){
(photoViewPager.adapter as PhotoViewerAdapter).setItems(arrayListOf(viewState.model))
(photoViewPager.adapter as PhotosAdapter).setItems(arrayListOf(viewState.model))
}
private fun render(viewState: PhotoScreenViewState.PhotoSelected){
// (photoViewPager.adapter as PhotoViewerAdapter).setItems(arrayListOf(viewState.model))
......
package com.biganto.visual.roompark.presentation.screen.photo.util
import android.content.Context
import android.graphics.SurfaceTexture
import android.util.AttributeSet
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.TextureView
import android.view.View
import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.photo_page_viewholder.view.*
import timber.log.Timber
/**
* Created by Vladislav Bogdashkin on 22.12.2019.
*/
class PhotoPreviewSlider @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextureView(context, attrs, defStyleAttr)
,TextureView.SurfaceTextureListener{
private var texThread: RendererThread?=null
private var adapter: PhotosAdapter? = null
private var vp:ViewPager2? = null
fun setUpViewPager(viewpager:ViewPager2) {
this.adapter = viewpager.adapter as PhotosAdapter
with(viewpager) {
setPageTransformer { page, position ->
setParallaxTransformation(page, position)
}
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
var actualPosition = 0
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
if (position == actualPosition){
processSliding(positionOffset)
}
else{
processSliding(positionOffset-1)
}
super.onPageScrolled(position, positionOffset, positionOffsetPixels)
}
override fun onPageSelected(position: Int) {
actualPosition = position
texThread?.setIndex(position)
super.onPageSelected(position)
}
})
}
vp = viewpager
}
private fun setParallaxTransformation(page: View, position: Float) {
page.apply {
when {
position < -1 -> // [-Infinity,-1)
// This page is way off-screen to the left.
alpha = 1f
position < -1 -> // [-1,0){
{
}
position <= 1 -> { // [-1,1]
photo_view.translationX = -position * (width / 2)
// slider.processSliding(position)
}
else -> // (1,+Infinity]
{
// This page is way off-screen to the right.
alpha = 1f
}
}
}
}
fun processSliding(_position:Float){
texThread?.setSlidePosition(_position)
}
val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener(){
override fun onDoubleTap(e: MotionEvent?): Boolean {
Timber.d( "double tap")
return true
}
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
mIsScrolling=true
Timber.d( "scroll")
texThread?.perfomDrag(distanceX)
return true
}
override fun onDown(e: MotionEvent?): Boolean {
Timber.d( "down")
return true
}
})
private var mIsScrolling = false
init {
surfaceTextureListener=this
val gestureListener = object : OnTouchListener {
private var gesture: GestureDetector = gestureDetector
override fun onTouch(v: View, event: MotionEvent): Boolean {
Timber.w(" ON TOUCH EVENT!")
val performTouch = gesture.onTouchEvent(event)
if (performTouch) return true
if (event.action == MotionEvent.ACTION_UP) {
if (mIsScrolling) {
Timber.d("OnTouchListener --> onTouch ACTION_UP")
mIsScrolling = false
texThread?.perfomDrag(null,mIsScrolling)
}
}
if (event.action == MotionEvent.ACTION_BUTTON_PRESS)
performClick()
return false
}
}
setOnTouchListener(gestureListener)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
Timber.d("On Size Chagned")
super.onSizeChanged(w, h, oldw, oldh)
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture?, width: Int, height: Int) {
print(" onSurfaceTextureSizeChanged")
texThread?.setSize(width,height)
//Ignored
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture?) {
print(" onSurfaceTextureUpdated")
//Ignored
}
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture?): Boolean {
texThread?.isStopped=true
return true
}
override fun onSurfaceTextureAvailable(surface: SurfaceTexture?, width: Int, height: Int) {
Timber.d("avaliable surf: $surface")
surface?.let {
if (texThread?.isAlive==true)
return
texThread = RendererThread(context,it)
texThread?.setSize(width,height)
texThread?.onChangePage = {ind -> vp?.setCurrentItem(ind,false)}
texThread?.start()
adapter?.let {adapter ->
texThread?.setAdapter(adapter.photosPreviewList)
// texThread?.setIndex(min(_l.size,2))
}?: error("adapter not ready!")
}
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
texThread?.isStopped=true
}
}
......@@ -8,7 +8,8 @@ import com.biganto.visual.roompark.domain.model.PhotoModel
import com.biganto.visual.roompark.domain.model.PhotoResolutionModel
import com.biganto.visual.roompark.presentation.screen.settings.util.CommonRecyclerAdapter
import com.biganto.visual.roompark.presentation.screen.settings.util.CommonViewHolder
import com.biganto.visual.roompark.util.view_utils.image_view.RoundedImageView
import com.github.chrisbanes.photoview.PhotoView
import timber.log.Timber
/**
* Created by Vladislav Bogdashkin on 16.10.2019.
......@@ -16,14 +17,23 @@ import com.biganto.visual.roompark.util.view_utils.image_view.RoundedImageView
class PhotosAdapter : CommonRecyclerAdapter<PhotosViewHolder, PhotoModel>() {
val photosPreviewList:List<String> get() {
val ret = list.map { it.resolutionList.lowelest()?.url?: error("No res urls!")}.toList()
Timber.d("$ret")
return ret
}
override val vhKlazz = PhotosViewHolder::class
override fun getVhLayout(): Int = R.layout.photo_preview_viewholder
override fun getVhLayout(): Int = R.layout.photo_page_viewholder
}
class PhotosViewHolder(itemView: View) : CommonViewHolder<PhotoModel>(itemView) {
@BindView(R.id.photo_preview_imageview) lateinit var photoPreview: RoundedImageView
@BindView(R.id.photo_view) lateinit var photoPreview: PhotoView
private val picassoAsync by lazy {
return@lazy RoomParkApplication.component.providePicassoAsync()
......@@ -39,5 +49,5 @@ class PhotosViewHolder(itemView: View) : CommonViewHolder<PhotoModel>(itemView)
}}
}
fun List<PhotoResolutionModel>.lowelest() =
private fun List<PhotoResolutionModel>.lowelest() =
this.minBy { it.resWidth * it.resHeight }
\ No newline at end of file
package com.biganto.visual.roompark.presentation.screen.photo.util
import android.content.Context
import android.graphics.*
import android.graphics.drawable.Drawable
import android.view.Surface
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import timber.log.Timber
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.sign
import kotlin.math.sin
/**
* Created by Vladislav Bogdashkin on 18.07.2019.
*/
private const val CONST_SLEEP_THRESHOLD = 8L
private const val CONST_CENTER_PHOTO_BORDER_PX = 8
private const val CONST_CENTER_HEIGHT_DIFF_PX = 0
private const val CONST_HEIGHT_MARGIN_BORDER_PX = 10
private const val CONST_DIFF_BORDER_PX = 1
private const val CONST_DECAY_TIMER = 500L
class RendererThread(val context: Context, val surface: SurfaceTexture)
: Thread(){
private val dens:Float by lazy { context.resources.displayMetrics.density }
private val SLEEP_THRESHOLD get() = CONST_SLEEP_THRESHOLD
private val CENTER_PHOTO_BORDER_PX get() =
(CONST_CENTER_PHOTO_BORDER_PX *dens).int
private val CENTER_HEIGHT_DIFF_PX get() =
(CONST_CENTER_HEIGHT_DIFF_PX *dens).int
private val HEIGHT_MARGIN_BORDER_PX get() =
(CONST_HEIGHT_MARGIN_BORDER_PX *dens).int
private val DIFF_BORDER_PX get() =
(CONST_DIFF_BORDER_PX *dens).int
private val DECAY_TIMER get() = CONST_DECAY_TIMER
private var isDirty = true
var isStopped = false
private var width = 0
private var height = 0
private var bmpCache:MutableList<Bitmap?> = mutableListOf()
private var holdIndex = 0
get() = if (isDragging) field
else currentIndex
private var currentIndex:Int = 0
set(value) {Timber.w("currentIndex to: $value");field = value}
private var bmpCacheUrls:List<String> = arrayListOf()
fun setAdapter(urls:List<String> ) {
bmpCacheUrls = urls
bmpCache = MutableList(bmpCacheUrls.size) { null}
bmpCacheUrls.fetchUrls()
bordersArray = IntArray(bmpCacheUrls.size*4)
cropArray = IntArray(bmpCacheUrls.size*4)
}
private fun List<String>.fetchUrls(){
this.forEachIndexed{index, s ->
if (bmpCache.getOrNull(index) == null){
Timber.d("loading: $s")
Glide.with(context)
.asBitmap()
.load(bmpCacheUrls[index])
.apply(RequestOptions().override(width/bmpCache.size, height))
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(object : CustomTarget<Bitmap>(){
override fun onLoadCleared(placeholder: Drawable?) {
Timber.d(" onLoadCleared ")
bmpCache[index] = null
}
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
Timber.d(" onResourceReady $resource")
bmpCache[index] = resource
}
})
}
}
}
fun setIndex(index:Int){
if (!isDragging) holdIndex = index
currentIndex = index
isDirty=true
}
private var bordersArray:IntArray? = null
private var cropArray:IntArray? = null
private fun IntArray.getPosition(dir:Float):Int {
if (dir < 0 && this.getX(currentIndex) > width / 2f)
return (currentIndex - 1).coerceIn(0,this.size/4-1)
if (dir > 0 && this.getRight(currentIndex) < width / 2f)
return (currentIndex + 1).coerceIn(0,this.size/4-1)
return currentIndex
}
private fun IntArray.getX(index: Int) = if (index>=0) this[index*4+0] else 0
private fun IntArray.getY(index: Int) = this[index*4+1]
private fun IntArray.getRight(index: Int) = if (index>=0) this[index*4+2] else 0
private fun IntArray.getBottom(index: Int) = this[index*4+3]
private fun IntArray.getWidth(index: Int) = this[index*4+2] - this[index*4+0]
private fun IntArray.getHeight(index: Int) = this[index*4+3] - this[index*4+1]
private fun IntArray.aspect(index: Int) =
this.getWidth(index).toFloat()/this.getHeight(index).toFloat()
private fun IntArray.setX(index:Int,block:()->Int){
this[index*4+0] = block.invoke()
}
private fun IntArray.setY(index:Int,block:()->Int){
this[index*4+1] = block.invoke()
}
private fun IntArray.setRight(index:Int,block:()->Int){
this[index*4+2] = block.invoke()
}
private fun IntArray.setBottom(index:Int,block:()->Int){
this[index*4+3] = block.invoke()
}
private val Bitmap.aspect:Float get() = this.width.toFloat()/this.height.toFloat()
private fun calcBorders() {
val centerX = width / 2f - scrollOffset
val adjWidth = height * .6f
bmpCache.forEachIndexed { index, bitmap ->
val limit = (2 - (index - currentIndex).absoluteValue).coerceIn(0, 1)
val direction = (index - currentIndex).sign//cos(((index-currentIndex).sign)*1.57f)
val ch2 = limit * (
(slidePosition * direction).coerceIn(0f, 1f)
+ (1f - direction.absoluteValue) * (1f - slidePosition.absoluteValue)
)
bordersArray?.let { arr ->
val wDiff = height * (bitmap?.aspect ?: 0f)
val _width = adjWidth +
dragKoef*((wDiff - adjWidth) / 2f + CENTER_PHOTO_BORDER_PX) * ch2
arr.setX(index) { arr.getRight(index - 1) }
arr.setRight(index) {(arr.getX(index) + _width).int}
arr.setY(index) {(HEIGHT_MARGIN_BORDER_PX + CENTER_HEIGHT_DIFF_PX * (1f - ch2)).int}
arr.setBottom(index) {
height - (HEIGHT_MARGIN_BORDER_PX + CENTER_HEIGHT_DIFF_PX * (1f - ch2)).int
}
}
cropArray?.let { arr ->
val asp = bordersArray?.aspect(index) ?: 0f
bitmap?.let {
if (asp >= it.aspect) {
arr.setX(index) { 0 }
arr.setRight(index) { it.width }
val hKef = it.aspect / asp
val marg = it.height * (1f - hKef) / 2f
arr.setY(index) { marg.int }
arr.setBottom(index) { it.height - marg.int }
} else {
arr.setY(index) { 0 }
arr.setBottom(index) { it.height }
val hKef = asp / it.aspect
val marg = it.width * (1f - hKef) / 2f
arr.setX(index) { marg.int }
arr.setRight(index) { it.width - marg.int }
}
}
}
}
bordersArray?.let { arr ->
val halfW = arr.getWidth(currentIndex) / 2f
val minusW = arr.getX(holdIndex) - centerX + halfW * (1f + 2f * slidePosition)
bmpCache.forEachIndexed { index, bitmap ->
val limit = (2 - (index - currentIndex).absoluteValue).coerceIn(0, 1)
val direction = (index - currentIndex).sign
val cBorder = (CENTER_PHOTO_BORDER_PX)
val borderKoef = (
direction + 1f
- limit * sin((direction + slidePosition.sign).coerceIn(-1f, 1f) * 1.57f)
* slidePosition.absoluteValue
)
val borderSize = dragKoef*cBorder * borderKoef
arr.setX(index) {(arr.getX(index) - minusW.int + borderSize + DIFF_BORDER_PX).int}
arr.setRight(index) {
(arr.getRight(index) - minusW.int + borderSize - DIFF_BORDER_PX).int
}
}
}
}
private val Float.int:Int get() {return this.toInt()}
fun perfomDrag(distanceX:Float?,dragging:Boolean = true){
isDragging = dragging
if (isDragging) distanceX?.let {perfomScroll(it)}
else {
holdIndex = currentIndex
scrollOffset = 0f
touchOffset=0f
}
isDirty = true
}
private fun perfomScroll(distanceX:Float) {
touchOffset += distanceX * scrollScaleFactor
val p = bordersArray?.getPosition(distanceX.sign)?:0
if (p != currentIndex) {
setIndex(p)
onChangePage?.invoke(p)
}
isDirty=true
}
var onChangePage: ((newPage:Int)->Unit)? = null
private var scrollScaleFactor = 1f
private var touchOffset = 0f
private var scrollOffset = 0f
private var isDragging = false
private var slidePosition = 0f
private var dragKoef = 1f
fun setSlidePosition(newPosition:Float){
slidePosition = newPosition
isDirty = true
}
private var decayFraction = 0f
private fun timerDecay(mills:Long){
isDirty=true
decayFraction+=mills
animateBorders()
scrollOffset = scrollOffset.smoothLerp(touchOffset-scrollOffset,.2f)
if (touchOffset> 0 && scrollOffset >= touchOffset) {
scrollOffset = touchOffset
isDirty=false
}
else if (touchOffset< 0 && scrollOffset <= touchOffset) {
scrollOffset = touchOffset
isDirty=false
}
if (decayFraction> DECAY_TIMER) isDirty=false
val dirty = animateBorders()
if (!isDirty) isDirty = dirty
}
private fun animateBorders():Boolean{
if (isDragging && dragKoef>0){
dragKoef = dragKoef.smoothLerp(-dragKoef,.12f)
if (dragKoef<.1f) dragKoef = 0f
return dragKoef>0
}
if (!isDragging && dragKoef<1f) {
dragKoef = dragKoef.smoothLerp(1f-dragKoef,.03f)
if (dragKoef>.99f) dragKoef = 1f
return dragKoef<1f
}
return false
}
private fun Float.smoothLerp( delta: Float, fraction:Float =.4f): Float {
return this + fraction * (delta)
}
fun setSize(w: Int, h: Int){
width=w
height=h
}
override fun run() {
super.run()
Timber.w("isStopped: $isStopped")
Timber.w("isDirty: $isDirty")
isDirty = true
while (!isStopped) {
while (!isDirty)
sleep(SLEEP_THRESHOLD) // in real life this sleep is more complicated
isDirty = false
val surf = Surface(surface)
val dRect = Rect(
width.div(4) + width.div(2)
, HEIGHT_MARGIN_BORDER_PX
, width - width.div(4)
, height.minus(HEIGHT_MARGIN_BORDER_PX)
)
val c = surf.lockCanvas(dRect)
c.drawColor(Color.TRANSPARENT)
timerDecay(SLEEP_THRESHOLD)
if (bmpCache.size>0) calcBorders()
Timber.d("bmpCache ${bmpCache.size}")
bmpCache.forEachIndexed { index, bitmap ->
// Timber.d("bitmap ${bitmap} / $index")
bitmap?.let {
val bmpX = cropArray?.getX(index) ?: 0
val bmpY = cropArray?.getY(index) ?: 0
val bmpRight = cropArray?.getRight(index) ?: 0
val bmpBottom = cropArray?.getBottom(index) ?: 0
val dstX = bordersArray?.getX(index) ?: 0
val dstY = bordersArray?.getY(index) ?: 0
val dstRight = bordersArray?.getRight(index) ?: 0
val dstBottom = bordersArray?.getBottom(index) ?: 0
c.drawBitmap(
it
, Rect(bmpX, bmpY, bmpRight, bmpBottom)
, Rect(dstX, dstY, dstRight, dstBottom)
, null
)
}
}
surf.unlockCanvasAndPost(c)
surf.release()
// isDirty = false
}
Timber.w("STOPPED; $isStopped")
Timber.w("STOPPED; $isDirty")
isDirty=false
}
private fun calcHeightPixel(index:Int, selectedIndex:Int): Int {
return height
}
private fun calcWidthPixel(index:Int, selectedIndex:Int): Int {
val dist = abs(index-selectedIndex)
val bled = index - selectedIndex
// Timber.d("gonna calc: $bled")
try {
var bled_res = 100;
// Timber.d("res is : $bled_res")
if (bled==0) bled_res = calcHeightPixel(index,selectedIndex)
else bled_res = (calcHeightPixel(index,selectedIndex)* (1f - .05f*dist)).int
bled_res = when(bled)
{
0 -> calcHeightPixel(index,selectedIndex)
in -1 downTo Int.MIN_VALUE -> (calcHeightPixel(index,selectedIndex)* (1f - .05f*dist)).int
in 1 .. Int.MAX_VALUE -> (calcHeightPixel(index,selectedIndex)* (1f - .05f*dist)).int
else -> error("Unexeptable value")
}
// Timber.d("res is : $bled_res")
return bled_res
}catch (e:Exception){Timber.e(e)}
return -1
}
private fun Bitmap.flip():Bitmap{
val flip = Matrix()
flip.postScale(1f, -1f)
return Bitmap.createBitmap(
this,
0,
0,
this.width,
this.height,
flip,
true
)
}
}
......@@ -67,12 +67,14 @@
app:layout_constraintTop_toTopOf="@+id/guideline2"
app:srcCompat="@drawable/iic_full_view" />
<androidx.recyclerview.widget.RecyclerView
<com.biganto.visual.roompark.presentation.screen.photo.util.PhotoPreviewSlider
android:id="@+id/photosPreviewSlider"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:orientation="horizontal"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/change_size_button"
app:layout_constraintStart_toEndOf="@+id/show_full_button"
......
......@@ -26,4 +26,5 @@ ext {
exoPlayerVersion = "2.10.8"
photoViewVersion = "2.0.0"
viewPager2Version = "1.0.0"
glideVersion = "4.11.0"
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment