fix: 完善消息详情界面

This commit is contained in:
xiaoyan 2023-01-03 16:07:11 +08:00
commit cf712a6b86
37 changed files with 1682 additions and 329 deletions

View File

@ -83,14 +83,21 @@ dependencies {
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.yanzhenjie.recyclerview:x:1.3.2'
// https://github.com/permissions-dispatcher/PermissionsDispatcher
implementation "com.github.permissions-dispatcher:permissionsdispatcher:4.9.2"
annotationProcessor "com.github.permissions-dispatcher:permissionsdispatcher-processor:4.9.2"
// 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/l123456789jy/Lazy
implementation 'com.github.lazylibrary:lazylibrary:1.0.2'
// 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"
}
// https://github.com/nhaarman/supertooltips
implementation 'com.nhaarman.supertooltips:library:3.0.0'
}

View File

@ -1,13 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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.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" />
@ -18,11 +21,21 @@
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.NavinfoVolvo"
tools:targetApi="31">
<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
android:name="com.navinfo.volvo.MainActivity"
android:exported="true"
@ -32,11 +45,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<meta-data
android:name="android.app.lib_name"
android:value="" />
<meta-data android:name="ScopedStorage" android:value="true" />
</application>
</manifest>

View File

@ -1,24 +1,42 @@
package com.navinfo.volvo
import android.Manifest
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import com.google.android.material.bottomnavigation.BottomNavigationView
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.easytools.tools.FileUtils
import com.elvishew.xlog.LogConfiguration
import com.elvishew.xlog.LogLevel
import com.elvishew.xlog.XLog
import com.elvishew.xlog.interceptor.BlacklistTagsFilterInterceptor
import com.elvishew.xlog.printer.AndroidPrinter
import com.elvishew.xlog.printer.ConsolePrinter
import com.elvishew.xlog.printer.Printer
import com.elvishew.xlog.printer.file.FilePrinter
import com.elvishew.xlog.printer.file.backup.NeverBackupStrategy
import com.elvishew.xlog.printer.file.naming.DateFileNameGenerator
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import com.navinfo.volvo.databinding.ActivityMainBinding
import permissions.dispatcher.*
import com.navinfo.volvo.ui.message.MessageActivity
import com.navinfo.volvo.utils.SystemConstant
//@RuntimePermissions
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
@NeedsPermission(Manifest.permission.CAMERA)
override fun onCreate(savedInstanceState: Bundle?) {
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
@ -34,12 +52,101 @@ class MainActivity : AppCompatActivity() {
R.id.navigation_home, R.id.navigation_dashboard, R.id.navigation_notifications, R.id.navigation_obtain_message
)
)
// setupActionBarWithNavController(navController, appBarConfiguration)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
findViewById<View>(R.id.fab_new_message).apply {
this.setOnClickListener {
// 跳转到Message的Fragment
val messageIntent:Intent = Intent(this@MainActivity, MessageActivity::class.java)
startActivity(messageIntent)
// findNavController(R.id.layer_main_child_fragment).navigate(R.id.navigation_obtain_message)
}
}
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)
}
}
})
}
@OnShowRationale(Manifest.permission.CAMERA)
fun showRationaleForCamera(request: PermissionRequest) {
// @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)
@ -47,19 +154,13 @@ class MainActivity : AppCompatActivity() {
.setMessage("当前操作需要您授权读写SD卡权限")
.setPositiveButton("确定", DialogInterface.OnClickListener { dialogInterface, i ->
dialogInterface.dismiss()
// 在SD卡创建项目目录
XXPermissions.startPermissionActivity(this@MainActivity, permissions)
})
.show()
}
@OnPermissionDenied(Manifest.permission.CAMERA)
fun onCameraDenied() {
// @OnPermissionDenied(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
fun onSDCardDenied() {
Toast.makeText(this, "当前操作需要您授权读写SD卡权限", Toast.LENGTH_SHORT).show()
}
@OnNeverAskAgain(Manifest.permission.CAMERA)
fun onCameraNeverAskAgain() {
Toast.makeText(this, "您已永久拒绝授权读写SD卡权限", Toast.LENGTH_SHORT).show()
}
}

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

@ -7,9 +7,17 @@ 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
@ -24,6 +32,9 @@ class CameraFragment : Fragment() {
// 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()
@ -34,7 +45,7 @@ class CameraFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
lifecycle.addObserver(cameraLifeCycleObserver)
// lifecycle.addObserver(cameraLifeCycleObserver)
val cameraViewModel =
ViewModelProvider(this).get(CameraViewModel::class.java)
@ -47,41 +58,55 @@ class CameraFragment : Fragment() {
cameraView.addCameraListener(object:CameraListener() { // 添加拍照回调
override fun onPictureTaken(result: PictureResult) {
super.onPictureTaken(result)
result.toFile()
// 压缩图片文件
Luban.with(context)
.load<Any>(photos)
.ignoreBy(100)
.setTargetDir(getPath())
.filter { path ->
!(TextUtils.isEmpty(path) || path.lowercase(Locale.getDefault())
.endsWith(".gif"))
// 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()
}
.setCompressListener(object : OnCompressListener {
override fun onStart() {
// TODO 压缩开始前调用,可以在方法内启动 loading UI
}
override fun onSuccess(file: File?) {
// TODO 压缩成功后调用,返回压缩后的图片文件
}
override fun onError(e: Throwable) {
// TODO 当压缩过程出现问题时调用
}
}).launch()
})
}
})
// 点击拍照
binding.imgStartCamera.setOnClickListener {
cameraView.takePicture()
}
return root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
lifecycle.removeObserver(cameraLifeCycleObserver)
// lifecycle.removeObserver(cameraLifeCycleObserver)
}
}

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

@ -1,27 +1,56 @@
package com.navinfo.volvo.ui.message
import android.Manifest
import android.content.DialogInterface
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.View.*
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.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
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.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.db.dao.entity.AttachmentType
import com.navinfo.volvo.db.dao.entity.Message
import com.navinfo.volvo.ui.markRequiredInRed
import permissions.dispatcher.*
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() {
private var _binding: FragmentObtainMessageBinding? = null
private val obtainMessageViewModel by lazy {
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
// onDestroyView.
@ -35,16 +64,43 @@ class ObtainMessageFragment: Fragment() {
_binding = FragmentObtainMessageBinding.inflate(inflater, container, false)
val root: View = binding.root
obtainMessageViewModel.setCurrentMessage(Message())
obtainMessageViewModel?.getMessageLiveData()?.observe(
viewLifecycleOwner, Observer {
// 初始化界面显示内容
if(it.title!=null)
if(it.title?.isNotEmpty() == true)
binding.tvMessageTitle?.setText(it.title)
if (it.sendDate!=null) {
binding.btnSendTime.setText(it.sendDate)
if (it.sendDate?.isNotEmpty() == true) {
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
}
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()
return root
}
@ -52,12 +108,42 @@ class ObtainMessageFragment: Fragment() {
fun initView() {
// 设置问候信息提示的红色星号
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 {
val dialog = DateTimePickerFragment.newInstance().mode(0)
dialog.listener = object : DateTimePickerFragment.OnClickListener {
override fun onClickListener(selectTime: String) {
obtainMessageViewModel.updateMessageSendTime(selectTime)
val sendDate = DateUtils.str2Date(selectTime, "yyyy-MM-dd HH:mm")
if (sendDate <= Date()) {
obtainMessageViewModel.updateMessageSendTime("现在")
} else {
obtainMessageViewModel.updateMessageSendTime(selectTime)
}
}
}
@ -67,7 +153,195 @@ class ObtainMessageFragment: Fragment() {
// 点击按钮选择拍照
binding.btnStartCamera.setOnClickListener {
// 启动相机
startCamera()
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)
}
}
}
@ -76,32 +350,44 @@ class ObtainMessageFragment: Fragment() {
_binding = null
}
@NeedsPermission(Manifest.permission.CAMERA)
fun startCamera() {
fun startCamera(it: View) {
Navigation.findNavController(binding.root).navigate(com.navinfo.volvo.R.id.nav_2_camera)
}
@OnShowRationale(Manifest.permission.CAMERA)
fun showRationaleForCamera(request: PermissionRequest) {
fun showRationaleForCamera(permissions: MutableList<String>) {
// showRationaleDialog(R.string.permission_camera_rationale, request)
// Toast.makeText(context, "当前操作需要您授权相机权限!", Toast.LENGTH_SHORT).show()
MaterialAlertDialogBuilder(context!!)
.setTitle("提示")
.setMessage("当前操作需要您授权相机权限!")
.setMessage("当前操作需要您授权拍摄权限!")
.setPositiveButton("确定", DialogInterface.OnClickListener { dialogInterface, i ->
startCamera()
dialogInterface.dismiss()
})
XXPermissions.startPermissionActivity(activity!!, permissions)
})
.show()
}
@OnPermissionDenied(Manifest.permission.CAMERA)
// @OnPermissionDenied(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
fun onCameraDenied() {
Toast.makeText(context, "当前操作需要您授权相机权限!", Toast.LENGTH_SHORT).show()
ToastUtils.showToast("当前操作需要您授权拍摄权限!")
}
@OnNeverAskAgain(Manifest.permission.CAMERA)
fun onCameraNeverAskAgain() {
Toast.makeText(context, "您已永久拒绝授权相机权限!", Toast.LENGTH_SHORT).show()
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

@ -4,8 +4,10 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import com.navinfo.volvo.db.dao.entity.Attachment
import com.navinfo.volvo.db.dao.entity.Message
import com.navinfo.volvo.db.dao.entity.AttachmentType
import java.util.UUID
class ObtainMessageViewModel: ViewModel() {
private val msgLiveData: MutableLiveData<Message> by lazy {
@ -27,22 +29,41 @@ class ObtainMessageViewModel: ViewModel() {
}
// 更新消息附件中的照片文件
fun updateMessagePic(picUrl: String) {
fun updateMessagePic(picUrl: String?) {
var hasPic = false
for (attachment in this.msgLiveData.value!!.attachment) {
if (attachment.attachmentType == AttachmentType.PIC) {
attachment.pathUrl = picUrl
if (picUrl==null||picUrl.isEmpty()) {
this.msgLiveData.value!!.attachment.remove(attachment)
} else {
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)
}
// 更新消息附件中的录音文件
fun updateMessageAudio(audioUrl: String) {
fun updateMessageAudio(audioUrl: String?) {
var hasAudio = false
for (attachment in this.msgLiveData.value!!.attachment) {
if (attachment.attachmentType == AttachmentType.AUDIO) {
attachment.pathUrl = audioUrl
if (audioUrl==null||audioUrl.isEmpty()) {
this.msgLiveData.value!!.attachment.remove(attachment)
} else {
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)
}

View File

@ -1,187 +0,0 @@
package com.navinfo.volvo.ui.widget
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.*
import android.widget.Scroller
import androidx.core.view.forEach
import androidx.recyclerview.widget.RecyclerView
import java.lang.Math.abs
class SlideRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr) {
//系统最小移动距离
private val mTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
//最小有效速度
private val mMinVelocity = 600
//增加手势控制,双击快速完成侧滑
private var isDoubleClick = false
private var mGestureDetector: GestureDetector =
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent?): Boolean {
e?.let { event ->
getSelectItem(event)
mItem?.let {
val deleteWith = it.getChildAt(it.childCount - 1).width
//触发移动至完全展开deleteWidth
if (it.scrollX == 0) {
mScroller.startScroll(0, 0, deleteWith, 0)
} else {
mScroller.startScroll(it.scrollX, 0, -it.scrollX, 0)
}
isDoubleClick = true
invalidate()
return true
}
}
//不进行拦截,只作为工具判断下双击
return false
}
})
//使用速度控制器,增加侧滑速度判定滑动成功,
//VelocityTracker 由native实现需要及时释放内存
private var mVelocityTracker: VelocityTracker? = null
//流畅滑动
private var mScroller = Scroller(context)
//当前选中item
private var mItem: ViewGroup? = null
//上次按下的横坐标
private var mLastX = 0f
//当前RecyclerView被上层ViewGroup分发到事件所有事件都会通过dispatchTouchEvent给到
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
mGestureDetector.onTouchEvent(ev)
return super.dispatchTouchEvent(ev)
}
//viewGroup对子控件的事件拦截一旦拦截后续事件序列不会再调用onInterceptTouchEvent
override fun onInterceptTouchEvent(e: MotionEvent?): Boolean {
e?.let {
when (e.action) {
MotionEvent.ACTION_DOWN -> {
getSelectItem(e)
mLastX = e.x
}
MotionEvent.ACTION_MOVE -> {
//移动控件
return moveItem(e)
}
// MotionEvent.ACTION_UP -> {
// stopMove(e)
// }
}
}
return super.onInterceptTouchEvent(e)
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(e: MotionEvent?): Boolean {
e?.let {
when (e.action) {
MotionEvent.ACTION_MOVE -> {
moveItem(e)
mLastX = e.x
}
MotionEvent.ACTION_UP -> {
stopMove()
}
}
}
return super.onTouchEvent(e)
}
//活动结束
//判断一下结束的位置,补充或恢复位置
private fun stopMove() {
mItem?.let {
//如果移动过半,判定左划成功
val deleteWidth = it.getChildAt(it.childCount - 1).width
//如果整个移动过程速度大于600也判定滑动成功
//注意如果没有拦截ACTION_MOVE,mVelocityTracker是没有初始化的
var velocity = 0f
mVelocityTracker?.let { tracker ->
tracker.computeCurrentVelocity(1000)
velocity = tracker.xVelocity
}
//判断结束情况,移动过半或者向左速度很快都展开
if ((abs(it.scrollX) >= deleteWidth / 2f) || (velocity < -mMinVelocity)) {
//触发移动至完全展开
mScroller.startScroll(it.scrollX, 0, deleteWidth - it.scrollX, 0)
invalidate()
} else {
//如果移动未过半应恢复状态
mScroller.startScroll(it.scrollX, 0, -it.scrollX, 0)
invalidate()
}
}
//清除状态
mLastX = 0f
//mVeloctityTracker由native实现需要及时释放
mVelocityTracker?.apply {
clear()
recycle()
}
mVelocityTracker = null
}
//移动Item
//绝对值小于删除按钮长度随便移动,大于则不移动
@SuppressLint("Recycle")
private fun moveItem(e: MotionEvent): Boolean {
mItem?.let {
val dx = mLastX - e.x
//最小的移动距离应该舍弃onInterceptTouchEvent不拦截onTouchEvent内才更新mLastX
// if (abs(dx) > mTouchSlop) {
//检查mItem移动后应该在【-deleteLength0】内
val deleteWith = it.getChildAt(it.childCount - 1).width
if ((it.scrollX + dx) <= deleteWith && (it.scrollX + dx) >= 0) {
//触发移动
it.scrollBy(dx.toInt(), 0)
//触发速度计算
//这里Rectycle不存在问题一旦返回true就会拦截事件就会到达ACTION_UP去回收
mVelocityTracker = mVelocityTracker ?: VelocityTracker.obtain()
mVelocityTracker!!.addMovement(e)
return true
// }
}
}
return false
}
//获取点击位置
//通过点击的y坐标除以Item高度得出
private fun getSelectItem(e: MotionEvent) {
val frame = Rect()
mItem = null
forEach {
if (it.visibility != GONE) {
it.getHitRect(frame)
if (frame.contains(e.x.toInt(), e.y.toInt())) {
mItem = it as ViewGroup
}
}
}
}
//流畅地滑动
override fun computeScroll() {
if (mScroller.computeScrollOffset()) {
mItem?.scrollBy(mScroller.currX, mScroller.currY)
postInvalidate()
}
}
}

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,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="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</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: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="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</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

@ -12,6 +12,7 @@
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:labelVisibilityMode="labeled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@ -29,4 +30,26 @@
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_new_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:src="@drawable/ic_add_24dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:elevation="8dp"
/>
<FrameLayout
android:id="@+id/layer_main_child_fragment"
android:layout_width="0dp"
android:layout_height="0dp"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="@navigation/nav_graph"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"></FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

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_height="wrap_content"
android:layout_marginLeft="8dp"
android:src="@mipmap/ic_launcher"
android:src="@mipmap/volvo_logo_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="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

@ -6,8 +6,8 @@
<com.otaliastudios.cameraview.CameraView
android:id="@+id/camera"
android:keepScreenOn="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/img_start_camera"

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

@ -27,7 +27,7 @@
app:counterEnabled="true"
app:counterMaxLength="10"
app:errorEnabled="true"
android:hint="问候信息"
android:hint="请输入问候信息"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -40,13 +40,19 @@
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</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
android:id="@+id/label_message_subtitle"
style="@style/TextAppearance.AppCompat.Subhead"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="问候信息"
app:layout_constraintTop_toBottomOf="@id/ti_layout_title"></com.google.android.material.textview.MaterialTextView>
android:text="问候附件"
app:layout_constraintTop_toBottomOf="@id/tt_title"></com.google.android.material.textview.MaterialTextView>
<com.google.android.material.divider.MaterialDivider
android:id="@+id/div_message"
@ -77,31 +83,74 @@
android:textColor="@color/red"></TextView>
<TextView
android:id="@+id/tv_upload_pic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="上传图片:"></TextView>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start_camera"
<LinearLayout
android:id="@+id/layer_get_photo"
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:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
<Space
android:layout_width="@dimen/default_widget_padding"
android:layout_height="wrap_content"></Space>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start_photo"
android:layout_width="wrap_content"
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:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
<Space
android:layout_width="@dimen/default_widget_padding"
android:layout_height="wrap_content"></Space>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.ElevatedButton"
app:icon="@drawable/ic_baseline_image_search_24"
android:text="相册选择"
android:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
</LinearLayout>
<LinearLayout
android:id="@+id/layer_photo_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.ElevatedButton"
app:icon="@drawable/ic_baseline_image_search_24"
android:text="相册选择"
android:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
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
style="@style/default_line"
android:layout_width="match_parent"
@ -119,25 +168,69 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="上传音频:"></TextView>
<LinearLayout
android:id="@+id/layer_get_audio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<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
android:layout_width="@dimen/default_widget_padding"
android:layout_height="wrap_content"></Space>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_select_sound"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.ElevatedButton"
app:icon="@drawable/ic_baseline_audio_file_24"
android:text="音频选择"
android:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/layer_audio_result"
android:layout_width="match_parent"
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
android:layout_width="@dimen/default_widget_padding"
android:layout_height="wrap_content"></Space>
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.ElevatedButton"
app:icon="@drawable/ic_baseline_audio_file_24"
android:text="音频选择"
android:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
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>
<com.google.android.material.textview.MaterialTextView
@ -188,6 +281,12 @@
android:background="@drawable/selector_bg_4_round_corner"></androidx.appcompat.widget.AppCompatEditText>
</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" />
<LinearLayout
style="@style/default_line"
android:layout_width="match_parent"
@ -206,13 +305,18 @@
android:layout_height="wrap_content"
android:text="发给谁:"></TextView>
<androidx.appcompat.widget.AppCompatEditText
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/edt_send_to"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector_bg_4_round_corner"></androidx.appcompat.widget.AppCompatEditText>
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
style="@style/default_line"
android:layout_width="match_parent"
@ -240,7 +344,11 @@
android:text="现在"
android:padding="@dimen/default_widget_padding"></com.google.android.material.button.MaterialButton>
</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>
</androidx.constraintlayout.widget.ConstraintLayout>
@ -252,6 +360,7 @@
app:layout_constraintBottom_toBottomOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_obtain_message_back"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -263,6 +372,7 @@
android:layout_height="wrap_content"></Space>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_obtain_message_confirm"
style="@style/Widget.Material3.Button"
android:layout_width="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>

View File

@ -5,16 +5,22 @@
android:id="@+id/navigation_home"
android:icon="@drawable/ic_home_black_24dp"
android:title="@string/title_home" />
<item
android:id="@+id/navigation_dashboard"
android:icon="@drawable/ic_dashboard_black_24dp"
android:title="@string/title_dashboard" />
<item
android:id="@+id/navigation_none"
android:icon="@color/black"
android:title=" " />
<item
android:id="@+id/navigation_notifications"
android:icon="@drawable/ic_notifications_black_24dp"
android:title="@string/title_notifications" />
<!-- <item-->
<!-- android:id="@+id/navigation_my"-->
<!-- android:icon="@drawable/ic_baseline_person_24"-->
<!-- android:title="@string/my" />-->
<item
android:id="@+id/navigation_obtain_message"

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@ -22,24 +22,16 @@
android:label="@string/title_dashboard"
tools:layout="@layout/fragment_dashboard" />
<fragment
android:id="@+id/fab_new_message"
android:name="com.navinfo.volvo.ui.dashboard.DashboardFragment"
android:label="@string/title_dashboard"
tools:layout="@layout/fragment_dashboard" />
<fragment
android:id="@+id/navigation_notifications"
android:name="com.navinfo.volvo.ui.notifications.NotificationsFragment"
android:label="@string/title_notifications"
tools:layout="@layout/fragment_notifications" />
<fragment
android:id="@+id/navigation_obtain_message"
android:name="com.navinfo.volvo.ui.message.ObtainMessageFragment"
android:label="问候编辑"
tools:layout="@layout/fragment_obtain_message" />
<fragment
android:id="@+id/navigation_camera"
android:name="com.navinfo.volvo.ui.camera.CameraFragment"
android:label="@string/title_dashboard"
tools:layout="@layout/fragment_camera" />
<action android:id="@+id/nav_2_camera"
app:destination="@id/nav_2_camera"></action>
</navigation>

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">
<!-- Base application theme. -->
<style name="Theme.NavinfoVolvo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.NavinfoVolvo" parent="Theme.Material3.Dark">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</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 @@
<string name="title_notifications">Notifications</string>
<string name="delete">删除</string>
<string name="share">分享</string>
<string name="my">我的</string>
</resources>

View File

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

View File

@ -5,4 +5,14 @@
<string name="title_notifications">Notifications</string>
<string name="delete">Del</string>
<string name="share">Share</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>

View File

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.NavinfoVolvo" parent="Theme.Material3.DayNight.NoActionBar">
<style name="Theme.NavinfoVolvo" parent="Theme.Material3.Light">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
@ -13,4 +13,13 @@
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</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>