라이브 방 룰렛 - 룰렛판 추가
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.profile.LiveRoomUserProfileDialog
|
||||||
import kr.co.vividnext.sodalive.live.room.update.LiveRoomInfoEditDialog
|
import kr.co.vividnext.sodalive.live.room.update.LiveRoomInfoEditDialog
|
||||||
import kr.co.vividnext.sodalive.live.roulette.RoulettePreviewDialog
|
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.live.roulette.config.RouletteConfigActivity
|
||||||
import kr.co.vividnext.sodalive.report.ProfileReportDialog
|
import kr.co.vividnext.sodalive.report.ProfileReportDialog
|
||||||
import kr.co.vividnext.sodalive.report.ReportType
|
import kr.co.vividnext.sodalive.report.ReportType
|
||||||
|
@ -1250,7 +1251,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun spinRoulette() {
|
private fun spinRoulette() {
|
||||||
viewModel.spinRoulette(roomId = roomId) { can, randomlySelectedItem ->
|
viewModel.spinRoulette(roomId = roomId) { can, items, randomlySelectedItem ->
|
||||||
val rawMessage = "[$randomlySelectedItem] 당첨!"
|
val rawMessage = "[$randomlySelectedItem] 당첨!"
|
||||||
val rouletteRawMessage = Gson().toJson(
|
val rouletteRawMessage = Gson().toJson(
|
||||||
LiveRoomChatRawMessage(
|
LiveRoomChatRawMessage(
|
||||||
|
@ -1261,25 +1262,32 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
agora.sendRawMessageToGroup(
|
RouletteSpinDialog(
|
||||||
rawMessage = rouletteRawMessage.toByteArray(),
|
activity = this@LiveRoomActivity,
|
||||||
onSuccess = {
|
items = items,
|
||||||
handler.post {
|
selectedItem = randomlySelectedItem,
|
||||||
chatAdapter.items.add(
|
layoutInflater = layoutInflater
|
||||||
LiveRoomRouletteDonationChat(
|
) {
|
||||||
profileUrl = SharedPreferenceManager.profileImage,
|
agora.sendRawMessageToGroup(
|
||||||
nickname = SharedPreferenceManager.nickname,
|
rawMessage = rouletteRawMessage.toByteArray(),
|
||||||
chat = rawMessage
|
onSuccess = {
|
||||||
|
handler.post {
|
||||||
|
chatAdapter.items.add(
|
||||||
|
LiveRoomRouletteDonationChat(
|
||||||
|
profileUrl = SharedPreferenceManager.profileImage,
|
||||||
|
nickname = SharedPreferenceManager.nickname,
|
||||||
|
chat = rawMessage
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
invalidateChat()
|
||||||
invalidateChat()
|
viewModel.addDonationCan(can)
|
||||||
viewModel.addDonationCan(can)
|
}
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
viewModel.refundRouletteDonation(roomId)
|
||||||
}
|
}
|
||||||
},
|
)
|
||||||
onFailure = {
|
}.show()
|
||||||
viewModel.refundRouletteDonation(roomId)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,6 @@ import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody.Companion.asRequestBody
|
import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
class LiveRoomViewModel(
|
class LiveRoomViewModel(
|
||||||
private val repository: LiveRepository,
|
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!!) {
|
if (!_isLoading.value!!) {
|
||||||
_isLoading.value = true
|
_isLoading.value = true
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
|
@ -905,26 +904,19 @@ class LiveRoomViewModel(
|
||||||
private fun randomSelectRouletteItem(
|
private fun randomSelectRouletteItem(
|
||||||
can: Int,
|
can: Int,
|
||||||
items: List<RouletteItem>,
|
items: List<RouletteItem>,
|
||||||
complete: (Int, String) -> Unit
|
complete: (Int, List<RouletteItem>, String) -> Unit
|
||||||
) {
|
) {
|
||||||
_isLoading.value = true
|
_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) {
|
val rouletteItems = mutableListOf<String>()
|
||||||
if (randomValue < cumulativeWeights[index]) {
|
items.asSequence().forEach { item ->
|
||||||
_isLoading.value = false
|
repeat(item.weight) {
|
||||||
complete(can, rouletteItemTitles[index - 1])
|
rouletteItems.add(item.title)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_isLoading.value = false
|
_isLoading.value = false
|
||||||
complete(can, rouletteItemTitles.last())
|
complete(can, items, rouletteItems.random())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculatePercentages(options: List<RouletteItem>): List<RoulettePreviewItem> {
|
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