라이브 방 룰렛 - 룰렛판 추가
This commit is contained in:
parent
9f66cb91fc
commit
63c2d607cc
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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>
|
Loading…
Reference in New Issue