Merge branch 'master' of gitlab.navinfo.com:vivo/navinfovivo

 Conflicts:
	app/build.gradle
	app/src/main/java/com/navinfo/volvo/ui/MainActivity.kt
	app/src/main/java/com/navinfo/volvo/ui/fragments/message/ObtainMessageFragment.kt
	app/src/main/java/com/navinfo/volvo/ui/fragments/message/ObtainMessageViewModel.kt
	app/src/main/res/layout/activity_main.xml
	app/src/main/res/layout/fragment_dashboard.xml
	app/src/main/res/layout/fragment_obtain_message.xml
	app/src/main/res/navigation/mobile_navigation.xml
	app/src/main/res/values-night/themes.xml
	app/src/main/res/values/themes.xml
This commit is contained in:
squallzhjch
2023-01-04 15:16:53 +08:00
38 changed files with 1921 additions and 87 deletions

View File

@@ -121,6 +121,24 @@ dependencies {
// androidTestImplementation "com.google.dagger:hilt-android-testing:2.41" // androidTestImplementation "com.google.dagger:hilt-android-testing:2.41"
// kaptAndroidTest "com.google.dagger:hilt-android-compiler:2.41" // kaptAndroidTest "com.google.dagger:hilt-android-compiler:2.41"
// 显示错误提示 https://github.com/nhaarman/supertooltips
implementation 'com.nhaarman.supertooltips:library:3.0.0'
// 权限请求框架https://github.com/getActivity/XXPermissions
implementation 'com.github.getActivity:XXPermissions:16.5'
// 相机库 https://natario1.github.io/CameraView/about/getting-started
implementation("com.otaliastudios:cameraview:2.7.2")
// 图片压缩算法 https://github.com/Curzibn/Luban
implementation 'top.zibin:Luban:1.1.8'
// Android工具类库 https://github.com/gycold/EasyAndroid
implementation 'io.github.gycold:easyandroid:2.0.7'
// 日志工具 https://github.com/elvishew/xLog/blob/master/README_ZH.md
implementation 'com.elvishew:xlog:1.10.1'
//加载图片的依赖包
implementation ("com.github.bumptech.glide:glide:4.11.0") {
exclude group: "com.android.support"
}
} }
kapt { kapt {

View File

@@ -1,6 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<!-- 拍照 -->
<uses-permission
android:name="android.permission.CAMERA"
android:required="false" /> <!-- 网络请求 -->
<uses-permission
android:name="android.permission.INTERNET"
android:required="false" /> <!-- 录音 -->
<uses-permission
android:name="android.permission.RECORD_AUDIO"
android:required="false" /> <!-- 读写文件 -->
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application <application
android:name=".MyApplication" android:name=".MyApplication"
@@ -9,11 +22,21 @@
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.NavinfoVolvo" android:theme="@style/Theme.NavinfoVolvo"
tools:targetApi="31"> android:usesCleartextTraffic="true">
<activity
android:name=".ui.message.MessageActivity"
android:exported="false"
android:label="@string/title_activity_second"
android:theme="@style/Theme.NavinfoVolvo.NoActionBar">
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity <activity
android:name="com.navinfo.volvo.ui.MainActivity" android:name="com.navinfo.volvo.ui.MainActivity"
android:exported="true" android:exported="true"
@@ -23,11 +46,12 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity>
<meta-data <meta-data
android:name="android.app.lib_name" android:name="android.app.lib_name"
android:value="" /> android:value="" />
</activity> <meta-data android:name="ScopedStorage" android:value="true" />
</application> </application>
<uses-permission android:name="android.permission.INTERNET"></uses-permission> <uses-permission android:name="android.permission.INTERNET"></uses-permission>
</manifest> </manifest>

View File

@@ -0,0 +1,66 @@
package com.navinfo.volvo
import android.media.MediaRecorder
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.easytools.tools.DateUtils
import com.elvishew.xlog.XLog
import com.navinfo.volvo.utils.SystemConstant
import java.util.*
class RecorderLifecycleObserver: DefaultLifecycleObserver {
private var mediaRecorder: MediaRecorder? = null
private lateinit var recorderAudioPath: String
fun initAndStartRecorder() {
recorderAudioPath = "${SystemConstant.SoundFolder}/${DateUtils.date2Str(Date(), DateUtils.FORMAT_YMDHMS)}.m4a"
mediaRecorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.DEFAULT)
setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
// 开始录音
setOutputFile(recorderAudioPath)
try {
prepare()
} catch (e: Exception) {
XLog.e("prepare() failed")
}
start()
}
}
fun stopAndReleaseRecorder(): String {
mediaRecorder?.stop()
mediaRecorder?.release()
mediaRecorder = null
return recorderAudioPath
}
// override fun onCreate(owner: LifecycleOwner) {
// super.onCreate(owner)
//
// }
//
// override fun onStart(owner: LifecycleOwner) {
// super.onStart(owner)
// }
//
// override fun onResume(owner: LifecycleOwner) {
// super.onResume(owner)
// }
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
mediaRecorder?.pause()
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
mediaRecorder?.stop()
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
mediaRecorder?.release()
}
}

View File

@@ -0,0 +1,7 @@
package com.navinfo.volvo.http
class DefaultResponse<T> {
var code: Int = 0
var message: String = ""
var data: T? = null
}

View File

@@ -0,0 +1,30 @@
package com.navinfo.volvo.http
import com.navinfo.volvo.db.dao.entity.Attachment
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create
import java.io.File
class NavinfoVolvoCall {
companion object {
private val service by lazy {
Retrofit.Builder().baseUrl("http://ec2-52-81-73-5.cn-north-1.compute.amazonaws.com.cn:8088/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NavinfoVolvoService::class.java)
}
private var instance: NavinfoVolvoCall? = null
get() {
if (field == null) {
field = NavinfoVolvoCall()
}
return field
}
fun getApi(): NavinfoVolvoService {
return service
}
}
}

View File

@@ -0,0 +1,26 @@
package com.navinfo.volvo.http
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import java.io.File
interface NavinfoVolvoService {
@POST("/navi/cardDelivery/insertCardByApp")
fun insertCardByApp(@Body insertData: MutableMap<String, String>)
@POST("/navi/cardDelivery/updateCardByApp")
fun updateCardByApp(@Body updateData: MutableMap<String, String>)
@POST("/navi/cardDelivery/queryCardListByApp")
fun queryCardListByApp(@Body queryData: MutableMap<String, String>)
@POST("/navi/cardDelivery/deleteCardByApp")
fun deleteCardByApp(@Body deleteData: MutableMap<String, String>)
@POST("/img/upload")
@Multipart
suspend fun uploadAttachment(@Part attachmentFile: MultipartBody.Part):DefaultResponse<MutableMap<String, String>>
@POST("/img/download")
fun downLoadAttachment(@Body downloadData: MutableMap<String, String>):Call<DefaultResponse<MutableMap<String, String>>>
}

View File

@@ -1,13 +1,18 @@
package com.navinfo.volvo.ui package com.navinfo.volvo.ui
import android.content.DialogInterface
import android.content.Intent
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.navinfo.volvo.R import com.navinfo.volvo.R
import com.navinfo.volvo.databinding.ActivityMainBinding import com.navinfo.volvo.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -23,6 +28,38 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setupNavigation() setupNavigation()
XXPermissions.with(this)
// 申请单个权限
.permission(Permission.WRITE_EXTERNAL_STORAGE)
.permission(Permission.READ_EXTERNAL_STORAGE)
// 设置权限请求拦截器(局部设置)
//.interceptor(new PermissionInterceptor())
// 设置不触发错误检测机制(局部设置)
//.unchecked()
.request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, all: Boolean) {
if (!all) {
Toast.makeText(this@MainActivity, "获取部分权限成功,但部分权限未正常授予", Toast.LENGTH_SHORT).show()
return
}
// 在SD卡创建项目目录
createRootFolder()
}
override fun onDenied(permissions: MutableList<String>, never: Boolean) {
if (never) {
Toast.makeText(this@MainActivity, "永久拒绝授权,请手动授权文件读写权限", Toast.LENGTH_SHORT).show()
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(this@MainActivity, permissions)
} else {
onSDCardDenied()
showRationaleForSDCard(permissions)
}
}
})
} }
private fun setupNavigation() { private fun setupNavigation() {
@@ -57,4 +94,73 @@ class MainActivity : AppCompatActivity() {
override fun onSupportNavigateUp() = override fun onSupportNavigateUp() =
findNavController(R.id.nav_host_fragment_activity_main).navigateUp() findNavController(R.id.nav_host_fragment_activity_main).navigateUp()
// @NeedsPermission(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
fun createRootFolder() {
// 在SD卡创建项目目录
val sdCardPath = getExternalFilesDir(null)
// SystemConstant.ROOT_PATH = "${sdCardPath}/${SystemConstant.FolderName}"
SystemConstant.ROOT_PATH = sdCardPath!!.absolutePath
SystemConstant.LogFolder = "${sdCardPath!!.absolutePath}/log"
FileUtils.createOrExistsDir(SystemConstant.LogFolder)
SystemConstant.CameraFolder = "${sdCardPath!!.absolutePath}/camera"
FileUtils.createOrExistsDir(SystemConstant.CameraFolder)
SystemConstant.SoundFolder = "${sdCardPath!!.absolutePath}/sound"
FileUtils.createOrExistsDir(SystemConstant.SoundFolder)
xLogInit(SystemConstant.LogFolder)
}
fun xLogInit(logFolder: String) {
val config = LogConfiguration.Builder()
.logLevel(
if (BuildConfig.DEBUG)
LogLevel.ALL // 指定日志级别,低于该级别的日志将不会被打印,默认为 LogLevel.ALL
else LogLevel.NONE
)
.tag("Volvo") // 指定 TAG默认为 "X-LOG"
.enableThreadInfo() // 允许打印线程信息,默认禁止
.enableStackTrace(2) // 允许打印深度为 2 的调用栈信息,默认禁止
.enableBorder() // 允许打印日志边框,默认禁止
.addInterceptor(
BlacklistTagsFilterInterceptor( // 添加黑名单 TAG 过滤器
"blacklist1", "blacklist2", "blacklist3"
)
)
.build()
val androidPrinter: Printer = AndroidPrinter(true) // 通过 android.util.Log 打印日志的打印器
val consolePrinter: Printer = ConsolePrinter() // 通过 System.out 打印日志到控制台的打印器
val filePrinter: Printer = FilePrinter.Builder("${SystemConstant.ROOT_PATH}/Logs") // 指定保存日志文件的路径
.fileNameGenerator(DateFileNameGenerator()) // 指定日志文件名生成器,默认为 ChangelessFileNameGenerator("log")
.backupStrategy(NeverBackupStrategy()) // 指定日志文件备份策略,默认为 FileSizeBackupStrategy(1024 * 1024)
.build()
XLog.init( // 初始化 XLog
config, // 指定日志配置,如果不指定,会默认使用 new LogConfiguration.Builder().build()
androidPrinter, // 添加任意多的打印器。如果没有添加任何打印器,会默认使用 AndroidPrinter(Android)/ConsolePrinter(java)
consolePrinter,
filePrinter
)
}
// @OnShowRationale(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
fun showRationaleForSDCard(permissions: MutableList<String>) {
// showRationaleDialog(R.string.permission_camera_rationale, request)
// Toast.makeText(context, "当前操作需要您授权相机权限!", Toast.LENGTH_SHORT).show()
MaterialAlertDialogBuilder(this)
.setTitle("提示")
.setMessage("当前操作需要您授权读写SD卡权限")
.setPositiveButton("确定", DialogInterface.OnClickListener { dialogInterface, i ->
dialogInterface.dismiss()
XXPermissions.startPermissionActivity(this@MainActivity, permissions)
})
.show()
}
// @OnPermissionDenied(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
fun onSDCardDenied() {
Toast.makeText(this, "当前操作需要您授权读写SD卡权限", Toast.LENGTH_SHORT).show()
}
} }

View File

@@ -0,0 +1,112 @@
package com.navinfo.volvo.ui.camera
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.get
import androidx.navigation.Navigation
import com.easytools.tools.DateUtils
import com.easytools.tools.FileUtils
import com.elvishew.xlog.XLog
import com.navinfo.volvo.databinding.FragmentCameraBinding
import com.navinfo.volvo.ui.message.ObtainMessageViewModel
import com.navinfo.volvo.utils.SystemConstant
import com.otaliastudios.cameraview.CameraListener
import com.otaliastudios.cameraview.CameraView
import com.otaliastudios.cameraview.FileCallback
import com.otaliastudios.cameraview.PictureResult
import top.zibin.luban.Luban
import top.zibin.luban.OnCompressListener
import java.io.File
import java.util.*
class CameraFragment : Fragment() {
private var _binding: FragmentCameraBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
// private val exportFolderPath by lazy {
// "${SystemConstant.ROOT_PATH}/exportPic/"
// }
private val cameraLifeCycleObserver: CameraLifeCycleObserver by lazy {
CameraLifeCycleObserver()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// lifecycle.addObserver(cameraLifeCycleObserver)
val cameraViewModel =
ViewModelProvider(this).get(CameraViewModel::class.java)
_binding = FragmentCameraBinding.inflate(inflater, container, false)
val root: View = binding.root
val cameraView: CameraView = binding.camera
cameraView.setLifecycleOwner(this)
cameraView.addCameraListener(object:CameraListener() { // 添加拍照回调
override fun onPictureTaken(result: PictureResult) {
super.onPictureTaken(result)
// FileUtils.createOrExistsDir(cameraFolderPath)
val resultFile = File("${SystemConstant.CameraFolder}/${DateUtils.date2Str(Date(), DateUtils.FORMAT_YMDHMS)}.jpg")
result.toFile(resultFile, object: FileCallback {
override fun onFileReady(resultFile: File?) {
// 压缩图片文件
Luban.with(context)
.load<Any>(mutableListOf(resultFile) as List<Any>?)
.ignoreBy(200)
.setTargetDir("${SystemConstant.CameraFolder}")
.filter { path ->
!(TextUtils.isEmpty(path) || path.lowercase(Locale.getDefault())
.endsWith(".gif"))
}
.setCompressListener(object : OnCompressListener {
override fun onStart() {
XLog.d("开始压缩图片")
}
override fun onSuccess(file: File?) {
XLog.d("压缩图片成功:${file?.absolutePath}")
// 删除源文件
if (!resultFile!!.absolutePath.equals(file!!.absolutePath)) {
resultFile!!.delete()
}
// 跳转回原Fragment展示拍摄的照片
ViewModelProvider(requireActivity()).get(ObtainMessageViewModel::class.java).updateMessagePic(file!!.absolutePath)
// 跳转回原界面
Navigation.findNavController(root).popBackStack()
}
override fun onError(e: Throwable) {
XLog.d("压缩图片失败:${e.message}")
}
}).launch()
}
})
}
})
// 点击拍照
binding.imgStartCamera.setOnClickListener {
cameraView.takePicture()
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
// lifecycle.removeObserver(cameraLifeCycleObserver)
}
}

View File

@@ -0,0 +1,31 @@
package com.navinfo.volvo.ui.camera
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
class CameraLifeCycleObserver: DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
super.onCreate(owner)
}
override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)
}
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
}
override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
}
}

View File

@@ -0,0 +1,13 @@
package com.navinfo.volvo.ui.camera
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class CameraViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is dashboard Fragment"
}
val text: LiveData<String> = _text
}

View File

@@ -1,21 +1,57 @@
package com.navinfo.volvo.ui.fragments.message package com.navinfo.volvo.ui.fragments.message
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.*
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.AdapterView.OnItemSelectedListener
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import com.bumptech.glide.Glide
import com.easytools.tools.DateUtils
import com.easytools.tools.ToastUtils
import com.elvishew.xlog.XLog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.gredicer.datetimepicker.DateTimePickerFragment import com.gredicer.datetimepicker.DateTimePickerFragment
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import com.navinfo.volvo.R
import com.navinfo.volvo.RecorderLifecycleObserver
import com.navinfo.volvo.databinding.FragmentObtainMessageBinding import com.navinfo.volvo.databinding.FragmentObtainMessageBinding
import com.navinfo.volvo.db.dao.entity.AttachmentType
import com.navinfo.volvo.db.dao.entity.Message
import com.navinfo.volvo.ui.markRequiredInRed import com.navinfo.volvo.ui.markRequiredInRed
import com.navinfo.volvo.utils.EasyMediaFile
import com.navinfo.volvo.utils.SystemConstant
import com.nhaarman.supertooltips.ToolTip
import top.zibin.luban.Luban
import top.zibin.luban.OnCompressListener
import java.io.File
import java.util.*
//@RuntimePermissions
class ObtainMessageFragment: Fragment() { class ObtainMessageFragment: Fragment() {
private var _binding: FragmentObtainMessageBinding? = null private var _binding: FragmentObtainMessageBinding? = null
private val obtainMessageViewModel by lazy { private val obtainMessageViewModel by lazy {
ViewModelProvider(requireActivity()).get(ObtainMessageViewModel::class.java) ViewModelProvider(requireActivity()).get(ObtainMessageViewModel::class.java)
} }
private val photoHelper by lazy {
EasyMediaFile().setCrop(true)
}
private val recorderLifecycleObserver by lazy {
RecorderLifecycleObserver()
}
// This property is only valid between onCreateView and // This property is only valid between onCreateView and
// onDestroyView. // onDestroyView.
@@ -29,38 +65,289 @@ class ObtainMessageFragment: Fragment() {
_binding = FragmentObtainMessageBinding.inflate(inflater, container, false) _binding = FragmentObtainMessageBinding.inflate(inflater, container, false)
val root: View = binding.root val root: View = binding.root
obtainMessageViewModel.getMessageLiveData()?.observe( obtainMessageViewModel.setCurrentMessage(Message())
obtainMessageViewModel?.getMessageLiveData()?.observe(
viewLifecycleOwner, Observer { viewLifecycleOwner, Observer {
// 初始化界面显示内容 // 初始化界面显示内容
if(it.title!=null) if(it.title?.isNotEmpty() == true)
binding.tvMessageTitle.setText(it.title) binding.tvMessageTitle?.setText(it.title)
if (it.sendDate!=null) { if (it.sendDate?.isNotEmpty() == true) {
binding.btnSendTime.text = it.sendDate binding.btnSendTime.text = it.sendDate
} }
var hasPhoto = false
var hasAudio = false
if (it.attachment.isNotEmpty()) {
// 展示照片文件或录音文件
for (attachment in it.attachment) {
if (attachment.attachmentType == AttachmentType.PIC) {
Glide.with(context!!)
.asBitmap().fitCenter()
.load(attachment.pathUrl)
.into(binding.imgMessageAttachment)
// 显示名称
binding.tvPhotoName.text = attachment.pathUrl.replace("\\", "/").substringAfterLast("/")
hasPhoto = true
// 如果当前attachment文件是本地文件开始尝试网络上传
if (!attachment.pathUrl.startsWith("http")) {
obtainMessageViewModel.uploadAttachment(File(attachment.pathUrl))
}
}
if (attachment.attachmentType == AttachmentType.AUDIO) {
binding.tvAudioName.text = attachment.pathUrl.replace("\\", "/").substringAfterLast("/")
hasAudio = true
}
}
}
binding.layerPhotoResult.visibility = if (hasPhoto) VISIBLE else GONE
binding.layerGetPhoto.visibility = if (hasPhoto) GONE else VISIBLE
binding.layerAudioResult.visibility = if (hasAudio) VISIBLE else GONE
binding.layerGetAudio.visibility = if (hasAudio) GONE else VISIBLE
} }
) )
lifecycle.addObserver(recorderLifecycleObserver)
initView() initView()
return root return root
} }
private fun initView() { fun initView() {
// 设置问候信息提示的红色星号 // 设置问候信息提示的红色星号
binding.tiLayoutTitle.markRequiredInRed() binding.tiLayoutTitle.markRequiredInRed()
binding.tvMessageTitle.addTextChangedListener {
obtainMessageViewModel.updateMessageTitle(it.toString())
}
binding.imgPhotoDelete.setOnClickListener {
obtainMessageViewModel.updateMessagePic(null)
}
binding.imgAudioDelete.setOnClickListener {
obtainMessageViewModel.updateMessageAudio(null)
}
val sendToArray = mutableListOf<String>("绑定车辆1(LYVXFEFEXNL754427)")
binding.edtSendTo.adapter = ArrayAdapter<String>(context!!,
android.R.layout.simple_dropdown_item_1line, android.R.id.text1, sendToArray)
binding.edtSendTo.onItemSelectedListener = object: OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
obtainMessageViewModel.getMessageLiveData().value?.toId = sendToArray[p2]
}
override fun onNothingSelected(p0: AdapterView<*>?) {
}
}
// 设置点击按钮选择发送时间 // 设置点击按钮选择发送时间
binding.btnSendTime.setOnClickListener { binding.btnSendTime.setOnClickListener {
val dialog = DateTimePickerFragment.newInstance().mode(0) val dialog = DateTimePickerFragment.newInstance().mode(0)
dialog.listener = object : DateTimePickerFragment.OnClickListener { dialog.listener = object : DateTimePickerFragment.OnClickListener {
override fun onClickListener(selectTime: String) { override fun onClickListener(selectTime: String) {
val sendDate = DateUtils.str2Date(selectTime, "yyyy-MM-dd HH:mm")
if (sendDate <= Date()) {
obtainMessageViewModel.updateMessageSendTime("现在")
} else {
obtainMessageViewModel.updateMessageSendTime(selectTime) obtainMessageViewModel.updateMessageSendTime(selectTime)
} }
}
} }
dialog.show(parentFragmentManager, "SelectSendTime") dialog.show(parentFragmentManager, "SelectSendTime")
} }
// 点击按钮选择拍照 // 点击按钮选择拍照
binding.edtSendTo.setOnClickListener { binding.btnStartCamera.setOnClickListener {
// 启动相机
XXPermissions.with(this)
// 申请单个权限
.permission(Permission.CAMERA)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, all: Boolean) {
if (!all) {
Toast.makeText(activity, "获取部分权限成功,但部分权限未正常授予", Toast.LENGTH_SHORT).show()
return
}
// 开始启动拍照界面
photoHelper.setCrop(true).takePhoto(activity!!)
}
override fun onDenied(permissions: MutableList<String>, never: Boolean) {
if (never) {
Toast.makeText(activity, "永久拒绝授权,请手动授权拍照权限", Toast.LENGTH_SHORT).show()
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(context!!, permissions)
} else {
onCameraDenied()
showRationaleForCamera(permissions)
}
}
})
// startCamera(it)
}
binding.btnStartPhoto.setOnClickListener {
photoHelper.setCrop(true).selectPhoto(activity!!)
}
// 用户选择录音文件
binding.btnSelectSound.setOnClickListener {
photoHelper.setCrop(false).selectAudio(activity!!)
}
// 开始录音
binding.btnStartRecord.setOnClickListener {
// 申请权限
XXPermissions.with(this)
// 申请单个权限
.permission(Permission.RECORD_AUDIO)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, all: Boolean) {
if (!all) {
Toast.makeText(activity, "获取部分权限成功,但部分权限未正常授予", Toast.LENGTH_SHORT).show()
return
}
if (it.isSelected) {
it.isSelected = false
val recorderAudioPath = recorderLifecycleObserver.stopAndReleaseRecorder()
if (File(recorderAudioPath).exists()) {
obtainMessageViewModel.updateMessageAudio(recorderAudioPath)
}
} else{
it.isSelected = true
recorderLifecycleObserver.initAndStartRecorder()
}
}
override fun onDenied(permissions: MutableList<String>, never: Boolean) {
if (never) {
Toast.makeText(activity, "永久拒绝授权,请手动授权拍照权限", Toast.LENGTH_SHORT).show()
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(context!!, permissions)
} else {
onCameraDenied()
showRationaleForCamera(permissions)
}
}
})
}
// 获取照片文件和音频文件
photoHelper.setCallback {
if (it.exists()) {
val fileName = it.name.lowercase()
if (fileName.endsWith(".jpg")||fileName.endsWith(".jpeg")||fileName.endsWith(".png")) {
// 获取选中的图片,自动压缩图片质量
// 压缩图片文件
Luban.with(context)
.load<Any>(mutableListOf(it) as List<Any>?)
.ignoreBy(200)
.setTargetDir("${SystemConstant.CameraFolder}")
.filter { path ->
!(TextUtils.isEmpty(path) || path.lowercase(Locale.getDefault())
.endsWith(".gif"))
}
.setCompressListener(object : OnCompressListener {
override fun onStart() {
XLog.d("开始压缩图片/${it.absolutePath}")
}
override fun onSuccess(file: File?) {
XLog.d("压缩图片成功:${file?.absolutePath}")
// 删除源文件
if (!it.absolutePath.equals(file?.absolutePath)) {
it?.delete()
}
// 跳转回原Fragment展示拍摄的照片
ViewModelProvider(requireActivity()).get(ObtainMessageViewModel::class.java).updateMessagePic(file!!.absolutePath)
}
override fun onError(e: Throwable) {
XLog.d("压缩图片失败:${e.message}")
}
}).launch()
} else if (fileName.endsWith(".mp3")||fileName.endsWith(".wav")||fileName.endsWith(".amr")||fileName.endsWith(".m4a")) {
ToastUtils.showToast(it.absolutePath)
obtainMessageViewModel.updateMessageAudio(it.absolutePath)
}
}
}
photoHelper.setError {
ToastUtils.showToast(it.message)
}
binding.btnObtainMessageBack.setOnClickListener {
Navigation.findNavController(it).popBackStack()
}
binding.btnObtainMessageConfirm.setOnClickListener {
// 检查当前输入数据
val messageData = obtainMessageViewModel.getMessageLiveData().value
if (messageData?.title?.isEmpty() == true) {
val toolTipRelativeLayout =
binding.ttTitle
val toolTip = ToolTip()
.withText("请输入问候信息")
.withColor(com.navinfo.volvo.R.color.purple_200)
.withShadow()
.withAnimationType(ToolTip.AnimationType.FROM_MASTER_VIEW)
toolTipRelativeLayout.showToolTipForView(toolTip, binding.tiLayoutTitle)
}
var hasPic = false
var hasAudio = false
for (attachment in messageData?.attachment!!) {
if (attachment.attachmentType == AttachmentType.PIC) {
hasPic = true
}
if (attachment.attachmentType == AttachmentType.AUDIO) {
hasAudio = true
}
}
if (!hasPic) {
val toolTipRelativeLayout =
binding.ttPic
val toolTip = ToolTip()
.withText("需要提供照片文件")
.withColor(com.navinfo.volvo.R.color.purple_200)
.withShadow()
.withAnimationType(ToolTip.AnimationType.FROM_MASTER_VIEW)
toolTipRelativeLayout.showToolTipForView(toolTip, binding.tvUploadPic)
}
if (!hasAudio) {
val toolTipRelativeLayout =
binding.ttAudio
val toolTip = ToolTip()
.withText("需要提供音频文件")
.withColor(com.navinfo.volvo.R.color.purple_200)
.withShadow()
.withAnimationType(ToolTip.AnimationType.FROM_MASTER_VIEW)
toolTipRelativeLayout.showToolTipForView(toolTip, binding.tvUploadPic)
}
if (messageData?.fromId?.isEmpty()==true) {
val toolTipRelativeLayout =
binding.ttSendFrom
val toolTip = ToolTip()
.withText("请输入您的名称")
.withColor(com.navinfo.volvo.R.color.purple_200)
.withShadow()
.withAnimationType(ToolTip.AnimationType.FROM_MASTER_VIEW)
toolTipRelativeLayout.showToolTipForView(toolTip, binding.edtSendFrom)
}
if (messageData?.toId?.isEmpty()==true) {
val toolTipRelativeLayout =
binding.ttSendTo
val toolTip = ToolTip()
.withText("请选择要发送的车辆")
.withColor(com.navinfo.volvo.R.color.purple_200)
.withShadow()
.withAnimationType(ToolTip.AnimationType.FROM_MASTER_VIEW)
toolTipRelativeLayout.showToolTipForView(toolTip, binding.edtSendTo)
}
} }
} }
@@ -68,4 +355,45 @@ class ObtainMessageFragment: Fragment() {
super.onDestroyView() super.onDestroyView()
_binding = null _binding = null
} }
fun startCamera(it: View) {
Navigation.findNavController(binding.root).navigate(com.navinfo.volvo.R.id.nav_2_camera)
}
fun showRationaleForCamera(permissions: MutableList<String>) {
// showRationaleDialog(R.string.permission_camera_rationale, request)
// Toast.makeText(context, "当前操作需要您授权相机权限!", Toast.LENGTH_SHORT).show()
MaterialAlertDialogBuilder(context!!)
.setTitle("提示")
.setMessage("当前操作需要您授权拍摄权限!")
.setPositiveButton("确定", DialogInterface.OnClickListener { dialogInterface, i ->
dialogInterface.dismiss()
XXPermissions.startPermissionActivity(activity!!, permissions)
})
.show()
}
// @OnPermissionDenied(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
fun onCameraDenied() {
ToastUtils.showToast("当前操作需要您授权拍摄权限!")
}
fun showRationaleForRecorder(permissions: MutableList<String>) {
MaterialAlertDialogBuilder(context!!)
.setTitle("提示")
.setMessage("当前操作需要您授权录音权限!")
.setPositiveButton("确定", DialogInterface.OnClickListener { dialogInterface, i ->
dialogInterface.dismiss()
XXPermissions.startPermissionActivity(activity!!, permissions)
})
.show()
}
fun onRecorderDenied() {
ToastUtils.showToast("当前操作需要您授权录音权限!")
}
override fun onDestroy() {
super.onDestroy()
lifecycle.removeObserver(recorderLifecycleObserver)
}
} }

View File

@@ -1,9 +1,19 @@
package com.navinfo.volvo.ui.fragments.message package com.navinfo.volvo.ui.fragments.message
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.*
import androidx.lifecycle.ViewModel import com.easytools.tools.ToastUtils
import com.navinfo.volvo.model.Message import com.elvishew.xlog.XLog
import com.navinfo.volvo.model.AttachmentType import com.navinfo.volvo.db.dao.entity.Attachment
import com.navinfo.volvo.db.dao.entity.AttachmentType
import com.navinfo.volvo.db.dao.entity.Message
import com.navinfo.volvo.http.NavinfoVolvoCall
import kotlinx.coroutines.launch
import okhttp3.MediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import java.io.File
import java.util.*
class ObtainMessageViewModel: ViewModel() { class ObtainMessageViewModel: ViewModel() {
private val msgLiveData: MutableLiveData<Message> by lazy { private val msgLiveData: MutableLiveData<Message> by lazy {
@@ -25,21 +35,40 @@ class ObtainMessageViewModel: ViewModel() {
} }
// 更新消息附件中的照片文件 // 更新消息附件中的照片文件
fun updateMessagePic(picUrl: String) { fun updateMessagePic(picUrl: String?) {
var hasPic = false
for (attachment in this.msgLiveData.value!!.attachment) { for (attachment in this.msgLiveData.value!!.attachment) {
if (attachment.attachmentType == AttachmentType.PIC) { if (attachment.attachmentType == AttachmentType.PIC) {
if (picUrl==null||picUrl.isEmpty()) {
this.msgLiveData.value!!.attachment.remove(attachment)
} else {
attachment.pathUrl = picUrl attachment.pathUrl = picUrl
} }
hasPic = true
}
}
if (!hasPic&&picUrl!=null) {
this.msgLiveData.value!!.attachment.add(Attachment(UUID.randomUUID().toString(), picUrl, AttachmentType.PIC))
} }
this.msgLiveData.postValue(this.msgLiveData.value) this.msgLiveData.postValue(this.msgLiveData.value)
} }
// 更新消息附件中的录音文件 // 更新消息附件中的录音文件
fun updateMessageAudio(audioUrl: String) { fun updateMessageAudio(audioUrl: String?) {
var hasAudio = false
for (attachment in this.msgLiveData.value!!.attachment) { for (attachment in this.msgLiveData.value!!.attachment) {
if (attachment.attachmentType == AttachmentType.AUDIO) { if (attachment.attachmentType == AttachmentType.AUDIO) {
if (audioUrl==null||audioUrl.isEmpty()) {
this.msgLiveData.value!!.attachment.remove(attachment)
} else {
attachment.pathUrl = audioUrl attachment.pathUrl = audioUrl
} }
hasAudio = true
}
}
if (!hasAudio&&audioUrl!=null) {
this.msgLiveData.value!!.attachment.add(Attachment(UUID.randomUUID().toString(), audioUrl, AttachmentType.AUDIO))
} }
this.msgLiveData.postValue(this.msgLiveData.value) this.msgLiveData.postValue(this.msgLiveData.value)
} }
@@ -61,4 +90,25 @@ class ObtainMessageViewModel: ViewModel() {
this.msgLiveData.value?.sendDate = sendTime this.msgLiveData.value?.sendDate = sendTime
this.msgLiveData.postValue(this.msgLiveData.value) this.msgLiveData.postValue(this.msgLiveData.value)
} }
fun uploadAttachment(attachmentFile: File) {
// 启用协程调用网络请求
viewModelScope.launch {
try {
val requestFile: RequestBody =
RequestBody.create(MediaType.parse("multipart/form-data"), attachmentFile)
val body = MultipartBody.Part.createFormData("picture", attachmentFile.getName(), requestFile)
val result = NavinfoVolvoCall.getApi().uploadAttachment(body)
XLog.d(result.code)
if (result.code == 200) { // 请求成功
// 获取上传后的结果
} else {
ToastUtils.showToast(result.message)
}
} catch (e: Exception) {
ToastUtils.showToast(e.message)
XLog.d(e.message)
}
}
}
} }

View File

@@ -0,0 +1,39 @@
package com.navinfo.volvo.ui.message
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.Navigation
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import com.navinfo.volvo.R
import com.navinfo.volvo.databinding.ActivityMessageBinding
class MessageActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMessageBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMessageBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
binding.toolbar.setOnClickListener {
Navigation.findNavController(it).popBackStack()
}
val navController = findNavController(R.id.nav_host_fragment_message)
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_message)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
}
}

View File

@@ -0,0 +1,648 @@
package com.navinfo.volvo.utils
import android.app.Activity
import android.app.Fragment
import android.content.*
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.os.*
import android.os.Environment.*
import android.provider.MediaStore
import java.io.File
import java.util.*
/**
* 创建日期2018/8/21 0021on 下午 4:40
* 描述:多媒体选择工具类
* @authorVincent
*/
class EasyMediaFile {
/**
* 设置图片选择结果回调
*/
private var callback: ((file: File) -> Unit)? = null
private var isCrop: Boolean = false
private var error: ((error: Exception) -> Unit)? = null
/**
* 视频录制/音频录制/拍照/剪切后图片的存放位置(参考file_provider_paths.xml中的路径)
*/
private var mFilePath: File? = null
private val mainHandler = Handler(Looper.getMainLooper())
fun setError(error: ((error: Exception) -> Unit)?): EasyMediaFile {
this.error = error
return this
}
fun setCallback(callback: ((file: File) -> Unit)): EasyMediaFile {
this.callback = callback
return this
}
fun setCrop(isCrop: Boolean): EasyMediaFile {
this.isCrop = isCrop
return this
}
/**
* 修改图片的存储路径默认的图片存储路径是SD卡上 Android/data/应用包名/时间戳.jpg
*
* @param imgPath 图片的存储路径(包括文件名和后缀)
*/
fun setFilePath(imgPath: String?): EasyMediaFile {
if (imgPath.isNullOrEmpty()) {
this.mFilePath = null
} else {
this.mFilePath = File(imgPath)
this.mFilePath?.parentFile?.mkdirs()
}
return this
}
/**
* 选择文件
* 支持图片、音频、视频
*/
// fun selectFile(activity: Activity) {
// isCrop = false
// val intent = Intent(Intent.ACTION_PICK, null).apply {
// setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "*/*")
// setDataAndType(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "*/*")
// setDataAndType(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "*/*")
// }
// if (Looper.myLooper() == Looper.getMainLooper()) {
// selectFileInternal(intent, activity, -1)
// } else {
// mainHandler.post { selectFileInternal(intent, activity, -1) }
// }
// }
/**
* 选择视频
*/
fun selectVideo(activity: Activity) {
isCrop = false
val intent = Intent(Intent.ACTION_PICK, null).apply {
setDataAndType(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, "video/*")
}
if (Looper.myLooper() == Looper.getMainLooper()) {
selectFileInternal(intent, activity, 2)
} else {
mainHandler.post { selectFileInternal(intent, activity, 2) }
}
}
/**
* 选择音频
*/
fun selectAudio(activity: Activity) {
isCrop = false
val intent = Intent(Intent.ACTION_PICK, null).apply {
setDataAndType(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, "audio/*")
}
if (Looper.myLooper() == Looper.getMainLooper()) {
selectFileInternal(intent, activity, 1)
} else {
mainHandler.post { selectFileInternal(intent, activity, 1) }
}
}
/**
* 选择图片
*/
fun selectPhoto(activity: Activity) {
val intent = Intent(Intent.ACTION_PICK, null).apply {
setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*")
}
if (Looper.myLooper() == Looper.getMainLooper()) {
selectFileInternal(intent, activity, 0)
} else {
mainHandler.post { selectFileInternal(intent, activity, 0) }
}
}
/**
* 选择文件
*/
private fun selectFileInternal(intent: Intent, activity: Activity, type: Int) {
val resolveInfoList = activity.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
if (resolveInfoList.isEmpty()) {
error?.invoke(IllegalStateException("No Activity found to handle Intent "))
} else {
PhotoFragment.findOrCreate(activity).start(intent, PhotoFragment.REQ_SELECT_FILE) { requestCode: Int, data: Intent? ->
if (requestCode != PhotoFragment.REQ_SELECT_FILE) {
return@start
}
data ?: return@start
data.data ?: return@start
try {
val inputFile = if (type != -1) {
uriToFile(activity, data.data!!, type)
} else {
if (data.data!!.path!!.contains(".")) {
File(data.data!!.path!!)
} else {
when {
data.data!!.path!!.contains("images") -> {
uriToFile(activity, data.data!!, 0)
}
data.data!!.path!!.contains("video") -> {
uriToFile(activity, data.data!!, 2)
}
else -> {
uriToFile(activity, data.data!!, 1)
}
}
}
}
if (isCrop) {//裁剪
zoomPhoto(inputFile, mFilePath
?: File(generateFilePath(activity)), activity)
} else {//不裁剪
callback?.invoke(inputFile)
}
} catch (e: Exception) {
error?.invoke(e)
}
}
}
}
private fun uriToFile(activity: Activity, uri: Uri): File {
// 首先使用系统提供的CursorLoader进行file获取
val context = activity.application
val projection = arrayOf(MediaStore.Images.Media.DATA)
var path: String
try {
CursorLoader(context, uri, projection, null, null, null)
.loadInBackground().apply {
getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
val index = getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
moveToFirst()
path = getString(index)
close()
}
return File(path)
} catch (e: Exception) {
// 当没获取到。再使用别的方式进行获取
val scheme = uri.scheme
path = uri.path ?: throw RuntimeException("Could not find path in this uri:[$uri]")
when (scheme) {
"file" -> {
val cr = context.contentResolver
val buff = StringBuffer()
buff.append("(").append(MediaStore.Images.ImageColumns.DATA).append("=").append("'$path'").append(")")
cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, arrayOf(MediaStore.Images.ImageColumns._ID,
MediaStore.Images.ImageColumns.DATA), buff.toString(), null, null).apply {
this ?: throw RuntimeException("cursor is null")
var dataIdx: Int
while (!this.isAfterLast) {
dataIdx = this.getColumnIndex(MediaStore.Images.ImageColumns.DATA)
path = this.getString(dataIdx)
this.moveToNext()
}
close()
}
return File(path)
}
"content" -> {
context.contentResolver.query(uri, arrayOf(MediaStore.Images.Media.DATA), null, null, null).apply {
this ?: throw RuntimeException("cursor is null")
if (this.moveToFirst()) {
val columnIndex = this.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
path = this.getString(columnIndex)
}
close()
}
return File(path)
}
else -> {
throw IllegalArgumentException("Could not find file by this uri$uri")
}
}
}
}
/**
* 拍照获取
*/
fun takePhoto(activity: Activity) {
val imgFile = if (isCrop) {
File(generateFilePath(activity))
} else {
mFilePath ?: File(generateFilePath(activity))
}
val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(imgFile)
} else {
//兼容android7.0 使用共享文件的形式
val contentValues = ContentValues(1)
contentValues.put(MediaStore.Images.Media.DATA, imgFile.absolutePath)
activity.application.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
}
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
if (Looper.myLooper() == Looper.getMainLooper()) {
takeFileInternal(imgFile, intent, activity)
} else {
mainHandler.post { takeFileInternal(imgFile, intent, activity) }
}
}
/**
* 音频录制
*/
fun takeAudio(activity: Activity) {
isCrop = false
val imgFile = mFilePath ?: File(generateFilePath(activity, 1))
val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(imgFile)
} else {
//兼容android7.0 使用共享文件的形式
val contentValues = ContentValues(1)
contentValues.put(MediaStore.Audio.Media.DATA, imgFile.absolutePath)
activity.application.contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues)
}
val intent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
if (Looper.myLooper() == Looper.getMainLooper()) {
takeFileInternal(imgFile, intent, activity, 1)
} else {
mainHandler.post { takeFileInternal(imgFile, intent, activity, 1) }
}
}
/**
* 视频录制
*/
fun takeVideo(activity: Activity) {
isCrop = false
val imgFile = mFilePath ?: File(generateFilePath(activity, 2))
val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(imgFile)
} else {
//兼容android7.0 使用共享文件的形式
val contentValues = ContentValues(1)
contentValues.put(MediaStore.Video.Media.DATA, imgFile.absolutePath)
activity.application.contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)
}
val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE).apply {
putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
// 默认录制时间10秒 部分手机该设置无效
// putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10000)
}
if (Looper.myLooper() == Looper.getMainLooper()) {
takeFileInternal(imgFile, intent, activity, 2)
} else {
mainHandler.post { takeFileInternal(imgFile, intent, activity, 2) }
}
}
/**
* 拍照或选择
*/
fun getImage(activity: Activity) {
val imgFile = if (isCrop) {
File(generateFilePath(activity))
} else {
mFilePath ?: File(generateFilePath(activity))
}
val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(imgFile)
} else {
//兼容android7.0 使用共享文件的形式
val contentValues = ContentValues(1)
contentValues.put(MediaStore.Images.Media.DATA, imgFile.absolutePath)
activity.application.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
}
val cameraIntents = ArrayList<Intent>()
val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val packageManager = activity.packageManager
val camList = packageManager.queryIntentActivities(captureIntent, 0)
for (res in camList) {
val packageName = res.activityInfo.packageName
val intent = Intent(captureIntent)
intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name)
intent.setPackage(packageName)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
cameraIntents.add(intent)
}
val intent = Intent.createChooser(createPickMore(), "请选择").also {
it.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toTypedArray())
}
if (Looper.myLooper() == Looper.getMainLooper()) {
takeFileInternal(imgFile, intent, activity)
} else {
mainHandler.post { takeFileInternal(imgFile, intent, activity) }
}
}
/**
* 音频录制或选择
*/
fun getAudio(activity: Activity) {
isCrop = false
val imgFile = mFilePath ?: File(generateFilePath(activity))
val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(imgFile)
} else {
//兼容android7.0 使用共享文件的形式
val contentValues = ContentValues(1)
contentValues.put(MediaStore.Audio.Media.DATA, imgFile.absolutePath)
activity.application.contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, contentValues)
}
val cameraIntents = ArrayList<Intent>()
val captureIntent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
val packageManager = activity.packageManager
val camList = packageManager.queryIntentActivities(captureIntent, 0)
for (res in camList) {
val packageName = res.activityInfo.packageName
val intent = Intent(captureIntent)
intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name)
intent.setPackage(packageName)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
cameraIntents.add(intent)
}
val intent = Intent.createChooser(createPickMore("audio/*"), "请选择").also {
it.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toTypedArray())
}
if (Looper.myLooper() == Looper.getMainLooper()) {
takeFileInternal(imgFile, intent, activity, 1)
} else {
mainHandler.post { takeFileInternal(imgFile, intent, activity, 1) }
}
}
/**
* 视频拍摄或选择
*/
fun getVideo(activity: Activity) {
isCrop = false
val imgFile = mFilePath ?: File(generateFilePath(activity, 2))
val imgUri = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Uri.fromFile(imgFile)
} else {
//兼容android7.0 使用共享文件的形式
val contentValues = ContentValues(1)
contentValues.put(MediaStore.Video.Media.DATA, imgFile.absolutePath)
activity.application.contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues)
}
val cameraIntents = ArrayList<Intent>()
val captureIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
// 某些手机此设置是不生效的,需要自行封装解决
// captureIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10000)
val packageManager = activity.packageManager
val camList = packageManager.queryIntentActivities(captureIntent, 0)
for (res in camList) {
val packageName = res.activityInfo.packageName
val intent = Intent(captureIntent)
intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name)
intent.setPackage(packageName)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUri)
cameraIntents.add(intent)
}
val intent = Intent.createChooser(createPickMore("video/*"), "请选择").also {
it.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toTypedArray())
}
if (Looper.myLooper() == Looper.getMainLooper()) {
takeFileInternal(imgFile, intent, activity, 2)
} else {
mainHandler.post { takeFileInternal(imgFile, intent, activity, 2) }
}
}
/**
* 向系统发出指令
*/
private fun takeFileInternal(takePhotoPath: File, intent: Intent, activity: Activity, type: Int = 0) {
val fragment = PhotoFragment.findOrCreate(activity)
fragment.start(intent, PhotoFragment.REQ_TAKE_FILE) { requestCode: Int, data: Intent? ->
if (requestCode == PhotoFragment.REQ_TAKE_FILE) {
if (data?.data != null) {
mFilePath = when (type) {
0 -> {
uriToFile(activity, data.data!!)
}
else -> uriToFile(activity, data.data!!, type)
}
if (isCrop) {
zoomPhoto(takePhotoPath, mFilePath
?: File(generateFilePath(activity)), activity)
} else {
callback?.invoke(mFilePath!!)
mFilePath = null
}
return@start
}
if (isCrop) {
zoomPhoto(takePhotoPath, mFilePath
?: File(generateFilePath(activity)), activity)
} else {
callback?.invoke(takePhotoPath)
}
}
}
}
private fun uriToFile(activity: Activity, data: Uri, type: Int): File {
val cursor = activity.managedQuery(data, arrayOf(if (type == 1) MediaStore.Audio.Media.DATA else MediaStore.Video.Media.DATA), null,
null, null)
val path = if (cursor == null) {
data.path
} else {
val index = cursor.getColumnIndexOrThrow(if (type == 1) MediaStore.Audio.Media.DATA else MediaStore.Video.Media.DATA)
cursor.moveToFirst()
cursor.getString(index)
}
// 手动关掉报错如下
// Caused by: android.database.StaleDataException: Attempted to access a cursor after it has been closed.
// cursor.close()
return File(path!!)
}
/***
* 图片裁剪
*/
private fun zoomPhoto(inputFile: File?, outputFile: File, activity: Activity) {
try {
val intent = Intent("com.android.camera.action.CROP")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setDataAndType(getImageContentUri(activity, inputFile), "image/*")
} else {
intent.setDataAndType(Uri.fromFile(inputFile), "image/*")
}
intent.putExtra("crop", "true")
// 是否返回uri
intent.putExtra("return-data", false)
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val imgFile = File("${Environment.getExternalStoragePublicDirectory(DIRECTORY_PICTURES)}/${outputFile.name}")
// 通过 MediaStore API 插入file 为了拿到系统裁剪要保存到的uri因为App没有权限不能访问公共存储空间需要通过 MediaStore API来操作
val values = ContentValues()
values.put(MediaStore.Images.Media.DATA, imgFile.getAbsolutePath());
values.put(MediaStore.Images.Media.DISPLAY_NAME, outputFile.name);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
val uri = activity.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile))
zoomPhotoInternal(outputFile, intent, activity)
}else {
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile))
zoomPhotoInternal(outputFile, intent, activity)
}
} catch (e: Exception) {
error?.invoke(e)
}
}
private fun zoomPhotoInternal(outputFile: File, intent: Intent, activity: Activity) {
PhotoFragment.findOrCreate(activity).start(intent, PhotoFragment.REQ_ZOOM_PHOTO) { requestCode: Int, data: Intent? ->
if (requestCode == PhotoFragment.REQ_ZOOM_PHOTO) {
data ?: return@start
callback?.invoke(outputFile)
}
}
}
/**构建文件多选Intent*/
private fun createPickMore(fileType: String = "image/*"): Intent {
val pictureChooseIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
type = fileType
}
pictureChooseIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true)
/**临时授权app访问URI代表的文件所有权*/
pictureChooseIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
return pictureChooseIntent
}
/**
* 产生图片的路径,带文件夹和文件名,文件名为当前毫秒数
*/
private fun generateFilePath(activity: Activity, fileType: Int = 0): String {
val file = when (fileType) {
// 音频路径
1 -> "${SystemConstant.SoundFolder}" + File.separator + System.currentTimeMillis().toString() + ".m4a"
// 视频路径
2 -> "${SystemConstant.CameraFolder}" + File.separator + System.currentTimeMillis().toString() + ".mp4"
// 图片路径
else -> "${SystemConstant.CameraFolder}" + File.separator + System.currentTimeMillis().toString() + ".jpg"
}
File(file).parentFile.mkdirs()
return file
}
/**
* 获取SD下的应用目录
*/
private fun getExternalStoragePath(activity: Activity): String {
val sb = "${activity.getExternalFilesDir(null)}/tmp"
return sb
}
/**
* 安卓7.0裁剪根据文件路径获取uri
*/
private fun getImageContentUri(context: Context, imageFile: File?): Uri? {
val filePath = imageFile?.absolutePath
val cursor = context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
arrayOf(MediaStore.Images.Media._ID),
MediaStore.Images.Media.DATA + "=? ",
arrayOf(filePath), null)
cursor.use { _ ->
return if (cursor != null && cursor.moveToFirst()) {
val id = cursor.getInt(cursor
.getColumnIndex(MediaStore.MediaColumns._ID))
val baseUri = Uri.parse("content://media/external/images/media")
Uri.withAppendedPath(baseUri, "" + id)
} else {
imageFile?.let {
if (it.exists()) {
val values = ContentValues()
values.put(MediaStore.Images.Media.DATA, filePath)
context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
} else {
null
}
}
}
}
}
/**
* 用于获取图片的Fragment
*/
class PhotoFragment : Fragment() {
/**
* Fragment处理照片后返回接口
*/
private var callback: ((requestCode: Int, intent: Intent?) -> Unit)? = null
/**
* 开启系统相册
* 裁剪图片、打开相册选择单张图片、拍照
*/
fun start(intent: Intent, requestCode: Int, callback: ((requestCode: Int, intent: Intent?) -> Unit)) {
this.callback = callback
startActivityForResult(intent, requestCode)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
callback?.invoke(requestCode, data)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
companion object {
const val REQ_TAKE_FILE = 10001
const val REQ_SELECT_FILE = 10002
const val REQ_ZOOM_PHOTO = 10003
private const val TAG = "EasyPhoto:PhotoFragment"
@JvmStatic
fun findOrCreate(activity: Activity): PhotoFragment {
var fragment: PhotoFragment? = activity.fragmentManager.findFragmentByTag(TAG) as PhotoFragment?
if (fragment == null) {
fragment = PhotoFragment()
activity.fragmentManager.beginTransaction()
.add(fragment, TAG)
.commitAllowingStateLoss()
activity.fragmentManager.executePendingTransactions()
}
return fragment
}
}
}
}

View File

@@ -0,0 +1,10 @@
package com.navinfo.volvo.utils
class SystemConstant {
companion object {
lateinit var ROOT_PATH: String
lateinit var CameraFolder: String
lateinit var SoundFolder: String
lateinit var LogFolder: String
}
}

View File

@@ -0,0 +1,6 @@
<vector android:height="64dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0"/>
<path android:fillColor="@android:color/white" android:pathData="M9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2L9,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM5,9v6h4l5,5V4L9,9H5z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7,9v6h4l5,5V4l-5,5H7z"/>
</vector>

View File

@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#000000" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
</vector>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.message.MessageActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.NavinfoVolvo.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:navigationIcon="@drawable/ic_back_file_picker"
app:popupTheme="@style/Theme.NavinfoVolvo.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_message" />
<!-- <com.google.android.material.floatingactionbutton.FloatingActionButton-->
<!-- android:id="@+id/fab"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_gravity="bottom|end"-->
<!-- android:layout_marginEnd="@dimen/fab_margin"-->
<!-- android:layout_marginBottom="16dp"-->
<!-- app:srcCompat="@android:drawable/ic_dialog_email" />-->
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -9,7 +9,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:layout_marginLeft="8dp"
android:src="@mipmap/ic_launcher" android:src="@mipmap/volvo_logo_small"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<fragment
android:id="@+id/nav_host_fragment_message"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.otaliastudios.cameraview.CameraView
android:id="@+id/camera"
android:keepScreenOn="true"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/img_start_camera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
app:shapeAppearance="@style/CircleStyle"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:src="@drawable/ic_baseline_camera_alt_24"></com.google.android.material.imageview.ShapeableImageView>
</RelativeLayout>

View File

@@ -1,11 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="com.navinfo.volvo.ui.fragments.dashboard.DashboardFragment"> tools:context="com.navinfo.volvo.ui.dashboard.CameraFragment">
<TextView <TextView
android:id="@+id/text_dashboard" android:id="@+id/text_dashboard"

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".FirstFragment">
<TextView
android:id="@+id/textview_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hello_first_fragment"
app:layout_constraintBottom_toTopOf="@id/button_first"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_first"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_first" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,20 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="@dimen/activity_default_padding" android:padding="@dimen/activity_default_padding"
tools:context=".ui.fragments.message.ObtainMessageFragment"> tools:context=".ui.message.ObtainMessageFragment">
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:overScrollMode="never"
android:scrollbars="none"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent"
android:overScrollMode="never"
android:scrollbars="none">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@@ -25,10 +24,10 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="@dimen/default_widget_padding" android:layout_margin="@dimen/default_widget_padding"
android:hint="问候信息"
app:counterEnabled="true" app:counterEnabled="true"
app:counterMaxLength="10" app:counterMaxLength="10"
app:errorEnabled="true" app:errorEnabled="true"
android:hint="请输入问候信息"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
@@ -41,13 +40,19 @@
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" /> tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.nhaarman.supertooltips.ToolTipRelativeLayout
android:id="@+id/tt_title"
app:layout_constraintTop_toBottomOf="@id/ti_layout_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/label_message_subtitle" android:id="@+id/label_message_subtitle"
style="@style/TextAppearance.AppCompat.Subhead" style="@style/TextAppearance.AppCompat.Subhead"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="问候信息" android:text="问候附件"
app:layout_constraintTop_toBottomOf="@id/ti_layout_title"></com.google.android.material.textview.MaterialTextView> app:layout_constraintTop_toBottomOf="@id/tt_title"></com.google.android.material.textview.MaterialTextView>
<com.google.android.material.divider.MaterialDivider <com.google.android.material.divider.MaterialDivider
android:id="@+id/div_message" android:id="@+id/div_message"
@@ -78,30 +83,73 @@
android:textColor="@color/red"></TextView> android:textColor="@color/red"></TextView>
<TextView <TextView
android:id="@+id/tv_upload_pic"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="上传图片:"></TextView> android:text="上传图片:"></TextView>
<LinearLayout
<com.google.android.material.button.MaterialButton android:id="@+id/layer_get_photo"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/default_widget_padding" android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start_camera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.ElevatedButton"
app:icon="@drawable/ic_baseline_camera_24"
android:text="点击拍照" android:text="点击拍照"
app:icon="@drawable/ic_baseline_camera_24"></com.google.android.material.button.MaterialButton> android:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
<Space <Space
android:layout_width="@dimen/default_widget_padding" android:layout_width="@dimen/default_widget_padding"
android:layout_height="wrap_content"></Space> android:layout_height="wrap_content"></Space>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
style="@style/Widget.Material3.Button.ElevatedButton" android:id="@+id/btn_start_photo"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/default_widget_padding" style="@style/Widget.Material3.Button.ElevatedButton"
app:icon="@drawable/ic_baseline_image_search_24"
android:text="相册选择" android:text="相册选择"
app:icon="@drawable/ic_baseline_image_search_24"></com.google.android.material.button.MaterialButton> android:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/layer_photo_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone"
android:orientation="horizontal">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tv_photo_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""></com.google.android.material.textview.MaterialTextView>
<Space
android:layout_width="@dimen/default_widget_padding"
android:layout_height="wrap_content"></Space>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/img_photo_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:src="@drawable/ic_baseline_delete_24"></com.google.android.material.imageview.ShapeableImageView>
</LinearLayout>
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/img_message_attachment"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="@dimen/activity_horizontal_margin"
android:scaleType="fitCenter">
</androidx.appcompat.widget.AppCompatImageView>
<com.nhaarman.supertooltips.ToolTipRelativeLayout
android:id="@+id/tt_pic"
app:layout_constraintTop_toBottomOf="@id/ti_layout_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout <LinearLayout
style="@style/default_line" style="@style/default_line"
@@ -120,27 +168,69 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="上传音频:"></TextView> android:text="上传音频:"></TextView>
<LinearLayout
<com.google.android.material.button.MaterialButton android:id="@+id/layer_get_audio"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/default_widget_padding" android:gravity="center"
android:text="长按录音" android:orientation="horizontal">
app:icon="@drawable/ic_baseline_fiber_manual_record_24"></com.google.android.material.button.MaterialButton> <com.google.android.material.button.MaterialButton
android:id="@+id/btn_start_record"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.ElevatedButton"
app:icon="@drawable/ic_baseline_fiber_manual_record_24"
android:text="录制音频"
android:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
<Space <Space
android:layout_width="@dimen/default_widget_padding" android:layout_width="@dimen/default_widget_padding"
android:layout_height="wrap_content"></Space> android:layout_height="wrap_content"></Space>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
style="@style/Widget.Material3.Button.ElevatedButton" android:id="@+id/btn_select_sound"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/default_widget_padding" style="@style/Widget.Material3.Button.ElevatedButton"
app:icon="@drawable/ic_baseline_audio_file_24"
android:text="音频选择" android:text="音频选择"
app:icon="@drawable/ic_baseline_audio_file_24"></com.google.android.material.button.MaterialButton> android:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/layer_audio_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:visibility="gone"
android:orientation="horizontal">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/tv_audio_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""></com.google.android.material.textview.MaterialTextView>
<Space
android:layout_width="@dimen/default_widget_padding"
android:layout_height="wrap_content"></Space>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/img_audio_delete"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:src="@drawable/ic_baseline_delete_24"></com.google.android.material.imageview.ShapeableImageView>
</LinearLayout>
</LinearLayout>
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/img_sound_play"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_volume_up_24"
android:visibility="gone"
android:layout_gravity="center"></com.google.android.material.imageview.ShapeableImageView>
<com.nhaarman.supertooltips.ToolTipRelativeLayout
android:id="@+id/tt_audio"
app:layout_constraintTop_toBottomOf="@id/ti_layout_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
@@ -162,12 +252,8 @@
android:id="@+id/layer_send_info" android:id="@+id/layer_send_info"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:background="@drawable/shape_radius5_white"
android:divider="@drawable/shape_divider_linear"
android:orientation="vertical" android:orientation="vertical"
android:padding="@dimen/default_widget_padding" android:padding="@dimen/default_widget_padding"
android:showDividers="middle"
app:layout_constraintTop_toBottomOf="@id/div_send_info"> app:layout_constraintTop_toBottomOf="@id/div_send_info">
<LinearLayout <LinearLayout
@@ -192,21 +278,44 @@
android:id="@+id/edt_send_from" android:id="@+id/edt_send_from"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/selector_bg_4_round_corner" android:background="@drawable/selector_bg_4_round_corner"></androidx.appcompat.widget.AppCompatEditText>
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck"></androidx.appcompat.widget.AppCompatEditText>
</LinearLayout> </LinearLayout>
<com.nhaarman.supertooltips.ToolTipRelativeLayout
android:id="@+id/tt_send_from"
app:layout_constraintTop_toBottomOf="@id/ti_layout_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView <LinearLayout
android:id="@+id/edt_send_to" style="@style/default_line"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_baseline_star_8" android:gravity="center_vertical"
android:drawableEnd="@drawable/ic_baseline_navigate_next_24" android:orientation="horizontal">
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="发给谁:" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="*"
android:textColor="@color/red"></TextView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="发给谁:"></TextView>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/edt_send_to"
android:layout_width="match_parent"
android:layout_height="wrap_content"></androidx.appcompat.widget.AppCompatSpinner>
</LinearLayout>
<com.nhaarman.supertooltips.ToolTipRelativeLayout
android:id="@+id/tt_send_to"
app:layout_constraintTop_toBottomOf="@id/ti_layout_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout <LinearLayout
style="@style/default_line" style="@style/default_line"
@@ -228,19 +337,22 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_send_time" android:id="@+id/btn_send_time"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/default_widget_padding" style="@style/Widget.Material3.Button.ElevatedButton"
app:icon="@drawable/ic_baseline_access_time_24"
android:text="现在" android:text="现在"
app:icon="@drawable/ic_baseline_access_time_24"></com.google.android.material.button.MaterialButton> android:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
</LinearLayout> </LinearLayout>
<com.nhaarman.supertooltips.ToolTipRelativeLayout
android:id="@+id/tt_send_time"
app:layout_constraintTop_toBottomOf="@id/ti_layout_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout> </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView> </ScrollView>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -248,6 +360,7 @@
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_obtain_message_back"
style="@style/Widget.Material3.Button.ElevatedButton" style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -259,6 +372,7 @@
android:layout_height="wrap_content"></Space> android:layout_height="wrap_content"></Space>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btn_obtain_message_confirm"
style="@style/Widget.Material3.Button" style="@style/Widget.Material3.Button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SecondFragment">
<TextView
android:id="@+id/textview_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/button_second"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button_second"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/previous"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textview_second" />
</androidx.constraintlayout.widget.ConstraintLayout>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/navigation_obtain_message">
<fragment
android:id="@+id/navigation_obtain_message"
android:name="com.navinfo.volvo.ui.message.ObtainMessageFragment"
android:label="问候编辑"
tools:layout="@layout/fragment_obtain_message" >
<action
android:id="@+id/nav_2_camera"
app:destination="@id/navigation_camera"></action>
</fragment>
<fragment
android:id="@+id/navigation_camera"
android:name="com.navinfo.volvo.ui.camera.CameraFragment"
android:label="拍照"
tools:layout="@layout/fragment_camera" />
</navigation>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.NavinfoVolvo" parent="Theme.Material3.DynamicColors.DayNight"> <style name="Theme.NavinfoVolvo" parent="Theme.Material3.Dark">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item> <item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorPrimaryVariant">@color/purple_700</item>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">200dp</dimen>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">48dp</dimen>
</resources>

View File

@@ -5,4 +5,5 @@
<dimen name="activity_default_padding">12dp</dimen> <dimen name="activity_default_padding">12dp</dimen>
<dimen name="default_font_size">18sp</dimen> <dimen name="default_font_size">18sp</dimen>
<dimen name="default_widget_padding">6dp</dimen> <dimen name="default_widget_padding">6dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources> </resources>

View File

@@ -6,4 +6,13 @@
<string name="delete">Del</string> <string name="delete">Del</string>
<string name="share">Share</string> <string name="share">Share</string>
<string name="my">My</string> <string name="my">My</string>
<string name="title_activity_second">SecondActivity</string>
<!-- Strings used for fragments for navigation -->
<string name="first_fragment_label">First Fragment</string>
<string name="second_fragment_label">Second Fragment</string>
<string name="next">Next</string>
<string name="previous">Previous</string>
<string name="hello_first_fragment">Hello first fragment</string>
<string name="hello_second_fragment">Hello second fragment. Arg: %1$s</string>
</resources> </resources>

View File

@@ -3,4 +3,10 @@
<style name="default_line"> <style name="default_line">
<item name="android:padding">@dimen/default_widget_padding</item> <item name="android:padding">@dimen/default_widget_padding</item>
</style> </style>
<!--ShapeableImageView 圆 -->
<style name="CircleStyle">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
</style>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.NavinfoVolvo" parent="Theme.Material3.DynamicColors.Light"> <style name="Theme.NavinfoVolvo" parent="Theme.Material3.Light">
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item> <item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorPrimaryVariant">@color/purple_700</item>
@@ -13,4 +13,13 @@
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item> <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. --> <!-- Customize your theme here. -->
</style> </style>
<style name="Theme.NavinfoVolvo.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.NavinfoVolvo.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="Theme.NavinfoVolvo.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources> </resources>