Author: Adam Evyčędo <git@apiote.xyz>
turn arrow based on heading, not bearing
%!v(PANIC=String method: strings: negative Repeat count)
diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt index a08bf4c2729a751497adeee3b29c5ec6c660c12d..3d6730f9a369a48fef475076e0746fb39a9b5dcc 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/dashboard/ui/home/HomeFragment.kt @@ -97,7 +97,7 @@ binding.searchBar.setNavigationOnClickListener { (context as MainActivity).onNavigationClicked() } binding.suggestionsRecycler.layoutManager = LinearLayoutManager(activity) - adapter = BimbaResultsAdapter(layoutInflater, activity, listOf(), null, false) + adapter = BimbaResultsAdapter(layoutInflater, activity, listOf(), null, null, false) binding.suggestionsRecycler.adapter = adapter binding.searchView.editText.addTextChangedListener( diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt index 4c94db9e8d6f4bed71759e0240ae038ab93301bd..7ce6dc062432c68e176a752e609cc2b966dbef52 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/Results.kt @@ -6,7 +6,6 @@ package xyz.apiote.bimba.czwek.search import android.content.Context import android.content.Intent -import android.graphics.Matrix import android.location.Location import android.view.LayoutInflater import android.view.View @@ -23,6 +22,9 @@ import xyz.apiote.bimba.czwek.repo.Queryable import xyz.apiote.bimba.czwek.repo.Stop import xyz.apiote.bimba.czwek.repo.StopStub import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings +import xyz.apiote.bimba.czwek.units.Metre +import xyz.apiote.bimba.czwek.units.UnitSystem +import kotlin.math.abs class BimbaViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val root: View = itemView.findViewById(R.id.suggestion) @@ -42,10 +44,21 @@ feeds: Map?, feedsSettings: FeedsSettings?, onClickListener: (Queryable) -> Unit, position: Location?, + heading: Float?, showArrow: Boolean ) { when (queryable) { - is Stop -> bindStop(queryable, holder, context, feeds, feedsSettings, position, showArrow) + is Stop -> bindStop( + queryable, + holder, + context, + feeds, + feedsSettings, + position, + heading, + showArrow + ) + is Line -> bindLine(queryable, holder, context, feeds, feedsSettings) } holder?.root?.setOnClickListener { @@ -90,27 +103,29 @@ context: Context?, feeds: Map<String, FeedInfo>?, feedsSettings: FeedsSettings?, position: Location?, + heading: Float?, showArrow: Boolean ) { - if (showArrow && position != null && position.hasBearing()) { + if (showArrow && position != null && heading != null) { Location(null).apply { latitude = stop.position.latitude longitude = stop.position.longitude }.let { - val rotation = - (360 + ((position.bearingTo(it) + 360).mod(360f)) - position.bearing).mod(360f) + val angle = + (360 + ((position.bearingTo(it) + 360).mod(360f)) - heading).mod(360f) val distance = position.distanceTo(it) holder?.arrow?.apply { setImageResource(R.drawable.arrow) - scaleType = ImageView.ScaleType.MATRIX - val matrix = Matrix() - matrix.postRotate(rotation, width.toFloat() / 2, height.toFloat() / 2) - imageMatrix = matrix + rotation = angle visibility = View.VISIBLE + contentDescription = "Arrow" // TODO } holder?.distance?.apply { - text = "$distance m" // TODO units + val us = UnitSystem.getSelected(context!!) + text = us.toString(context, us.distanceUnit(Metre(distance.toDouble()))) + contentDescription = + us.distanceUnit(Metre(distance.toDouble())).contentDescription(context, us.base) visibility = View.VISIBLE } } @@ -185,6 +200,7 @@ private val inflater: LayoutInflater, private val context: Context?, private var queryables: List<Queryable>, private var position: Location?, + private var heading: Float?, private var showArrow: Boolean ) : RecyclerView.Adapter<BimbaViewHolder>() { @@ -193,6 +209,8 @@ private val oldQueryables: List , private val newQueryables: List<Queryable>, private val oldPosition: Location?, private val newPosition: Location?, + private val oldHeading: Float?, + private val newHeading: Float?, private val oldShowArrow: Boolean, private val newShowArrow: Boolean ) : DiffUtil.Callback() { @@ -243,7 +261,11 @@ val oldChangeOptions = oldQueryable.changeOptions.joinToString { "${it.line}->${it.headsign}" } val newChangeOptions = (newQueryable as Stop).changeOptions.joinToString { "${it.line}->${it.headsign}" } - oldQueryable.name == newQueryable.name && oldChangeOptions == newChangeOptions && oldPosition == newPosition && oldShowArrow == newShowArrow + oldQueryable.name == newQueryable.name && oldChangeOptions == newChangeOptions && + oldPosition?.latitude == newPosition?.latitude && + oldPosition?.longitude == newPosition?.longitude && + oldHeading == newHeading && + oldShowArrow == newShowArrow } else -> false // XXX unreachable @@ -290,27 +312,80 @@ feeds, feedsSettings, onClickListener, this.position, + heading, showArrow ) } override fun getItemCount(): Int = queryables.size - fun update(queryables: List<Queryable>?, position: Location?, showArrow: Boolean) { - val newQueryables = queryables ?: emptyList() + fun update( + queryables: List<Queryable>?, + position: Location?, + heading: Float?, + showArrow: Boolean + ) { + val diff = DiffUtil.calculateDiff( + DiffUtilCallback( + this.queryables, + queryables ?: emptyList(), + this.position, + position, + this.heading, + heading, + this.showArrow, + showArrow + ) + ) + this.position = position + this.heading = heading + this.showArrow = showArrow + this.queryables = queryables ?: emptyList() + diff.dispatchUpdatesTo(this) + } + + fun update( + heading: Float?, + ) { + if (abs((heading ?: 0f) - (this.heading ?: 0f)) < 15) { + return + } + val diff = DiffUtil.calculateDiff( + DiffUtilCallback( + queryables, + queryables, + position, + position, + this.heading, + heading, + showArrow, + showArrow + ) + ) + this.heading = heading + diff.dispatchUpdatesTo(this) + } + + fun update( + queryables: List<Queryable>?, + position: Location?, + showArrow: Boolean + ) { val diff = DiffUtil.calculateDiff( DiffUtilCallback( this.queryables, - newQueryables, + queryables ?: emptyList(), this.position, position, + heading, + heading, this.showArrow, showArrow ) ) this.position = position this.showArrow = showArrow - this.queryables = newQueryables + this.queryables = queryables ?: emptyList() diff.dispatchUpdatesTo(this) } diff --git a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt index 066d95276405023a5d7f868e9c92f4c49c6c6886..b6790e7d43cb8f79a365e3bb9b3fa4d5f7bf7e25 100644 --- a/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt +++ b/app/src/main/java/xyz/apiote/bimba/czwek/search/ResultsActivity.kt @@ -5,6 +5,10 @@ package xyz.apiote.bimba.czwek.search import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager import android.location.Location import android.location.LocationListener import android.location.LocationManager @@ -36,7 +40,7 @@ import xyz.apiote.bimba.czwek.repo.Queryable import xyz.apiote.bimba.czwek.repo.TrafficResponseException import xyz.apiote.bimba.czwek.settings.feeds.FeedsSettings -class ResultsActivity : AppCompatActivity(), LocationListener { +class ResultsActivity : AppCompatActivity(), LocationListener, SensorEventListener { enum class Mode { MODE_LOCATION, MODE_SEARCH, MODE_POSITION } @@ -48,6 +52,8 @@ private lateinit var adapter: BimbaResultsAdapter private val handler = Handler(Looper.getMainLooper()) private var runnable = Runnable {} + private var gravity: FloatArray? = null + private var geomagnetic: FloatArray? = null override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() @@ -73,7 +79,7 @@ windowInsets } binding.resultsRecycler.layoutManager = LinearLayoutManager(this) - adapter = BimbaResultsAdapter(layoutInflater, this, listOf(), null, false) + adapter = BimbaResultsAdapter(layoutInflater, this, listOf(), null, null, false) binding.resultsRecycler.adapter = adapter when (getMode()) { @@ -111,6 +117,12 @@ } } private fun locate() { + val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager + val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + val magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) + sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL) + sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_NORMAL) + try { val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager locationManager.requestLocationUpdates( @@ -133,6 +145,21 @@ handler.removeCallbacks(runnable) getQueryablesByLocation(location, this, true) } + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} + override fun onSensorChanged(event: SensorEvent?) { + if (event?.sensor?.type == Sensor.TYPE_ACCELEROMETER) gravity = event.values + if (event?.sensor?.type == Sensor.TYPE_MAGNETIC_FIELD) geomagnetic = event.values + if (gravity != null && geomagnetic != null) { + val r = FloatArray(9) + val success = SensorManager.getRotationMatrix(r, FloatArray(9), gravity, geomagnetic) + if (success) { + val orientation = FloatArray(3) + SensorManager.getOrientation(r, orientation) + adapter.update((orientation[0]*180/Math.PI).toFloat()) + } + } + } + override fun onResume() { super.onResume() if (getMode() == Mode.MODE_LOCATION) { @@ -145,6 +172,8 @@ super.onPause() val locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager locationManager.removeUpdates(this) handler.removeCallbacks(runnable) + val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager + sensorManager.unregisterListener(this) } override fun onDestroy() { @@ -187,7 +216,8 @@ ) { MainScope().launch { try { val repository = OnlineRepository() - val result = repository.locateQueryables(Position(position.latitude, position.longitude), context) + val result = + repository.locateQueryables(Position(position.latitude, position.longitude), context) getFeeds() updateItems(result, position, showArrow) } catch (e: TrafficResponseException) { diff --git a/app/src/main/res/layout/result.xml b/app/src/main/res/layout/result.xml index 6d71f2fea7c9a4c4742a87913b9f1ea7aef6cb3d..e7dd45553043a5ffad0cfceb1ed5fc71ace8b0b6 100644 --- a/app/src/main/res/layout/result.xml +++ b/app/src/main/res/layout/result.xml @@ -28,27 +28,28 @@ android:id="@+id/arrow" android:layout_width="24dp" android:layout_height="24dp" + android:visibility="gone" + android:layout_marginStart="11dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" android:importantForAccessibility="no" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toEndOf="@+id/distance" + app:layout_constraintStart_toStartOf="@+id/distance" app:layout_constraintTop_toTopOf="parent" - android:layout_marginEnd="8dp" - android:layout_marginTop="8dp" - android:visibility="gone" tool:src="@drawable/arrow" /> <com.google.android.material.textview.MaterialTextView android:id="@+id/distance" + android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="4dp" - android:visibility="gone" - app:layout_constraintBottom_toBottomOf="@+id/arrow" - app:layout_constraintEnd_toStartOf="@+id/arrow" - app:layout_constraintTop_toTopOf="@+id/arrow" + android:layout_marginTop="4dp" + android:layout_marginEnd="8dp" android:textAppearance="@style/TextAppearance.Material3.BodySmall" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/arrow" tool:text="650m" /> - <!-- todo maxWidth or separate layout for graphView --> <com.google.android.material.textview.MaterialTextView android:id="@+id/suggestion_title" android:maxWidth="320dp" @@ -71,14 +72,17 @@ app:layout_constraintStart_toStartOf="@+id/suggestion_title" app:layout_constraintTop_toBottomOf="@+id/suggestion_title" tool:text="TfL London" /> + <!-- todo maxWidth or separate layout for graphView --> <com.google.android.material.textview.MaterialTextView android:id="@+id/suggestion_description" style="@style/Theme.Bimba.SearchResult.Description" - android:layout_width="wrap_content" + android:layout_width="0dp" android:layout_height="wrap_content" android:maxWidth="360dp" android:layout_marginTop="4dp" + android:layout_marginEnd="4dp" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/distance" app:layout_constraintStart_toStartOf="@+id/suggestion_title" app:layout_constraintTop_toBottomOf="@+id/feed_name" tool:text="Metropolitan » Baker Street, Tower Hill The Monument, Westminster, Piccadilly Circus, Oxford Street" />