Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.glide)
testImplementation(libs.junit)
testImplementation(libs.kotlin.coroutines.test)
}
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<application
android:allowBackup="true"
android:name=".App"
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/kotlin/ru/otus/cookbook/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package ru.otus.cookbook

import android.os.Bundle
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import ru.otus.cookbook.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {
Expand All @@ -12,5 +14,11 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

onBackPressedDispatcher.addCallback(this) {
if (!findNavController(R.id.fragment_container_view).popBackStack()) {
finish()
}
}
}
}
59 changes: 58 additions & 1 deletion app/src/main/kotlin/ru/otus/cookbook/ui/CookbookFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
import ru.otus.cookbook.R
import ru.otus.cookbook.data.RecipeListItem
import ru.otus.cookbook.databinding.FragmentCookbookBinding

Expand All @@ -34,13 +41,63 @@ class CookbookFragment : Fragment() {
.flowWithLifecycle(viewLifecycleOwner.lifecycle)
.collect(::onRecipeListUpdated)
}
setupSearch()

binding.withBinding {
btnClose.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.close_application))
.setMessage(getString(R.string.are_you_sure_you_want_to_exit))
.setPositiveButton(getString(R.string.yes)) { _, _ ->
val activity = requireActivity()
NotificationManagerCompat.from(activity).cancelAll()
activity.finishAndRemoveTask()
}
.setNegativeButton(getString(R.string.no), null)
.show()
}
}
}

private fun performSearch(query: String) {
// Реализация поиска
Toast.makeText(requireContext(), getString(R.string.query, query), Toast.LENGTH_SHORT).show()
}

private fun setupSearch() = binding.withBinding {

// Обработчик нажатия на иконку
searchIcon.setOnClickListener {
val query = searchInput.text.toString()
performSearch(query)
}

// Обработчик нажатия кнопки "Поиск" на клавиатуре
searchInput.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
val query = searchInput.text.toString()
performSearch(query)
true
} else {
false
}
}
}

private fun setupRecyclerView() = binding.withBinding {
// Setup RecyclerView
val adapter = RecipeAdapter(requireContext()) { recipeId ->
// Handle recipe click, e.g., navigate to RecipeFragment
val action = CookbookFragmentDirections.actionCookbookFragmentToRecipeFragment(recipeId)
findNavController().navigate(action)
}

recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = adapter
}

private fun onRecipeListUpdated(recipeList: List<RecipeListItem>) {
private fun onRecipeListUpdated(recipeList: List<RecipeListItem>) = binding.withBinding {
// Handle recipe list
(recyclerView.adapter as RecipeAdapter).submitList(recipeList)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package ru.otus.cookbook.ui

import android.app.Dialog
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import ru.otus.cookbook.R


class DeleteRecipeDialogFragment : DialogFragment() {

companion object {
const val CONFIRMATION_RESULT = "confirmation_result"
}

private val args: DeleteRecipeDialogFragmentArgs by navArgs()

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val recipeTitle = args.recipeTitle
return MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.delete_recipe_title))
.setMessage(getString(R.string.delete_recipe_message, recipeTitle))
.setPositiveButton(R.string.ok) { _, _ ->
dismiss()
setResult(true)
}
.setNegativeButton(R.string.cancel, { _, _ ->
dismiss()
setResult(false)
}
)
.create()
}

private fun setResult(result: Boolean) {
findNavController().previousBackStackEntry?.savedStateHandle?.set(
CONFIRMATION_RESULT,
result
)
findNavController().popBackStack()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package ru.otus.cookbook.ui

import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PorterDuff
import android.graphics.RectF
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.security.MessageDigest
import kotlin.math.min


class NormalizeAndRoundedCornersTransformation(
private val context: Context, // Контекст для доступа к ресурсам
private val radius: Int, // Радиус скругления в dp
private val margin: Int, // Отступ в dp
private val cornerType: CornerType
) : BitmapTransformation() {

enum class CornerType {
ALL, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, LEFT, RIGHT
}

override fun transform(
pool: BitmapPool,
toTransform: Bitmap,
outWidth: Int,
outHeight: Int
): Bitmap {
val normalized = normalizeImage(toTransform, outWidth, outHeight)
return roundedCorners(pool, normalized, outWidth, outHeight)
}

private fun normalizeImage(source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val sourceWidth = source.width
val sourceHeight = source.height

// Рассчитываем соотношения сторон
val widthRatio = outWidth.toFloat() / sourceWidth
val heightRatio = outHeight.toFloat() / sourceHeight

// Выбираем наименьшее соотношение, чтобы сохранить пропорции
val scaleFactor = min(widthRatio, heightRatio)

// Вычисляем новые размеры
val targetWidth = (sourceWidth * scaleFactor).toInt()
val targetHeight = (sourceHeight * scaleFactor).toInt()

// Масштабируем изображение с учетом пропорций
return Bitmap.createScaledBitmap(source, targetWidth, targetHeight, true)
}

private fun roundedCorners(pool: BitmapPool, source: Bitmap, width: Int, height: Int): Bitmap {
var output = pool[width, height, Bitmap.Config.ARGB_8888]
if (output == null) {
output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
}

val canvas = Canvas(output)
val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.DITHER_FLAG or Paint.FILTER_BITMAP_FLAG)
paint.isAntiAlias = true

// Преобразование радиуса и отступов из dp в пиксели
val density = context.resources.displayMetrics.density
val radiusPx = radius * density
val marginPx = margin * density

// Определяем радиусы углов для каждого CornerType
val radii = when (cornerType) {
CornerType.ALL -> floatArrayOf(
radiusPx, radiusPx, // Top-left
radiusPx, radiusPx, // Top-right
radiusPx, radiusPx, // Bottom-right
radiusPx, radiusPx // Bottom-left
)
CornerType.TOP_LEFT -> floatArrayOf(
radiusPx, radiusPx, // Top-left
0f, 0f, // Top-right
0f, 0f, // Bottom-right
0f, 0f // Bottom-left
)
CornerType.TOP_RIGHT -> floatArrayOf(
0f, 0f, // Top-left
radiusPx, radiusPx, // Top-right
0f, 0f, // Bottom-right
0f, 0f // Bottom-left
)
CornerType.BOTTOM_LEFT -> floatArrayOf(
0f, 0f, // Top-left
0f, 0f, // Top-right
0f, 0f, // Bottom-right
radiusPx, radiusPx // Bottom-left
)
CornerType.BOTTOM_RIGHT -> floatArrayOf(
0f, 0f, // Top-left
0f, 0f, // Top-right
radiusPx, radiusPx, // Bottom-right
0f, 0f // Bottom-left
)
CornerType.LEFT -> floatArrayOf(
radiusPx, radiusPx, // Top-left
0f, 0f, // Top-right
0f, 0f, // Bottom-right
radiusPx, radiusPx // Bottom-left
)
CornerType.RIGHT -> floatArrayOf(
0f, 0f, // Top-left
radiusPx, radiusPx, // Top-right
radiusPx, radiusPx, // Bottom-right
0f, 0f // Bottom-left
)
}

// Создаем путь с учетом радиусов углов и отступов
val path = Path()
path.addRoundRect(
RectF(
marginPx,
marginPx,
width.toFloat() - marginPx,
height.toFloat() - marginPx
),
radii,
Path.Direction.CW
)

// Очищаем холст и применяем клип
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
canvas.clipPath(path)
canvas.drawBitmap(source, null, RectF(0f, 0f, width.toFloat(), height.toFloat()), paint)

return output
}


override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(("normalized_rounded_" + radius + "_" + margin + "_" + cornerType).toByteArray(CHARSET))
}
}
Loading