我目前正在将 Chromcast 集成到我们的应用程序中。我们对所有 UI 元素使用 Jetpack Compose。我们正在使用 TopAppBar 可组合项,我尝试使用 MediaRouteActionProvider 将 Chromecast 按钮添加到其中。我找到使用 MediaRouteActionProvider 的唯一方法是使用 menu.xml 并在 onCreateOptionsMenu 中扩充菜单。
有谁知道在 Jetpack Compose 菜单上下文之外使用 ActionProvider 的方法,还是我现在只能使用菜单?
更新:决定再次尝试解决这个问题。
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.mediarouter.app.MediaRouteDialogFactory
import androidx.mediarouter.media.MediaControlIntent
import androidx.mediarouter.media.MediaRouteSelector
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastState
import com.google.android.gms.cast.framework.CastStateListener
class MediaRouteViewModel(context: Context) : ViewModel(), CastStateListener {
var isCastingAvailable = false
private set
val castingStateLiveData = MutableLiveData(CastingState.UNKNOWN)
val mDialogFactory = MediaRouteDialogFactory.getDefault()
val mSelector: MediaRouteSelector = MediaRouteSelector.Builder()
.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
.build()
init {
try {
isCastingAvailable = true
val castingContext = CastContext.getSharedInstance(context)
castingContext.addCastStateListener(this)
} catch (e: Exception) {
// handle me please
}
}
fun shouldShowChooserFragment(): Boolean {
return when (castingStateLiveData.value) {
CastingState.NOT_CONNECTED -> true
CastingState.UNKNOWN -> true
CastingState.NO_DEVICES_AVAILABLE -> true
CastingState.CONNECTING -> false
CastingState.CONNECTED -> false
else -> false
}
}
enum class CastingState {
CONNECTED,
CONNECTING,
NOT_CONNECTED,
NO_DEVICES_AVAILABLE,
UNKNOWN
}
class Factory(val context: Context) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return MediaRouteViewModel(context = context) as T
}
}
override fun onCastStateChanged(p0: Int) {
val castingState = when (p0) {
CastState.CONNECTED -> CastingState.CONNECTED
CastState.CONNECTING -> CastingState.CONNECTING
CastState.NOT_CONNECTED -> CastingState.NOT_CONNECTED
CastState.NO_DEVICES_AVAILABLE -> CastingState.NO_DEVICES_AVAILABLE
else -> CastingState.UNKNOWN
}
castingStateLiveData.postValue(castingState)
}
}
@Composable
fun MediaRouter(
modifier: Modifier,
iconWidth: Dp,
fragmentManager: FragmentManager
) {
val context = LocalContext.current
val mediaRouteProviderViewModel: MediaRouteViewModel =
viewModel(factory = MediaRouteViewModel.Factory(context))
// Can use the casting state to change the painter accordingly
val castingState = mediaRouteProviderViewModel.castingStateLiveData.observeAsState()
if (mediaRouteProviderViewModel.isCastingAvailable) {
Box(modifier = modifier.size(iconWidth)) {
Image(
painter = painterResource(id = R.drawable.ic_baseline_cast_connected_24),
contentDescription = null,
modifier = Modifier
.clickable {
val shouldShowChooserFragment = mediaRouteProviderViewModel.shouldShowChooserFragment()
val fragmentTag = if (shouldShowChooserFragment) "MediaRouteChooserDialogFragment" else "MediaRouteControllerDialogFragment"
val fragment = if (shouldShowChooserFragment) {
mediaRouteProviderViewModel.mDialogFactory
.onCreateChooserDialogFragment()
.apply {
routeSelector = mediaRouteProviderViewModel.mSelector
}
} else {
mediaRouteProviderViewModel.mDialogFactory.onCreateControllerDialogFragment()
}
val transaction = fragmentManager.beginTransaction()
transaction.add(fragment, fragmentTag)
transaction.commitAllowingStateLoss()
},
contentScale = ContentScale.FillBounds
)
}
}
}
旧帖子:
我确信在撰写中有更好的方法来做到这一点,但这至少可以完成工作。
class MediaRouteViewModel(context: Context) : ViewModel() {
private val mediaRouteActionProvider = MediaRouteActionProvider(context)
val buttonView: View
init {
buttonView = mediaRouteActionProvider.onCreateActionView()
mediaRouteActionProvider.routeSelector = MediaRouteSelector.Builder()
.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
.build()
}
fun onClick() {
mediaRouteActionProvider.onPerformDefaultAction()
}
}
@Composable
fun MediaRouter(modifier: Modifier, iconWidth: Dp) {
val context = LocalContext.current
val mediaRouteProviderViewModel = MediaRouteViewModel(context)
Box(modifier = modifier.size(iconWidth)) {
AndroidView(factory = {
mediaRouteProviderViewModel.buttonView
}, modifier = Modifier.fillMaxSize())
Box(modifier = Modifier
.fillMaxSize()
.clickable { mediaRouteProviderViewModel.onClick() })
}
}
对于我的情况,我不必使用任何片段管理器。最终得到以下结果。
我有一个数据源类来简单地跟踪应用程序是否连接到投射设备。
interface CastDataSource {
val castState: StateFlow<Int>
}
class CastDataSourceImpl(private val castContext: CastContext) : CastDataSource {
private var castSession: CastSession? = null
private val _castState = MutableStateFlow<Int>(CastState.NOT_CONNECTED)
override val castState = _castState.asStateFlow()
init {
castContext.sessionManager.addSessionManagerListener(object :
SessionManagerListener<CastSession> {
override fun onSessionEnded(session: CastSession, error: Int) {
if (session == castContext) {
castSession = null
}
_castState.value = CastState.NOT_CONNECTED
}
override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
castSession = session
_castState.value = CastState.CONNECTED
}
override fun onSessionStarted(session: CastSession, sessionId: String) {
castSession = session
_castState.value = CastState.CONNECTED
}
override fun onSessionStarting(session: CastSession) {}
override fun onSessionStartFailed(session: CastSession, error: Int) {}
override fun onSessionEnding(session: CastSession) {}
override fun onSessionResuming(session: CastSession, sessionId: String) {}
override fun onSessionResumeFailed(session: CastSession, error: Int) {}
override fun onSessionSuspended(session: CastSession, reason: Int) {}
}, CastSession::class.java)
}
}
然后实例化它。 (我在依赖注入中定义了这个)
CastDataSourceImpl(CastContext.getSharedInstance(appContext))
为了简单起见,您可以直接在可组合项中使用此数据源。 (在我的项目中,我更喜欢使用存储库和视图模型。
现在使用此数据源,您可以简单地收集投射状态并根据该状态启动必要的对话框。如果未连接,则使用
chooser
,否则使用 controller
class YourActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
...
setContent {
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = { Text(text = title) },
actions = {
IconButton(onClick = {
if(castState == CastState.CONNECTED) {
MediaRouteControllerDialog(this@VideoBrowserActivity).show()
} else {
MediaRouteChooserDialog(this@VideoBrowserActivity).apply {
routeSelector = MediaRouteSelector.Builder()
.addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
.build()
}.show()
}
}) {
Icon(
imageVector = Icons.Filled.Cast,
contentDescription = null
)
}
}
)
}
)
}
}
}