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/base/src/main/res/values/strings.xml b/core/base/src/main/res/values/strings.xml index 3d78c03..d728980 100644 --- a/core/base/src/main/res/values/strings.xml +++ b/core/base/src/main/res/values/strings.xml @@ -72,4 +72,15 @@ Данные получены из кэша Данные получены из API + + Overlapping Pie Chart + Tap on sectors to select them + 3 + 4 + 5 + 6 + Clear Selection + + + \ 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..150ef5d --- /dev/null +++ b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartFragment.kt @@ -0,0 +1,83 @@ +package com.example.androiddevelopment2.customview + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import by.kirich1409.viewbindingdelegate.viewBinding +import com.example.androiddevelopment2.customview.PieChartView.Companion.NO_SELECTION +import com.example.androiddevelopment2.customview.databinding.FragmentPieChartBinding +import com.example.androiddevelopment2.customview.state.PieChartEvent +import com.example.androiddevelopment2.customview.state.PieChartState +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + + +@AndroidEntryPoint +class PieChartFragment : Fragment(R.layout.fragment_pie_chart) { + private val viewBinding: FragmentPieChartBinding by viewBinding(FragmentPieChartBinding::bind) + + private val viewModel: PieChartViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupPieChart() + setupClickListeners() + observeViewModel() + } + + private fun setupPieChart() { + viewBinding.pieChartView.apply { + onSelectionChanged = { index -> + if (index == NO_SELECTION) { + viewModel.reduce(PieChartEvent.OnClearSelection) + } else { + viewModel.reduce(PieChartEvent.OnSectorSelected(index)) + } + } + } + } + + @SuppressLint("ClickableViewAccessibility") + private fun setupClickListeners() { + viewBinding.apply { + root.setOnClickListener { + viewModel.reduce(PieChartEvent.OnClearSelection) + } + btn3Sectors.setOnClickListener { + viewModel.reduce(PieChartEvent.OnSectorCountChanged(3)) + } + btn4Sectors.setOnClickListener { + viewModel.reduce(PieChartEvent.OnSectorCountChanged(4)) + } + btn5Sectors.setOnClickListener { + viewModel.reduce(PieChartEvent.OnSectorCountChanged(5)) + } + btn6Sectors.setOnClickListener { + viewModel.reduce(PieChartEvent.OnSectorCountChanged(6)) + } + btnClearSelection.setOnClickListener { viewModel.reduce(PieChartEvent.OnClearSelection) } + } + } + + private fun observeViewModel() { + viewLifecycleOwner.lifecycleScope.launch { + viewModel.state.collect { state -> + when (state) { + is PieChartState.Initial -> { + } + is PieChartState.Success -> { + viewBinding.pieChartView.updateData( + count = state.sectorCount, + colors = state.colors, + selectedIndex = state.selectedSector + ) + } + } + } + } + } +} 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..e6cc54b --- /dev/null +++ b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartView.kt @@ -0,0 +1,284 @@ +package com.example.androiddevelopment2.customview + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.sin +import kotlin.math.sqrt +import androidx.core.graphics.toColorInt + +class PieChartView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + companion object { + private const val DEFAULT_SECTOR_COUNT = 3 + const val NO_SELECTION = -1 + private const val RADIUS_RATIO = 0.8f + private const val STROKE_WIDTH_RATIO = 0.6f + private const val CENTER_BORDER_WIDTH = 2f + private const val TEXT_SIZE_RATIO = 0.6f + private const val TEXT_VERTICAL_OFFSET = 3f + private const val LIGHTEN_FACTOR = 0.3f + private const val START_ANGLE = 270f + private const val DARK_GRAY_COLOR = "#333333" + private const val LIGHT_GRAY_COLOR = "#E0E0E0" + private const val SHADOW_COLOR = "#20000000" + } + + private var sectorCount = DEFAULT_SECTOR_COUNT + private var sectorColors: List = emptyList() + private var selectedIndex = NO_SELECTION + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + textAlign = Paint.Align.CENTER + typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) + color = DARK_GRAY_COLOR.toColorInt() + } + + private val shadowPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = SHADOW_COLOR.toColorInt() + maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL) + } + + private val centerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = Color.WHITE + } + + private val centerBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + color = LIGHT_GRAY_COLOR.toColorInt() + strokeWidth = CENTER_BORDER_WIDTH + } + + init { + isClickable = true + + attrs?.let { attributeSet -> + val typedArray = context.obtainStyledAttributes( + attributeSet, + R.styleable.PieChartView, + defStyleAttr, + 0 + ) + + try { + val count = + typedArray.getInt(R.styleable.PieChartView_sectorsCount, DEFAULT_SECTOR_COUNT) + val colorsResId = typedArray.getResourceId(R.styleable.PieChartView_sectorColors, 0) + + val colors = if (colorsResId != 0) { + context.resources.getIntArray(colorsResId).toList() + } else { + emptyList() + } + + updateData(count, colors) + } finally { + typedArray.recycle() + } + } + } + + fun updateData(count: Int, colors: List, selectedIndex: Int = NO_SELECTION) { + if (count < 0 || colors.isEmpty()) return + + this.sectorCount = count + this.sectorColors = colors + this.selectedIndex = selectedIndex.coerceIn(NO_SELECTION, count - 1) + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (sectorCount <= 0 || sectorColors.isEmpty()) return + + val centerX = width / 2f + val centerY = height / 2f + val radius = min(centerX, centerY) * RADIUS_RATIO + val strokeWidth = radius * STROKE_WIDTH_RATIO + val innerRadius = radius - strokeWidth + + drawShadow(canvas, centerX, centerY, radius + 4f, innerRadius - 4f) + + for (i in sectorCount - 1 downTo 0) { + drawSectorBody(canvas, centerX, centerY, radius, innerRadius, strokeWidth, i) + } + + for (i in sectorCount - 1 downTo 0) { + drawSectorCap(canvas, centerX, centerY, radius, innerRadius, strokeWidth, i) + } + + drawCenter(canvas, centerX, centerY, innerRadius) + } + + private fun drawSectorBody( + canvas: Canvas, + centerX: Float, + centerY: Float, + outerRadius: Float, + innerRadius: Float, + strokeWidth: Float, + sectorIndex: Int + ) { + val sweepAngle = 360f / sectorCount + val startAngle = START_ANGLE + sectorIndex * sweepAngle + 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 endInnerX = centerX + innerRadius * cos(endAngleRad) + val endInnerY = centerY + innerRadius * sin(endAngleRad) + + 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) + } + + private fun drawSectorCap( + canvas: Canvas, + centerX: Float, + centerY: Float, + outerRadius: Float, + innerRadius: Float, + strokeWidth: Float, + sectorIndex: Int + ) { + val sweepAngle = 360f / sectorCount + val startAngle = 270f + sectorIndex * sweepAngle + val endAngle = startAngle + sweepAngle + + val endAngleRad = Math.toRadians(endAngle.toDouble()).toFloat() + + val capRadius = strokeWidth / 2f + val capMidRadius = innerRadius + capRadius + + val capCenterX = centerX + capMidRadius * cos(endAngleRad) + val capCenterY = centerY + capMidRadius * sin(endAngleRad) + + paint.color = getSectorColor(sectorIndex) + canvas.drawCircle(capCenterX, capCenterY, capRadius, paint) + } + + private fun drawShadow( + canvas: Canvas, + centerX: Float, + centerY: Float, + outerRadius: Float, + innerRadius: Float + ) { + Path().apply { + addCircle(centerX, centerY, outerRadius, Path.Direction.CW) + addCircle(centerX, centerY, innerRadius, Path.Direction.CCW) + fillType = Path.FillType.EVEN_ODD + canvas.drawPath(this, shadowPaint) + } + } + + private fun drawCenter(canvas: Canvas, centerX: Float, centerY: Float, innerRadius: Float) { + canvas.drawCircle(centerX, centerY, innerRadius, centerPaint) + canvas.drawCircle(centerX, centerY, innerRadius, centerBorderPaint) + + textPaint.textSize = innerRadius * TEXT_SIZE_RATIO + canvas.drawText( + sectorCount.toString(), + centerX, + centerY + textPaint.textSize / TEXT_VERTICAL_OFFSET, + textPaint + ) + } + + var onSelectionChanged: ((Int) -> Unit)? = null + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + val touchedSector = getTouchedSector(event.x, event.y) + selectedIndex = touchedSector + invalidate() + onSelectionChanged?.invoke(touchedSector) + return true + } + } + return super.onTouchEvent(event) + } + + private fun getTouchedSector(x: Float, y: Float): Int { + val centerX = width / 2f + val centerY = height / 2f + val radius = min(centerX, centerY) * RADIUS_RATIO + val strokeWidth = radius * STROKE_WIDTH_RATIO + val innerRadius = radius - strokeWidth + + val distance = sqrt((x - centerX).pow(2) + (y - centerY).pow(2)) + + if (distance < innerRadius || distance > radius) { + return NO_SELECTION + } + + var angle = Math.toDegrees(atan2(y - centerY, x - centerX).toDouble()).toFloat() + angle = (angle + 360 + 90) % 360 + return (angle / (360f / sectorCount)).toInt().coerceIn(0, sectorCount - 1) + } + + private fun getSectorColor(index: Int): Int { + val color = sectorColors[index % sectorColors.size] + return if (index == selectedIndex) lightenColor(color) else color + } + + private fun lightenColor(color: Int): Int { + return Color.rgb( + (Color.red(color) + (255 - Color.red(color)) * LIGHTEN_FACTOR).toInt(), + (Color.green(color) + (255 - Color.green(color)) * LIGHTEN_FACTOR).toInt(), + (Color.blue(color) + (255 - Color.blue(color)) * LIGHTEN_FACTOR).toInt() + ) + } +} \ No newline at end of file diff --git a/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartViewModel.kt b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartViewModel.kt new file mode 100644 index 0000000..3018fdc --- /dev/null +++ b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/PieChartViewModel.kt @@ -0,0 +1,72 @@ +package com.example.androiddevelopment2.customview + +import androidx.lifecycle.ViewModel +import com.example.androiddevelopment2.customview.state.PieChartEvent +import com.example.androiddevelopment2.customview.state.PieChartState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject +import androidx.core.graphics.toColorInt + +@HiltViewModel +class PieChartViewModel @Inject constructor() : ViewModel() { + private val _state = MutableStateFlow(PieChartState.Initial) + val state: StateFlow = _state.asStateFlow() + + private val availableColors = listOf( + "#FF6B6B", "#4ECDC4", "#45B7D1", "#FFA726", + "#66BB6A", "#AB47BC", "#E91E63", "#9C27B0" + ).map { it.toColorInt() } + + init { + generateInitialData() + } + + fun reduce(event: PieChartEvent) { + when (event) { + is PieChartEvent.OnSectorCountChanged -> generateSectorData(event.count) + is PieChartEvent.OnSectorSelected -> updateSelection(event.index) + PieChartEvent.OnClearSelection -> clearSelection() + } + } + + private fun generateInitialData() { + generateSectorData(4) + } + + private fun generateSectorData(count: Int) { + val colors = generateDistinctColors(count) + _state.value = PieChartState.Success(count, colors) + } + + private fun generateDistinctColors(count: Int): List { + val colors = mutableListOf() + val shuffledColors = availableColors.shuffled() + + for (i in 0 until count) { + var color = shuffledColors[i % shuffledColors.size] + if (colors.isNotEmpty() && color == colors.last()) { + color = shuffledColors.first { it != colors.last() } + } + colors.add(color) + } + + if (colors.size > 1 && colors.first() == colors.last()) { + colors[colors.lastIndex] = shuffledColors.first { it != colors.first() } + } + + return colors + } + + private fun updateSelection(index: Int) { + val currentState = _state.value as? PieChartState.Success ?: return + _state.value = currentState.copy(selectedSector = index) + } + + private fun clearSelection() { + val currentState = _state.value as? PieChartState.Success ?: return + _state.value = currentState.copy(selectedSector = -1) + } +} diff --git a/feature/customview/src/main/java/com/example/androiddevelopment2/customview/state/PieChartEvent.kt b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/state/PieChartEvent.kt new file mode 100644 index 0000000..e364e69 --- /dev/null +++ b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/state/PieChartEvent.kt @@ -0,0 +1,7 @@ +package com.example.androiddevelopment2.customview.state + +sealed class PieChartEvent { + data class OnSectorCountChanged(val count: Int) : PieChartEvent() + data class OnSectorSelected(val index: Int) : PieChartEvent() + object OnClearSelection : PieChartEvent() +} \ No newline at end of file diff --git a/feature/customview/src/main/java/com/example/androiddevelopment2/customview/state/PieChartState.kt b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/state/PieChartState.kt new file mode 100644 index 0000000..ecc6084 --- /dev/null +++ b/feature/customview/src/main/java/com/example/androiddevelopment2/customview/state/PieChartState.kt @@ -0,0 +1,10 @@ +package com.example.androiddevelopment2.customview.state + +sealed class PieChartState { + object Initial : PieChartState() + data class Success( + val sectorCount: Int, + val colors: List, + val selectedSector: Int = -1 + ) : PieChartState() +} \ No newline at end of file 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..cef3d8a --- /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..ac6d7be --- /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..16b2548 --- /dev/null +++ b/feature/customview/src/main/res/layout/fragment_pie_chart.xml @@ -0,0 +1,125 @@ + + + + + + + + + + + +