Created Photos logic and displaying,

also realised saving in cash and getting from cash
This commit is contained in:
Alexey committed 2025-03-15 23:54:11 +06:00
1 parent 9ec4271c73
commit 2e9df075b8
40 files changed
+775 -186

No files matched your search

+8
View File
@@ -4,6 +4,14 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-03-15T17:40:38.891328100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Notnik_kg\.android\avd\Pixel_4_XL_API_30_1.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>
+2 -1
View File
@@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@@ -13,7 +14,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.GalleryKotlin" android:theme="@style/Theme.Gallery"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,15 @@
package com.example.gallery
import android.app.Application
class App : Application() {
companion object {
lateinit var instance: App
private set
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
@@ -7,22 +7,35 @@ import kotlinx.coroutines.withContext
class GalleryRepository { class GalleryRepository {
private val apiService = RetrofitClient.apiService private val apiService = RetrofitClient.apiService
private val photoCache = PhotoCache()
suspend fun getAlbums(): Result<List<Album>> = withContext(Dispatchers.IO) { suspend fun getAlbums(): Result<List<Album>> = withContext(Dispatchers.IO) {
try { try {
val response = apiService.getAlbums() val response = apiService.getAlbums()
photoCache.saveAlbums(response)
Result.success(response) Result.success(response)
} catch (e: Exception) { } catch (e: Exception) {
val cachedAlbums = photoCache.getAlbums()
if (cachedAlbums != null) {
Result.success(cachedAlbums)
} else {
Result.failure(e) Result.failure(e)
} }
} }
}
suspend fun getAlbumDetails(albumId: Int): Result<Album> = withContext(Dispatchers.IO) { suspend fun getAlbumDetails(albumId: Int): Result<Album> = withContext(Dispatchers.IO) {
try { try {
val response = apiService.getAlbumDetails(albumId) val response = apiService.getAlbumDetails(albumId)
photoCache.saveAlbumDetails(response)
Result.success(response) Result.success(response)
} catch (e: Exception) { } catch (e: Exception) {
val cachedAlbum = photoCache.getAlbumDetails(albumId)
if (cachedAlbum != null) {
Result.success(cachedAlbum)
} else {
Result.failure(e) Result.failure(e)
} }
} }
} }
}
@@ -0,0 +1,45 @@
package com.example.gallery.repository
import android.content.Context
import com.example.gallery.App
import com.example.gallery.models.Album
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class PhotoCache {
companion object {
private const val ALBUMS_CACHE_KEY = "albums_cache"
private const val ALBUM_DETAILS_CACHE_KEY = "album_details_cache_"
}
private val sharedPreferences = App.instance.getSharedPreferences("photo_cache", Context.MODE_PRIVATE)
fun saveAlbums(albums: List<Album>) {
val json = Gson().toJson(albums)
sharedPreferences.edit().putString(ALBUMS_CACHE_KEY, json).apply()
}
fun getAlbums(): List<Album>? {
val json = sharedPreferences.getString(ALBUMS_CACHE_KEY, null) ?: return null
val type = object : TypeToken<List<Album>>() {}.type
return try {
Gson().fromJson(json, type)
} catch (e: Exception) {
null
}
}
fun saveAlbumDetails(album: Album) {
val json = Gson().toJson(album)
sharedPreferences.edit().putString(ALBUM_DETAILS_CACHE_KEY + album.id, json).apply()
}
fun getAlbumDetails(albumId: Int): Album? {
val json = sharedPreferences.getString(ALBUM_DETAILS_CACHE_KEY + albumId, null) ?: return null
return try {
Gson().fromJson(json, Album::class.java)
} catch (e: Exception) {
null
}
}
}
@@ -80,6 +80,12 @@ class AlbumsFragment : Fragment() {
.show() .show()
} }
} }
viewModel.isFromCache.observe(viewLifecycleOwner) { isFromCache ->
if (isFromCache) {
Snackbar.make(binding.root, "Данные загружены из кэша. Нет подключения к интернету.", Snackbar.LENGTH_LONG).show()
}
}
} }
override fun onDestroyView() { override fun onDestroyView() {
@@ -1,9 +1,13 @@
package com.example.gallery.ui.albums package com.example.gallery.ui.albums
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.example.gallery.App
import com.example.gallery.models.Album import com.example.gallery.models.Album
import com.example.gallery.repository.GalleryRepository import com.example.gallery.repository.GalleryRepository
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -20,6 +24,9 @@ class AlbumsViewModel : ViewModel() {
private val _error = MutableLiveData<String?>() private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _error val error: LiveData<String?> = _error
private val _isFromCache = MutableLiveData<Boolean>()
val isFromCache: LiveData<Boolean> = _isFromCache
init { init {
loadAlbums() loadAlbums()
} }
@@ -28,11 +35,16 @@ class AlbumsViewModel : ViewModel() {
viewModelScope.launch { viewModelScope.launch {
_isLoading.value = true _isLoading.value = true
_error.value = null _error.value = null
_isFromCache.value = false
repository.getAlbums().fold( repository.getAlbums().fold(
onSuccess = { albumsList -> onSuccess = { albumsList ->
_albums.value = albumsList _albums.value = albumsList
_isLoading.value = false _isLoading.value = false
if (albumsList.isNotEmpty() && !isInternetAvailable()) {
_isFromCache.value = true
}
}, },
onFailure = { e -> onFailure = { e ->
_error.value = e.message ?: "Неизвестная ошибка" _error.value = e.message ?: "Неизвестная ошибка"
@@ -45,4 +57,11 @@ class AlbumsViewModel : ViewModel() {
fun refreshAlbums() { fun refreshAlbums() {
loadAlbums() loadAlbums()
} }
private fun isInternetAvailable(): Boolean {
val connectivityManager = App.instance.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
} }
@@ -0,0 +1,56 @@
package com.example.gallery.ui.photos
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.example.gallery.databinding.ItemPhotoBinding
import com.example.gallery.models.Photo
import com.example.gallery.ui.ImageLoader
class PhotoAdapter(private val onPhotoClick: (Photo) -> Unit) :
ListAdapter<Photo, PhotoAdapter.PhotoViewHolder>(PhotoDiffCallback()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhotoViewHolder {
val binding = ItemPhotoBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return PhotoViewHolder(binding)
}
override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
holder.bind(getItem(position))
}
inner class PhotoViewHolder(private val binding: ItemPhotoBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
binding.root.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
onPhotoClick(getItem(position))
}
}
}
fun bind(photo: Photo) {
binding.photoTitle.text = photo.title
binding.photoDescription.text = photo.description
ImageLoader.loadImage(binding.photoImage, photo.imageUrl)
}
}
private class PhotoDiffCallback : DiffUtil.ItemCallback<Photo>() {
override fun areItemsTheSame(oldItem: Photo, newItem: Photo): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Photo, newItem: Photo): Boolean {
return oldItem == newItem
}
}
}
@@ -0,0 +1,45 @@
package com.example.gallery.ui.photos
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs
import com.example.gallery.databinding.FragmentPhotoDetailBinding
import com.example.gallery.ui.ImageLoader
class PhotoDetailFragment : Fragment() {
private var _binding: FragmentPhotoDetailBinding? = null
private val binding get() = _binding!!
private val args: PhotoDetailFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentPhotoDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val photo = args.photo
binding.apply {
photoTitle.text = photo.title
photoDescription.text = photo.description
ImageLoader.loadImage(photoImage, photo.imageUrl)
}
requireActivity().title = photo.title
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,104 @@
package com.example.gallery.ui.photos
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import com.example.gallery.databinding.FragmentPhotosBinding
import com.google.android.material.snackbar.Snackbar
class PhotosFragment : Fragment() {
private var _binding: FragmentPhotosBinding? = null
private val binding get() = _binding!!
private val viewModel: PhotosViewModel by viewModels()
private lateinit var photoAdapter: PhotoAdapter
private val args: PhotosFragmentArgs by navArgs()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentPhotosBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
setupSwipeRefresh()
observeViewModel()
viewModel.loadPhotos(args.album)
}
private fun setupRecyclerView() {
photoAdapter = PhotoAdapter { photo ->
val action = PhotosFragmentDirections.actionPhotosFragmentToPhotoDetailFragment(photo)
findNavController().navigate(action)
}
binding.recyclerView.apply {
adapter = photoAdapter
layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
setHasFixedSize(true)
}
}
private fun setupSwipeRefresh() {
binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.refreshPhotos(args.album.id)
}
}
private fun observeViewModel() {
viewModel.photos.observe(viewLifecycleOwner) { photos ->
photoAdapter.submitList(photos)
binding.emptyView.isVisible = photos.isEmpty()
}
viewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
binding.swipeRefreshLayout.isRefreshing = isLoading
binding.shimmerLayout.isVisible = isLoading && photoAdapter.itemCount == 0
if (isLoading) {
binding.shimmerLayout.startShimmer()
} else {
binding.shimmerLayout.stopShimmer()
}
}
viewModel.error.observe(viewLifecycleOwner) { errorMessage ->
if (!errorMessage.isNullOrEmpty()) {
Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_LONG)
.setAction("Повторить") {
viewModel.refreshPhotos(args.album.id)
}
.show()
}
}
viewModel.isFromCache.observe(viewLifecycleOwner) { isFromCache ->
if (isFromCache) {
Snackbar.make(binding.root, "Данные загружены из кэша. Нет подключения к интернету.", Snackbar.LENGTH_LONG).show()
}
}
viewModel.albumTitle.observe(viewLifecycleOwner) { title ->
requireActivity().title = title
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,94 @@
package com.example.gallery.ui.photos
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.gallery.App
import com.example.gallery.models.Album
import com.example.gallery.models.Photo
import com.example.gallery.repository.GalleryRepository
import kotlinx.coroutines.launch
class PhotosViewModel : ViewModel() {
private val repository = GalleryRepository()
private val _photos = MutableLiveData<List<Photo>>()
val photos: LiveData<List<Photo>> = _photos
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean> = _isLoading
private val _error = MutableLiveData<String?>()
val error: LiveData<String?> = _error
private val _albumTitle = MutableLiveData<String>()
val albumTitle: LiveData<String> = _albumTitle
private val _isFromCache = MutableLiveData<Boolean>()
val isFromCache: LiveData<Boolean> = _isFromCache
fun loadPhotos(album: Album) {
_albumTitle.value = album.title
_isFromCache.value = false
album.photos?.let {
_photos.value = it
return
}
viewModelScope.launch {
_isLoading.value = true
_error.value = null
repository.getAlbumDetails(album.id).fold(
onSuccess = { albumDetails ->
_photos.value = albumDetails.photos ?: emptyList()
_isLoading.value = false
if ((albumDetails.photos?.isNotEmpty() == true) && !isInternetAvailable()) {
_isFromCache.value = true
}
},
onFailure = { e ->
_error.value = e.message ?: "Неизвестная ошибка"
_isLoading.value = false
}
)
}
}
fun refreshPhotos(albumId: Int) {
viewModelScope.launch {
_isLoading.value = true
_error.value = null
_isFromCache.value = false
repository.getAlbumDetails(albumId).fold(
onSuccess = { albumDetails ->
_photos.value = albumDetails.photos ?: emptyList()
_isLoading.value = false
if ((albumDetails.photos?.isNotEmpty() == true) && !isInternetAvailable()) {
_isFromCache.value = true
}
},
onFailure = { e ->
_error.value = e.message ?: "Неизвестная ошибка"
_isLoading.value = false
}
)
}
}
// Проверка доступности интернета
private fun isInternetAvailable(): Boolean {
val connectivityManager = App.instance.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val networkCapabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M22,16L22,4c0,-1.1 -0.9,-2 -2,-2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2zM11,12l2.03,2.71L16,11l4,5L8,16l3,-4zM2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6L2,6z" />
</vector>
@@ -1,170 +1,74 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector
android:width="108dp"
android:height="108dp" android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> xmlns:android="http://schemas.android.com/apk/res/android">
<path <path android:fillColor="#3DDC84"
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/> android:pathData="M0,0h108v108h-108z"/>
<path <path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M9,0L9,108" <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:pathData="M19,0L19,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M29,0L29,108" <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:pathData="M39,0L39,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M49,0L49,108" <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:pathData="M59,0L59,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M69,0L69,108" <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:pathData="M79,0L79,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M89,0L89,108" <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:pathData="M99,0L99,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,9L108,9" <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:pathData="M0,19L108,19" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,29L108,29" <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector> </vector>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/colorPrimary" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/ic_gallery_logo" />
</item>
</layer-list>
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
tools:context=".ui.photo_detail.PhotoDetailFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.card.MaterialCardView
android:id="@+id/imageCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardCornerRadius="12dp"
app:cardElevation="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/photoImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="Полное фото"
android:scaleType="fitCenter"
tools:src="@tools:sample/backgrounds/scenic" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/photoTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageCard"
tools:text="Название фотографии" />
<TextView
android:id="@+id/photoDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/photoTitle"
app:layout_constraintVertical_bias="0.0"
tools:text="Подробное описание фотографии. Это может быть длинный текст с информацией о фотографии, месте, где она была сделана, и другими интересными деталями." />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.photos.PhotosFragment">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="8dp"
app:layoutManager="androidx.recyclerview.widget.StaggeredGridLayoutManager"
app:spanCount="2"
tools:itemCount="6"
tools:listitem="@layout/item_photo" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/shimmerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include layout="@layout/shimmer_photo_item" />
<include layout="@layout/shimmer_photo_item" />
<include layout="@layout/shimmer_photo_item" />
<include layout="@layout/shimmer_photo_item" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
<TextView
android:id="@+id/emptyView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="В этом альбоме нет фотографий"
android:textSize="18sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
+63
View File
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/photoImage"
android:layout_width="0dp"
android:layout_height="0dp"
android:contentDescription="Фотография"
android:scaleType="centerCrop"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/backgrounds/scenic" />
<TextView
android:id="@+id/photoTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/photoImage"
tools:text="Название фото" />
<TextView
android:id="@+id/photoDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:ellipsize="end"
android:maxLines="2"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/photoTitle"
tools:text="Описание фотографии" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<View
android:id="@+id/shimmerImage"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="#DDDDDD"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/shimmerTitle"
android:layout_width="0dp"
android:layout_height="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:background="#BBBBBB"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/shimmerImage" />
<View
android:id="@+id/shimmerDescription"
android:layout_width="0dp"
android:layout_height="14dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="12dp"
android:background="#DDDDDD"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/shimmerTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/> <background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 1022 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

+3 -3
View File
@@ -22,7 +22,7 @@
tools:layout="@layout/fragment_photos"> tools:layout="@layout/fragment_photos">
<argument <argument
android:name="album" android:name="album"
app:argType="com.example.gallery.data.model.Album" /> app:argType="com.example.gallery.models.Album" />
<action <action
android:id="@+id/action_photosFragment_to_photoDetailFragment" android:id="@+id/action_photosFragment_to_photoDetailFragment"
app:destination="@id/photoDetailFragment" /> app:destination="@id/photoDetailFragment" />
@@ -30,11 +30,11 @@
<fragment <fragment
android:id="@+id/photoDetailFragment" android:id="@+id/photoDetailFragment"
android:name="com.example.gallery.ui.photo_detail.PhotoDetailFragment" android:name="com.example.gallery.ui.photos.PhotoDetailFragment"
android:label="Просмотр фото" android:label="Просмотр фото"
tools:layout="@layout/fragment_photo_detail"> tools:layout="@layout/fragment_photo_detail">
<argument <argument
android:name="photo" android:name="photo"
app:argType="com.example.gallery.data.model.Photo" /> app:argType="com.example.gallery.models.Photo" />
</fragment> </fragment>
</navigation> </navigation>
+11 -5
View File
@@ -1,7 +1,13 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <?xml version="1.0" encoding="utf-8"?>
<!-- Base application theme. --> <resources>
<style name="Base.Theme.GalleryKotlin" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Theme.Gallery" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your dark theme here. --> <item name="colorPrimary">@color/colorPrimary</item>
<!-- <item name="colorPrimary">@color/my_dark_primary</item> --> <item name="colorPrimaryDark">@color/black</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:statusBarColor">@color/black</item>
<item name="android:colorBackground">#121212</item>
<item name="android:textColorPrimary">@color/white</item>
<item name="android:textColorSecondary">#B3FFFFFF</item>
<item name="android:windowActivityTransitions">true</item>
</style> </style>
</resources> </resources>
+7 -2
View File
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="black">#FF000000</color> <color name="colorPrimary">#3F51B5</color>
<color name="white">#FFFFFFFF</color> <color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
<color name="lightGray">#F5F5F5</color>
</resources> </resources>
+14 -6
View File
@@ -1,9 +1,17 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <?xml version="1.0" encoding="utf-8"?>
<!-- Base application theme. --> <resources>
<style name="Base.Theme.GalleryKotlin" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Theme.Gallery" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Customize your light theme here. --> <item name="colorPrimary">@color/colorPrimary</item>
<!-- <item name="colorPrimary">@color/my_light_primary</item> --> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:statusBarColor">@color/colorPrimaryDark</item>
<item name="android:colorBackground">@color/lightGray</item>
<item name="android:textColorPrimary">@color/black</item>
<item name="android:textColorSecondary">#616161</item>
<item name="android:windowActivityTransitions">true</item>
</style> </style>
<style name="Theme.GalleryKotlin" parent="Base.Theme.GalleryKotlin" /> <style name="SplashTheme" parent="Theme.Gallery">
<item name="android:windowBackground">@drawable/splash_background</item>
</style>
</resources> </resources>