概述
内容
直播界面适配平板大屏幕(车机屏幕)
时间
预计 2/15~2/17 + 2/20
内容拆分
- 检查登录页是否有显示异常(布局重叠)
- 新增页面
- 设备列表Activity(仅横屏,复用设备卡片网格布局,点击进入直播查看Activity)
- 直播查看Activity(仅横屏,只保留看直播视频功能)
- 临时修改跳转:启动APP直接进入新增的设备列表页
- 功能对接
- 参考现有的直播页,利用已有的代码实现
- 列表页点击卡片跳转设备直播页
- 进入直播页面就加载直播视频
- 加载失败的时候展示重试按钮(状态)
- 对接现有的播放器手势操作:滑动到边缘控制云台
参考
强制Activity横屏或竖屏:https://blog.csdn.net/Smile_Qian/article/details/99187728
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
卡片网格布局:https://www.cnblogs.com/guanxinjing/p/13037271.html
界面跳转封装:com.danale.edge.base.BaseNavigation
当前摄像头直播界面:com.danale.edge.ui.devicecontrol.ipc.IPCLiveFragment
视频渲染View手势处理封装类:com.danale.edge.ui.common.view.ScaleTextureView
设置点击图标拉起的启动Activity:
1 2
| <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" />
|
测试账号
15105982803
App_2021
过程
大概的逻辑
主页面
LandScapeListActivity
作为主页面,在里面放置activity_land_scape_list
设备列表
activity_land_scape_list
中绑定对应的LandScapeListViewModel
LandScapeListViewModel
中设置onclick函数,通过路由跳转到直播页面
直播页
LandScapeIPCActivity
作为直播的主页面,内部放置DeviceListToIPCFragment
容器
主页
由于主页是通过Fragment切换动态创建的,所以首先创建一个新的Fragment放置设备列表
LandScapeListActivity.kt
创建主页的activity,绑定对应的xml布局文件
ui/devicelist/landscape/LandScapeListActivity.kt
1 2 3 4 5 6 7 8 9 10 11 12
| package com.danale.edge.ui.devicelist.landscape
import android.os.Bundle import com.danale.edge.R import com.danale.edge.base.BaseActivity
class LandScapeListActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_land_scape_list) } }
|
activity_land_scape_list.xml
activity布局文件,内部套一个Fragment
layout/activity_land_scape_list.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" tools:context=".ui.VideoFlowActivity">
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true" android:orientation="vertical" android:background="@color/black">
<androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_container" android:name="com.danale.edge.ui.devicelist.landscape.LandScapeListFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:layout="@layout/fragment_land_scape_list" />
</androidx.constraintlayout.widget.ConstraintLayout> </layout>
|
fragment_land_scape_list.xml
创建主页的Fragment组件,通过variable实现数据绑定
通过RecyclerView实现网格布局
layout/fragment_land_scape_list.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="viewModel" type="com.danale.edge.ui.devicelist.landscape.LandScapeListViewModel" /> </data>
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/app_std_background_gray">
<com.danale.edge.ui.common.view.TopSafeAreaGuideLine android:id="@+id/safe_area_top_guide" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_begin="24dp" tools:ignore="MissingConstraints" />
<include android:id="@+id/component_title_bar" layout="@layout/component_title_bar" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/safe_area_top_guide" />
<androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/component_title_bar" />
</androidx.constraintlayout.widget.ConstraintLayout> </layout>
|
LandScapeListFragment.kt
实现Fragment的布局和绑定,添加binding和ViewModel,在GridLayoutManager(requireContext(), 4)
处设置布局样式
com/danale/edge/ui/devicelist/landscape/LandScapeListFragment.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| package com.danale.edge.ui.devicelist.landscape
import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.get import androidx.recyclerview.widget.GridLayoutManager import com.danale.edge.R import com.danale.edge.base.BaseFragment import com.danale.edge.databinding.FragmentLandScapeListBinding import com.danale.edge.usersdk.type.DeviceInfo
class LandScapeListFragment : BaseFragment() {
private lateinit var binding: FragmentLandScapeListBinding private lateinit var viewModel: LandScapeListViewModel
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View {
logger.i(TAG, "onCreateView")
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_land_scape_list, container, false) binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(requireActivity()).get() observeNavigation(viewModel)
binding.recyclerView.apply { adapter = viewModel.adapterForClassCode(DeviceInfo.DEVICE_TYPE_IPC) layoutManager = GridLayoutManager(requireContext(), 4) }
return binding.root }
override fun onResume() { super.onResume() setStatusBarDarkText(true) viewModel.loadDeviceList() }
}
|
LandScapeListViewModel.kt
ViewModel文件,用来实现内部逻辑
com/danale/edge/ui/devicelist/landscape/LandScapeListViewModel.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package com.danale.edge.ui.devicelist.landscape
import android.app.Application import android.content.Intent import com.danale.edge.base.BaseNavigation import com.danale.edge.ui.devicelist.ListDeviceViewModel import com.danale.edge.ui.devicelist.card.CardAdapter import com.danale.edge.usersdk.type.DeviceInfo import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject
@HiltViewModel class LandScapeListViewModel @Inject constructor(application: Application) : ListDeviceViewModel(application), CardAdapter.Delegate {
override fun navigateDevicePage(item: DeviceInfo) { val type = item.getDeviceType() if (type != DeviceInfo.DEVICE_TYPE_IPC) return
item.getDeviceInfoDid()?.let { did -> val intent = Intent().apply { putExtra(BaseNavigation.Constants.EXTRA_PLATFORM_DEVICE_ID_32, did) putExtra( BaseNavigation.Constants.EXTRA_PLATFORM_DEVICE_LIKE_NAME, item.getDeviceInfoLikeName() ) } navigationRequiredEvent.postValue( BaseNavigation( BaseNavigation.Route.CONTROL_LAND_SCAPE_IPC, intent ) ) } }
}
|
直播页
LandScapeIPCActivity.kt
创建一个直播页的activity
com/danale/edge/ui/devicecontrol/landscape/LandScapeIPCActivity.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| package com.danale.edge.ui.devicecontrol.landscape
import android.os.Bundle import androidx.activity.viewModels import com.danale.edge.R import com.danale.edge.base.BaseActivity import com.danale.edge.base.BaseNavigation import com.danale.edge.foundation.privacy.Fuzzy import com.danale.edge.ui.devicecontrol.common.DeviceLiveViewModel
class LandScapeIPCActivity : BaseActivity() {
private val liveVm: DeviceLiveViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_land_scape_ipc)
intent?.let { intent -> intent.getStringExtra(BaseNavigation.Constants.EXTRA_PLATFORM_DEVICE_ID_32) ?.let { did -> logger.d(TAG, "onCreate, did=${Fuzzy.interval(did)}") liveVm.deviceId32 = did } intent.getStringExtra(BaseNavigation.Constants.EXTRA_PLATFORM_DEVICE_LIKE_NAME) ?.let { name -> logger.d(TAG, "onCreate, name=${Fuzzy.interval(name)}") liveVm.observableDeviceName.set(name) } } } }
|
activity_land_scape_ipc.xml
layout/activity_land_scape_ipc.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.devicecontrol.landscape.LandScapeIPCActivity">
<androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_container_ipc" android:name="com.danale.edge.ui.devicecontrol.landscape.LandScapeIPCLiveFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:layout="@layout/fragment_land_scape_ipc_live" />
</androidx.constraintlayout.widget.ConstraintLayout>
|
fragment_land_scape_ipc_live.xml
layout/fragment_land_scape_ipc_live.xmll
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools">
<data>
<variable name="viewModel" type="com.danale.edge.ui.devicecontrol.common.DeviceLiveViewModel" />
<import type="android.view.View" /> </data>
<androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/black"> <com.danale.edge.ui.common.view.TopSafeAreaGuideLine android:id="@+id/safe_area_top_guide" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_begin="24dp" tools:ignore="MissingConstraints" />
<com.danale.edge.ui.common.view.ScaleTextureView android:id="@+id/texture_view" android:layout_width="match_parent" android:layout_height="match_parent" />
<androidx.appcompat.widget.LinearLayoutCompat android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="start" android:orientation="horizontal" android:paddingHorizontal="12dp" android:paddingVertical="4dp" android:visibility="visible" app:layout_constraintTop_toBottomOf="@id/safe_area_top_guide">
<androidx.appcompat.widget.AppCompatImageView android:id="@+id/iv_back_icon" android:layout_width="48dp" android:layout_height="48dp" android:layout_marginHorizontal="3dp" android:layout_marginVertical="15dp" android:padding="6dp" app:srcCompat="@mipmap/ic_back_white" />
</androidx.appcompat.widget.LinearLayoutCompat>
<com.github.ybq.android.spinkit.SpinKitView style="@style/SpinKitView.ThreeBounce" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{viewModel.observableVideoState.showLoadingUi ? View.VISIBLE : View.GONE}" app:SpinKit_Color="@color/white" app:layout_constraintBottom_toBottomOf="@id/texture_view" app:layout_constraintEnd_toEndOf="@id/texture_view" app:layout_constraintStart_toStartOf="@id/texture_view" app:layout_constraintTop_toTopOf="@id/texture_view" />
<androidx.appcompat.widget.AppCompatTextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/bg_round_color" android:backgroundTint="@color/app_std_white_20" android:gravity="center" android:onClick="@{viewModel::onClickLoadLiveVideo}" android:paddingHorizontal="24dp" android:paddingVertical="8dp" android:text="@string/text_retry_loading" android:textColor="@color/white" android:visibility="@{viewModel.observableVideoState.showErrorUi ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toBottomOf="@id/texture_view" app:layout_constraintEnd_toEndOf="@id/texture_view" app:layout_constraintStart_toStartOf="@id/texture_view" app:layout_constraintTop_toTopOf="@id/texture_view" />
</androidx.constraintlayout.widget.ConstraintLayout> </layout>
|
![image]()
LandScapeIPCLiveFragment.kt
com/danale/edge/ui/devicecontrol/landscape/LandScapeIPCLiveFragment.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
| package com.danale.edge.ui.devicecontrol.landscape
import android.content.res.Configuration import android.graphics.SurfaceTexture import android.os.Bundle import android.view.* import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.get import com.danale.edge.R import com.danale.edge.base.BaseFragment import com.danale.edge.databinding.FragmentLandScapeIpcLiveBinding import com.danale.edge.foundation.media.PcmTrack import com.danale.edge.ui.common.view.ScaleTextureView import com.danale.edge.ui.devicecontrol.common.DeviceLiveViewModel import com.danale.edge.usersdk.usecase.UserSdkUseCase import javax.inject.Inject
class LandScapeIPCLiveFragment : BaseFragment(), TextureView.SurfaceTextureListener {
private lateinit var binding: FragmentLandScapeIpcLiveBinding private lateinit var viewModel: DeviceLiveViewModel private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
private var surfaceCallback: ((Surface) -> Unit)? = null private var surface: Surface? = null
@Inject lateinit var sdk: UserSdkUseCase
lateinit var deviceId32: String
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = DataBindingUtil.inflate( inflater, R.layout.fragment_land_scape_ipc_live, container, false ) binding.lifecycleOwner = viewLifecycleOwner
viewModel = ViewModelProvider(requireActivity()).get() binding.viewModel = this.viewModel
deviceId32 = viewModel.deviceId32.toString()
binding.textureView.surfaceTextureListener = this binding.textureView.setCanTouch(true) binding.textureView.setScaleTextureOnTouchLimitListener(object : ScaleTextureView.ScaleTextureOnTouchLimitListener { override fun onLeftLimit() { logger.d(TAG, "video gesture, left limit") viewModel.setPtzNeedStop(false) viewModel.sendRequest(DeviceLiveViewModel.IPCDirection.LEFT) }
override fun onRightLimit() { logger.d(TAG, "video gesture, right limit") viewModel.setPtzNeedStop(false) viewModel.sendRequest(DeviceLiveViewModel.IPCDirection.RIGHT) }
override fun onTopLimit() { logger.d(TAG, "video gesture, top limit") viewModel.setPtzNeedStop(false) viewModel.sendRequest(DeviceLiveViewModel.IPCDirection.TOP) }
override fun onBottomLimit() { logger.d(TAG, "video gesture, bottom limit") viewModel.setPtzNeedStop(false) viewModel.sendRequest(DeviceLiveViewModel.IPCDirection.BOTTOM) }
override fun onFinish() { logger.d(TAG, "video gesture, finish") viewModel.setPtzNeedStop(true) viewModel.sendRequest(DeviceLiveViewModel.IPCDirection.STOP) }
override fun onScale(scale: Float) { logger.d(TAG, "video gesture, scale $scale") }
override fun onOnlyClick() { logger.d(TAG, "video gesture, click") viewModel.toggleOverlayVisible() }
})
binding.ivBackIcon.setOnClickListener { onBackPressed() }
requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { logger.d(TAG, "mic permission result, it=$it") }
return binding.root }
override fun onResume() { super.onResume() PcmTrack.startAudioTrackSync() ensureSurface { viewModel.startLive(it) } }
override fun onPause() { super.onPause() PcmTrack.releaseAudioTrackSync() viewModel.releaseDeviceAndPlayer() }
private fun ensureSurface(callback: (Surface) -> Unit) { val s = surface if (s != null) { callback(s) return } else { surfaceCallback = callback } }
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { logger.i(TAG, "onSurfaceTextureAvailable") val s = Surface(surface) this.surface = s surfaceCallback?.let { it(s) } surfaceCallback = null }
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { logger.i(TAG, "onSurfaceTextureSizeChanged") }
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { logger.i(TAG, "onSurfaceTextureDestroyed") this.surface = null return true }
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) viewModel.setOrientation(newConfig.orientation) } }
|
云台控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| fun sendRequest(direction: IPCDirection) { val did = deviceId32 ?: return val thing = deviceThingInfo ?: return
viewModelScope.launch { val stop = DpCommand("direction", "8", "1") val flipState = performOnIOWithDefault(DpFlipState.NORMAL) { sdk.getDeviceFlipStatusWithCache(thing).await() }
val request = when (direction) { IPCDirection.LEFT -> if (flipState == DpFlipState.UP_SIDE_DOWN) { DpCommand("direction", "8", "5") } else { DpCommand("direction", "8", "4") } IPCDirection.RIGHT -> if (flipState == DpFlipState.UP_SIDE_DOWN) { DpCommand("direction", "8", "4") } else { DpCommand("direction", "8", "5") } IPCDirection.TOP -> if (flipState == DpFlipState.UP_SIDE_DOWN) { DpCommand("direction", "8", "3") } else { DpCommand("direction", "8", "2") } IPCDirection.BOTTOM -> if (flipState == DpFlipState.UP_SIDE_DOWN) { DpCommand("direction", "8", "2") } else { DpCommand("direction", "8", "3") } else -> stop } withContext(Dispatchers.IO) { try { sdk.commandDeviceDp(did, DpCode.PTZ_CONTROL, listOf(request)).await() if (ptzNeedStop.get()) { logger.w(TAG, "ptz, control, need stop") sdk.commandDeviceDp(did, DpCode.PTZ_CONTROL, listOf(stop)).await() } } catch (e: Exception) { logger.e(TAG, "ptz, control, error", e) try { sdk.commandDeviceDp(did, DpCode.PTZ_CONTROL, listOf(stop)).await() } catch (_: Exception) { } } } }
}
fun setPtzNeedStop(flag: Boolean){ ptzNeedStop.set(flag) }
|
强制横屏
Androidmanifest.xml
中添加activity并设置intent-filter
1 2 3 4 5 6 7 8 9 10 11
| <activity android:name="com.danale.edge.ui.devicelist.landscape.LandScapeListActivity" android:exported="true" android:screenOrientation="landscape" android:configChanges="orientation|keyboardHidden">
<intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
|