geohash 推坐标
Recently, I worked on a problem in which I had to mark certain x,y coordinate on an ImageView . Consider this problem similar to solving the “Spot the difference” game in which the player has to identify the difference in the two images correctly.
最近,我处理了一个必须在ImageView上标记某些x,y坐标的问题。 考虑与解决“ 发现差异 ”游戏类似的问题,在该游戏中,玩家必须正确识别两个图像中的差异。
Consider the below example. We want to mark the differences correctly on the second image when the user taps at the correct spots on the image.
考虑下面的例子。 当用户点击图像上的正确位置时,我们希望在第二张图像上正确标记差异。
High level approach to the problem :
解决问题的高级方法 :
Since we need to draw over the existing image we can create our own image view by extending
ImageView
. Now in theonTouchListener
,we can get the x, y coordinates and then store them in a list . After that when the user taps we can check if the tapped co-ordinates are same, if yes then we can draw the circle to mark the touched point.由于我们需要绘制现有图像,因此可以通过扩展
ImageView
来创建自己的图像视图。 现在,在onTouchListener
,我们可以获取x,y坐标,然后将它们存储在列表中。 之后,当用户点击时,我们可以检查所点击的坐标是否相同,如果是,那么我们可以绘制圆圈以标记触摸点。
This approach seems fine and would not scale well on different android phones due the difference in the device densities of the phones. We would need to tweak it a little bit to handle the extrapolation correctly in multiple devices.
这种方法似乎很好,并且由于手机的设备密度不同,因此无法在不同的Android手机上很好地扩展。 我们需要对其进行一些微调,以在多个设备中正确处理外推。
Solution : Let’s start with creating our custom implementation of the ImageView.
解决方案:让我们开始创建ImageView的自定义实现。
Defining the custom View - We can call it MarkerImage
. If you notice there are two main tasks which are expected from this view :
定义自定义 MarkerImage
我们可以将其称为MarkerImage
。 如果您注意到此视图需要完成两个主要任务:
- Draw the circle on the image at the correct x, y location 在正确的x,y位置在图像上绘制圆
- Handle the scenarios when the difference is already marked (avoiding duplicate marking of the difference ) 处理已经标记差异的情况(避免重复标记差异)
Lets’s add a method named markedDown
, it will just add the point to the list and draws them on the image. This would be called from the activity in response to the touch on the image. Notice we have to call invalidate()
, the reason for that is to redraw the newly added points too. To calculate the radius while drawing the circle I have used device density
, this helps in making sure the size of the circle is correct on different devices.
让我们添加一个名为markedDown
的方法,它将只将点添加到列表中并将其绘制在图像上。 响应于图像上的触摸,可以从活动中调用它。 注意,我们必须调用invalidate()
,其原因也是要重绘新添加的点。 在使用设备density
绘制圆形时,要计算半径,这有助于确保在不同设备上圆形的大小正确。
class MarkerImage : androidx.appcompat.widget.AppCompatImageView {
constructor(context: Context) : super(context)
// constructor 2
constructor(context: Context, attributeSet: AttributeSet?) : super(context, attributeSet)
// constructor 3
constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int) : super(
context,
attributeSet,
defStyleAttr
)
// list of already drawn points
val listOfDrawnPoints = mutableListOf<Point>()
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
listOfDrawnPoints.forEach {
val paint = getPaint()
val density = this.getResources().getDisplayMetrics().density
val radius = 10 * density
canvas!!.drawCircle(it.x.toFloat(), it.y.toFloat(), radius, paint)
}
}
// define the paint
fun getPaint(): Paint {
val paint = Paint()
paint.color = Color.BLUE
paint.style = Paint.Style.STROKE
val density: Float = this.getResources().getDisplayMetrics().density
paint.strokeWidth = 5 * density
return paint
}
fun markPoint(x: Int, y: Int) {
listOfDrawnPoints.add(Point(x, y))
invalidate()
}
}
Defining the Layout : We handled the first task of the view. Now our custom image view is ready to be used and we can add it to the layout. We would be looking for the second task of the view in the further parts of the discussion. We would need two ImageViews in the layout, We can use the custom image view to hold the image which would listen to the user touch and mark the points and the regular ImageView to hold the main image. This is what the layout file looks like :
定义布局:我们处理了视图的第一项任务。 现在我们的自定义图像视图已准备就绪,可以使用并将其添加到布局中。 我们将在讨论的其他部分中寻找该视图的第二个任务。 我们将在布局中需要两个ImageViews。我们可以使用自定义图像视图保存图像,该图像将侦听用户触摸并标记点,并使用常规ImageView保留主图像。 这是布局文件的样子:
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp"
android:orientation="vertical">
<ImageView
android:id="@+id/main_image"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitXY"
android:src="@drawable/main_image" />
<com.example.findthedifference.MarkerImage
android:id="@+id/guess_image"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitXY"
android:src="@drawable/diff_image" />
</LinearLayout>
</LinearLayout>
Identifying the heatPoints : Let’s look at the activity now. In the second image user would tap on the area of the image where they find the difference. What we would need to do is to identify those points, we can call these points heat points. Now we can store these points in the list and when the user taps again we can check from the list of the heat points.
确定热点 :让我们现在看一下活动。 在第二幅图像中,用户将点击他们发现差异的图像区域。 我们需要做的就是识别这些点,我们可以将这些点称为热点 。 现在我们可以将这些点存储在列表中,当用户再次点击时,我们可以从热点列表中进行检查。
To get the heatPoints, we can simply use the setOnTouchListener
and tap on the diff area and record the points. I have captured the points and added them to the list in getHeatPointsOfImage
.
要获取HeatPoints,我们可以简单地使用setOnTouchListener
并点击diff区域并记录点。 我已经捕获了这些点并将它们添加到getHeatPointsOfImage
的列表中。
Now what we can do is when the user tap on the image we can check if the current touch point matches any of the points in the heat points list , draw the circle on the Image at that point 🎉 🎉 .
现在,我们可以做的是,当用户点击图像时,我们可以检查当前接触点是否与热点列表中的任何一个点匹配,并在图像上的那个点the上绘制圆。
There is a catch in this case, this approach only works on the same emulator on which you would have captured the heat points. On other emulator this might not work. This happens since the image is rendered differently on different devices because of different device density and thus the x, y coordinates can be extrapolated.
在这种情况下有一个陷阱,这种方法仅适用于捕获了热点的同一模拟器。 在其他模拟器上,这可能不起作用。 发生这种情况是因为由于设备密度不同,在不同的设备上渲染的图像也不同,因此可以推断x,y坐标。
Extrapolation : To get the points correctly on different devices we need to massage the heatPoints too. we can follow the below technique to get the new coordinates for the point. Notice in the code for oldResolution I have hard coded the resolution of the devices which I used to get the heat points.
外推法:要在不同设备上正确获取点,我们也需要按摩heatPoints。 我们可以按照以下方法获取该点的新坐标。 注意,在oldResolution的代码中,我已经硬编码了用于获取热点的设备的分辨率。
function newCoords(oldCoords) {
newCoords.x = oldCoords.x / oldResolution.x * newResolution.x;
newCoords.y = oldCoords.y / oldResolution.y * newResolution.y;
return newCoords;
}
We can get the resolution of the device by using the windowsManager.defaultDisplay
. The following are the points which I captured and are scaled as per the new density of the devices
我们可以使用windowsManager.defaultDisplay
获得设备的分辨率。 以下是我捕获的点,并根据设备的新密度进行缩放
companion object {
const val oldDeviceWidth = 1080f
const val oldDeviceHeight = 1794f
}val currentDeviceWidth by lazy {
val currentResolution = windowManager.defaultDisplay
currentResolution.width}
val currentDeviceHeight by lazy {
val currentResolution = windowManager.defaultDisplay
currentResolution.height}private fun getHeatPointsOfImage(): List<Point> {
return listOf(
Point(x = (336 / oldDeviceWidth) * currentDeviceWidth, y = (634 / oldDeviceHeight) * currentDeviceHeight), // dog
Point(x = (254 / oldDeviceWidth) * currentDeviceWidth, y = (426 / oldDeviceHeight) * currentDeviceHeight), // gas
Point(x = (282 / oldDeviceWidth) * currentDeviceWidth, y = (265 / oldDeviceHeight) * currentDeviceHeight), // toaster
Point(x = (716 / oldDeviceWidth) * currentDeviceWidth, y = (437 / oldDeviceHeight) * currentDeviceHeight), // salt pepper
Point(x = (957 / oldDeviceWidth) * currentDeviceWidth, y = (166 / oldDeviceHeight) * currentDeviceHeight), // Fridge
Point(x = (441 / oldDeviceWidth) * currentDeviceWidth, y = (292 / oldDeviceHeight) * currentDeviceHeight), // kettles on table
Point(x = (382 / oldDeviceWidth) * currentDeviceWidth, y = (172 / oldDeviceHeight) * currentDeviceHeight) // paper roll
)
}
Now the next thing would be to check if the user touch was anywhere near the heat point. It can be very hard for user to match the exact x, y coordinate to identify the difference. What we can do in this case is to check if the user tapped close enough to the heatPoints. If it was close enough then we call the markPoint
method available on the our custom image view.
现在,下一步是检查用户触摸是否在热点附近。 用户可能很难匹配精确的x,y坐标来识别差异。 在这种情况下,我们可以做的是检查用户是否轻触了HeatPoints。 如果距离足够近,则在自定义图像视图上调用可用的markPoint
方法。
Following is how the activity looks like :
以下是该活动的外观:
class SpotTheDifferenceActivity : AppCompatActivity() {
var density: Float = 0f
val threshold by lazy {
15 * density
}
val currentDeviceWidth by lazy {
val currentResolution = windowManager.defaultDisplay
currentResolution.width
}
val currentDeviceHeight by lazy {
val currentResolution = windowManager.defaultDisplay
currentResolution.height
}
companion object {
const val oldDeviceWidth = 1080f
const val oldDeviceHeight = 1794f
}
data class Point(val x: Float, val y: Float)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.another_activity)
guess_image.setOnTouchListener { v, ev -> onTouch(v, ev) }
density = this.getResources().getDisplayMetrics().density
}
fun onTouch(v: View?, ev: MotionEvent): Boolean {
val action = ev.action
val evX = ev.x.toInt()
val evY = ev.y.toInt()
when (action) {
MotionEvent.ACTION_UP -> {
if (touchMatchesHeatPoints(evX, evY)) {
guess_image.markPoint(evX, evY)
}
}
}
return true
}
// this checks if the user touch was close enough to the any of heatPoints
fun touchMatchesHeatPoints(xCord: Int, yCord: Int): Boolean {
val heatPoints = getHeatPointsOfImage()
return heatPoints.filter {
// https://stackoverflow.com/questions/32755825/extrapolation-of-x-y-coordinates-from-one-screen-size-and-resolution-to-other-sc
// This is just the distance between two points
val tempX = it.x - xCord
val tempY = it.y - yCord
val xPosTemp = tempX * tempX
val yPosTemp = tempY * tempY
val dist = Math.sqrt((xPosTemp + yPosTemp).toDouble())
dist <= threshold
}.map {
it
}.size > 0
}
private fun getHeatPointsOfImage(): List<Point> {
return listOf(
Point(x = (336 / oldDeviceWidth) * currentDeviceWidth, y = (634 / oldDeviceHeight) * currentDeviceHeight), // dog
Point(x = (254 / oldDeviceWidth) * currentDeviceWidth, y = (426 / oldDeviceHeight) * currentDeviceHeight), // gas
Point(x = (282 / oldDeviceWidth) * currentDeviceWidth, y = (265 / oldDeviceHeight) * currentDeviceHeight), // toaster
Point(x = (716 / oldDeviceWidth) * currentDeviceWidth, y = (437 / oldDeviceHeight) * currentDeviceHeight), // salt pepper
Point(x = (957 / oldDeviceWidth) * currentDeviceWidth, y = (166 / oldDeviceHeight) * currentDeviceHeight), // Fridge
Point(x = (441 / oldDeviceWidth) * currentDeviceWidth, y = (292 / oldDeviceHeight) * currentDeviceHeight), // kettles on table
Point(x = (382 / oldDeviceWidth) * currentDeviceWidth, y = (172 / oldDeviceHeight) * currentDeviceHeight) // paper roll
)
}
}
Maths involved: To check if the user was close enough, we can use a circular periphery. What we would be doing is calculating the distance between the user touch and the heat points. If the distance is less than our threshold (which is basically radius of the circle), we can mark the point on the image. Following is the picture to demonstrate how to calculate the distance between the two points. Had to do some math using pythagorean theorem
涉及的数学:要检查用户是否足够靠近,我们可以使用圆形外围。 我们要做的是计算用户触摸和热点之间的距离。 如果距离小于我们的阈值(基本上是圆的半径),我们可以在图像上标记该点。 下图是演示如何计算两点之间的距离的图片。 必须使用毕达哥拉斯定理做一些数学运算
Now when you run the app and try to mark the points. You would be able to mark the heat points correctly on the image, however there is just one minor thing which is remaining. If you try to click near an already drawn heat point, you would notice some times you are able to draw another circle at the same point. We don’t want that, as marking the point once should be enough. This would handle the second task of the custom Image View.
现在,当您运行应用程序并尝试标记点时。 您将能够在图像上正确标记热点,但是只剩下一小部分。 如果尝试在已经绘制的热点附近单击,您会发现有时可以在同一点绘制另一个圆。 我们不希望那样,因为标记一次就足够了。 这将处理自定义图像视图的第二个任务。
Handle the scenarios when the difference is already marked (avoiding duplicate marking of the difference) : To handle this we can modify the custom image view. Here we can use the same logic as before. We are already saving the drawn points in the listOfDrawnPoints
, when we call markPoint
we can check if we are in periphery of the already point. If yes then we don’t need to draw again that point.
处理已经标记差异的场景(避免重复标记差异) :要处理此问题,我们可以修改自定义图像视图。 在这里,我们可以使用与以前相同的逻辑。 我们已经将绘制的点保存在listOfDrawnPoints
,当我们调用markPoint
我们可以检查我们是否在该点的外围。 如果是,那么我们无需再次提一点。
fun markPoint(x: Int, y: Int) {
if (!isAlreadyDrawn(x, y)) { // Draw the point if not drawn
listOfDrawnPoints.add(Point(x, y))
invalidate()
}
}fun isAlreadyDrawn(x: Int, y: Int): Boolean {
return listOfDrawnPoints.filter {
checkIfLiesInCircle(x, y, it.x, it.y)
}.size > 0
}// This checks if the point was already drawn and there was another click.
// we don't want to draw the circle again in that peripheryfun checkIfLiesInCircle(x: Int, y: Int, existingX: Int, existingY: Int): Boolean {
val density: Float = this.getResources().getDisplayMetrics().density
val xPosTemp = existingX - x
val yPosTemp = existingY - y
val sqx = xPosTemp * xPosTemp
val sqy = yPosTemp * yPosTemp
val dist = Math.sqrt((sqx + sqy).toDouble())
return dist <= 25 * density
}
Great now your spot the diff app is ready, you would be able to mark the difference correctly.
现在好了,差异应用程序已准备就绪,您将能够正确标记差异。
This is was a basic approach towards solving this problem. In complex scenarios we can use polygons instead of circle to define the region for the heatPoints. It was great learning for me to write this. I am open feedback, feel free to share your suggestions if there is any other way to solve this problem.
这是解决此问题的基本方法。 在复杂的场景中,我们可以使用多边形而不是圆形来定义HeatPoints的区域。 对我来说写这本书真是太好了。 我是开放式反馈,如果有其他方法可以解决此问题,请随时分享您的建议。
Happy Learning ya’all
快乐学习你们
geohash 推坐标