I am trying to make an example in which when I click on a symbollayer it is expanded to know which one is pressed. I enter the data thanks to two geojson files that I created with Mapbox Studio. I tried to follow this examplehttps://docs.mapbox.com/android/maps/examples/icon-size-change-on-click/
but either none is zoomed or all of the same color are zoomed (same layer). Any ideas? What am I doing wrong? For now I am just trying to zoom those with the layer "first-layer-id".
Thank you very much.
My code here.
package com.novadev.mapboxexample.marker
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.mapboxsdk.Mapbox
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.style.layers.Property
import com.mapbox.mapboxsdk.style.layers.PropertyFactory
import com.mapbox.mapboxsdk.style.layers.SymbolLayer
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource
import com.novadev.mapboxexample.R
import kotlinx.android.synthetic.main.activity_marker.*
import java.net.URI
import java.net.URISyntaxException
class MarkerGeojson : AppCompatActivity(),
OnMapReadyCallback,
MapboxMap.OnMapClickListener {
private lateinit var mapboxMap: MapboxMap
private lateinit var markerAnimator: ValueAnimator
private var markerSelected = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Mapbox access token is configured here. This needs to be called either in your application
// object or in the same activity which contains the mapview.
Mapbox.getInstance(this, getString(R.string.map_box_auth_key))
// This contains the MapView in XML and needs to be called after the access token is configured.
setContentView(R.layout.activity_marker)
mapView.onCreate(savedInstanceState)
mapView.getMapAsync(this)
initListeners()
}
override fun onMapReady(mapboxMap: MapboxMap) {
this.mapboxMap = mapboxMap
getMap()
mapboxMap.addOnMapClickListener(this)
}
override fun onMapClick(point: LatLng): Boolean {
val pixel = mapboxMap.projection.toScreenLocation(point)
val features = mapboxMap.queryRenderedFeatures(pixel,"first-layer-id")
val selectedFeature = mapboxMap.queryRenderedFeatures(
pixel, "selected-marker-layer"
)
mapboxMap.getStyle{ style->
val selectedMarkerSymbolLayer =
(style.getLayer("selected-marker-layer") as SymbolLayer)
if (selectedFeature.size > 0 && markerSelected) false
if (features.isEmpty()) if (markerSelected) {
deselectMarker(selectedMarkerSymbolLayer)
}else false
val source: GeoJsonSource? = style.getSourceAs("selected-marker")
source?.setGeoJson(
FeatureCollection.fromFeatures(
arrayOf(
Feature.fromGeometry(
features[0].geometry()
)
)
)
)
if (markerSelected) {
deselectMarker(selectedMarkerSymbolLayer)
}
if (features.size > 0) {
selectMarker(selectedMarkerSymbolLayer)
}
// Get the first feature within the list if one exist
if (features.size > 0) {
val feature = features[0]
// Ensure the feature has properties defined
for ((key, value) in feature.properties()!!.entrySet()) {
// Log all the properties
Log.d("TAG", String.format("%s = %s", key, value))
when (key) {
"NOMBRE" -> {
tvTitleMarker.text = value.toString()
cvInfo.visibility = View.VISIBLE
}
"TELEFONO" -> tvSubtitlemarker.text = value.toString()
}
}
}
}
return true
}
private fun initListeners() {
ivClose.setOnClickListener {
cvInfo.visibility = View.GONE
}
}
private fun getMap() {
mapboxMap.setStyle(
Style.MAPBOX_STREETS
) {
// Map is set up and the style has loaded. Now you can add data or make other map adjustments.
geoJSONToMap(
"first-source-id",
"first-layer-id",
"asset://madridmujeres.geojson",
it
)
geoJSONToMap(
"second-source-id",
"second-layer-id",
"asset://madridoficinascorreos.geojson",
it
)
}
}
private fun drawableToBitmap (drawable : Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
var bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
var canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
private fun geoJSONToMap(
sourceId: String,
layerId: String,
asset_id: String,
style: Style) {
try {
val source = GeoJsonSource(sourceId, URI(asset_id))
style.addSource(source)
if (layerId == "first-layer-id") {
var icon = drawableToBitmap(this.resources.getDrawable(R.drawable.ic_location_purple))
style.addImage("img", icon)
val symbolLayer = SymbolLayer(layerId, sourceId)
symbolLayer.withProperties(
PropertyFactory.iconImage("img"),
PropertyFactory.iconAllowOverlap(true),
PropertyFactory.iconOffset(arrayOf(0f, -9f)),
PropertyFactory.iconAnchor(Property.ICON_ANCHOR_BOTTOM),
PropertyFactory.iconIgnorePlacement(true)
)
style.addLayer(symbolLayer)
val sourceMarker = GeoJsonSource("selected-marker")
style.addSource(sourceMarker)
val symbolLayerSelected = SymbolLayer("selected-marker-layer", "selected-marker")
symbolLayerSelected.withProperties(
PropertyFactory.iconImage("img"),
PropertyFactory.iconAllowOverlap(true),
PropertyFactory.iconOffset(arrayOf(0f, -9f)),
PropertyFactory.iconAnchor(Property.ICON_ANCHOR_BOTTOM),
PropertyFactory.iconIgnorePlacement(true))
style.addLayer(symbolLayerSelected)
} else {
style.addImage("$layerId marker", this.resources.getDrawable(R.drawable.ic_location_yellow))
val symbolLayer = SymbolLayer(layerId, sourceId)
symbolLayer.setProperties(
PropertyFactory.iconImage("$layerId marker"),
PropertyFactory.iconAllowOverlap(true),
PropertyFactory.iconAnchor(Property.ICON_ANCHOR_BOTTOM),
PropertyFactory.iconIgnorePlacement(true)
)
style.addLayer(symbolLayer)
}
} catch (e: URISyntaxException) {
e.printStackTrace()
}
}
private fun selectMarker(iconLayer: SymbolLayer) {
markerAnimator = ValueAnimator()
markerAnimator.setObjectValues(1f, 2f)
markerAnimator.duration = 300
markerAnimator.addUpdateListener { animator ->
iconLayer.setProperties(
PropertyFactory.iconSize(animator.animatedValue as Float)
)
}
markerAnimator.start()
markerSelected = true
}
private fun deselectMarker(iconLayer: SymbolLayer) {
markerAnimator.setObjectValues(2f, 1f)
markerAnimator.duration = 300
markerAnimator.addUpdateListener { animator ->
iconLayer.setProperties(
PropertyFactory.iconSize(animator.animatedValue as Float)
)
}
markerAnimator.start()
markerSelected = false
}
// Add the mapView lifecycle to the activity's lifecycle methods
public override fun onResume() {
super.onResume()
mapView!!.onResume()
}
override fun onStart() {
super.onStart()
mapView!!.onStart()
}
override fun onStop() {
super.onStop()
mapView!!.onStop()
}
public override fun onPause() {
super.onPause()
mapView!!.onPause()
}
override fun onLowMemory() {
super.onLowMemory()
mapView!!.onLowMemory()
}
override fun onDestroy() {
super.onDestroy()
mapView!!.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
mapView!!.onSaveInstanceState(outState)
}
}
It looks as if you are not setting the GeoJSONsource for the selected marker layer.
GeoJsonSource source = style.getSourceAs("selected-marker");
if (source != null) {
source.setGeoJson(FeatureCollection.fromFeatures(
new Feature[]{Feature.fromGeometry(features.get(0).geometry())}));
}
By not setting the geoJSON source, the source will remain the geoJSON containing the FeatureCollection containing all markers. --> All markers will be expanded instead of just the one you have clicked on.
Related
For an app I am making I have a list in which I display pixel art creations, I do this with a RecyclerView and DiffUtil, here is the code:
package com.therealbluepandabear.pixapencil.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.therealbluepandabear.pixapencil.R
import com.therealbluepandabear.pixapencil.databinding.RecentCreationsLayoutBinding
import com.therealbluepandabear.pixapencil.enums.SnackbarDuration
import com.therealbluepandabear.pixapencil.extensions.setOnLongPressListener
import com.therealbluepandabear.pixapencil.extensions.showSnackbar
import com.therealbluepandabear.pixapencil.listeners.RecentCreationsListener
import com.therealbluepandabear.pixapencil.models.PixelArt
import com.therealbluepandabear.pixapencil.viewholders.PixelArtViewHolder
class PixelArtAdapter(
private val snackbarView: View,
private val listener: RecentCreationsListener,
private val context: Context
) : ListAdapter<PixelArt, RecyclerView.ViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val binding = RecentCreationsLayoutBinding.inflate(LayoutInflater.from(parent.context))
return PixelArtViewHolder(binding, context)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val pixelArt = getItem(position)
if (holder is PixelArtViewHolder) {
holder.bind(pixelArt)
holder.binding.recentCreationsLayoutMaterialCardView.setOnClickListener {
listener.onCreationTapped(pixelArt)
}
holder.binding.recentCreationsLayoutMaterialCardView.setOnLongPressListener {
listener.onCreationLongTapped(pixelArt)
}
holder.binding.recentCreationsLayoutFavoriteButton.setOnClickListener {
if (pixelArt.starred) {
pixelArt.starred = false
listener.onUnstarredTapped(pixelArt)
unFavouriteRecentCreation(snackbarView, pixelArt)
holder.bind(pixelArt)
} else {
pixelArt.starred = true
listener.onStarredTapped(pixelArt)
favouriteRecentCreation(snackbarView, pixelArt)
holder.bind(pixelArt)
}
}
}
}
private fun favouriteRecentCreation(contextView: View, pixelArt: PixelArt) { // move to listener
contextView.showSnackbar(contextView.context.getString(R.string.snackbar_pixel_art_project_saved_to_starred_items_in_code_str, pixelArt.title), SnackbarDuration.Default)
pixelArt.starred = true
}
private fun unFavouriteRecentCreation(contextView: View, pixelArt: PixelArt) {
contextView.showSnackbar(contextView.context.getString(R.string.snackbar_pixel_art_project_removed_from_starred_items_in_code_str, pixelArt.title), SnackbarDuration.Default)
pixelArt.starred = false
}
companion object {
val diffCallback: DiffUtil.ItemCallback<PixelArt> = object : DiffUtil.ItemCallback<PixelArt>() {
override fun areItemsTheSame(oldItem: PixelArt, newItem: PixelArt): Boolean {
return oldItem.objId == newItem.objId
}
override fun areContentsTheSame(oldItem: PixelArt, newItem: PixelArt): Boolean {
return oldItem == newItem
}
}
}
}
ViewHolder:
class PixelArtViewHolder(val binding: RecentCreationsLayoutBinding, private val context: Context) : RecyclerView.ViewHolder(binding.root) {
private fun loadPixelArtCoverImage(pixelArt: PixelArt) {
val widthHeight = if (context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
350
} else {
750
}
val requestOptions: RequestOptions = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.priority(Priority.IMMEDIATE)
.encodeFormat(Bitmap.CompressFormat.PNG)
.override(widthHeight, widthHeight)
.centerInside()
.format(DecodeFormat.DEFAULT)
Glide.with(itemView.context)
.setDefaultRequestOptions(requestOptions)
.load(File(itemView.context.getFileStreamPath(pixelArt.coverBitmapFilePath).absolutePath))
.transition(DrawableTransitionOptions.withCrossFade())
.placeholder(R.drawable.transparent_placeholder)
.into(binding.recentCreationsLayoutImageView)
}
private fun loadPixelArtTitle(pixelArt: PixelArt) {
if (pixelArt.title.length > 6) {
binding.recentCreationsLayoutTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.recentCreationsLayoutTitle.isSelected = true
binding.recentCreationsLayoutTitle.isSingleLine = true
(pixelArt.title + " ".repeat(10)).repeat(200).also { binding.recentCreationsLayoutTitle.text = it }
} else {
binding.recentCreationsLayoutTitle.text = pixelArt.title
}
}
private fun loadPixelArtStarred(pixelArt: PixelArt) {
binding.recentCreationsLayoutFavoriteButton.setImageResource(
if (pixelArt.starred) {
R.drawable.ic_baseline_star_24
} else {
R.drawable.ic_baseline_star_border_24
}
)
}
fun bind(pixelArt: PixelArt){
loadPixelArtCoverImage(pixelArt)
binding.recentCreationsLayoutSubtitle.text = context.getString(R.string.recentCreationsLayoutSubtitle_str, pixelArt.width, pixelArt.height)
loadPixelArtStarred(pixelArt)
loadPixelArtTitle(pixelArt)
}
}
Here is the result:
When the user long taps on a project, they get the following dialog:
When they press 'Rename', they get the following dialog where they can rename the project:
My issue is, that when the user types in a new name, and then presses OK, the data is not updating. Sometimes it takes twice to update, sometimes I need to restart the app for it to update, and sometimes it doesn't update at all.
Here is the code responsible for renaming:
fun MainActivity.extendedOnRenameTapped(pixelArt: PixelArt, bottomSheetDialog: BottomSheetDialog) {
val inflatedActivity = activity()?.layoutInflater?.inflate(R.layout.save_file_under_new_name_alert, activity()?.findViewById(android.R.id.content),false)
val textInput: TextInputLayout = inflatedActivity as TextInputLayout
showDialog(
getString(R.string.dialog_rename_title_in_code_str),
null,
getString(R.string.generic_ok_in_code_str), { _, _ ->
val input: String = textInput.editText?.text.toString()
if (input.isNotBlank()) {
pixelArt.title = input
pixelArtViewModel.update(pixelArt)
adapter.submitList(pixelArtData)
bottomSheetDialog.dismiss()
}
}, getString(R.string.generic_cancel_in_code_str), null, view = textInput, dimBackground = false
)
}
I am following everything by the book, so I am confused why this is not working.
Edit
I tried to make it all 'val' and then add this:
pixelArtViewModel.update(pixelArt.copy(title = input))
pixelArtViewModel.getAll().observe(this) {
adapter.submitList(it)
}
bottomSheetDialog.dismiss()
Still not working.
I see that you are setting pixelArt.title, which means your PixelArt class is mutable (has var properties or val properties that reference mutable classes). DiffUtil is 100% incompatible with mutable classes, because they make it impossible to compare items in the old and new lists. It will see the old list as having the new value already so it will treat it as unchanged.
Example with my imagined version of your PixelArt class.
data class PixelArt(
val objId: Long,
val name: String,
val starred: Boolean,
val imageFilePath: String
)
// In ViewModel:
// You probably have the list backed up to disk somehow. I'm just using
// placeholder functions to represent working with the repo or files or
// whatever you use.
val pixelArtLiveData = MutableLiveData<List<PixelArt>>().also {
viewModelScope.launch { it.value = readThePersistedData() }
}
private fun modifyItem(oldItem: PixelArt, newItem: PixelArt) {
pixelArtLiveData.value = pixelArtLiveData.value.orEmpty()
.map { if (it == oldItem) newItem else it }
// also update your persisted data here
}
fun renameItem(originalItem: PixelArt, newName: String) {
modifyItem(originalItem, originalItem.copy(name = newName))
}
fun toggleItemStarred(originalItem: PixelArt) {
modifyItem(originalItem, originalItem.copy(starred = !originalItem.starred))
}
// etc. or you could just make modifyItem public instead of making
// all these helper functions
Then in your adapter, you must call through to these ViewModel functions instead of directly modifying the items or the list or calling submitList. Since the adapter doesn't have direct access to the ViewModel, you probably use your RecentCreationsListener for this by adding
appropriate actions to it that your various click listeners can call.
Your Activity or Fragment would observe this LiveData and simply call submitList() with the observed value.
In my app, when I click on an item (Issue) in a RecyclerView which uses FirebaseRecyclerPagingAdapter to paginate data from Firebase realtime database, it displays details about the item clicked in another fragment (using navigation component). This works fine on the first click, however when I return to the previous fragment and click the same item on the RecyclerView a second time, the details of the item are not shown.
Because I use safe args to pass the item id (issueId) to the next fragment which it uses to query the firebase realtime database and retrieve the details to be displayed, I decide to log the item id to my console in onViewCreated() just to be sure that the item id is being passed on the second click and also that the details (names of user who added an issue) are being retrieved from the database, but just not showing. Then, I noticed a weird behaviour.
On the first click, the item id is logged to the console, the details are logged to the console as well and the fragment displays the details. However on the second click, the item id is logged to the console (showing that the item id is being passed as should be the case), but the details are not logged to the console and not displayed in the fragment (hence the fragment shows up empty). Now the weird part, when I navigate back to the previous fragment, then I see a log of the details displayed twice.
Another strange thing I noticed is that, every item on the RecyclerView has this weird behaviour except the last item. The last item displays its details on the second click, but any other item I click doesn't.
I also noticed that the log shows the details for every item I have previously clicked twice when I navigate back even though I am clicking on a different item
I changed the adapter from FirebaseRecyclerPagingAdapter to FirebaseRecyclerAdapter, everything works fine. When I change back to using FirebaseRecyclerPagingAdapter, the same problem exists.
Is this a bug in my code or FirebaseRecyclerPagingAdapter itself. What could be the problem and what can I do to fix it?
Below is the FirebaseRecyclerPagingAdapter:
package com.colley.android.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.colley.android.R
import com.colley.android.databinding.ItemIssueBinding
import com.colley.android.model.Issue
import com.colley.android.model.Profile
import com.firebase.ui.database.paging.DatabasePagingOptions
import com.firebase.ui.database.paging.FirebaseRecyclerPagingAdapter
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.ktx.database
import com.google.firebase.database.ktx.getValue
import com.google.firebase.ktx.Firebase
class IssuesPagingAdapter(
options: DatabasePagingOptions<Issue>,
private val context: Context,
private val currentUser: FirebaseUser?,
private val clickListener: IssuePagingItemClickedListener
) : FirebaseRecyclerPagingAdapter<Issue, IssuePagingViewHolder>(options) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): IssuePagingViewHolder {
val viewBinding = ItemIssueBinding
.inflate(LayoutInflater.from(parent.context), parent, false)
return IssuePagingViewHolder(viewBinding)
}
override fun onBindViewHolder(viewHolder: IssuePagingViewHolder, position: Int, model: Issue) {
viewHolder.bind(currentUser, model, context, clickListener)
}
interface IssuePagingItemClickedListener {
fun onItemClick(issueId: String, view: View)
fun onItemLongCLicked(issueId: String, view: View)
fun onUserClicked(userId: String, view: View)
}
}
class IssuePagingViewHolder (private val itemBinding : ItemIssueBinding) : RecyclerView.ViewHolder(itemBinding.root) {
#SuppressLint("SetTextI18n")
fun bind(
currentUser: FirebaseUser?,
issue: Issue, context: Context,
clickListener: IssuesPagingAdapter.IssuePagingItemClickedListener) = with(itemBinding) {
//set issue title, body, timeStamp, contributions and endorsements count
issueTitleTextView.text = issue.title
issueBodyTextView.text = issue.body
issueTimeStampTextView.text = issue.timeStamp
contributionsTextView.text = issue.contributionsCount.toString()
endorsementTextView.text = issue.endorsementsCount.toString()
//check if userId is not null
issue.userId?.let { userId ->
//retrieve user profile
Firebase.database.reference.child("profiles").child(userId)
.addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val profile = snapshot.getValue<Profile>()
if (profile != null) {
//set the name of user who raised this issue
userNameTextView.text = profile.name
//set the school of the user who raised this issue
userSchoolTextView.text = profile.school
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
//retrieve user photo
Firebase.database.reference.child("photos").child(userId)
.addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val photo = snapshot.getValue<String>()
//set photo
if (photo != null) {
Glide.with(root.context).load(photo)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(userImageView)
} else {
Glide.with(root.context).load(R.drawable.ic_person).into(userImageView)
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
}
root.setOnClickListener {
if(issue.issueId != null) {
clickListener.onItemClick(issue.issueId, it)
}
}
root.setOnLongClickListener {
if(issue.issueId != null) {
clickListener.onItemLongCLicked(issue.issueId, it)
}
true
}
userNameTextView.setOnClickListener {
if(issue.userId != null) {
clickListener.onUserClicked(issue.userId, it)
}
}
}
}
Here is the fragment to display the item details:
package com.colley.android.view.fragment
import android.os.Bundle
import android.util.Log
import android.view.*
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.colley.android.R
import com.colley.android.adapter.IssuesCommentsRecyclerAdapter
import com.colley.android.databinding.FragmentViewIssueBinding
import com.colley.android.model.Comment
import com.colley.android.model.Issue
import com.colley.android.model.Profile
import com.colley.android.view.dialog.IssueCommentBottomSheetDialogFragment
import com.firebase.ui.database.FirebaseRecyclerOptions
import com.firebase.ui.database.ObservableSnapshotArray
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.*
import com.google.firebase.database.ktx.database
import com.google.firebase.database.ktx.getValue
import com.google.firebase.ktx.Firebase
class ViewIssueFragment :
Fragment(),
IssuesCommentsRecyclerAdapter.ItemClickedListener,
IssuesCommentsRecyclerAdapter.DataChangedListener {
private val args: ViewIssueFragmentArgs by navArgs()
private var _binding: FragmentViewIssueBinding? = null
private val binding get() = _binding
private lateinit var dbRef: DatabaseReference
private lateinit var auth: FirebaseAuth
private lateinit var currentUser: FirebaseUser
private lateinit var recyclerView: RecyclerView
private lateinit var commentSheetDialog: IssueCommentBottomSheetDialogFragment
private var issue: Issue? = null
private var adapter: IssuesCommentsRecyclerAdapter? = null
private var manager: LinearLayoutManager? = null
private val uid: String
get() = currentUser.uid
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentViewIssueBinding.inflate(inflater, container, false)
recyclerView = binding?.issuesCommentsRecyclerView!!
return binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//initialize Realtime Database
dbRef = Firebase.database.reference
//initialize authentication
auth = Firebase.auth
//initialize currentUser
currentUser = auth.currentUser!!
//log item id
Log.d("Log itemId", args.issueId)
//get a query reference to issue comments //order by time stamp
val commentsRef = dbRef.child("issues").child(args.issueId)
.child("comments").orderByChild("commentTimeStamp")
//the FirebaseRecyclerAdapter class and options come from the FirebaseUI library
//build an options to configure adapter. setQuery takes firebase query to listen to and a
//model class to which snapShots should be parsed
val options = FirebaseRecyclerOptions.Builder<Comment>()
.setQuery(commentsRef, Comment::class.java)
.build()
//initialize issue comments adapter
adapter = IssuesCommentsRecyclerAdapter(
options,
currentUser,
this,
this,
requireContext())
manager = LinearLayoutManager(requireContext())
//reversing layout and stacking fron end so that the most recent comments appear at the top
manager?.reverseLayout = true
manager?.stackFromEnd = true
recyclerView.layoutManager = manager
recyclerView.adapter = adapter
dbRef.child("issues").child(args.issueId).addValueEventListener(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
issue = snapshot.getValue<Issue>()
if(issue != null) {
//listener for contrbutions count used to set count text
dbRef.child("issues").child(args.issueId)
.child("contributionsCount").addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val count = snapshot.getValue<Int>()
if(count != null) {
binding?.contributionsTextView?.text = count.toString()
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
//listener for endorsement counts used to set endorsement count text
dbRef.child("issues").child(args.issueId)
.child("endorsementsCount").addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val count = snapshot.getValue<Int>()
if(count != null) {
binding?.endorsementTextView?.text = count.toString()
}
}
override fun onCancelled(error: DatabaseError) {} }
)
//set issue title, body and time stamp, these don't need to change
binding?.issueTitleTextView?.text = issue?.title
binding?.issueBodyTextView?.text = issue?.body
binding?.issueTimeStampTextView?.text = issue?.timeStamp.toString()
//listener for user photo
dbRef.child("photos").child(issue?.userId.toString())
.addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val photo = snapshot.getValue<String>()
if(photo != null) {
context?.let { context -> binding?.userImageView?.let {
imageView ->
Glide.with(context).load(photo).into(
imageView
)
} }
} else {
context?.let { context -> binding?.userImageView?.let {
imageView ->
Glide.with(context).load(R.drawable.ic_profile).into(
imageView
)
} }
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
//listener for profile to set name and school
dbRef.child("profiles").child(issue?.userId.toString())
.addListenerForSingleValueEvent(
object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val profile = snapshot.getValue<Profile>()
if (profile != null) {
//log name details to console
profile.name?.let { Log.d("Log Details", it) }
binding?.userNameTextView?.text = profile.name
binding?.userSchoolTextView?.text = profile.school
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
}
}
override fun onCancelled(error: DatabaseError) {}
}
)
binding?.commentLinearLayout?.setOnClickListener {
commentSheetDialog = IssueCommentBottomSheetDialogFragment(
requireContext(),
requireView())
commentSheetDialog.arguments = bundleOf("issueIdKey" to args.issueId)
commentSheetDialog.show(parentFragmentManager, null)
}
binding?.endorseLinearLayout?.setOnClickListener {
//update contributions count
dbRef.child("issues").child(args.issueId).child("endorsementsCount")
.runTransaction(
object : Transaction.Handler {
override fun doTransaction(currentData: MutableData): Transaction.Result {
//retrieve the current value of endorsement count at this location
var endorsementsCount = currentData.getValue<Int>()
if (endorsementsCount != null) {
//increase the count by 1
endorsementsCount++
//reassign the value to reflect the new update
currentData.value = endorsementsCount
}
//set database issue value to the new update
return Transaction.success(currentData)
}
override fun onComplete(
error: DatabaseError?,
committed: Boolean,
currentData: DataSnapshot?
) {
if (error == null && committed) {
Toast.makeText(requireContext(), "Endorsed", Toast.LENGTH_SHORT)
.show()
}
}
}
)
}
//view profile when clicked
binding?.userImageView?.setOnClickListener {
val action = issue?.userId?.let { it1 ->
ViewIssueFragmentDirections.actionViewIssueFragmentToUserInfoFragment(it1)
}
if (action != null) {
parentFragment?.findNavController()?.navigate(action)
}
}
//view user profile when clicked
binding?.userNameTextView?.setOnClickListener {
val action = issue?.userId?.let { it1 ->
ViewIssueFragmentDirections.actionViewIssueFragmentToUserInfoFragment(it1)
}
if (action != null) {
parentFragment?.findNavController()?.navigate(action)
}
}
}
override fun onItemClick(comment: Comment, view: View) {
//expand comment
}
override fun onItemLongCLicked(comment: Comment, view: View) {
//create option to delete
//create option to respond
}
//view user profile
override fun onUserClicked(userId: String, view: View) {
val action = ViewIssueFragmentDirections.actionViewIssueFragmentToUserInfoFragment(userId)
parentFragment?.findNavController()?.navigate(action)
}
override fun onStart() {
super.onStart()
adapter?.startListening()
}
override fun onStop() {
super.onStop()
adapter?.stopListening()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onDataAvailable(snapshotArray: ObservableSnapshotArray<Comment>) {
//dismiss progress bar once snapshot is available
binding?.issuesCommentProgressBar?.visibility = GONE
//show that there are no comments if snapshot is empty else hide view
//show recycler view if snapshot is not empty else hide
if (snapshotArray.isEmpty()) {
binding?.noCommentsLayout?.visibility = VISIBLE
} else {
binding?.noCommentsLayout?.visibility = GONE
binding?.issuesCommentsRecyclerView?.visibility = VISIBLE
}
}
}
Here is the fragment with the recyclerView showing how I have initialised the adapter:
package com.colley.android.view.fragment
import android.os.Bundle
import android.view.*
import android.view.View.GONE
import android.view.View.VISIBLE
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.paging.LoadState
import androidx.paging.PagingConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.colley.android.R
import com.colley.android.adapter.IssuesPagingAdapter
import com.colley.android.databinding.FragmentIssuesBinding
import com.colley.android.model.Issue
import com.firebase.ui.database.paging.DatabasePagingOptions
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.auth.ktx.auth
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class IssuesFragment :
Fragment(),
IssuesPagingAdapter.IssuePagingItemClickedListener {
private var _binding: FragmentIssuesBinding? = null
private val binding get() = _binding!!
private lateinit var dbRef: DatabaseReference
private lateinit var auth: FirebaseAuth
private lateinit var currentUser: FirebaseUser
private var adapter: IssuesPagingAdapter? = null
private var manager: LinearLayoutManager? = null
private lateinit var recyclerView: RecyclerView
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
private val uid: String
get() = currentUser.uid
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//fragment can participate in populating the options menu
setHasOptionsMenu(true)
//initialize Realtime Database
dbRef = Firebase.database.reference
//initialize authentication
auth = Firebase.auth
//initialize currentUser
currentUser = auth.currentUser!!
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super.onCreateOptionsMenu(menu, inflater)
menu.clear()
inflater.inflate(R.menu.isssues_menu, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.search_issues_menu_item -> {
Toast.makeText(context, "Searching issues", Toast.LENGTH_LONG).show()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentIssuesBinding.inflate(inflater, container, false)
recyclerView = binding.issueRecyclerView
swipeRefreshLayout = binding.swipeRefreshLayout
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//get a query reference to issues
val issuesQuery = dbRef.child("issues")
//configuration for how the FirebaseRecyclerPagingAdapter should load pages
val config = PagingConfig(
pageSize = 30,
prefetchDistance = 15,
enablePlaceholders = false
)
//Options to configure an FirebasePagingAdapter
val options = DatabasePagingOptions.Builder<Issue>()
.setLifecycleOwner(viewLifecycleOwner)
.setQuery(issuesQuery, config, Issue::class.java)
.setDiffCallback(object : DiffUtil.ItemCallback<DataSnapshot>() {
override fun areItemsTheSame(
oldItem: DataSnapshot,
newItem: DataSnapshot
): Boolean {
return oldItem.getValue(Issue::class.java)?.issueId == newItem.getValue(Issue::class.java)?.issueId
}
override fun areContentsTheSame(
oldItem: DataSnapshot,
newItem: DataSnapshot
): Boolean {
return oldItem.getValue(Issue::class.java) == newItem.getValue(Issue::class.java)
}
})
.build()
//instantiate adapter
adapter = IssuesPagingAdapter(
options,
requireContext(),
currentUser,
this)
//Perform some action every time data changes or when there is an error.
viewLifecycleOwner.lifecycleScope.launch {
adapter?.loadStateFlow?.collectLatest { loadStates ->
when (loadStates.refresh) {
is LoadState.Error -> {
// The initial load failed. Call the retry() method
// in order to retry the load operation.
Toast.makeText(context, "Error fetching issues! Retrying..", Toast.LENGTH_SHORT).show()
//display no posts available at the moment
binding.noIssuesLayout.visibility = VISIBLE
adapter?.retry()
}
is LoadState.Loading -> {
// The initial Load has begun
// ...
swipeRefreshLayout.isRefreshing = true
}
is LoadState.NotLoading -> {
// The previous load (either initial or additional) completed
swipeRefreshLayout.isRefreshing = false
//remove display no posts available at the moment
binding.noIssuesLayout.visibility = GONE
}
}
when (loadStates.append) {
is LoadState.Error -> {
// The additional load failed. Call the retry() method
// in order to retry the load operation.
adapter?.retry()
}
is LoadState.Loading -> {
// The adapter has started to load an additional page
// ...
swipeRefreshLayout.isRefreshing = true
}
is LoadState.NotLoading -> {
if (loadStates.append.endOfPaginationReached) {
// The adapter has finished loading all of the data set
swipeRefreshLayout.isRefreshing = false
}
}
}
}
}
//set recycler view layout manager
manager = LinearLayoutManager(requireContext())
recyclerView.layoutManager = manager
//initialize adapter
recyclerView.adapter = adapter
swipeRefreshLayout.setOnRefreshListener {
adapter?.refresh()
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
//navigate to new fragment with issue id
override fun onItemClick(issueId: String, view: View) {
val action = HomeFragmentDirections.actionHomeFragmentToViewIssueFragment(issueId)
parentFragment?.findNavController()?.navigate(action)
}
override fun onItemLongCLicked(issueId: String, view: View) {
}
override fun onUserClicked(userId: String, view: View) {
val action = HomeFragmentDirections.actionHomeFragmentToUserInfoFragment(userId)
parentFragment?.findNavController()?.navigate(action)
}
}
Before click
After first click
After second click
Use addListenerForSingleValueEvent instead of addValueEventListener to query the item details (issue) from the database in the fragment that displays the clicked item details. Otherwise, remove the addValueEventListener in onStop() so that the listener is no longer attached to the database when navigating back to the previous fragment.
MainActivity.kt
package com.memex.eu
import android.Manifest
import android.app.Activity
import android.app.ActivityManager
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.util.Size
import android.view.Gravity
import android.view.MotionEvent
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.ar.core.HitResult
import com.google.ar.core.Plane
import com.google.ar.sceneform.AnchorNode
import com.google.ar.sceneform.rendering.ModelRenderable
import com.google.ar.sceneform.ux.ArFragment
import com.google.ar.sceneform.ux.TransformableNode
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.objects.ObjectDetection
import com.google.mlkit.vision.objects.ObjectDetector
import com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions
import kotlinx.android.synthetic.main.activity_main.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import java.util.function.Consumer
import java.util.function.Function
class MainActivity : AppCompatActivity() {
private lateinit var cameraExecutor: ExecutorService
private val TAG: String = MainActivity::class.java.getSimpleName()
private var arFragment: ArFragment? = null
private var andyRenderable: ModelRenderable? = null
private lateinit var options: ObjectDetectorOptions
private lateinit var objectDetector: ObjectDetector
private lateinit var analyzer: MyImageAnalyzer
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Request camera permissions
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
)
}
cameraExecutor = Executors.newSingleThreadExecutor()
if (!checkIsSupportedDeviceOrFinish(this)) {
return
}
arFragment = supportFragmentManager.findFragmentById(R.id.sceneform_fragment) as ArFragment?
// When you build a Renderable, Sceneform loads its resources in the background while returning
// a CompletableFuture. Call thenAccept(), handle(), or check isDone() before calling get().
ModelRenderable.builder()
.setSource(this, R.raw.andy)
.build()
.thenAccept(Consumer { renderable: ModelRenderable? ->
andyRenderable = renderable
})
.exceptionally(
Function<Throwable, Void?> { throwable: Throwable? ->
val toast =
Toast.makeText(this, "Unable to load andy renderable", Toast.LENGTH_LONG)
toast.setGravity(Gravity.CENTER, 0, 0)
toast.show()
null
})
arFragment!!.setOnTapArPlaneListener { hitResult: HitResult, plane: Plane?, motionEvent: MotionEvent? ->
if (andyRenderable == null) {
return#setOnTapArPlaneListener
}
// Create the Anchor.
val anchor = hitResult.createAnchor()
val anchorNode =
AnchorNode(anchor)
anchorNode.setParent(arFragment!!.arSceneView.scene)
// Create the transformable andy and add it to the anchor.
val andy =
TransformableNode(arFragment!!.transformationSystem)
andy.setParent(anchorNode)
andy.renderable = andyRenderable
andy.select()
}
// Live detection and tracking
options = ObjectDetectorOptions.Builder()
.setDetectorMode(ObjectDetectorOptions.STREAM_MODE)
.enableClassification() // Optional
.build()
objectDetector = ObjectDetection.getClient(options)
#androidx.camera.core.ExperimentalGetImage
analyzer = MyImageAnalyzer()
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray
) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(
this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT
).show()
finish()
}
}
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.createSurfaceProvider())
}
val imageAnalyzer = ImageAnalysis.Builder()
.setTargetResolution(Size(1280, 720))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(cameraExecutor, analyzer)
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalyzer)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it
) == PackageManager.PERMISSION_GRANTED
}
companion object {
private const val TAG = "MainActivity"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private const val MIN_OPENGL_VERSION = 3.0
/**
* Returns false and displays an error message if Sceneform can not run, true if Sceneform can run
* on this device.
*
*
* Sceneform requires Android N on the device as well as OpenGL 3.0 capabilities.
*
*
* Finishes the activity if Sceneform can not run
*/
fun checkIsSupportedDeviceOrFinish(activity: Activity): Boolean {
val openGlVersionString =
(activity.getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.deviceConfigurationInfo
.glEsVersion
if (openGlVersionString.toDouble() < MIN_OPENGL_VERSION) {
Log.e(TAG, "Sceneform requires OpenGL ES 3.0 later")
Toast.makeText(
activity,
"Sceneform requires OpenGL ES 3.0 or later",
Toast.LENGTH_LONG
)
.show()
activity.finish()
return false
}
return true
}
}
}
MyImageAnalyzer.kt
package com.memex.eu
import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.fragment.app.FragmentManager
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.objects.DetectedObject
import com.google.mlkit.vision.objects.ObjectDetection
import com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions
import com.google.mlkit.vision.objects.defaults.PredefinedCategory
#androidx.camera.core.ExperimentalGetImage
class MyImageAnalyzer : ImageAnalysis.Analyzer{
// Live detection and tracking
val options = ObjectDetectorOptions.Builder()
.setDetectorMode(ObjectDetectorOptions.STREAM_MODE)
.enableClassification() // Optional
.build()
val objectDetector = ObjectDetection.getClient(options)
private val TAG: String = MyImageAnalyzer::class.java.getSimpleName()
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
Log.e(TAG, "DETECTIONS")
if (mediaImage != null) {
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
objectDetector.process(image)
.addOnCompleteListener {
imageProxy.close()
if (it.isSuccessful) {
displayDets(it.result as List<DetectedObject>)
} else {
it.exception?.printStackTrace()
}
}
}
imageProxy.close()
}
private fun displayDets(results: List<DetectedObject>) {
for (detectedObject in results) {
val boundingBox = detectedObject.boundingBox
val trackingId = detectedObject.trackingId
for (label in detectedObject.labels) {
val text = label.text
if (PredefinedCategory.FOOD == text) {
Log.e(TAG, text)
}
val index = label.index
if (PredefinedCategory.FOOD_INDEX == index) {
Log.e(TAG, text)
}
val confidence = label.confidence
Log.e(TAG, confidence.toString())
Log.e(TAG, trackingId.toString())
Log.e(TAG, boundingBox.toString())
}
}
}
}
But in my console, I see none of the Log statements from ImageAnalysis.Analyzer. What's going on? Why is ImageAnalysis.Analyzer not being called? I am printing out the log statements first before I eventually draw the bounding boxes. However, the analyze function of ImageAnalysis.Analyzer is not being called at all. How do I tell camerax to call that function?
Hello I have an app that used now deprecated Camera module in android for displaying the camera view and drawing filters onto it using mlkit face detection. Recently we've decided to upgrade it to CameraX, I did all the necessary steps to make it work as a separate android app and it works. When I want to incorporate it to our existing Custom React Native module no matter what I do, it doesn't show anything on the screen. I've checked the view sizes and they seem to be ok. Below is the code I use to start the camera module.
package com.myProject.speech.components
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.hardware.camera2.CameraManager
import android.os.Bundle
import android.util.AttributeSet
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.size
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import com.facebook.react.uimanager.ThemedReactContext
import com.google.mlkit.common.MlKitException
import com.google.mlkit.vision.face.FaceDetectorOptions
import com.myProject.grpc.speech.StickerDatabase
import com.myProject.speech.R
import com.myProject.speech.utils.PreferenceUtils
import java.util.*
import androidx.databinding.DataBindingUtil
import com.myProject.camera_test.mlkit.facedetection.FaceContourDetectionProcessor
import com.myProject.speech.mlkit.GraphicOverlay
import java.util.concurrent.ExecutorService
class myProjectCameraView : RelativeLayout {
private var previewView: PreviewView? = null
private var graphicOverlay: GraphicOverlay? = null
private var context: ThemedReactContext? = null
private lateinit var cameraExecutor: ExecutorService
var faceRecognitionEnabled = false
var cameraSelector: CameraSelector? = null
private var cameraProvider: ProcessCameraProvider? = null
var cameraLifecycleObserver: CameraLifecycleObserver? = null
private var lensFacing = CameraSelector.LENS_FACING_FRONT
private lateinit var cameraManager: CameraManager
private var imageCapture: ImageCapture? = null
val TAG = "myProjectCameraView"
constructor(context: ThemedReactContext,
cameraLifecycleObserver: CameraLifecycleObserver?) : super(context) {
this.context = context
this.cameraLifecycleObserver = cameraLifecycleObserver;
Log.d(TAG, "constructor Called")
val inflater = context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
inflater.inflate(R.layout.live_preview, this, true)
previewView = findViewById(R.id.firePreview)
graphicOverlay = findViewById(R.id.fireFaceOverlay)
if (allPermissionsGranted()) {
startCamera()
if (cameraLifecycleObserver != null) {
cameraLifecycleObserver.registerActionHandler(this);
}
} else {
Log.d(TAG, "Not all Permissions Granted")
}
this.cameraLifecycleObserver!!.registerActionHandler(this)
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this.context as Context)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(previewView!!.createSurfaceProvider())
}
imageCapture = ImageCapture.Builder()
.build()
val imageAnalyzer = ImageAnalysis.Builder().setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(cameraExecutor, FaceContourDetectionProcessor(graphicOverlay!!))
}
// Select back camera as a default
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_FRONT)
.build()
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this.context!!.currentActivity!! as AppCompatActivity, cameraSelector, preview, imageCapture, imageAnalyzer)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this.context as Context))
}
fun enableFaceRecognition(enabled: Boolean) {
Log.d(TAG, "enableFaceRecognition Called")
if (enabled) {
Log.d(TAG, "With FaceRecognition")
Log.d(TAG, "Parent size: ${this!!.measuredWidth}x${this!!.measuredHeight}")
Log.d(TAG, "previewView sizes: ${previewView!!.measuredWidth}x${previewView!!.measuredHeight}")
Log.d(TAG, "graphicOverlay sizes: ${graphicOverlay!!.measuredWidth}x${graphicOverlay!!.measuredHeight}")
} else {
Log.d(TAG, "Without FaceRecognition")
}
}
fun onActivityStopped() {
Log.d(TAG, "onActivityStopped")
}
fun onActivityResumed() {
Log.d(TAG, "onActivityResumed")
}
fun onActivityPaused() {
Log.d(TAG, "onActivityPaused")
}
fun onActivityDestroyed() {
Log.d(TAG, "onActivityDestroyed")
}
private fun allPermissionsGranted(): Boolean {
for (permission in requiredPermissions) {
if (!isPermissionGranted(getContext(), permission)) {
return false
}
}
return true
}
private val requiredPermissions: Array<String?>
private get() = try {
val info = getContext().packageManager.getPackageInfo(
getContext().packageName, PackageManager.GET_PERMISSIONS)
val ps = info.requestedPermissions
if (ps != null && ps.size > 0) {
ps
} else {
arrayOfNulls(0)
}
} catch (e: Exception) {
arrayOfNulls(0)
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {}
constructor(context: Context?, attrs: AttributeSet?,
defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
}
val viewBitmap: Bitmap
get() {
val v = rootView
val b = Bitmap.createBitmap(v.layoutParams.width,
v.layoutParams.height,
Bitmap.Config.ARGB_8888)
val c = Canvas(b)
v.layout(v.left, v.top, v.right, v.bottom)
v.draw(c)
return b
}
private fun isPermissionGranted(context: Context,
permission: String?): Boolean {
if (ContextCompat.checkSelfPermission(context, permission!!) ==
PackageManager.PERMISSION_GRANTED) {
Log.i(this.TAG, "Permission granted: $permission")
return true
}
Log.i(TAG, "Permission NOT granted: $permission")
return false
}
companion object {
private const val FACE_DETECTION = "Face Detection"
private const val CLASSIFICATION = "Classification"
private const val PERMISSION_REQUESTS = 1
private const val lastDBPath = ""
private var context: ThemedReactContext? = null
val lastDB: StickerDatabase? = null
}
}
Any insights would be very appreciated.
Thanks
The issue happens because the relative layout is not resized after being added to the scene. The workaround is not mine, but I could not find where it was from right now so I'll just leave it here in case someone else is having the similar issue. I solved the issue by calling the layoutHack in the constructor of my RelativeLayout
fun setupLayoutHack() {
Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
manuallyLayoutChildren()
viewTreeObserver.dispatchOnGlobalLayout()
Choreographer.getInstance().postFrameCallback(this)
}
})
}
fun manuallyLayoutChildren() {
for (i in 0 until childCount) {
val child = getChildAt(i)
child.measure(MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY))
child.layout(0, 0, child.measuredWidth, child.measuredHeight)
}
}
I am trying to recognize qr code in my app when camera is turned on. And then transfer to according activity depend on qr code text.
I am doing it with firebase ML kit and CameraX library with help of google's documentation but I have errors with ImageAnalyzer's analyze method.
ImageAnalyzer offers me to implement analyze method even though it's already implemented. But it has two parameters: (imageProxy: ImageProxy?, degrees: Int) and says that it overrides nothing. if I delete second parameter (degrees: Int), then it recognizes, but "degrees" is not recognized.
I tried to follow some tutorials but they use lots of third party libraries. I'd like to use cameraX and ML Kit.
How can I fix this?
Here is my code:
package ge.softservice.nfcwithactivties
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.google.firebase.ml.vision.FirebaseVision
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcodeDetectorOptions
import com.google.firebase.ml.vision.common.FirebaseVisionImage
import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata
import kotlinx.android.synthetic.main.activity_qr.*
import java.io.File
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class QrActivity : AppCompatActivity() {
private var preview: Preview? = null
private var imageAnalyzer: ImageAnalysis? = null
private var camera: Camera? = null
internal var isDetected = false
private lateinit var outputDirectory: File
private lateinit var cameraExecutor: ExecutorService
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray
) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(
this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT
).show()
finish()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_qr)
// Request camera permissions
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
)
}
// outputDirectory = getOutputDirectory()
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun startCamera() {
val options = FirebaseVisionBarcodeDetectorOptions.Builder()
.setBarcodeFormats(
FirebaseVisionBarcode.FORMAT_QR_CODE,
FirebaseVisionBarcode.FORMAT_AZTEC
)
.build()
val detector = FirebaseVision.getInstance().getVisionBarcodeDetector(options)
val result = detector.detectInImage(image)
.addOnSuccessListener { barcodes ->
// Task completed successfully
// ...
}
.addOnFailureListener {
// Task failed with an exception
// ...
}
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener(Runnable {
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
preview = Preview.Builder()
.build()
// Select back camera
val cameraSelector =
CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, preview
)
preview?.setSurfaceProvider(viewFinder.createSurfaceProvider(/*camera?.cameraInfo*/))
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun takePhoto() {
// TODO
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it
) == PackageManager.PERMISSION_GRANTED
}
/* fun getOutputDirectory(): File {
val mediaDir = externalMediaDirs.firstOrNull()?.let {
File(it, resources.getString(R.string.app_name)).apply { mkdirs() } }
return if (mediaDir != null && mediaDir.exists())
mediaDir else filesDir
}*/
companion object {
private const val TAG = "CameraXBasic"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
}
private class MyImageAnalyzer : ImageAnalysis.Analyzer {
private fun degreesToFirebaseRotation(degrees: Int): Int = when(degrees) {
0 -> FirebaseVisionImageMetadata.ROTATION_0
90 -> FirebaseVisionImageMetadata.ROTATION_90
180 -> FirebaseVisionImageMetadata.ROTATION_180
270 -> FirebaseVisionImageMetadata.ROTATION_270
else -> throw Exception("Rotation must be 0, 90, 180, or 270.")
}
#SuppressLint("UnsafeExperimentalUsageError")
override fun analyze(imageProxy: ImageProxy?, degrees: Int) {
val mediaImage = imageProxy?.image
val imageRotation = degreesToFirebaseRotation(degrees)
if (mediaImage != null) {
val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
// Pass image to an ML Kit Vision API
// ...
}
}
}
Looking at your code snippet, it seems you're using camera-camera2 version beta04 with camerax-view version alpha11.
The documentation may be out of date, the Analyzer now only receives an ImageProxy inside its analyze callback. The degrees information that was previously also passed in can now be accessed via ImageProxy.getImageInfo().getRotationDegrees().
So your Analyzer should look like this
private class MyImageAnalyzer : ImageAnalysis.Analyzer {
private fun degreesToFirebaseRotation(degrees: Int): Int {
// ...
}
override fun analyze(imageProxy: ImageProxy) {
val mediaImage = imageProxy.image
val imageRotation = degreesToFirebaseRotation(imageProxy.imageInfo.rotationDegrees)
val image = FirebaseVisionImage.fromMediaImage(mediaImage, imageRotation)
// ...
}
}