Android 自定義圖片地圖坐標功能的實現
一、前言
最近項目要求實現一個在自定義地圖圖片上添加坐標信息的功能,類似於在圖片做標註的功能。如下圖所示。坐標的位置是相對於圖片寬高的百分比
二、思路
改功能主要分為三個視圖,1.繼承FrameLayout作為父容器;2.添加一個鋪滿父佈局的ImageView顯示地圖圖片;3.動態添加自定義坐標視圖
三、代碼實現
1. 自定義坐標視圖
<?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="wrap_content" android:layout_height="wrap_content"> <ImageView android:id="@+id/iv_sign" android:layout_width="20dp" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:src="@mipmap/dot2" app:layout_constraintEnd_toStartOf="@+id/tv_sign_name" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv_sign_name" android:layout_width="80dp" android:layout_height="wrap_content" android:background="@color/white" android:text="美食城" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/tv_sign_state" android:layout_width="80dp" android:layout_height="wrap_content" android:background="@color/teal_200" android:text="正常" android:textColor="@color/white" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/tv_sign_name" app:layout_constraintTop_toBottomOf="@+id/tv_sign_name" /> </androidx.constraintlayout.widget.ConstraintLayout>
class SignView : ConstraintLayout { private val TAG = SignView::class.java.simpleName private var view: View private var signIv: ImageView private var signNameTv: TextView private var signStateTv: TextView constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int) : super( context, attrs, defStyleAttr ) init { view = LayoutInflater.from(context).inflate(R.layout.sign_view, this, true) signIv = view.findViewById(R.id.iv_sign) signNameTv = view.findViewById(R.id.tv_sign_name) signStateTv = view.findViewById(R.id.tv_sign_state) } /** * 設置坐標信息 * @param signBean SignBean */ fun setData(signBean: SignBean) { signNameTv.text = signBean.name signStateTv.text = signBean.state } /** * 計算坐標圖標在整個視圖的偏移量 * @return IntArray */ fun getSignOffset(): IntArray { val w = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) val h = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) signIv.measure(w, h) val offset = IntArray(2) val signImageWidth = signIv.measuredWidth val signImageHeight = signIv.measuredHeight offset[0] = signImageWidth / 2 offset[1] = 20 + signImageHeight - offset[0] Log.d(TAG, "getSignOffset: x:${offset[0]}, y:${offset[1]}") return offset } }
自定義的坐標視圖是一個組合的控件,主要是要計算出坐標圖片在整個控件的偏移量
2. 父容器
class MapView : FrameLayout { private val TAG = MapView::class.java.simpleName //地圖圖片 private var mapImage = ImageView(context) private var mapWidth = 0 private var mapHeight = 0 private var mapLeft = 0 private var mapTop = 0 private var signBeanList = listOf<SignBean>() private var signOffsetList = mutableListOf<IntArray>() private var signViewList = mutableListOf<SignView>() private var capturedViewIndex = 0 private val mDragger: ViewDragHelper = ViewDragHelper.create(this, 1.0f, object : ViewDragHelper.Callback() { override fun tryCaptureView(child: View, pointerId: Int): Boolean { return child != mapImage } override fun onViewCaptured(capturedChild: View, activePointerId: Int) { signViewList.forEachIndexed { index, signView -> if (signView == capturedChild) { capturedViewIndex = index return@forEachIndexed } } } override fun onViewPositionChanged( changedView: View, left: Int, top: Int, dx: Int, dy: Int ) { signOffsetList[capturedViewIndex][0] += dx signOffsetList[capturedViewIndex][1] += dy } override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int { val move = if (left <= mapLeft) mapLeft else if (left >= mapWidth + mapLeft) mapWidth + mapLeft else left return move } override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int { val move = if (top <= mapTop) mapTop else if (top >= mapHeight + mapTop) mapHeight + mapLeft else top return move } }) constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int) : this( context, attrs, defStyleAttr, 0 ) constructor( context: Context, attrs: AttributeSet?, @AttrRes defStyleAttr: Int, @StyleRes defStyleRes: Int ) : super(context, attrs, defStyleAttr, defStyleRes) /** * 添加地圖圖片 * @param resId Int */ fun setMapImage(@DrawableRes resId: Int) { removeAllViews() mapImage.setImageResource(resId) addView(mapImage) } /** * 設置坐標列表 * @param list List<SignBean> */ fun setSignData(list: List<SignBean>) { val mapOffset = getBitmapOffset(mapImage, true) mapLeft = mapOffset[0] mapTop = mapOffset[1] mapWidth = mapImage.width - mapLeft * 2 mapHeight = mapImage.height - mapTop * 2 var signOffset = IntArray(2) var boolean = true Log.d(TAG, "mapWidth:$mapWidth, mapHeight:$mapHeight, mapLeft:$mapLeft, mapTop:$mapTop") signBeanList = list removeViews(1, childCount - 1) signViewList.clear() signOffsetList.clear() list.forEach { val signView = SignView(context).apply { setData(it) } // 隻需要計算一次 if (boolean) { boolean = false signOffset = signView.getSignOffset() } signView.layoutParams = getParams(it, signOffset) addView(signView) signViewList.add(signView) signOffsetList.add(intArrayOf((it.x * mapWidth).toInt(), (it.y * mapHeight).toInt())) } } /** * 獲取移動後的坐標信息 * @return List<SignBean> */ fun getMoveSignData(): List<SignBean> { val data = mutableListOf<SignBean>() signOffsetList.forEachIndexed { index, ints -> val signBean = signBeanList[index] data.add( SignBean( signBean.name, signBean.state, ints[0] / mapWidth.toFloat(), ints[1] / mapHeight.toFloat() ) ) } return data } /** * 計算坐標位置 * @param signBean SignBean * @return LayoutParams */ private fun getParams(signBean: SignBean, signOffset: IntArray): LayoutParams { val params = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) params.setMargins( (signBean.x * mapWidth + mapLeft - signOffset[0]).toInt(), (signBean.y * mapHeight + mapTop - signOffset[1]).toInt(), 0, 0 ) return params } /** * 計算圖像在ImageView的位移量 * @param img ImageView * @param includeLayout Boolean * @return IntArray? */ private fun getBitmapOffset(img: ImageView, includeLayout: Boolean): IntArray { val offset = IntArray(2) val values = FloatArray(9) val m: Matrix = img.imageMatrix m.getValues(values) offset[0] = values[2].toInt() offset[1] = values[5].toInt() if (includeLayout) { val lp = img.layoutParams as MarginLayoutParams offset[0] += img.paddingLeft + lp.leftMargin offset[1] += img.paddingTop + lp.topMargin } return offset } override fun onInterceptTouchEvent(event: MotionEvent): Boolean { return mDragger.shouldInterceptTouchEvent(event) } override fun onTouchEvent(event: MotionEvent): Boolean { mDragger.processTouchEvent(event) return true } }
父容器中要註意的是由於圖片不拉伸,所以會出現圖片不會完成鋪滿ImageView,會有黑邊。所以要計算出實際圖片顯示的大小。
3. Activity
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <com.itc.floatparade.MapView android:id="@+id/map" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginBottom="12dp" android:background="@color/black" app:layout_constraintBottom_toTopOf="@+id/tv_add_sign" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/tv_add_sign" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="25dp" android:layout_marginBottom="12dp" android:text="添加坐標" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" /> <Button android:id="@+id/btn_get_sign" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="25dp" android:text="獲取坐標" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/map" /> <TextView android:id="@+id/tv_sign_list" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginEnd="8dp" android:text="" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/btn_get_sign" app:layout_constraintStart_toEndOf="@+id/tv_add_sign" app:layout_constraintTop_toBottomOf="@+id/map" /> </androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.map.setMapImage(R.mipmap.map) binding.tvAddSign.setOnClickListener { val list = mutableListOf<SignBean>() list.add(SignBean("美食城", "正常", 0.2f, 0.4f)) list.add(SignBean("恐龍危機", "正常", 0.5f, 0.5f)) list.add(SignBean("海盜船", "正常", 0.7f, 0.6f)) list.add(SignBean("魔法城堡", "正常", 0.4f, 0.8f)) binding.map.setSignData(list) } binding.btnGetSign.setOnClickListener { val list = binding.map.getMoveSignData() binding.tvSignList.text = list.toString() } } }
完整代碼:https://github.com/MattLjp/FloatParade
到此這篇關於Android 自定義圖片地圖坐標的文章就介紹到這瞭,更多相關Android 自定義地圖內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 關於Android輸入法彈窗bug的優雅處理
- android跑馬燈出現重復跳動以及不滾動問題的解決方法
- Android移動應用開發指南之六種佈局詳解
- Android ListView仿微信聊天界面
- Android studio實現簡易的計算器功能