Skip to content

Commit

Permalink
Merge pull request #4899 from sjudd:compose_gallery_sample
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 476430507
  • Loading branch information
glide-copybara-robot committed Sep 23, 2022
2 parents 5366e7b + 272c6c3 commit 94b86cc
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 197 deletions.
14 changes: 12 additions & 2 deletions samples/gallery/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ apply plugin: 'com.google.devtools.ksp'

dependencies {
implementation project(':library')
implementation project(':integration:compose')
implementation(project(':integration:recyclerview')) {
transitive = false
}

implementation "androidx.recyclerview:recyclerview:$ANDROID_X_RECYCLERVIEW_VERSION"
implementation "androidx.fragment:fragment-ktx:$ANDROID_X_FRAGMENT_VERSION"
implementation "androidx.core:core-ktx:$ANDROID_X_CORE_KTX_VERSION"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$ANDROID_X_LIFECYCLE_KTX_VERSION"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$ANDROID_X_LIFECYCLE_KTX_VERSION"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$JETBRAINS_KOTLINX_COROUTINES_VERSION"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$JETBRAINS_KOTLINX_COROUTINES_VERSION"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$JETBRAINS_KOTLIN_VERSION"
implementation "androidx.compose.foundation:foundation:$ANDROID_X_COMPOSE_VERSION"
implementation "androidx.compose.ui:ui:$ANDROID_X_COMPOSE_VERSION"

ksp project(':annotation:ksp')
}
Expand All @@ -37,6 +38,15 @@ android {
versionCode 1
versionName '1.0'
}
buildFeatures {
compose = true
}
kotlinOptions {
jvmTarget = "11"
}
composeOptions {
kotlinCompilerExtensionVersion '1.2.0'
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_11
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,101 @@
package com.bumptech.glide.samples.gallery

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.GridLayoutManager
import com.bumptech.glide.Glide
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
import kotlinx.coroutines.launch
import com.bumptech.glide.RequestManager
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
import com.bumptech.glide.integration.compose.GlideImage
import com.bumptech.glide.integration.compose.GlideLazyListPreloader
import com.bumptech.glide.signature.MediaStoreSignature

/** Displays media store data in a recycler view. */
/** Displays media store data in a recycler view. */
@OptIn(ExperimentalGlideComposeApi::class)
class HorizontalGalleryFragment : Fragment() {
private lateinit var adapter: RecyclerAdapter
private lateinit var recyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
val galleryViewModel: GalleryViewModel by viewModels()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
galleryViewModel.mediaStoreData.collect { data ->
adapter.setData(data)
}
return ComposeView(requireContext()).apply {
setContent { LoadableDeviceMedia(galleryViewModel) }
}
}

@Composable
fun LoadableDeviceMedia(viewModel: GalleryViewModel) {
val mediaStoreData = viewModel.mediaStoreData.collectAsState()
DeviceMedia(mediaStoreData.value)
}

@Composable
fun DeviceMedia(mediaStoreData: List<MediaStoreData>) {
val state = rememberLazyListState()
val requestManager = rememberRequestManager()
LazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp), state = state) {
items(mediaStoreData) { mediaStoreItem ->
MediaStoreView(mediaStoreItem, requestManager, Modifier.fillParentMaxSize())
}
}

GlideLazyListPreloader(
state = state,
data = mediaStoreData,
size = THUMBNAIL_SIZE,
numberOfItemsToPreload = 15,
fixedVisibleItemCount = 2,
) { item, requestBuilder ->
requestBuilder.load(item.uri).signature(item.signature())
}
}

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?,
): View? {
val result = inflater.inflate(R.layout.recycler_view, container, false)
recyclerView = result.findViewById<View>(R.id.recycler_view) as RecyclerView
val layoutManager = GridLayoutManager(activity, 1)
layoutManager.orientation = RecyclerView.HORIZONTAL
recyclerView.layoutManager = layoutManager
recyclerView.setHasFixedSize(true)

val glideRequests = Glide.with(this)
adapter = RecyclerAdapter(requireContext(), glideRequests)
val preloader = RecyclerViewPreloader(glideRequests, adapter, adapter, 3)
recyclerView.addOnScrollListener(preloader)
recyclerView.adapter = adapter
return result
@Composable
private fun rememberRequestManager() =
LocalContext.current.let { remember(it) { Glide.with(it) } }

private fun MediaStoreData.signature() = MediaStoreSignature(mimeType, dateModified, orientation)

@Composable
fun MediaStoreView(item: MediaStoreData, requestManager: RequestManager, modifier: Modifier) {
val signature = item.signature()

GlideImage(
model = item.uri,
contentDescription = item.displayName,
modifier = modifier,
) {
it
.thumbnail(
requestManager
.asDrawable()
.load(item.uri)
.signature(signature)
.override(THUMBNAIL_DIMENSION)
)
.signature(signature)
}
}

companion object {
private const val THUMBNAIL_DIMENSION = 50
private val THUMBNAIL_SIZE = Size(THUMBNAIL_DIMENSION.toFloat(), THUMBNAIL_DIMENSION.toFloat())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,48 +18,53 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize

/** Loads metadata from the media store for images and videos. */
class MediaStoreDataSource internal constructor(
/** Loads metadata from the media store for images and videos. */
class MediaStoreDataSource
internal constructor(
private val context: Context,
) {

fun loadMediaStoreData(): Flow<List<MediaStoreData>> = callbackFlow {
val contentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
launch {
trySend(query())
val contentObserver =
object : ContentObserver(Handler(Looper.getMainLooper())) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
launch { trySend(query()) }
}
}
}

context.contentResolver.registerContentObserver(
MEDIA_STORE_FILE_URI,
/* notifyForDescendants=*/ true,
contentObserver)
contentObserver
)

trySend(query())

awaitClose {
context.contentResolver.unregisterContentObserver(contentObserver)
}
awaitClose { context.contentResolver.unregisterContentObserver(contentObserver) }
}

private fun query(): MutableList<MediaStoreData> {
Preconditions.checkArgument(
Util.isOnBackgroundThread(), "Can only query from a background thread")
Util.isOnBackgroundThread(),
"Can only query from a background thread"
)
val data: MutableList<MediaStoreData> = ArrayList()
val cursor =
context
.contentResolver
.query(
MEDIA_STORE_FILE_URI,
PROJECTION,
FileColumns.MEDIA_TYPE + " = " + FileColumns.MEDIA_TYPE_IMAGE
+ " OR " + FileColumns.MEDIA_TYPE + " = " + FileColumns.MEDIA_TYPE_VIDEO,
/* selectionArgs= */ null,
"${MediaColumns.DATE_TAKEN} DESC"
) ?: return data
context.contentResolver.query(
MEDIA_STORE_FILE_URI,
PROJECTION,
FileColumns.MEDIA_TYPE +
" = " +
FileColumns.MEDIA_TYPE_IMAGE +
" OR " +
FileColumns.MEDIA_TYPE +
" = " +
FileColumns.MEDIA_TYPE_VIDEO,
/* selectionArgs= */ null,
"${MediaColumns.DATE_TAKEN} DESC"
)
?: return data

@Suppress("NAME_SHADOWING") // Might as well, it's the same object?
cursor.use { cursor ->
Expand All @@ -69,18 +74,18 @@ class MediaStoreDataSource internal constructor(
val mimeTypeColNum = cursor.getColumnIndexOrThrow(MediaColumns.MIME_TYPE)
val orientationColNum = cursor.getColumnIndexOrThrow(MediaColumns.ORIENTATION)
val mediaTypeColumnIndex = cursor.getColumnIndexOrThrow(FileColumns.MEDIA_TYPE)
val displayNameIndex = cursor.getColumnIndexOrThrow(FileColumns.DISPLAY_NAME)

while (cursor.moveToNext()) {
val id = cursor.getLong(idColNum)
val dateTaken = cursor.getLong(dateTakenColNum)
val mimeType = cursor.getString(mimeTypeColNum)
val dateModified = cursor.getLong(dateModifiedColNum)
val orientation = cursor.getInt(orientationColNum)
val displayName = cursor.getString(displayNameIndex)
val type =
if (cursor.getInt(mediaTypeColumnIndex) == FileColumns.MEDIA_TYPE_IMAGE)
Type.IMAGE
else
Type.VIDEO
if (cursor.getInt(mediaTypeColumnIndex) == FileColumns.MEDIA_TYPE_IMAGE) Type.IMAGE
else Type.VIDEO
data.add(
MediaStoreData(
type = type,
Expand All @@ -89,25 +94,31 @@ class MediaStoreDataSource internal constructor(
mimeType = mimeType,
dateModified = dateModified,
orientation = orientation,
dateTaken = dateTaken))
dateTaken = dateTaken,
displayName = displayName,
)
)
}
}
return data
}

companion object {
private val MEDIA_STORE_FILE_URI = MediaStore.Files.getContentUri("external")
private val PROJECTION = arrayOf(
MediaColumns._ID,
MediaColumns.DATE_TAKEN,
MediaColumns.DATE_MODIFIED,
MediaColumns.MIME_TYPE,
MediaColumns.ORIENTATION,
FileColumns.MEDIA_TYPE)
private val PROJECTION =
arrayOf(
MediaColumns._ID,
MediaColumns.DATE_TAKEN,
MediaColumns.DATE_MODIFIED,
MediaColumns.MIME_TYPE,
MediaColumns.ORIENTATION,
MediaColumns.DISPLAY_NAME,
FileColumns.MEDIA_TYPE
)
}
}

/** A data model containing data for a single media item. */
/** A data model containing data for a single media item. */
@Parcelize
data class MediaStoreData(
private val type: Type,
Expand All @@ -117,9 +128,11 @@ data class MediaStoreData(
val dateModified: Long,
val orientation: Int,
val dateTaken: Long,
val displayName: String?
) : Parcelable

/** The type of data. */
/** The type of data. */
enum class Type {
VIDEO, IMAGE
}
VIDEO,
IMAGE
}
Loading

0 comments on commit 94b86cc

Please sign in to comment.