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
commit 9492580b84
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"
// 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 {

View File

@ -1,6 +1,19 @@
<?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.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".MyApplication"
@ -9,11 +22,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">
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
android:name="com.navinfo.volvo.ui.MainActivity"
android:exported="true"
@ -23,11 +46,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>
<uses-permission android:name="android.permission.INTERNET"></uses-permission>
</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
import android.content.DialogInterface
import android.content.Intent
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import android.widget.Toast
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.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.navinfo.volvo.R
import com.navinfo.volvo.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
@ -23,6 +28,38 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
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() {
@ -57,4 +94,73 @@ class MainActivity : AppCompatActivity() {
override fun onSupportNavigateUp() =
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
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.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.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 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.
@ -29,29 +65,91 @@ class ObtainMessageFragment: Fragment() {
_binding = FragmentObtainMessageBinding.inflate(inflater, container, false)
val root: View = binding.root
obtainMessageViewModel.getMessageLiveData()?.observe(
obtainMessageViewModel.setCurrentMessage(Message())
obtainMessageViewModel?.getMessageLiveData()?.observe(
viewLifecycleOwner, Observer {
// 初始化界面显示内容
if(it.title!=null)
binding.tvMessageTitle.setText(it.title)
if (it.sendDate!=null) {
if(it.title?.isNotEmpty() == true)
binding.tvMessageTitle?.setText(it.title)
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
// 如果当前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()
return root
}
private fun initView() {
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)
}
}
}
@ -59,8 +157,197 @@ class ObtainMessageFragment: Fragment() {
}
// 点击按钮选择拍照
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()
_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
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.navinfo.volvo.model.Message
import com.navinfo.volvo.model.AttachmentType
import androidx.lifecycle.*
import com.easytools.tools.ToastUtils
import com.elvishew.xlog.XLog
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() {
private val msgLiveData: MutableLiveData<Message> by lazy {
@ -25,22 +35,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)
}
@ -61,4 +90,25 @@ class ObtainMessageViewModel: ViewModel() {
this.msgLiveData.value?.sendDate = sendTime
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_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

@ -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"?>
<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:tools="http://schemas.android.com/tools"
android:layout_width="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
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"?>
<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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/activity_default_padding"
tools:context=".ui.fragments.message.ObtainMessageFragment">
tools:context=".ui.message.ObtainMessageFragment">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:scrollbars="none"
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
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -25,10 +24,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/default_widget_padding"
android:hint="问候信息"
app:counterEnabled="true"
app:counterMaxLength="10"
app:errorEnabled="true"
android:hint="请输入问候信息"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -41,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"
@ -78,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
style="@style/Widget.Material3.Button.ElevatedButton"
<LinearLayout
android:id="@+id/layer_get_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/default_widget_padding"
android:text="点击拍照"
app:icon="@drawable/ic_baseline_camera_24"></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
style="@style/Widget.Material3.Button.ElevatedButton"
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"
android:padding="@dimen/default_widget_padding"
android:text="相册选择"
app:icon="@drawable/ic_baseline_image_search_24"></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"
@ -120,27 +168,69 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="上传音频:"></TextView>
<com.google.android.material.button.MaterialButton
style="@style/Widget.Material3.Button.ElevatedButton"
<LinearLayout
android:id="@+id/layer_get_audio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="@dimen/default_widget_padding"
android:text="长按录音"
app:icon="@drawable/ic_baseline_fiber_manual_record_24"></com.google.android.material.button.MaterialButton>
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>
<Space
android:layout_width="@dimen/default_widget_padding"
android:layout_height="wrap_content"></Space>
<com.google.android.material.button.MaterialButton
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/layer_audio_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/default_widget_padding"
android:text="音频选择"
app:icon="@drawable/ic_baseline_audio_file_24"></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
@ -162,12 +252,8 @@
android:id="@+id/layer_send_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:background="@drawable/shape_radius5_white"
android:divider="@drawable/shape_divider_linear"
android:orientation="vertical"
android:padding="@dimen/default_widget_padding"
android:showDividers="middle"
app:layout_constraintTop_toBottomOf="@id/div_send_info">
<LinearLayout
@ -192,21 +278,44 @@
android:id="@+id/edt_send_from"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/selector_bg_4_round_corner"
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck"></androidx.appcompat.widget.AppCompatEditText>
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" />
<TextView
android:id="@+id/edt_send_to"
<LinearLayout
style="@style/default_line"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_baseline_star_8"
android:drawableEnd="@drawable/ic_baseline_navigate_next_24"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="发给谁:" />
android:gravity="center_vertical"
android:orientation="horizontal">
<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
style="@style/default_line"
@ -228,19 +337,22 @@
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_send_time"
style="@style/Widget.Material3.Button.ElevatedButton"
android:layout_width="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="现在"
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>
<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>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -248,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"
@ -259,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>

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">
<!-- Base application theme. -->
<style name="Theme.NavinfoVolvo" parent="Theme.Material3.DynamicColors.DayNight">
<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 @@
<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

@ -6,4 +6,13 @@
<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

@ -3,4 +3,10 @@
<style name="default_line">
<item name="android:padding">@dimen/default_widget_padding</item>
</style>
<!--ShapeableImageView 圆 -->
<style name="CircleStyle">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">50%</item>
</style>
</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.DynamicColors.Light">
<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>