diff --git a/core/function-impl/mogo-core-function-devatools/src/main/java/com/zhjt/mogo_core_function_devatools/workorder/FaultReasonWindow.kt b/core/function-impl/mogo-core-function-devatools/src/main/java/com/zhjt/mogo_core_function_devatools/workorder/FaultReasonWindow.kt new file mode 100644 index 0000000000..50ee45482a --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools/src/main/java/com/zhjt/mogo_core_function_devatools/workorder/FaultReasonWindow.kt @@ -0,0 +1,348 @@ +package com.zhjt.mogo_core_function_devatools.workorder + + +import android.annotation.SuppressLint +import android.app.Activity +import android.graphics.PixelFormat +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.util.DisplayMetrics +import android.util.Log +import android.view.Gravity +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.View.OnFocusChangeListener +import android.view.WindowManager +import android.view.animation.Animation +import android.view.animation.ScaleAnimation +import android.widget.EditText +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.widget.AppCompatEditText +import androidx.constraintlayout.widget.Group +import androidx.recyclerview.widget.RecyclerView +import com.iflytek.cloud.ErrorCode +import com.iflytek.cloud.InitListener +import com.iflytek.cloud.RecognizerListener +import com.iflytek.cloud.RecognizerResult +import com.iflytek.cloud.SpeechError +import com.iflytek.cloud.SpeechRecognizer +import com.mogo.eagle.core.function.call.devatools.CallerDevaToolsListenerManager +import com.mogo.eagle.core.utilcode.util.BarUtils +import com.mogo.eagle.core.utilcode.util.JsonParser +import com.mogo.eagle.core.utilcode.util.TimeUtils +import com.mogo.eagle.core.utilcode.util.TimeUtils.millis2String +import com.mogo.eagle.core.utilcode.util.ToastUtils +import com.mogo.tts.base.SpeechUtils +import com.zhjt.mogo_core_function_devatools.R +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlin.math.absoluteValue + +/** + * 故障原因编辑窗口 + */ +class FaultReasonWindow constructor(activity: Activity) : View.OnTouchListener{ + + companion object{ + const val TAG = "FaultReasonWindow" + } + + private var mActivity: Activity = activity + private var mWindowParams: WindowManager.LayoutParams? = null + private var mWindowManager: WindowManager? = null + private lateinit var mFloatLayout: View + + private var mInViewX = 0f + private var mInViewY = 0f + private var mInScreenX = 0f + private var mInScreenY = 0f + + // 语音听写对象 + private var mIat: SpeechRecognizer? = null + // 用HashMap存储听写结果 + private val mIatResults: HashMap = LinkedHashMap() + + var ret = 0 // 函数调用返回值 + private var audioStatus = false + + private var workOrderOccurrenceTime = System.currentTimeMillis() //故障发生时间 + + private var clickListener: ClickListener? = null + + private lateinit var tvFaultTitle: TextView + private lateinit var tvFaultTime: TextView + private lateinit var tvFaultType: TextView + private lateinit var ivFaultTypeSelect: ImageView + private lateinit var tvFaultReason: TextView + private lateinit var ivFaultReasonSelect: ImageView + private lateinit var ivTimeReduce: ImageView + private lateinit var tvOccurrenceTime: TextView + private lateinit var ivTimeAdd: ImageView + private lateinit var etNoteInput: AppCompatEditText + private lateinit var ivNoteAudio: ImageView + private lateinit var tvFaultReport: TextView + private lateinit var tvFaultCancel: TextView + private lateinit var rvFaultList: RecyclerView + private lateinit var tvUploadSuccess: TextView + private lateinit var faultReasonGroup: Group + + init { + initFloatWindow() + } + + @SuppressLint("InflateParams") + private fun initFloatWindow(){ + mFloatLayout = LayoutInflater.from(mActivity).inflate(R.layout.view_fault_reason, null) as View + mFloatLayout.setOnTouchListener(this) + + // 初始化识别无UI识别对象 + // 使用SpeechRecognizer对象,可根据回调消息自定义界面; + mIat = SpeechRecognizer.createRecognizer(mActivity, mInitListener) + initView() + initEvent() + mWindowParams = WindowManager.LayoutParams() + mWindowManager = mActivity.windowManager + mWindowParams?.let { + it.format = PixelFormat.RGBA_8888 + it.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + it.gravity = Gravity.START or Gravity.TOP + it.width = 844 + it.height = 991 + it.alpha = 1.0f + } + } + + private fun initView(){ + tvFaultTitle = mFloatLayout.findViewById(R.id.tvFaultTitle) + tvFaultTime = mFloatLayout.findViewById(R.id.tvFaultTime) + tvFaultType = mFloatLayout.findViewById(R.id.tvFaultType) + ivFaultTypeSelect = mFloatLayout.findViewById(R.id.ivFaultTypeSelect) + tvFaultReason = mFloatLayout.findViewById(R.id.tvFaultReason) + ivFaultReasonSelect = mFloatLayout.findViewById(R.id.ivFaultReasonSelect) + ivTimeReduce = mFloatLayout.findViewById(R.id.ivTimeReduce) + tvOccurrenceTime = mFloatLayout.findViewById(R.id.tvOccurrenceTime) + ivTimeAdd = mFloatLayout.findViewById(R.id.ivTimeAdd) + etNoteInput = mFloatLayout.findViewById(R.id.etNoteInput) + ivNoteAudio = mFloatLayout.findViewById(R.id.ivNoteAudio) + tvFaultReport = mFloatLayout.findViewById(R.id.tvFaultReport) + tvFaultCancel = mFloatLayout.findViewById(R.id.tvFaultCancel) + rvFaultList = mFloatLayout.findViewById(R.id.rvFaultList) + tvUploadSuccess = mFloatLayout.findViewById(R.id.tvUploadSuccess) + faultReasonGroup = mFloatLayout.findViewById(R.id.faultReasonGroup) + } + + @OptIn(DelicateCoroutinesApi::class) + @SuppressLint("SetTextI18n") + private fun initEvent(){ + //弹窗展示时间 + tvFaultTime.text = mActivity.resources.getString(R.string.take_over_time) + + millis2String(System.currentTimeMillis(), TimeUtils.getHourMinSecondFormat()) + //发生时间 + tvOccurrenceTime.text = millis2String(workOrderOccurrenceTime, TimeUtils.getHourMinFormat()) + ivTimeReduce.setOnClickListener { + workOrderOccurrenceTime -= 60000 + tvOccurrenceTime.text = millis2String(workOrderOccurrenceTime, TimeUtils.getHourMinFormat()) + } + ivTimeAdd.setOnClickListener { + if(workOrderOccurrenceTime + 60000 > System.currentTimeMillis()){ + ToastUtils.showShort("发生时间应在当前时间之前") + return@setOnClickListener + } + workOrderOccurrenceTime += 60000 + tvOccurrenceTime.text = millis2String(workOrderOccurrenceTime, TimeUtils.getHourMinFormat()) + } + //补充描述 + etNoteInput.onFocusChangeListener = OnFocusChangeListener { v, hasFocus -> + val edit = v as EditText + if(hasFocus){ + edit.hint = "" + }else{ + edit.hint = "手动输入" + } + } + etNoteInput.addTextChangedListener(object: TextWatcher{ + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + + } + + override fun afterTextChanged(s: Editable?) { + + } + + }) + //问题描述录音 + ivNoteAudio.setOnClickListener { + audioStatus = !audioStatus + setAudio(audioStatus) + } + //上报 + tvFaultReport.setOnClickListener{ + //TODO + } + //取消 + tvFaultCancel.setOnClickListener { + hideFloatWindow() + } + + } + + private fun setAudio(status: Boolean){ + if(status){ + //开始录音 + mIat?.let { + //清空之前的内容 + mIatResults.clear() + SpeechUtils.setParam(it) + // 不显示听写对话框 + ret = it.startListening(mRecognizerListener) + if (ret != ErrorCode.SUCCESS) { + ToastUtils.showShort("听写失败,错误码:$ret,请点击网址https://www.xfyun.cn/document/error-code查询解决方案") + } + } + //开始录音,展示放大缩小动效 + val scaleAnimation = ScaleAnimation( + 1.0f, 0.8f, 1.0f, 0.8f, + Animation.RELATIVE_TO_SELF, 0.8f, Animation.RELATIVE_TO_SELF, 0.8f + ) + scaleAnimation.duration = 1000 + scaleAnimation.repeatCount = -1 + ivNoteAudio.startAnimation(scaleAnimation) + }else{ + //停止语音听写 + mIat?.stopListening() + //结束动画 + ivNoteAudio.clearAnimation() + } + } + + /** + * 初始化监听器。 + */ + private val mInitListener = InitListener { code -> + if (code != ErrorCode.SUCCESS) { + ToastUtils.showShort("讯飞语音听写初始化失败,错误码:$code") + } + } + + /** + * 听写监听器。 + */ + private val mRecognizerListener: RecognizerListener = object : RecognizerListener{ + override fun onVolumeChanged(p0: Int, p1: ByteArray?) { + //showTip("当前正在说话,音量大小 = " + volume + " 返回音频数据 = " + data.length); + } + + override fun onBeginOfSpeech() { + // 此回调表示:sdk内部录音机已经准备好了,用户可以开始语音输入 + } + + override fun onEndOfSpeech() { + // 此回调表示:检测到了语音的尾端点,已经进入识别过程,不再接受语音输入 + } + + override fun onResult(results: RecognizerResult?, isLast: Boolean) { + results?.let { + printResult(it) + } + } + + override fun onError(p0: SpeechError?) { + // Tips: + // 错误码:10118(您没有说话),可能是录音机权限被禁,需要提示用户打开应用的录音权限。 + } + + override fun onEvent(p0: Int, p1: Int, p2: Int, p3: Bundle?) { + // 以下代码用于获取与云端的会话id,当业务出错时将会话id提供给技术支持人员,可用于查询会话日志,定位出错原因 + // 若使用本地能力,会话id为null + // if (SpeechEvent.EVENT_SESSION_ID == eventType) { + // String sid = obj.getString(SpeechEvent.KEY_EVENT_SESSION_ID); + // Log.d(TAG, "session id =" + sid); + // } + } + + } + + /** + * 显示结果 + */ + @SuppressLint("SetTextI18n") + private fun printResult(results: RecognizerResult) { + val text: String = JsonParser.parseIatResult(results.resultString) + Log.i(TAG, "语音内容=$text") + if(text.isNotEmpty()){ + if(etNoteInput.text.toString().isEmpty()){ + etNoteInput.setText(text) + etNoteInput.setSelection(text.length) + }else{ + val startStr = etNoteInput.text.toString().substring(0,etNoteInput.selectionStart) + val endStr = etNoteInput.text.toString().substring(etNoteInput.selectionEnd,etNoteInput.text.toString().length) + etNoteInput.setText(startStr+text+endStr) + etNoteInput.setSelection(startStr.length+text.length) + } + + } + } + + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View?, motionEvent: MotionEvent?): Boolean { + when (motionEvent?.action) { + MotionEvent.ACTION_DOWN -> { + // 获取相对View的坐标,即以此View左上角为原点 + mInViewX = motionEvent.x + mInViewY = motionEvent.y + // 获取相对屏幕的坐标,即以屏幕左上角为原点 + mInScreenX = motionEvent.rawX + mInScreenY = motionEvent.rawY + } + MotionEvent.ACTION_MOVE -> { + // 更新浮动窗口位置参数 + mInScreenX = motionEvent.rawX + mInScreenY = motionEvent.rawY + if(((mInScreenX - mInViewX)-mWindowParams!!.x).absoluteValue>150 || ((mInScreenY - mInViewY)-mWindowParams!!.y).absoluteValue>150){ + return true + } + mWindowParams!!.x = (mInScreenX - mInViewX).toInt() + mWindowParams!!.y = (mInScreenY - mInViewY).toInt() + // 手指移动的时候更新小悬浮窗的位置 + mWindowManager!!.updateViewLayout(mFloatLayout, mWindowParams) + } + } + return true + } + + fun showFloatWindow() { + if (mFloatLayout.parent == null) { + val metrics = DisplayMetrics() + // 默认固定位置,靠屏幕右边缘的中间 + mWindowManager!!.defaultDisplay.getMetrics(metrics) + mWindowParams!!.x = metrics.widthPixels-890 + mWindowParams!!.y = metrics.heightPixels - BarUtils.getStatusBarHeight()-1140 + mWindowManager!!.addView(mFloatLayout, mWindowParams) + + + } + } + + fun hideFloatWindow() { + if (mFloatLayout.parent != null){ + mWindowManager!!.removeView(mFloatLayout) + } + CallerDevaToolsListenerManager.removeListener(TAG) + } + + fun setClickListener(clickListener: ClickListener) { + this.clickListener = clickListener + } + + interface ClickListener { + fun closeWindow() + } + +} \ No newline at end of file diff --git a/core/function-impl/mogo-core-function-devatools/src/main/java/com/zhjt/mogo_core_function_devatools/workorder/adapter/BottomDecoration.java b/core/function-impl/mogo-core-function-devatools/src/main/java/com/zhjt/mogo_core_function_devatools/workorder/adapter/BottomDecoration.java new file mode 100644 index 0000000000..3b702ff0f9 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools/src/main/java/com/zhjt/mogo_core_function_devatools/workorder/adapter/BottomDecoration.java @@ -0,0 +1,39 @@ +package com.zhjt.mogo_core_function_devatools.workorder.adapter; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +public class BottomDecoration extends RecyclerView.ItemDecoration { + /** + * 第一个视图和最后一个视图偏移的距离 + */ + public static int distance = 0; + + /** + * 设置RecyclerView子视图的边距,本示例仅用于定义两个子视图之间的边距,为space*2 + */ + public BottomDecoration(int distance) { + this.distance = distance; + } + + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + int pos = parent.getChildAdapterPosition(view); + RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams)view.getLayoutParams(); + + /** + * 通过设置Item左右边距实现第一个左侧和最后一个右侧设置边距,确保显示的视图位于屏幕中间 + */ + int itemCount = parent.getAdapter().getItemCount(); + if(pos == itemCount-1){ + layoutParams.setMargins(0,0,0,distance); + }else { + layoutParams.setMargins(0,0,0,0); + } + view.setLayoutParams(layoutParams); + super.getItemOffsets(outRect, view, parent, state); + } +} diff --git a/core/function-impl/mogo-core-function-devatools/src/main/res/drawable/bg_fault_reason.png b/core/function-impl/mogo-core-function-devatools/src/main/res/drawable/bg_fault_reason.png new file mode 100644 index 0000000000..778b364988 Binary files /dev/null and b/core/function-impl/mogo-core-function-devatools/src/main/res/drawable/bg_fault_reason.png differ diff --git a/core/function-impl/mogo-core-function-devatools/src/main/res/layout/view_fault_reason.xml b/core/function-impl/mogo-core-function-devatools/src/main/res/layout/view_fault_reason.xml new file mode 100644 index 0000000000..33fda05378 --- /dev/null +++ b/core/function-impl/mogo-core-function-devatools/src/main/res/layout/view_fault_reason.xml @@ -0,0 +1,363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/util/TimeUtils.java b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/util/TimeUtils.java index 3edf2d84c8..79bbd5bf17 100644 --- a/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/util/TimeUtils.java +++ b/core/mogo-core-utils/src/main/java/com/mogo/eagle/core/utilcode/util/TimeUtils.java @@ -55,6 +55,10 @@ public final class TimeUtils { return getSafeDateFormat("yyyyMMdd"); } + public static SimpleDateFormat getYMDFormat(){ + return getSafeDateFormat("yyyy年MM月dd日"); + } + public static SimpleDateFormat getHourMinSecondFormat(){ return getSafeDateFormat("HH:mm:ss"); }