添加了界面及逻辑

This commit is contained in:
zxj
2025-07-18 17:16:00 +08:00
parent b68c967d82
commit f97ef151c3
74 changed files with 3108 additions and 34 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<component name="VisualizationToolProject"> <component name="VisualizationToolProject">
<option name="state"> <option name="state">
<ProjectState> <ProjectState>
<option name="scale" value="0.19390498954287422" /> <option name="scale" value="0.24414062499999997" />
</ProjectState> </ProjectState>
</option> </option>
</component> </component>
+9 -7
View File
@@ -18,9 +18,9 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk { // ndk {
abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/)) // abiFilters.addAll(listOf("armeabi-v7a"/*, "arm64-v8a"*/))
} // }
} }
buildTypes { buildTypes {
@@ -62,11 +62,9 @@ dependencies {
implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.view)
implementation(libs.androidx.camera.extensions) implementation(libs.androidx.camera.extensions)
// 权限申请
implementation(libs.accompanist.permissions)
// hilt注入 // hilt注入
implementation(libs.hilt.android) // implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler) // ksp(libs.hilt.android.compiler)
// retrofit网络请求 // retrofit网络请求
implementation(libs.retrofit) implementation(libs.retrofit)
@@ -82,4 +80,8 @@ dependencies {
implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.android)
implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.fragment.ktx)
// implementation(libs.adapter.coroutines)
} }
+9 -4
View File
@@ -20,6 +20,9 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SmartPlateCabinet" android:theme="@style/Theme.SmartPlateCabinet"
tools:targetApi="31"> tools:targetApi="31">
<activity
android:name=".activity.TestActivity"
android:exported="true"></activity>
<activity <activity
android:name=".activity.LoginByPwdActivity" android:name=".activity.LoginByPwdActivity"
android:exported="false" android:exported="false"
@@ -37,11 +40,13 @@
<activity <activity
android:name=".activity.MainActivity" android:name=".activity.MainActivity"
android:exported="true"> android:exported="true">
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MAIN" />-->
<!-- <category android:name="android.intent.category.LAUNCHER" />--> <!-- <intent-filter> -->
<!-- </intent-filter>--> <!-- <action android:name="android.intent.action.MAIN" /> -->
<!-- <category android:name="android.intent.category.LAUNCHER" /> -->
<!-- </intent-filter> -->
</activity> </activity>
</application> </application>
@@ -0,0 +1,14 @@
package com.sw.platecabinet
object GlobalData {
}
/**
*
*/
object GlobalKey {
const val KEY_TOKEN = "tokenKey"
const val KEY_FIRST_RUN = "firstRun"
const val KEY_USER_INFO = "userInfoKey"
}
@@ -1,9 +1,16 @@
package com.sw.platecabinet package com.sw.platecabinet
import android.app.Application
import android.content.Context
import com.sw.plate.App import com.sw.plate.App
import timber.log.Timber
class MyApp : App() { class MyApp : App() {
companion object {
const val DEBUG: Boolean = true
}
override fun onCreate() {
super.onCreate()
Timber.plant(Timber.DebugTree())
Timber.d("初始化")
}
} }
@@ -1,36 +1,39 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager import android.text.TextUtils
import android.view.View
import android.widget.TextView
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.sw.inbound.utils.DateTimeUtils import com.sw.inbound.utils.DateTimeUtils
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.ext.setClickListeners
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.view.CustomDialog
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
/** /**
* activity 基类 * activity 基类
*/ */
abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() { abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
protected lateinit var binding: VB protected lateinit var binding: VB
private set
private var headerBinding: ItemTitleTimeBinding? = null private var headerBinding: ItemTitleTimeBinding? = null
protected lateinit var context: Context protected lateinit var context: Context
private var timeJob: Job? = null private var timeJob: Job? = null
private var mDialogWaiting: CustomDialog? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
context = this context = this
binding = inflateViewBinding() binding = inflateViewBinding()
headerBinding = inflateTitleBinding() headerBinding = inflateTitleBinding()
@@ -41,6 +44,11 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
fun updateTime() { fun updateTime() {
headerBinding?.let { headerBinding?.let {
headerBinding!!.tvRightTime.setClickListeners(
onDoubleClick = {
MainActivity.start(context, pageType = PageType.SETTING_LIST)
}
)
val scope = CoroutineScope(Dispatchers.Main) val scope = CoroutineScope(Dispatchers.Main)
timeJob = scope.launch { timeJob = scope.launch {
DateTimeUtils.realTimeChineseDateFlow() DateTimeUtils.realTimeChineseDateFlow()
@@ -52,6 +60,45 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
} }
} }
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// 统一处理权限拒绝情况
val allGranted = PermissionHelper.handlePermissionResult(
this,
requestCode,
permissions,
grantResults
) { permanentlyDeniedPermissions ->
// 权限被永久拒绝的统一处理
Timber.e("用户永久拒绝权限 $permanentlyDeniedPermissions")
}
}
/**
* 显示等待提示框
*/
fun showWaitingDialog(tip: String?): Dialog? {
hideWaitingDialog()
val view = View.inflate(this, R.layout.dialog_waiting, null)
if (!TextUtils.isEmpty(tip)) (view.findViewById<View?>(R.id.tvTip) as TextView).setText(tip)
mDialogWaiting = CustomDialog(this, view, R.style.MyDialog)
mDialogWaiting!!.show()
mDialogWaiting!!.setCancelable(true)
return mDialogWaiting
}
/**
* 隐藏等待提示框
*/
fun hideWaitingDialog() {
mDialogWaiting?.dismiss()
mDialogWaiting = null
}
protected abstract fun inflateViewBinding(): VB protected abstract fun inflateViewBinding(): VB
protected abstract fun inflateTitleBinding(): ItemTitleTimeBinding? protected abstract fun inflateTitleBinding(): ItemTitleTimeBinding?
@@ -1,9 +1,11 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.Manifest
import android.content.Intent import android.content.Intent
import android.graphics.Outline import android.graphics.Outline
import android.view.View import android.view.View
import android.view.ViewOutlineProvider import android.view.ViewOutlineProvider
import androidx.activity.viewModels
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
import androidx.camera.core.Preview import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
@@ -11,6 +13,8 @@ import androidx.core.content.ContextCompat
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.sw.platecabinet.databinding.ActivityLoginFaceBinding import com.sw.platecabinet.databinding.ActivityLoginFaceBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.utils.PermissionHelper
import com.sw.platecabinet.viewmodel.UserViewModel
import timber.log.Timber import timber.log.Timber
/** /**
@@ -18,6 +22,10 @@ import timber.log.Timber
*/ */
class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() { class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider> private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>
private val viewModel by viewModels<UserViewModel>()
private val CAMERA_PERMISSION_REQUEST_CODE = 100
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
override fun inflateViewBinding(): ActivityLoginFaceBinding { override fun inflateViewBinding(): ActivityLoginFaceBinding {
return ActivityLoginFaceBinding.inflate(layoutInflater) return ActivityLoginFaceBinding.inflate(layoutInflater)
@@ -28,14 +36,25 @@ class LoginByFaceActivity : BaseActivity<ActivityLoginFaceBinding>() {
} }
override fun initialize() { override fun initialize() {
// 请求权限
checkCameraPermission()
initView() initView()
startCamera() viewModel.generateToken()
binding.llToPwd.setOnClickListener { binding.llToPwd.setOnClickListener {
val intent = Intent(this, LoginByPwdActivity::class.java) val intent = Intent(this, LoginByPwdActivity::class.java)
startActivity(intent) startActivity(intent)
} }
} }
private fun checkCameraPermission() {
PermissionHelper.with(this, REQUIRED_PERMISSIONS, CAMERA_PERMISSION_REQUEST_CODE)
.onGranted {
// 权限已授予
startCamera()
}
.checkAndRequest()
}
private fun initView() { private fun initView() {
binding.previewView.outlineProvider = object : ViewOutlineProvider() { binding.previewView.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) { override fun getOutline(view: View, outline: Outline) {
@@ -14,6 +14,9 @@ class LoginByPwdActivity : BaseActivity<ActivityLoginByPwdBinding>() {
} }
override fun initialize() { override fun initialize() {
binding.btnLogin.setOnClickListener {
MainActivity.start(context, pageType = PageType.PLATE_CABINET_FULL)
}
binding.tvFaceRec.setOnClickListener { binding.tvFaceRec.setOnClickListener {
val intent = Intent(this, LoginByFaceActivity::class.java) val intent = Intent(this, LoginByFaceActivity::class.java)
startActivity(intent) startActivity(intent)
@@ -1,9 +1,20 @@
package com.sw.platecabinet.activity package com.sw.platecabinet.activity
import android.content.Context
import android.content.Intent
import com.sw.platecabinet.R
import com.sw.platecabinet.databinding.ActivityMainBinding import com.sw.platecabinet.databinding.ActivityMainBinding
import com.sw.platecabinet.databinding.ItemTitleTimeBinding import com.sw.platecabinet.databinding.ItemTitleTimeBinding
import com.sw.platecabinet.fragment.BindPlateFragment
import com.sw.platecabinet.fragment.PlateCabinetFullFragment
import com.sw.platecabinet.fragment.PlateOpenFragment
import com.sw.platecabinet.fragment.SettingListFragment
import com.sw.platecabinet.fragment.UnBindPlateFragment
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.utils.FragmentHelper
class MainActivity : BaseActivity<ActivityMainBinding>() { class MainActivity : BaseActivity<ActivityMainBinding>() {
private lateinit var fragmentHelper: FragmentHelper
override fun inflateViewBinding(): ActivityMainBinding { override fun inflateViewBinding(): ActivityMainBinding {
return ActivityMainBinding.inflate(layoutInflater) return ActivityMainBinding.inflate(layoutInflater)
@@ -13,7 +24,65 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
return binding.includeHeader return binding.includeHeader
} }
override fun initialize() { // 静态启动方法
companion object {
private const val PARAM_PAGE_TYPE = "pageType"
private const val PARAM_EQUIPMENT_INFO = "equipmentUserInfo"
fun start(
context: Context,
pageType: PageType,
equipmentUserInfo: EquipmentUserInfo? = null
) {
val intent = Intent(context, MainActivity::class.java)
intent.putExtra(PARAM_PAGE_TYPE, pageType.name)
intent.putExtra(PARAM_EQUIPMENT_INFO, equipmentUserInfo)
context.startActivity(intent)
}
} }
}
override fun initialize() {
val param = intent.getStringExtra(PARAM_PAGE_TYPE)
val equipmentUserInfo = intent.getParcelableExtra<EquipmentUserInfo>(PARAM_EQUIPMENT_INFO)
val pageType = param?.let { PageType.valueOf(param) } ?: PageType.SETTING_LIST
fragmentHelper = FragmentHelper(supportFragmentManager, R.id.fragment_container)
val page = when (pageType) {
PageType.SETTING_LIST -> SettingListFragment()
PageType.BIND_PLATE -> BindPlateFragment.newInstance(equipmentUserInfo)
PageType.PLATE_OPEN -> PlateOpenFragment()
PageType.UNBIND_PLATE -> UnBindPlateFragment.newInstance(equipmentUserInfo)
PageType.PLATE_CABINET_FULL -> PlateCabinetFullFragment()
}
fragmentHelper.addFragment(page)
}
}
/**
* 界面类型
*/
enum class PageType {
/**
* 餐盘柜打开
*/
PLATE_OPEN,
/**
* 设置列表
*/
SETTING_LIST,
/**
* 绑定餐盘
*/
BIND_PLATE,
/**
* 解绑餐盘
*/
UNBIND_PLATE,
/**
* 餐盘柜已满
*/
PLATE_CABINET_FULL
}
@@ -0,0 +1,97 @@
package com.sw.platecabinet.activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.sw.platecabinet.R
import kotlin.math.ceil
class TestActivity : AppCompatActivity() {
private lateinit var parentRecyclerView: RecyclerView
private lateinit var pageAdapter: PageAdapter
private val itemsPerPage = 22 // 每行2个,每列11个,共22个
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
parentRecyclerView = findViewById(R.id.parentRecyclerView)
parentRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
// 假设有100个数据项
val allItems = (1..30).map { "Item $it" }
val pages = allItems.chunked(itemsPerPage)
pageAdapter = PageAdapter(pages)
parentRecyclerView.adapter = pageAdapter
}
private fun convertToColumnFirst(original: List<String>, cols: Int): List<String> {
val rows = ceil(original.size.toDouble() / cols).toInt()
return List(original.size) { pos ->
val row = pos % rows
val col = pos / rows
val originalPos = col + row * cols
original.getOrElse(originalPos) { "" }
}
}
inner class PageAdapter(private val pages: List<List<String>>) :
RecyclerView.Adapter<PageAdapter.PageViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.page_item_layout, parent, false)
return PageViewHolder(view)
}
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
holder.bind(pages[position])
}
override fun getItemCount(): Int = pages.size
inner class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val childRecyclerView: RecyclerView =
itemView.findViewById(R.id.childRecyclerView)
fun bind(items: List<String>) {
childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
childRecyclerView.adapter = ChildAdapter(items)
}
}
}
inner class ChildAdapter(private val items: List<String>) :
RecyclerView.Adapter<ChildAdapter.ItemViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_bind_view, parent, false)
return ItemViewHolder(view)
}
override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(position, items[position])
}
override fun getItemCount(): Int = items.size
inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val llRoot: LinearLayout = itemView.findViewById(R.id.ll_root)
private val tvNum: TextView = itemView.findViewById(R.id.tv_num)
fun bind(position: Int, item: String) {
tvNum.text = item
llRoot.setBackgroundResource(if (position % 3 == 0) R.drawable.grid_item_bind else R.drawable.grid_item_unbind)
}
}
}
}
@@ -0,0 +1,84 @@
package com.sw.platecabinet.adapter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.sw.platecabinet.R
/**
* 通用分页适配器(支持ViewBinding
*/
class GenericPageAdapter<T, VB : ViewBinding>(
private var pages: List<List<T>>,
private val pageLayoutId: Int,
private val itemBindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
private val itemBindCallback: VB.(item: T, position: Int) -> Unit
) : RecyclerView.Adapter<GenericPageAdapter<T, VB>.PageViewHolder>() {
fun updatePages(newPages: List<List<T>>) {
this.pages = newPages
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
val view = LayoutInflater.from(parent.context).inflate(pageLayoutId, parent, false)
return PageViewHolder(view)
}
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
holder.bind(pages[position])
}
override fun getItemCount(): Int = pages.size
inner class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val childRecyclerView: RecyclerView = itemView.findViewById(R.id.childRecyclerView)
fun bind(items: List<T>) {
childRecyclerView.layoutManager = GridLayoutManager(itemView.context, 2)
// 添加间距装饰(12dp)
childRecyclerView.addItemDecoration(
GridSpacingItemDecoration(
spanCount = 2, // 2列
spacing = itemView.context.dpToPx(12), // 12dp
includeEdge = true // 包含边缘间距
)
)
childRecyclerView.adapter =
GenericChildAdapter(items, itemBindingInflater, itemBindCallback)
}
}
}
/**
* 通用子项适配器(支持ViewBinding
*/
class GenericChildAdapter<T, VB : ViewBinding>(
private val items: List<T>,
private val bindingInflater: (LayoutInflater, ViewGroup, Boolean) -> VB,
private val bindCallback: VB.(item: T, position: Int) -> Unit,
) : RecyclerView.Adapter<GenericChildAdapter<T, VB>.ViewHolder>() {
inner class ViewHolder(private val binding: VB) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: T, position: Int) {
binding.bindCallback(item, position)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = bindingInflater(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position], position)
}
override fun getItemCount(): Int = items.size
}
@@ -0,0 +1,54 @@
package com.sw.platecabinet.adapter
import android.content.Context
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
/**
* 为RecyclerView添加间距的ItemDecoration
*/
class GridSpacingItemDecoration(
private val spanCount: Int, // 列数
private val spacing: Int, // 间距(dp)
private val includeEdge: Boolean // 是否包含边缘
) : RecyclerView.ItemDecoration() {
override fun getItemOffsets(
outRect: Rect,
view: View,
parent: RecyclerView,
state: RecyclerView.State
) {
val position = parent.getChildAdapterPosition(view) // item position
val column = position % spanCount // item column
if (includeEdge) {
// 左右间距计算
outRect.left = spacing - column * spacing / spanCount
outRect.right = (column + 1) * spacing / spanCount
// 第一行添加顶部间距
if (position < spanCount) {
outRect.top = spacing
}
outRect.bottom = spacing // 每行底部都添加间距
} else {
// 左右间距计算(不包含边缘)
outRect.left = column * spacing / spanCount
outRect.right = spacing - (column + 1) * spacing / spanCount
// 不是第一行时添加顶部间距
if (position >= spanCount) {
outRect.top = spacing
}
}
}
}
/**
* 将dp值转换为px
*/
fun Context.dpToPx(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}
@@ -46,4 +46,8 @@ val Float.sp: Float
TypedValue.COMPLEX_UNIT_SP, TypedValue.COMPLEX_UNIT_SP,
this, this,
Resources.getSystem().displayMetrics Resources.getSystem().displayMetrics
) )
fun formatNumber(num: String): String {
return num.padStart(2, '0')
}
@@ -0,0 +1,33 @@
package com.sw.platecabinet.ext
import android.os.Handler
import android.os.Looper
import android.view.View
import android.widget.TextView
/**
* 设置 TextView 的双击和单击事件监听器
* @param doubleClickInterval 双击间隔时间(毫秒),默认 300ms
* @param onDoubleClick 双击回调
* @param onSingleClick 单击回调
*/
fun TextView.setClickListeners(
doubleClickInterval: Long = 300,
onDoubleClick: (View) -> Unit = {},
onSingleClick: (View) -> Unit = {}
) {
var clickCount = 0
val handler = Handler(Looper.getMainLooper())
setOnClickListener { view ->
clickCount++
handler.postDelayed({
if (clickCount == 1) {
onSingleClick(view)
} else if (clickCount == 2) {
onDoubleClick(view)
}
clickCount = 0 // 重置计数
}, doubleClickInterval)
}
}
@@ -0,0 +1,41 @@
package com.sw.platecabinet.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.viewbinding.ViewBinding
abstract class BaseFragment<VB : ViewBinding>(
private val bindingInflater: (inflater: LayoutInflater, parent: ViewGroup?, attachToParent: Boolean) -> VB
) : Fragment() {
private var _binding: VB? = null
protected val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = bindingInflater(inflater, container, false)
initialize()
return binding.root
}
abstract fun initialize()
class ViewModelFactory<T : ViewModel>(private val creator: () -> T) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return creator() as T
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,65 @@
package com.sw.platecabinet.fragment
import android.os.Bundle
import com.sw.platecabinet.R
import com.sw.platecabinet.adapter.GenericChildAdapter
import com.sw.platecabinet.databinding.FragmentBindPlateBinding
import com.sw.platecabinet.databinding.ItemSearchUserInfoBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo
import timber.log.Timber
/**
* 餐盘柜绑定
*/
class BindPlateFragment : BaseFragment<FragmentBindPlateBinding>(
FragmentBindPlateBinding::inflate
) {
private lateinit var adapter: GenericChildAdapter<EquipmentUserInfo, ItemSearchUserInfoBinding>
internal val ARG_PARAM1 = "param1"
private var info: EquipmentUserInfo? = null
companion object {
@JvmStatic
fun newInstance(param1: EquipmentUserInfo?) =
BindPlateFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_PARAM1, param1)
}
}
}
override fun initialize() {
arguments?.let {
info = it.getParcelable(ARG_PARAM1)
info?.let { binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}" }
}
adapter = createAdapter()
registerDateChange()
binding.tvBack.setOnClickListener { activity?.finish() }
}
private fun registerDateChange() {
}
private fun createAdapter(): GenericChildAdapter<EquipmentUserInfo, ItemSearchUserInfoBinding> {
return GenericChildAdapter(
items = emptyList(),
bindingInflater = ItemSearchUserInfoBinding::inflate,
bindCallback = { item, position ->
this.tvName.text = item.name
this.tvPhone.text = item.phone
if (item.isBound()) {
this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
} else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
}
this.llRoot.setOnClickListener {
Timber.d("itemClick ${item.name}, position = $position")
}
}
)
}
}
@@ -0,0 +1,29 @@
package com.sw.platecabinet.fragment
import android.os.CountDownTimer
import com.sw.platecabinet.databinding.FragmentPlateCabinetFullBinding
/**
* 餐盘柜已满
*/
class PlateCabinetFullFragment :
BaseFragment<FragmentPlateCabinetFullBinding>(FragmentPlateCabinetFullBinding::inflate) {
private var totalTimeInMillis: Long = 5 * 1000
override fun initialize() {
initCountTime()
}
fun initCountTime() {
object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
binding.tvAutoClose.text = "${(millisUntilFinished / 1000).toInt()}秒后返回主屏"
}
override fun onFinish() {
activity?.finish()
}
}.start()
}
}
@@ -0,0 +1,28 @@
package com.sw.platecabinet.fragment
import android.os.CountDownTimer
import com.sw.platecabinet.databinding.FragmentPlateOpenBinding
/**
* 餐盘柜打开界面
*/
class PlateOpenFragment :
BaseFragment<FragmentPlateOpenBinding>(FragmentPlateOpenBinding::inflate) {
private var totalTimeInMillis: Long = 5 * 1000
override fun initialize() {
initCountTime()
}
fun initCountTime() {
object : CountDownTimer(totalTimeInMillis, 1000) {
override fun onTick(millisUntilFinished: Long) {
binding.tvAutoClose.text = (millisUntilFinished / 1000).toInt().toString()
}
override fun onFinish() {
activity?.finish()
}
}.start()
}
}
@@ -0,0 +1,135 @@
package com.sw.platecabinet.fragment
import android.view.View
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import com.sw.inbound.utils.DateTimeUtils
import com.sw.platecabinet.R
import com.sw.platecabinet.activity.MainActivity
import com.sw.platecabinet.activity.PageType
import com.sw.platecabinet.adapter.GenericPageAdapter
import com.sw.platecabinet.databinding.FragmentSettingListBinding
import com.sw.platecabinet.databinding.ItemBindViewBinding
import com.sw.platecabinet.ext.formatNumber
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.viewmodel.SettingViewModel
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.math.ceil
/**
* 餐盘柜绑定的用户列表界面
*/
class SettingListFragment :
BaseFragment<FragmentSettingListBinding>(FragmentSettingListBinding::inflate) {
private val viewModel: SettingViewModel by viewModels<SettingViewModel>()
private lateinit var pageAdapter: GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding>
private val itemsPerPage = 22 // 每行2个,每列11个,共22个
private fun registerDateChange() {
lifecycleScope.launch {
// 使用 repeatOnLifecycle 确保只在特定生命周期状态收集
// repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.equipmentList.collect { it ->
Timber.d("registerDateChange updateAdapter it = $it")
updateAdapter(it)
}
// }
}
}
override fun initialize() {
binding.mainRecyclerView.apply {
layoutManager = LinearLayoutManager(context)
pageAdapter = createAdapter()
adapter = pageAdapter
}
registerDateChange()
viewModel.getEquipmentList(EquipmentParam(equipmentCode = "202501171634"))
}
private fun createAdapter(): GenericPageAdapter<EquipmentUserInfo, ItemBindViewBinding> {
return GenericPageAdapter(
pages = emptyList(),
pageLayoutId = R.layout.page_item_layout,
itemBindingInflater = ItemBindViewBinding::inflate,
itemBindCallback = { item, position ->
this.tvNum.text = formatNumber(item.equipmentBoxCode ?: "")
if (item.isBound()) {
val date = DateTimeUtils.parseDateTime(item.updateTime)
val timeInMillis = date?.time ?: 0L
val isOldTime = DateTimeUtils.isMoreThan36HoursFromNow(timeInMillis)
this.llRoot.setBackgroundResource(R.drawable.grid_item_bind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_4E535D))
this.tvLastTime.setTextColor(
if (isOldTime) resources.getColor(R.color.bind_time_old) else resources.getColor(
R.color.bind_585868
)
)
this.llOpen.setBackgroundResource(R.drawable.grid_button_bind)
this.tvOpen.setTextColor(resources.getColor(R.color.bind_F0C8B4))
this.llBindInfo.visibility = View.VISIBLE
this.tvUnbind.visibility = View.GONE
this.tvName.text = item.name
this.tvLastTime.text = DateTimeUtils.getTimeAgo(date)
} else {
this.llRoot.setBackgroundResource(R.drawable.grid_item_unbind)
this.tvNum.setTextColor(resources.getColor(R.color.bind_F0C8B4))
this.llOpen.setBackgroundResource(R.drawable.grid_button_unbind)
this.tvOpen.setTextColor(resources.getColor(R.color.unbind_32283C))
this.llBindInfo.visibility = View.GONE
this.tvUnbind.visibility = View.VISIBLE
}
this.llRoot.setOnClickListener {
Timber.d("itemClick llRoot ${item.name}, position = $position")
if (item.isBound()) {
MainActivity.start(requireContext(), pageType = PageType.UNBIND_PLATE)
} else {
MainActivity.start(requireContext(), pageType = PageType.BIND_PLATE)
}
}
this.llOpen.setOnClickListener {
Timber.d("itemClick llOpen ${item.equipmentCode}")
MainActivity.start(requireContext(), pageType = PageType.PLATE_OPEN)
}
}
)
}
fun updateAdapter(items: List<EquipmentUserInfo>) {
val pages = if (items.size > itemsPerPage) {
items.chunked(itemsPerPage) {
convertToColumnFirst(it, 11)
}
} else {
listOf(
// 根据条件判断是否需要重新排列
// items
convertToColumnFirst(items, 11)
)
}
pageAdapter.updatePages(pages)
}
/**
* 重新排列数组
*/
private fun convertToColumnFirst(
original: List<EquipmentUserInfo>,
cols: Int
): List<EquipmentUserInfo> {
val rows = ceil(original.size.toDouble() / cols).toInt()
return List(original.size) { pos ->
val row = pos % rows
val col = pos / rows
val originalPos = col + row * cols
original[originalPos]
}
}
}
@@ -0,0 +1,42 @@
package com.sw.platecabinet.fragment
import android.os.Bundle
import com.sw.inbound.utils.DateTimeUtils
import com.sw.platecabinet.databinding.FragmentUnbindPlateBinding
import com.sw.platecabinet.model.response.EquipmentUserInfo
/**
* 餐盘柜绑定
*/
class UnBindPlateFragment private constructor() : BaseFragment<FragmentUnbindPlateBinding>(
FragmentUnbindPlateBinding::inflate
) {
internal val ARG_PARAM1 = "param1"
private var param1: EquipmentUserInfo? = null
private var info: EquipmentUserInfo? = null
companion object {
@JvmStatic
fun newInstance(param1: EquipmentUserInfo?) =
UnBindPlateFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_PARAM1, param1)
}
}
}
override fun initialize() {
arguments?.let {
info = it.getParcelable(ARG_PARAM1)
info?.let {
binding.tvNum.text = "${it.equipmentName}-${it.equipmentBoxCode}"
binding.tvPlateNumberValue.text = it.plateNumber
binding.tvNameValue.text = it.name
binding.tvPhoneValue.text = it.phone
val timeInfo = DateTimeUtils.parseDateTime(it.updateTime)
val timeAgo = DateTimeUtils.getTimeAgo(timeInfo)
binding.tvUpdateTimeValue.text = timeAgo
}
}
}
}
@@ -0,0 +1,35 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
*
* 设备与用户餐盘信息绑定解绑请求参数
*/
@Parcelize
data class BindParam(
/**
*设备盒子编号
*/
@SerializedName("equipmentBoxCode")
val equipmentBoxCode: String? = "",
/**
*设备编号
*/
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
/**
*
* 会员Id
*/
@SerializedName("memberId")
val memberId: Int? = 0,
/**
*餐盘编号
*/
@SerializedName("plateNumber")
val plateNumber: String? = ""
) : Parcelable
@@ -0,0 +1,15 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
/**
* 根据设备ID查询当前用户的餐盘信息,用户信息,设备盒子信息请求参数
*/
@Parcelize
data class EquipmentParam(
/**
* 设备ID
*/
var equipmentCode: String = ""
) : Parcelable
@@ -0,0 +1,33 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 登录参数
*/
@Parcelize
data class LoginParam(
/**
* 设备ID
*/
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
/**
* 会员信息
*/
@SerializedName("memberId")
val memberId: String? = "",
/**
* 密码
*/
@SerializedName("password")
val password: String? = "",
/**
* 手机号
*/
@SerializedName("phone")
val phone: String? = ""
) : Parcelable
@@ -0,0 +1,25 @@
package com.sw.platecabinet.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* (手机号/会员信息/会员卡号)模糊搜索
*/
@Parcelize
data class SearchParam(
@SerializedName("consumptionTime")
val consumptionTime: List<String?>? = listOf(),
@SerializedName("createTime")
val createTime: List<String?>? = listOf(),
@SerializedName("memberFrom")
val memberFrom: Int? = 0,
@SerializedName("pageNum")
val pageNum: Int? = 0,
@SerializedName("pageSize")
val pageSize: Int? = 0,
@SerializedName("param")
val `param`: String? = ""
) : Parcelable
@@ -0,0 +1,10 @@
package com.sw.platecabinet.model.response
data class ApiResponse<T>(
val code: Int,
val success: Boolean? = false,
val msg: String? = "",
val data: T? = null
) {
fun isSuccess(): Boolean = code == 200
}
@@ -0,0 +1,65 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 设备绑定用户信息
*/
@Parcelize
data class EquipmentUserInfo(
/**
* 设备盒子编号
*/
@SerializedName("equipmentBoxCode")
val equipmentBoxCode: String? = "",
/**
* 设备编号
*/
@SerializedName("equipmentCode")
val equipmentCode: String? = "",
/**
*
* 设备Id
*/
@SerializedName("equipmentId")
val equipmentId: String? = "",
/**
* 设备名称
*/
@SerializedName("equipmentName")
val equipmentName: String? = "",
/**
* 主键
*/
@SerializedName("id")
val id: Int? = 0,
/**
* 会员Id
*/
@SerializedName("memberId")
val memberId: Int? = 0,
@SerializedName("name")
val name: String? = "",
@SerializedName("phone")
val phone: String? = "",
/**
* 餐盘编号
*/
@SerializedName("plateNumber")
val plateNumber: String? = "",
/**
*
* 更新时间
*/
@SerializedName("updateTime")
val updateTime: String? = ""
) : Parcelable {
fun isBound(): Boolean {
return memberId != null
}
}
@@ -0,0 +1,78 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class SearchResult(
@SerializedName("endRow")
val endRow: Int? = 0,
@SerializedName("hasNextPage")
val hasNextPage: Boolean? = false,
@SerializedName("hasPreviousPage")
val hasPreviousPage: Boolean? = false,
@SerializedName("isFirstPage")
val isFirstPage: Boolean? = false,
@SerializedName("isLastPage")
val isLastPage: Boolean? = false,
@SerializedName("list")
val list: List<Member>? = listOf(),
@SerializedName("navigateFirstPage")
val navigateFirstPage: Int? = 0,
@SerializedName("navigateLastPage")
val navigateLastPage: Int? = 0,
@SerializedName("navigatePages")
val navigatePages: Int? = 0,
@SerializedName("navigatepageNums")
val navigatepageNums: List<Int?>? = listOf(),
@SerializedName("nextPage")
val nextPage: Int? = 0,
@SerializedName("pageNum")
val pageNum: Int? = 0,
@SerializedName("pageSize")
val pageSize: Int? = 0,
@SerializedName("pages")
val pages: Int? = 0,
@SerializedName("prePage")
val prePage: Int? = 0,
@SerializedName("size")
val size: Int? = 0,
@SerializedName("startRow")
val startRow: Int? = 0,
@SerializedName("total")
val total: Int? = 0
) : Parcelable {
@Parcelize
data class Member(
@SerializedName("cardBalance")
val cardBalance: String? = "",
@SerializedName("cardCode")
val cardCode: String? = "",
@SerializedName("consumptionCount")
val consumptionCount: Int? = 0,
@SerializedName("consumptionTotal")
val consumptionTotal: String? = "",
@SerializedName("createTime")
val createTime: String? = "",
@SerializedName("firstTopUpTime")
val firstTopUpTime: String? = "",
@SerializedName("id")
val id: Int? = 0,
@SerializedName("integralBalance")
val integralBalance: Int? = 0,
@SerializedName("lastConsumptionTime")
val lastConsumptionTime: String? = "",
@SerializedName("memberFrom")
val memberFrom: Int? = 0,
@SerializedName("name")
val name: String? = "",
@SerializedName("phone")
val phone: String? = "",
@SerializedName("rewardBalance")
val rewardBalance: String? = "",
@SerializedName("topUpBalance")
val topUpBalance: String? = ""
) : Parcelable
}
@@ -0,0 +1,23 @@
package com.sw.platecabinet.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
/**
* 用户人脸信息
*/
@Parcelize
data class UserFaceModel(
@SerializedName("faceFeature")
val faceFeature: String? = "",
@SerializedName("faceFeatureString")
val faceFeatureString: String? = "",
@SerializedName("faceType")
val faceType: String? = "",
@SerializedName("userFaceId")
val userFaceId: String? = "",
@SerializedName("userId")
val userId: String? = ""
) : Parcelable
@@ -0,0 +1,45 @@
package com.sw.platecabinet.network
import com.sw.platecabinet.MyApp
import com.sw.platecabinet.network.api.ApiService
import com.sw.platecabinet.network.interceptor.RequestInterceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import timber.log.Timber
import java.util.concurrent.TimeUnit
object ApiClient {
private const val BASE_URL = "https://vip.shuziweidao.com/shuwei-zhct/"
// private const val BASE_URL = "http://192.168.1.8:9092"
private const val TIME_OUT = 30L // 超时时间(秒)
private val okHttpClient = OkHttpClient.Builder()
.connectTimeout(TIME_OUT, TimeUnit.SECONDS)
.readTimeout(TIME_OUT, TimeUnit.SECONDS)
.writeTimeout(TIME_OUT, TimeUnit.SECONDS)
.addNetworkInterceptor(HttpLoggingInterceptor(logger = {
Timber.d("okhttp logger ==>${it}")
}).apply {
level = if (MyApp.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.addInterceptor(RequestInterceptor())
.build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
// .addCallAdapterFactory(CoroutineCallAdapterFactory()) // 协程适配器
.build()
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
}
@@ -0,0 +1,54 @@
package com.sw.platecabinet.network.api
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.request.SearchParam
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
interface ApiService {
/**
* 生成token
*/
@GET("scales/generateToken")
suspend fun generateToken(@Query("deviceId") deviceId: String): ApiResponse<String>
/**
* 获取人脸数据
*/
@GET("scales/getUserFaceCache")
suspend fun getUserFaceCache(): ApiResponse<List<UserFaceModel>>
/**
* 餐盘用户信息获取
*/
@POST("swEquipmentRelUser/equipmentBoxLogin")
suspend fun equipmentBoxLogin(@Body param: LoginParam): ApiResponse<EquipmentUserInfo>
/**
* 餐盘用户信息列表查询
*/
@POST("swEquipmentRelUser/list")
suspend fun getEquipmentList(@Body param: EquipmentParam): ApiResponse<List<EquipmentUserInfo>>
/**
* 餐盘用户信息绑定解绑
*/
@POST("swEquipmentRelUser/addOrEdit")
suspend fun bindEquipment(@Body param: BindParam): ApiResponse<EquipmentUserInfo?>
/**
* 用户信息模糊搜索
*/
@POST("swclientUserInfoShop/selectList")
suspend fun searchUser(@Body param: SearchParam): ApiResponse<SearchResult>
}
@@ -0,0 +1,32 @@
package com.sw.platecabinet.network.interceptor
import com.sw.inbound.utils.SPUtil
import com.sw.plate.App
import com.sw.platecabinet.GlobalKey
import okhttp3.Interceptor
import okhttp3.Response
/**
* 请求拦截器
*/
class RequestInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
.header("Content-Type", "application/json")
.header("Accept", "application/json")
// .header("Authorization", "Bearer ${getToken()}")
.header("Authorization", getToken())
val newRequest = requestBuilder.build()
return chain.proceed(newRequest)
}
private fun getToken(): String {
// 从本地获取token的逻辑
val spUtil = SPUtil.getInstance(context = App.getContext())
return spUtil.get(GlobalKey.KEY_TOKEN, "") as String
// return "eyJhbGciOiJIUzUxMiJ9.eyJpZCI6MTQ2LCJ1c2VyTmFtZSI6IjEzNjgxNDQ4ODU2IiwibmFtZSI6IuW-kOejiiIsInBhc3N3b3JkIjoiOTllOTQ1ZmVjZmZjNWIzNDI4MmUwNDRlODYyMzdjM2UxZjU5OWY5OCIsInNhbHQiOiI0NmEzMzUzYWU4OTA0MDYxYjMzODU5ZWNlYTBlMGE2NyIsInBob25lIjoiMTM2ODE0NDg4NTYiLCJzdGF0dXMiOjEsInVzZXJUeXBlIjoyLCJjcmVhdGVVc2VyTm8iOiIxNDEiLCJjcmVhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJ1cGRhdGVVc2VyTm8iOiIxNDEiLCJ1cGRhdGVUaW1lIjoxNjI2OTQzNDE2MDAwLCJpc0RlbCI6ZmFsc2UsImVhSWQiOjk5LCJlYUlkTGlzdCI6Ijk5IiwiaXNTaG9wTWFuYWdlciI6dHJ1ZSwidXNlck5vIjoiMWY5Nzk5ZWMtODlkYi00MWYyLTk1YTEtY2UzNTA3Y2QyMTU2In0.f7wImPgBOYMV0AqRchnXGPkUWZN9dFJ9gLPsaB8uNldd21IfXLjJl8y-FiWVuVUvlwUvGpgqGDFR1JKj5H7amw"
}
}
@@ -0,0 +1,50 @@
package com.sw.platecabinet.repository
import com.google.gson.JsonParseException
import com.sw.platecabinet.model.response.ApiResponse
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
import java.net.ConnectException
import java.net.SocketTimeoutException
import javax.net.ssl.SSLHandshakeException
abstract class BaseRepository {
suspend fun <T> safeApiCall(apiCall: suspend () -> ApiResponse<T>): ApiResponse<T> {
return try {
apiCall()
} catch (e: Exception) {
Timber.e("safeApiCall Exception: ${e.stackTraceToString()}")
when (e) {
is HttpException -> {
ApiResponse(code = e.code(), msg = e.message())
}
is SocketTimeoutException -> {
ApiResponse(code = -2, msg = "请求超时: ${e.message}")
}
is ConnectException -> {
ApiResponse(code = -3, msg = "连接失败: ${e.message}")
}
is SSLHandshakeException -> {
ApiResponse(code = -4, msg = "SSL握手失败: ${e.message}")
}
is JsonParseException -> {
ApiResponse(code = -5, msg = "JSON解析错误: ${e.message}")
}
is IOException -> {
ApiResponse(code = -6, msg = "网络IO错误: ${e.message}")
}
else -> {
ApiResponse(code = -1, msg = "未知错误: ${e.message ?: "无错误信息"}")
}
}
}
}
}
@@ -0,0 +1,62 @@
package com.sw.platecabinet.repository
import com.sw.platecabinet.model.request.BindParam
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.request.LoginParam
import com.sw.platecabinet.model.request.SearchParam
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.model.response.EquipmentUserInfo
import com.sw.platecabinet.model.response.SearchResult
import com.sw.platecabinet.model.response.UserFaceModel
import com.sw.platecabinet.network.api.ApiService
import retrofit2.http.POST
/**
* 远程数据处理
*/
class RemoteRepository constructor(
private val apiService: ApiService
) : BaseRepository() {
/**
* 生成token
*/
suspend fun generateToken(deviceId: String): ApiResponse<String> {
return safeApiCall { apiService.generateToken(deviceId) }
}
/**
* 获取人脸数据
*/
suspend fun getUserFaceCache(): ApiResponse<List<UserFaceModel>> {
return safeApiCall { apiService.getUserFaceCache() }
}
/**
* 登录
*/
suspend fun equipmentBoxLogin(param: LoginParam): ApiResponse<EquipmentUserInfo> {
return safeApiCall { apiService.equipmentBoxLogin(param) }
}
/**
* 获取设备绑定用户列表
*/
suspend fun getEquipmentList(param: EquipmentParam): ApiResponse<List<EquipmentUserInfo>> {
return safeApiCall { apiService.getEquipmentList(param) }
}
/**
* 餐盘用户信息绑定解绑
*/
suspend fun bindEquipment(param: BindParam): ApiResponse<EquipmentUserInfo?> {
return safeApiCall { apiService.bindEquipment(param) }
}
/**
* 用户信息模糊搜索
*/
@POST("/swclientUserInfoShop/selectList")
suspend fun searchUser(param: SearchParam): ApiResponse<SearchResult> {
return safeApiCall { apiService.searchUser(param) }
}
}
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.flow
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import kotlin.math.abs
/** /**
* 时间格式化工具类 * 时间格式化工具类
@@ -47,4 +48,66 @@ object DateTimeUtils {
delay(intervalMillis) delay(intervalMillis)
} }
} }
/**
* 解析日期时间字符串
* @param timeString 格式为 "yyyy-MM-dd HH:mm:ss" 的字符串
* @return Date 对象,解析失败返回 null
*/
fun parseDateTime(timeString: String?): Date? {
return try {
if (timeString == null) return null
val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
format.parse(timeString)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 判断给定时间是否距离当前时间超过36小时
* @param timeInMillis 时间戳(毫秒)
* @return true 表示超过36小时,false 表示未超过
*/
fun isMoreThan36HoursFromNow(timeInMillis: Long): Boolean {
val currentTime = System.currentTimeMillis()
val timeDifference = currentTime - timeInMillis
val hoursDifference = timeDifference / (1000 * 60 * 60) // 毫秒转小时
return hoursDifference >= 36
}
/**
* 获取时间间隔描述
*/
fun getTimeAgo(date: Date?): String {
if (date == null) return "未知时间"
val now = Date()
val diffMillis = now.time - date.time
// 如果是未来时间
if (diffMillis < 0) {
val futureHours = abs(diffMillis) / (1000 * 60 * 60)
return if (futureHours < 24) {
"未来 $futureHours 小时"
} else {
val days = futureHours / 24
"未来 $days"
}
}
// 过去时间
val hours = diffMillis / (1000 * 60 * 60)
return when {
hours < 1 -> "刚刚"
hours < 24 -> "${hours}小时前"
// hours < 48 -> "昨天"
// hours < 72 -> "前天"
else -> {
val days = hours / 24
"${days}天前"
}
}
}
} }
@@ -0,0 +1,141 @@
package com.sw.platecabinet.utils
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
class FragmentHelper(
private val fragmentManager: FragmentManager,
private val containerId: Int
) {
/**
* 添加Fragment到容器中
* @param fragment 要添加的Fragment
* @param addToBackStack 是否添加到返回栈
* @param tag Fragment的标签
*/
fun addFragment(fragment: Fragment, addToBackStack: Boolean = false, tag: String? = null) {
fragmentManager.beginTransaction().apply {
add(containerId, fragment, tag)
if (addToBackStack) {
addToBackStack(tag)
}
commit()
}
}
/**
* 替换容器中的Fragment
* @param fragment 要替换的Fragment
* @param addToBackStack 是否添加到返回栈
* @param tag Fragment的标签
* @param animEnter 进入动画
* @param animExit 退出动画
* @param animPopEnter 弹出进入动画
* @param animPopExit 弹出退出动画
*/
fun replaceFragment(
fragment: Fragment,
addToBackStack: Boolean = true,
tag: String? = null,
animEnter: Int = 0,
animExit: Int = 0,
animPopEnter: Int = 0,
animPopExit: Int = 0
) {
fragmentManager.beginTransaction().apply {
if (animEnter != 0 || animExit != 0 || animPopEnter != 0 || animPopExit != 0) {
setCustomAnimations(animEnter, animExit, animPopEnter, animPopExit)
}
replace(containerId, fragment, tag)
if (addToBackStack) {
addToBackStack(tag)
}
commit()
}
}
/**
* 显示Fragment
* @param fragment 要显示的Fragment
*/
fun showFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.show(fragment)
.commit()
}
/**
* 隐藏Fragment
* @param fragment 要隐藏的Fragment
*/
fun hideFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.hide(fragment)
.commit()
}
/**
* 移除Fragment
* @param fragment 要移除的Fragment
*/
fun removeFragment(fragment: Fragment) {
fragmentManager.beginTransaction()
.remove(fragment)
.commit()
}
/**
* 返回上一个Fragment
* @return 是否成功返回
*/
fun popBackStack(): Boolean {
return fragmentManager.popBackStackImmediate()
}
/**
* 返回到指定的Fragment
* @param tag Fragment的标签
* @param inclusive 是否包含指定的Fragment
*/
fun popBackStackTo(tag: String, inclusive: Boolean = false): Boolean {
return fragmentManager.popBackStackImmediate(
tag,
if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0
)
}
/**
* 获取当前显示的Fragment
*/
fun getCurrentFragment(): Fragment? {
return fragmentManager.findFragmentById(containerId)
}
/**
* 通过tag查找Fragment
*/
fun findFragmentByTag(tag: String): Fragment? {
return fragmentManager.findFragmentByTag(tag)
}
/**
* 清除所有Fragment
*/
fun clearAllFragments() {
// 清除所有回退栈中的Fragment
fragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
// 清除所有已添加的Fragment
fragmentManager.fragments.forEach { fragment ->
removeFragment(fragment)
}
}
/**
* 检查返回栈是否为空
*/
fun isBackStackEmpty(): Boolean {
return fragmentManager.backStackEntryCount == 0
}
}
@@ -0,0 +1,156 @@
package com.sw.platecabinet.utils
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
class PermissionHelper private constructor(
private val context: Context,
private val permissions: Array<String>,
private val requestCode: Int,
private val rationale: String? = null
) {
private var onGranted: (() -> Unit)? = null
companion object {
/**
* 创建权限请求构建器
* @param context 上下文(Activity或Fragment
* @param permissions 需要请求的权限数组
* @param requestCode 请求码
*/
fun with(context: Context, permissions: Array<String>, requestCode: Int): Builder {
return Builder(context, permissions, requestCode)
}
/**
* 检查是否已授予所有权限
*/
fun areAllPermissionsGranted(context: Context, permissions: Array<String>): Boolean {
return permissions.all { permission ->
ContextCompat.checkSelfPermission(
context,
permission
) == PackageManager.PERMISSION_GRANTED
}
}
/**
* 处理权限请求结果(供BaseActivity/BaseFragment调用)
*/
fun handlePermissionResult(
context: Context,
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray,
onPermissionPermanentlyDenied: (List<String>) -> Unit
): Boolean {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
return true
}
val permanentlyDeniedPermissions = mutableListOf<String>()
permissions.forEachIndexed { index, permission ->
if (grantResults[index] != PackageManager.PERMISSION_GRANTED) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(
context as Activity,
permission
)
) {
permanentlyDeniedPermissions.add(permission)
}
}
}
if (permanentlyDeniedPermissions.isNotEmpty()) {
onPermissionPermanentlyDenied(permanentlyDeniedPermissions)
}
return false
}
/**
* 跳转到应用设置页面
*/
fun openAppSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
if (context is Activity) {
context.startActivity(intent)
} else if (context is Fragment) {
context.startActivity(intent)
}
}
}
class Builder(
private val context: Context,
private val permissions: Array<String>,
private val requestCode: Int
) {
private var rationale: String? = null
/**
* 设置权限说明(当需要向用户解释权限用途时显示)
*/
fun setRationale(rationale: String): Builder {
this.rationale = rationale
return this
}
/**
* 设置权限授予回调
*/
fun onGranted(callback: () -> Unit): PermissionHelper {
val helper = PermissionHelper(context, permissions, requestCode, rationale)
helper.onGranted = callback
return helper
}
}
/**
* 检查并请求权限
*/
fun checkAndRequest() {
if (areAllPermissionsGranted(context, permissions)) {
onGranted?.invoke()
} else {
requestPermissions()
}
}
private fun requestPermissions() {
val activity = context as? Activity ?: return
// 检查是否需要显示权限说明
val shouldShowRationale = permissions.any { permission ->
ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)
}
if (shouldShowRationale && rationale != null) {
// 显示解释对话框
AlertDialog.Builder(activity)
.setTitle("权限说明")
.setMessage(rationale)
.setPositiveButton("确定") { _, _ ->
doRequestPermissions(activity)
}
.setNegativeButton("取消", null)
.show()
} else {
// 直接请求权限
doRequestPermissions(activity)
}
}
private fun doRequestPermissions(activity: Activity) {
ActivityCompat.requestPermissions(activity, permissions, requestCode)
}
}
@@ -0,0 +1,88 @@
package com.sw.platecabinet.view;
import android.app.Dialog;
import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.Gravity;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
public class CustomDialog extends Dialog {
/**
* 宽高由布局文件中指定(但是最底层的宽度无效,可以多嵌套一层解决)
*/
public CustomDialog(Context context, View layout, int style) {
super(context, style);
setContentView(layout);
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
params.gravity = Gravity.CENTER;
window.setAttributes(params);
}
/**
* 宽高由该方法的参数设置
*/
public CustomDialog(Context context, int width, int height, View layout,
int style) {
super(context, style);
// 设置内容
setContentView(layout);
// 设置窗口属性
Window window = getWindow();
WindowManager.LayoutParams params = window.getAttributes();
// 设置宽度、高度、密度、对齐方式
float density = getDensity(context);
params.width = (int) (width * density);
params.height = (int) (height * density);
params.gravity = Gravity.CENTER;
window.setAttributes(params);
}
/**
* 获取显示密度
*
* @param context
* @return
*/
public float getDensity(Context context) {
Resources res = context.getResources();
DisplayMetrics dm = res.getDisplayMetrics();
return dm.density;
}
private void fullScreenImmersive(View view) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int uiOptions = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_FULLSCREEN;
view.setSystemUiVisibility(uiOptions);
}
}
@Override
public void show() {
this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
super.show();
// fullScreenImmersive(getWindow().getDecorView());
// this.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
}
@@ -0,0 +1,65 @@
package com.sw.platecabinet.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sw.plate.utils.ToastUtils
import com.sw.platecabinet.model.response.ApiResponse
import com.sw.platecabinet.network.ApiClient
import com.sw.platecabinet.repository.RemoteRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
abstract class BaseViewModel() : ViewModel() {
protected val repository = RemoteRepository(ApiClient.apiService)
// 显示进度条
private val _showLoading = MutableStateFlow<Boolean>(false)
val showLoading: StateFlow<Boolean> = _showLoading
/**
* 带进度条的请求
*/
protected fun launchWithLoading(block: suspend () -> Unit) {
viewModelScope.launch {
try {
_showLoading.value = true
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
} finally {
_showLoading.value = false
}
}
}
/**
* 不带进度条的请求
*/
protected fun launch(block: suspend () -> Unit) {
viewModelScope.launch() {
try {
block()
} catch (e: Exception) {
// 错误处理可被子类重写
handleError(e)
}
}
}
protected open fun parseResponse(response: ApiResponse<*>): Boolean {
if (response.isSuccess()) {
return true
}
Timber.d("msg = ${response.msg}, code = ${response.code}")
ToastUtils.showToast("${response.msg}(${response.code})")
return false
}
fun handleError(exception: Exception) {
Timber.d("handleError ${exception.message}")
}
}
@@ -0,0 +1,21 @@
package com.sw.platecabinet.viewmodel
import com.sw.platecabinet.model.request.EquipmentParam
import com.sw.platecabinet.model.response.EquipmentUserInfo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class SettingViewModel : BaseViewModel() {
// 设备绑定列表
private val _equipmentList = MutableStateFlow<List<EquipmentUserInfo>>(emptyList())
val equipmentList: StateFlow<List<EquipmentUserInfo>> = _equipmentList
fun getEquipmentList(param: EquipmentParam) {
launchWithLoading {
val response = repository.getEquipmentList(param)
if (parseResponse(response)) {
_equipmentList.value = response.data ?: emptyList()
}
}
}
}
@@ -0,0 +1,43 @@
package com.sw.platecabinet.viewmodel
import com.sw.inbound.utils.SPUtil
import com.sw.platecabinet.GlobalKey
class UserViewModel : BaseViewModel() {
fun generateToken(deviceId: String = "SWSN:88:12:AC:4E:D9:CC") {
launchWithLoading {
val response = repository.generateToken(deviceId)
if (parseResponse(response)) {
// 缓存token
SPUtil.getInstance().put(GlobalKey.KEY_TOKEN, response.data)
// 首次运行获取人脸数据
if (SPUtil.getInstance().get(GlobalKey.KEY_FIRST_RUN, false) != true) {
getUserFaceCache()
}
}
}
}
fun getUserFaceCache() {
launchWithLoading {
val response = repository.getUserFaceCache()
if (parseResponse(response)) {
// 获取成功一次后缓存状态
SPUtil.getInstance().put(GlobalKey.KEY_FIRST_RUN, true)
}
}
}
/**
* 激活 arcsoft 人脸
*/
fun activeEngine(
appId: String = "Hkz1rBk6PZXbS8KwKr67K2eZtsz8bRoMHLg64bUdgCZj",
sdkKey: String = "AabXs3sHM8UhhE7oCGf4LVMLrdkGb1nJNksbjjTdVt7k",
activeKey: String = "085F-118G-Q391-53YL"
) {
// val runtimeABI: RuntimeABI? = FaceEngine.getRuntimeABI()
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="5dp" />
<solid android:color="@color/black" />
</shape>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="6dp" />
<solid android:color="#3C3D45" />
</shape>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="6dp" />
<solid android:color="#F0C8B4" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#282931" />
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
<stroke
android:width="2dp"
android:color="#34363E" />
</shape>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#ff3b3638" />
<corners
android:bottomLeftRadius="12dp"
android:bottomRightRadius="12dp"
android:topLeftRadius="12dp"
android:topRightRadius="12dp" />
<stroke
android:width="2dp"
android:color="#907C74" />
</shape>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/transparent" />
<!-- 边框设置 -->
<stroke
android:width="2dp"
android:color="#8c7c75" />
<!-- 圆角半径 -->
<corners android:radius="12dp" />
</shape>
@@ -2,7 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@mipmap/bg" android:background="#14141E"
android:orientation="vertical"> android:orientation="vertical">
<include <include
@@ -4,7 +4,7 @@
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@mipmap/bg" android:background="#14141E"
android:orientation="vertical" android:orientation="vertical"
tools:context=".activity.LoginByFaceActivity"> tools:context=".activity.LoginByFaceActivity">
@@ -36,6 +36,7 @@
<androidx.camera.view.PreviewView <androidx.camera.view.PreviewView
android:id="@+id/previewView" android:id="@+id/previewView"
android:layout_margin="10dp"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
+10 -2
View File
@@ -1,12 +1,20 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main" android:id="@+id/main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"
android:background="#14141E"
tools:context=".activity.MainActivity"> tools:context=".activity.MainActivity">
<include <include
android:id="@+id/includeHeader" android:id="@+id/includeHeader"
layout="@layout/item_title_time" /> layout="@layout/item_title_time" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/parentRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</LinearLayout>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="240dp"
android:layout_height="80dp"
android:background="@drawable/dialog_waiting">
<TextView
android:id="@+id/tvTip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textColor="@color/white"
android:textSize="22sp" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginRight="30dp"
android:layout_toLeftOf="@id/tvTip"
android:indeterminateTint="@color/teal_200"
android:indeterminateTintMode="src_atop" />
</RelativeLayout>
</FrameLayout>
@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingHorizontal="60dp"
tools:context=".fragment.BindPlateFragment">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="80dp"
android:text="柜体编号"
android:textColor="#ff7c6d6a"
android:textSize="20sp" />
<TextView
android:id="@+id/tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="23dp"
android:text="1-12"
android:textColor="#fff0c8b4"
android:textSize="36sp"
android:textStyle="bold" />
<ImageView
android:layout_width="280dp"
android:layout_height="24dp"
android:layout_marginTop="24dp"
android:src="@mipmap/ic_bind_img" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="54dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="餐盘码"
android:textColor="#fff0c8b4"
android:textSize="26sp" />
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="确认餐盘码是否有误"
android:textColor="#ff7c6d6a"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="23dp"
android:background="@drawable/shape_pwd_bg"
android:gravity="center_vertical">
<EditText
android:id="@+id/et_code"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent"
android:gravity="center"
android:textColor="#fff0c8b4"
android:textSize="26sp"
android:textStyle="bold"
tools:text="100888" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="会员"
android:textColor="#fff0c8b4"
android:textSize="26sp" />
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="仅限VIP会员绑定,非VIP暂不支持"
android:textColor="#ff7c6d6a"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="23dp"
android:background="@drawable/shape_pwd_bg"
android:gravity="center_vertical">
<EditText
android:id="@+id/et_user_info"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/transparent"
android:gravity="center"
android:hint="请输入会员姓名或手机号"
android:textColor="#F0C8B4"
android:textColorHint="#ff7c6d6a"
android:textSize="26sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="188dp"
android:layout_marginTop="24dp" />
<LinearLayout
android:id="@+id/ll_bind"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="28dp"
android:background="@drawable/shape_btn_bg"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="绑定开门"
android:textColor="#ff11111c"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/tv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:paddingHorizontal="20dp"
android:text="返回"
android:textColor="#ff887872"
android:textSize="26sp" />
</LinearLayout>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
tools:context=".fragment.PlateCabinetFullFragment">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/ic_full" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:text="餐盘柜已满"
android:textColor="#fff0c8b4"
android:textSize="36sp" />
<TextView
android:id="@+id/tv_auto_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="3秒后返回主屏"
android:textColor="#ff887872"
android:textSize="20sp" />
</LinearLayout>
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<FrameLayout
android:layout_width="380dp"
android:layout_height="380dp"
android:layout_marginTop="119dp"
android:padding="10dp"
android:visibility="visible">
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="0dp"
android:background="@drawable/circle_dashed_border" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<ImageView
android:layout_width="60dp"
android:layout_height="60dp"
android:src="@mipmap/ic_plate_open" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="柜门开启"
android:textColor="#ff02f1be"
android:textSize="36sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="23dp"
android:text="餐盘取出后请关闭柜门"
android:textColor="#ff887872"
android:textSize="24sp" />
<TextView
android:id="@+id/tv_auto_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="34dp"
android:text="3"
android:textColor="#fff0c8b4"
android:textSize="36sp" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".fragment.SettingListFragment">
<include
android:id="@+id/includeHeader"
layout="@layout/item_title_time" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="6dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mainRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingHorizontal="60dp"
tools:context=".fragment.BindPlateFragment">
<ImageView
android:layout_width="280dp"
android:layout_height="24dp"
android:layout_marginTop="92dp"
android:src="@mipmap/ic_unbind_img" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:text="柜体编号"
android:textColor="#ff7c6d6a"
android:textSize="20sp" />
<TextView
android:id="@+id/tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="23dp"
android:text="1-12"
android:textStyle="bold"
android:textColor="#fff0c8b4"
android:textSize="36sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="328dp"
android:layout_marginTop="48dp"
android:background="@drawable/rounded_border_transparent_8c7c75"
android:orientation="vertical"
android:paddingHorizontal="30dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_plate_number_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="餐盘码"
android:textColor="#ff7c6d6a"
android:textSize="24sp" />
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_plate_number_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="100888"
android:textColor="#fff0c8b4"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#3C363A" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_name_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="姓名"
android:textColor="#ff7c6d6a"
android:textSize="24sp" />
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_name_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="曹*磊"
android:textColor="#fff0c8b4"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#3C363A" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_phone_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="手机号"
android:textColor="#ff7c6d6a"
android:textSize="24sp" />
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_phone_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="138****8000"
android:textColor="#fff0c8b4"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#3C363A" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="80dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_update_time_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="最近使用"
android:textColor="#ff7c6d6a"
android:textSize="24sp" />
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_update_time_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="14天前使用"
android:textColor="#fff0c8b4"
android:textSize="24sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_unbind"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_marginTop="187dp"
android:background="@drawable/shape_btn_bg"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="解除绑定"
android:textColor="#ff11111c"
android:textSize="30sp"
android:textStyle="bold" />
</LinearLayout>
<TextView
android:id="@+id/tv_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:paddingHorizontal="20dp"
android:text="返回"
android:textColor="#ff887872"
android:textSize="26sp" />
</LinearLayout>
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="75dp"
android:id="@+id/ll_root"
android:background="@drawable/grid_item_unbind"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="6dp"
android:paddingEnd="6dp"
android:paddingBottom="6dp">
<TextView
android:id="@+id/tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="02"
android:textColor="#fff0c8b4"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_unbind"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_weight="1"
android:text="未绑定"
android:visibility="gone"
android:textColor="#fff0c8b4"
android:textSize="18sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/ll_bind_info"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="马*飞(8000)"
android:textColor="#585868"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_last_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="9小时前使用"
android:textColor="#585868"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_open"
android:layout_width="60dp"
android:layout_height="63dp"
android:background="@drawable/grid_button_unbind"
android:gravity="center">
<TextView
android:id="@+id/tv_open"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开启"
android:textColor="#ff32283c"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/grid_item_bind"
android:padding="18dp"
android:id="@+id/ll_root"
android:orientation="vertical">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="马*飞(8000)"
android:textColor="#fff0c8b4"
android:textSize="26sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="138****8000"
android:textColor="#ff7c6d6a"
android:textSize="20sp"
android:textStyle="bold" />
</LinearLayout>
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="282dp"
android:layout_height="75dp"
android:id="@+id/ll_root"
android:background="@drawable/grid_item_unbind"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingTop="6dp"
android:paddingEnd="6dp"
android:paddingBottom="6dp">
<TextView
android:id="@+id/tv_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="02"
android:textColor="#fff0c8b4"
android:textSize="24sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_unbind"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_weight="1"
android:text="未绑定"
android:visibility="gone"
android:textColor="#fff0c8b4"
android:textSize="18sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/ll_bind"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="马*飞(8000)"
android:textColor="#585868"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_last_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:text="9小时前使用"
android:textColor="#585868"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<LinearLayout
android:id="@+id/tv_open"
android:layout_width="60dp"
android:layout_height="63dp"
android:background="@drawable/grid_button_unbind"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开启"
android:textColor="#ff32283c"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/childRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

+7
View File
@@ -3,4 +3,11 @@
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="transparent">#00000000</color> <color name="transparent">#00000000</color>
<color name="teal_200">#FF03DAC5</color>
<color name="bind_4E535D">#4E535D</color>
<color name="bind_585868">#585868</color>
<color name="bind_time_old">#FF9632</color>
<color name="bind_F0C8B4">#F0C8B4</color>
<color name="unbind_F0C8B4">#F0C8B4</color>
<color name="unbind_32283C">#32283C</color>
</resources> </resources>
+2
View File
@@ -1,3 +1,5 @@
<resources> <resources>
<string name="app_name">SmartPlateCabinet</string> <string name="app_name">SmartPlateCabinet</string>
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
</resources> </resources>
+19 -3
View File
@@ -1,9 +1,25 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources>
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Base.Theme.SmartPlateCabinet" parent="Theme.Material3.DayNight.NoActionBar"> <style name="Base.Theme.SmartPlateCabinet" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. --> <item name="android:windowFullscreen">true</item>
<!-- <item name="colorPrimary">@color/my_light_primary</item> --> <item name="android:windowTranslucentStatus">false</item>
<item name="android:windowTranslucentNavigation">false</item>
</style> </style>
<style name="Theme.SmartPlateCabinet" parent="Base.Theme.SmartPlateCabinet" /> <style name="Theme.SmartPlateCabinet" parent="Base.Theme.SmartPlateCabinet" />
<!--圆角Dialog-->
<style name="MyDialog" parent="@android:style/Theme.Dialog">
<item name="android:windowFrame">@null</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:background">@android:color/transparent</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:backgroundDimEnabled">true</item>
<item name="android:backgroundDimAmount">0.6</item>
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
</style>
</resources> </resources>
+4 -1
View File
@@ -1,5 +1,7 @@
[versions] [versions]
activityKtx = "1.9.0"
agp = "8.10.1" agp = "8.10.1"
fragmentKtx = "1.5.6"
kotlin = "2.0.21" kotlin = "2.0.21"
coreKtx = "1.10.1" coreKtx = "1.10.1"
junit = "4.13.2" junit = "4.13.2"
@@ -22,7 +24,9 @@ gson = "2.13.1"
hiltAndroid = "2.56.2" hiltAndroid = "2.56.2"
[libraries] [libraries]
androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtx" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -34,7 +38,6 @@ material = { group = "com.google.android.material", name = "material", version.r
androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistPermissions" }
androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" } androidx-camera-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "cameraCore" }
androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraCore" } androidx-camera-core = { module = "androidx.camera:camera-core", version.ref = "cameraCore" }
androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraCore" } androidx-camera-extensions = { module = "androidx.camera:camera-extensions", version.ref = "cameraCore" }
@@ -0,0 +1,167 @@
package com.sw.plate.utils;
import static android.content.Context.INPUT_SERVICE;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Handler;
import android.text.TextUtils;
import android.view.KeyEvent;
/**
* 扫描枪事件处理
*/
public class ScanGunKeyEventHelper {
private final static long MESSAGE_DELAY = 500; //延迟500ms,判断扫码是否完成。
private final StringBuffer mStringBufferResult; //扫码内容
private boolean mCaps; //大小写区分
private final Handler mHandler;
private final Runnable mScanningFishedRunnable;
private OnScanSuccessListener mOnScanSuccessListener;
private final Context mContext;
// private String mDeviceName = "TMC HIDKeyBoard";
private String mDeviceName = "Linux 3.4.35 with ak-hsudc Composite Gadget (ACM + HID)";
public ScanGunKeyEventHelper(Context context, OnScanSuccessListener onScanSuccessListener) {
mContext = context;
mOnScanSuccessListener = onScanSuccessListener;
mStringBufferResult = new StringBuffer();
mHandler = new Handler();
mScanningFishedRunnable = this::performScanSuccess;
}
/**
* 返回扫码成功后的结果
*/
private void performScanSuccess() {
String barcode = mStringBufferResult.toString();
if (mOnScanSuccessListener != null && !TextUtils.isEmpty(barcode))
mOnScanSuccessListener.onScanSuccess(barcode);
mStringBufferResult.setLength(0);
}
/**
* 扫码枪事件解析
*
* @param event
*/
public void analysisKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
//字母大小写判断
checkLetterStatus(event);
if (event.getAction() == KeyEvent.ACTION_DOWN) {
char aChar = getInputCode(event);
if (aChar != 0) {
mStringBufferResult.append(aChar);
}
if (keyCode == KeyEvent.KEYCODE_ENTER) {
//若为回车键,直接返回
mHandler.removeCallbacks(mScanningFishedRunnable);
mHandler.post(mScanningFishedRunnable);
} else {
//延迟post,若500ms内,有其他事件
mHandler.removeCallbacks(mScanningFishedRunnable);
mHandler.postDelayed(mScanningFishedRunnable, MESSAGE_DELAY);
}
}
}
//检查shift键
private void checkLetterStatus(KeyEvent event) {
int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT || keyCode == KeyEvent.KEYCODE_SHIFT_LEFT) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
//按着shift键,表示大写
mCaps = true;
} else {
//松开shift键,表示小写
mCaps = false;
}
}
}
/**
* 获取扫描内容
*
* @param event
* @return
*/
private char getInputCode(KeyEvent event) {
int keyCode = event.getKeyCode();
char aChar;
if (keyCode >= KeyEvent.KEYCODE_A && keyCode <= KeyEvent.KEYCODE_Z) {
//字母
aChar = (char) ((mCaps ? 'A' : 'a') + keyCode - KeyEvent.KEYCODE_A);
} else if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
//数字
aChar = (char) ('0' + keyCode - KeyEvent.KEYCODE_0);
} else {
//其他符号
switch (keyCode) {
case KeyEvent.KEYCODE_PERIOD:
aChar = '.';
break;
case KeyEvent.KEYCODE_MINUS:
aChar = mCaps ? '_' : '-';
break;
case KeyEvent.KEYCODE_SLASH:
aChar = '/';
break;
case KeyEvent.KEYCODE_BACKSLASH:
aChar = mCaps ? '|' : '\\';
break;
default:
aChar = 0;
break;
}
}
return aChar;
}
public interface OnScanSuccessListener {
void onScanSuccess(String barcode);
}
public void onDestroy() {
mHandler.removeCallbacks(mScanningFishedRunnable);
mOnScanSuccessListener = null;
}
/**
* 输入设备是否存在
*
* @param deviceName
* @return
*/
public boolean isInputDeviceExist(String deviceName) {
InputManager inputManager = (InputManager) mContext.getSystemService(INPUT_SERVICE);
int[] deviceIds = inputManager.getInputDeviceIds();
for (int id : deviceIds) {
if (inputManager.getInputDevice(id).getName().equals(deviceName)) {
return true;
}
}
return false;
}
/**
* 是否为扫码枪事件(部分机型KeyEvent获取的名字错误)
*
* @param event
* @return
*/
public boolean isScanGunEvent(KeyEvent event) {
// L.e("event===" + event.getDevice().getName() +
// "===Char===" + event.getCharacters() +
// "===Action===" + event.getAction());
// return event.getDevice().getName().equals(mDeviceName);
return true;
}
}
@@ -0,0 +1,105 @@
package com.sw.plate.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.sw.plate.App;
import com.sw.plate.R;
import java.util.Timer;
import java.util.TimerTask;
/**
* The type Toast utils.
*/
public class ToastUtils {
private static Toast toast;
@SuppressLint("StaticFieldLeak")
private static TextView textCenterView;
/**
* Show center toast.
*
* @param text the text
*/
public static void showToast(String text) {
Context context = App.getContext();
if (toast == null) {
View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
textCenterView = view.findViewById(R.id.toast_tv);
toast = new Toast(context);
toast.setGravity(Gravity.CENTER, 0, 20);
toast.setDuration(Toast.LENGTH_SHORT);
toast.setView(view);
}
textCenterView.setText(text);
toast.show();
}
/**
* Show center toast.
*
* @param text the text
*/
public static void showToast(String text, int duration) {
Context context = App.getContext();
if (toast == null) {
View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
textCenterView = view.findViewById(R.id.toast_tv);
toast = new Toast(context);
toast.setGravity(Gravity.CENTER, 0, 20);
toast.setView(view);
}
textCenterView.setText(text);
toast.setDuration(duration);
toast.show();
}
public static void showLongToast(String text) {
Context context = App.getContext();
if (toast == null) {
View view = LayoutInflater.from(context).inflate(R.layout.toast_bg, null);
textCenterView = view.findViewById(R.id.toast_tv);
toast = new Toast(context);
toast.setGravity(Gravity.TOP, 0, 20);
toast.setDuration(Toast.LENGTH_LONG);
toast.setView(view);
}
textCenterView.setText(text);
showMyToast(toast, 1000000 * 30);
}
//自定义Toast控件
private static void showMyToast(final Toast toast, final int cnt) {
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
toast.show();
}
}, 0, Toast.LENGTH_LONG);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
toast.cancel();
timer.cancel();
}
}, cnt);
}
public static void showSystemToast(String text) {
Context context = App.getContext();
Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
}
}
@@ -0,0 +1,79 @@
package com.sw.plate.utils.arcface;
import android.content.Context;
import com.arcsoft.face.FaceEngine;
import com.arcsoft.face.enums.RuntimeABI;
import com.sw.plate.App;
import com.sw.plate.utils.L;
import com.sw.plate.utils.arcface.face.model.FacePreviewInfo;
import com.sw.plate.utils.arcface.facedb.FaceDatabase;
import com.sw.plate.utils.arcface.facedb.entity.FaceEntity;
import java.util.ArrayList;
import java.util.List;
public class FaceApi {
public interface ActiveCallback {
void onSuccess(int code);
void onFail(Exception e);
}
/**
* 更新人脸数据
*
* @param index
* @param list
*/
public void updateFaceData(int index, List<FaceEntity> list) {
if (index == 0) {
FaceDatabase.getInstance(App.getContext()).faceDao().deleteAll();
FaceDatabase.getInstance(App.getContext()).faceDao().resetId();
}
List<FaceEntity> faceEntities = new ArrayList<>();
FaceDatabase.getInstance(App.getContext()).faceDao().insert(faceEntities);
}
/**
* 激活arcsoft 人脸
*
* @param context
* @param arcsoftAppId
* @param arcsoftSdkKey
* @param arcsoftActiveKey
* @param callback
*/
public void activeEngine(Context context, String arcsoftAppId,
String arcsoftSdkKey,
String arcsoftActiveKey, ActiveCallback callback) {
new Thread(() -> {
try {
RuntimeABI runtimeABI = FaceEngine.getRuntimeABI();
L.e("subscribe: getRuntimeABI() " + runtimeABI);
long start = System.currentTimeMillis();
int activeCode = FaceEngine.activeOnline(context, arcsoftActiveKey,
arcsoftAppId, arcsoftSdkKey);
L.e("subscribe cost: " + (System.currentTimeMillis() - start));
callback.onSuccess(activeCode);
} catch (Exception e) {
callback.onFail(e);
}
}).start();
}
/**
* 传入可见光相机预览数据
*
* @param nv21 可见光相机预览数据
* @param doRecognize 是否进行识别
* @return 当前帧的检测结果信息
*/
public List<FacePreviewInfo> onPreviewFrame(byte[] nv21, boolean doRecognize) {
// List<FacePreviewInfo> facePreviewInfoList = recognizeViewModel.onPreviewFrame(nv21, true);
return null;
}
}
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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"
android:orientation="vertical"
app:cardCornerRadius="5dp"
app:cardPreventCornerOverlap="true">
<TextView
android:id="@+id/toast_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:maxLines="1"
android:paddingStart="15dp"
android:paddingTop="5dp"
android:paddingEnd="15dp"
android:paddingBottom="5dp"
android:textColor="@color/black"
android:textSize="28sp" />
</com.google.android.material.card.MaterialCardView>
+1
View File
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="color_bg_notification">#80000000</color> <color name="color_bg_notification">#80000000</color>
<color name="black">#000000</color>
</resources> </resources>