Compare commits

...

6 Commits

Author SHA1 Message Date
cxf 45ef877b4f 合并远程初始提交(保留本地 README) 2025-10-20 17:41:42 +08:00
cxf 823949a7d7 首次提交 2025-10-20 17:36:31 +08:00
cxf bb56200f79 首次提交 2025-10-20 17:12:08 +08:00
cxf 8e4287100b 第一次提交 2025-10-20 16:18:56 +08:00
cxf 3ad016fc75 首次提交 2025-10-20 16:07:07 +08:00
cxf 91bdec4aa6 Initial commit 2025-10-20 15:44:02 +08:00
1769 changed files with 150852 additions and 1 deletions

67
.gitignore vendored Normal file
View File

@ -0,0 +1,67 @@
<<<<<<< HEAD
# ----------- Gradle / 构建产物 -----------
.gradle/
build/
*.apk
*.aab
# ----------- 本地密钥 / 环境文件 -----------
local.properties
*.jks
*.keystore
google-services.json
# ----------- IDEA / AS 缓存 & 个人配置 -----------
.idea/caches/
.idea/libraries/
.idea/workspace.xml
.idea/tasks.xml
.idea/navEditor.xml
.idea/assetWizardSettings.xml
*.iml
# ----------- OS 垃圾 -----------
.DS_Store
Thumbs.db
# ----------- NDK / 外部构建缓存 -----------
.externalNativeBuild
.cxx
*.hprof
=======
# ---> Android
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof
>>>>>>> 91bdec4aa6da285c4dd9a861e3b748515542f0b4

3
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name Normal file
View File

@ -0,0 +1 @@
MSDKSample

6
.idea/compiler.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

21
.idea/gradle.xml Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/android-sdk-v5-uxsdk" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

30
.idea/jarRepositories.xml Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

6
.idea/kotlinc.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.7.21" />
</component>
</project>

9
.idea/misc.xml Normal file
View File

@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,3 +1,3 @@
# makcar
马克图姆
马克图姆小车

View File

@ -0,0 +1,70 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion Integer.parseInt(project.ANDROID_COMPILE_SDK_VERSION)
resourcePrefix "uxsdk_"
defaultConfig {
minSdkVersion Integer.parseInt(project.ANDROID_MIN_SDK_VERSION)
targetSdkVersion Integer.parseInt(project.ANDROID_TARGET_SDK_VERSION)
versionCode 1
versionName "1.0"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions{
jvmTarget = JavaVersion.VERSION_1_8
freeCompilerArgs += ["-Xjvm-default=all"]
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation deps.annotation
implementation deps.appcompat
implementation deps.multidex
implementation deps.legacySupport
implementation deps.recyclerview
implementation deps.okio
implementation deps.wire
implementation deps.constraintLayout
implementation deps.lifecycleJava8
implementation deps.lifecycleRuntime
implementation deps.lifecycleProcess
implementation deps.media
implementation deps.kotlinLib
implementation deps.ktxCore
implementation deps.rx3Android
implementation deps.wpmzSdk
implementation deps.rx3Kt
implementation deps.lottie
implementation deps.cardview
implementation deps.mikepenzCommunityMaterial
implementation deps.mikepenzGoogleMaterial
implementation deps.mikepenzIconicsViews
implementation deps.mikepenzIconicsCore
implementation deps.mikepenzIonicons
implementation deps.material
api deps.maplibreTurf
api deps.maplibreSdk
api deps.playservicesplaces
api deps.playservicesmaps
api deps.playserviceslocation
api deps.playservicesbase
compileOnly deps.aircraftProvided
implementation deps.aircraft
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dji.v5.ux">
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
</manifest>

View File

@ -0,0 +1,95 @@
<!--
~ Copyright (c) 2018-2020 DJI
~
~ Permission is hereby granted, free of charge, to any person obtaining a copy
~ of this software and associated documentation files (the "Software"), to deal
~ in the Software without restriction, including without limitation the rights
~ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
~ copies of the Software, and to permit persons to whom the Software is
~ furnished to do so, subject to the following conditions:
~
~ The above copyright notice and this permission notice shall be included in all
~ copies or substantial portions of the Software.
~
~ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
~ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
~ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
~ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
~ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
~ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
~ SOFTWARE.
~
-->
<!DOCTYPE html>
<!-- saved from url=(0048)http://djistatic.com/agreement/dji-go-4-tos.html -->
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>DJI AirSense Warnings</title>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta name="description" content="DJI Pilot App Terms of Use">
<meta name="keywords" content="djigo4,goapp,dji">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="cleartype" content="on">
<style> *{margin:0;padding:0}body{font-size:14px;font-family:sans-serif;color:#555}h1{text-align:center;margin:30px 20px;color:#000;font-size:20px}a{color:#000}p{line-height:20px;margin:0 0 20px;word-break:break-all;font-size:14px}strong{font-weight:bold;color:#000}.content{width:80%;margin:0 auto}ol{padding-left:20px}li{margin-bottom:25px;line-height:20px}dt{font-weight:bold;margin-bottom:10px;color:#000}h3{margin-bottom:15px;color:#000}h4{font-weight:bold;color:#333;margin-bottom:10px}ul{padding-left:20px}@media (max-width:640px){.content{width:90%}}
</style>
</head>
<body>
<h1>DJI AirSense Warnings</h1>
<div class="content">
<ul>
<li>
DJI AirSense can detect nearby civil aircrafts and send out warnings only under certain circumstancesit will NOT control DJI aircraft to avoid other aircraft automatically. Make sure to fly with your aircraft within visual line of sight at all times, and always fly with caution. After receiving warnings, lower your aircraft to safe height. In addition, DJI AirSense has the following limitations:
</li>
</ul>
<ol>
<li>
<dl>
<dt>
DJI AirSense can only receive messages sent from civil aircraft equipped with an ADS-B out device under 1090ES (RTCA DO-260) or UAT (RTCA Do-282) standards. For civil aircraft without ADS-B outs or with malfunctioning ADS-B outs, DJI AirSense cannot receive related broadcasted messages or send out warnings.
</dt>
</dl>
</li>
<li>
<dl>
<dt>
When there are obstacles in between a civil aircraft and DJI aircraft, DJI AirSense will fail to receive ADS-B messages sent from civil aircraft or to send out warnings.
</dt>
</dl>
</li>
<li>
<dl>
<dt>
DJI AirSense may fail to receive ADS-B messages sent from civil aircraft or send out warnings due to ever changing circumstances and interference. It is highly recommended to fly with caution and stay aware of your surroundings during flight.
</dt>
</dl>
</li>
<li>
<dl>
<dt>
DJI AirSense cannot send out warnings when the DJI aircraft cannot accurately determine its location.
</dt>
</dl>
</li>
<li>
<dl>
<dt>
DJI AirSense cannot receive ADS-B messages sent from civil aircraft or send out warnings when it is disabled or misconfigured.
</dt>
</dl>
</li>
</ol>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 496 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -0,0 +1,200 @@
package dji.v5.ux.accessory
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.content.ContextCompat
import dji.v5.utils.common.LogUtils
import dji.v5.ux.R
import dji.v5.ux.core.extension.hide
import dji.v5.ux.core.extension.show
import java.util.ArrayList
/**
* Description :RTK坐标系和RTK服务类型选择器
*
* @author: Byte.Cai
* date : 2022/7/25
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
private const val TAG = "DescSpinnerCell"
open class DescSpinnerCell @kotlin.jvm.JvmOverloads constructor(
context: Context,
val attrs: AttributeSet? = null,
val defStyleAttr: Int = 0,
) : LinearLayout(context, attrs, defStyleAttr) {
private var mSpinner: Spinner? = null
private var mSummary: TextView? = null
private var mDesc: TextView? = null
private var mAdapter: ArrayAdapter<String>? = null
private var mSelectedPosition = 0
private var mSelectedListener: OnItemSelectedListener? = null
init {
initView()
initListener()
initAttrs()
}
private fun initView() {
LayoutInflater.from(context).inflate(R.layout.uxsdk_spinner_desc_layout, this, true)
mSpinner = findViewById(R.id.spinner)
mSummary = findViewById(R.id.summary)
mDesc = findViewById(R.id.desc)
mAdapter = object : ArrayAdapter<String>(context, R.layout.uxsdk_spinner_item_bord) {
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View? {
var rootView = super.getDropDownView(position, convertView, parent)
rootView?.let {
val view = rootView as TextView
// 右边的下拉图标
if (position != 0 && checkRightCompoundDrawable(view)) {
val rightDrawable = view.compoundDrawables[2].mutate()
rightDrawable.alpha = 0
view.setCompoundDrawables(null, null, rightDrawable, null)
}
if (mSelectedPosition == position) {
view.setTextColor(ContextCompat.getColor(context, R.color.uxsdk_white))
} else {
view.setTextColor(ContextCompat.getColor(context, R.color.uxsdk_white_75_percent))
}
}
return rootView
}
private fun checkRightCompoundDrawable(view: TextView?): Boolean {
return view?.compoundDrawables != null && view.compoundDrawables.size == 4 && view.compoundDrawables[2] != null
}
}
mAdapter?.setDropDownViewResource(R.layout.uxsdk_spinner_item_drop)
}
private fun initListener() {
// 这里需要设置不保存状态。否则在view被销毁重新加载并恢复时由于spinnerID相同导致value被复用
mSpinner?.isSaveEnabled = false
mSpinner?.adapter = mAdapter
mSpinner?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
LogUtils.i(TAG, "onItemSelected , mSelectedPosition=$mSelectedPosition,position=$position")
if (mSelectedPosition != position) {
mSpinner?.setSelection(position, true)
mSelectedListener?.onItemSelected(position)
mSelectedPosition = position
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
LogUtils.e(TAG, "onNothingSelected")
//不需要实现
}
}
}
fun initAttrs() {
val ta = context.theme.obtainStyledAttributes(attrs, R.styleable.DescSpinnerCell, defStyleAttr, 0)
if (ta.hasValue(R.styleable.DescSpinnerCell_uxsdk_summary)) {
mSummary?.show()
mSummary?.text = ta.getString(R.styleable.DescSpinnerCell_uxsdk_summary)
} else {
mSummary?.hide()
}
if (ta.hasValue(R.styleable.DescSpinnerCell_uxsdk_desc)) {
mDesc?.show()
mDesc?.text = ta.getString(R.styleable.DescSpinnerCell_uxsdk_desc)
} else {
mDesc?.hide()
}
if (ta.hasValue(R.styleable.DescSpinnerCell_uxsdk_entries)) {
var entries = ta.getTextArray(R.styleable.DescSpinnerCell_uxsdk_entries)
if (entries != null && entries.isNotEmpty()) {
val list: MutableList<String> = ArrayList(entries.size)
for (s in entries) {
list.add(s.toString())
}
setEntries(list)
}
}
}
interface OnItemSelectedListener {
fun onItemSelected(position: Int)
}
fun addOnItemSelectedListener(listener: OnItemSelectedListener?) {
mSelectedListener = listener
}
open fun setSummaryText(summaryText: String) {
if (mSummary?.text?.equals(summaryText) != true) {
mSummary?.show()
mSummary?.text = summaryText
}
}
open fun setSummaryText(summaryTextId: Int) {
mSummary?.show()
mSummary?.setText(summaryTextId)
}
open fun setSDescText(descText: String) {
if (mDesc?.text?.equals(descText) != true) {
mDesc?.show()
mDesc?.text = descText
}
}
open fun getDescText():TextView?{
return mDesc
}
open fun getSelectPosition() :Int{
return mSelectedPosition;
}
open fun setEntries(entries: List<String?>) {
mAdapter?.clear()
mAdapter?.addAll(entries)
}
fun select(position: Int) {
if (position >= 0 && position < mAdapter?.count ?: -1) {
mSpinner?.setSelection(position, true)
mSelectedPosition = position
}
invalidate()
}
override fun setEnabled(enable: Boolean) {
super.setEnabled(enable)
mSpinner?.isEnabled = enable
}
}

View File

@ -0,0 +1,369 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.accessory
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.AttributeSet
import android.widget.CompoundButton
import android.widget.Switch
import android.widget.TextView
import androidx.annotation.*
import androidx.core.content.res.use
import dji.v5.utils.common.DisplayUtil
import dji.v5.utils.common.LogUtils
import dji.v5.ux.R
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.processors.PublishProcessor
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.SchedulerProvider
import dji.v5.ux.core.base.widget.ConstraintLayoutWidget
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.extension.*
import dji.v5.ux.core.util.UxErrorHandle
private const val TAG = "RTKEnabledWidget"
/**
* This widget displays a switch that will enable or disable RTK.
*/
open class RTKEnabledWidget @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayoutWidget<RTKEnabledWidget.ModelState>(context, attrs, defStyleAttr), CompoundButton.OnCheckedChangeListener {
//region Fields
private val rtkTitleTextView: TextView = findViewById(R.id.textview_rtk_title)
private val rtkEnabledSwitch: Switch = findViewById(R.id.switch_rtk_enabled)
private val rtkEnabledDescriptionTextView: TextView = findViewById(R.id.textview_rtk_enabled_description)
private val uiUpdateStateProcessor: PublishProcessor<UIState> = PublishProcessor.create()
private val widgetModel by lazy {
RTKEnabledWidgetModel(
DJISDKModel.getInstance(),
ObservableInMemoryKeyedStore.getInstance())
}
/**
* Background of the title text
*/
var titleTextBackground: Drawable?
get() = rtkTitleTextView.background
set(value) {
rtkTitleTextView.background = value
}
/**
* Size of the title text
*/
var titleTextSize: Float
@Dimension
get() = rtkTitleTextView.textSize
set(@Dimension textSize) {
rtkTitleTextView.textSize = textSize
}
/**
* Color of the title text
*/
var titleTextColor: Int
@ColorInt
get() = rtkTitleTextView.textColor
set(@ColorInt textColor) {
rtkTitleTextView.textColor = textColor
}
/**
* The drawable resource for the RTK enabled switch's thumb
*/
var rtkEnabledSwitchThumbIcon: Drawable
@JvmName("getRTKEnabledSwitchThumbIcon")
get() = rtkEnabledSwitch.thumbDrawable
@JvmName("setRTKEnabledSwitchThumbIcon")
set(value) {
rtkEnabledSwitch.thumbDrawable = value
}
/**
* The drawable resource for the RTK enabled switch's track
*/
var rtkEnabledSwitchTrackIcon: Drawable
@JvmName("getRTKEnabledSwitchTrackIcon")
get() = rtkEnabledSwitch.trackDrawable
@JvmName("setRTKEnabledSwitchTrackIcon")
set(value) {
rtkEnabledSwitch.trackDrawable = value
}
/**
* The text color state list for the RTK enabled switch's track
*/
var rtkEnabledSwitchTrackColor: ColorStateList?
@RequiresApi(Build.VERSION_CODES.M)
@JvmName("getRTKEnabledSwitchTrackColor")
get() = rtkEnabledSwitch.trackTintList
@RequiresApi(Build.VERSION_CODES.M)
@JvmName("setRTKEnabledSwitchTrackColor")
set(value) {
rtkEnabledSwitch.trackTintList = value
}
/**
* Background of the description text
*/
var descriptionTextBackground: Drawable?
get() = rtkEnabledDescriptionTextView.background
set(value) {
rtkEnabledDescriptionTextView.background = value
}
/**
* Size of the description text
*/
var descriptionTextSize: Float
@Dimension
get() = rtkEnabledDescriptionTextView.textSize
set(@Dimension textSize) {
rtkEnabledDescriptionTextView.textSize = textSize
}
/**
* Color of the description text
*/
var descriptionTextColor: Int
@ColorInt
get() = rtkEnabledDescriptionTextView.textColor
set(@ColorInt textColor) {
rtkEnabledDescriptionTextView.textColor = textColor
}
//endregion
//region Constructor
override fun initView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
inflate(context, R.layout.uxsdk_widget_rtk_enabled, this)
}
init {
rtkEnabledSwitch.setOnCheckedChangeListener(this)
attrs?.let { initAttributes(context, it) }
}
//endregion
//region Lifecycle
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!isInEditMode) {
widgetModel.setup()
}
}
override fun onDetachedFromWindow() {
if (!isInEditMode) {
widgetModel.cleanup()
}
super.onDetachedFromWindow()
}
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
addDisposable(widgetModel.canEnableRTK.firstOrError()
.observeOn(SchedulerProvider.ui())
.subscribe({ canEnableRTK: Boolean ->
if (!canEnableRTK) {
setRTKSwitch(!isChecked)
showLongToast(R.string.uxsdk_rtk_enabled_motors_running)
} else {
setRTKEnabled(isChecked)
}
}, UxErrorHandle.logErrorConsumer(TAG, "canEnableRTK: ")))
uiUpdateStateProcessor.onNext(UIState.SwitchChanged(isChecked))
}
override fun reactToModelChanges() {
addReaction(widgetModel.rtkEnabled
.observeOn(SchedulerProvider.ui())
.subscribe { updateUIForRTKEnabled(it) })
addReaction(widgetModel.productConnection
.observeOn(SchedulerProvider.ui())
.subscribe { widgetStateDataProcessor.onNext(ModelState.ProductConnected(it)) })
}
//endregion
//region Reaction helpers
private fun updateUIForRTKEnabled(rtkEnabled: Boolean) {
setRTKSwitch(rtkEnabled)
widgetStateDataProcessor.onNext(ModelState.RTKEnabledUpdated(rtkEnabled))
}
private fun setRTKEnabled(enabled: Boolean) {
addDisposable(widgetModel.rtkEnabled
.firstOrError()
.observeOn(SchedulerProvider.ui())
.subscribe({ rtkEnabled: Boolean ->
if (rtkEnabled != enabled) {
addDisposable(toggleRTK(enabled))
}
}, UxErrorHandle.logErrorConsumer(TAG, "rtkEnabled: ")))
}
private fun toggleRTK(enabled: Boolean): Disposable {
return widgetModel.setRTKEnabled(enabled)
.observeOn(SchedulerProvider.ui())
.subscribe({}) { throwable: Throwable ->
setRTKSwitch(!enabled)
LogUtils.e(TAG, "setRTKEnabled: " + throwable.localizedMessage)
}
}
private fun setRTKSwitch(isChecked: Boolean) {
rtkEnabledSwitch.setOnCheckedChangeListener(null)
rtkEnabledSwitch.isChecked = isChecked
rtkEnabledSwitch.setOnCheckedChangeListener(this)
}
//endregion
//region Customization
override fun getIdealDimensionRatioString(): String {
return getString(R.string.uxsdk_widget_rtk_enabled_ratio)
}
/**
* Set text appearance of the title text
*
* @param textAppearanceResId Style resource for text appearance
*/
fun setTitleTextAppearance(@StyleRes textAppearanceResId: Int) {
rtkTitleTextView.setTextAppearance(context, textAppearanceResId)
}
/**
* Set the resource ID for the RTK enabled switch's thumb
*
* @param resourceId Integer ID of the drawable resource
*/
fun setRTKEnabledSwitchThumbIcon(@DrawableRes resourceId: Int) {
rtkEnabledSwitchThumbIcon = getDrawable(resourceId)
}
/**
* Set the resource ID for the RTK enabled switch's track
*
* @param resourceId Integer ID of the drawable resource
*/
fun setRTKEnabledSwitchTrackIcon(@DrawableRes resourceId: Int) {
rtkEnabledSwitchTrackIcon = getDrawable(resourceId)
}
/**
* Set text appearance of the description text
*
* @param textAppearanceResId Style resource for text appearance
*/
fun setDescriptionTextAppearance(@StyleRes textAppearanceResId: Int) {
rtkEnabledDescriptionTextView.setTextAppearance(context, textAppearanceResId)
}
@SuppressLint("Recycle")
private fun initAttributes(context: Context, attrs: AttributeSet) {
context.obtainStyledAttributes(attrs, R.styleable.RTKEnabledWidget).use { typedArray ->
typedArray.getResourceIdAndUse(R.styleable.RTKEnabledWidget_uxsdk_titleTextAppearance) {
setTitleTextAppearance(it)
}
typedArray.getDimensionAndUse(R.styleable.RTKEnabledWidget_uxsdk_titleTextSize) {
titleTextSize = DisplayUtil.pxToSp(context, it)
}
typedArray.getColorAndUse(R.styleable.RTKEnabledWidget_uxsdk_titleTextColor) {
titleTextColor = it
}
typedArray.getDrawableAndUse(R.styleable.RTKEnabledWidget_uxsdk_titleTextBackground) {
titleTextBackground = it
}
typedArray.getDrawableAndUse(R.styleable.RTKEnabledWidget_uxsdk_rtkEnabledSwitchThumbIcon) {
rtkEnabledSwitchThumbIcon = it
}
typedArray.getDrawableAndUse(R.styleable.RTKEnabledWidget_uxsdk_rtkEnabledSwitchTrackIcon) {
rtkEnabledSwitchTrackIcon = it
}
typedArray.getResourceIdAndUse(R.styleable.RTKEnabledWidget_uxsdk_descriptionTextAppearance) {
setDescriptionTextAppearance(it)
}
typedArray.getDimensionAndUse(R.styleable.RTKEnabledWidget_uxsdk_descriptionTextSize) {
descriptionTextSize = DisplayUtil.pxToSp(context, it)
}
typedArray.getColorAndUse(R.styleable.RTKEnabledWidget_uxsdk_descriptionTextColor) {
descriptionTextColor = it
}
typedArray.getDrawableAndUse(R.styleable.RTKEnabledWidget_uxsdk_descriptionTextBackground) {
descriptionTextBackground = it
}
}
}
//endregion
//region Hooks
/**
* Get the [UIState] updates
*/
fun getUIStateUpdates(): Flowable<UIState> {
return uiUpdateStateProcessor.onBackpressureBuffer()
}
/**
* Widget UI update State
*/
sealed class UIState {
/**
* RTK enabled switch check changed update
*/
data class SwitchChanged(val isChecked: Boolean) : UIState()
}
/**
* Get the [ModelState] updates
*/
@SuppressWarnings
override fun getWidgetStateUpdate(): Flowable<ModelState> {
return super.getWidgetStateUpdate()
}
/**
* Class defines the widget state updates
*/
sealed class ModelState {
/**
* Product connection update
*/
data class ProductConnected(val isConnected: Boolean) : ModelState()
/**
* RTK enabled update
*/
data class RTKEnabledUpdated(val isRTKEnabled: Boolean) : ModelState()
}
//endregion
}

View File

@ -0,0 +1,114 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.accessory
import dji.sdk.keyvalue.key.FlightControllerKey
import dji.sdk.keyvalue.key.DJIKey
import dji.sdk.keyvalue.key.RtkMobileStationKey
import dji.sdk.keyvalue.value.rtkmobilestation.RTKHomePointDataSource
import dji.sdk.keyvalue.value.rtkmobilestation.RTKHomePointInfo
import dji.sdk.keyvalue.value.rtkmobilestation.RTKTakeoffAltitudeInfo
import dji.sdk.keyvalue.key.KeyTools
import dji.v5.manager.KeyManager
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.WidgetModel
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.util.DataProcessor
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
/**
* Widget Model for the [RTKEnabledWidget] used to define
* the underlying logic and communication
*/
private const val TAG = "RTKEnabledWidgetModel"
class RTKEnabledWidgetModel(
djiSdkModel: DJISDKModel,
keyedStore: ObservableInMemoryKeyedStore
) : WidgetModel(djiSdkModel, keyedStore) {
//region Fields
private val isRTKEnabledKey: DJIKey<Boolean> = KeyTools.createKey(
RtkMobileStationKey.KeyRTKEnable)
private val isRTKEnabledProcessor: DataProcessor<Boolean> = DataProcessor.create(false)
private val isMotorOnProcessor: DataProcessor<Boolean> = DataProcessor.create(false)
private val homePointDataSourceProcessor: DataProcessor<RTKHomePointInfo> = DataProcessor.create(RTKHomePointInfo())
private val isRTKTakeoffHeightSetProcessor: DataProcessor<RTKTakeoffAltitudeInfo> = DataProcessor.create(RTKTakeoffAltitudeInfo())
private val canEnableRTKProcessor: DataProcessor<Boolean> = DataProcessor.create(true)
//endregion
//region Data
/**
* Get whether RTK is enabled.
*/
val rtkEnabled: Flowable<Boolean>
@JvmName("getRTKEnabled")
get() = isRTKEnabledProcessor.toFlowable()
/**
* Get whether RTK can be enabled.
*/
val canEnableRTK: Flowable<Boolean>
get() = canEnableRTKProcessor.toFlowable()
//endregion
//region Lifecycle
override fun inSetup() {
bindDataProcessor(isRTKEnabledKey, isRTKEnabledProcessor)
bindDataProcessor(
KeyTools.createKey(
FlightControllerKey.KeyAreMotorsOn), isMotorOnProcessor)
bindDataProcessor(
KeyTools.createKey(
RtkMobileStationKey.KeyRTKHomePointInfo), homePointDataSourceProcessor)
bindDataProcessor(
KeyTools.createKey(
RtkMobileStationKey.KeyRTKTakeoffAltitudeInfo), isRTKTakeoffHeightSetProcessor)
}
override fun inCleanup() {
KeyManager.getInstance().cancelListen(this)
}
/**
* RTK能否开启这里捆绑了几个逻辑
* 1只有在电机关闭时才可以设置RTK开启/关闭状态
* 2如果电机已开启则需要飞行高度已设置+返航点的类型设置为RTK这时才可以开启/关闭RTK
*/
override fun updateStates() {
canEnableRTKProcessor.onNext(
!isMotorOnProcessor.value || (isRTKTakeoffHeightSetProcessor.value.valid
&& homePointDataSourceProcessor.value.homePointDataSource == RTKHomePointDataSource.RTK)
)
}
//endregion
//region User interaction
fun setRTKEnabled(enabled: Boolean): Completable {
return djiSdkModel.setValue(isRTKEnabledKey, enabled)
}
//endregion
}

View File

@ -0,0 +1,106 @@
package dji.v5.ux.accessory
import android.content.Context
import android.util.AttributeSet
import android.widget.CompoundButton
import android.widget.Switch
import dji.v5.manager.aircraft.rtk.RTKCenter
import dji.v5.ux.R
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.SchedulerProvider
import dji.v5.ux.core.base.widget.ConstraintLayoutWidget
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.extension.getString
import dji.v5.ux.core.util.UxErrorHandle
import io.reactivex.rxjava3.core.CompletableObserver
import io.reactivex.rxjava3.disposables.Disposable
/**
* Description : This widget displays a switch that will enable or disable RTK Keep Status.
*
* @author: Byte.Cai
* date : 2022/7/11
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
private const val TAG = "RTKKeepStatusWidget"
class RTKKeepStatusWidget @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : ConstraintLayoutWidget<Boolean>(context, attrs, defStyleAttr), CompoundButton.OnCheckedChangeListener {
private val rtkKeepStatusSwitch: Switch = findViewById(R.id.rtk_keep_status_switch)
private val widgetModel by lazy {
RTKKeepStatusWidgetModel(
DJISDKModel.getInstance(),
ObservableInMemoryKeyedStore.getInstance(),
RTKCenter.getInstance()
)
}
override fun initView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
inflate(context, R.layout.uxsdk_widget_keep_status_enable_layout, this)
}
init {
rtkKeepStatusSwitch.setOnCheckedChangeListener(this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!isInEditMode) {
widgetModel.setup()
}
}
override fun onDetachedFromWindow() {
if (!isInEditMode) {
widgetModel.cleanup()
}
super.onDetachedFromWindow()
}
override fun reactToModelChanges() {
addReaction(widgetModel.rtkKeepStatusEnable
.observeOn(SchedulerProvider.ui())
.subscribe {
setRTKSwitch(it)
}
)
}
private fun setRTKSwitch(isChecked: Boolean) {
rtkKeepStatusSwitch.setOnCheckedChangeListener(null)
rtkKeepStatusSwitch.isChecked = isChecked
rtkKeepStatusSwitch.setOnCheckedChangeListener(this)
}
override fun getIdealDimensionRatioString(): String {
return getString(R.string.uxsdk_widget_rtk_keep_status_ratio)
}
override fun onCheckedChanged(buttonView: CompoundButton?, isChecked: Boolean) {
widgetModel.setRTKKeepStatusEnable(isChecked)
.observeOn(SchedulerProvider.ui())
.subscribe(object : CompletableObserver {
override fun onSubscribe(d: Disposable) {
//Do nothing
}
override fun onComplete() {
//Do nothing
}
override fun onError(e: Throwable) {
setRTKSwitch(!isChecked)
UxErrorHandle.logErrorConsumer(TAG, "canEnableRTK: ")
}
})
}
}

View File

@ -0,0 +1,67 @@
package dji.v5.ux.accessory
import dji.v5.common.callback.CommonCallbacks
import dji.v5.common.error.IDJIError
import dji.v5.common.error.RxError
import dji.v5.manager.aircraft.rtk.RTKSystemState
import dji.v5.manager.aircraft.rtk.RTKSystemStateListener
import dji.v5.manager.interfaces.IRTKCenter
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.WidgetModel
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.util.DataProcessor
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.schedulers.Schedulers
/**
* Description :Widget Model for the [RTKKeepStatusWidget] used to define
* the underlying logic and communication
*
* @author: Byte.Cai
* date : 2022/7/11
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
class RTKKeepStatusWidgetModel(
djiSdkModel: DJISDKModel,
uxKeyManager: ObservableInMemoryKeyedStore,
private val rtkCenter: IRTKCenter,
) : WidgetModel(djiSdkModel, uxKeyManager), RTKSystemStateListener {
private val isRTKKeepStatusEnabledProcessor: DataProcessor<Boolean> = DataProcessor.create(false)
override fun inSetup() {
rtkCenter.addRTKSystemStateListener(this)
}
override fun inCleanup() {
rtkCenter.removeRTKSystemStateListener(this)
}
val rtkKeepStatusEnable: Flowable<Boolean>
get() = isRTKKeepStatusEnabledProcessor.toFlowable()
fun setRTKKeepStatusEnable(enabled: Boolean): Completable {
return Completable.create {
rtkCenter.setRTKMaintainAccuracyEnabled(enabled, object : CommonCallbacks.CompletionCallback {
override fun onSuccess() {
it.onComplete()
}
override fun onFailure(error: IDJIError) {
it.onError(RxError(error))
}
})
}.subscribeOn(Schedulers.computation())
}
override fun onUpdate(rtkSystemState: RTKSystemState?) {
rtkSystemState?.let {
isRTKKeepStatusEnabledProcessor.onNext(it.rtkMaintainAccuracyEnabled)
}
}
}

View File

@ -0,0 +1,331 @@
package dji.v5.ux.accessory
import dji.sdk.keyvalue.value.rtkbasestation.RTKReferenceStationSource
import dji.sdk.keyvalue.value.rtkbasestation.RTKServiceState
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationConnetState
import dji.sdk.keyvalue.value.rtkmobilestation.RTKLocation
import dji.v5.common.error.IDJIError
import dji.v5.manager.aircraft.rtk.*
import dji.v5.manager.aircraft.rtk.network.INetworkServiceInfoListener
import dji.v5.manager.aircraft.rtk.station.RTKStationConnectStatusListener
import dji.v5.manager.interfaces.IRTKCenter
import dji.v5.utils.common.LogUtils
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.SchedulerProvider
import dji.v5.ux.core.base.WidgetModel
import dji.v5.ux.core.communication.GlobalPreferenceKeys
import dji.v5.ux.core.communication.GlobalPreferencesInterface
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.communication.UXKeys
import dji.v5.ux.core.util.DataProcessor
import dji.v5.ux.core.util.UnitConversionUtil
import io.reactivex.rxjava3.core.Flowable
/**
* Description :Widget Model for the [RTKSatelliteStatusWidget] used to define
* the underlying logic and communication
*
* @author: Byte.Cai
* date : 2022/5/23
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
class RTKSatelliteStatusWidgetModel(
djiSdkModel: DJISDKModel,
uxKeyManager: ObservableInMemoryKeyedStore,
private val preferencesManager: GlobalPreferencesInterface?,
private val rtkCenter: IRTKCenter,
) :
WidgetModel(djiSdkModel, uxKeyManager) {
private val TAG = LogUtils.getTag(this)
private val rtkLocationInfoProcessor: DataProcessor<RTKLocationInfo> = DataProcessor.create(RTKLocationInfo())
private val rtkSystemStateProcessor: DataProcessor<RTKSystemState> = DataProcessor.create(RTKSystemState())
private val rtkStationConnectStateProcessor: DataProcessor<RTKStationConnetState> =
DataProcessor.create(RTKStationConnetState.UNKNOWN)
private val rtkNetworkServiceInfoProcessor: DataProcessor<RTKServiceState> =
DataProcessor.create(RTKServiceState.UNKNOWN)
private val unitTypeProcessor: DataProcessor<UnitConversionUtil.UnitType> =
DataProcessor.create(UnitConversionUtil.UnitType.METRIC)
//rtk连接数据的封装Processor用于widget显示rtk是否连接和数据是否正在使用
private val rtkBaseStationStateProcessor: DataProcessor<RTKBaseStationState> =
DataProcessor.create(RTKBaseStationState.DISCONNECTED)
private val rtkNetworkServiceStateProcessor: DataProcessor<RTKNetworkServiceState> = DataProcessor.create(
RTKNetworkServiceState(
RTKServiceState.UNKNOWN,
isRTKBeingUsed = false,
isNetworkServiceOpen = false,
rtkSignal = RTKReferenceStationSource.UNKNOWN
)
)
//标准差
private val standardDeviationProcessor: DataProcessor<StandardDeviation> = DataProcessor.create(
StandardDeviation(
0f,
0f,
0f,
UnitConversionUtil.UnitType.METRIC
)
)
private val rtkLocationInfoListener = RTKLocationInfoListener {
rtkLocationInfoProcessor.onNext(it)
updateStandardDeviation(it.rtkLocation)
}
private val rtkSystemStateListener = RTKSystemStateListener {
rtkSystemStateProcessor.onNext(it)
//RTKSystemState涉及RTK服务类型的改变所以有关于RTK服务类型的都需要更新
updateRTKConnectionState()
updateRTKListener(it.rtkReferenceStationSource)
}
private val stationConnectStatusListener = RTKStationConnectStatusListener {
LogUtils.i(TAG, it)
rtkStationConnectStateProcessor.onNext(it)
updateRTKConnectionState()
}
private var mRTKServiceState = RTKServiceState.UNKNOWN
private val networkServiceInfoListener: INetworkServiceInfoListener = object :
INetworkServiceInfoListener {
override fun onServiceStateUpdate(state: RTKServiceState?) {
state?.let {
if (mRTKServiceState != state) {
LogUtils.i(TAG, "onServiceStateUpdate RTKServiceState=$state")
mRTKServiceState = state
rtkNetworkServiceInfoProcessor.onNext(it)
updateRTKConnectionState()
}
}
}
override fun onErrorCodeUpdate(error: IDJIError?) {
error?.let {
LogUtils.e(TAG, error.toString())
}
}
}
@get:JvmName("getRTKLocationInfo")
val rtkLocationInfo: Flowable<RTKLocationInfo>
get() = rtkLocationInfoProcessor.toFlowable()
@get:JvmName("getRTKSystemState")
val rtkSystemState: Flowable<RTKSystemState>
get() = rtkSystemStateProcessor.toFlowable()
/**
* Get the standard deviation of the location accuracy.
*/
val standardDeviation: Flowable<StandardDeviation>
get() = standardDeviationProcessor.toFlowable()
/**
* Get the state of the RTK base station.
*/
@get:JvmName("getRTKBaseStationState")
val rtkBaseStationState: Flowable<RTKBaseStationState>
get() = rtkBaseStationStateProcessor.toFlowable()
/**
* Get the state of the network service.
*/
@get:JvmName("getRTKNetworkServiceState")
val rtkNetworkServiceState: Flowable<RTKNetworkServiceState>
get() = rtkNetworkServiceStateProcessor.toFlowable()
//region Constructor
init {
if (preferencesManager != null) {
unitTypeProcessor.onNext(preferencesManager.unitType)
}
}
//endregion
override fun inSetup() {
rtkCenter.addRTKLocationInfoListener(rtkLocationInfoListener)
rtkCenter.addRTKSystemStateListener(rtkSystemStateListener)
rtkCenter.qxrtkManager.addNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.customRTKManager.addNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.cmccrtkManager.addNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.rtkStationManager.addRTKStationConnectStatusListener(stationConnectStatusListener)
//测试发现productConnection有时候返回true比较慢所以在其值返回也是要刷新依赖其者的状态
productConnection.observeOn(SchedulerProvider.ui()).subscribe {
updateRTKConnectionState()
}
val unitKey = UXKeys.create(GlobalPreferenceKeys.UNIT_TYPE)
bindDataProcessor(unitKey, unitTypeProcessor)
updateRTKConnectionState()
}
override fun inCleanup() {
rtkCenter.removeRTKLocationInfoListener(rtkLocationInfoListener)
rtkCenter.removeRTKSystemStateListener(rtkSystemStateListener)
rtkCenter.rtkStationManager.removeRTKStationConnectStatusListener(stationConnectStatusListener)
rtkCenter.qxrtkManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.customRTKManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.cmccrtkManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
}
private fun updateRTKListener(rtkSource: RTKReferenceStationSource) {
when (rtkSource) {
RTKReferenceStationSource.QX_NETWORK_SERVICE -> {
rtkCenter.customRTKManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.cmccrtkManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.rtkStationManager.removeRTKStationConnectStatusListener(stationConnectStatusListener)
rtkCenter.qxrtkManager.addNetworkRTKServiceInfoListener(networkServiceInfoListener)
}
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE -> {
rtkCenter.qxrtkManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.cmccrtkManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.rtkStationManager.removeRTKStationConnectStatusListener(stationConnectStatusListener)
rtkCenter.customRTKManager.addNetworkRTKServiceInfoListener(networkServiceInfoListener)
}
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE -> {
rtkCenter.qxrtkManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.customRTKManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.rtkStationManager.removeRTKStationConnectStatusListener(stationConnectStatusListener)
rtkCenter.cmccrtkManager.addNetworkRTKServiceInfoListener(networkServiceInfoListener)
}
RTKReferenceStationSource.BASE_STATION -> {
rtkCenter.qxrtkManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.customRTKManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.cmccrtkManager.removeNetworkRTKServiceInfoListener(networkServiceInfoListener)
rtkCenter.rtkStationManager.addRTKStationConnectStatusListener(stationConnectStatusListener)
}
else -> {
//do nothing
}
}
}
private fun updateStandardDeviation(rtkLocation: RTKLocation?) {
var stdLatitude = 0f
var stdLongitude = 0f
var stdAltitude = 0f
rtkLocation?.let {
if (unitTypeProcessor.value == UnitConversionUtil.UnitType.IMPERIAL) {
stdLatitude = UnitConversionUtil.convertMetersToFeet(it.stdLatitude.toFloat())
stdLongitude = UnitConversionUtil.convertMetersToFeet(it.stdLongitude.toFloat())
stdAltitude = UnitConversionUtil.convertMetersToFeet(it.stdAltitude.toFloat())
} else {
stdLatitude = it.stdLatitude.toFloat()
stdLongitude = it.stdLongitude.toFloat()
stdAltitude = it.stdAltitude.toFloat()
}
}
standardDeviationProcessor.onNext(
StandardDeviation(
stdLatitude,
stdLongitude,
stdAltitude,
unitTypeProcessor.value
)
)
}
/**
* Sends the latest network service state or base station state to the corresponding flowable.
*/
fun updateRTKConnectionState() {
rtkSystemStateProcessor.value.rtkReferenceStationSource?.let {
if (isNetworkServiceOpen(it)) {
updateNetworkServiceState()
} else {
updateBaseStationState()
}
}
}
/**
* The state of the network service.
*/
data class RTKNetworkServiceState(
val state: RTKServiceState?,
val isRTKBeingUsed: Boolean?,
val isNetworkServiceOpen: Boolean?,
@get:JvmName("getRTKSignal")
val rtkSignal: RTKReferenceStationSource?,
)
/**
* The state of the RTK base station
*/
enum class RTKBaseStationState {
/**
* The RTK base station is connected and in use.
*/
CONNECTED_IN_USE,
/**
* The RTK base station is connected and not in use.
*/
CONNECTED_NOT_IN_USE,
/**
* The RTK base station is disconnected.
*/
DISCONNECTED
}
/**
* The standard deviation of the location accuracy.
*/
data class StandardDeviation(
val latitude: Float,
val longitude: Float,
val altitude: Float,
val unitType: UnitConversionUtil.UnitType,
)
//region Helper methods
private fun updateNetworkServiceState() {
rtkNetworkServiceStateProcessor.onNext(
RTKNetworkServiceState(
rtkNetworkServiceInfoProcessor.value,
rtkSystemStateProcessor.value.rtkHealthy,
isNetworkServiceOpen(rtkSystemStateProcessor.value.rtkReferenceStationSource),
rtkSystemStateProcessor.value.rtkReferenceStationSource
)
)
}
private fun updateBaseStationState() {
if (rtkStationConnectStateProcessor.value == RTKStationConnetState.CONNECTED && productConnectionProcessor.value) {
if (rtkSystemStateProcessor.value.rtkHealthy) {
rtkBaseStationStateProcessor.onNext(RTKBaseStationState.CONNECTED_IN_USE)
} else {
rtkBaseStationStateProcessor.onNext(RTKBaseStationState.CONNECTED_NOT_IN_USE)
}
} else {
rtkBaseStationStateProcessor.onNext(RTKBaseStationState.DISCONNECTED)
}
}
private fun isNetworkServiceOpen(rtkSignal: RTKReferenceStationSource?): Boolean {
return rtkSignal == RTKReferenceStationSource.QX_NETWORK_SERVICE
|| rtkSignal == RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE
|| rtkSignal == RTKReferenceStationSource.NTRIP_NETWORK_SERVICE
}
}

View File

@ -0,0 +1,373 @@
package dji.v5.ux.accessory
import android.os.Handler
import android.widget.Toast
import dji.rtk.CoordinateSystem
import dji.sdk.keyvalue.key.*
import dji.sdk.keyvalue.value.product.ProductType
import dji.sdk.keyvalue.value.remotecontroller.RCMode
import dji.sdk.keyvalue.value.rtkbasestation.RTKCustomNetworkSetting
import dji.sdk.keyvalue.value.rtkbasestation.RTKReferenceStationSource
import dji.v5.common.callback.CommonCallbacks
import dji.v5.common.error.IDJIError
import dji.v5.common.utils.RxUtil
import dji.v5.et.create
import dji.v5.et.get
import dji.v5.manager.aircraft.rtk.RTKCenter
import dji.v5.manager.aircraft.rtk.RTKSystemStateListener
import dji.v5.utils.common.*
import dji.v5.ux.R
import dji.v5.ux.core.util.DataProcessor
import dji.v5.ux.core.util.ViewUtil
import io.reactivex.rxjava3.core.Flowable
import java.util.concurrent.atomic.AtomicBoolean
/**
* Description :用于实现自动重连RTK逻辑
*
* @author: Byte.Cai
* date : 2022/8/16
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
object RTKStartServiceHelper {
private const val TAG = "RTKStartServiceHelper"
private val rtkCenter = RTKCenter.getInstance()
private val qxRTKManager = RTKCenter.getInstance().qxrtkManager
private val customManager = RTKCenter.getInstance().customRTKManager
private val cmccRtkManager = RTKCenter.getInstance().cmccrtkManager
private var isStartByUser=false
private var productType: ProductType = ProductType.UNKNOWN
private var rtkDongleConnection = false
private var fcConnected = false
private val rtkModuleAvailableProcessor = DataProcessor.create(false)
private var rtkSource: RTKReferenceStationSource = RTKReferenceStationSource.UNKNOWN
private val isStartRTKing = AtomicBoolean(false)
private val isHasStartRTK = AtomicBoolean(false)
private val handle = Handler()
private val rtkSystemStateListener = RTKSystemStateListener {
if (rtkSource != it.rtkReferenceStationSource) {
rtkSource = it.rtkReferenceStationSource
LogUtils.i(TAG, "rtkSource change into:$rtkSource")
//避免未输入账号信息下去启动导致的失败
if (rtkSource != RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE) {
startRtkService()
}
}
}
init {
//观测RTK模块的连接情况
observerRTKNoduleAvailable()
//观测RTKSource的变化
rtkCenter.addRTKSystemStateListener(rtkSystemStateListener)
}
private fun observerRTKNoduleAvailable() {
RxUtil.addListener(
KeyTools.createKey(
ProductKey.KeyProductType), this).subscribe {
if (it != ProductType.UNRECOGNIZED && productType != it) {
LogUtils.i(TAG, "productType=$it")
productType = it
updateData()
}
}
RxUtil.addListener(
KeyTools.createKey(
RtkMobileStationKey.KeyIsRTKDongleConnect), this).subscribe {
if (rtkDongleConnection != it) {
LogUtils.i(TAG, "rtkDongleConnection=$it")
rtkDongleConnection = it
updateData()
}
}
RxUtil.addListener(
KeyTools.createKey(
FlightControllerKey.KeyConnection), this).subscribe {
if (fcConnected != it) {
LogUtils.i(TAG, "fcConnected=$it")
fcConnected = it
updateData()
}
}
}
@Synchronized
private fun updateData() {
val isRtkModuleAvailable = when (productType) {
ProductType.DJI_MAVIC_3_ENTERPRISE_SERIES ->
// 外接RTK的判断RTK Dongle连接状态
rtkDongleConnection && fcConnected
else ->
// 其他行业飞机内置RTK的飞机都是true
fcConnected
}
rtkModuleAvailableProcessor.onNext(isRtkModuleAvailable)
if (isRtkModuleAvailable && !isHasStartRTK.get()) {
startRtkService()
}
}
@Synchronized
fun startRtkService(isStartByUser:Boolean=false) {
this.isStartByUser =isStartByUser
LogUtils.i(TAG, "startRtkService")
if (!rtkModuleAvailableProcessor.value) {
LogUtils.e(TAG, "rtkModule is unAvailable,startRtkServiceIfNeed fail!")
return
}
if (!isNeedStartRtkNetworkService()) {
LogUtils.e(TAG, "don not need start rtk Service!")
return
}
LogUtils.i(TAG, "rtkSource=$rtkSource")
when (rtkSource) {
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE -> startRtkCustomNetworkService()
RTKReferenceStationSource.QX_NETWORK_SERVICE -> startQxRtkService()
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE -> startCMCCRtkService()
RTKReferenceStationSource.BASE_STATION -> {
LogUtils.i(TAG, "D-RTK2 固件底层已实现自动重连,不需要外部手动重连")
}
else -> {
LogUtils.e(TAG, "UnKnown rtkSource:$rtkSource")
}
}
}
@Synchronized
private fun startCMCCRtkService() {
val rtkNetworkCoordinateSystem = RTKUtil.getNetRTKCoordinateSystem(RTKReferenceStationSource.NTRIP_NETWORK_SERVICE)
setStartRTKState(true)
isHasStartRTK.set(false)
LogUtils.i(TAG, "startCMCCRtkService,rtkNetworkCoordinateSystem=$rtkNetworkCoordinateSystem")
if (rtkNetworkCoordinateSystem == null) {
setStartRTKState(false)
LogUtils.e(TAG, "getCMCCRtk CoordinateSystem == nullstartCMCCRtkService finish")
return
}
rtkNetworkCoordinateSystem.let {
LogUtils.i(TAG, "startCMCCRtkService,coordinateName=$rtkNetworkCoordinateSystem")
cmccRtkManager.stopNetworkRTKService(object : CommonCallbacks.CompletionCallback {
override fun onSuccess() {
cmccRtkManager.startNetworkRTKService(rtkNetworkCoordinateSystem, object :
CommonCallbacks.CompletionCallback {
override fun onSuccess() {
LogUtils.i(TAG, "startCMCCRtkService success")
setStartRTKState(false)
isHasStartRTK.set(true)
}
override fun onFailure(error: IDJIError) {
LogUtils.e(TAG, "startCMCCRtkService fail:rtkNetworkCoordinateSystem=$rtkNetworkCoordinateSystem,error=$error")
setStartRTKState(false)
isHasStartRTK.set(false)
if (isStartByUser) {
showToast(StringUtils.getResStr(R.string.uxsdk_rtk_setting_menu_setting_fail))
}
}
})
}
override fun onFailure(error: IDJIError) {
LogUtils.e(TAG, "stopNetworkRTKService fail:$error")
isHasStartRTK.set(false)
setStartRTKState(false)
}
})
}
}
private fun showToast(msg: String) {
ViewUtil.showToast(ContextUtil.getContext(), msg, Toast.LENGTH_SHORT)
}
/**
* 启动千寻RTK
*/
@Synchronized
private fun startQxRtkService() {
var rtkNetworkCoordinateSystem = RTKUtil.getNetRTKCoordinateSystem(RTKReferenceStationSource.QX_NETWORK_SERVICE)
LogUtils.i(TAG, "startQxRtkService rtkNetworkCoordinateSystem=$rtkNetworkCoordinateSystem")
if (rtkNetworkCoordinateSystem != null) {
startQxRtkService(rtkNetworkCoordinateSystem)
} else {
RTKCenter.getInstance().qxrtkManager.getNetworkRTKCoordinateSystem(object :
CommonCallbacks.CompletionCallbackWithParam<CoordinateSystem> {
override fun onSuccess(t: CoordinateSystem?) {
t?.let { startQxRtkService(it) }
}
override fun onFailure(error: IDJIError) {
//未实现
}
})
}
}
private fun startQxRtkService(coordinateSystem: CoordinateSystem) {
setStartRTKState(true)
isHasStartRTK.set(false)
qxRTKManager.stopNetworkRTKService(object : CommonCallbacks.CompletionCallback {
override fun onSuccess() {
qxRTKManager.startNetworkRTKService(coordinateSystem, object :
CommonCallbacks.CompletionCallback {
override fun onSuccess() {
LogUtils.i(TAG, "startQxRtkService success")
setStartRTKState(false)
isHasStartRTK.set(true)
}
override fun onFailure(error: IDJIError) {
LogUtils.e(TAG, "startQxRtkService fail:$error")
if (isStartByUser) {
showToast(StringUtils.getResStr(R.string.uxsdk_rtk_setting_menu_setting_fail))
}
setStartRTKState(false)
isHasStartRTK.set(false)
}
})
}
override fun onFailure(error: IDJIError) {
LogUtils.e(TAG, "stopNetworkRTKService fail:$error")
isHasStartRTK.set(false)
setStartRTKState(false)
}
})
}
/**
* 从本地缓存中获取自定义网络RTK配置信息启动自定义网络RTK
*/
@Synchronized
private fun startRtkCustomNetworkService() {
isHasStartRTK.set(false)
setStartRTKState(true)
LogUtils.i(TAG, "startRtkCustomNetworkService")
val rtkCustomNetworkSetting: RTKCustomNetworkSetting? = RTKUtil.getRtkCustomNetworkSetting()
if (rtkCustomNetworkSetting == null) {
setStartRTKState(false)
LogUtils.e(TAG, "get rtkCustomNetworkSetting == nullstartRtkCustomNetworkService finish")
return
}
rtkCustomNetworkSetting.let {
LogUtils.i(TAG, "rtkCustomNetworkSetting=$it")
customManager.stopNetworkRTKService(object : CommonCallbacks.CompletionCallback {
override fun onSuccess() {
customManager.customNetworkRTKSettings = it
customManager.startNetworkRTKService(object :
CommonCallbacks.CompletionCallback {
override fun onSuccess() {
LogUtils.i(TAG, "startRtkCustomNetworkService success")
setStartRTKState(false)
isHasStartRTK.set(true)
}
override fun onFailure(error: IDJIError) {
LogUtils.e(TAG, "startRtkCustomNetworkService fail:$error")
setStartRTKState(false)
isHasStartRTK.set(false)
if (isStartByUser) {
ViewUtil.showToast(ContextUtil.getContext(),R.string.uxsdk_rtk_setting_menu_customer_rtk_save_failed_tips,Toast.LENGTH_SHORT)
}
}
})
}
override fun onFailure(error: IDJIError) {
LogUtils.e(TAG, "stopNetworkRTKService fail:$error")
setStartRTKState(false)
isHasStartRTK.set(false)
}
})
}
}
/**
* 是否允许启动网络RTK 未判断网络数据模式
*/
private fun isNeedStartRtkNetworkService(): Boolean {
val isConnected: Boolean = FlightControllerKey.KeyConnection.create().get(false)
return (isConnected
&& isNetworkRTK(rtkSource)
&& NetworkUtils.isNetworkAvailable()
&& !isChannelB()
&& rtkModuleAvailableProcessor.value)
&& !isStartRTKing.get()
}
//后续供其他RTK相关Widget使用
val rtkModuleAvailable: Flowable<Boolean>
get() = rtkModuleAvailableProcessor.toFlowable()
/**
* 判断一个差分数据源是否是网络RTK
*/
fun isNetworkRTK(source: RTKReferenceStationSource?): Boolean {
return when (source) {
RTKReferenceStationSource.QX_NETWORK_SERVICE,
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE,
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE,
-> true
else -> false
}
}
/**
* 是否为B控非双控机型返回false
*/
fun isChannelB(): Boolean {
return RCMode.CHANNEL_B == RemoteControllerKey.KeyRcMachineMode.create().get(RCMode.UNKNOWN)
}
private fun setStartRTKState(isRTKStart: Boolean) {
if (isRTKStart) {
isStartRTKing.set(true)
handle.postDelayed({
LogUtils.e(TAG, "start rtk service timeout")
isStartRTKing.set(false)
}, 15 * 1000)
} else {
isStartRTKing.set(false)
handle.removeCallbacksAndMessages(null)
}
}
}

View File

@ -0,0 +1,393 @@
package dji.v5.ux.accessory
import android.content.Context
import android.os.Looper
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.TextPaint
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.util.AttributeSet
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationConnetState
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationInfo
import dji.v5.manager.aircraft.rtk.RTKCenter
import dji.v5.manager.aircraft.rtk.station.ConnectedRTKStationInfo
import dji.v5.utils.common.LogUtils
import dji.v5.utils.common.StringUtils
import dji.v5.ux.R
import dji.v5.ux.accessory.data.DJIRTKBaseStationConnectInfo
import dji.v5.ux.accessory.data.RtkStationScanAdapter
import dji.v5.ux.accessory.item.RtkGuidanceView
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.SchedulerProvider
import dji.v5.ux.core.base.widget.ConstraintLayoutWidget
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.extension.getString
import dji.v5.ux.core.extension.hide
import dji.v5.ux.core.extension.show
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import java.util.concurrent.TimeUnit
/**
* Description :D-RTK2 扫描连接Widget
*
* @author: Byte.Cai
* date : 2022/9/1
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
private const val TAG = "RTKStationConnectWidget"
class RTKStationConnectWidget @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : ConstraintLayoutWidget<Boolean>(context, attrs, defStyleAttr), RtkStationScanAdapter.OnItemClickListener, View.OnClickListener {
private var rtkStationScanAdapter: RtkStationScanAdapter
private val stationList = ArrayList<DJIRTKBaseStationConnectInfo>()
private val searchBt: Button = findViewById(R.id.bt_rtk_signal_search_again)
private val checkReasonTv: TextView = findViewById(R.id.tv_rtk_signal_problem_checked_reason)
private val searchIv: ImageView = findViewById(R.id.iv_rtk_signal_search_iv)
private val stationListView: RecyclerView = findViewById(R.id.rl_rtk_signal_searching_list)
private val stationScanningView: ConstraintLayout = findViewById(R.id.cl_rtk_has_found)
private val stationHasNotFoundView: ConstraintLayout = findViewById(R.id.cl_rtk_not_found)
private var connectState = RTKStationConnetState.UNKNOWN
private val scanHandler = android.os.Handler(Looper.getMainLooper())
private var scanTimeOutDisposable: Disposable? = null
private var isMotorOn = false
private var firstEnter = false
private val SCAN_TIME_OUT = 5.0
private val widgetModel by lazy {
RTKStationConnectWidgetModel(
DJISDKModel.getInstance(),
ObservableInMemoryKeyedStore.getInstance(),
RTKCenter.getInstance()
)
}
init {
//初始化stationTRTK列表
val layoutManager = LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)
stationListView.layoutManager = layoutManager
rtkStationScanAdapter = RtkStationScanAdapter(getContext(), stationList)
stationListView.adapter = rtkStationScanAdapter
searchBt.setOnClickListener(this)
searchIv.setOnClickListener(this)
rtkStationScanAdapter.setOnItemClickListener(this)
initCheckReasonContent()
}
override fun initView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
inflate(context, R.layout.uxsdk_widget_rtk_connect_status_layout, this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!isInEditMode) {
widgetModel.setup()
}
firstEnter = true
}
/**
* 初始化CheckReason View的属性
*/
private fun initCheckReasonContent() {
val reason: String = StringUtils.getResStr(R.string.uxsdk_rtk_base_station_not_found_reason)
val description: String = StringUtils.getResStr(R.string.uxsdk_rtk_connect_description)
//设置mCheckReasonTv的部分内容可点击属性
val spannableStringBuilder = SpannableStringBuilder()
spannableStringBuilder.append(reason).append(" ").append(description)
//mCheckReasonTv的部分内容点击事件
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(view: View) {
//RTK引导界面
val guidanceView = RtkGuidanceView(context)
guidanceView.showPopupWindow(view)
}
override fun updateDrawState(ds: TextPaint) {
ds.isUnderlineText = false
}
}
//点击范围为description部分
spannableStringBuilder.setSpan(clickableSpan, reason.length, spannableStringBuilder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
//设置可点击部分的字体颜色
val foregroundColorSpan = ForegroundColorSpan(resources.getColor(R.color.uxsdk_blue_highlight))
spannableStringBuilder.setSpan(foregroundColorSpan, reason.length + 1, spannableStringBuilder.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
//设置为超链接方式
checkReasonTv.movementMethod = LinkMovementMethod.getInstance()
checkReasonTv.text = spannableStringBuilder
}
override fun onDetachedFromWindow() {
if (!isInEditMode) {
widgetModel.cleanup()
}
super.onDetachedFromWindow()
}
override fun onClick(v: View?) {
when (v?.id) {
R.id.iv_rtk_signal_search_iv,
R.id.bt_rtk_signal_search_again,
-> startScanning()
}
}
override fun reactToModelChanges() {
addReaction(widgetModel.connectedRTKStationInfo.observeOn(SchedulerProvider.ui()).subscribe {
handleReconnectedStationInfo(it)
})
addReaction(widgetModel.isMotorOn.subscribe {
isMotorOn = it
})
addReaction(widgetModel.stationConnectStatus.observeOn(SchedulerProvider.ui()).subscribe {
updateConnectStatus(it)
})
addReaction(widgetModel.stationList.observeOn(SchedulerProvider.ui()).subscribe {
handleStationRTKList(it)
})
}
override fun getIdealDimensionRatioString(): String? {
return getString(R.string.uxsdk_widget_rtk_keep_status_ratio)
}
private fun updateRefreshUI(boolean: Boolean) {
//底部的重新扫描按钮是否可点击
searchBt.isClickable = boolean
searchBt.setBackgroundResource(if (boolean) R.drawable.uxsdk_bg_white_radius else R.drawable.uxsdk_bg_gray_radius)
searchIv.visibility = if (boolean) visibility else GONE
}
private fun handleStationRTKList(list: List<DJIRTKBaseStationConnectInfo>?) {
//过滤重复的数据,防止界面重新刷新
if (checkNeedUpdateUI(list)) {
stationList.clear()
LogUtils.i(TAG, "has found rtkclear stationList")
list?.let {
for (i in it) {
LogUtils.i(TAG, "stationName=${i.rtkStationName},signalLevel=${i.signalLevel}")
stationList.add(i)
}
}
searchIv.setImageResource(R.drawable.uxsdk_ic_refresh)
rtkStationScanAdapter.notifyDataSetChanged()
}
}
private fun updateConnectStatus(rtkBaseStationConnectState: RTKStationConnetState?) {
if (rtkBaseStationConnectState == null) {
return
}
LogUtils.i(TAG, "Current station status is $rtkBaseStationConnectState")
when (rtkBaseStationConnectState) {
RTKStationConnetState.IDLE,
RTKStationConnetState.UNKNOWN,
-> {
if (firstEnter) {
LogUtils.i(TAG, "first enterstartScanning auto")
firstEnter = false
updateConnectStatus(RTKStationConnetState.SCANNING)
//这里延时2秒发送扫描命令避免针对已连上RTK的设备固件底层会自动重连再次扫描固件自动重连逻辑会被破坏
scanHandler.postDelayed({
startScanning()
}, 2000)
} else {
searchIv.setImageResource(R.drawable.uxsdk_ic_refresh)
}
}
RTKStationConnetState.DISCONNECTED -> {
stationHasNotFoundView.hide()
stationScanningView.show()
searchIv.setImageResource(R.drawable.uxsdk_ic_refresh)
Toast.makeText(context, "Station has disconnected", Toast.LENGTH_SHORT).show()
}
RTKStationConnetState.SCANNING -> {
stationHasNotFoundView.hide()
stationScanningView.show()
searchIv.setImageResource(R.drawable.uxsdk_rotate_progress_circle)
LogUtils.i(TAG, "scan rtk ing...")
}
RTKStationConnetState.CONNECTED -> {
LogUtils.i(TAG, "rtk has connected")
stationHasNotFoundView.hide()
stationScanningView.show()
searchIv.setImageResource(R.drawable.uxsdk_ic_refresh)
//将连接成功的基站放在列表首页
if (stationList.remove(selectedRTKStationConnectInfo)) {
stationList.add(0, selectedRTKStationConnectInfo)
}
rtkStationScanAdapter.notifyDataSetChanged()
}
else -> {
stationHasNotFoundView.hide()
stationScanningView.show()
searchIv.setImageResource(R.drawable.uxsdk_ic_refresh)
}
}
connectState = rtkBaseStationConnectState
//返回连接状态更新UI
selectedRTKStationConnectInfo.refresh(rtkBaseStationConnectState)
//飞行过程中不允许点击扫描按钮
updateRefreshUI(!isMotorOn)
}
private fun checkNeedUpdateUI(list: List<RTKStationInfo>?): Boolean {
if (list?.size != stationList.size) {
return true
}
if (!stationList.containsAll(list) || !list.containsAll(stationList)) {
return true
}
return false
}
/**
* 选中某个基站,注意这里selectedRTKStationConnectInfo不能初始化为null
*/
private var selectedRTKStationConnectInfo: DJIRTKBaseStationConnectInfo = DJIRTKBaseStationConnectInfo()
override fun onItemClick(view: View?, position: Int) {
selectedRTKStationConnectInfo = stationList[position]
LogUtils.i(TAG, "click and connecting rtk:$selectedRTKStationConnectInfo")
//连接某一个基站时,将其他基站的连接状态重置为空闲,因为一次只能连接一个基站
for (stationInfo in stationList) {
stationInfo.connectStatus = RTKStationConnetState.IDLE
}
selectedRTKStationConnectInfo.refresh(RTKStationConnetState.CONNECTING)
startConnectStation(selectedRTKStationConnectInfo)
}
private fun DJIRTKBaseStationConnectInfo.refresh(connectState: RTKStationConnetState?) {
connectState?.let {
LogUtils.i(TAG, "connectState=$connectState")
this.connectStatus = it
}
rtkStationScanAdapter.notifyDataSetChanged()
}
private fun startConnectStation(selectedRTKStationConnectInfo: DJIRTKBaseStationConnectInfo) {
selectedRTKStationConnectInfo.run {
addDisposable(widgetModel.startConnectToRTKStation(baseStationId).observeOn(SchedulerProvider.ui()).subscribe({
LogUtils.i(TAG, "$rtkStationName connect success")
}, {
//连接失败,恢复未连接状态
selectedRTKStationConnectInfo.refresh(RTKStationConnetState.IDLE)
Toast.makeText(context, StringUtils.getResStr(R.string.uxsdk_rtk_base_station_connect_fail), Toast.LENGTH_SHORT).show()
LogUtils.e(TAG, "${selectedRTKStationConnectInfo.rtkStationName}connect fail")
}))
}
}
private fun startScanning() {
LogUtils.i(TAG, "startScanning now")
//添加计时器
scanTimeOut()
//清除已有的基站列表
stationList.clear()
rtkStationScanAdapter.notifyDataSetChanged()
//发送命令开始扫描
addDisposable(widgetModel.startSearchStationRTK().observeOn(SchedulerProvider.ui()).subscribe({
stationHasNotFoundView.hide()
stationScanningView.show()
}, {
stationHasNotFoundView.show()
stationScanningView.hide()
Toast.makeText(context, StringUtils.getResStr(R.string.uxsdk_rtk_base_station_search_false_and_try_again), Toast.LENGTH_SHORT).show()
LogUtils.e(TAG, "startSearchStationRTK fail:${it.localizedMessage}")
}))
//某些情况下不会返回状态,需要手动更新状态
updateConnectStatus(RTKStationConnetState.SCANNING)
}
private fun scanTimeOut() {
disposeTimeout(scanTimeOutDisposable)
scanTimeOutDisposable = Observable.timer(SCAN_TIME_OUT.toLong(), TimeUnit.SECONDS)
.observeOn(SchedulerProvider.ui()).subscribe({
if (!isHasFoundRTK()) {
LogUtils.e(TAG, "scanTimeOut ,stop search station RTK")
//扫描到基站列表超时则停止扫描同时展示RTK找不到的布局
stationHasNotFoundView.show()
stationScanningView.hide()
widgetModel.stopSearchStationRTK()
//停止扫码不会返回状态,需要手动更新状态
updateConnectStatus(RTKStationConnetState.IDLE)
} else {
LogUtils.i(TAG, "scan finishhas found rtk")
//扫描到基站则隐藏基站未找到的界面显示基站列表页同时更新对应刷新Image Icon为刷新图片
stationHasNotFoundView.hide()
stationScanningView.show()
//停止扫描
widgetModel.stopSearchStationRTK()
searchIv.setImageResource(R.drawable.uxsdk_ic_refresh)
}
}, {
LogUtils.e(TAG, it.localizedMessage)
disposeTimeout(scanTimeOutDisposable)
})
scanTimeOutDisposable?.let {
if (!it.isDisposed) {
addDisposable(it)
}
}
}
private fun isHasFoundRTK(): Boolean {
return connectState == RTKStationConnetState.CONNECTED || connectState == RTKStationConnetState.CONNECTING || stationList.isNotEmpty()
}
/**
* 关闭计时器
*/
private fun disposeTimeout(timeOutDisposable: Disposable?) {
timeOutDisposable?.let {
if (!it.isDisposed) {
it.dispose()
}
}
}
private fun handleReconnectedStationInfo(infoConnected: ConnectedRTKStationInfo?) {
infoConnected?.run {
//第一次连接过基站后再次重启飞机或者重启App固件会帮忙自动连接基站。这里就是为了构建自动重连的基站信息
if (selectedRTKStationConnectInfo.baseStationId == 0) {
LogUtils.i(TAG, "RTK Station has reconnected and remove scanHandler message")
selectedRTKStationConnectInfo = DJIRTKBaseStationConnectInfo(stationId, signalLevel, stationName, RTKStationConnetState.CONNECTED)
scanHandler.removeCallbacksAndMessages(null)
updateConnectStatus(RTKStationConnetState.CONNECTED)
handleStationRTKList(arrayListOf(selectedRTKStationConnectInfo))
}
}
}
}

View File

@ -0,0 +1,161 @@
package dji.v5.ux.accessory
import dji.sdk.keyvalue.key.FlightControllerKey
import dji.sdk.keyvalue.key.KeyTools
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationConnetState
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationInfo
import dji.v5.common.callback.CommonCallbacks
import dji.v5.common.error.IDJIError
import dji.v5.common.error.RxError
import dji.v5.manager.aircraft.rtk.station.ConnectedRTKStationInfo
import dji.v5.manager.aircraft.rtk.station.ConnectedRTKStationInfoListener
import dji.v5.manager.aircraft.rtk.station.RTKStationConnectStatusListener
import dji.v5.manager.aircraft.rtk.station.SearchRTKStationListener
import dji.v5.manager.interfaces.IRTKCenter
import dji.v5.ux.accessory.data.DJIRTKBaseStationConnectInfo
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.SchedulerProvider
import dji.v5.ux.core.base.WidgetModel
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.util.DataProcessor
import io.reactivex.rxjava3.core.*
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import java.util.ArrayList
/**
* Description :Widget Model for the [RTKStationConnectWidget] used to define
* the underlying logic and communication
*
* @author: Byte.Cai
* date : 2022/9/1
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
class RTKStationConnectWidgetModel(
djiSdkModel: DJISDKModel,
keyedStore: ObservableInMemoryKeyedStore,
val rtkCenter: IRTKCenter,
) : WidgetModel(djiSdkModel, keyedStore) {
private val rtkStationManager = rtkCenter.rtkStationManager
private val rtkStationConnectStateProcessor: DataProcessor<RTKStationConnetState> =
DataProcessor.create(RTKStationConnetState.UNKNOWN)
private val stationListProcessor: DataProcessor<ArrayList<DJIRTKBaseStationConnectInfo>> =
DataProcessor.create(arrayListOf())
private val connectedRTKStationInfoProcessor: BehaviorProcessor<ConnectedRTKStationInfo> =
BehaviorProcessor.create()
private val isMotorOnProcessor: DataProcessor<Boolean> = DataProcessor.create(false)
private val stationConnectStatusListener =
RTKStationConnectStatusListener { rtkBaseStationConnectState ->
rtkStationConnectStateProcessor.onNext(rtkBaseStationConnectState)
}
private val connectedRTKStationInfoListener =
ConnectedRTKStationInfoListener {
connectedRTKStationInfoProcessor.onNext(it)
}
private val searchStationListener =
SearchRTKStationListener { newConnectInfoList ->
val convertToDJIRTKBaseStationConnectInfo = convertToDJIRTKBaseStationConnectInfo(newConnectInfoList)
stationListProcessor.onNext(convertToDJIRTKBaseStationConnectInfo)
}
val isMotorOn: Flowable<Boolean>
get() = isMotorOnProcessor.toFlowable()
val stationConnectStatus: Flowable<RTKStationConnetState>
get() = rtkStationConnectStateProcessor.toFlowable()
val connectedRTKStationInfo: Flowable<ConnectedRTKStationInfo>
get() = connectedRTKStationInfoProcessor.observeOn(SchedulerProvider.computation()).onBackpressureLatest()
val stationList: Flowable<ArrayList<DJIRTKBaseStationConnectInfo>>
get() = stationListProcessor.toFlowable()
override fun inSetup() {
bindDataProcessor(
KeyTools.createKey(
FlightControllerKey.KeyAreMotorsOn), isMotorOnProcessor)
rtkStationManager.addRTKStationConnectStatusListener(stationConnectStatusListener)
rtkStationManager.addConnectedRTKStationInfoListener(connectedRTKStationInfoListener)
rtkStationManager.addSearchRTKStationListener(searchStationListener)
}
override fun inCleanup() {
rtkStationManager.removeRTKStationConnectStatusListener(stationConnectStatusListener)
rtkStationManager.removeConnectedRTKStationInfoListener(connectedRTKStationInfoListener)
rtkStationManager.removeSearchRTKStationListener(searchStationListener)
}
fun startConnectToRTKStation(stationId: Int): Single<Boolean> {
return Single.create(SingleOnSubscribe<Boolean> {
rtkStationManager.startConnectToRTKStation(stationId, object :
CommonCallbacks.CompletionCallback {
override fun onSuccess() {
it.onSuccess(true)
}
override fun onFailure(error: IDJIError) {
it.onError(RxError(error))
}
})
}).subscribeOn(Schedulers.computation())
}
fun startSearchStationRTK(): Single<Boolean> {
return Single.create(SingleOnSubscribe<Boolean> {
rtkStationManager.startSearchRTKStation(object :
CommonCallbacks.CompletionCallback {
override fun onSuccess() {
it.onSuccess(true)
}
override fun onFailure(error: IDJIError) {
it.onError(RxError(error))
}
})
}).subscribeOn(Schedulers.computation())
}
fun stopSearchStationRTK(): Single<Boolean> {
return Single.create(SingleOnSubscribe<Boolean> {
rtkStationManager.startSearchRTKStation(object :
CommonCallbacks.CompletionCallback {
override fun onSuccess() {
it.onSuccess(true)
}
override fun onFailure(error: IDJIError) {
it.onError(RxError(error))
}
})
}).subscribeOn(Schedulers.computation())
}
private fun convertToDJIRTKBaseStationConnectInfo(list: List<RTKStationInfo>?): ArrayList<DJIRTKBaseStationConnectInfo> {
val djiRTKBaseStationConnectInfoList = ArrayList<DJIRTKBaseStationConnectInfo>()
list?.let {
for (i in list) {
val djirtkBaseStationConnectInfo = i.toDJIRTKBaseStationConnectInfo()
djiRTKBaseStationConnectInfoList.add(djirtkBaseStationConnectInfo)
}
}
return djiRTKBaseStationConnectInfoList
}
private fun RTKStationInfo.toDJIRTKBaseStationConnectInfo(): DJIRTKBaseStationConnectInfo {
val baseStationId = this.stationId
val name = this.stationName
val signalLevel = this.signalLevel
return DJIRTKBaseStationConnectInfo(baseStationId, signalLevel, name)
}
}

View File

@ -0,0 +1,447 @@
package dji.v5.ux.accessory
import android.content.Context
import android.text.TextUtils
import android.util.AttributeSet
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import dji.rtk.CoordinateSystem
import dji.sdk.keyvalue.value.rtkbasestation.RTKCustomNetworkSetting
import dji.sdk.keyvalue.value.rtkbasestation.RTKReferenceStationSource
import dji.v5.common.callback.CommonCallbacks
import dji.v5.common.error.IDJIError
import dji.v5.manager.aircraft.rtk.RTKCenter
import dji.v5.manager.areacode.AreaCodeManager
import dji.v5.utils.common.LogUtils
import dji.v5.utils.common.StringUtils
import dji.v5.ux.R
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.SchedulerProvider
import dji.v5.ux.core.base.widget.ConstraintLayoutWidget
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.extension.getString
import dji.v5.ux.core.extension.hide
import dji.v5.ux.core.extension.isFastClick
import dji.v5.ux.core.extension.show
import dji.v5.ux.core.util.ViewUtil
import dji.v5.ux.util.RtkSettingWatcher
/**
* Description :基站启动页
*
* @author: Byte.Cai
* date : 2022/8/1
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
private const val TAG = "RTKTypeSwitchWidget"
open class RTKTypeSwitchWidget @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : ConstraintLayoutWidget<Boolean>(context, attrs, defStyleAttr), RtkSettingWatcher.OnEditTextEmptyChangedListener {
private val rtkTypeCell: DescSpinnerCell = findViewById(R.id.cell_rtk_type)
private val coordinateSystemCell: DescSpinnerCell = findViewById(R.id.cell_coordinate_system)
private val edHost: TextView = findViewById(R.id.net_rtk_ntrip_host)
private val edPort: TextView = findViewById(R.id.net_rtk_ntrip_port)
private val edUser: TextView = findViewById(R.id.net_rtk_ntrip_user)
private val edMountPoint: TextView = findViewById(R.id.net_rtk_ntrip_mountpoint)
private val edPassword: TextView = findViewById(R.id.net_rtk_ntrip_pwd)
private val btSaveRtkInfo: Button = findViewById(R.id.btn_set_net_rtk_info)
private val customSetting: LinearLayout = findViewById(R.id.ll_rtk_custom_detail_view)
private var rtkSourceList: List<RTKReferenceStationSource> = ArrayList()
private var coordinateSystemList: List<CoordinateSystem> = arrayListOf()
private var isMotorsOn = false
private var currentRTKSource: RTKReferenceStationSource = RTKReferenceStationSource.UNKNOWN
private val textWatcher = RtkSettingWatcher(this)
private val INITIAL_INDEX: Int = -1
private var lastSelectedRTKTypeIndex: Int = INITIAL_INDEX
private var lastSelectedCoordinateSystemIndex: Int = INITIAL_INDEX
private val widgetModel by lazy {
RTKTypeSwitchWidgetModel(
DJISDKModel.getInstance(),
ObservableInMemoryKeyedStore.getInstance(),
AreaCodeManager.getInstance(), RTKCenter.getInstance())
}
private val rtkTypeSelectListener = object : DescSpinnerCell.OnItemSelectedListener {
override fun onItemSelected(position: Int) {
if (position == lastSelectedRTKTypeIndex) {
return
}
// B控不可以打开网络RTK。
if (RTKStartServiceHelper.isChannelB() && RTKStartServiceHelper.isNetworkRTK(rtkSourceList[position])) {
//回滚之前的选择,并提示用户
rtkTypeCell.select(lastSelectedRTKTypeIndex)
Toast.makeText(getContext(), getTip(position), Toast.LENGTH_SHORT).show()
return
}
//电机起转后,并且选择过服务类型则不允许再次切换RTK类型
if (isMotorsOn && lastSelectedRTKTypeIndex != -1) {
rtkTypeCell.select(lastSelectedRTKTypeIndex)
val tip = StringUtils.getResStr(R.string.uxsdk_rtk_setting_menu_esc_beeping_tip)
Toast.makeText(getContext(), tip, Toast.LENGTH_SHORT).show()
return
}
setRTKType(position)
}
}
private val coordinateSelectListener = object : DescSpinnerCell.OnItemSelectedListener {
override fun onItemSelected(position: Int) {
if (position == lastSelectedCoordinateSystemIndex || coordinateSystemList.isEmpty() || position >= coordinateSystemList.size || position < 0) {
return
}
lastSelectedCoordinateSystemIndex = position
val coordinate = coordinateSystemList[position]
RTKUtil.saveRTKCoordinateSystem(currentRTKSource, coordinate)
LogUtils.i(TAG, "select:$coordinate, reStartRtkService now!(Thread.currentThread().name${Thread.currentThread().name})")
RTKStartServiceHelper.startRtkService(true)
}
}
init {
edHost.addTextChangedListener(textWatcher)
edUser.addTextChangedListener(textWatcher)
edPassword.addTextChangedListener(textWatcher)
edMountPoint.addTextChangedListener(textWatcher)
edPort.addTextChangedListener(textWatcher)
btSaveRtkInfo.setOnClickListener {
if (!btSaveRtkInfo.isFastClick()) {
saveRtkCustomUserInfo()
}
}
//读取默认的配置并启动RTK
LogUtils.i(TAG, "RTKTypeSwitchWidget init,startRtkService now!(Thread.currentThread().name=${Thread.currentThread().name})")
RTKStartServiceHelper.startRtkService()
}
override fun initView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
inflate(context, R.layout.uxsdk_widget_rtk_type_switch, this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!isInEditMode) {
widgetModel.setup()
}
}
override fun onDetachedFromWindow() {
if (!isInEditMode) {
widgetModel.cleanup()
}
edHost.removeTextChangedListener(textWatcher)
edUser.removeTextChangedListener(textWatcher)
edMountPoint.removeTextChangedListener(textWatcher)
edPort.removeTextChangedListener(textWatcher)
edPassword.removeTextChangedListener(textWatcher)
super.onDetachedFromWindow()
}
override fun reactToModelChanges() {
addReaction(widgetModel.isMotorsOn.subscribe {
isMotorsOn = it
})
addReaction(widgetModel.rtkSource.observeOn(SchedulerProvider.ui()).subscribe {
LogUtils.i(TAG, "currentRTKSource=$it")
//获取RTK服务类型并切换UI
currentRTKSource = it
updateRTKView()
initDefaultNetRtkUI()
})
addReaction(widgetModel.supportReferenceStationList.observeOn(SchedulerProvider.ui()).subscribe {
//更新支持的RTK列表
if (it.isNotEmpty() && !rtkSourceList.containsAll(it)) {
LogUtils.i(TAG, "supportReferenceStationList=$it")
rtkSourceList = it
val referenceStationSourceNames = getReferenceStationSourceNames(it)
rtkTypeCell.setEntries(referenceStationSourceNames)
rtkTypeCell.addOnItemSelectedListener(rtkTypeSelectListener)
initDefaultNetRtkUI()
}
})
addReaction(widgetModel.coordinateSystemList.observeOn(SchedulerProvider.ui()).subscribe {
LogUtils.i(TAG, "coordinateSystemList=$it")
//更新坐标系
if (it.isNotEmpty() && !coordinateSystemList.containsAll(it)) {
LogUtils.i(TAG, "coordinateSystemList=$it")
coordinateSystemList = it
coordinateSystemCell.addOnItemSelectedListener(null)
coordinateSystemCell.setEntries(getCoordinateSystemName(it))
coordinateSystemCell.addOnItemSelectedListener(coordinateSelectListener)
initDefaultNetRtkUI()
}
})
//进入App后初始化用户上次选择
initDefaultCustomSetting()
}
override fun getIdealDimensionRatioString(): String? {
return getString(R.string.uxsdk_widget_rtk_keep_status_ratio)
}
private fun setRTKType(position: Int) {
if (rtkSourceList.isEmpty() || position >= rtkSourceList.size || position < 0) {
return
}
val rtkSource: RTKReferenceStationSource = rtkSourceList[position]
LogUtils.i(TAG, "selected $rtkSource")
rtkTypeCell.isEnabled = false
RTKCenter.getInstance().setRTKReferenceStationSource(rtkSource, object : CommonCallbacks.CompletionCallback {
override fun onSuccess() {
lastSelectedRTKTypeIndex = position
rtkTypeCell.isEnabled = true
}
override fun onFailure(error: IDJIError) {
rtkTypeCell.isEnabled = true
//切换RTK服务类型失败回滚到上次选择
for ((index, source) in rtkSourceList.withIndex()) {
if (source == currentRTKSource) {
rtkTypeCell.select(index)
}
}
}
})
}
private fun updateRTKView() {
val rtkSwitchDec = StringUtils.getResStr(R.string.uxsdk_rtk_setting_menu_switch_des_info)
var rtkSwitchDecDetail = ""
when (currentRTKSource) {
RTKReferenceStationSource.BASE_STATION -> {
customSetting.hide()
coordinateSystemCell.hide()
rtkSwitchDecDetail = StringUtils.getResStr(R.string.uxsdk_rtk_setting_menu_base_gps_input_desc)
}
RTKReferenceStationSource.QX_NETWORK_SERVICE,
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE,
-> {
if (!RTKStartServiceHelper.isChannelB()) {
customSetting.hide()
coordinateSystemCell.show()
} else {
customSetting.hide()
coordinateSystemCell.hide()
}
rtkSwitchDecDetail = StringUtils.getResStr(R.string.uxsdk_rtk_setting_menu_station_net_rtk_desc)
}
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE -> {
if (!RTKStartServiceHelper.isChannelB()) {
customSetting.show()
coordinateSystemCell.hide()
} else {
customSetting.hide()
coordinateSystemCell.hide()
}
rtkSwitchDecDetail = StringUtils.getResStr(R.string.uxsdk_rtk_setting_menu_station_net_rtk_desc)
}
else -> {
customSetting.hide()
coordinateSystemCell.hide()
}
}
if (TextUtils.isEmpty(rtkSwitchDecDetail)) {
rtkTypeCell.setSDescText(rtkSwitchDec)
} else {
rtkTypeCell.setSDescText(rtkSwitchDec + "\n" + rtkSwitchDecDetail)
}
}
private fun getReferenceStationSourceNames(list: List<RTKReferenceStationSource>): List<String> {
return list.map {
val res = when (it) {
RTKReferenceStationSource.BASE_STATION ->
R.string.uxsdk_rtk_setting_menu_type_rtk_station
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE ->
R.string.uxsdk_rtk_setting_menu_type_custom_rtk
RTKReferenceStationSource.QX_NETWORK_SERVICE ->
R.string.uxsdk_rtk_setting_menu_type_qx_rtk
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE ->
R.string.uxsdk_rtk_setting_menu_type_cmcc_rtk
else ->
R.string.uxsdk_rtk_setting_menu_type_rtk_none
}
StringUtils.getResStr(res)
}
}
private fun getCoordinateSystemName(list: List<CoordinateSystem>): List<String> {
return list.map {
val coordinateName: String = when (it) {
CoordinateSystem.CGCS2000 ->
CoordinateSystem.CGCS2000.name
CoordinateSystem.WGS84 ->
CoordinateSystem.WGS84.name
else -> {
LogUtils.e(TAG, "UnSupport CoordinateSystem:$it")
CoordinateSystem.UNKNOWN.name
}
}
coordinateName
}
}
private fun getTip(position: Int): String {
return if (rtkSourceList[position] == RTKReferenceStationSource.QX_NETWORK_SERVICE || rtkSourceList[position] == RTKReferenceStationSource.NTRIP_NETWORK_SERVICE) {
StringUtils.getResStr(R.string.uxsdk_rtk_channel_b_not_support_net_rtk)
} else {
StringUtils.getResStr(R.string.uxsdk_rtk_channel_b_not_support_net_custom_rtk)
}
}
override fun `isTextEmptyChanged`() {
//检查RTK设置项的填写状态并更新设置button的状态
var shouldEnableButton = true
if (TextUtils.isEmpty(edUser.text)) {
shouldEnableButton = false
}
if (TextUtils.isEmpty(edHost.text)) {
shouldEnableButton = false
}
if (TextUtils.isEmpty(edMountPoint.text)) {
shouldEnableButton = false
}
if (TextUtils.isEmpty(edPort.text)) {
shouldEnableButton = false
}
if (TextUtils.isEmpty(edPassword.text)) {
shouldEnableButton = false
}
//根据各项填写情况,更新保存按钮的值
if (shouldEnableButton) {
btSaveRtkInfo.isEnabled = true
btSaveRtkInfo.setTextColor(resources.getColor(R.color.uxsdk_setting_menu_rtk_tiny_green))
} else {
btSaveRtkInfo.isEnabled = false
btSaveRtkInfo.setTextColor(resources.getColor(R.color.uxsdk_setting_menu_rtk_txt_gray))
}
}
private fun saveRtkCustomUserInfo() {
// 检查输入是否有效
var isHostValid = true
var isPortValid = true
var isUserValid = true
var isPwdValid = true
var isMountPointValid = true
val host: String = edHost.text.toString()
if (TextUtils.isEmpty(host)) {
isHostValid = false
}
val portStr: String = edPort.text.toString()
var port = -1
if (!TextUtils.isEmpty(portStr) && TextUtils.isDigitsOnly(portStr)) {
port = portStr.toInt()
if (port < 0) {
isPortValid = false
}
} else {
isPortValid = false
}
val user: String = edUser.text.toString()
if (TextUtils.isEmpty(user)) {
isUserValid = false
}
val pw: String = edPassword.text.toString()
if (TextUtils.isEmpty(pw)) {
isPwdValid = false
}
val mountPint: String = edMountPoint.text.toString()
if (TextUtils.isEmpty(mountPint)) {
isMountPointValid = false
}
// 参数无效select设为true时EditText边框设为红色
edHost.isSelected = !isHostValid
edPort.isSelected = !isPortValid
edUser.isSelected = !isUserValid
edPassword.isSelected = !isPwdValid
edMountPoint.isSelected = !isMountPointValid
val isParamsValid = (isHostValid && isMountPointValid
&& isPortValid && isUserValid && isPwdValid)
if (!isParamsValid) {
ViewUtil.showToast(context, R.string.uxsdk_rtk_setting_menu_customer_rtk_save_failed_tips, Toast.LENGTH_SHORT)
return
}
startRtkCustomNetwork(host, port, user, pw, mountPint)
}
private fun startRtkCustomNetwork(host: String, port: Int, user: String, pw: String, mountPint: String) {
val rtkSetting = RTKCustomNetworkSetting()
rtkSetting.serverAddress = host
rtkSetting.port = port
rtkSetting.userName = user
rtkSetting.password = pw
rtkSetting.mountPoint = mountPint
RTKUtil.saveRtkCustomNetworkSetting(rtkSetting)
LogUtils.i(TAG, "rtkSetting=$rtkSetting,startRtkCustomNetwork now!(Thread.currentThread().name${Thread.currentThread().name})")
RTKStartServiceHelper.startRtkService(true)
}
/**
* 初始化用户设置的自定义网络RTK设置的信息
*/
private fun initDefaultCustomSetting() {
RTKUtil.getRtkCustomNetworkSetting()?.run {
LogUtils.i(TAG, "getRtkCustomNetworkSetting=$this")
edMountPoint.text = mountPoint
edHost.text = serverAddress
edPassword.text = password
edUser.text = userName
edPort.text = port.toString()
}
isTextEmptyChanged()
}
private fun initDefaultNetRtkUI() {
//初始化用户上次选择的RTK服务类型
if (currentRTKSource != RTKReferenceStationSource.UNKNOWN && rtkSourceList.isNotEmpty()) {
for ((index, rtkSource) in rtkSourceList.withIndex()) {
if (rtkSource == currentRTKSource) {
lastSelectedRTKTypeIndex = index
rtkTypeCell.select(index)
}
}
}
//初始化用户上次选择的坐标系
val netRTKCoordinateSystem = RTKUtil.getNetRTKCoordinateSystem(currentRTKSource)
for ((index, coordinate) in coordinateSystemList.withIndex()) {
if (netRTKCoordinateSystem == coordinate) {
lastSelectedCoordinateSystemIndex = index
coordinateSystemCell.select(index)
}
}
}
}

View File

@ -0,0 +1,201 @@
package dji.v5.ux.accessory
import dji.rtk.CoordinateSystem
import dji.sdk.keyvalue.key.FlightControllerKey
import dji.sdk.keyvalue.key.KeyTools
import dji.sdk.keyvalue.key.ProductKey
import dji.sdk.keyvalue.utils.ProductUtil
import dji.sdk.keyvalue.value.product.ProductType
import dji.sdk.keyvalue.value.rtkbasestation.RTKReferenceStationSource
import dji.v5.common.utils.RxUtil
import dji.v5.manager.KeyManager
import dji.v5.manager.aircraft.rtk.RTKSystemStateListener
import dji.v5.manager.areacode.AreaCode
import dji.v5.manager.areacode.AreaCodeChangeListener
import dji.v5.manager.areacode.AreaCodeManager
import dji.v5.manager.interfaces.IAreaCodeManager
import dji.v5.manager.interfaces.INetworkRTKManager
import dji.v5.manager.interfaces.IRTKCenter
import dji.v5.utils.common.LogUtils
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.WidgetModel
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.util.DataProcessor
import io.reactivex.rxjava3.core.Flowable
/**
* Description :
*
* @author: Byte.Cai
* date : 2022/8/15
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
class RTKTypeSwitchWidgetModel(
djiSdkModel: DJISDKModel,
uxKeyManager: ObservableInMemoryKeyedStore,
val areaCodeManager: IAreaCodeManager,
val rtkCenter: IRTKCenter,
) : WidgetModel(djiSdkModel, uxKeyManager) {
companion object {
private const val TAG = "RTKTypeSwitchWidgetModel"
private const val CUSTOM_RTK_SETTING_CACHE = "customRTKSettingCache"
}
private val supportReferenceStationListProcessor: DataProcessor<List<RTKReferenceStationSource>> = DataProcessor.create(ArrayList())
private val areaCodeProcessor: DataProcessor<String> = DataProcessor.create(AreaCode.UNKNOWN.code)
private val productTypeProcessor: DataProcessor<ProductType> = DataProcessor.create(ProductType.UNKNOWN)
private val rtkSourceProcessor: DataProcessor<RTKReferenceStationSource> = DataProcessor.create(RTKReferenceStationSource.UNKNOWN)
private val coordinateSystemListProcessor: DataProcessor<List<CoordinateSystem>> = DataProcessor.create(arrayListOf())
private val isMotorOnProcessor: DataProcessor<Boolean> = DataProcessor.create(false)
private var qxRTKManager: INetworkRTKManager? = null
private var customNetworkRTKManager: INetworkRTKManager? = null
private var currentRtkSource: RTKReferenceStationSource = RTKReferenceStationSource.UNKNOWN
private val areaCodeChangeListener = AreaCodeChangeListener { _, result ->
areaCodeProcessor.onNext(result.areaCode)
updateSupportReferenceStationList()
}
private val rtkSystemStateListener = RTKSystemStateListener {
val rtkSource = it.rtkReferenceStationSource
var coordinateSystemList: List<CoordinateSystem> = arrayListOf()
when (rtkSource) {
RTKReferenceStationSource.QX_NETWORK_SERVICE,
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE,
-> {
coordinateSystemList = arrayListOf(CoordinateSystem.WGS84, CoordinateSystem.CGCS2000)
}
else -> {
//Do Nothing
}
}
if (currentRtkSource != rtkSource) {
currentRtkSource = rtkSource
rtkSourceProcessor.onNext(rtkSource)
coordinateSystemListProcessor.onNext(coordinateSystemList)
}
}
val isMotorsOn: Flowable<Boolean>
get() = isMotorOnProcessor.toFlowable()
val rtkSource: Flowable<RTKReferenceStationSource>
get() = rtkSourceProcessor.toFlowable()
val coordinateSystemList: Flowable<List<CoordinateSystem>>
get() = coordinateSystemListProcessor.toFlowable()
val supportReferenceStationList: Flowable<List<RTKReferenceStationSource>>
get() = supportReferenceStationListProcessor.toFlowable()
init {
qxRTKManager = rtkCenter.qxrtkManager
customNetworkRTKManager = rtkCenter.customRTKManager
}
override fun inSetup() {
bindDataProcessor(
KeyTools.createKey(
FlightControllerKey.KeyAreMotorsOn
), isMotorOnProcessor
)
addDisposable(RxUtil.addListener(
KeyTools.createKey(
ProductKey.KeyProductType
), this
).subscribe {
productTypeProcessor.onNext(it)
updateSupportReferenceStationList()
})
areaCodeManager.addAreaCodeChangeListener(areaCodeChangeListener)
rtkCenter.addRTKSystemStateListener(rtkSystemStateListener)
}
override fun inCleanup() {
areaCodeManager.removeAreaCodeChangeListener(areaCodeChangeListener)
rtkCenter.removeRTKSystemStateListener(rtkSystemStateListener)
KeyManager.getInstance().cancelListen(this)
}
private fun updateSupportReferenceStationList() {
if (ProductType.DJI_MAVIC_3_ENTERPRISE_SERIES == productTypeProcessor.value) {
when (areaCodeProcessor.value) {
AreaCode.CHINA.code -> {
supportReferenceStationListProcessor.onNext(getSupportReferenceStationSource(true))
}
else -> {
supportReferenceStationListProcessor.onNext(getSupportReferenceStationSource(false))
}
}
} else {
supportReferenceStationListProcessor.onNext(getSupportReferenceStationSource(isInChina()))
}
}
/**
* 获取支持的差分数据源
*/
private fun getSupportReferenceStationSource(supportNetworkRTK: Boolean): List<RTKReferenceStationSource> {
return if (ProductUtil.isM3EProduct() || ProductUtil.isM4EProduct() || ProductUtil.isM4DProduct()) {
getMavicSupportReferenceStationSource(supportNetworkRTK)
} else {
getDefaultSupportReferenceStationSource(supportNetworkRTK)
}
}
private fun getMavicSupportReferenceStationSource(supportNetworkRTK: Boolean): MutableList<RTKReferenceStationSource> {
return if (supportNetworkRTK) {
mutableListOf(
RTKReferenceStationSource.NONE,
RTKReferenceStationSource.BASE_STATION,
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE,
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE
)
} else {
mutableListOf(
RTKReferenceStationSource.NONE,
RTKReferenceStationSource.BASE_STATION,
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE
)
}
}
private fun getDefaultSupportReferenceStationSource(supportNetworkRTK: Boolean): MutableList<RTKReferenceStationSource> {
LogUtils.i(TAG, "supportNetworkRTK=$supportNetworkRTK")
return if (supportNetworkRTK) {
mutableListOf(
RTKReferenceStationSource.NONE,
RTKReferenceStationSource.BASE_STATION,
RTKReferenceStationSource.QX_NETWORK_SERVICE,
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE,
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE
)
} else {
mutableListOf(
RTKReferenceStationSource.NONE,
RTKReferenceStationSource.BASE_STATION,
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE
)
}
}
/**
* 是否在中国
*/
private fun isInChina(): Boolean {
val countryCode = AreaCodeManager.getInstance().areaCode.areaCode
return AreaCode.CHINA.code.equals(countryCode)
}
}

View File

@ -0,0 +1,129 @@
package dji.v5.ux.accessory
import android.text.TextUtils
import android.view.View
import dji.rtk.CoordinateSystem
import dji.sdk.keyvalue.value.rtkbasestation.RTKCustomNetworkSetting
import dji.sdk.keyvalue.value.rtkbasestation.RTKReferenceStationSource
import dji.sdk.keyvalue.value.rtkmobilestation.RTKPositioningSolution
import dji.v5.manager.aircraft.rtk.RTKCenter
import dji.v5.utils.common.ContextUtil
import dji.v5.utils.common.DjiSharedPreferencesManager
import dji.v5.utils.common.JsonUtil
import dji.v5.utils.common.LogUtils
import dji.v5.ux.R
import dji.v5.ux.core.extension.getString
/**
* Description :
*
* @author: Byte.Cai
* date : 2022/5/24
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
object RTKUtil {
private const val TAG = "RTKUtil"
private const val USER_RTK_NETWORK_SERVICE_SETTINGS = "user_rtk_network_service_settings"
private const val USER_RTK_REFERENCE_SOURCE = "user_rtk_reference_source"
private const val USER_RTK_NETWORK_COORDINATE_SYSTEM_CMCC = "user_rtk_network_coordinate_system_cmcc"
private const val USER_RTK_NETWORK_COORDINATE_SYSTEM_QX = "user_rtk_network_coordinate_system_qx"
fun getRTKTypeName(view: View, rtkReferenceStationSource: RTKReferenceStationSource?): String {
view.run {
if (rtkReferenceStationSource == null) {
return getString(R.string.uxsdk_rtk_type_unknown_rtk);
}
return when (rtkReferenceStationSource) {
RTKReferenceStationSource.QX_NETWORK_SERVICE -> getString(R.string.uxsdk_rtk_type_nrtk)
RTKReferenceStationSource.BASE_STATION -> getString(R.string.uxsdk_rtk_type_rtk_mobile_station)
RTKReferenceStationSource.CUSTOM_NETWORK_SERVICE -> getString(R.string.uxsdk_rtk_type_custom_rtk)
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE -> getString(R.string.uxsdk_rtk_type_cmcc_rtk)
else -> getString(R.string.uxsdk_rtk_type_unknown_rtk)
}
}
}
fun getRTKStatusName(view: View, positioningSolution: RTKPositioningSolution?): String {
view.run {
if (positioningSolution == null) {
return getString(R.string.uxsdk_rtk_solution_unknown)
}
return when (positioningSolution) {
RTKPositioningSolution.NONE -> getString(R.string.uxsdk_rtk_solution_none)
RTKPositioningSolution.SINGLE_POINT -> getString(R.string.uxsdk_rtk_solution_single)
RTKPositioningSolution.FLOAT -> getString(R.string.uxsdk_rtk_solution_float)
RTKPositioningSolution.FIXED_POINT -> getString(R.string.uxsdk_rtk_solution_fixed)
RTKPositioningSolution.UNKNOWN -> getString(R.string.uxsdk_rtk_solution_unknown)
else -> getString(R.string.uxsdk_rtk_solution_unknown)
}
}
}
fun getNetRTKCoordinateSystem(rtkReferenceStationSource: RTKReferenceStationSource): CoordinateSystem? {
var coordinateSystem = ""
when (rtkReferenceStationSource) {
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE -> {
coordinateSystem = DjiSharedPreferencesManager.getString(ContextUtil.getContext(),
USER_RTK_NETWORK_COORDINATE_SYSTEM_CMCC, "")
}
RTKReferenceStationSource.QX_NETWORK_SERVICE -> {
coordinateSystem = DjiSharedPreferencesManager.getString(ContextUtil.getContext(), USER_RTK_NETWORK_COORDINATE_SYSTEM_QX, "")
}
else -> {
LogUtils.e(TAG, "getNetRTKCoordinateSystem error,unSupport rtkReferenceStationSource:$rtkReferenceStationSource")
}
}
return if (TextUtils.isEmpty(coordinateSystem)) null else getCoordinateName(coordinateSystem)
}
fun saveRTKCoordinateSystem(rtkReferenceStationSource: RTKReferenceStationSource, coordinateSystem: CoordinateSystem) {
when (rtkReferenceStationSource) {
RTKReferenceStationSource.QX_NETWORK_SERVICE -> {
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), USER_RTK_NETWORK_COORDINATE_SYSTEM_QX, coordinateSystem.name)
}
RTKReferenceStationSource.NTRIP_NETWORK_SERVICE -> {
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), USER_RTK_NETWORK_COORDINATE_SYSTEM_CMCC, coordinateSystem.name)
}
else -> {
LogUtils.e(TAG, "saveRTKCoordinateSystem error,unSupport rtkReferenceStationSource:$rtkReferenceStationSource")
}
}
}
fun saveRtkCustomNetworkSetting(settings: RTKCustomNetworkSetting) {
DjiSharedPreferencesManager.putString(ContextUtil.getContext(), USER_RTK_NETWORK_SERVICE_SETTINGS, settings.toString())
}
fun getRtkCustomNetworkSetting(): RTKCustomNetworkSetting? {
val localSetting = DjiSharedPreferencesManager.getString(ContextUtil.getContext(), USER_RTK_NETWORK_SERVICE_SETTINGS, "")
return if (!TextUtils.isEmpty(localSetting)) {
JsonUtil.toBean(localSetting, RTKCustomNetworkSetting::class.java)
} else RTKCenter.getInstance().customRTKManager.customNetworkRTKSettings
?: RTKCustomNetworkSetting(
"",
0,
"",
"",
""
)
}
private fun getCoordinateName(value: String): CoordinateSystem {
return when (value) {
CoordinateSystem.CGCS2000.name ->
CoordinateSystem.CGCS2000
CoordinateSystem.WGS84.name ->
CoordinateSystem.WGS84
else -> {
//如果未设置过坐标系则默认使用CGCS2000作为启动的坐标系
CoordinateSystem.CGCS2000
}
}
}
}

View File

@ -0,0 +1,183 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.accessory
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.StyleRes
import androidx.core.content.res.use
import dji.v5.utils.common.LogUtils
import dji.v5.ux.R
import io.reactivex.rxjava3.core.Flowable
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.SchedulerProvider
import dji.v5.ux.core.base.widget.ConstraintLayoutWidget
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.extension.*
private const val TAG = "RTKWidget"
/**
* This widget contains multiple widgets to control and get information related to RTK.
*/
open class RTKWidget @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayoutWidget<RTKWidget.ModelState>(context, attrs, defStyleAttr) {
//region Fields
private val rtkDialogSeparator: View = findViewById(R.id.rtk_dialog_separator)
private val widgetModel by lazy {
RTKWidgetModel(
DJISDKModel.getInstance(),
ObservableInMemoryKeyedStore.getInstance()
)
}
/**
* Get the RTK Enabled Widget so it can be customized.
*/
@get:JvmName("getRTKEnabledWidget")
val rtkEnabledWidget: RTKEnabledWidget = findViewById(R.id.widget_rtk_enabled)
/**
* Get the RTK Satellite Status Widget so it can be customized.
*/
@get:JvmName("getRTKSatelliteStatusWidget")
val rtkSatelliteStatusWidget: RTKSatelliteStatusWidget = findViewById(R.id.widget_rtk_satellite_status)
/**
* Get the RTK Type Switch Widget so it can be customized.
*/
@get:JvmName("getRTKTypeSwitchWidget")
val rtkTypeSwitchWidget: RTKTypeSwitchWidget = findViewById(R.id.widget_rtk_type_switch)
/**
* The color for the separator line views
*/
var rtkSeparatorsColor: Int
@JvmName("getRTKSeparatorsColor")
@ColorInt
get() = rtkSatelliteStatusWidget.rtkSeparatorsColor
@JvmName("setRTKSeparatorsColor")
set(@ColorInt color) {
rtkSatelliteStatusWidget.rtkSeparatorsColor = color
rtkDialogSeparator.setBackgroundColor(color)
}
//endregion
//region Constructor
override fun initView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
inflate(context, R.layout.uxsdk_widget_rtk, this)
}
init {
LogUtils.i(TAG, "init")
attrs?.let { initAttributes(context, it) }
}
//endregion
//region Lifecycle
override fun onAttachedToWindow() {
super.onAttachedToWindow()
LogUtils.i(TAG, "onAttachedToWindow")
if (!isInEditMode) {
widgetModel.setup()
LogUtils.i(TAG, "widgetModel.setup()")
}
}
override fun onDetachedFromWindow() {
if (!isInEditMode) {
widgetModel.cleanup()
}
super.onDetachedFromWindow()
}
override fun reactToModelChanges() {
//do nothing
}
//region Customization
override fun getIdealDimensionRatioString(): String {
return resources.getString(R.string.uxsdk_widget_rtk_ratio)
}
//Initialize all customizable attributes
@SuppressLint("Recycle")
private fun initAttributes(context: Context, attrs: AttributeSet) {
context.obtainStyledAttributes(attrs, R.styleable.RTKWidget).use { typedArray ->
typedArray.getColorAndUse(R.styleable.RTKWidget_uxsdk_rtkSeparatorsColor) {
rtkSeparatorsColor = it
}
}
}
//endregion
/**
* Get the [ModelState] updates
*/
@SuppressWarnings
override fun getWidgetStateUpdate(): Flowable<ModelState> {
return super.getWidgetStateUpdate()
}
/**
* Class defines the widget state updates
*/
sealed class ModelState {
/**
* Product connection update
*/
data class ProductConnected(val isConnected: Boolean) : ModelState()
/**
* RTK enabled update
*/
data class RTKEnabledUpdated(val isRTKEnabled: Boolean) : ModelState()
}
//endregion
}

View File

@ -0,0 +1,74 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.accessory
import dji.sdk.keyvalue.key.DJIKey
import dji.sdk.keyvalue.key.KeyTools
import dji.sdk.keyvalue.key.RtkMobileStationKey
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.WidgetModel
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.util.DataProcessor
import io.reactivex.rxjava3.core.Flowable
private const val TAG = "RTKWidgetModel"
/**
* Widget Model for the [RTKWidget] used to define
* the underlying logic and communication
*/
class RTKWidgetModel(
djiSdkModel: DJISDKModel,
uxKeyManager: ObservableInMemoryKeyedStore
) : WidgetModel(djiSdkModel, uxKeyManager) {
//region Fields
private val rtkEnabledProcessor: DataProcessor<Boolean> = DataProcessor.create(false)
//endregion
//region Data
/**
* Get whether the RTK is enabled.
*/
val rtkEnabled: Flowable<Boolean>
@JvmName("getRTKEnabled")
get() = rtkEnabledProcessor.toFlowable()
//region Lifecycle
override fun inSetup() {
val rtkEnabledKey: DJIKey<Boolean> = KeyTools.createKey(
RtkMobileStationKey.KeyRTKEnable)
bindDataProcessor(rtkEnabledKey, rtkEnabledProcessor)
}
override fun inCleanup() {
// do nothing
}
override fun updateStates() {
// Nothing to update
}
//endregion
}

View File

@ -0,0 +1,26 @@
package dji.v5.ux.accessory.data
import dji.sdk.keyvalue.value.rtkbasestation.BaseStationDeviceType
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationInfo
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationConnetState
/**
* Description :驱动UI的数据模型基于RTKBaseStationConnectInfo新增一个连接状态属性
*
* @author: Byte.Cai
* date : 2022/3/6
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
class DJIRTKBaseStationConnectInfo(
var baseStationId: Int,
var signalLevel: Int,
var rtkStationName: String,
var connectStatus: RTKStationConnetState = RTKStationConnetState.IDLE
) : RTKStationInfo(baseStationId, signalLevel, rtkStationName, BaseStationDeviceType.BS_RTK2) {
constructor() : this(0, 0, "")
override fun toString(): String {
return super.toString()
}
}

View File

@ -0,0 +1,149 @@
package dji.v5.ux.accessory.data
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.recyclerview.widget.RecyclerView
import dji.sdk.keyvalue.value.rtkbasestation.RTKStationConnetState
import dji.v5.utils.common.LogUtils
import dji.v5.ux.R
/**
* Description :基站RTK的Adapter展示扫码到的基站情况
*
* @author: Byte.Cai
* date : 2022/3/6
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
class RtkStationScanAdapter(val context: Context, list: List<DJIRTKBaseStationConnectInfo>?) :
RecyclerView.Adapter<RtkStationScanAdapter.RtkViewHolder>() {
private val TAG = "RtkStationScanAdapter"
private val LEVEL_0 = 0
private val LEVEL_1 = 1
private val LEVEL_2 = 2
private val LEVEL_3 = 3
private val LEVEL_4 = 4
private var baseStationInfoList: List<DJIRTKBaseStationConnectInfo>? = list
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RtkViewHolder {
val view: View = LayoutInflater.from(context).inflate(R.layout.uxsdk_widget_rtk_connect_status_item, parent, false)
return RtkViewHolder(view)
}
override fun onBindViewHolder(holder: RtkViewHolder, position: Int) {
baseStationInfoList?.get(position)?.let { info ->
holder.mRtkStationNameTv.text = info.rtkStationName
holder.mConnectSignalIv.setBackgroundResource(getSignalLevelDrawable(info.signalLevel))
holder.itemView.setOnClickListener {
val pos = holder.layoutPosition
val isConnecting = checkConnecting()
val hasConnected = checkConnected(pos)
if (!isConnecting && !hasConnected) {
//上一笔连接还没结束或者基站已连接则不响应新的连接请求
mOnItemClickListener?.onItemClick(holder.itemView, pos)
} else if (checkConnecting()) {
LogUtils.e(TAG, "The station is currently connecting, please try to connect later")
} else {
LogUtils.e(TAG, "The station has connected!")
}
}
when (info.connectStatus) {
RTKStationConnetState.CONNECTING -> {
holder.mConnectStatusIv.visible()
holder.mConnectStatusIv.setImageResource(R.drawable.uxsdk_rotate_progress_circle)
}
RTKStationConnetState.CONNECTED -> {
holder.mConnectStatusIv.visible()
holder.mConnectStatusIv.setImageResource(R.drawable.uxsdk_ic_confirm)
}
else -> {
holder.mConnectStatusIv.gone()
}
}
}
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun getItemCount(): Int {
return if (baseStationInfoList == null) 0 else baseStationInfoList!!.size
}
interface OnItemClickListener {
fun onItemClick(view: View?, position: Int)
}
private var mOnItemClickListener: OnItemClickListener? = null
fun setOnItemClickListener(onItemClickListener: OnItemClickListener?) {
mOnItemClickListener = onItemClickListener
}
@DrawableRes
fun getSignalLevelDrawable(signalLevel: Int): Int {
LogUtils.i(TAG, "getSignalLevelDrawable,signalLevel=$signalLevel")
return when (signalLevel) {
LEVEL_0 -> R.drawable.uxsdk_ic_topbar_signal_level_0
LEVEL_1 -> R.drawable.uxsdk_ic_topbar_signal_level_1
LEVEL_2 -> R.drawable.uxsdk_ic_topbar_signal_level_2
LEVEL_3 -> R.drawable.uxsdk_ic_topbar_signal_level_3
LEVEL_4 -> R.drawable.uxsdk_ic_topbar_signal_level_4
else -> R.drawable.uxsdk_ic_topbar_signal_level_5
}
}
class RtkViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var mConnectStatusIv: ImageView
var mRtkStationNameTv: TextView
var mConnectSignalIv: ImageView
init {
mConnectStatusIv = itemView.findViewById(R.id.connect_status_iv)
mRtkStationNameTv = itemView.findViewById(R.id.station_name_tv)
mConnectSignalIv = itemView.findViewById(R.id.connect_signal_iv)
mConnectStatusIv.visibility = View.GONE
}
}
fun View.visible() {
this.visibility = View.VISIBLE
}
fun View.gone() {
this.visibility = View.GONE
}
private fun checkConnecting(): Boolean {
baseStationInfoList?.let {
for (station in it) {
if (station.connectStatus == RTKStationConnetState.CONNECTING) {
return true
}
}
return false
}
return false
}
private fun checkConnected(position: Int): Boolean {
baseStationInfoList?.run {
val stationInfo = get(position)
if (stationInfo.connectStatus == RTKStationConnetState.CONNECTED) {
return true
}
return false
}
return false
}
}

View File

@ -0,0 +1,332 @@
package dji.v5.ux.accessory.item;
import android.animation.Animator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.annotation.ColorRes;
import androidx.annotation.IntDef;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Constraints;
import com.airbnb.lottie.LottieAnimationView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import dji.v5.utils.common.LogUtils;
import dji.v5.utils.common.StringUtils;
import dji.v5.ux.R;
public class RtkGuidanceView extends ConstraintLayout implements View.OnClickListener {
@IntDef({
GuidanceMode.STEP_RESET,
GuidanceMode.STEP_FIRST,
GuidanceMode.STEP_SECOND,
GuidanceMode.STEP_THIRD,
})
@Retention(RetentionPolicy.SOURCE)
private @interface GuidanceMode {
int STEP_RESET = 0;
int STEP_FIRST = 1;
int STEP_SECOND = 2;
int STEP_THIRD = 3;
}
//顶部导航栏控件
private View mNavigationLayout;
private TextView mStepFirstTv;
private TextView mStepSecondTv;
private TextView mStepThirdTv;
private ImageView mStepFirstIv;
private ImageView mStepSecondIv;
private View mStepStartDivider;
private View mStepEndDivider;
//左边动画控件
private LottieAnimationView mLottieAnimationView;
private View mReplayView;
private Button mReplayBtn;
private TextView mLottieTip;
//左边文字控件
private TextView mTitleTv;
private TextView mContentTv;
private View mImageView;
private TextView mImageDescTv;
private Button mPreviousBtn;
private Button mNextBtn;
private PopupWindow mPopupWindow;
private int mStep; //0重置1步骤12步骤23步骤3
private String[] mTitleStr;
private String[] mContentStr;
private int[] mAnimationRes;
private String[] mImageAssetsFolderStr;
private boolean mStartAnimation;
private Runnable mTipRunnable;
public RtkGuidanceView(Context context) {
this(context, null);
}
public RtkGuidanceView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RtkGuidanceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
LayoutInflater.from(context).inflate(R.layout.uxsdk_rtk_guidance_layout, this, true);
initView();
initWidgetParams();
initParams(context);
initListener();
}
private void initView() {
//顶部导航栏控件
mNavigationLayout = findViewById(R.id.rtk_guidance_step_navigation);
mStepFirstTv = findViewById(R.id.rtk_guidance_step_first_number);
mStepSecondTv = findViewById(R.id.rtk_guidance_step_second_number);
mStepThirdTv = findViewById(R.id.rtk_guidance_step_third_number);
mStepFirstIv = findViewById(R.id.rtk_guidance_step_first_image);
mStepSecondIv = findViewById(R.id.rtk_guidance_step_second_image);
mStepStartDivider = findViewById(R.id.rtk_guidance_start_divider);
mStepEndDivider = findViewById(R.id.rtk_guidance_end_divider);
//左边动画控件
mLottieAnimationView = findViewById(R.id.rtk_guidance_lottie_animation);
mReplayView = findViewById(R.id.rtk_guidance_replay_view);
mReplayBtn = findViewById(R.id.rtk_guidance_replay_btn);
mLottieTip = findViewById(R.id.rtk_guidance_lottie_tip);
//左边文字控件
mTitleTv = findViewById(R.id.rtk_guidance_step_title);
mContentTv = findViewById(R.id.rtk_guidance_step_content);
mImageView = findViewById(R.id.rtk_guidance_step_image);
mImageDescTv = findViewById(R.id.rtk_guidance_step_image_desc);
mPreviousBtn = findViewById(R.id.rtk_guidance_step_previous);
mNextBtn = findViewById(R.id.rtk_guidance_step_next);
}
private void initListener() {
findViewById(R.id.rtk_guidance_close).setOnClickListener(this);
findViewById(R.id.rtk_guidance_step_previous).setOnClickListener(this);
findViewById(R.id.rtk_guidance_step_next).setOnClickListener(this);
findViewById(R.id.rtk_guidance_replay_btn).setOnClickListener(this);
}
private void initWidgetParams() {
setLayoutParams(new Constraints.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
setBackgroundResource(R.color.uxsdk_white);
mLottieAnimationView.addAnimatorListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
updateReplayView(false);
}
@Override
public void onAnimationEnd(Animator animation) {
updateReplayView(true);
}
@Override
public void onAnimationCancel(Animator animation) {
//动画取消,无需操作
}
@Override
public void onAnimationRepeat(Animator animation) {
updateReplayView(false);
}
});
}
private void initParams(Context context) {
mStep = GuidanceMode.STEP_FIRST;
mTitleStr = context.getResources().getStringArray(R.array.uxsdk_rtk_guidance_title);
mContentStr = context.getResources().getStringArray(R.array.uxsdk_rtk_guidance_content);
mAnimationRes = new int[]{
R.raw.uxsdk_rtk_guidance_reset,
R.raw.uxsdk_rtk_guidance_step1,
R.raw.uxsdk_rtk_guidance_step2,
R.raw.uxsdk_rtk_guidance_step3
};
mImageAssetsFolderStr = new String[]{
"rtk_guidance_reset_images",
"rtk_guidance_step1_images",
"rtk_guidance_step2_images",
"rtk_guidance_step3_images"
};
}
private void updateView() {
//更新视图可见性
updateVisibility();
//更新顶部导航栏
if (mStep != GuidanceMode.STEP_RESET) {
mStepFirstTv.setVisibility(mStep <= GuidanceMode.STEP_FIRST ? VISIBLE : GONE);
mStepSecondTv.setVisibility(mStep <= GuidanceMode.STEP_SECOND ? VISIBLE : GONE);
updateSelectedTopView(mStepSecondTv, mStep < GuidanceMode.STEP_SECOND);
updateSelectedTopView(mStepThirdTv, mStep < GuidanceMode.STEP_THIRD);
mStepFirstIv.setVisibility(mStep > GuidanceMode.STEP_FIRST ? VISIBLE : GONE);
mStepSecondIv.setVisibility(mStep > GuidanceMode.STEP_SECOND ? VISIBLE : GONE);
mStepStartDivider.setBackgroundResource(mStep == GuidanceMode.STEP_FIRST ? R.color.uxsdk_gray : R.color.uxsdk_blue);
mStepEndDivider.setBackgroundResource(mStep <= GuidanceMode.STEP_SECOND ? R.color.uxsdk_gray : R.color.uxsdk_blue);
}
//更新左边动画
mLottieAnimationView.setImageAssetsFolder(mImageAssetsFolderStr[mStep]);
mLottieAnimationView.setAnimation(mAnimationRes[mStep]);
mLottieAnimationView.playAnimation();
//在步骤2中更新右边动画中 tip 视图的显示
updateLottieTipView();
//更新右边文案
mTitleTv.setText(mTitleStr[mStep]);
mContentTv.setText(mContentStr[mStep]);
mNextBtn.setText(StringUtils.getResStr(mStep == GuidanceMode.STEP_RESET
|| mStep == GuidanceMode.STEP_THIRD ? R.string.uxsdk_rtk_guidance_btn_finish : R.string.uxsdk_rtk_guidance_btn_next));
}
private void updateVisibility() {
mNavigationLayout.setVisibility(mStep == GuidanceMode.STEP_RESET ? INVISIBLE : VISIBLE);
mImageView.setVisibility(mStep == GuidanceMode.STEP_SECOND ? VISIBLE : INVISIBLE);
mImageDescTv.setVisibility(mStep == GuidanceMode.STEP_SECOND ? VISIBLE : INVISIBLE);
mPreviousBtn.setVisibility(mStep == GuidanceMode.STEP_RESET
|| mStep == GuidanceMode.STEP_FIRST ? GONE : VISIBLE);
}
/**
* 更新顶部步骤view
*
* @param textView 步骤view
* @param isSelected 是否被选中
*/
private void updateSelectedTopView(TextView textView, boolean isSelected) {
textView.setBackgroundResource(isSelected ?
R.drawable.uxsdk_bg_rtk_guidance_step_oval_gray :
R.drawable.uxsdk_bg_rtk_guidance_step_oval_blue_solid);
textView.setTextColor(isSelected ?
getResColor(R.color.uxsdk_black) :
getResColor(R.color.uxsdk_white));
}
/**
* 更新动画上方提示语
*/
private void updateLottieTipView() {
removeCallbacks(mTipRunnable);
mLottieTip.setVisibility(GONE);
if (mStep == GuidanceMode.STEP_RESET) {
delayedUpdateResetTipView();
}
}
/**
* 延迟更新动画上方提示语重置成功
*/
private void delayedUpdateResetTipView() {
mTipRunnable = () -> {
mLottieTip.setText(R.string.uxsdk_rtk_guidance_reset_tip);
mLottieTip.setVisibility(mStartAnimation ? VISIBLE : GONE);
if (mStartAnimation) {
mStartAnimation = false;
postDelayed(mTipRunnable, 2500);
}
};
mStartAnimation = true;
//重置在14秒后显示重置成功
postDelayed(mTipRunnable, 14000);
}
public void updateReplayView(boolean isShow) {
int visible = isShow ? VISIBLE : GONE;
mReplayView.setVisibility(visible);
mReplayBtn.setVisibility(visible);
}
@Override
protected void onDetachedFromWindow() {
removeCallbacks(mTipRunnable);
mStep = GuidanceMode.STEP_FIRST;
//停止动画
if (mLottieAnimationView != null && mLottieAnimationView.isAnimating()) {
mLottieAnimationView.cancelAnimation();
}
super.onDetachedFromWindow();
}
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.rtk_guidance_close) {
closePopupWindow();
} else if (id == R.id.rtk_guidance_step_previous) {
mStep--;
updateView();
} else if (id == R.id.rtk_guidance_step_next) {
if (mStep != GuidanceMode.STEP_RESET && mStep != GuidanceMode.STEP_THIRD) {
mStep++;
updateView();
} else {
closePopupWindow();
}
} else if (id == R.id.rtk_guidance_replay_btn) {
mLottieAnimationView.playAnimation();
updateLottieTipView();
}
}
private void closePopupWindow() {
if (mPopupWindow != null) {
mPopupWindow.dismiss();
}
}
public void showPopupWindow(View parent) {
showPopupWindow(parent, GuidanceMode.STEP_FIRST);
}
public void showPopupWindow(View parent, int guidanceMode) {
if (guidanceMode < GuidanceMode.STEP_RESET || guidanceMode > GuidanceMode.STEP_THIRD) {
return;
}
mStep = guidanceMode;
if (mPopupWindow == null) {
mPopupWindow = new PopupWindow(this, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, true);
}
updateView();
mPopupWindow.setClippingEnabled(false);
mPopupWindow.showAtLocation(parent, Gravity.NO_GRAVITY, 0, 0);
}
public int getResColor(@ColorRes int resId) {
if (getContext() != null) {
return getContext().getResources().getColor(resId);
}
return 0;
}
}

View File

@ -0,0 +1,68 @@
package dji.v5.ux.accessory.item
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.TextView
import dji.v5.ux.R
/**
* Description :垂直显示标题和值的控件
*
* @author: Byte.Cai
* date : 2022/9/7
*
* Copyright (c) 2022, DJI All Rights Reserved.
*/
class TitleValueCell @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr) {
private var tvTitle: TextView
private var tvValue: TextView
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.FpvTitleValueCell, defStyleAttr, 0)
val title = ta.getString(R.styleable.FpvTitleValueCell_uxsdk_title)
val value = ta.getString(R.styleable.FpvTitleValueCell_uxsdk_value)
val layoutId = ta.getResourceId(R.styleable.FpvTitleValueCell_uxsdk_layout, R.layout.uxsdk_fpv_top_bar_popover_title_value_cell)
if (layoutId == 0) {
throw IllegalArgumentException("layout can not be null.")
}
inflate(context, layoutId, this)
ta.recycle()
tvTitle = findViewById(R.id.tv_title)
tvValue = findViewById(R.id.tv_value)
tvTitle.text = title
tvValue.text = value
}
var title: CharSequence?
get() = tvTitle.text
set(value) {
if (value != tvTitle.text) {
tvTitle.text = value
}
}
var value: CharSequence?
get() = tvValue.text
set(value) {
if (value != tvValue.text) {
tvValue.text = value
}
}
fun setValueTextColor(color: Int) {
tvValue.setTextColor(color)
}
fun setTitleTextColor(color: Int) {
tvTitle.setTextColor(color)
}
}

View File

@ -0,0 +1,182 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import androidx.annotation.ColorInt;
import dji.v5.ux.R;
/**
* Ring progress bar.
* The view shows circular animation to indicate progress.
*/
public class ProgressRingView extends View {
private RectF boundaries;
private Paint paint;
private Animation indeterminateAnimation;
private Shader progressGradient;
private boolean indeterminate;
private int ringColor = Color.WHITE;
private int height;
private int width;
public ProgressRingView(Context context) {
super(context, null, 0);
}
public ProgressRingView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ProgressRingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context) {
boundaries = new RectF();
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.SQUARE);
paint.setColor(ringColor);
if (!isInEditMode()) {
indeterminateAnimation = AnimationUtils.loadAnimation(context, R.anim.uxsdk_anim_rotate);
indeterminateAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
indeterminate = true;
if (progressGradient != null) {
paint.setShader(progressGradient);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
// Nothing to do here
}
@Override
public void onAnimationEnd(Animation animation) {
paint.setShader(null);
invalidate();
}
});
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// StrokeWidth percentage
// Color
float strokeWidth = w * .08f;
paint.setStrokeWidth(strokeWidth / 2);
width = w;
height = h;
initProgressGradient(w, h);
float padding = strokeWidth / 2;
boundaries.top = padding;
boundaries.left = padding;
boundaries.bottom = getMeasuredWidth() - padding;
boundaries.right = getMeasuredHeight() - padding;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawArc(boundaries, 0, 360, false, paint);
}
/**
* Set the color of the progress ring.
*
* @param color integer value
*/
public void setRingColor(@ColorInt int color) {
ringColor = color;
paint.setColor(color);
initProgressGradient(width, height);
invalidate();
}
/**
* Check if the ring is currently animating.
*
* @return boolean value
*/
public boolean isIndeterminate() {
return indeterminate;
}
/**
* Start/Stop the ring animation.
*
* @param indeterminate boolean value
* true - to start animation
* false - to stop animation
*/
public void setIndeterminate(boolean indeterminate) {
if (indeterminate == this.indeterminate) return;
this.indeterminate = indeterminate;
if (indeterminate) {
startAnimation(indeterminateAnimation);
} else {
clearAnimation();
indeterminateAnimation.cancel();
indeterminateAnimation.reset();
}
}
private void initProgressGradient(int w, int h) {
progressGradient = null;
float cx = w / 2f;
float cy = h / 2f;
int[] colors = {ringColor, Color.TRANSPARENT, Color.TRANSPARENT};
float[] positions = {0, 0.7f, 1};
progressGradient = new SweepGradient(cx, cy, colors, positions);
}
}

View File

@ -0,0 +1,201 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.util;
import android.content.Context;
import androidx.annotation.NonNull;
import dji.v5.ux.R;
import dji.v5.ux.core.util.AudioUtil;
import io.reactivex.rxjava3.disposables.Disposable;
/**
* Class used as a util for playing sounds of
* capture photo and record video
*/
public class CameraActionSound {
private final Context context;
private ShutterSoundCount shutterCount;
public CameraActionSound(Context con) {
context = con;
}
private int shutterCountSound(ShutterSoundCount value) {
int soundId;
switch (value) {
case ONE:
soundId = R.raw.uxsdk_shutter_1;
break;
case THREE:
soundId = R.raw.uxsdk_shutter_3;
break;
case FIVE:
soundId = R.raw.uxsdk_shutter_5;
break;
case SEVEN:
soundId = R.raw.uxsdk_shutter_7;
break;
case TEN:
soundId = R.raw.uxsdk_shutter_10;
break;
case FOURTEEN:
soundId = R.raw.uxsdk_shutter_14;
break;
case UNKNOWN:
default:
soundId = R.raw.uxsdk_shutter_3;
break;
}
return soundId;
}
/**
* Set the count of photos based on current mode
*
* @param count int value
*/
public void setShutterCount(@NonNull ShutterSoundCount count) {
shutterCount = count;
}
/**
* Play sound for Capture Photo
*
* @return Disposable
*/
@NonNull
public Disposable playCapturePhoto() {
return AudioUtil.playSoundInBackground(context, shutterCountSound(shutterCount));
}
/**
* Play sound for start record video
*
* @return Disposable
*/
@NonNull
public Disposable playStartRecordVideo() {
return AudioUtil.playSoundInBackground(context, R.raw.uxsdk_video_voice);
}
/**
* Play sound for stop record video
*
* @return Disposable
*/
@NonNull
public Disposable playStopRecordVideo() {
return AudioUtil.playSoundInBackground(context, R.raw.uxsdk_end_video_record);
}
/**
* Enum for shutter sound count
*/
public enum ShutterSoundCount {
/**
* Single photo shutter sound
*/
ONE(1),
/**
* Three photos shutter sound
*/
THREE(3),
/**
* Five photos shutter sound
*/
FIVE(5),
/**
* Seven photos shutter sound
*/
SEVEN(7),
/**
* Ten photos shutter sound
*/
TEN(10),
/**
* Fourteen photos shutter sound
*/
FOURTEEN(14),
/**
* Will default to one shutter sound
*/
UNKNOWN(1);
private int value;
ShutterSoundCount(int value) {
this.value = value;
}
/**
* Returns the enum constant of this type with the input integer value.
*
* @param value The input integer value
* @return The enum constant of this type
*/
public static ShutterSoundCount find(int value) {
ShutterSoundCount result = UNKNOWN;
for (int i = 0; i < getValues().length; i++) {
if (getValues()[i]._equals(value)) {
result = getValues()[i];
break;
}
}
return result;
}
/**
* Returns the real value of an enum value.
*
* @return integer The real value.
*/
public int value() {
return this.value;
}
/**
* Compares the input integer value with the real value of an enum value.
*
* @param b The input integer value.
* @return boolean The compared result.
*/
private boolean _equals(int b) {
return value == b;
}
private static ShutterSoundCount[] values;
public static ShutterSoundCount[] getValues() {
if (values == null) {
values = values();
}
return values;
}
}
}

View File

@ -0,0 +1,139 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.util;
import dji.sdk.keyvalue.value.camera.CameraShootPhotoMode;
import dji.sdk.keyvalue.value.camera.PhotoPanoramaMode;
import dji.v5.ux.R;
public class CameraResource {
public static int getPhotoModeImgResId(final int mode, final int value) {
if (CameraShootPhotoMode.HDR.value() == mode) {
return R.drawable.uxsdk_ic_photo_mode_hdr;
} else if (CameraShootPhotoMode.BURST.value() == mode) {
return getImgResIdFromBurstMode(value);
} else if (CameraShootPhotoMode.RAW_BURST.value() == mode) {
return getImgResIdFromRawBurstMode(value);
} else if (CameraShootPhotoMode.AEB.value() == mode) {
return getImgResIdFromAebMode(value);
} else if (CameraShootPhotoMode.INTERVAL.value() == mode) {
return getImgResIdFromIntervalMode(value);
} else if (CameraShootPhotoMode.VISION_BOKEH.value() == mode) {
return R.drawable.uxsdk_ic_photo_mode_shallow_focus;
} else if (CameraShootPhotoMode.VISION_PANO.value() == mode) {
return getImgResIdFromVisionPanoMode(value);
} else if (CameraShootPhotoMode.EHDR.value() == mode) {
return R.drawable.uxsdk_ic_photo_mode_nor;
} else if (CameraShootPhotoMode.HYPER_LIGHT.value() == mode) {
return R.drawable.uxsdk_ic_photo_mode_nor;
} else {
return R.drawable.uxsdk_ic_photo_mode_nor;
}
}
private static int getImgResIdFromBurstMode(int value) {
if (value == 14) {
return R.drawable.uxsdk_ic_photo_mode_continuous_14;
} else if (value == 10) {
return R.drawable.uxsdk_ic_photo_mode_continuous_10;
} else if (value == 7) {
return R.drawable.uxsdk_ic_photo_mode_continuous_7;
} else if (value == 5) {
return R.drawable.uxsdk_ic_photo_mode_continuous_5;
} else {
return R.drawable.uxsdk_ic_photo_mode_continuous_3;
}
}
private static int getImgResIdFromRawBurstMode(int value) {
if (value == 255) {
return R.drawable.uxsdk_ic_photo_mode_raw_burst_infinity;
} else if (value == 14) {
return R.drawable.uxsdk_ic_photo_mode_raw_burst_14;
} else if (value == 10) {
return R.drawable.uxsdk_ic_photo_mode_raw_burst_10;
} else if (value == 7) {
return R.drawable.uxsdk_ic_photo_mode_raw_burst_7;
} else if (value == 5) {
return R.drawable.uxsdk_ic_photo_mode_raw_burst_5;
} else {
return R.drawable.uxsdk_ic_photo_mode_raw_burst_3;
}
}
private static int getImgResIdFromAebMode(int value) {
if (value == 7) {
return R.drawable.uxsdk_ic_photo_mode_aeb_continuous_7;
} else if (value == 5) {
return R.drawable.uxsdk_ic_photo_mode_aeb_continuous_5;
} else {
return R.drawable.uxsdk_ic_photo_mode_aeb_continuous_3;
}
}
private static int getImgResIdFromIntervalMode(int value) {
if (value == 60) {
return R.drawable.uxsdk_ic_photo_mode_timepause_60s;
} else if (value == 30) {
return R.drawable.uxsdk_ic_photo_mode_timepause_30s;
} else if (value == 20) {
return R.drawable.uxsdk_ic_photo_mode_timepause_20s;
} else if (value == 15) {
return R.drawable.uxsdk_ic_photo_mode_timepause_15s;
} else if (value == 10) {
return R.drawable.uxsdk_ic_photo_mode_timepause_10s;
} else if (value == 7) {
return R.drawable.uxsdk_ic_photo_mode_timepause_7s;
} else if (value == 4) {
return R.drawable.uxsdk_ic_photo_mode_timepause_4s;
} else if (value == 3) {
return R.drawable.uxsdk_ic_photo_mode_timepause_3s;
} else if (value == 2) {
return R.drawable.uxsdk_ic_photo_mode_timepause_2s;
} else {
return R.drawable.uxsdk_ic_photo_mode_timepause_5s;
}
}
private static int getImgResIdFromVisionPanoMode(int value) {
if (PhotoPanoramaMode.MODE_3x1.value() == value) {
return R.drawable.uxsdk_ic_photo_mode_pano_3x1;
} else if (PhotoPanoramaMode.MODE_1x3.value() == value) {
return R.drawable.uxsdk_ic_photo_mode_pano_180;
} else if (PhotoPanoramaMode.MODE_3x3.value() == value) {
return R.drawable.uxsdk_ic_photo_mode_pano_3x3;
} else if (PhotoPanoramaMode.MODE_SUPER_RESOLUTION.value() == value) {
return R.drawable.uxsdk_ic_photo_mode_nor;
} else if (PhotoPanoramaMode.MODE_SPHERE.value() == value) {
return R.drawable.uxsdk_ic_photo_mode_pano_sphere;
} else {
return R.drawable.uxsdk_ic_photo_mode_pano_180;
}
}
private CameraResource() {
}
}

View File

@ -0,0 +1,467 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.autoexposurelock;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import dji.sdk.keyvalue.value.common.CameraLensType;
import dji.sdk.keyvalue.value.common.ComponentIndexType;
import dji.v5.ux.R;
import dji.v5.ux.core.base.DJISDKModel;
import dji.v5.ux.core.base.ICameraIndex;
import dji.v5.ux.core.base.SchedulerProvider;
import dji.v5.ux.core.base.widget.ConstraintLayoutWidget;
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore;
import dji.v5.ux.core.util.UxErrorHandle;
import dji.v5.ux.core.util.ViewUtil;
/**
* Auto Exposure Lock Widget will display the current state of exposure lock.
* <p>
* When locked the exposure of the camera will remain constant.
* Changing the exposure parameters manually will release the lock.
*/
public class AutoExposureLockWidget extends ConstraintLayoutWidget<Object> implements View.OnClickListener, ICameraIndex {
//region Fields
private static final String TAG = "AutoExposureLockWidget";
private ImageView foregroundImageView;
private TextView titleTextView;
private AutoExposureLockWidgetModel widgetModel;
private Drawable autoExposureLockDrawable;
private Drawable autoExposureUnlockDrawable;
private ColorStateList lockDrawableTint;
private ColorStateList unlockDrawableTint;
//endregion
//region Lifecycle
public AutoExposureLockWidget(@NonNull Context context) {
super(context);
}
public AutoExposureLockWidget(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public AutoExposureLockWidget(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
inflate(context, R.layout.uxsdk_widget_auto_exposure_lock, this);
if (getBackground() == null) {
setBackgroundResource(R.drawable.uxsdk_background_black_rectangle);
}
foregroundImageView = findViewById(R.id.auto_exposure_lock_widget_foreground_image_view);
titleTextView = findViewById(R.id.auto_exposure_lock_widget_title_text_view);
if (!isInEditMode()) {
widgetModel =
new AutoExposureLockWidgetModel(DJISDKModel.getInstance(),
ObservableInMemoryKeyedStore.getInstance());
}
initDefaults();
if (attrs != null) {
initAttributes(context, attrs);
}
setOnClickListener(this);
}
@Override
protected void reactToModelChanges() {
addReaction(widgetModel.isAutoExposureLockOn().observeOn(SchedulerProvider.ui()).subscribe(this::onAELockChange));
}
@Override
public void onClick(View v) {
if (v == this) {
setAutoExposureLock();
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!isInEditMode()) {
widgetModel.setup();
}
}
@Override
protected void onDetachedFromWindow() {
if (!isInEditMode()) {
widgetModel.cleanup();
}
super.onDetachedFromWindow();
}
//endregion
//region private methods
@MainThread
private void onAELockChange(boolean isLocked) {
if (isLocked) {
foregroundImageView.setImageDrawable(autoExposureLockDrawable);
if (lockDrawableTint != null) ViewUtil.tintImage(foregroundImageView, lockDrawableTint);
} else {
foregroundImageView.setImageDrawable(autoExposureUnlockDrawable);
if (unlockDrawableTint != null)
ViewUtil.tintImage(foregroundImageView, unlockDrawableTint);
}
}
private void setAutoExposureLock() {
addDisposable(widgetModel.toggleAutoExposureLock()
.observeOn(SchedulerProvider.ui())
.subscribe(() -> {
// Do nothing
}, UxErrorHandle.logErrorConsumer(TAG, "set auto exposure lock: ")));
}
private void checkAndUpdateAELock() {
if (!isInEditMode()) {
addDisposable(widgetModel.isAutoExposureLockOn().firstOrError()
.observeOn(SchedulerProvider.ui())
.subscribe(this::onAELockChange, UxErrorHandle.logErrorConsumer(TAG, "Update AE Lock ")));
}
}
private void initDefaults() {
autoExposureLockDrawable = getResources().getDrawable(R.drawable.uxsdk_ic_auto_exposure_lock);
autoExposureUnlockDrawable = getResources().getDrawable(R.drawable.uxsdk_ic_auto_exposure_unlock);
setTitleTextColor(getResources().getColorStateList(R.color.uxsdk_color_selector_auto_exposure_lock));
}
private void initAttributes(@NonNull Context context, @NonNull AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.AutoExposureLockWidget);
if (!isInEditMode()) {
updateCameraSource(ComponentIndexType.find(typedArray.getInt(R.styleable.AutoExposureLockWidget_uxsdk_cameraIndex, 0)), CameraLensType.find(typedArray.getInt(R.styleable.AutoExposureLockWidget_uxsdk_lensType, 0)));
}
ColorStateList colorStateList = typedArray.getColorStateList(R.styleable.AutoExposureLockWidget_uxsdk_widgetTitleTextColor);
if (colorStateList != null) {
setTitleTextColor(colorStateList);
}
int colorResource = typedArray.getColor(R.styleable.AutoExposureLockWidget_uxsdk_widgetTitleTextColor, INVALID_COLOR);
if (colorResource != INVALID_COLOR) {
setTitleTextColor(colorResource);
}
colorStateList = typedArray.getColorStateList(R.styleable.AutoExposureLockWidget_uxsdk_autoExposureUnlockDrawableTint);
if (colorStateList != null) {
setAutoExposureUnlockIconTint(colorStateList);
}
colorResource = typedArray.getColor(R.styleable.AutoExposureLockWidget_uxsdk_autoExposureUnlockDrawableTint, INVALID_COLOR);
if (colorResource != INVALID_COLOR) {
setAutoExposureUnlockIconTint(colorResource);
}
colorStateList = typedArray.getColorStateList(R.styleable.AutoExposureLockWidget_uxsdk_autoExposureLockDrawableTint);
if (colorStateList != null) {
setAutoExposureLockIconTint(colorStateList);
}
colorResource = typedArray.getColor(R.styleable.AutoExposureLockWidget_uxsdk_autoExposureLockDrawableTint, INVALID_COLOR);
if (colorResource != INVALID_COLOR) {
setAutoExposureLockIconTint(colorResource);
}
int textAppearance = typedArray.getResourceId(R.styleable.AutoExposureLockWidget_uxsdk_widgetTitleTextAppearance, INVALID_RESOURCE);
if (textAppearance != INVALID_RESOURCE) {
setTitleTextAppearance(textAppearance);
}
if (typedArray.getDrawable(R.styleable.AutoExposureLockWidget_uxsdk_autoExposureLockDrawable) != null) {
autoExposureLockDrawable = typedArray.getDrawable(R.styleable.AutoExposureLockWidget_uxsdk_autoExposureLockDrawable);
}
if (typedArray.getDrawable(R.styleable.AutoExposureLockWidget_uxsdk_autoExposureUnlockDrawable) != null) {
autoExposureUnlockDrawable = typedArray.getDrawable(R.styleable.AutoExposureLockWidget_uxsdk_autoExposureUnlockDrawable);
}
typedArray.recycle();
}
//endregion
//region customization
@NonNull
@Override
public String getIdealDimensionRatioString() {
return getResources().getString(R.string.uxsdk_widget_auto_exposure_lock_ratio);
}
@NonNull
public ComponentIndexType getCameraIndex() {
return widgetModel.getCameraIndex();
}
@NonNull
@Override
public CameraLensType getLensType() {
return widgetModel.getLensType();
}
@Override
public void updateCameraSource(@NonNull ComponentIndexType cameraIndex, @NonNull CameraLensType lensType) {
widgetModel.updateCameraSource(cameraIndex, lensType);
}
/**
* Set drawable for auto exposure lock in locked state
*
* @param resourceId to be used
*/
public void setAutoExposureLockIcon(@DrawableRes int resourceId) {
setAutoExposureLockIcon(getResources().getDrawable(resourceId));
}
/**
* Set drawable for auto exposure lock in locked state
*
* @param drawable to be used
*/
public void setAutoExposureLockIcon(@Nullable Drawable drawable) {
this.autoExposureLockDrawable = drawable;
checkAndUpdateAELock();
}
/**
* Get current drawable resource for auto exposure lock in locked state
*
* @return Drawable
*/
@Nullable
public Drawable getAutoExposureLockDrawable() {
return autoExposureLockDrawable;
}
/**
* Set resource for auto exposure lock in unlocked state
*
* @param resourceId to be used
*/
public void setAutoExposureUnlockIcon(@DrawableRes int resourceId) {
setAutoExposureUnlockIcon(getResources().getDrawable(resourceId));
}
/**
* Set drawable for auto exposure lock in unlocked state
*
* @param drawable to be used
*/
public void setAutoExposureUnlockIcon(@Nullable Drawable drawable) {
this.autoExposureUnlockDrawable = drawable;
checkAndUpdateAELock();
}
/**
* Get current drawable resource for auto exposure lock in unlocked state
*
* @return Drawable
*/
@Nullable
public Drawable getAutoExposureUnlockDrawable() {
return autoExposureUnlockDrawable;
}
/**
* Get current text color state list of widget title
*
* @return ColorStateList used
*/
@Nullable
public ColorStateList getTitleTextColors() {
return titleTextView.getTextColors();
}
/**
* Get the current color of title text
*
* @return integer value representing color
*/
@ColorInt
public int getTitleTextColor() {
return titleTextView.getCurrentTextColor();
}
/**
* Set text color state list to the widget title
*
* @param colorStateList to be used
*/
public void setTitleTextColor(@Nullable ColorStateList colorStateList) {
titleTextView.setTextColor(colorStateList);
}
/**
* Set the color of title text
*
* @param color integer value
*/
public void setTitleTextColor(@ColorInt int color) {
titleTextView.setTextColor(color);
}
/**
* Set text appearance of the widget title
*
* @param textAppearance to be used
*/
public void setTitleTextAppearance(@StyleRes int textAppearance) {
titleTextView.setTextAppearance(getContext(), textAppearance);
}
/**
* Get current background of icon
*
* @return Drawable
*/
@NonNull
public Drawable getIconBackground() {
return foregroundImageView.getBackground();
}
/**
* Set background to icon
*
* @param resourceId to be used
*/
public void setIconBackground(@DrawableRes int resourceId) {
setIconBackground(getResources().getDrawable(resourceId));
}
/**
* Set background to icon
*
* @param drawable to be used
*/
public void setIconBackground(@Nullable Drawable drawable) {
foregroundImageView.setBackground(drawable);
}
/**
* Get current background of title text
*
* @return Drawable
*/
@Nullable
public Drawable getTitleBackground() {
return titleTextView.getBackground();
}
/**
* Set background to title text
*
* @param resourceId to be used
*/
public void setTitleBackground(@DrawableRes int resourceId) {
setTitleBackground(getResources().getDrawable(resourceId));
}
/**
* Set background to title text
*
* @param drawable to be used
*/
public void setTitleBackground(@Nullable Drawable drawable) {
titleTextView.setBackground(drawable);
}
@Override
public void setEnabled(boolean enabled) {
titleTextView.setEnabled(enabled);
foregroundImageView.setEnabled(enabled);
super.setEnabled(enabled);
}
/**
* Get the color tint for exposure settings unlocked icon
*
* @return ColorStateList used as color tint
*/
@Nullable
public ColorStateList getAutoExposureUnlockIconTint() {
return unlockDrawableTint;
}
/**
* Set the color of tint for exposure settings unlocked icon
*
* @param color int value
*/
public void setAutoExposureUnlockIconTint(@ColorInt int color) {
setAutoExposureUnlockIconTint(ColorStateList.valueOf(color));
}
/**
* Set the color of tint for the exposure settings unlocked icon
*
* @param colorStateList to be used
*/
public void setAutoExposureUnlockIconTint(@Nullable ColorStateList colorStateList) {
unlockDrawableTint = colorStateList;
checkAndUpdateAELock();
}
/**
* Get the color tint for exposure settings locked icon
*
* @return ColorStateList used as color tint
*/
@Nullable
public ColorStateList getAutoExposureLockIconTint() {
return lockDrawableTint;
}
/**
* Set the color of tint for exposure settings locked icon
*
* @param color int value
*/
public void setAutoExposureLockIconTint(@ColorInt int color) {
setAutoExposureLockIconTint(ColorStateList.valueOf(color));
}
/**
* Set the color of tint for the exposure settings locked icon
*
* @param colorStateList to be used
*/
public void setAutoExposureLockIconTint(@Nullable ColorStateList colorStateList) {
lockDrawableTint = colorStateList;
checkAndUpdateAELock();
}
//endregion
}

View File

@ -0,0 +1,119 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.autoexposurelock;
import androidx.annotation.NonNull;
import dji.sdk.keyvalue.key.CameraKey;
import dji.sdk.keyvalue.key.KeyTools;
import dji.sdk.keyvalue.value.common.CameraLensType;
import dji.sdk.keyvalue.value.common.ComponentIndexType;
import dji.v5.ux.core.base.DJISDKModel;
import dji.v5.ux.core.base.ICameraIndex;
import dji.v5.ux.core.base.WidgetModel;
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore;
import dji.v5.ux.core.util.DataProcessor;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
/**
* Auto Exposure Lock Widget Model
* <p>
* Widget Model for the {@link AutoExposureLockWidget} used to define the
* underlying logic and communication
*/
public class AutoExposureLockWidgetModel extends WidgetModel implements ICameraIndex {
//region Fields
private final DataProcessor<Boolean> autoExposureLockBooleanProcessor;
private ComponentIndexType cameraIndex = ComponentIndexType.LEFT_OR_MAIN;
private CameraLensType lensType = CameraLensType.CAMERA_LENS_ZOOM;
//endregion
public AutoExposureLockWidgetModel(@NonNull DJISDKModel djiSdkModel,
@NonNull ObservableInMemoryKeyedStore uxKeyManager) {
super(djiSdkModel, uxKeyManager);
autoExposureLockBooleanProcessor = DataProcessor.create(false);
}
//region Data
/**
* Check if the auto exposure lock is enabled
*
* @return Flowable with boolean true - enabled false - disabled
*/
public Flowable<Boolean> isAutoExposureLockOn() {
return autoExposureLockBooleanProcessor.toFlowable();
}
//endregion
//region Actions
@NonNull
public ComponentIndexType getCameraIndex() {
return cameraIndex;
}
@NonNull
@Override
public CameraLensType getLensType() {
return lensType;
}
@Override
public void updateCameraSource(@NonNull ComponentIndexType cameraIndex, @NonNull CameraLensType lensType) {
this.cameraIndex = cameraIndex;
this.lensType = lensType;
restart();
}
/**
* Set auto exposure lock the opposite of its current state
*
* @return Completable representing success and failure of action
*/
public Completable toggleAutoExposureLock() {
return djiSdkModel.setValue(KeyTools.createCameraKey(CameraKey.KeyAELockEnabled, cameraIndex, lensType), !autoExposureLockBooleanProcessor.getValue());
}
//endregion
//region Lifecycle
@Override
protected void inSetup() {
KeyTools.createCameraKey(CameraKey.KeyAELockEnabled, cameraIndex, lensType);
bindDataProcessor(KeyTools.createCameraKey(CameraKey.KeyAELockEnabled, cameraIndex, lensType), autoExposureLockBooleanProcessor);
}
@Override
protected void inCleanup() {
// nothing to clean
}
@Override
protected void updateStates() {
// No States
}
//endregion
}

View File

@ -0,0 +1,224 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import java.util.HashMap;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Constraints;
import dji.sdk.keyvalue.value.camera.CameraMode;
import dji.sdk.keyvalue.value.common.CameraLensType;
import dji.sdk.keyvalue.value.common.ComponentIndexType;
import dji.v5.ux.R;
import dji.v5.ux.cameracore.widget.cameracapture.recordvideo.RecordVideoWidget;
import dji.v5.ux.cameracore.widget.cameracapture.shootphoto.ShootPhotoWidget;
import dji.v5.ux.core.base.DJISDKModel;
import dji.v5.ux.core.base.ICameraIndex;
import dji.v5.ux.core.base.SchedulerProvider;
import dji.v5.ux.core.base.widget.ConstraintLayoutWidget;
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore;
import dji.v5.ux.core.util.UxErrorHandle;
/**
* Camera Capture Widget
* <p>
* Widget can be used to shoot photo and record video. It reacts to change in {@link CameraMode}
* It encloses {@link ShootPhotoWidget} and {@link RecordVideoWidget} for respective modes
*/
public class CameraCaptureWidget extends ConstraintLayoutWidget<Object> implements ICameraIndex {
//region Fields
private static final String TAG = "CameraCaptureWidget";
private CameraCaptureWidgetModel widgetModel;
private Map<CameraMode, View> widgetMap;
//endregion
//region Lifecycle
public CameraCaptureWidget(Context context) {
super(context);
}
public CameraCaptureWidget(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CameraCaptureWidget(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
widgetMap = new HashMap<>();
if (!isInEditMode()) {
addViewByMode(CameraMode.PHOTO_NORMAL, new ShootPhotoWidget(context));
addViewByMode(CameraMode.VIDEO_NORMAL, new RecordVideoWidget(context));
widgetModel = new CameraCaptureWidgetModel(DJISDKModel.getInstance(), ObservableInMemoryKeyedStore.getInstance());
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!isInEditMode()) {
widgetModel.setup();
}
}
@Override
protected void onDetachedFromWindow() {
if (!isInEditMode()) {
widgetModel.cleanup();
}
super.onDetachedFromWindow();
}
@Override
protected void reactToModelChanges() {
addReaction(
widgetModel.getCameraMode()
.observeOn(SchedulerProvider.ui())
.subscribe(
this::onCameraModeChange,
UxErrorHandle.logErrorConsumer(TAG, "Camera Mode Change: ")));
}
@NonNull
@Override
public String getIdealDimensionRatioString() {
return getResources().getString(R.string.uxsdk_widget_default_ratio);
}
//endregion
@NonNull
public ComponentIndexType getCameraIndex() {
return widgetModel.getCameraIndex();
}
@NonNull
@Override
public CameraLensType getLensType() {
return widgetModel.getLensType();
}
@Override
public void updateCameraSource(@NonNull ComponentIndexType cameraIndex, @NonNull CameraLensType lensType) {
widgetModel.updateCameraSource(cameraIndex, lensType);
ShootPhotoWidget shootPhotoWidget = getShootPhotoWidget();
if (shootPhotoWidget != null) {
shootPhotoWidget.updateCameraSource(cameraIndex, lensType);
}
RecordVideoWidget recordVideoWidget = getRecordVideoWidget();
if (recordVideoWidget != null) {
recordVideoWidget.updateCameraSource(cameraIndex, lensType);
}
}
//region private helpers
private void onCameraModeChange(CameraMode cameraMode) {
for (View view : widgetMap.values()) {
if (view != null) view.setVisibility(INVISIBLE);
}
View currentView = widgetMap.get(cameraMode);
if (currentView != null) {
widgetMap.get(cameraMode).setVisibility(VISIBLE);
}
}
//endregion
//region customizations
/**
* Add view to be shown based on camera mode
*
* @param cameraMode instance of camera mode
* @param view the view to be shown for camera mode
*/
public void addViewByMode(@NonNull CameraMode cameraMode, @NonNull View view) {
if (widgetMap.get(cameraMode) != null) {
removeView(widgetMap.get(cameraMode));
}
widgetMap.put(cameraMode, view);
view.setVisibility(INVISIBLE);
addView(view);
ConstraintLayout.LayoutParams lp = new Constraints.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
view.setLayoutParams(lp);
}
/**
* Remove the view based on camera mode
*
* @param cameraMode for which the view should be removed
*/
public void removeViewByMode(@NonNull CameraMode cameraMode) {
if (widgetMap.get(cameraMode) == null) return;
removeView(widgetMap.get(cameraMode));
widgetMap.remove(cameraMode);
}
/**
* Get the view that will be shown based on camera mode
*
* @param cameraMode for which the view is shown
* @return View for the mode
*/
@Nullable
public View getViewByMode(@NonNull CameraMode cameraMode) {
return widgetMap.get(cameraMode);
}
/**
* Get shoot photo widget
*
* @return instance of {@link ShootPhotoWidget}
*/
@Nullable
public ShootPhotoWidget getShootPhotoWidget() {
if (widgetMap.get(CameraMode.PHOTO_NORMAL) == null || !(widgetMap.get(CameraMode.PHOTO_NORMAL) instanceof ShootPhotoWidget)) {
return null;
}
return (ShootPhotoWidget) widgetMap.get(CameraMode.PHOTO_NORMAL);
}
/**
* Get record video widget
*
* @return instance of {@link RecordVideoWidget}
*/
@Nullable
public RecordVideoWidget getRecordVideoWidget() {
if (widgetMap.get(CameraMode.VIDEO_NORMAL) == null || !(widgetMap.get(CameraMode.VIDEO_NORMAL) instanceof RecordVideoWidget)) {
return null;
}
return (RecordVideoWidget) widgetMap.get(CameraMode.VIDEO_NORMAL);
}
//endregion
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture;
import androidx.annotation.NonNull;
import dji.sdk.keyvalue.value.camera.CameraMode;
import dji.sdk.keyvalue.value.common.CameraLensType;
import dji.sdk.keyvalue.value.common.ComponentIndexType;
import dji.v5.ux.core.base.DJISDKModel;
import dji.v5.ux.core.base.ICameraIndex;
import dji.v5.ux.core.base.WidgetModel;
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore;
import dji.v5.ux.core.module.FlatCameraModule;
import io.reactivex.rxjava3.core.Flowable;
/**
* Camera Capture Widget Model
* <p>
* Widget Model for {@link CameraCaptureWidget} used to define underlying logic
* and communication
*/
public class CameraCaptureWidgetModel extends WidgetModel implements ICameraIndex {
//region Fields
private FlatCameraModule flatCameraModule;
private ComponentIndexType cameraIndex = ComponentIndexType.LEFT_OR_MAIN;
private CameraLensType lensType = CameraLensType.CAMERA_LENS_ZOOM;
//region Lifecycle
public CameraCaptureWidgetModel(@NonNull DJISDKModel djiSdkModel,
@NonNull ObservableInMemoryKeyedStore keyedStore) {
super(djiSdkModel, keyedStore);
flatCameraModule = new FlatCameraModule();
addModule(flatCameraModule);
}
@Override
protected void inSetup() {
// do nothing
}
@Override
protected void inCleanup() {
// do nothing
}
@Override
public void updateCameraSource(@NonNull ComponentIndexType cameraIndex, @NonNull CameraLensType lensType) {
this.cameraIndex = cameraIndex;
this.lensType = lensType;
flatCameraModule.updateCameraSource(cameraIndex,lensType);
restart();
}
@NonNull
public ComponentIndexType getCameraIndex() {
return cameraIndex;
}
@NonNull
public CameraLensType getLensType() {
return lensType;
}
@Override
protected void updateStates() {
// Empty function
}
//endregion
//region Data
/**
* Get the current camera Mode
*
* @return Flowable with {@link CameraMode} instance
*/
public Flowable<CameraMode> getCameraMode() {
return flatCameraModule.getCameraModeDataProcessor().toFlowable();
}
//endregion
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.recordvideo;
import dji.sdk.keyvalue.value.camera.CameraSDCardState;
import dji.sdk.keyvalue.value.camera.CameraStorageLocation;
import dji.sdk.keyvalue.value.camera.SSDOperationState;
/**
* Class represents storage state for SD card and internal storage
* when camera is in record video mode
*/
public class CameraSDVideoStorageState extends CameraVideoStorageState {
private CameraSDCardState sdCardOperationState;
public CameraSDVideoStorageState(CameraStorageLocation storageLocation, int availableRecordingTimeInSeconds, CameraSDCardState sdCardOperationState) {
super(storageLocation, availableRecordingTimeInSeconds);
this.sdCardOperationState = sdCardOperationState;
}
/**
* Get the operation state of current storage
*
* @return instance of {@link SSDOperationState} representing state for SDCard or internal Storage
*/
public CameraSDCardState getSdCardOperationState() {
return sdCardOperationState;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CameraSDVideoStorageState)) return false;
if (!super.equals(o)) return false;
CameraSDVideoStorageState that = (CameraSDVideoStorageState) o;
return getSdCardOperationState() == that.getSdCardOperationState();
}
@Override
public int hashCode() {
return 31 * getSdCardOperationState().value();
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.recordvideo;
import dji.sdk.keyvalue.value.camera.CameraStorageLocation;
import dji.sdk.keyvalue.value.camera.SSDOperationState;
/**
* Class represents SSD Storage state for Record Video Mode
*/
public class CameraSSDVideoStorageState extends CameraVideoStorageState {
private SSDOperationState ssdOperationState;
public CameraSSDVideoStorageState(CameraStorageLocation storageLocation, int availableRecordingTimeInSeconds, SSDOperationState ssdOperationState) {
super(storageLocation, availableRecordingTimeInSeconds);
this.ssdOperationState = ssdOperationState;
}
/**
* Get operation state of SSD
*
* @return {@link SSDOperationState}
*/
public SSDOperationState getSsdOperationState() {
return ssdOperationState;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CameraSSDVideoStorageState)) return false;
if (!super.equals(o)) return false;
CameraSSDVideoStorageState that = (CameraSSDVideoStorageState) o;
return getSsdOperationState() == that.getSsdOperationState();
}
@Override
public int hashCode() {
return 31 * getSsdOperationState().value();
}
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.recordvideo;
import dji.sdk.keyvalue.value.camera.CameraStorageLocation;
/**
* Class represents the storage state when camera is in Record Video Mode
* The state determines the storage location and the available space
* in terms of duration in seconds
*/
public abstract class CameraVideoStorageState {
private final CameraStorageLocation storageLocation;
private final int availableRecordingTimeInSeconds;
protected CameraVideoStorageState(CameraStorageLocation storageLocation, int availableRecordingTimeInSeconds) {
this.storageLocation = storageLocation;
this.availableRecordingTimeInSeconds = availableRecordingTimeInSeconds;
}
/**
* Get the current storage location
*
* @return instance of CameraStorageLocation
*/
public CameraStorageLocation getStorageLocation() {
return storageLocation;
}
/**
* Get the duration in seconds that can be recorded before running out of storage
*
* @return integer value representing seconds
*/
public int getAvailableRecordingTimeInSeconds() {
return availableRecordingTimeInSeconds;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CameraVideoStorageState)) return false;
CameraVideoStorageState that = (CameraVideoStorageState) o;
return getAvailableRecordingTimeInSeconds() == that.getAvailableRecordingTimeInSeconds() &&
getStorageLocation() == that.getStorageLocation();
}
@Override
public int hashCode() {
int result = 31 * getStorageLocation().value();
result = result + 31 * getAvailableRecordingTimeInSeconds();
return result;
}
}

View File

@ -0,0 +1,669 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.recordvideo;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.HashMap;
import java.util.Map;
import androidx.annotation.ColorInt;
import androidx.annotation.Dimension;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import dji.sdk.keyvalue.value.camera.CameraSDCardState;
import dji.sdk.keyvalue.value.camera.CameraStorageLocation;
import dji.sdk.keyvalue.value.camera.SSDOperationState;
import dji.sdk.keyvalue.value.common.CameraLensType;
import dji.sdk.keyvalue.value.common.ComponentIndexType;
import dji.v5.ux.R;
import dji.v5.ux.cameracore.util.CameraActionSound;
import dji.v5.ux.cameracore.widget.cameracapture.recordvideo.RecordVideoWidgetModel.RecordingState;
import dji.v5.ux.cameracore.widget.cameracontrols.RemoteControllerButtonDownModel;
import dji.v5.ux.core.base.DJISDKModel;
import dji.v5.ux.core.base.ICameraIndex;
import dji.v5.ux.core.base.SchedulerProvider;
import dji.v5.ux.core.base.widget.ConstraintLayoutWidget;
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore;
import dji.v5.ux.core.util.CameraUtil;
import dji.v5.ux.core.util.UxErrorHandle;
import io.reactivex.rxjava3.core.Completable;
/**
* Record Video Widget
* <p>
* Widget can be used for recording video. The widget displays the current video mode. It also
* displays the storage state and errors associated with it.
*/
public class RecordVideoWidget extends ConstraintLayoutWidget<Object> implements OnClickListener, ICameraIndex {
//region Fields
private static final String TAG = "RecordVideoWidget";
private RecordVideoWidgetModel widgetModel;
private RemoteControllerButtonDownModel buttonDownModel;
private ImageView centerImageView;
private TextView videoTimerTextView;
private ImageView storageStatusOverlayImageView;
private Map<StorageIconState, Drawable> storageInternalIconMap;
private Map<StorageIconState, Drawable> storageSSDIconMap;
private Map<StorageIconState, Drawable> storageSDCardIconMap;
private Drawable recordVideoStartDrawable;
private Drawable recordVideoStopDrawable;
private CameraActionSound cameraActionSound;
//endregion
//region Lifecycle
public RecordVideoWidget(Context context) {
super(context);
}
public RecordVideoWidget(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RecordVideoWidget(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
inflate(context, R.layout.uxsdk_widget_record_video, this);
centerImageView = findViewById(R.id.image_view_center);
videoTimerTextView = findViewById(R.id.text_view_video_record_time);
storageStatusOverlayImageView = findViewById(R.id.image_view_storage_status_overlay);
storageInternalIconMap = new HashMap<>();
storageSSDIconMap = new HashMap<>();
storageSDCardIconMap = new HashMap<>();
centerImageView.setOnClickListener(this);
cameraActionSound = new CameraActionSound(context);
if (!isInEditMode()) {
widgetModel = new RecordVideoWidgetModel(DJISDKModel.getInstance(), ObservableInMemoryKeyedStore.getInstance());
buttonDownModel = new RemoteControllerButtonDownModel(DJISDKModel.getInstance(), ObservableInMemoryKeyedStore.getInstance());
}
initDefaults();
if (attrs != null) {
initAttributes(context, attrs);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!isInEditMode()) {
widgetModel.setup();
buttonDownModel.setup();
}
}
@Override
protected void onDetachedFromWindow() {
if (!isInEditMode()) {
widgetModel.cleanup();
buttonDownModel.cleanup();
}
super.onDetachedFromWindow();
}
@Override
protected void reactToModelChanges() {
addReaction(widgetModel.getRecordingTimeInSeconds()
.observeOn(SchedulerProvider.ui())
.subscribe(this::updateRecordingTime, UxErrorHandle.logErrorConsumer(TAG, "record time: ")));
addReaction(widgetModel.getRecordingState()
.observeOn(SchedulerProvider.ui())
.subscribe(recordingState -> onIsRecordingVideoChange(recordingState, true), UxErrorHandle.logErrorConsumer(TAG, "is recording: ")));
addReaction(widgetModel.getCameraVideoStorageState()
.observeOn(SchedulerProvider.ui())
.subscribe(this::updateCameraForegroundResource, UxErrorHandle.logErrorConsumer(TAG, "camera storage update: ")));
addReaction(buttonDownModel.isRecordButtonDownProcessor().toFlowable()
.observeOn(SchedulerProvider.ui())
.subscribe(aBoolean -> {
if (aBoolean == Boolean.TRUE) {
actionOnRecording();
}
}));
}
@NonNull
@Override
public String getIdealDimensionRatioString() {
return getResources().getString(R.string.uxsdk_widget_default_ratio);
}
@NonNull
public ComponentIndexType getCameraIndex() {
return widgetModel.getCameraIndex();
}
@NonNull
@Override
public CameraLensType getLensType() {
return widgetModel.getLensType();
}
@Override
public void updateCameraSource(@NonNull ComponentIndexType cameraIndex, @NonNull CameraLensType lensType) {
if (!isInEditMode()) {
widgetModel.updateCameraSource(cameraIndex, lensType);
}
}
@Override
public void onClick(View v) {
if (v.equals(centerImageView)) {
actionOnRecording();
}
}
private void actionOnRecording() {
if (!widgetModel.isVideoMode()) {
return;
}
widgetModel.getRecordingState().firstOrError().flatMapCompletable(recordingState -> {
if (recordingState == RecordingState.RECORDING_IN_PROGRESS) {
return widgetModel.stopRecordVideo();
} else if (recordingState == RecordingState.RECORDING_STOPPED || recordingState == RecordingState.RECORDING_NOT_STARED) {
return widgetModel.startRecordVideo();
} else {
return Completable.complete();
}
}).observeOn(SchedulerProvider.ui()).subscribe(() -> {
}, UxErrorHandle.logErrorConsumer(TAG, "START STOP VIDEO"));
}
//region private helpers
private void initDefaults() {
recordVideoStartDrawable = getResources().getDrawable(R.drawable.uxsdk_selector_start_record_video);
recordVideoStopDrawable = getResources().getDrawable(R.drawable.uxsdk_selector_stop_record_video);
setInternalStorageIcon(StorageIconState.NOT_INSERTED, R.drawable.uxsdk_ic_internal_storage_not_inserted);
setInternalStorageIcon(StorageIconState.SLOW, R.drawable.uxsdk_ic_internal_storage_slow);
setInternalStorageIcon(StorageIconState.FULL, R.drawable.uxsdk_ic_internal_storage_full);
setSDCardStorageIcon(StorageIconState.NOT_INSERTED, R.drawable.uxsdk_ic_sdcard_not_inserted);
setSDCardStorageIcon(StorageIconState.SLOW, R.drawable.uxsdk_ic_sdcard_slow);
setSDCardStorageIcon(StorageIconState.FULL, R.drawable.uxsdk_ic_sdcard_full);
setSSDStorageIcon(StorageIconState.NOT_INSERTED, R.drawable.uxsdk_ic_ssd_not_inserted);
setSSDStorageIcon(StorageIconState.FULL, R.drawable.uxsdk_ic_ssd_full);
setVideoTimerTextColor(Color.WHITE);
}
private void initAttributes(@NonNull Context context, @NonNull AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RecordVideoWidget);
updateCameraSource(ComponentIndexType.find(typedArray.getInt(R.styleable.RecordVideoWidget_uxsdk_cameraIndex, 0)),
CameraLensType.find(typedArray.getInt(R.styleable.RecordVideoWidget_uxsdk_lensType, 0)));
int textAppearance = typedArray.getResourceId(R.styleable.RecordVideoWidget_uxsdk_timerTextAppearance, INVALID_RESOURCE);
if (textAppearance != INVALID_RESOURCE) {
setTimerTextAppearance(textAppearance);
}
setVideoTimerTextColor(typedArray.getColor(R.styleable.RecordVideoWidget_uxsdk_timerTextColor, Color.WHITE));
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_timerTextBackground) != null) {
setVideoTimerTextBackground(typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_timerTextBackground));
}
setVideoTimerTextSize(typedArray.getDimension(R.styleable.RecordVideoWidget_uxsdk_timerTextSize, 12));
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_foregroundIconBackground) != null) {
setForegroundIconBackground(typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_foregroundIconBackground));
}
initRecordVideoDrawable(typedArray);
initInternalStorageIcon(typedArray);
initSDCardStorageIcon(typedArray);
initSSDStorageIcon(typedArray);
typedArray.recycle();
}
private void initSSDStorageIcon(TypedArray typedArray) {
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_ssdNotInsertedIcon) != null) {
setSSDStorageIcon(StorageIconState.NOT_INSERTED, typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_ssdNotInsertedIcon));
}
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_ssdFullIcon) != null) {
setSSDStorageIcon(StorageIconState.FULL, typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_ssdFullIcon));
}
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_ssdSlowIcon) != null) {
setSSDStorageIcon(StorageIconState.SLOW, typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_ssdSlowIcon));
}
}
private void initSDCardStorageIcon(TypedArray typedArray) {
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_sdCardNotInsertedIcon) != null) {
setSDCardStorageIcon(StorageIconState.NOT_INSERTED, typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_sdCardNotInsertedIcon));
}
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_sdCardFullIcon) != null) {
setSDCardStorageIcon(StorageIconState.FULL, typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_sdCardFullIcon));
}
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_sdCardSlowIcon) != null) {
setSDCardStorageIcon(StorageIconState.SLOW, typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_sdCardSlowIcon));
}
}
private void initInternalStorageIcon(TypedArray typedArray) {
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_internalStorageNotInsertedIcon) != null) {
setInternalStorageIcon(StorageIconState.NOT_INSERTED, typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_internalStorageNotInsertedIcon));
}
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_internalStorageFullIcon) != null) {
setInternalStorageIcon(StorageIconState.FULL, typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_internalStorageFullIcon));
}
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_internalStorageSlowIcon) != null) {
setInternalStorageIcon(StorageIconState.SLOW, typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_internalStorageSlowIcon));
}
}
private void initRecordVideoDrawable(TypedArray typedArray) {
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_recordStartIcon) != null) {
recordVideoStartDrawable = typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_recordStartIcon);
}
if (typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_recordStopIcon) != null) {
recordVideoStopDrawable = typedArray.getDrawable(R.styleable.RecordVideoWidget_uxsdk_recordStopIcon);
}
}
private void updateCameraForegroundResource(CameraVideoStorageState cameraVideoStorageState) {
Drawable foregroundResource = null;
if (cameraVideoStorageState instanceof CameraSDVideoStorageState) {
if (cameraVideoStorageState.getStorageLocation() == CameraStorageLocation.SDCARD) {
foregroundResource = updateResourceWithStorageInSDCard(cameraVideoStorageState);
} else if (cameraVideoStorageState.getStorageLocation() == CameraStorageLocation.INTERNAL) {
foregroundResource = updateResourceWithStorageInternal(cameraVideoStorageState);
}
} else if (cameraVideoStorageState instanceof CameraSSDVideoStorageState) {
if (((CameraSSDVideoStorageState) cameraVideoStorageState).getSsdOperationState()
== SSDOperationState.NOT_FOUND) {
foregroundResource = getSSDStorageIcon(StorageIconState.NOT_INSERTED);
} else if (((CameraSSDVideoStorageState) cameraVideoStorageState).getSsdOperationState()
== SSDOperationState.FULL) {
foregroundResource = getSSDStorageIcon(StorageIconState.FULL);
}
}
storageStatusOverlayImageView.setImageDrawable(foregroundResource);
}
private Drawable updateResourceWithStorageInSDCard(CameraVideoStorageState cameraVideoStorageState) {
Drawable foregroundResource = null;
if (((CameraSDVideoStorageState) cameraVideoStorageState).getSdCardOperationState()
== CameraSDCardState.NOT_INSERTED) {
foregroundResource = getSDCardStorageIcon(StorageIconState.NOT_INSERTED);
} else if (((CameraSDVideoStorageState) cameraVideoStorageState).getSdCardOperationState()
== CameraSDCardState.FULL) {
foregroundResource = getSDCardStorageIcon(StorageIconState.FULL);
} else if (((CameraSDVideoStorageState) cameraVideoStorageState).getSdCardOperationState()
== CameraSDCardState.SLOW) {
foregroundResource = getSDCardStorageIcon(StorageIconState.SLOW);
}
return foregroundResource;
}
private Drawable updateResourceWithStorageInternal(CameraVideoStorageState cameraVideoStorageState) {
Drawable foregroundResource = null;
if (((CameraSDVideoStorageState) cameraVideoStorageState).getSdCardOperationState()
== CameraSDCardState.NOT_INSERTED) {
foregroundResource = getInternalStorageIcon(StorageIconState.NOT_INSERTED);
} else if (((CameraSDVideoStorageState) cameraVideoStorageState).getSdCardOperationState()
== CameraSDCardState.FULL) {
foregroundResource = getInternalStorageIcon(StorageIconState.FULL);
} else if (((CameraSDVideoStorageState) cameraVideoStorageState).getSdCardOperationState()
== CameraSDCardState.SLOW) {
foregroundResource = getInternalStorageIcon(StorageIconState.SLOW);
}
return foregroundResource;
}
private void updateRecordingTime(int seconds) {
videoTimerTextView.setText(CameraUtil.formatVideoTime(getResources(), seconds));
}
private void onIsRecordingVideoChange(RecordingState recordingState, boolean playSound) {
boolean isRecordingVideo = recordingState == RecordingState.RECORDING_IN_PROGRESS;
Drawable recordStart = recordVideoStartDrawable;
Drawable recordStop = recordVideoStopDrawable;
centerImageView.setImageDrawable(isRecordingVideo ? recordStop : recordStart);
videoTimerTextView.setVisibility(isRecordingVideo ? View.VISIBLE : View.INVISIBLE);
storageStatusOverlayImageView.setVisibility(isRecordingVideo ? View.GONE : View.VISIBLE);
if (playSound) {
if (recordingState == RecordingState.RECORDING_IN_PROGRESS) {
cameraActionSound.playStartRecordVideo();
} else if (recordingState == RecordingState.RECORDING_STOPPED) {
cameraActionSound.playStopRecordVideo();
}
}
}
private void checkAndUpdateCameraForegroundResource() {
if (!isInEditMode()) {
addDisposable(widgetModel.getCameraVideoStorageState().firstOrError()
.observeOn(SchedulerProvider.ui())
.subscribe(this::updateCameraForegroundResource,
UxErrorHandle.logErrorConsumer(TAG, "check and update camera foreground resource: ")));
}
}
private void checkAndUpdateCenterImageView() {
if (!isInEditMode()) {
addDisposable(widgetModel.getRecordingState().firstOrError()
.observeOn(SchedulerProvider.ui())
.subscribe(recordingState -> onIsRecordingVideoChange(recordingState, false),
UxErrorHandle.logErrorConsumer(TAG, "check and update camera foreground resource: ")));
}
}
//endregion
//region customizations
/**
* Get the current start recording video icon
*
* @return Drawable currently used
*/
@Nullable
public Drawable getRecordVideoStartDrawable() {
return recordVideoStartDrawable;
}
/**
* Set the start record video icon
*
* @param resourceId to be used
*/
public void setRecordVideoStartDrawable(@DrawableRes int resourceId) {
setRecordVideoStartDrawable(getResources().getDrawable(resourceId));
}
/**
* Set the start record video icon
*
* @param drawable to be used
*/
public void setRecordVideoStartDrawable(@Nullable Drawable drawable) {
recordVideoStartDrawable = drawable;
checkAndUpdateCenterImageView();
}
/**
* Get the current stop video recording icon
*
* @return Drawable currently used
*/
@Nullable
public Drawable getRecordVideoStopDrawable() {
return recordVideoStopDrawable;
}
/**
* Set stop video recording icon
*
* @param resourceId to be used
*/
public void setRecordVideoStopDrawable(@DrawableRes int resourceId) {
setRecordVideoStopDrawable(getResources().getDrawable(resourceId));
}
/**
* Set stop video recording icon
*
* @param drawable to be used
*/
public void setRecordVideoStopDrawable(@Nullable Drawable drawable) {
recordVideoStopDrawable = drawable;
checkAndUpdateCenterImageView();
}
/**
* Set the icon for internal storage based on storage icon state
*
* @param storageIconState for which icon is used
* @param resourceId to be used
*/
public void setInternalStorageIcon(@NonNull StorageIconState storageIconState, @DrawableRes int resourceId) {
setInternalStorageIcon(storageIconState, getResources().getDrawable(resourceId));
}
/**
* Set the icon for internal storage based on storage icon state
*
* @param storageIconState for which icon is used
* @param drawable to be used
*/
public void setInternalStorageIcon(@NonNull StorageIconState storageIconState, @Nullable Drawable drawable) {
storageInternalIconMap.put(storageIconState, drawable);
checkAndUpdateCameraForegroundResource();
}
/**
* Get the icon for internal storage based on storage icon state
*
* @param storageIconState for which icon is used
* @return Drawable currently used
*/
@Nullable
public Drawable getInternalStorageIcon(@NonNull StorageIconState storageIconState) {
return storageInternalIconMap.get(storageIconState);
}
/**
* Set the icon for SD card storage based on storage icon state
*
* @param storageIconState for which icon is used
* @param resourceId to be used
*/
public void setSDCardStorageIcon(@NonNull StorageIconState storageIconState, @DrawableRes int resourceId) {
setSDCardStorageIcon(storageIconState, getResources().getDrawable(resourceId));
}
/**
* Set the icon for SD card storage based on storage icon state
*
* @param storageIconState for which icon is used
* @param drawable to be used
*/
public void setSDCardStorageIcon(@NonNull StorageIconState storageIconState, @Nullable Drawable drawable) {
storageSDCardIconMap.put(storageIconState, drawable);
checkAndUpdateCameraForegroundResource();
}
/**
* Get the icon for SD card storage based on storage icon state
*
* @param storageIconState for which icon is used
* @return Drawable currently used
*/
@Nullable
public Drawable getSDCardStorageIcon(@NonNull StorageIconState storageIconState) {
return storageSDCardIconMap.get(storageIconState);
}
/**
* Set the icon for SSD storage based on storage icon state
*
* @param storageIconState for which icon should be used
* @param resourceId to be used
*/
public void setSSDStorageIcon(@NonNull StorageIconState storageIconState, @DrawableRes int resourceId) {
setSSDStorageIcon(storageIconState, getResources().getDrawable(resourceId));
}
/**
* Set the icon for SSD storage based on storage icon state
*
* @param storageIconState for which icon should be used
* @param drawable Drawable to be used
*/
public void setSSDStorageIcon(@NonNull StorageIconState storageIconState, @Nullable Drawable drawable) {
storageSSDIconMap.put(storageIconState, drawable);
checkAndUpdateCameraForegroundResource();
}
/**
* Get the icon for SSD storage based on storage icon state
*
* @param storageIconState for which icon is used
* @return Drawable currently used
*/
@Nullable
public Drawable getSSDStorageIcon(@NonNull StorageIconState storageIconState) {
return storageSSDIconMap.get(storageIconState);
}
/**
* Set the background of the foreground icon
*
* @param resourceId to be used as background
*/
public void setForegroundIconBackground(@DrawableRes int resourceId) {
storageStatusOverlayImageView.setBackgroundResource(resourceId);
}
/**
* Set background of the foreground icon
*
* @param drawable to be used as background
*/
public void setForegroundIconBackground(@Nullable Drawable drawable) {
storageStatusOverlayImageView.setBackground(drawable);
}
/**
* Get current background of foreground icon
*
* @return Drawable being used
*/
public Drawable getForegroundIconBackground() {
return storageStatusOverlayImageView.getBackground();
}
/**
* Set the background of the video record duration text
*
* @param resourceId to be used
*/
public void setVideoTimerTextBackground(@DrawableRes int resourceId) {
videoTimerTextView.setBackgroundResource(resourceId);
}
/**
* Set the background of the video record duration text
*
* @param drawable to be used
*/
public void setVideoTimerTextBackground(@Nullable Drawable drawable) {
videoTimerTextView.setBackground(drawable);
}
/**
* Get the color state list currently used for video record duration text
*
* @return ColorSateList
*/
@Nullable
public ColorStateList getVideoTimerTextColors() {
return videoTimerTextView.getTextColors();
}
/**
* Set the color state list for video record duration text
*
* @param colorStateList to be used
*/
public void setVideoTimerTextColors(@Nullable ColorStateList colorStateList) {
videoTimerTextView.setTextColor(colorStateList);
}
/**
* Get the current text color of video record duration text
*
* @return integer value representing color
*/
@ColorInt
public int getVideoTimerTextColor() {
return videoTimerTextView.getCurrentTextColor();
}
/**
* Set the text color of video record duration text
*
* @param color integer value representing color
*/
public void setVideoTimerTextColor(@ColorInt int color) {
videoTimerTextView.setTextColor(color);
}
/**
* Get the current text size of video record duration text
*
* @return float value representing text size
*/
@Dimension
public float getVideoTimerTextSize() {
return videoTimerTextView.getTextSize();
}
/**
* Set the text size of video record duration text
*
* @param textSize float value
*/
public void setVideoTimerTextSize(@Dimension float textSize) {
videoTimerTextView.setTextSize(textSize);
}
/**
* Set the text appearance for video record duration text
*
* @param textAppearance to be used
*/
public void setTimerTextAppearance(@StyleRes int textAppearance) {
videoTimerTextView.setTextAppearance(getContext(), textAppearance);
}
//endregion
/**
* Enum indicating storage error state
*/
public enum StorageIconState {
/**
* The storage is slow
*/
SLOW,
/**
* The storage is full
*/
FULL,
/**
* The storage is not inserted
*/
NOT_INSERTED
}
}

View File

@ -0,0 +1,330 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.recordvideo;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import dji.sdk.keyvalue.key.CameraKey;
import dji.sdk.keyvalue.key.KeyTools;
import dji.sdk.keyvalue.value.camera.CameraSDCardState;
import dji.sdk.keyvalue.value.camera.CameraStorageInfo;
import dji.sdk.keyvalue.value.camera.CameraStorageInfos;
import dji.sdk.keyvalue.value.camera.CameraStorageLocation;
import dji.sdk.keyvalue.value.camera.CameraType;
import dji.sdk.keyvalue.value.camera.SSDOperationState;
import dji.sdk.keyvalue.value.camera.VideoFrameRate;
import dji.sdk.keyvalue.value.camera.VideoResolution;
import dji.sdk.keyvalue.value.camera.VideoResolutionFrameRate;
import dji.sdk.keyvalue.value.common.CameraLensType;
import dji.sdk.keyvalue.value.common.ComponentIndexType;
import dji.v5.ux.core.base.DJISDKModel;
import dji.v5.ux.core.base.ICameraIndex;
import dji.v5.ux.core.base.WidgetModel;
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore;
import dji.v5.ux.core.module.FlatCameraModule;
import dji.v5.ux.core.util.DataProcessor;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
/**
* Record Video Widget Model
* <p>
* Widget Model for {@link RecordVideoWidget} used to define underlying
* logic and communication
*/
public class RecordVideoWidgetModel extends WidgetModel implements ICameraIndex {
private static final int INVALID_AVAILABLE_RECORDING_TIME = -1;
private static final int MAX_VIDEO_TIME_THRESHOLD_MINUTES = 29;
private static final int SECONDS_PER_MIN = 60;
private final DataProcessor<CameraVideoStorageState> cameraVideoStorageState;
private final DataProcessor<Boolean> isRecording;
private final DataProcessor<String> cameraDisplayName;
private final DataProcessor<CameraType> cameraType;
private final DataProcessor<Integer> recordingTimeInSeconds;
private final DataProcessor<VideoResolutionFrameRate> recordedVideoParameters;
private final DataProcessor<VideoResolutionFrameRate> nonSSDRecordedVideoParameters;
private final DataProcessor<VideoResolutionFrameRate> ssdRecordedVideoParameters;
private final DataProcessor<CameraStorageLocation> storageLocation;
private final DataProcessor<CameraSDCardState> sdCardState;
private final DataProcessor<CameraSDCardState> storageState;
private final DataProcessor<CameraSDCardState> innerStorageState;
private final DataProcessor<SSDOperationState> ssdState;
private final DataProcessor<CameraStorageInfos> cameraStorageInfos;
private final DataProcessor<RecordingState> recordingStateProcessor;
private final FlatCameraModule flatCameraModule;
private ComponentIndexType cameraIndex = ComponentIndexType.LEFT_OR_MAIN;
private CameraLensType lensType = CameraLensType.CAMERA_LENS_ZOOM;
private boolean lastIsRecording = false;
//endregion
//region Constructor
public RecordVideoWidgetModel(@NonNull DJISDKModel djiSdkModel,
@NonNull ObservableInMemoryKeyedStore uxKeyManager) {
super(djiSdkModel, uxKeyManager);
CameraSDVideoStorageState cameraSDVideoStorageState = new CameraSDVideoStorageState(
CameraStorageLocation.SDCARD,
0,
CameraSDCardState.NOT_INSERTED);
cameraVideoStorageState = DataProcessor.create(cameraSDVideoStorageState);
isRecording = DataProcessor.create(false);
cameraDisplayName = DataProcessor.create("");
cameraType = DataProcessor.create(CameraType.NOT_SUPPORTED);
recordingTimeInSeconds = DataProcessor.create(0);
VideoResolutionFrameRate resolutionAndFrameRate = new VideoResolutionFrameRate(
VideoResolution.UNKNOWN,
VideoFrameRate.UNKNOWN);
recordedVideoParameters = DataProcessor.create(resolutionAndFrameRate);
nonSSDRecordedVideoParameters = DataProcessor.create(resolutionAndFrameRate);
ssdRecordedVideoParameters = DataProcessor.create(resolutionAndFrameRate);
storageLocation = DataProcessor.create(CameraStorageLocation.SDCARD);
sdCardState = DataProcessor.create(CameraSDCardState.UNKNOWN_ERROR);
storageState = DataProcessor.create(CameraSDCardState.NORMAL);
innerStorageState = DataProcessor.create(CameraSDCardState.UNKNOWN_ERROR);
ssdState = DataProcessor.create(SSDOperationState.UNKNOWN);
cameraStorageInfos = DataProcessor.create(new CameraStorageInfos(CameraStorageLocation.UNKNOWN, new ArrayList<>()));
recordingStateProcessor = DataProcessor.create(RecordingState.UNKNOWN);
flatCameraModule = new FlatCameraModule();
addModule(flatCameraModule);
}
//endregion
//region Lifecycle
@Override
protected void inSetup() {
bindDataProcessor(KeyTools.createKey(CameraKey.KeyIsRecording, cameraIndex), isRecording, newValue -> {
if (newValue == null) {
recordingStateProcessor.onNext(RecordingState.UNKNOWN);
return;
}
if (lastIsRecording && !newValue) { //只有从ture变化为false时才发stopped
recordingStateProcessor.onNext(RecordingState.RECORDING_STOPPED);
} else if (newValue) {
recordingStateProcessor.onNext(RecordingState.RECORDING_IN_PROGRESS);
} else {
recordingStateProcessor.onNext(RecordingState.RECORDING_NOT_STARED);
}
lastIsRecording = newValue;
});
bindDataProcessor(KeyTools.createKey(CameraKey.KeyRecordingTime, cameraIndex), recordingTimeInSeconds);
bindDataProcessor(KeyTools.createKey(CameraKey.KeyCameraType, cameraIndex), cameraType, type -> cameraDisplayName.onNext(type.name()));
bindDataProcessor(KeyTools.createKey(CameraKey.KeyCameraStorageLocation, cameraIndex), storageLocation);
bindDataProcessor(KeyTools.createKey(CameraKey.KeyCameraSDCardState, cameraIndex), sdCardState);
bindDataProcessor(KeyTools.createKey(CameraKey.KeyInternalStorageState, cameraIndex), innerStorageState);
bindDataProcessor(KeyTools.createKey(CameraKey.KeySSDOperationState, cameraIndex), ssdState);
bindDataProcessor(KeyTools.createKey(CameraKey.KeyCameraStorageInfos, cameraIndex), cameraStorageInfos);
// Resolution and Frame Rates
bindDataProcessor(KeyTools.createCameraKey(CameraKey.KeyVideoResolutionFrameRate, cameraIndex, lensType), nonSSDRecordedVideoParameters);
bindDataProcessor(KeyTools.createCameraKey(CameraKey.KeySSDVideoResolutionFrameRate, cameraIndex, lensType), ssdRecordedVideoParameters);
}
@Override
protected void inCleanup() {
// Do nothing
}
@Override
protected void updateStates() {
updateVideoStorageState();
if (isRecording.getValue()) {
checkIsOverRecordTime(recordingTimeInSeconds.getValue());
}
}
@Override
protected void onProductConnectionChanged(boolean isConnected) {
super.onProductConnectionChanged(isConnected);
if (!isConnected) {
recordingStateProcessor.onNext(RecordingState.UNKNOWN);
}
}
public boolean isVideoMode() {
return flatCameraModule.getCameraModeDataProcessor().getValue().isVideoMode();
}
/**
* Get the current camera video storage state
*
* @return Flowable with {@link CameraVideoStorageState} instance
*/
public Flowable<CameraVideoStorageState> getCameraVideoStorageState() {
return cameraVideoStorageState.toFlowable();
}
/**
* Check the recording state of the camera
*
* @return Flowable with {@link RecordingState} value
*/
public Flowable<RecordingState> getRecordingState() {
return recordingStateProcessor.toFlowable();
}
/**
* Get the display name of the camera which the model is reacting to
*
* @return Flowable with string of camera name
*/
public Flowable<String> getCameraDisplayName() {
return cameraDisplayName.toFlowable();
}
/**
* Get the duration of on going video recording in seconds
*
* @return Flowable with integer value representing seconds
*/
public Flowable<Integer> getRecordingTimeInSeconds() {
return recordingTimeInSeconds.toFlowable();
}
@NonNull
public ComponentIndexType getCameraIndex() {
return cameraIndex;
}
@NonNull
@Override
public CameraLensType getLensType() {
return lensType;
}
@Override
public void updateCameraSource(@NonNull ComponentIndexType cameraIndex, @NonNull CameraLensType lensType) {
this.cameraIndex = cameraIndex;
this.lensType = lensType;
flatCameraModule.updateCameraSource(cameraIndex,lensType);
restart();
}
//endregion
//region Actions
/**
* Start video recording
*
* @return Completable to determine the status of the action
*/
public Completable startRecordVideo() {
if (isRecording.getValue()) {
return Completable.complete();
}
return djiSdkModel.performActionWithOutResult(KeyTools.createKey(CameraKey.KeyStartRecord, cameraIndex));
}
/**
* Stop video recording
*
* @return Completable to determine the status of the action
*/
public Completable stopRecordVideo() {
if (!isRecording.getValue()) {
return Completable.complete();
}
return djiSdkModel.performActionWithOutResult(KeyTools.createKey(CameraKey.KeyStopRecord, cameraIndex));
}
//endregion
//region Helpers
private void updateVideoStorageState() {
CameraStorageLocation currentStorageLocation = storageLocation.getValue();
if (CameraStorageLocation.UNKNOWN.equals(currentStorageLocation)) {
return;
}
int availableRecordingTime = getAvailableRecordingTime();
CameraVideoStorageState newCameraVideoStorageState = null;
if (CameraStorageLocation.SDCARD.equals(currentStorageLocation)) {
if (!CameraSDCardState.UNKNOWN_ERROR.equals(sdCardState.getValue())) {
newCameraVideoStorageState = new CameraSDVideoStorageState(currentStorageLocation, availableRecordingTime, sdCardState.getValue());
} else if (!CameraSDCardState.UNKNOWN_ERROR.equals(storageState.getValue())) {
newCameraVideoStorageState = new CameraSDVideoStorageState(currentStorageLocation, availableRecordingTime, storageState.getValue());
}
recordedVideoParameters.onNext(nonSSDRecordedVideoParameters.getValue());
} else if (CameraStorageLocation.INTERNAL.equals(currentStorageLocation)) {
newCameraVideoStorageState = new CameraSDVideoStorageState(currentStorageLocation, availableRecordingTime, innerStorageState.getValue());
}
if (newCameraVideoStorageState != null) {
cameraVideoStorageState.onNext(newCameraVideoStorageState);
}
}
private int getAvailableRecordingTime() {
CameraStorageInfo info = cameraStorageInfos.getValue().getCurrentCameraStorageInfo();
if (info == null) {
return INVALID_AVAILABLE_RECORDING_TIME;
}
Integer availableVideoDuration = info.getAvailableVideoDuration();
return availableVideoDuration == null ? INVALID_AVAILABLE_RECORDING_TIME : availableVideoDuration;
}
/**
* Determine if the time is exceeded, and close the video in
* {@link RecordVideoWidgetModel#MAX_VIDEO_TIME_THRESHOLD_MINUTES} minutes.
*
* @param recordTime The time to check.
*/
private void checkIsOverRecordTime(int recordTime) {
if (recordTime > (MAX_VIDEO_TIME_THRESHOLD_MINUTES * SECONDS_PER_MIN)) {
stopRecordVideo();
}
}
//endregion
//region Classes
/**
* The recording state of the camera.
*/
public enum RecordingState {
/**
* No product is connected, or the recording state is unknown.
*/
UNKNOWN,
RECORDING_NOT_STARED,
/**
* The camera is recording video.
*/
RECORDING_IN_PROGRESS,
/**
* The camera is not recording video.
*/
RECORDING_STOPPED
}
//endregion
}

View File

@ -0,0 +1,68 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.shootphoto;
import dji.sdk.keyvalue.value.camera.CameraShootPhotoMode;
import dji.sdk.keyvalue.value.camera.PhotoAEBPhotoCount;
/**
* Camera AEB Photo State
* <p>
* Class represents camera photo state. It will be returned when device is in
* CameraShootPhotoMode AEB mode.
* It will also return the aeb count
*/
public class CameraAEBPhotoState extends CameraPhotoState {
private PhotoAEBPhotoCount photoAEBCount;
public CameraAEBPhotoState(CameraShootPhotoMode shootPhotoMode, PhotoAEBPhotoCount photoAEBCount) {
super(shootPhotoMode);
this.photoAEBCount = photoAEBCount;
}
/**
* Get AEB photo count
*/
public PhotoAEBPhotoCount getPhotoAEBCount() {
return photoAEBCount;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CameraAEBPhotoState) {
return ((CameraAEBPhotoState) obj).getShootPhotoMode() == this.getShootPhotoMode()
&& ((CameraAEBPhotoState) obj).getPhotoAEBCount() == this.getPhotoAEBCount();
}
return false;
}
@Override
public int hashCode() {
int result = 31 * getShootPhotoMode().value();
result = result + 31 * getPhotoAEBCount().value();
return result;
}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.shootphoto;
import androidx.annotation.NonNull;
import dji.sdk.keyvalue.value.camera.CameraShootPhotoMode;
import dji.sdk.keyvalue.value.camera.PhotoBurstCount;
/**
* Camera Burst Photo State
* <p>
* Class represents camera photo state. It will be returned when device is in
* CameraShootPhotoMode photo burst mode.
* It will also return the burst count.
*/
public class CameraBurstPhotoState extends CameraPhotoState {
private PhotoBurstCount photoBurstCount;
public CameraBurstPhotoState(@NonNull CameraShootPhotoMode shootPhotoMode, @NonNull PhotoBurstCount photoBurstCount) {
super(shootPhotoMode);
this.photoBurstCount = photoBurstCount;
}
/**
* Get the burst count
*
* @return PhotoBurstCount
*/
public PhotoBurstCount getPhotoBurstCount() {
return photoBurstCount;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CameraBurstPhotoState) {
return ((CameraBurstPhotoState) obj).getShootPhotoMode() == this.getShootPhotoMode()
&& ((CameraBurstPhotoState) obj).getPhotoBurstCount() == this.getPhotoBurstCount();
}
return false;
}
@Override
public int hashCode() {
int result = 31 * getShootPhotoMode().value();
result = result + 31 * getPhotoBurstCount().value();
return result;
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.shootphoto;
import androidx.annotation.NonNull;
import dji.sdk.keyvalue.value.camera.CameraShootPhotoMode;
/**
* Camera Interval Photo State
* <p>
* Class represents camera photo state. It will be returned when device is in
* CameraShootPhotoMode interval mode.
* It will also return the capture count and interval duration in seconds
*/
public class CameraIntervalPhotoState extends CameraPhotoState {
private int captureCount;
private int timeIntervalInSeconds;
public CameraIntervalPhotoState(@NonNull CameraShootPhotoMode shootPhotoMode, int captureCount, int timeIntervalInSeconds) {
super(shootPhotoMode);
this.captureCount = captureCount;
this.timeIntervalInSeconds = timeIntervalInSeconds;
}
/**
* Get capture count
*
* @return int value
*/
public int getCaptureCount() {
return captureCount;
}
/**
* Get time interval in seconds
*
* @return int value
*/
public int getTimeIntervalInSeconds() {
return timeIntervalInSeconds;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CameraIntervalPhotoState) {
return ((CameraIntervalPhotoState) obj).getShootPhotoMode() == this.getShootPhotoMode()
&& ((CameraIntervalPhotoState) obj).getCaptureCount() == this.getCaptureCount()
&& ((CameraIntervalPhotoState) obj).getTimeIntervalInSeconds() == this.getTimeIntervalInSeconds();
}
return false;
}
@Override
public int hashCode() {
int result = 31 * getShootPhotoMode().value();
result = result + 31 * getCaptureCount();
result = result + 31 * getTimeIntervalInSeconds();
return result;
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.shootphoto;
import androidx.annotation.NonNull;
import dji.sdk.keyvalue.value.camera.CameraShootPhotoMode;
import dji.sdk.keyvalue.value.camera.PhotoPanoramaMode;
/**
* Camera Panorama Photo State
* <p>
* Class represents camera photo state. It will be returned when device is in
* CameraShootPhotoMode panorama mode.
* It will also return the value of PhotoPanoramaMode
*/
public class CameraPanoramaPhotoState extends CameraPhotoState {
private PhotoPanoramaMode photoPanoramaMode;
public CameraPanoramaPhotoState(@NonNull CameraShootPhotoMode shootPhotoMode, @NonNull PhotoPanoramaMode photoPanoramaMode) {
super(shootPhotoMode);
this.photoPanoramaMode = photoPanoramaMode;
}
/**
* Get the current panorama mode value
*
* @return instance of PhotoPanoramaMode
*/
public PhotoPanoramaMode getPhotoPanoramaMode() {
return photoPanoramaMode;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CameraPanoramaPhotoState) {
return ((CameraPanoramaPhotoState) obj).getShootPhotoMode() == this.getShootPhotoMode()
&& ((CameraPanoramaPhotoState) obj).getPhotoPanoramaMode() == this.getPhotoPanoramaMode();
}
return false;
}
@Override
public int hashCode() {
int result = 31 * getShootPhotoMode().value();
result = result + 31 * getPhotoPanoramaMode().value();
return result;
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.shootphoto;
import androidx.annotation.NonNull;
import dji.sdk.keyvalue.value.camera.CameraShootPhotoMode;
/**
* Camera Photo State
* <p>
* Class will be returned representing CameraShootPhotoMode
*/
public class CameraPhotoState {
private CameraShootPhotoMode shootPhotoMode;
public CameraPhotoState(CameraShootPhotoMode shootPhotoMode) {
this.shootPhotoMode = shootPhotoMode;
}
/**
* Get the current shoot photo phone
*
* @return instance of CameraShootPhotoMode
*/
@NonNull
public CameraShootPhotoMode getShootPhotoMode() {
return shootPhotoMode;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CameraPhotoState) {
return ((CameraPhotoState) obj).getShootPhotoMode() == this.getShootPhotoMode();
}
return false;
}
@Override
public int hashCode() {
return 31 * shootPhotoMode.value();
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.shootphoto;
import dji.sdk.keyvalue.value.camera.CameraStorageLocation;
/**
* Class represents the storage state when camera is in Shoot Photo Mode
* The state determines the storage location and the available space
* in terms of capture count
*/
public abstract class CameraPhotoStorageState {
private final CameraStorageLocation storageLocation;
private final long availableCaptureCount;
protected CameraPhotoStorageState(CameraStorageLocation storageLocation, long availableCaptureCount) {
this.storageLocation = storageLocation;
this.availableCaptureCount = availableCaptureCount;
}
/**
* Get the current storage location
*
* @return instance of CameraStorageLocation
*/
public CameraStorageLocation getStorageLocation() {
return storageLocation;
}
/**
* Get the number of photos that can be clicked before running out of storage
*
* @return long value representing count of photos
*/
public long getAvailableCaptureCount() {
return availableCaptureCount;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CameraPhotoStorageState)) return false;
CameraPhotoStorageState that = (CameraPhotoStorageState) o;
return availableCaptureCount == that.availableCaptureCount &&
storageLocation == that.storageLocation;
}
@Override
public int hashCode() {
int result = 31 * storageLocation.value();
result = result + 31 * (int) availableCaptureCount;
return result;
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.shootphoto;
import dji.sdk.keyvalue.value.camera.CameraStorageLocation;
import dji.sdk.keyvalue.value.camera.SDCardLoadState;
/**
* Class represents storage state for SD card and internal storage
* when camera is in shoot photo mode
*
*/
public class CameraSDPhotoStorageState extends CameraPhotoStorageState {
private SDCardLoadState storageOperationState;
public CameraSDPhotoStorageState(CameraStorageLocation storageLocation, long availableCaptureCount, SDCardLoadState storageOperationState) {
super(storageLocation, availableCaptureCount);
this.storageOperationState = storageOperationState;
}
/**
* Get the operation state of current storage
*
* @return instance of CameraSDCardState
* representing state for SDCard or internal Storage
*/
public SDCardLoadState getStorageOperationState() {
return storageOperationState;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CameraSDPhotoStorageState)) return false;
if (!super.equals(o)) return false;
CameraSDPhotoStorageState that = (CameraSDPhotoStorageState) o;
return storageOperationState == that.storageOperationState;
}
@Override
public int hashCode() {
int result = 31 * storageOperationState.value();
result = result + 31 * getStorageLocation().value();
result = result + 31 * (int) getAvailableCaptureCount();
return result;
}
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.shootphoto;
import dji.sdk.keyvalue.value.camera.CameraStorageLocation;
import dji.sdk.keyvalue.value.camera.SSDOperationState;
/**
* Class represents SSD Storage state for Shoot Photo Mode
*/
public class CameraSSDPhotoStorageState extends CameraPhotoStorageState {
private SSDOperationState storageOperationState;
public CameraSSDPhotoStorageState(CameraStorageLocation storageLocation, long availableCaptureCount, SSDOperationState storageOperationState) {
super(storageLocation, availableCaptureCount);
this.storageOperationState = storageOperationState;
}
/**
* Get operation state of SSD
*
* @return {@link SSDOperationState}
*/
public SSDOperationState getStorageOperationState() {
return storageOperationState;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof CameraSSDPhotoStorageState)) return false;
if (!super.equals(o)) return false;
CameraSSDPhotoStorageState that = (CameraSSDPhotoStorageState) o;
return storageOperationState == that.storageOperationState;
}
@Override
public int hashCode() {
return 31 * storageOperationState.value();
}
}

View File

@ -0,0 +1,641 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.shootphoto;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.View;
import android.widget.ImageView;
import java.util.HashMap;
import java.util.Map;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import dji.sdk.keyvalue.value.camera.CameraShootPhotoMode;
import dji.sdk.keyvalue.value.camera.CameraStorageLocation;
import dji.sdk.keyvalue.value.camera.SDCardLoadState;
import dji.sdk.keyvalue.value.camera.SSDOperationState;
import dji.sdk.keyvalue.value.common.CameraLensType;
import dji.sdk.keyvalue.value.common.ComponentIndexType;
import dji.v5.ux.R;
import dji.v5.ux.cameracore.ui.ProgressRingView;
import dji.v5.ux.cameracore.util.CameraActionSound;
import dji.v5.ux.cameracore.util.CameraResource;
import dji.v5.ux.cameracore.widget.cameracontrols.RemoteControllerButtonDownModel;
import dji.v5.ux.core.base.DJISDKModel;
import dji.v5.ux.core.base.ICameraIndex;
import dji.v5.ux.core.base.SchedulerProvider;
import dji.v5.ux.core.base.widget.ConstraintLayoutWidget;
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore;
import dji.v5.ux.core.util.UxErrorHandle;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
/**
* Shoot Photo Widget
* <p>
* Widget can be used for shooting photo. The widget displays the current photo mode. It also
* displays the storage state and errors associated with it.
*/
public class ShootPhotoWidget extends ConstraintLayoutWidget<Object> implements View.OnClickListener, ICameraIndex {
//region Fields
private static final String TAG = "ShootPhotoWidget";
private ShootPhotoWidgetModel widgetModel;
private RemoteControllerButtonDownModel buttonDownModel;
private ProgressRingView borderProgressRingView;
private ImageView centerImageView;
private ImageView storageStatusOverlayImageView;
private Drawable startShootPhotoDrawable;
private Drawable stopShootPhotoDrawable;
@ColorInt
private int progressRingColor;
private Map<StorageIconState, Drawable> storageInternalIconMap;
private Map<StorageIconState, Drawable> storageSSDIconMap;
private Map<StorageIconState, Drawable> storageSDCardIconMap;
private CameraActionSound cameraActionSound;
//endregion
//region Lifecycle
public ShootPhotoWidget(Context context) {
super(context);
}
public ShootPhotoWidget(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ShootPhotoWidget(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
inflate(context, R.layout.uxsdk_widget_shoot_photo, this);
borderProgressRingView = findViewById(R.id.progress_ring_view_border);
centerImageView = findViewById(R.id.image_view_center);
storageStatusOverlayImageView = findViewById(R.id.image_view_storage_status_overlay);
storageInternalIconMap = new HashMap<>();
storageSSDIconMap = new HashMap<>();
storageSDCardIconMap = new HashMap<>();
cameraActionSound = new CameraActionSound(context);
if (!isInEditMode()) {
centerImageView.setOnClickListener(this);
widgetModel = new ShootPhotoWidgetModel(DJISDKModel.getInstance(), ObservableInMemoryKeyedStore.getInstance());
buttonDownModel = new RemoteControllerButtonDownModel(DJISDKModel.getInstance(), ObservableInMemoryKeyedStore.getInstance());
}
initDefaults();
if (attrs != null) {
initAttributes(context, attrs);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (!isInEditMode()) {
widgetModel.setup();
buttonDownModel.setup();
}
}
@Override
protected void onDetachedFromWindow() {
if (!isInEditMode()) {
widgetModel.cleanup();
buttonDownModel.cleanup();
}
super.onDetachedFromWindow();
}
@Override
protected void reactToModelChanges() {
addReaction(widgetModel.isShootingPhoto().observeOn(SchedulerProvider.ui())
.subscribe(this::onIsShootingPhotoChange, UxErrorHandle.logErrorConsumer(TAG, "isShootingPhoto: ")));
addReaction(reactToCanStartOrStopShootingPhoto());
addReaction(reactToPhotoStateAndPhotoStorageState());
addReaction(buttonDownModel.isShutterButtonDownProcessor().toFlowable()
.observeOn(SchedulerProvider.ui())
.subscribe(aBoolean -> {
if (aBoolean == Boolean.TRUE) {
actionOnShootingPhoto();
}
}));
}
@NonNull
@Override
public String getIdealDimensionRatioString() {
return getContext().getResources().getString(R.string.uxsdk_widget_default_ratio);
}
@NonNull
public ComponentIndexType getCameraIndex() {
return widgetModel.getCameraIndex();
}
@NonNull
@Override
public CameraLensType getLensType() {
return widgetModel.getLensType();
}
@Override
public void updateCameraSource(@NonNull ComponentIndexType cameraIndex, @NonNull CameraLensType lensType) {
if (!isInEditMode()) {
widgetModel.updateCameraSource(cameraIndex, lensType);
}
}
@Override
public void onClick(View v) {
if (v.equals(centerImageView)) {
actionOnShootingPhoto();
}
}
private void actionOnShootingPhoto() {
if (!widgetModel.isPhotoMode()) {
return;
}
Single<Boolean> stop = widgetModel.canStopShootingPhoto().firstOrError();
Single<Boolean> start = widgetModel.canStartShootingPhoto().firstOrError();
addDisposable(Single.zip(stop, start, Pair::new).flatMapCompletable(pairs -> {
if (pairs.first) {
return widgetModel.stopShootPhoto();
} else if (pairs.second) {
return widgetModel.startShootPhoto();
}
return Completable.complete();
}).observeOn(SchedulerProvider.ui())
.subscribe(() -> {
}, UxErrorHandle.logErrorConsumer(TAG, "Start Stop Shoot Photo")));
}
//endregion
//region private helpers
private Drawable getCameraResourceDrawable(int resourceId) {
return getResources().getDrawable(resourceId);
}
private void updateCameraForegroundResource(@NonNull CameraPhotoState cameraPhotoState, @NonNull CameraPhotoStorageState cameraPhotoStorageState) {
Drawable foregroundDrawable = updateCameraActionSound(cameraPhotoState);
if (cameraPhotoStorageState instanceof CameraSDPhotoStorageState) {
CameraSDPhotoStorageState sdStorageState = (CameraSDPhotoStorageState) cameraPhotoStorageState;
if (cameraPhotoStorageState.getStorageLocation() == CameraStorageLocation.SDCARD) {
foregroundDrawable = updateResourceWithStorageInSDCard(sdStorageState);
} else if (cameraPhotoStorageState.getStorageLocation() == CameraStorageLocation.INTERNAL) {
foregroundDrawable = updateResourceWithStorageInternal(sdStorageState);
}
} else if (cameraPhotoStorageState instanceof CameraSSDPhotoStorageState) {
CameraSSDPhotoStorageState ssdStorageState = (CameraSSDPhotoStorageState) cameraPhotoStorageState;
if (ssdStorageState.getStorageOperationState() == SSDOperationState.NOT_FOUND) {
foregroundDrawable = getSSDStorageIcon(StorageIconState.NOT_INSERTED);
} else if (ssdStorageState.getStorageOperationState() == SSDOperationState.FULL) {
foregroundDrawable = getSSDStorageIcon(StorageIconState.FULL);
}
}
storageStatusOverlayImageView.setImageDrawable(foregroundDrawable);
}
private Drawable updateResourceWithStorageInternal(CameraSDPhotoStorageState sdStorageState) {
Drawable foregroundDrawable = null;
if (sdStorageState.getStorageOperationState() == SDCardLoadState.NOT_INSERTED) {
foregroundDrawable = getInternalStorageIcon(StorageIconState.NOT_INSERTED);
}
return foregroundDrawable;
}
private Drawable updateResourceWithStorageInSDCard(CameraSDPhotoStorageState sdStorageState) {
Drawable foregroundDrawable = null;
if (sdStorageState.getStorageOperationState() == SDCardLoadState.NOT_INSERTED) {
foregroundDrawable = getSDCardStorageIcon(StorageIconState.NOT_INSERTED);
}
return foregroundDrawable;
}
private Drawable updateCameraActionSound(CameraPhotoState cameraPhotoState) {
Drawable foregroundDrawable = null;
if (cameraPhotoState instanceof CameraPanoramaPhotoState) {
foregroundDrawable = getCameraResourceDrawable(CameraResource.getPhotoModeImgResId(cameraPhotoState.getShootPhotoMode().value(),
((CameraPanoramaPhotoState) cameraPhotoState).getPhotoPanoramaMode().value()));
cameraActionSound.setShutterCount(CameraActionSound.ShutterSoundCount.ONE);
} else if (cameraPhotoState instanceof CameraAEBPhotoState) {
int photoCount = ((CameraAEBPhotoState) cameraPhotoState).getPhotoAEBCount().value();
foregroundDrawable = getCameraResourceDrawable(CameraResource.getPhotoModeImgResId(cameraPhotoState.getShootPhotoMode().value(),
photoCount));
cameraActionSound.setShutterCount(CameraActionSound.ShutterSoundCount.find(photoCount));
} else if (cameraPhotoState instanceof CameraBurstPhotoState) {
int photoCount = ((CameraBurstPhotoState) cameraPhotoState).getPhotoBurstCount().value();
foregroundDrawable = getCameraResourceDrawable(CameraResource.getPhotoModeImgResId(cameraPhotoState.getShootPhotoMode().value(),
photoCount));
cameraActionSound.setShutterCount(CameraActionSound.ShutterSoundCount.find(photoCount));
} else if (cameraPhotoState instanceof CameraIntervalPhotoState) {
foregroundDrawable = getCameraResourceDrawable(CameraResource.getPhotoModeImgResId(cameraPhotoState.getShootPhotoMode().value(),
((CameraIntervalPhotoState) cameraPhotoState).getTimeIntervalInSeconds()));
cameraActionSound.setShutterCount(CameraActionSound.ShutterSoundCount.ONE);
} else {
if (cameraPhotoState.getShootPhotoMode() != CameraShootPhotoMode.NORMAL) {
foregroundDrawable = getCameraResourceDrawable(CameraResource.getPhotoModeImgResId(cameraPhotoState.getShootPhotoMode().value(),
0));
cameraActionSound.setShutterCount(CameraActionSound.ShutterSoundCount.ONE);
}
}
return foregroundDrawable;
}
private void onIsShootingPhotoChange(boolean isShootingPhoto) {
borderProgressRingView.setIndeterminate(isShootingPhoto);
if (isShootingPhoto) {
cameraActionSound.playCapturePhoto();
}
}
private Disposable reactToPhotoStateAndPhotoStorageState() {
return Flowable.combineLatest(widgetModel.getCameraPhotoState(), widgetModel.getCameraStorageState(), Pair::new)
.observeOn(SchedulerProvider.ui())
.subscribe(values -> updateCameraForegroundResource(values.first, values.second),
UxErrorHandle.logErrorConsumer(TAG, "reactToPhotoStateAndPhotoStorageState "));
}
private Disposable reactToCanStartOrStopShootingPhoto() {
return Flowable.combineLatest(widgetModel.canStartShootingPhoto(), widgetModel.canStopShootingPhoto(), Pair::new)
.observeOn(SchedulerProvider.ui())
.subscribe(values -> updateImages(values.first, values.second),
UxErrorHandle.logErrorConsumer(TAG, "reactToCanStartOrStopShootingPhoto: "));
}
private void checkAndUpdatePhotoStateAndPhotoStorageState() {
if (!isInEditMode()) {
addDisposable(Flowable.combineLatest(widgetModel.getCameraPhotoState(),
widgetModel.getCameraStorageState(),
Pair::new)
.firstOrError()
.observeOn(SchedulerProvider.ui())
.subscribe(values -> updateCameraForegroundResource(values.first, values.second),
UxErrorHandle.logErrorConsumer(TAG, "checkAndUpdatePhotoStateAndPhotoStorageState ")));
}
}
private void checkAndUpdateCanStartOrStopShootingPhoto() {
if (!isInEditMode()) {
addDisposable(Flowable.combineLatest(
widgetModel.canStartShootingPhoto(),
widgetModel.canStopShootingPhoto(),
Pair::new)
.firstOrError()
.observeOn(SchedulerProvider.ui())
.subscribe(values -> updateImages(values.first, values.second),
UxErrorHandle.logErrorConsumer(TAG, "checkAndUpdateCanStartOrStopShootingPhoto: ")));
}
}
private void updateImages(boolean canStartShootingPhoto, boolean canStopShootingPhoto) {
if (!canStopShootingPhoto) {
enableAction(canStartShootingPhoto);
centerImageView.setImageDrawable(startShootPhotoDrawable);
} else {
enableAction(true);
centerImageView.setImageDrawable(stopShootPhotoDrawable);
}
storageStatusOverlayImageView.setVisibility(canStopShootingPhoto ? View.GONE : View.VISIBLE);
borderProgressRingView.setRingColor(progressRingColor);
}
private void enableAction(boolean isEnabled) {
centerImageView.setEnabled(isEnabled);
}
private void initDefaults() {
startShootPhotoDrawable = getResources().getDrawable(R.drawable.uxsdk_shape_circle);
stopShootPhotoDrawable = getResources().getDrawable(R.drawable.uxsdk_ic_shutter_stop);
setProgressRingColor(Color.WHITE);
setInternalStorageIcon(StorageIconState.NOT_INSERTED, R.drawable.uxsdk_ic_internal_storage_not_inserted);
setInternalStorageIcon(StorageIconState.SLOW, R.drawable.uxsdk_ic_internal_storage_slow);
setInternalStorageIcon(StorageIconState.FULL, R.drawable.uxsdk_ic_internal_storage_full);
setSDCardStorageIcon(StorageIconState.NOT_INSERTED, R.drawable.uxsdk_ic_sdcard_not_inserted);
setSDCardStorageIcon(StorageIconState.SLOW, R.drawable.uxsdk_ic_sdcard_slow);
setSDCardStorageIcon(StorageIconState.FULL, R.drawable.uxsdk_ic_sdcard_full);
setSSDStorageIcon(StorageIconState.NOT_INSERTED, R.drawable.uxsdk_ic_ssd_not_inserted);
setSSDStorageIcon(StorageIconState.FULL, R.drawable.uxsdk_ic_ssd_full);
}
private void initAttributes(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ShootPhotoWidget);
updateCameraSource(ComponentIndexType.find(typedArray.getInt(R.styleable.ShootPhotoWidget_uxsdk_cameraIndex, 0)), CameraLensType.UNKNOWN);
setForegroundIconBackground(typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_foregroundIconBackground));
setProgressRingColor(typedArray.getColor(R.styleable.ShootPhotoWidget_uxsdk_progressRingColor, Color.WHITE));
initShootPhotoDrawable(typedArray);
initInternalStorageIcon(typedArray);
initSSDStorageIcon(typedArray);
initSDCardStorageIcon(typedArray);
typedArray.recycle();
}
private void initSSDStorageIcon(TypedArray typedArray) {
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_ssdNotInsertedIcon) != null) {
setSSDStorageIcon(StorageIconState.NOT_INSERTED, typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_ssdNotInsertedIcon));
}
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_ssdFullIcon) != null) {
setSSDStorageIcon(StorageIconState.FULL, typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_ssdFullIcon));
}
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_ssdSlowIcon) != null) {
setSSDStorageIcon(StorageIconState.SLOW, typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_ssdSlowIcon));
}
}
private void initSDCardStorageIcon(TypedArray typedArray) {
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_sdCardNotInsertedIcon) != null) {
setSDCardStorageIcon(StorageIconState.NOT_INSERTED, typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_sdCardNotInsertedIcon));
}
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_sdCardFullIcon) != null) {
setSDCardStorageIcon(StorageIconState.FULL, typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_sdCardFullIcon));
}
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_sdCardSlowIcon) != null) {
setSDCardStorageIcon(StorageIconState.SLOW, typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_sdCardSlowIcon));
}
}
private void initInternalStorageIcon(TypedArray typedArray) {
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_internalStorageNotInsertedIcon) != null) {
setInternalStorageIcon(StorageIconState.NOT_INSERTED, typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_internalStorageNotInsertedIcon));
}
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_internalStorageFullIcon) != null) {
setInternalStorageIcon(StorageIconState.FULL, typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_internalStorageFullIcon));
}
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_internalStorageSlowIcon) != null) {
setInternalStorageIcon(StorageIconState.SLOW, typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_internalStorageSlowIcon));
}
}
private void initShootPhotoDrawable(TypedArray typedArray) {
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_shootPhotoStartIcon) != null) {
startShootPhotoDrawable = typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_shootPhotoStartIcon);
}
if (typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_shootPhotoStopIcon) != null) {
stopShootPhotoDrawable = typedArray.getDrawable(R.styleable.ShootPhotoWidget_uxsdk_shootPhotoStopIcon);
}
}
//endregion
//region customizations
/**
* Get the current start shooting photo icon
*
* @return Drawable currently used
*/
public Drawable getStartShootPhotoDrawable() {
return startShootPhotoDrawable;
}
/**
* Set the start shoot photo icon
*
* @param resourceId to be used
*/
public void setStartShootPhotoDrawable(@DrawableRes int resourceId) {
setStartShootPhotoDrawable(getResources().getDrawable(resourceId));
}
/**
* Set the start shoot photo icon
*
* @param drawable to be used
*/
public void setStartShootPhotoDrawable(@Nullable Drawable drawable) {
startShootPhotoDrawable = drawable;
checkAndUpdateCanStartOrStopShootingPhoto();
}
/**
* Get the current stop shooting photo icon
*
* @return Drawable currently used
*/
@Nullable
public Drawable getStopShootPhotoDrawable() {
return stopShootPhotoDrawable;
}
/**
* Set stop shoot photo icon
*
* @param resourceId to be used
*/
public void setStopShootPhotoDrawable(@DrawableRes int resourceId) {
setStopShootPhotoDrawable(getResources().getDrawable(resourceId));
}
/**
* Set stop shoot photo icon
*
* @param drawable to be used
*/
public void setStopShootPhotoDrawable(@Nullable Drawable drawable) {
stopShootPhotoDrawable = drawable;
checkAndUpdateCanStartOrStopShootingPhoto();
}
/**
* Set the icon for internal storage based on storage icon state
*
* @param storageIconState for which icon is used
* @param resourceId to be used
*/
public void setInternalStorageIcon(@NonNull StorageIconState storageIconState, @DrawableRes int resourceId) {
setInternalStorageIcon(storageIconState, getResources().getDrawable(resourceId));
}
/**
* Set the icon for internal storage based on storage icon state
*
* @param storageIconState for which icon is used
* @param drawable to be used
*/
public void setInternalStorageIcon(@NonNull StorageIconState storageIconState, @Nullable Drawable drawable) {
storageInternalIconMap.put(storageIconState, drawable);
checkAndUpdatePhotoStateAndPhotoStorageState();
}
/**
* Get the icon for internal storage based on storage icon state
*
* @param storageIconState for which icon is used
* @return Drawable currently used
*/
@Nullable
public Drawable getInternalStorageIcon(@NonNull StorageIconState storageIconState) {
return storageInternalIconMap.get(storageIconState);
}
/**
* Set the icon for SD card storage based on storage icon state
*
* @param storageIconState for which icon is used
* @param resourceId to be used
*/
public void setSDCardStorageIcon(@NonNull StorageIconState storageIconState, @DrawableRes int resourceId) {
setSDCardStorageIcon(storageIconState, getResources().getDrawable(resourceId));
}
/**
* Set the icon for SD card storage based on storage icon state
*
* @param storageIconState for which icon is used
* @param drawable to be used
*/
public void setSDCardStorageIcon(@NonNull StorageIconState storageIconState, @Nullable Drawable drawable) {
storageSDCardIconMap.put(storageIconState, drawable);
checkAndUpdatePhotoStateAndPhotoStorageState();
}
/**
* Get the icon for SD card storage based on storage icon state
*
* @param storageIconState for which icon is used
* @return Drawable currently used
*/
@Nullable
public Drawable getSDCardStorageIcon(@NonNull StorageIconState storageIconState) {
return storageSDCardIconMap.get(storageIconState);
}
/**
* Set the icon for SSD storage based on storage icon state
*
* @param storageIconState for which icon should be used
* @param resourceId to be used
*/
public void setSSDStorageIcon(@NonNull StorageIconState storageIconState, @DrawableRes int resourceId) {
setSSDStorageIcon(storageIconState, getResources().getDrawable(resourceId));
}
/**
* Set the icon for SSD storage based on storage icon state
*
* @param storageIconState for which icon should be used
* @param drawable Drawable to be used
*/
public void setSSDStorageIcon(@NonNull StorageIconState storageIconState, @Nullable Drawable drawable) {
storageSSDIconMap.put(storageIconState, drawable);
checkAndUpdatePhotoStateAndPhotoStorageState();
}
/**
* Get the icon for SSD storage based on storage icon state
*
* @param storageIconState for which icon is used
* @return Drawable currently used
*/
@Nullable
public Drawable getSSDStorageIcon(@NonNull StorageIconState storageIconState) {
return storageSSDIconMap.get(storageIconState);
}
/**
* Get current background of foreground icon
*
* @return Drawable being used
*/
@Nullable
public Drawable getForegroundIconBackground() {
return storageStatusOverlayImageView.getBackground();
}
/**
* Set the background of the foreground icon
*
* @param resourceId to be used as background
*/
public void setForegroundIconBackground(@DrawableRes int resourceId) {
storageStatusOverlayImageView.setBackgroundResource(resourceId);
}
/**
* Set background of the foreground icon
*
* @param drawable to be used as background
*/
public void setForegroundIconBackground(@Nullable Drawable drawable) {
storageStatusOverlayImageView.setBackground(drawable);
}
/**
* Get the color of the progress ring
*
* @return integer representing color
*/
@ColorInt
public int getProgressRingColor() {
return progressRingColor;
}
/**
* Set the color of the progress ring
*
* @param color integer value
*/
public void setProgressRingColor(@ColorInt int color) {
progressRingColor = color;
checkAndUpdateCanStartOrStopShootingPhoto();
}
//endregion
/**
* Enum indicating storage error state
*/
public enum StorageIconState {
/**
* The storage is slow
*/
SLOW,
/**
* The storage is full
*/
FULL,
/**
* The storage is not inserted
*/
NOT_INSERTED
}
}

View File

@ -0,0 +1,458 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracapture.shootphoto;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import dji.sdk.keyvalue.key.CameraKey;
import dji.sdk.keyvalue.key.KeyTools;
import dji.sdk.keyvalue.utils.ProductUtil;
import dji.sdk.keyvalue.value.camera.CameraMode;
import dji.sdk.keyvalue.value.camera.CameraShootPhotoMode;
import dji.sdk.keyvalue.value.camera.CameraStorageInfo;
import dji.sdk.keyvalue.value.camera.CameraStorageInfos;
import dji.sdk.keyvalue.value.camera.CameraStorageLocation;
import dji.sdk.keyvalue.value.camera.CameraType;
import dji.sdk.keyvalue.value.camera.PhotoAEBPhotoCount;
import dji.sdk.keyvalue.value.camera.PhotoAEBSettings;
import dji.sdk.keyvalue.value.camera.PhotoBurstCount;
import dji.sdk.keyvalue.value.camera.PhotoIntervalShootSettings;
import dji.sdk.keyvalue.value.camera.PhotoPanoramaMode;
import dji.sdk.keyvalue.value.camera.SDCardLoadState;
import dji.sdk.keyvalue.value.camera.SSDOperationState;
import dji.sdk.keyvalue.value.common.CameraLensType;
import dji.sdk.keyvalue.value.common.ComponentIndexType;
import dji.v5.manager.KeyManager;
import dji.v5.ux.core.base.DJISDKModel;
import dji.v5.ux.core.base.ICameraIndex;
import dji.v5.ux.core.base.WidgetModel;
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore;
import dji.v5.ux.core.module.FlatCameraModule;
import dji.v5.ux.core.util.CameraUtil;
import dji.v5.ux.core.util.DataProcessor;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Flowable;
/**
* Shoot Photo Widget Model
* <p>
* Widget Model for {@link ShootPhotoWidget} used to define underlying
* logic and communication
*/
public class ShootPhotoWidgetModel extends WidgetModel implements ICameraIndex {
//region Constants
private static final int INVALID_AVAILABLE_CAPTURE_COUNT = -1;
//endregion
//region Public data
private final DataProcessor<CameraPhotoState> cameraPhotoState;
private final DataProcessor<CameraPhotoStorageState> cameraStorageState;
private final DataProcessor<Boolean> isShootingPhoto;
private final DataProcessor<Boolean> isStoringPhoto;
private final DataProcessor<Boolean> canStartShootingPhoto;
private final DataProcessor<Boolean> shootPhotoNotAllowed;
private final DataProcessor<Boolean> canStopShootingPhoto;
private final DataProcessor<String> cameraDisplayName;
private final DataProcessor<Boolean> isShootingInterval;
private final DataProcessor<CameraType> cameraType;
//endregion
//region Internal data
private final DataProcessor<Boolean> isShootingPanorama;
private final DataProcessor<PhotoAEBSettings> aebSettings;
private final DataProcessor<PhotoAEBPhotoCount> aebCount;
private final DataProcessor<PhotoBurstCount> burstCount;
private final DataProcessor<PhotoBurstCount> rawBurstCount;
private final DataProcessor<PhotoIntervalShootSettings> timeIntervalSettings;
private final DataProcessor<PhotoPanoramaMode> panoramaMode;
private final DataProcessor<CameraStorageInfos> storageInfosProcessor;
private final DataProcessor<CameraStorageLocation> storageLocation;
private final DataProcessor<SDCardLoadState> sdCardState;
private final DataProcessor<SDCardLoadState> innerStorageState;
private final DataProcessor<SSDOperationState> ssdState;
private final DataProcessor<Integer> sdAvailableCaptureCount;
private final DataProcessor<Integer> innerStorageAvailableCaptureCount;
private final DataProcessor<Integer> rawPhotoBurstCaptureCount;
private final DataProcessor<Boolean> isProductConnected;
//endregion
//region Other fields
private final PhotoIntervalShootSettings defaultIntervalSettings;
private ComponentIndexType cameraIndex = ComponentIndexType.LEFT_OR_MAIN;
private CameraLensType lensType = CameraLensType.UNKNOWN;
private FlatCameraModule flatCameraModule;
//endregion
//region Constructor
public ShootPhotoWidgetModel(@NonNull DJISDKModel djiSdkModel,
@NonNull ObservableInMemoryKeyedStore keyedStore) {
super(djiSdkModel, keyedStore);
defaultIntervalSettings = new PhotoIntervalShootSettings();
CameraPhotoState photoState = new CameraPhotoState(CameraShootPhotoMode.UNKNOWN);
this.cameraPhotoState = DataProcessor.create(photoState);
CameraSDPhotoStorageState cameraSDStorageState = new CameraSDPhotoStorageState(
CameraStorageLocation.SDCARD, 0, SDCardLoadState.NOT_INSERTED);
cameraStorageState = DataProcessor.create(cameraSDStorageState);
canStartShootingPhoto = DataProcessor.create(false);
shootPhotoNotAllowed = DataProcessor.create(true);
canStopShootingPhoto = DataProcessor.create(false);
cameraDisplayName = DataProcessor.create("");
aebSettings = DataProcessor.create(new PhotoAEBSettings());
aebCount = DataProcessor.create(PhotoAEBPhotoCount.UNKNOWN);
burstCount = DataProcessor.create(PhotoBurstCount.UNKNOWN);
rawBurstCount = DataProcessor.create(PhotoBurstCount.UNKNOWN);
timeIntervalSettings = DataProcessor.create(defaultIntervalSettings);
panoramaMode = DataProcessor.create(PhotoPanoramaMode.UNKNOWN);
isShootingPhoto = DataProcessor.create(false);
isShootingInterval = DataProcessor.create(false);
cameraType = DataProcessor.create(CameraType.NOT_SUPPORTED);
isShootingPanorama = DataProcessor.create(false);
isStoringPhoto = DataProcessor.create(false);
storageLocation = DataProcessor.create(CameraStorageLocation.SDCARD);
sdCardState = DataProcessor.create(SDCardLoadState.UNKNOWN);
innerStorageState = DataProcessor.create(SDCardLoadState.UNKNOWN);
ssdState = DataProcessor.create(SSDOperationState.UNKNOWN);
sdAvailableCaptureCount = DataProcessor.create(INVALID_AVAILABLE_CAPTURE_COUNT);
innerStorageAvailableCaptureCount = DataProcessor.create(INVALID_AVAILABLE_CAPTURE_COUNT);
rawPhotoBurstCaptureCount = DataProcessor.create(INVALID_AVAILABLE_CAPTURE_COUNT);
isProductConnected = DataProcessor.create(false);
storageInfosProcessor = DataProcessor.create(new CameraStorageInfos(CameraStorageLocation.UNKNOWN, new ArrayList<>()));
flatCameraModule = new FlatCameraModule();
addModule(flatCameraModule);
}
//endregion
//region Data
/**
* Get the current shoot photo mode
*
* @return Flowable with {@link CameraPhotoState} instance
*/
public Flowable<CameraPhotoState> getCameraPhotoState() {
return cameraPhotoState.toFlowable();
}
/**
* Get the current camera photo storage location
*
* @return Flowable with {@link CameraPhotoStorageState} instance
*/
public Flowable<CameraPhotoStorageState> getCameraStorageState() {
return cameraStorageState.toFlowable();
}
/**
* Check if the device is currently shooting photo
*
* @return Flowable with boolean value
* true - if camera is shooting photo false - camera is not shooting photo
*/
public Flowable<Boolean> isShootingPhoto() {
return isShootingPhoto.toFlowable();
}
/**
* Check if the device is currently in the process of storing photo
*
* @return Flowable with boolean value
* true - if device is storing photo false - device is not storing photo
*/
public Flowable<Boolean> isStoringPhoto() {
return isStoringPhoto.toFlowable();
}
/**
* Check if the device is ready to shoot photo.
*
* @return Flowable with boolean value
* true - device ready false - device not ready
*/
public Flowable<Boolean> canStartShootingPhoto() {
return canStartShootingPhoto.toFlowable();
}
/**
* Check if the device is currently shooting photo and is ready to stop
*
* @return Flowable with boolean value
* true - can stop shooting false - can not stop shooting photo
*/
public Flowable<Boolean> canStopShootingPhoto() {
return canStopShootingPhoto.toFlowable();
}
@NonNull
public ComponentIndexType getCameraIndex() {
return cameraIndex;
}
@NonNull
@Override
public CameraLensType getLensType() {
return lensType;
}
@Override
public void updateCameraSource(@NonNull ComponentIndexType cameraIndex, @NonNull CameraLensType lensType) {
this.cameraIndex = cameraIndex;
this.lensType = lensType;
flatCameraModule.updateCameraSource(cameraIndex,lensType);
restart();
}
/**
* Get the display name of the camera which the model is reacting to
*
* @return Flowable with string of camera name
*/
public Flowable<String> getCameraDisplayName() {
return cameraDisplayName.toFlowable();
}
//endregion
//region Actions
/**
* Start shooting photo
*
* @return Completable to determine the status of the action
*/
public Completable startShootPhoto() {
if (!canStartShootingPhoto.getValue()) {
return Completable.complete();
}
return djiSdkModel.performActionWithOutResult(KeyTools.createKey(CameraKey.KeyStartShootPhoto, cameraIndex));
}
/**
* Stop shooting photo
*
* @return Completable to determine the status of the action
*/
public Completable stopShootPhoto() {
if (!canStopShootingPhoto.getValue()) {
return Completable.complete();
}
return djiSdkModel.performActionWithOutResult(KeyTools.createKey(CameraKey.KeyStopShootPhoto, cameraIndex));
}
//endregion
//region Lifecycle
@Override
protected void inSetup() {
// Product connection
bindDataProcessor(KeyTools.createKey(CameraKey.KeyConnection, cameraIndex), isProductConnected, newValue -> onCameraConnected((boolean) newValue));
// Photo mode
bindDataProcessor(KeyTools.createKey(CameraKey.KeyAEBSettings, cameraIndex), aebSettings, photoAEBSettings -> {
aebCount.onNext(photoAEBSettings.getCount());
});
bindDataProcessor(KeyTools.createKey(CameraKey.KeyPhotoBurstCount, cameraIndex), burstCount);
bindDataProcessor(KeyTools.createKey(CameraKey.KeyRawBurstCount, cameraIndex), rawBurstCount);
bindDataProcessor(KeyTools.createKey(CameraKey.KeyPhotoIntervalShootSettings, cameraIndex), timeIntervalSettings);
bindDataProcessor(KeyTools.createKey(CameraKey.KeyPhotoPanoramaMode, cameraIndex), panoramaMode);
// Is shooting photo state
bindDataProcessor(KeyTools.createKey(CameraKey.KeyIsShootingPhoto, cameraIndex), isShootingPhoto);
// Is storing photo state
bindDataProcessor(KeyTools.createKey(CameraKey.KeyCameraStoringFile, cameraIndex), isStoringPhoto);
// Can start shooting photo
// can't take photo when product is not connected
bindDataProcessor(KeyTools.createKey(CameraKey.KeyShootPhotoNotAllowed, cameraIndex), shootPhotoNotAllowed, aBoolean -> {
canStartShootingPhoto.onNext(!aBoolean);
});
// Can stop shooting photo
bindDataProcessor(KeyTools.createKey(CameraKey.KeyCameraShootingContinuousPhotos, cameraIndex), isShootingInterval, this::onCanStopShootingPhoto);
bindDataProcessor(KeyTools.createKey(CameraKey.KeyIsShootingPhotoPanorama, cameraIndex), isShootingPanorama, newValue -> onCanStopShootingPhoto((boolean) newValue));
// Display name
bindDataProcessor(KeyTools.createKey(CameraKey.KeyCameraType, cameraIndex), cameraType, type -> cameraDisplayName.onNext(type.name()));
// Storage
bindDataProcessor(KeyTools.createKey(CameraKey.KeyCameraStorageInfos, cameraIndex), storageInfosProcessor, cameraStorageInfos -> {
storageLocation.onNext(cameraStorageInfos.getCurrentStorageType());
CameraStorageInfo internalInfo = cameraStorageInfos.getCameraStorageInfoByLocation(CameraStorageLocation.INTERNAL);
if (internalInfo != null) {
innerStorageState.onNext(internalInfo.getStorageState());
sdAvailableCaptureCount.onNext(internalInfo.getAvailablePhotoCount());
}
CameraStorageInfo sdcardInfo = cameraStorageInfos.getCameraStorageInfoByLocation(CameraStorageLocation.SDCARD);
if (sdcardInfo != null) {
sdCardState.onNext(sdcardInfo.getStorageState());
innerStorageAvailableCaptureCount.onNext(sdcardInfo.getAvailablePhotoCount());
}
});
bindDataProcessor(KeyTools.createKey(CameraKey.KeySSDOperationState, cameraIndex), ssdState);
bindDataProcessor(KeyTools.createKey(CameraKey.KeySDCardAvailablePhotoCount, cameraIndex), sdAvailableCaptureCount);
bindDataProcessor(KeyTools.createKey(CameraKey.KeyInternalStorageAvailablePhotoCount, cameraIndex), innerStorageAvailableCaptureCount);
bindDataProcessor(KeyTools.createKey(CameraKey.KeyH1PhotoBurstCount, cameraIndex), rawPhotoBurstCaptureCount);
}
@Override
protected void inCleanup() {
// do nothing
}
@Override
protected void updateStates() {
updateCameraPhotoState();
updateCameraStorageState();
}
public boolean isPhotoMode(){
return flatCameraModule.getCameraModeDataProcessor().getValue().isPhotoMode()
|| (flatCameraModule.getCameraModeDataProcessor().getValue() == CameraMode.VIDEO_NORMAL && isSupportShootPhoto());
}
private boolean isSupportShootPhoto(){
CameraType cameraType = KeyManager.getInstance().getValue(KeyTools.createKey(CameraKey.KeyCameraType ,cameraIndex) ,CameraType.NOT_SUPPORTED);
return cameraType == CameraType.M3T || cameraType == CameraType.M3E || cameraType == CameraType.M3TA || cameraType == CameraType.M3M || cameraType == CameraType.ZENMUSE_H20T;
}
//region Helpers
private void updateCameraPhotoState() {
CameraPhotoState state = null;
CameraShootPhotoMode shootPhotoMode = flatCameraModule.getShootPhotoModeProcessor().getValue();
switch (shootPhotoMode) {
case NORMAL:
case HDR:
case HYPER_LIGHT:
//case SHALLOW_FOCUS:
case EHDR:
state = new CameraPhotoState(shootPhotoMode);
break;
case BURST:
if (!PhotoBurstCount.UNKNOWN.equals(burstCount.getValue())) {
state = new CameraBurstPhotoState(
shootPhotoMode,
burstCount.getValue());
}
break;
case RAW_BURST:
if (!PhotoBurstCount.UNKNOWN.equals(rawBurstCount.getValue())) {
state = new CameraBurstPhotoState(
shootPhotoMode,
rawBurstCount.getValue()
);
}
break;
case AEB:
if (!PhotoAEBPhotoCount.UNKNOWN.equals(aebCount.getValue())) {
state = new CameraAEBPhotoState(
shootPhotoMode,
aebCount.getValue()
);
}
break;
case INTERVAL:
PhotoIntervalShootSettings intervalSettings = timeIntervalSettings.getValue();
if (!defaultIntervalSettings.equals(timeIntervalSettings.getValue())) {
state = new CameraIntervalPhotoState(
shootPhotoMode,
intervalSettings.getCount(),
intervalSettings.getInterval().intValue()
);
}
break;
case PANO_APP:
if (!PhotoPanoramaMode.UNKNOWN.equals(panoramaMode.getValue())) {
state = new CameraPanoramaPhotoState(
shootPhotoMode,
panoramaMode.getValue()
);
}
break;
default:
break;
}
if (state != null) {
this.cameraPhotoState.onNext(state);
}
}
private void updateCameraStorageState() {
CameraStorageLocation currentStorageLocation = storageLocation.getValue();
if (CameraStorageLocation.UNKNOWN.equals(currentStorageLocation)) {
return;
}
CameraShootPhotoMode currentShootPhotoMode = flatCameraModule.getShootPhotoModeProcessor().getValue();
long availableCaptureCount = getAvailableCaptureCount(currentStorageLocation, currentShootPhotoMode);
CameraPhotoStorageState newCameraPhotoStorageState = null;
if (currentShootPhotoMode == CameraShootPhotoMode.RAW_BURST) {
newCameraPhotoStorageState = new CameraSSDPhotoStorageState(CameraStorageLocation.UNKNOWN, availableCaptureCount, ssdState.getValue());
} else if (CameraStorageLocation.SDCARD.equals(currentStorageLocation)) {
if (!SDCardLoadState.UNKNOWN.equals(sdCardState.getValue())) {
newCameraPhotoStorageState = new CameraSDPhotoStorageState(currentStorageLocation, availableCaptureCount, sdCardState.getValue());
}
} else if (CameraStorageLocation.INTERNAL.equals(currentStorageLocation)) {
newCameraPhotoStorageState = new CameraSDPhotoStorageState(currentStorageLocation, availableCaptureCount, innerStorageState.getValue());
}
if (newCameraPhotoStorageState != null) {
cameraStorageState.onNext(newCameraPhotoStorageState);
}
}
private long getAvailableCaptureCount(CameraStorageLocation storageLocation,
CameraShootPhotoMode shootPhotoMode) {
if (shootPhotoMode == CameraShootPhotoMode.RAW_BURST) {
return rawPhotoBurstCaptureCount.getValue();
}
switch (storageLocation) {
case SDCARD:
return sdAvailableCaptureCount.getValue();
case INTERNAL:
return innerStorageAvailableCaptureCount.getValue();
case UNKNOWN:
default:
return INVALID_AVAILABLE_CAPTURE_COUNT;
}
}
private void onCanStopShootingPhoto(boolean canStopShootingPhoto) {
this.canStopShootingPhoto.onNext(canStopShootingPhoto);
}
private void onCameraConnected(boolean isCameraConnected) {
if (!isCameraConnected) {
// Reset storage state
sdCardState.onNext(SDCardLoadState.UNKNOWN);
innerStorageState.onNext(SDCardLoadState.UNKNOWN);
ssdState.onNext(SSDOperationState.UNKNOWN);
}
}
//endregion
}

View File

@ -0,0 +1,221 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracontrols;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import dji.sdk.keyvalue.value.common.CameraLensType;
import dji.sdk.keyvalue.value.common.ComponentIndexType;
import dji.v5.ux.R;
import dji.v5.ux.cameracore.widget.cameracapture.CameraCaptureWidget;
import dji.v5.ux.cameracore.widget.cameracontrols.camerasettingsindicator.CameraSettingsMenuIndicatorWidget;
import dji.v5.ux.cameracore.widget.cameracontrols.exposuresettingsindicator.ExposureSettingsIndicatorWidget;
import dji.v5.ux.cameracore.widget.cameracontrols.photovideoswitch.PhotoVideoSwitchWidget;
import dji.v5.ux.core.base.ICameraIndex;
import dji.v5.ux.core.base.widget.ConstraintLayoutWidget;
/**
* Compound widget which combines the state and interaction related to camera.
* It includes {@link CameraSettingsMenuIndicatorWidget}, {@link ExposureSettingsIndicatorWidget},
* {@link PhotoVideoSwitchWidget} and {@link CameraCaptureWidget}.
* <p>
* The widget gives access to all the child widgets.
*/
public class CameraControlsWidget extends ConstraintLayoutWidget<Object> implements ICameraIndex {
//region Fields
private CameraSettingsMenuIndicatorWidget cameraSettingsMenuIndicatorWidget;
private PhotoVideoSwitchWidget photoVideoSwitchWidget;
private CameraCaptureWidget cameraCaptureWidget;
private ExposureSettingsIndicatorWidget exposureSettingsIndicatorWidget;
//endregion
//region Lifecycle
public CameraControlsWidget(@NonNull Context context) {
super(context);
}
public CameraControlsWidget(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CameraControlsWidget(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
inflate(context, R.layout.uxsdk_widget_camera_controls, this);
setBackgroundResource(R.drawable.uxsdk_background_black_rectangle);
cameraCaptureWidget = findViewById(R.id.widget_camera_control_camera_capture);
photoVideoSwitchWidget = findViewById(R.id.widget_camera_control_photo_video_switch);
exposureSettingsIndicatorWidget = findViewById(R.id.widget_camera_control_camera_exposure_settings);
cameraSettingsMenuIndicatorWidget = findViewById(R.id.widget_camera_control_camera_settings_menu);
}
@Override
protected void reactToModelChanges() {
//Do Nothing
}
@NonNull
@Override
public String getIdealDimensionRatioString() {
return getResources().getString(R.string.uxsdk_widget_camera_controls_ratio);
}
@NonNull
@Override
public ComponentIndexType getCameraIndex() {
return cameraCaptureWidget.getCameraIndex();
}
@NonNull
@Override
public CameraLensType getLensType() {
return cameraCaptureWidget.getLensType();
}
@Override
public void updateCameraSource(@NonNull ComponentIndexType cameraIndex, @NonNull CameraLensType lensType) {
cameraCaptureWidget.updateCameraSource(cameraIndex, lensType);
photoVideoSwitchWidget.updateCameraSource(cameraIndex, lensType);
exposureSettingsIndicatorWidget.updateCameraSource(cameraIndex, lensType);
}
/**
* Get the camera capture widget
*
* @return {@link CameraCaptureWidget}
*/
@NonNull
public CameraCaptureWidget getCameraCaptureWidget() {
return cameraCaptureWidget;
}
/**
* Get the camera settings menu widget
*
* @return {@link CameraSettingsMenuIndicatorWidget}
*/
@NonNull
public CameraSettingsMenuIndicatorWidget getCameraSettingsMenuIndicatorWidget() {
return cameraSettingsMenuIndicatorWidget;
}
/**
* Get the exposure settings widget
*
* @return {@link ExposureSettingsIndicatorWidget}
*/
@NonNull
public ExposureSettingsIndicatorWidget getExposureSettingsIndicatorWidget() {
return exposureSettingsIndicatorWidget;
}
/**
* Get the photo video switch widget
*
* @return {@link PhotoVideoSwitchWidget}
*/
@NonNull
public PhotoVideoSwitchWidget getPhotoVideoSwitchWidget() {
return photoVideoSwitchWidget;
}
/**
* Show/Hide {@link CameraSettingsMenuIndicatorWidget}
*
* @param isVisible boolean true - visible false - gone
*/
public void setCameraSettingsMenuIndicatorWidgetVisibility(boolean isVisible) {
cameraSettingsMenuIndicatorWidget.setVisibility(isVisible ? VISIBLE : GONE);
}
/**
* Check if {@link CameraSettingsMenuIndicatorWidget} is visible
*
* @return boolean true - visible false - gone
*/
public boolean isCameraSettingsMenuIndicatorWidgetVisible() {
return cameraSettingsMenuIndicatorWidget.getVisibility() == VISIBLE;
}
/**
* Show/Hide {@link PhotoVideoSwitchWidget}
*
* @param isVisible boolean true - visible false - gone
*/
public void setPhotoVideoSwitchWidgetVisibility(boolean isVisible) {
photoVideoSwitchWidget.setVisibility(isVisible ? VISIBLE : GONE);
}
/**
* Check if {@link PhotoVideoSwitchWidget} is Visible
*
* @return boolean true - visible false - gone
*/
public boolean isPhotoVideoSwitchWidgetVisible() {
return photoVideoSwitchWidget.getVisibility() == VISIBLE;
}
/**
* Show/Hide {@link CameraCaptureWidget}
*
* @param isVisible boolean true - visible false - gone
*/
public void setCameraCaptureWidgetVisibility(boolean isVisible) {
cameraCaptureWidget.setVisibility(isVisible ? VISIBLE : GONE);
}
/**
* Check if {@link CameraCaptureWidget} is visible
*
* @return boolean true - visible false - gone
*/
public boolean isCameraCaptureWidgetVisible() {
return cameraCaptureWidget.getVisibility() == VISIBLE;
}
/**
* Show/Hide {@link ExposureSettingsIndicatorWidget}
*
* @param isVisible boolean true - visible false - gone
*/
public void setExposureSettingsIndicatorWidgetVisibility(boolean isVisible) {
exposureSettingsIndicatorWidget.setVisibility(isVisible ? VISIBLE : GONE);
}
/**
* Check if {@link ExposureSettingsIndicatorWidget}
*
* @return boolean true - visible false - gone
*/
public boolean isExposureSettingsIndicatorWidgetVisible() {
return exposureSettingsIndicatorWidget.getVisibility() == VISIBLE;
}
}

View File

@ -0,0 +1,34 @@
package dji.v5.ux.cameracore.widget.cameracontrols
import dji.sdk.keyvalue.key.RemoteControllerKey
import dji.v5.et.create
import dji.v5.ux.core.base.DJISDKModel
import dji.v5.ux.core.base.WidgetModel
import dji.v5.ux.core.communication.ObservableInMemoryKeyedStore
import dji.v5.ux.core.util.DataProcessor
/**
* Class Description
*
* @author Hoker
* @date 2021/12/13
*
* Copyright (c) 2021, DJI All Rights Reserved.
*/
open class RemoteControllerButtonDownModel constructor(
djiSdkModel: DJISDKModel,
keyedStore: ObservableInMemoryKeyedStore
) : WidgetModel(djiSdkModel, keyedStore) {
val isShutterButtonDownProcessor: DataProcessor<Boolean> = DataProcessor.create(false)
val isRecordButtonDownProcessor: DataProcessor<Boolean> = DataProcessor.create(false)
override fun inSetup() {
bindDataProcessor(RemoteControllerKey.KeyShutterButtonDown.create(), isShutterButtonDownProcessor)
bindDataProcessor(RemoteControllerKey.KeyRecordButtonDown.create(), isRecordButtonDownProcessor)
}
override fun inCleanup() {
//暂未实现
}
}

View File

@ -0,0 +1,212 @@
/*
* Copyright (c) 2018-2020 DJI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
package dji.v5.ux.cameracore.widget.cameracontrols.camerasettingsindicator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.Dimension;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import dji.v5.ux.R;
import dji.v5.ux.cameracore.widget.cameracontrols.CameraControlsWidget;
import dji.v5.ux.core.base.widget.FrameLayoutWidget;
import dji.v5.ux.core.communication.OnStateChangeCallback;
/**
* Camera Settings Menu Indicator Widget
* <p>
* The widget is part of the {@link CameraControlsWidget}.
* Tapping the widget can be used to open the camera settings panel
*/
public class CameraSettingsMenuIndicatorWidget extends FrameLayoutWidget<Object> implements View.OnClickListener {
//region Fields
private TextView foregroundTextView;
private OnStateChangeCallback<Object> stateChangeCallback = null;
//endregion
//region Lifecycle
public CameraSettingsMenuIndicatorWidget(@NonNull Context context) {
super(context);
}
public CameraSettingsMenuIndicatorWidget(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CameraSettingsMenuIndicatorWidget(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void initView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
inflate(context, R.layout.uxsdk_widget_camera_settings_menu_indicator, this);
foregroundTextView = findViewById(R.id.text_view_menu);
setOnClickListener(this);
if (attrs != null) {
initAttributes(context, attrs);
}
}
@Override
protected void reactToModelChanges() {
// No Code
}
@NonNull
@Override
public String getIdealDimensionRatioString() {
return getResources().getString(R.string.uxsdk_widget_default_ratio);
}
@Override
public void onClick(View v) {
if (stateChangeCallback != null) {
stateChangeCallback.onStateChange(null);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
initializeListener();
}
@Override
protected void onDetachedFromWindow() {
destroyListener();
super.onDetachedFromWindow();
}
//endregion
//region private methods
private void initializeListener() {
//暂未实现
}
private void destroyListener() {
stateChangeCallback = null;
}
private void initAttributes(Context context, AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CameraSettingsMenuIndicatorWidget);
setLabelTextSize(typedArray.getDimension(R.styleable.CameraSettingsMenuIndicatorWidget_uxsdk_settingsTextSize, 12));
setLabelTextColor(typedArray.getColor(R.styleable.CameraSettingsMenuIndicatorWidget_uxsdk_settingsTextColor, Color.WHITE));
setLabelTextBackground(typedArray.getDrawable(R.styleable.CameraSettingsMenuIndicatorWidget_uxsdk_settingsTextBackground));
typedArray.recycle();
}
//endregion
//region customizations
/**
* Set callback for when the widget is tapped.
* This can be used to link the widget to //TODO camera settings menu panel
*
* @param stateChangeCallback listener to handheld callback
*/
public void setStateChangeCallback(@NonNull OnStateChangeCallback<Object> stateChangeCallback) {
this.stateChangeCallback = stateChangeCallback;
}
/**
* Get the color of the widget label text
*
* @return integer color value
*/
@ColorInt
public int getLabelTextColor() {
return foregroundTextView.getCurrentTextColor();
}
/**
* Set the color of the widget label text
*
* @param color integer value to be used
*/
public void setLabelTextColor(@ColorInt int color) {
foregroundTextView.setTextColor(color);
}
/**
* Set the background of the widget label
*
* @param resourceId to be used
*/
public void setLabelTextBackground(@DrawableRes int resourceId) {
foregroundTextView.setBackgroundResource(resourceId);
}
/**
* Set the background of the widget label
*
* @param drawable to be used
*/
public void setLabelTextBackground(@Nullable Drawable drawable) {
foregroundTextView.setBackground(drawable);
}
/**
* Get the background of the widget label
*
* @return Drawable
*/
@Nullable
public Drawable getLabelBackground() {
return foregroundTextView.getBackground();
}
/**
* Get the text size of the widget label
*
* @return float value representing text size
*/
@Dimension
public float getLabelTextSize() {
return foregroundTextView.getTextSize();
}
/**
* Set the text size of the widget label
*
* @param textSize float value representing text size
*/
public void setLabelTextSize(@Dimension float textSize) {
foregroundTextView.setTextSize(textSize);
}
//endregion
}

Some files were not shown because too many files have changed in this diff Show More