Created Photos logic and displaying,

also realised saving in cash and getting from cash
This commit is contained in:
lepri4dw 2025-03-15 23:54:11 +06:00
parent 9ec4271c73
commit 2e9df075b8
40 changed files with 781 additions and 192 deletions

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>

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

View File

@ -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
}
}

View File

@ -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) {
Result.failure(e) val cachedAlbums = photoCache.getAlbums()
if (cachedAlbums != null) {
Result.success(cachedAlbums)
} else {
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) {
Result.failure(e) val cachedAlbum = photoCache.getAlbumDetails(albumId)
if (cachedAlbum != null) {
Result.success(cachedAlbum)
} else {
Result.failure(e)
}
} }
} }
} }

View File

@ -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
}
}
}

View File

@ -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() {

View File

@ -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)
}
} }

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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 android:fillColor="#00000000" android:pathData="M9,0L9,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:pathData="M9,0L9,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M19,0L19,108" <path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M59,0L59,108"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:pathData="M29,0L29,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M39,0L39,108" <path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,9L108,9"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:pathData="M49,0L49,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M59,0L59,108" <path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M0,59L108,59"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:pathData="M69,0L69,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M79,0L79,108" <path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M19,29L89,29"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:pathData="M89,0L89,108" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M99,0L99,108" <path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M19,79L89,79"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:pathData="M0,9L108,9" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeWidth="0.8" <path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" /> android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path <path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:fillColor="#00000000" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:pathData="M0,19L108,19" <path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:strokeColor="#33FFFFFF" /> <path android:fillColor="#00000000" android:pathData="M69,19L69,89"
<path android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
android:fillColor="#00000000" <path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:pathData="M0,29L108,29" android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

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>

View File

@ -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>

View File

@ -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>

View File

@ -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

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>

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>

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>

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>