라이브 방 룰렛 - 룰렛판 추가

This commit is contained in:
klaus 2023-12-04 15:17:05 +09:00
parent 9f66cb91fc
commit 63c2d607cc
6 changed files with 326 additions and 33 deletions

View File

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

View File

@ -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<RouletteItem>, String) -> Unit) {
if (!_isLoading.value!!) {
_isLoading.value = true
compositeDisposable.add(
@ -905,26 +904,19 @@ class LiveRoomViewModel(
private fun randomSelectRouletteItem(
can: Int,
items: List<RouletteItem>,
complete: (Int, String) -> Unit
complete: (Int, List<RouletteItem>, 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<String>()
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<RouletteItem>): List<RoulettePreviewItem> {

View File

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

View File

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

View File

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

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<FrameLayout
android:layout_width="300dp"
android:layout_height="300dp"
tools:ignore="UselessParent">
<kr.co.vividnext.sodalive.live.roulette.RouletteView
android:id="@+id/roulette"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
<kr.co.vividnext.sodalive.live.roulette.RouletteInvertedTriangle
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
</LinearLayout>