From b6452bbb3c447ebca11f8b665534f6254a5ea470 Mon Sep 17 00:00:00 2001 From: Raliiinz Date: Fri, 13 Jun 2025 21:00:33 +0300 Subject: [PATCH 1/3] first --- app/build.gradle.kts | 1 + .../androiddevelopment2/nav/NavMainImpl.kt | 4 + app/src/main/res/navigation/graph_global.xml | 5 + .../androiddevelopment2/navigation/NavMain.kt | 2 + feature/customview/.gitignore | 1 + feature/customview/build.gradle.kts | 64 ++ feature/customview/consumer-rules.pro | 0 feature/customview/proguard-rules.pro | 21 + .../customview/ExampleInstrumentedTest.kt | 24 + .../customview/src/main/AndroidManifest.xml | 4 + .../customview/PieChartFragment.kt | 156 ++++ .../customview/PieChartView.kt | 689 ++++++++++++++++++ .../src/main/res/drawable/button_selector.xml | 16 + .../res/drawable/clear_button_selector.xml | 15 + .../main/res/layout/fragment_pie_chart.xml | 117 +++ .../src/main/res/navigation/nav_custom.xml | 13 + .../customview/src/main/res/values/arrays.xml | 10 + .../customview/src/main/res/values/attrs.xml | 7 + .../customview/src/main/res/values/attrs2.xml | 10 + .../customview/ExampleUnitTest.kt | 17 + .../search/SearchFragment.kt | 7 + .../search/SearchViewModel.kt | 4 + .../src/main/res/layout/fragment_recipes.xml | 12 + .../search/src/main/res/values/strings.xml | 4 + settings.gradle.kts | 1 + 25 files changed, 1204 insertions(+) create mode 100644 feature/customview/.gitignore create mode 100644 feature/customview/build.gradle.kts create mode 100644 feature/customview/consumer-rules.pro create mode 100644 feature/customview/proguard-rules.pro create mode 100644 feature/customview/src/androidTest/java/com/example/androiddevelopment2/customview/ExampleInstrumentedTest.kt create mode 100644 feature/customview/src/main/AndroidManifest.xml create mode 100644 feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartFragment.kt create mode 100644 feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartView.kt create mode 100644 feature/customview/src/main/res/drawable/button_selector.xml create mode 100644 feature/customview/src/main/res/drawable/clear_button_selector.xml create mode 100644 feature/customview/src/main/res/layout/fragment_pie_chart.xml create mode 100644 feature/customview/src/main/res/navigation/nav_custom.xml create mode 100644 feature/customview/src/main/res/values/arrays.xml create mode 100644 feature/customview/src/main/res/values/attrs.xml create mode 100644 feature/customview/src/main/res/values/attrs2.xml create mode 100644 feature/customview/src/test/java/com/example/androiddevelopment2/customview/ExampleUnitTest.kt create mode 100644 feature/search/src/main/res/values/strings.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b809120..70f0da1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(project(path = ":feature:authorization")) implementation(project(path = ":feature:registration")) implementation(project(path = ":feature:graph")) + implementation(project(path = ":feature:customview")) implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) diff --git a/app/src/main/java/com/example/androiddevelopment2/nav/NavMainImpl.kt b/app/src/main/java/com/example/androiddevelopment2/nav/NavMainImpl.kt index 202b5d1..759efab 100644 --- a/app/src/main/java/com/example/androiddevelopment2/nav/NavMainImpl.kt +++ b/app/src/main/java/com/example/androiddevelopment2/nav/NavMainImpl.kt @@ -33,4 +33,8 @@ class NavMainImpl @Inject constructor( override fun goToGraphPage() { navigatorDelegate.navigate(action = R.id.action_global_graph) } + + override fun goToCustomPage() { + navigatorDelegate.navigate(action = R.id.action_global_custom) + } } diff --git a/app/src/main/res/navigation/graph_global.xml b/app/src/main/res/navigation/graph_global.xml index b36ff83..46d75b5 100644 --- a/app/src/main/res/navigation/graph_global.xml +++ b/app/src/main/res/navigation/graph_global.xml @@ -29,4 +29,9 @@ android:id="@+id/action_global_graph" app:destination="@id/nav_graph"/> + + + \ No newline at end of file diff --git a/core/navigation/src/main/java/com/example/androiddevelopment2/navigation/NavMain.kt b/core/navigation/src/main/java/com/example/androiddevelopment2/navigation/NavMain.kt index 1288738..0dc66e8 100644 --- a/core/navigation/src/main/java/com/example/androiddevelopment2/navigation/NavMain.kt +++ b/core/navigation/src/main/java/com/example/androiddevelopment2/navigation/NavMain.kt @@ -9,4 +9,6 @@ interface NavMain { fun goToDetailsPage(recipeId: Int) fun goToGraphPage() + + fun goToCustomPage() } \ No newline at end of file diff --git a/feature/customview/.gitignore b/feature/customview/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/feature/customview/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/customview/build.gradle.kts b/feature/customview/build.gradle.kts new file mode 100644 index 0000000..28662dd --- /dev/null +++ b/feature/customview/build.gradle.kts @@ -0,0 +1,64 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) + id("androidx.navigation.safeargs.kotlin") +} + +android { + namespace = "com.example.androiddevelopment2.customview" + compileSdk = libs.versions.compileSdk.get().toInt() + + defaultConfig { + minSdk = libs.versions.minSdk.get().toInt() + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + viewBinding = true + } +} + +dependencies { + + implementation(project(path = ":core:base")) + implementation(project(path = ":core:domain")) + implementation(project(path = ":core:utils")) + implementation(project(path = ":core:navigation")) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + + // Hilt + implementation(libs.hilt) + ksp(libs.hilt.compiler) + + // VBPD + implementation(libs.viewbindingpropertydelegate.noreflection) + + implementation(libs.navigation.fragment) + implementation(libs.navigation.ui) +} \ No newline at end of file diff --git a/feature/customview/consumer-rules.pro b/feature/customview/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/feature/customview/proguard-rules.pro b/feature/customview/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/feature/customview/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/feature/customview/src/androidTest/java/com/example/androiddevelopment2/customview/ExampleInstrumentedTest.kt b/feature/customview/src/androidTest/java/com/example/androiddevelopment2/customview/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..16569de --- /dev/null +++ b/feature/customview/src/androidTest/java/com/example/androiddevelopment2/customview/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.androiddevelopment2.customview + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.androiddevelopment2.customview.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/feature/customview/src/main/AndroidManifest.xml b/feature/customview/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/feature/customview/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartFragment.kt b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartFragment.kt new file mode 100644 index 0000000..629ef5a --- /dev/null +++ b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartFragment.kt @@ -0,0 +1,156 @@ +package com.example.androiddevelopment2.customview + +import android.graphics.Color +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.TextView +import android.widget.Toast +import androidx.fragment.app.Fragment +import by.kirich1409.viewbindingdelegate.viewBinding +import com.example.androiddevelopment2.customview.databinding.FragmentPieChartBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class PieChartFragment : Fragment(R.layout.fragment_pie_chart) { + private val viewBinding: FragmentPieChartBinding by viewBinding(FragmentPieChartBinding::bind) + + + private lateinit var pieChartView: PieChartView + private lateinit var statusText: TextView + private lateinit var btn3Sectors: Button + private lateinit var btn4Sectors: Button + private lateinit var btn5Sectors: Button + private lateinit var btn6Sectors: Button + private lateinit var btnClearSelection: Button + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + initViews() + setupClickListeners() + setupPieChart() + +// val colors = listOf( +// Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW, +// Color.MAGENTA, Color.CYAN, Color.GRAY, Color.DKGRAY +// ) +// +// viewBinding.pieChartView.apply { +// setSectorCount(5) +// setSectorColors(colors) +// } + } + + private fun initViews() { + + pieChartView = viewBinding.pieChartView + statusText = viewBinding.statusText + btn3Sectors = viewBinding.btn3Sectors + btn4Sectors = viewBinding.btn4Sectors + btn5Sectors = viewBinding.btn5Sectors + btn6Sectors = viewBinding.btn6Sectors + btnClearSelection = viewBinding.btnClearSelection + } + + private fun setupClickListeners() { + btn3Sectors.setOnClickListener { + setupSectors(3) + } + + btn4Sectors.setOnClickListener { + setupSectors(4) + } + + btn5Sectors.setOnClickListener { + setupSectors(5) + } + + btn6Sectors.setOnClickListener { + setupSectors(6) + } + + btnClearSelection.setOnClickListener { + pieChartView.clearSelection() + updateStatusText() + } + } + + private fun setupPieChart() { + // Set initial configuration + pieChartView.setSectorCount(4) + + // Set custom colors + val colors = listOf( + Color.parseColor("#FF6B6B"), // Red + Color.parseColor("#4ECDC4"), // Teal + Color.parseColor("#45B7D1"), // Blue + Color.parseColor("#FFA726") // Orange + ) + pieChartView.setSectorColors(colors) + + // Listen for selection changes + setupSelectionListener() + updateStatusText() + } + + private fun setupSelectionListener() { + // Since we don't have a built-in selection listener, we'll check periodically + // In a real implementation, you might want to add a proper callback mechanism + pieChartView.setOnTouchListener { _, _ -> + // Post delayed to ensure the touch handling is complete + pieChartView.post { + updateStatusText() + } + false // Let the view handle the touch event + } + } + + private fun setupSectors(count: Int) { + pieChartView.setSectorCount(count) + + val colors = when (count) { + 3 -> listOf( + Color.parseColor("#FF6B6B"), // Red + Color.parseColor("#4ECDC4"), // Teal + Color.parseColor("#FFA726") // Orange + ) + 4 -> listOf( + Color.parseColor("#FF6B6B"), // Red + Color.parseColor("#4ECDC4"), // Teal + Color.parseColor("#45B7D1"), // Blue + Color.parseColor("#FFA726") // Orange + ) + 5 -> listOf( + Color.parseColor("#FF6B6B"), // Red + Color.parseColor("#4ECDC4"), // Teal + Color.parseColor("#45B7D1"), // Blue + Color.parseColor("#FFA726"), // Orange + Color.parseColor("#66BB6A") // Green + ) + 6 -> listOf( + Color.parseColor("#FF6B6B"), // Red + Color.parseColor("#4ECDC4"), // Teal + Color.parseColor("#45B7D1"), // Blue + Color.parseColor("#FFA726"), // Orange + Color.parseColor("#66BB6A"), // Green + Color.parseColor("#AB47BC") // Purple + ) + else -> listOf(Color.GRAY) + } + + pieChartView.setSectorColors(colors) + updateStatusText() + + Toast.makeText(requireContext(), "Switched to $count sectors", Toast.LENGTH_SHORT).show() + } + + private fun updateStatusText() { + val selectedSector = pieChartView.getSelectedSector() + statusText.text = if (selectedSector >= 0) { + "Selected sector: ${selectedSector + 1}" + } else { + "No sector selected" + } + } +} \ No newline at end of file diff --git a/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartView.kt b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartView.kt new file mode 100644 index 0000000..38bde24 --- /dev/null +++ b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartView.kt @@ -0,0 +1,689 @@ +package com.example.androiddevelopment2.customview + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import androidx.core.content.withStyledAttributes +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.sin +import kotlin.math.sqrt + + +//class PieChartView @JvmOverloads constructor( +// context: Context, +// attrs: AttributeSet? = null, +// defStyleAttr: Int = 0 +//) : View(context, attrs, defStyleAttr) { +// +// private var sectorCount = 3 +// private var sectorColors: List = listOf(Color.RED, Color.YELLOW, Color.BLUE) +// private var selectedIndex: Int = -1 +// private val paint = Paint(Paint.ANTI_ALIAS_FLAG) +// +// private val path = Path() +// private val centerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { +// color = Color.WHITE +// } +// private val sectorPath = Path() +// +// init { +// // Optional: parse attributes here if needed +// context.theme.obtainStyledAttributes( +// attrs, +// R.styleable.CustomView, +// 0, 0 +// ).apply { +// try { +// // parse custom attributes if needed +// } finally { +// recycle() +// } +// } +// +// setOnTouchListener { _, event -> +// if (event.action == MotionEvent.ACTION_DOWN) { +// handleTouch(event.x, event.y) +// return@setOnTouchListener true +// } +// false +// } +// } +// +//// override fun onDraw(canvas: Canvas) { +//// super.onDraw(canvas) +//// +//// val centerX = width / 2f +//// val centerY = height / 2f +//// val outerRadius = min(centerX, centerY) * 0.9f +//// val thickness = outerRadius * 0.4f // делаем толще +//// val innerRadius = outerRadius - thickness +//// +//// val outerRect = RectF(centerX - outerRadius, centerY - outerRadius, centerX + outerRadius, centerY + outerRadius) +//// val innerRect = RectF(centerX - innerRadius, centerY - innerRadius, centerX + innerRadius, centerY + innerRadius) +//// +//// val sweepAngle = 360f / sectorCount +//// +//// for (i in 0 until sectorCount) { +//// val path = Path() +//// val color = getSectorColor(i) +//// paint.color = color +//// +//// val startAngle = 270f + i * sweepAngle +//// +//// // Внешняя дуга +//// path.arcTo(outerRect, startAngle, sweepAngle) +//// +//// // Закруглённый конец (внешний радиус) +//// val endAngleRad = Math.toRadians((startAngle + sweepAngle).toDouble()) +//// val endX = centerX + outerRadius * cos(endAngleRad).toFloat() +//// val endY = centerY + outerRadius * sin(endAngleRad).toFloat() +//// path.addCircle(endX, endY, thickness / 2f, Path.Direction.CW) +//// +//// // Внутренняя дуга (обратно) +//// path.arcTo(innerRect, startAngle + sweepAngle, -sweepAngle) +//// +//// // Закруглённое начало +//// val startAngleRad = Math.toRadians(startAngle.toDouble()) +//// val startX = centerX + outerRadius * cos(startAngleRad).toFloat() +//// val startY = centerY + outerRadius * sin(startAngleRad).toFloat() +//// path.addCircle(startX, startY, thickness / 2f, Path.Direction.CW) +//// +//// path.close() +//// +//// canvas.drawPath(path, paint) +//// } +//// +//// // Текст в центре +//// paint.color = Color.BLACK +//// paint.textSize = outerRadius * 0.3f +//// paint.textAlign = Paint.Align.CENTER +//// canvas.drawText(sectorCount.toString(), centerX, centerY + paint.textSize / 3, paint) +//// } +// +// +// override fun onDraw(canvas: Canvas) { +// super.onDraw(canvas) +// +// val centerX = width / 2f +// val centerY = height / 2f +// val radius = min(centerX, centerY) * 0.9f +// val strokeWidth = radius * 0.5f +// val innerRadius = radius - strokeWidth +// val capRadius = strokeWidth / 2f +// +// val outerRect = RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius) +// val innerRect = RectF(centerX - innerRadius, centerY - innerRadius, centerX + innerRadius, centerY + innerRadius) +// +// val sweepAngle = 360f / sectorCount +// val overlapAngle = 5f // Угол перекрытия между секторами +// +// for (i in 0 until sectorCount) { +// val startAngle = 270f + i * sweepAngle +// val endAngle = startAngle + sweepAngle +// +// paint.color = getSectorColor(i) +// path.reset() +// +// // 1. Начинаем с острого конца (внутренний радиус) +// val startInnerX = centerX + innerRadius * cos(Math.toRadians(startAngle.toDouble())).toFloat() +// val startInnerY = centerY + innerRadius * sin(Math.toRadians(startAngle.toDouble())).toFloat() +// path.moveTo(startInnerX, startInnerY) +// +// // 2. Линия к внешнему радиусу (острый край) +// val startOuterX = centerX + radius * cos(Math.toRadians(startAngle.toDouble())).toFloat() +// val startOuterY = centerY + radius * sin(Math.toRadians(startAngle.toDouble())).toFloat() +// path.lineTo(startOuterX, startOuterY) +// +// // 3. Внешняя дуга (немного не доходя до конца для скругления) +// path.arcTo(outerRect, startAngle, sweepAngle - overlapAngle, false) +// +// // 4. Скругленный конец +// val capAngle = endAngle - overlapAngle/2 +// val capCenterX = centerX + (radius - capRadius) * cos(Math.toRadians(capAngle.toDouble())).toFloat() +// val capCenterY = centerY + (radius - capRadius) * sin(Math.toRadians(capAngle.toDouble())).toFloat() +// path.arcTo( +// RectF( +// capCenterX - capRadius, +// capCenterY - capRadius, +// capCenterX + capRadius, +// capCenterY + capRadius +// ), +// capAngle - 90, +// 180f, +// false +// ) +// +// // 5. Внутренняя дуга (обратно) +// path.arcTo(innerRect, endAngle, -sweepAngle, false) +// +// path.close() +// canvas.drawPath(path, paint) +// } +// +// // Центральный круг +// canvas.drawCircle(centerX, centerY, innerRadius * 0.8f, centerPaint) +// +// // Текст с количеством секторов +// paint.color = Color.BLACK +// paint.textSize = radius / 3 +// paint.textAlign = Paint.Align.CENTER +// canvas.drawText(sectorCount.toString(), centerX, centerY + paint.textSize / 3, paint) +// } +// +// +// +//// override fun onDraw(canvas: Canvas) { +//// super.onDraw(canvas) +//// +//// val centerX = width / 2f +//// val centerY = height / 2f +//// val radius = min(centerX, centerY) * 0.9f +//// val strokeWidth = radius * 0.5f +//// val innerRadius = radius - strokeWidth +//// val capRadius = strokeWidth / 2f +//// +//// val sweepAngle = 360f / sectorCount +//// +//// for (i in 0 until sectorCount) { +//// val startAngle = 270f + i * sweepAngle +//// val endAngle = startAngle + sweepAngle +//// +//// paint.color = getSectorColor(i) +//// +//// val path = Path() +//// +//// // 1. Начало сектора (острое) +//// val startOuterX = centerX + radius * cos(Math.toRadians(startAngle.toDouble())).toFloat() +//// val startOuterY = centerY + radius * sin(Math.toRadians(startAngle.toDouble())).toFloat() +//// path.moveTo(startOuterX, startOuterY) +//// +//// // 2. Внешняя дуга +//// path.arcTo( +//// RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius), +//// startAngle, +//// sweepAngle - 15f, // чуть меньше полного угла, чтобы оставить место для скругления +//// false +//// ) +//// +//// // 3. Скруглённый конец (cap) +//// val capAngle = endAngle - 7.5f // центр скругления +//// val capCenterX = centerX + (innerRadius + capRadius) * cos(Math.toRadians(capAngle.toDouble())).toFloat() +//// val capCenterY = centerY + (innerRadius + capRadius) * sin(Math.toRadians(capAngle.toDouble())).toFloat() +//// path.arcTo( +//// RectF( +//// capCenterX - capRadius, +//// capCenterY - capRadius, +//// capCenterX + capRadius, +//// capCenterY + capRadius +//// ), +//// capAngle - 90f, +//// 180f, +//// false +//// ) +//// +//// // 4. Внутренняя дуга (обратно) +//// path.arcTo( +//// RectF(centerX - innerRadius, centerY - innerRadius, centerX + innerRadius, centerY + innerRadius), +//// endAngle, +//// -(sweepAngle - 15f), +//// false +//// ) +//// +//// path.close() +//// canvas.drawPath(path, paint) +//// } +//// +//// // Центр круга: число +//// paint.color = Color.BLACK +//// paint.textSize = radius / 3 +//// paint.textAlign = Paint.Align.CENTER +//// canvas.drawText(sectorCount.toString(), centerX, centerY + paint.textSize / 3, paint) +//// } +// +// +//// override fun onDraw(canvas: Canvas) { +//// super.onDraw(canvas) +//// +//// val centerX = width / 2f +//// val centerY = height / 2f +//// val radius = min(centerX, centerY) * 0.9f +//// val strokeWidth = radius * 0.5f +//// val innerRadius = radius - strokeWidth +//// +//// val sweepAngle = 360f / sectorCount +//// +//// for (i in 0 until sectorCount) { +//// val startAngle = 270f + i * sweepAngle +//// val endAngle = startAngle + sweepAngle +//// +//// paint.color = getSectorColor(i) +//// +//// val path = Path() +//// +//// // Точка старта на внешнем радиусе +//// val startOuterX = centerX + radius * cos(Math.toRadians(startAngle.toDouble())).toFloat() +//// val startOuterY = centerY + radius * sin(Math.toRadians(startAngle.toDouble())).toFloat() +//// +//// // Точка конца на внешнем радиусе +//// val endOuterX = centerX + radius * cos(Math.toRadians(endAngle.toDouble())).toFloat() +//// val endOuterY = centerY + radius * sin(Math.toRadians(endAngle.toDouble())).toFloat() +//// +//// // Точка конца на внутреннем радиусе +//// val endInnerX = centerX + innerRadius * cos(Math.toRadians(endAngle.toDouble())).toFloat() +//// val endInnerY = centerY + innerRadius * sin(Math.toRadians(endAngle.toDouble())).toFloat() +//// +//// // Точка старта на внутреннем радиусе +//// val startInnerX = centerX + innerRadius * cos(Math.toRadians(startAngle.toDouble())).toFloat() +//// val startInnerY = centerY + innerRadius * sin(Math.toRadians(startAngle.toDouble())).toFloat() +//// +//// // 1. Дуга по внешнему радиусу +//// path.moveTo(startOuterX, startOuterY) +//// path.arcTo( +//// RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius), +//// startAngle, +//// sweepAngle, +//// false +//// ) +//// +//// // 2. Скругленный конец (дуга радиусом = половина толщины сектора) +//// val capRadius = (radius - innerRadius) / 2f +//// val capCenterX = centerX + (innerRadius + capRadius) * cos(Math.toRadians(endAngle.toDouble())).toFloat() +//// val capCenterY = centerY + (innerRadius + capRadius) * sin(Math.toRadians(endAngle.toDouble())).toFloat() +//// path.arcTo( +//// RectF( +//// capCenterX - capRadius, +//// capCenterY - capRadius, +//// capCenterX + capRadius, +//// capCenterY + capRadius +//// ), +//// endAngle - 90f, +//// 180f, +//// false +//// ) +//// +//// // 3. Дуга по внутреннему радиусу (обратно) +//// path.arcTo( +//// RectF(centerX - innerRadius, centerY - innerRadius, centerX + innerRadius, centerY + innerRadius), +//// endAngle, +//// -sweepAngle, +//// false +//// ) +//// +//// // 4. Замыкаем путь +//// path.close() +//// +//// canvas.drawPath(path, paint) +//// } +//// +//// // Центр круга: число +//// paint.color = Color.BLACK +//// paint.textSize = radius / 3 +//// paint.textAlign = Paint.Align.CENTER +//// canvas.drawText(sectorCount.toString(), centerX, centerY + paint.textSize / 3, paint) +//// } +// +// +//// override fun onDraw(canvas: Canvas) { +//// super.onDraw(canvas) +//// +//// val centerX = width / 2f +//// val centerY = height / 2f +//// val radius = min(centerX, centerY) * 0.9f +//// val strokeWidth = radius * 0.5f +//// val innerRadius = radius - strokeWidth +//// +//// +//// val outerRect = RectF(centerX - radius, centerY - radius, centerX + radius, centerY + radius) +//// val innerRect = RectF(centerX - innerRadius, centerY - innerRadius, centerX + innerRadius, centerY + innerRadius) +//// +//// val sweepAngle = 360f / sectorCount +//// +//// for (i in 0 until sectorCount) { +//// val startAngle = 270f + i * sweepAngle +//// paint.color = getSectorColor(i) +//// +//// val path = Path() +//// +//// // Внешний круг +////// path.arcTo(outerRect, startAngle, sweepAngle) +////// +////// // Закругление на конце +////// path.arcTo(innerRect, startAngle + sweepAngle, -sweepAngle) +////// path.close() +////// +////// canvas.drawPath(path, paint) +////// } +//// +//// // Центр круга: число +//// paint.color = Color.BLACK +//// paint.textSize = radius / 3 +//// paint.textAlign = Paint.Align.CENTER +//// canvas.drawText(sectorCount.toString(), centerX, centerY + paint.textSize / 3, paint) +//// } +// +// +// +// private fun getSectorColor(index: Int): Int { +// val color = sectorColors[index % sectorColors.size] +// return if (index == selectedIndex) lightenColor(color, 0.3f) else color +// } +// +//// private fun handleTouch(x: Float, y: Float) { +//// val dx = x - width / 2f +//// val dy = y - height / 2f +//// val distance = sqrt(dx * dx + dy * dy) +//// val outerRadius = min(width, height) * 0.9f / 2f +//// val innerRadius = outerRadius - outerRadius * 0.4f +//// +//// if (distance !in innerRadius..outerRadius) { +//// selectedIndex = -1 +//// } else { +//// var angle = Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat() +//// angle = (angle + 360 + 90) % 360 +//// selectedIndex = (angle / (360f / sectorCount)).toInt() +//// } +//// +//// invalidate() +//// } +// +// private fun handleTouch(x: Float, y: Float) { +// val dx = x - width / 2f +// val dy = y - height / 2f +// val distance = sqrt(dx * dx + dy * dy) +// val outerRadius = min(width, height) * 0.9f / 2f +// val innerRadius = outerRadius * 0.5f +// +// if (distance !in innerRadius..outerRadius) { +// selectedIndex = -1 +// } else { +// var angle = Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat() +// angle = (angle + 360 + 90) % 360 +// selectedIndex = (angle / (360f / sectorCount)).toInt() +// } +// +// invalidate() +// } +// +// +// +// private fun lightenColor(color: Int, factor: Float): Int { +// val r = Color.red(color) +// val g = Color.green(color) +// val b = Color.blue(color) +// return Color.rgb( +// (r + (255 - r) * factor).toInt(), +// (g + (255 - g) * factor).toInt(), +// (b + (255 - b) * factor).toInt() +// ) +// } +// +// fun setSectorCount(count: Int) { +// sectorCount = count +// invalidate() +// } +// +// fun setSectorColors(colors: List) { +// require(colors.distinct().size >= min(colors.size, sectorCount)) { +// "Цвета секторов должны быть разными и уникальными" +// } +// sectorColors = colors +// invalidate() +// } +//} + + + + + +class PieChartView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var sectorCount = 3 + private var sectorColors: List = listOf( + Color.parseColor("#FF6B6B"), // Red + Color.parseColor("#4ECDC4"), // Teal + Color.parseColor("#FFA726") // Orange + ) + private var selectedIndex: Int = -1 + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG) + + init { + setupPaints() + } + + private fun setupPaints() { + paint.style = Paint.Style.FILL + + textPaint.apply { + textAlign = Paint.Align.CENTER + typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) + color = Color.parseColor("#333333") + } + + shadowPaint.apply { + color = Color.parseColor("#20000000") + maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL) + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (sectorCount <= 0) return + + val centerX = width / 2f + val centerY = height / 2f + val radius = min(centerX, centerY) * 0.8f + val strokeWidth = radius * 0.6f + val innerRadius = radius - strokeWidth + + // Draw shadow + drawShadow(canvas, centerX, centerY, radius + 4f, innerRadius - 4f) + + // Draw sectors (in reverse order so overlapping works correctly) + for (i in sectorCount - 1 downTo 0) { + drawSector(canvas, centerX, centerY, radius, innerRadius, strokeWidth, i) + } + + // Draw center circle and text + drawCenter(canvas, centerX, centerY, innerRadius) + } + + private fun drawShadow(canvas: Canvas, centerX: Float, centerY: Float, outerRadius: Float, innerRadius: Float) { + val shadowPath = Path().apply { + addCircle(centerX, centerY, outerRadius, Path.Direction.CW) + addCircle(centerX, centerY, innerRadius, Path.Direction.CCW) + fillType = Path.FillType.EVEN_ODD + } + canvas.drawPath(shadowPath, shadowPaint) + } + + private fun drawSector( + canvas: Canvas, + centerX: Float, + centerY: Float, + outerRadius: Float, + innerRadius: Float, + strokeWidth: Float, + sectorIndex: Int + ) { + val sweepAngle = 360f / sectorCount + val startAngle = 270f + sectorIndex * sweepAngle // Начало с 9 часов (270°) + val endAngle = startAngle + sweepAngle + + // Устанавливаем цвет для основного сектора + paint.color = getSectorColor(sectorIndex) + + val path = Path() + + val startAngleRad = Math.toRadians(startAngle.toDouble()).toFloat() + val endAngleRad = Math.toRadians(endAngle.toDouble()).toFloat() + + val startOuterX = centerX + outerRadius * cos(startAngleRad) + val startOuterY = centerY + outerRadius * sin(startAngleRad) + val startInnerX = centerX + innerRadius * cos(startAngleRad) + val startInnerY = centerY + innerRadius * sin(startAngleRad) + val endOuterX = centerX + outerRadius * cos(endAngleRad) + val endOuterY = centerY + outerRadius * sin(endAngleRad) + val endInnerX = centerX + innerRadius * cos(endAngleRad) + val endInnerY = centerY + innerRadius * sin(endAngleRad) + + // 1. Построение основного контура сектора (без закругления через arcTo на конце) + path.moveTo(startInnerX, startInnerY) + path.lineTo(startOuterX, startOuterY) + path.arcTo( + RectF(centerX - outerRadius, centerY - outerRadius, centerX + outerRadius, centerY + outerRadius), + startAngle, + sweepAngle, + false + ) + // Линия от внешней конечной точки к внутренней конечной точке (прямая, не дуга) + path.lineTo(endInnerX, endInnerY) + path.arcTo( + RectF(centerX - innerRadius, centerY - innerRadius, centerX + innerRadius, centerY + innerRadius), + endAngle, + -sweepAngle, // Обратное направление + false + ) + path.close() + canvas.drawPath(path, paint) + + // --- 2. Отдельное рисование закругленного конца --- + // Этот круг будет нарисован поверх основной формы + val capRadius = strokeWidth / 2f + + // Центр круга закругления должен быть в середине толщины линии + // на конце сектора. + val capMidRadius = innerRadius + capRadius + + val capCenterX = centerX + capMidRadius * cos(endAngleRad) + val capCenterY = centerY + capMidRadius * sin(endAngleRad) + + // Используем тот же paint, который уже настроен на цвет сектора + // Стиль paint уже установлен на FILL + canvas.drawCircle(capCenterX, capCenterY, capRadius, paint) + } + + private fun drawCenter(canvas: Canvas, centerX: Float, centerY: Float, innerRadius: Float) { + // Draw center circle background + paint.color = Color.WHITE + canvas.drawCircle(centerX, centerY, innerRadius, paint) + + // Draw center circle border + paint.apply { + color = Color.parseColor("#E0E0E0") + style = Paint.Style.STROKE + strokeWidth = 2f + } + canvas.drawCircle(centerX, centerY, innerRadius, paint) + paint.style = Paint.Style.FILL + + // Draw center text + textPaint.textSize = innerRadius * 0.6f + val textY = centerY + textPaint.textSize / 3f + canvas.drawText(sectorCount.toString(), centerX, textY, textPaint) + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + handleTouch(event.x, event.y) + return true + } + } + return super.onTouchEvent(event) + } + + private fun handleTouch(x: Float, y: Float) { + val centerX = width / 2f + val centerY = height / 2f + val radius = min(centerX, centerY) * 0.8f + val strokeWidth = radius * 0.6f + val innerRadius = radius - strokeWidth // Синхронизируем с onDraw +// val innerRadius = radius * 0.6f + + val dx = x - centerX + val dy = y - centerY + val distance = sqrt(dx * dx + dy * dy) + + // Check if touch is within the ring area + if (distance in innerRadius..radius) { + // Calculate angle from center + var angle = Math.toDegrees(atan2(dy.toDouble(), dx.toDouble())).toFloat() + // Adjust angle to start from 9 o'clock (270°) and go clockwise + angle = (angle + 360 + 90) % 360 + + // Determine which sector was touched + val sectorAngle = 360f / sectorCount + selectedIndex = (angle / sectorAngle).toInt() + + // Ensure selectedIndex is within bounds + if (selectedIndex >= sectorCount) { + selectedIndex = sectorCount - 1 + } + } else { + // Touch is outside the ring, clear selection + selectedIndex = -1 + } + + invalidate() + } + + private fun getSectorColor(index: Int): Int { + val color = sectorColors[index % sectorColors.size] + return if (index == selectedIndex) { + lightenColor(color, 0.3f) + } else { + color + } + } + + private fun lightenColor(color: Int, factor: Float): Int { + val r = Color.red(color) + val g = Color.green(color) + val b = Color.blue(color) + return Color.rgb( + (r + (255 - r) * factor).toInt(), + (g + (255 - g) * factor).toInt(), + (b + (255 - b) * factor).toInt() + ) + } + + // Public API methods + fun setSectorCount(count: Int) { + require(count > 0) { "Sector count must be positive" } + sectorCount = count + selectedIndex = -1 // Reset selection + invalidate() + } + + fun setSectorColors(colors: List) { + require(colors.isNotEmpty()) { "Colors list cannot be empty" } + require(colors.size == colors.distinct().size) { "All colors must be unique" } + sectorColors = colors + invalidate() + } + + fun getSelectedSector(): Int = selectedIndex + + fun setSelectedSector(index: Int) { + selectedIndex = if (index in 0 until sectorCount) index else -1 + invalidate() + } + + fun clearSelection() { + selectedIndex = -1 + invalidate() + } +} diff --git a/feature/customview/src/main/res/drawable/button_selector.xml b/feature/customview/src/main/res/drawable/button_selector.xml new file mode 100644 index 0000000..029dc32 --- /dev/null +++ b/feature/customview/src/main/res/drawable/button_selector.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/feature/customview/src/main/res/drawable/clear_button_selector.xml b/feature/customview/src/main/res/drawable/clear_button_selector.xml new file mode 100644 index 0000000..9c737e9 --- /dev/null +++ b/feature/customview/src/main/res/drawable/clear_button_selector.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/feature/customview/src/main/res/layout/fragment_pie_chart.xml b/feature/customview/src/main/res/layout/fragment_pie_chart.xml new file mode 100644 index 0000000..ac98d39 --- /dev/null +++ b/feature/customview/src/main/res/layout/fragment_pie_chart.xml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + +