From 63c2d607cc9dfafe11ab2b62a795b6a95cc51c84 Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 4 Dec 2023 15:17:05 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=EB=B0=A9=20?= =?UTF-8?q?=EB=A3=B0=EB=A0=9B=20-=20=EB=A3=B0=EB=A0=9B=ED=8C=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/LiveRoomActivity.kt | 44 +++-- .../sodalive/live/room/LiveRoomViewModel.kt | 22 +-- .../live/roulette/RouletteInvertedTriangle.kt | 45 +++++ .../live/roulette/RouletteSpinDialog.kt | 62 +++++++ .../sodalive/live/roulette/RouletteView.kt | 162 ++++++++++++++++++ .../main/res/layout/dialog_roulette_spin.xml | 24 +++ 6 files changed, 326 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteInvertedTriangle.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteSpinDialog.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteView.kt create mode 100644 app/src/main/res/layout/dialog_roulette_spin.xml diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt index 0c8c737..d67df7a 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt @@ -74,6 +74,7 @@ import kr.co.vividnext.sodalive.live.room.profile.LiveRoomProfileListAdapter import kr.co.vividnext.sodalive.live.room.profile.LiveRoomUserProfileDialog import kr.co.vividnext.sodalive.live.room.update.LiveRoomInfoEditDialog import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewDialog +import kr.co.vividnext.sodalive.live.roulette.RouletteSpinDialog import kr.co.vividnext.sodalive.live.roulette.config.RouletteConfigActivity import kr.co.vividnext.sodalive.report.ProfileReportDialog import kr.co.vividnext.sodalive.report.ReportType @@ -1250,7 +1251,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } private fun spinRoulette() { - viewModel.spinRoulette(roomId = roomId) { can, randomlySelectedItem -> + viewModel.spinRoulette(roomId = roomId) { can, items, randomlySelectedItem -> val rawMessage = "[$randomlySelectedItem] 당첨!" val rouletteRawMessage = Gson().toJson( LiveRoomChatRawMessage( @@ -1261,25 +1262,32 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB ) ) - agora.sendRawMessageToGroup( - rawMessage = rouletteRawMessage.toByteArray(), - onSuccess = { - handler.post { - chatAdapter.items.add( - LiveRoomRouletteDonationChat( - profileUrl = SharedPreferenceManager.profileImage, - nickname = SharedPreferenceManager.nickname, - chat = rawMessage + RouletteSpinDialog( + activity = this@LiveRoomActivity, + items = items, + selectedItem = randomlySelectedItem, + layoutInflater = layoutInflater + ) { + agora.sendRawMessageToGroup( + rawMessage = rouletteRawMessage.toByteArray(), + onSuccess = { + handler.post { + chatAdapter.items.add( + LiveRoomRouletteDonationChat( + profileUrl = SharedPreferenceManager.profileImage, + nickname = SharedPreferenceManager.nickname, + chat = rawMessage + ) ) - ) - invalidateChat() - viewModel.addDonationCan(can) + invalidateChat() + viewModel.addDonationCan(can) + } + }, + onFailure = { + viewModel.refundRouletteDonation(roomId) } - }, - onFailure = { - viewModel.refundRouletteDonation(roomId) - } - ) + ) + }.show() } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt index 698e79e..e34742b 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomViewModel.kt @@ -34,7 +34,6 @@ import okhttp3.MultipartBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.File -import kotlin.random.Random class LiveRoomViewModel( private val repository: LiveRepository, @@ -825,7 +824,7 @@ class LiveRoomViewModel( } } - fun spinRoulette(roomId: Long, complete: (Int, String) -> Unit) { + fun spinRoulette(roomId: Long, complete: (Int, List, String) -> Unit) { if (!_isLoading.value!!) { _isLoading.value = true compositeDisposable.add( @@ -905,26 +904,19 @@ class LiveRoomViewModel( private fun randomSelectRouletteItem( can: Int, items: List, - complete: (Int, String) -> Unit + complete: (Int, List, String) -> Unit ) { _isLoading.value = true - val rouletteItemTitles = items.asSequence().map { it.title }.toList() - val cumulativeWeights = items.runningFold(0) { sum, item -> - sum + item.weight - } - val totalWeight = items.asSequence().map { it.weight }.sum() - val randomValue = Random.nextInt(0, totalWeight) - for (index in 1 until cumulativeWeights.size) { - if (randomValue < cumulativeWeights[index]) { - _isLoading.value = false - complete(can, rouletteItemTitles[index - 1]) - return + val rouletteItems = mutableListOf() + items.asSequence().forEach { item -> + repeat(item.weight) { + rouletteItems.add(item.title) } } _isLoading.value = false - complete(can, rouletteItemTitles.last()) + complete(can, items, rouletteItems.random()) } private fun calculatePercentages(options: List): List { diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteInvertedTriangle.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteInvertedTriangle.kt new file mode 100644 index 0000000..8a1159f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteInvertedTriangle.kt @@ -0,0 +1,45 @@ +package kr.co.vividnext.sodalive.live.roulette + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.util.AttributeSet +import android.view.View + +class RouletteInvertedTriangle @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + private val trianglePath = Path() + private var trianglePaint = Paint() + + private val triangleSize = 60f + + init { + trianglePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.RED // Set the color of the triangle + style = Paint.Style.FILL // Fill the triangle + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + // Clear the old path + trianglePath.reset() + + // Define the new path for the inverted triangle + // Starting point (top of the triangle) + trianglePath.moveTo((width / 2f) - 30, 10f) + // Line to bottom left of the triangle + trianglePath.lineTo((width / 2f) + 30, 10f) + // Line to bottom right of the triangle + trianglePath.lineTo(width / 2f, 10f + triangleSize) + // Close the path to form a triangle + trianglePath.close() + + canvas.drawPath(trianglePath, trianglePaint) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteSpinDialog.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteSpinDialog.kt new file mode 100644 index 0000000..cc4f8f0 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteSpinDialog.kt @@ -0,0 +1,62 @@ +package kr.co.vividnext.sodalive.live.roulette + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.Window +import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.FragmentActivity +import kr.co.vividnext.sodalive.databinding.DialogRouletteSpinBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class RouletteSpinDialog( + private val activity: FragmentActivity, + private val items: List, + private val selectedItem: String, + layoutInflater: LayoutInflater, + private val complete: () -> Unit +) { + private val alertDialog: AlertDialog + private val dialogView = DialogRouletteSpinBinding.inflate(layoutInflater) + private val handler = Handler(Looper.getMainLooper()) + + init { + val dialogBuilder = AlertDialog.Builder(activity) + dialogBuilder.setView(dialogView.root) + + alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + alertDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + setupView() + } + + fun show() { + alertDialog.show() + + val lp = WindowManager.LayoutParams() + lp.copyFrom(alertDialog.window?.attributes) + lp.width = activity.resources.displayMetrics.widthPixels - (26.7f.dpToPx()).toInt() + lp.height = WindowManager.LayoutParams.WRAP_CONTENT + + alertDialog.window?.attributes = lp + rotateToOption() + } + + private fun setupView() { + dialogView.roulette.items = items + } + + private fun rotateToOption() { + dialogView.roulette.rotateToOption(selectedItem) { + handler.postDelayed({ + alertDialog.dismiss() + complete() + }, 1500) + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteView.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteView.kt new file mode 100644 index 0000000..f8ff981 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/roulette/RouletteView.kt @@ -0,0 +1,162 @@ +package kr.co.vividnext.sodalive.live.roulette + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.util.AttributeSet +import android.view.View +import android.view.animation.Animation +import android.view.animation.RotateAnimation +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.sin + +class RouletteView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + private var rect = RectF() + + + private val bgPaint = Paint() + private val fillPaint = Paint() + private val textPaint = Paint() + private val strokePaint = Paint() + + var items = listOf() + + private val colors = listOf( + Color.parseColor("#e6548f7d"), + Color.parseColor("#e62d7390"), + Color.parseColor("#e64d6aa4"), + Color.parseColor("#e659548f"), + Color.parseColor("#e6d38c38"), + Color.parseColor("#e6d85e37"), + ) + + init { + bgPaint.apply { + color = Color.WHITE + style = Paint.Style.FILL + isAntiAlias = true + } + + strokePaint.apply { + color = Color.BLACK + style = Paint.Style.STROKE + strokeWidth = 10f + isAntiAlias = true + } + + fillPaint.apply { + style = Paint.Style.FILL + isAntiAlias = true + } + + textPaint.apply { + color = Color.BLACK + textSize = 30f + textAlign = Paint.Align.CENTER + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + + val diameter = min(width, height) - strokePaint.strokeWidth + rect.set( + 0f + strokePaint.strokeWidth / 2, + 0f + strokePaint.strokeWidth / 2, + diameter, + diameter + ) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + canvas.drawCircle(rect.centerX(), rect.centerY(), rect.width() / 2, bgPaint) + + val totalWeight = items.asSequence().map { it.weight }.sum() + var startAngle = -90f + + items.forEachIndexed { index, (option, weight) -> + val sweepAngle = (weight / totalWeight.toFloat()) * 360f - 1 + fillPaint.color = colors[index] + canvas.drawArc(rect, startAngle, sweepAngle, true, fillPaint) + + drawOptionText(canvas, option, startAngle, sweepAngle) + startAngle += sweepAngle + 1 + } + + canvas.drawCircle(rect.centerX(), rect.centerY(), rect.width() / 2, strokePaint) + } + + private fun drawOptionText( + canvas: Canvas, + option: String, + startAngle: Float, + sweepAngle: Float + ) { + val textRadius = rect.width() / 4 // Increase radius to move text outside the circle + val angle = Math.toRadians((startAngle + sweepAngle / 2).toDouble()).toFloat() + + // Calculate the text position + val x = rect.centerX() + textRadius * cos(angle) + 10 + val y = rect.centerY() + textRadius * sin(angle) + + // Save the canvas state + val saveCount = canvas.save() + + // Rotate the canvas around the text position + canvas.rotate(startAngle + sweepAngle / 2, x, y) + + // Draw the text aligned with the segment + canvas.drawText(option, x, y, textPaint) + + // Restore the canvas to its previous state + canvas.restoreToCount(saveCount) + } + + private fun getAngleForOption(option: String): Float { + val totalWeight = items.asSequence().map { it.weight }.sum() + var startAngle = 0f + + items.forEach { (currentOption, weight) -> + val sweepAngle = (weight / totalWeight.toFloat()) * 360f + if (currentOption == option) { + // Return the midpoint angle of the segment + return (startAngle + sweepAngle / 2) + } + startAngle += sweepAngle + } + return 0f + } + + fun rotateToOption(option: String, complete: () -> Unit) { + val targetAngle = 0 - (getAngleForOption(option) + 360 * 10) + + val rotateAnimation = RotateAnimation( + 0f, targetAngle, + RotateAnimation.RELATIVE_TO_SELF, 0.5f, + RotateAnimation.RELATIVE_TO_SELF, 0.5f + ) + rotateAnimation.duration = 2000 + rotateAnimation.fillAfter = true + rotateAnimation.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationStart(animation: Animation?) { + } + + override fun onAnimationEnd(animation: Animation?) { + complete() + } + + override fun onAnimationRepeat(animation: Animation?) { + } + }) + + startAnimation(rotateAnimation) + } +} diff --git a/app/src/main/res/layout/dialog_roulette_spin.xml b/app/src/main/res/layout/dialog_roulette_spin.xml new file mode 100644 index 0000000..7f9c9ca --- /dev/null +++ b/app/src/main/res/layout/dialog_roulette_spin.xml @@ -0,0 +1,24 @@ + + + + + + + + + +