赞
踩
正如你们看到的,这个Demo的功能是我们可以从手机里或者是拍照的方式获取到某张图片,然后经过OCR识别文字后,将识别出来的文字在图片上全部都框选出来,并且在底部以扩展界面的方式可以查看识别内容的列表,点击列表里的某一项识别项就会在图片上选中这一项识别项,反过来点击图片上的框选的识别项也会在列表中进行这一对应项的选中。
想要实现上面的这种效果,我们需要解决这几个技术点
让我们一步一步来实现并且一步步的解决这些问题
首先我们为应用创建一个带BottomSheet的布局,实现main页面布局
activity_main.xml中
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/coordinator_Layout" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_height="match_parent" android:layout_width="match_parent" android:background="@color/white"> <io.github.karl.ocrdemo.OcrImageView android:id="@+id/image_preview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center|top" android:scaleType="fitCenter" tools:src="@tools:sample/backgrounds/scenic" /> <LinearLayout android:id="@+id/custom_bottom_sheet" android:layout_width="match_parent" android:layout_height="210dp" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" app:layout_anchorGravity="bottom|end" app:behavior_peekHeight="50dp" app:behavior_hideable="false" android:background="@drawable/bottom_sheet_layout_shape" android:paddingStart="6dp" android:paddingEnd="6dp" android:orientation="vertical"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/image_view" android:layout_width="match_parent" android:layout_height="50dp" android:layout_marginTop="2dp" android:scaleType="center" android:src="@mipmap/round_bar_icon" app:layout_constraintTop_toTopOf="parent" android:clickable="true" android:focusable="true"/> <ImageView android:id="@+id/open_take_pic" android:layout_width="30dp" android:layout_height="30dp" android:alpha="0.25" android:src="@mipmap/icon_takepic" android:scaleType="fitCenter" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:tint="@color/black" /> <ImageView android:id="@+id/open_select_image" android:layout_width="20dp" android:layout_height="20dp" android:layout_marginEnd="10dp" android:alpha="0.25" android:src="@mipmap/picture" android:scaleType="fitCenter" app:layout_constraintRight_toLeftOf="@id/open_take_pic" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:tint="@color/black" /> </androidx.constraintlayout.widget.ConstraintLayout> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycle_view" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/image_view" android:nestedScrollingEnabled="true" /> </LinearLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
这边的页面中,为了使用BottomSheet(对ButtonSheet不熟悉的可以看BottomSheet的使用),
这边我们要注意,我们的BottomSheet即我们的LinearLayout可以给其一个高度,不然图片都被其展开的时候都遮住了也就没什么意义了
需要说明的是,这边我使用的数据是通过服务端接口返回的,这个接口是内部使用的,而我这边为了方便起见,拿来借用了一下,下面会贴出来其数据格式,数据格式字段都很清晰一看就知道了,这边的数据只是图个方便,当然OCR识别这块的API也有很多厂商都提供的,所以数据格式的话,需要自行解析,我这边就写死一个数据内容来模拟我从我们内部使用的接口获取的数据来进行说明,数据如下:
private val mockResponseJsonStr = """ { "errId": 0, "errMsg": "", "result": [{ "boxes": [[71, 1798],[264, 1806],[262, 1854],[69, 1846]], "text": "Authority", "score": 0.9999019 }, { "boxes": [[75, 1742],[716, 1748],[714, 1794],[73, 1788]], "text": "Government Root Certification", "score": 0.9929104 }, { "boxes": [[77, 1552],[714, 1552],[714, 1586],[77, 1586]], "text": "Go Daddy Root Certificate Authority-G2", "score": 0.99476784 }, { "boxes": [[79, 1484],[470, 1490],[468, 1536],[77, 1530]], "text": "saacammonne", "score": 0.55913043 }, { "boxes": [[71, 1284],[387, 1288],[385, 1328],[69, 1324]], "text": "GlobalSign Root CA", "score": 0.9982361 }, { "boxes": [[73, 1222],[432, 1230],[430, 1282],[71, 1274]], "text": "GlobalSign nv-sa", "score": 0.9990079 }, { "boxes": [[73, 1026],[250, 1034],[248, 1074],[71, 1066]], "text": "GlobalSign", "score": 0.99623775 }, { "boxes": [[73, 966],[301, 974],[299, 1022],[71, 1014]], "text": "GlobalSign", "score": 0.98376197 }, { "boxes": [[73, 768],[250, 776],[248, 816],[71, 808]], "text": "Gamasaspe", "score": 0.59074664 }, { "boxes": [[73, 704],[305, 714],[303, 768],[71, 758]], "text": "GlobalSign", "score": 0.9983882 }, { "boxes": [[71, 510],[252, 518],[250, 558],[69, 550]], "text": "GlobalSign", "score": 0.99850523 }, { "boxes": [[73, 444],[305, 454],[303, 508],[71, 498]], "text": "swdsasignGd", "score": 0.6600375 }, { "boxes": [[665, 232],[752, 232],[752, 286],[665, 286]], "text": "电电电", "score": 0.68177694 }, { "boxes": [[329, 230],[418, 230],[418, 284],[329, 284]], "text": "杂东景", "score": 0.28567907 }, { "boxes": [[73, 122],[450, 122],[450, 176],[73, 176]], "text": "个一信任的证书", "score": 0.9976823 }, { "boxes": [[889, 32],[1038, 28],[1040, 76],[891, 80]], "text": "物电区A", "score": 0.0875141 }, { "boxes": [[41, 34],[297, 28],[299, 74],[43, 80]], "text": "17:030Ri08", "score": 0.73744255 }] } """.trimIndent()
这个数据对应的图片是这张,接下来我都会使用这张图片以及这个数据进行说明演示
()
原图大小1080*1920,boxes中存放了对应于上传解析的原图的OCR识别的坐标点信息,是一个闭合的四边形,4个数组分别是四边形左上,右上,右下,左下四个角的坐标点,每个坐标点数组中的2个值代表了其X轴和Y轴坐标,text为识别出来的文本内容,score为识别可行度评分
数据有了,下来我们就来实现代码
在mainActivity中,我们初始化我们的BottomSheet控件,进行一些设置
private lateinit var behavior: BottomSheetBehavior<View> //获取状态栏的高度 private fun getStatusBarHeight(activity: Activity): Int { val resourceId = activity.resources.getIdentifier( "status_bar_height", "dimen", "android" ) return if (resourceId > 0) { activity.resources.getDimensionPixelSize(resourceId) } else 0 } private val displayWidth: Int by lazy { resources.displayMetrics.widthPixels } private val displayHeight: Int by lazy { resources.displayMetrics.heightPixels - behavior.peekHeight - getStatusBarHeight(this) } override fun onCreate(savedInstanceState: Bundle?) { ... behavior = BottomSheetBehavior.from(findViewById(R.id.custom_bottom_sheet)) behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { } override fun onSlide(bottomSheet: View, slideOffset: Float) { val bottomSheetHeightOffset = (bottomSheet.height - behavior.peekHeight) * slideOffset val layoutParams = image_preview.layoutParams val height = displayHeight layoutParams.height = (height - bottomSheetHeightOffset).toInt() image_preview.layoutParams = layoutParams } }) ... }
我们为BottomSheet设置了一个callback监听,监听其折叠和展开的事件,我们在onSlide中得到其slideOffset的值(关于这个值的详细描述可以看BottomSheet的使用),并根据这个值计算出我们的图片的高度要如何调整,首先 B o t t o m S h e e t 高度偏移量 = ( b o t t o m S h e e t 的总高度 − b o t t o m S h e e t 的 p e e k H e i g h t 折叠高度) ∗ o f f s e t 比例值 BottomSheet高度偏移量 = (bottomSheet的总高度 - bottomSheet的peekHeight折叠高度) * offset比例值 BottomSheet高度偏移量=(bottomSheet的总高度−bottomSheet的peekHeight折叠高度)∗offset比例值,算出高度偏移量后,我们改变ImageView的高度值,使其等于 I m a g e V i e w 控件高度 − B o t t o m S h e e t 高度偏移量 ImageView控件高度 - BottomSheet高度偏移量 ImageView控件高度−BottomSheet高度偏移量,控件的高度我们给个固定值,即 屏幕高度 − B o t t o m S h e e t 折叠时候的高度 − 状态栏的高度 屏幕高度 - BottomSheet折叠时候的高度 - 状态栏的高度 屏幕高度−BottomSheet折叠时候的高度−状态栏的高度,这样,当我们展开或折叠BottomSheet的时候,ImageView会被重新计算其大小,并会进行与调节BottomSheet一样高度的值来调节其高度值
完成上一步后,接下来我们的BottomSheet会展现一个识别内容的列表,我们已经在xml中定义了RecycleView控件,那么我们为这个控件进行一些初始化,使其能展现我们上面提供的数据出来
MainActivity.kt
private val adapter = OcrListAdapter(OcrItemListener { _, item -> image_preview.selectOcrBox(ocrItem = item) }) override fun onCreate(savedInstanceState: Bundle?) { ... recycle_view.adapter = adapter recycle_view.layoutManager = LinearLayoutManager(this) recycle_view.addItemDecoration( DividerItemDecoration( this, LinearLayoutManager.VERTICAL ) ) ... }
上面代码简单的进行了初始化的工作,我们会自定义一个Adapter,并且我们会有一个点击列表中某一Item的回调,回调中我们会使其与我们的imageView进行交互,使得我们点击列表中的某一项的时候,也会同时在图片中显示选中效果,我们先将回调接口方法全都定义好
OcrItemListener.kt
class OcrItemListener(private val clickListener: (position: Int, ocrItem : OcrItem) -> Unit) {
fun onClick(position: Int, ocrItem: OcrItem) = clickListener(position, ocrItem)
}
然后我们来实现我们的Adapter并简单实现一下RecycleView布局
ocr_row_item.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:clickable="true" android:background="@drawable/ocr_list_bg_no_selector" > <TextView android:id="@+id/ocr_result_text" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/ocr_result_score" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" app:layout_constraintStart_toEndOf="@id/ocr_result_text" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>
OcrListAdapter.kt
class OcrListAdapter( private val ocrItemListener: OcrItemListener ) : ListAdapter<OcrItem, OcrListAdapter.ViewHolder>(OcrDiffCallBack()) { class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val textView: TextView = view.findViewById(R.id.ocr_result_text) val scoreView: TextView = view.findViewById(R.id.ocr_result_score) } var selectPosition = -1 var isClick: Boolean = false override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.ocr_row_item, parent, false) return ViewHolder(view).apply { view.setOnClickListener { refreshClickItem(this.adapterPosition) onClick(this.adapterPosition) } } } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val ocrItem = getItem(position) holder.textView.text = ocrItem.text holder.textView.setTextColor(ocrItem.color.toArgb()) holder.scoreView.text = ocrItem.score.toString() holder.scoreView.setTextColor(ocrItem.color.toArgb()) if (selectPosition == position && isClick) { holder.itemView.setBackgroundResource(R.color.select_background) } else { holder.itemView.setBackgroundResource(R.color.white) } } private fun onClick(position: Int) = ocrItemListener.onClick(position, getItem(position)) fun linkageClick(position: Int) { refreshClickItem(position) } private fun refreshClickItem(position: Int) { isClick = if (!isClick) { true } else { selectPosition != position } notifyItemChanged(selectPosition) selectPosition = position notifyItemChanged(selectPosition) } class OcrDiffCallBack : DiffUtil.ItemCallback<OcrItem>() { override fun areItemsTheSame( oldItem: OcrItem, newItem: OcrItem ): Boolean { return oldItem.text == newItem.text } override fun areContentsTheSame( oldItem: OcrItem, newItem: OcrItem ): Boolean { return oldItem == newItem } } }
我们来解释一下比较特殊的代码部分
OcrItem.kt
data class OcrItem(
val boxes: List<List<Int>> = listOf(),
val text: String,
val color: Color,
val score: Float
)
至此,BottomSheet已经实现了,接下来我们会实现图片的展示以及识别内容的展示和交互功能,在这之前,还是建议先把这一部分的文章理解下,因为下一篇文章是在这一篇基础上来实现的,另,先附上完整的项目大家可以到github去查看:https://github.com/xiaozeiqwe8/OCRDemo,这样也能更加好的结合下一篇内容来看
最后还望各位兄弟姐妹们点个赞,关个注,更多的我理解的内容我还会陆续和大家分享的,谢谢大家!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。