Failed to take picture from arFragment - ARCore - Augmented Face - kotlin

I follow code here but failed to capture my ArFragment. Please help me.
https://codelabs.developers.google.com/codelabs/sceneform-intro/index.html?index=..%2F..io2018#15
This project's link: https://github.com/DSparda/Breakable-Cam
Always get IOException: "Failed to save bitmap to disk"
My WritingArFragment:
class FaceArFragment : ArFragment() {
override fun getSessionConfiguration(session: Session?): Config {
return Config(session).apply { augmentedFaceMode = Config.AugmentedFaceMode.MESH3D }
}
override fun getSessionFeatures(): MutableSet<Session.Feature> {
return EnumSet.of(Session.Feature.FRONT_CAMERA)
}
override fun getAdditionalPermissions(): Array<String?>? {
val additionalPermissions = super.getAdditionalPermissions()
val permissionLength =
additionalPermissions?.size ?: 0
val permissions =
arrayOfNulls<String>(permissionLength + 1)
permissions[0] = Manifest.permission.WRITE_EXTERNAL_STORAGE
if (permissionLength > 0) {
System.arraycopy(
additionalPermissions,
0,
permissions,
1,
additionalPermissions!!.size
)
}
return permissions
}
/**
* Override to turn off planeDiscoveryController. Plane trackables are not supported with the
* front camera.
*/
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val layout = super.onCreateView(inflater, container, savedInstanceState) as FrameLayout
planeDiscoveryController.apply {
hide()
setInstructionView(null)
}
return layout
}
}
My Activity XML:
<layout 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">
<data>
<variable
name="takePictureViewModel"
type="com.example.breakablecam.screens.takingPicture.TakePictureViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/face_fragment_cointanier"
android:layout_width="match_parent"
android:layout_height="0dp"
android:paddingBottom="16dp"
app:layout_constraintBottom_toTopOf="#+id/takePhotoView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<fragment
android:id="#+id/face_fragment"
android:name="com.example.breakablecam.screens.takingPicture.FaceArFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.fragment.app.FragmentContainerView>
<ImageView
android:id="#+id/backArrow"
android:layout_width="#android:dimen/app_icon_size"
android:layout_height="#android:dimen/app_icon_size"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ImageView
android:id="#+id/stickerView"
android:layout_width="#android:dimen/app_icon_size"
android:layout_height="#android:dimen/app_icon_size"
android:layout_marginBottom="16dp"
android:onClick="#{() -> takePictureViewModel.tapSticker()}"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/takePhotoView"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="#+id/stickerView1"
android:layout_width="#android:dimen/app_icon_size"
android:layout_height="#android:dimen/app_icon_size"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:visibility="gone"
android:onClick="#{() -> takePictureViewModel.tapSticker1()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="#+id/takePhotoView"
android:layout_width="#android:dimen/app_icon_size"
android:layout_height="#android:dimen/app_icon_size"
android:layout_marginBottom="16dp"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="#+id/makeupView"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="#+id/stickerView" />
<ImageView
android:id="#+id/makeupView"
android:layout_width="#android:dimen/app_icon_size"
android:layout_height="#android:dimen/app_icon_size"
android:layout_marginBottom="16dp"
android:onClick="#{() -> takePictureViewModel.tapMakeupView()}"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="#+id/takePhotoView" />
<ImageView
android:id="#+id/makeupView1a"
android:layout_width="#android:dimen/app_icon_size"
android:layout_height="#android:dimen/app_icon_size"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:onClick="#{() -> takePictureViewModel.tapMakeup1aView()}"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<ImageView
android:id="#+id/makeupView1"
android:layout_width="#android:dimen/app_icon_size"
android:layout_height="#android:dimen/app_icon_size"
android:layout_marginStart="16dp"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="#id/makeupView1a"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
My Activity:
class TakePictureActivity : AppCompatActivity() {
private lateinit var viewModel: TakePictureViewModel
private lateinit var arFragment: FaceArFragment
private var modelRenderable: ModelRenderable? = null
private var meshTexture: Texture? = null
private val faceNodeMap = HashMap<AugmentedFace, AugmentedFaceNode?>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: ActivityTakePictureBinding =
DataBindingUtil.setContentView(this, R.layout.activity_take_picture)
viewModel = ViewModelProvider(this).get(TakePictureViewModel::class.java)
binding.apply {
viewModel.apply {
setStickerViewSource(stickerView)
setMakeupViewSource(makeupView)
setTakePhotoViewSource(takePhotoView)
setMakeup1ViewSource(makeupView1)
setMakeup1aViewSource(makeupView1a)
setSticker1ViewSource(stickerView1)
setBackArrowSource(backArrow)
}
}
binding.takePictureViewModel = viewModel
binding.lifecycleOwner = this
viewModel.makeupTap.observe(this, Observer { check ->
if (check == 1) {
binding.apply {
makeupView.visibility = GONE
takePhotoView.visibility = GONE
stickerView.visibility = GONE
makeupView1.visibility = VISIBLE
makeupView1a.visibility = VISIBLE
backArrow.visibility = VISIBLE
val params =
faceFragmentCointanier.layoutParams as ConstraintLayout.LayoutParams
params.bottomToTop = R.id.makeupView1
}
viewModel.doneTapMakeup()
}
})
viewModel.stickerTap.observe(this, Observer { check ->
if (check == 1) {
binding.apply {
makeupView.visibility = GONE
takePhotoView.visibility = GONE
stickerView.visibility = GONE
stickerView1.visibility = VISIBLE
backArrow.visibility = VISIBLE
val params =
faceFragmentCointanier.layoutParams as ConstraintLayout.LayoutParams
params.bottomToTop = R.id.stickerView1
}
viewModel.doneTapSticker()
}
})
binding.apply {
backArrow.setOnClickListener {
makeupView.visibility = VISIBLE
takePhotoView.visibility = VISIBLE
stickerView.visibility = VISIBLE
makeupView1.visibility = GONE
makeupView1a.visibility = GONE
backArrow.visibility = GONE
stickerView1.visibility = GONE
backArrow.visibility = GONE
val params =
faceFragmentCointanier.layoutParams as ConstraintLayout.LayoutParams
params.bottomToTop = R.id.takePhotoView
}
}
arFragment = supportFragmentManager.findFragmentById(R.id.face_fragment) as FaceArFragment
val sceneView = arFragment.arSceneView
sceneView.cameraStreamRenderPriority = Renderable.RENDER_PRIORITY_FIRST
val scene = sceneView.scene
loadTexture(R.drawable.makeup1a)
loadModel(R.raw.fox_face)
viewModel.makeupTap1a.observe(this, Observer { check ->
when (check) {
1 -> {
scene.addOnUpdateListener {
val collection: Collection<AugmentedFace>? =
sceneView.session?.getAllTrackables(AugmentedFace::class.java)
collection?.forEach { face ->
if (!faceNodeMap.containsKey(face)) {
val faceNode = AugmentedFaceNode(face)
faceNode.apply {
setParent(scene)
faceMeshTexture = meshTexture
}
faceNodeMap[face] = faceNode
}
}
val iterator = faceNodeMap.entries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
val face = entry.key
if (face.trackingState == TrackingState.STOPPED) {
val faceNode = entry.value
faceNode!!.setParent(null)
iterator.remove()
}
}
}
}
2 -> {
val children: List<Node> =
ArrayList(arFragment.arSceneView.scene.children)
for (node in children) {
if (node is AnchorNode) {
if (node.anchor != null) {
node.anchor?.detach()
}
}
if (node !is Camera && node !is Sun) {
node.setParent(null)
}
}
}
}
})
viewModel.sticker1Tap.observe(this, Observer { check ->
when (check) {
1 -> {
scene.addOnUpdateListener {
val collection: Collection<AugmentedFace>? =
sceneView.session?.getAllTrackables(AugmentedFace::class.java)
collection?.forEach { face ->
if (!faceNodeMap.containsKey(face)) {
val faceNode = AugmentedFaceNode(face)
faceNode.apply {
setParent(scene)
faceRegionsRenderable = modelRenderable
}
faceNodeMap[face] = faceNode
}
}
val iterator = faceNodeMap.entries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
val face = entry.key
if (face.trackingState == TrackingState.STOPPED) {
val faceNode = entry.value
faceNode!!.setParent(null)
iterator.remove()
}
}
}
}
2 -> {
val children: List<Node> =
ArrayList(arFragment.arSceneView.scene.children)
for (node in children) {
if (node is AnchorNode) {
if (node.anchor != null) {
node.anchor?.detach()
}
}
if (node !is Camera && node !is Sun) {
node.setParent(null)
}
}
}
}
})
binding.takePhotoView.setOnClickListener {
takePhoto()
}
}
private fun loadTexture(tex: Int) {
Texture.builder()
.setSource(this, tex)
.build()
.thenAccept { texture -> meshTexture = texture }
}
private fun loadModel(mod: Int) {
ModelRenderable.builder()
.setSource(this, mod)
.build()
.thenAccept { model ->
model.apply {
isShadowCaster = false // optional
isShadowReceiver = false
}
modelRenderable = model
}
}
private fun generateFilename(): String? {
val date =
SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault())
.format(Date())
return Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES
).toString() + File.separator + "Sceneform/" + date + "_screenshot.jpg"
}
#Throws(IOException::class)
private fun saveBitmapToDisk(bitmap: Bitmap, filename: String) {
val out = File(filename)
if (!out.parentFile.exists()) {
out.parentFile.mkdirs()
}
try {
FileOutputStream(filename).use { outputStream ->
ByteArrayOutputStream().use { outputData ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputData)
outputData.writeTo(outputStream)
outputStream.flush()
outputStream.close()
}
}
} catch (ex: IOException) {
throw IOException("Failed to save bitmap to disk", ex)
}
}
private fun takePhoto() {
val filename = generateFilename()
val view: ArSceneView = arFragment.getArSceneView()
// Create a bitmap the size of the scene view.
val bitmap = Bitmap.createBitmap(
view.width, view.height,
Bitmap.Config.ARGB_8888
)
// Create a handler thread to offload the processing of the image.
val handlerThread = HandlerThread("PixelCopier")
handlerThread.start()
// Make the request to copy.
PixelCopy.request(view, bitmap, { copyResult ->
if (copyResult === PixelCopy.SUCCESS) {
try {
saveBitmapToDisk(bitmap, filename!!)
} catch (e: IOException) {
val toast = Toast.makeText(
this, e.toString(),
Toast.LENGTH_LONG
)
toast.show()
return#request
}
val snackbar = Snackbar.make(
findViewById(android.R.id.content),
"Photo saved", Snackbar.LENGTH_LONG
)
snackbar.setAction(
"Open in Photos"
) { v: View? ->
val photoFile = File(filename)
val photoURI = FileProvider.getUriForFile(
this,
this.getPackageName()
.toString() + ".ar.codelab.name.provider",
photoFile
)
val intent = Intent(Intent.ACTION_VIEW, photoURI)
intent.setDataAndType(photoURI, "image/*")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(intent)
}
snackbar.show()
} else {
val toast = Toast.makeText(
this,
"Failed to copyPixels: $copyResult", Toast.LENGTH_LONG
)
toast.show()
}
handlerThread.quitSafely()
}, Handler(handlerThread.looper))
}
}
Thank everyone so much.

try add
<application android:requestLegacyExternalStorage="true" ... > ... </application>
to your manifest.
I assume that this is related to the upcoming scoped storage enforcement in Android 11.

You have to add the tags in your AndroidManifest.xml specified in the codelabs(permissions and file provider.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.ar.codelab.name.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/paths"/>
</provider>
Then you have to migrate from using File to using Uris and ContentResolvers because Android 10 deprecated the File functionality and you have to use MediaStore.
https://developer.android.com/training/data-storage/shared/media

Related

Kotlin use tabLayout with 2 activities

I have 2 activities. Which I tried to convert into fragments but somehow, the code I use for the activities won't work within fragments. I get a bunch of errors.
The first activity is a card where when clicked a random text will appear and the card is flipping.
The second activity is a timer with a 3th party progress bar that the fragment gives an error.
Now my question is, how can I use 2 activities within a tabLayout since I cannot convert the activity code into a fragment?
I have been searching the internet for hours but I can only find that you can use fragments within a tabLayout.
Is there a tutorial on how to create a tabLayout with 2 activities?
I also followed the tutorial https://mkyong.com/android/android-tablayout-example/ but that didn't work.
This is my Timer activity:
Kotlin:
class NewTimerActivity : AppCompatActivity() {
enum class TimerState {
Stopped, Paused, Running
}
private lateinit var timer: CountDownTimer
private var timerLengthSeconds: Int = 30
private var timerState = TimerState.Stopped
private var secondsRemaining = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_timer)
val display = supportActionBar
display?.title = ""
display?.setDisplayHomeAsUpEnabled(true)
val fab_start = findViewById<Button>(R.id.fab_start)
val fab_pause = findViewById<Button>(R.id.fab_pause)
val fab_stop = findViewById<Button>(R.id.fab_stop)
fab_start.setOnClickListener {
startTimer()
timerState = TimerState.Running
updateButtons()
}
fab_pause.setOnClickListener {
timer.cancel()
timerState = TimerState.Paused
updateButtons()
}
fab_stop.setOnClickListener {
timer.cancel()
onTimerFinished()
updateButtons()
}
}
override fun onResume() {
super.onResume()
initTimer()
}
override fun onPause() {
super.onPause()
if (timerState == TimerState.Running) {
timer.cancel()
} else if (timerState == TimerState.Paused) {
}
PrefUtil.setPreviousTimerLengthSeconds(timerLengthSeconds, this)
PrefUtil.setSecondsRemaining(secondsRemaining, this)
PrefUtil.setTimerState(timerState, this)
}
fun initTimer() {
timerState = PrefUtil.getTimerState(this)
if (timerState == TimerState.Stopped)
setNewTimerLength()
else
setPreviousTimerLength()
secondsRemaining = if (timerState == TimerState.Running || timerState == TimerState.Paused)
PrefUtil.getSecondsRemaining(this).toInt()
else
timerLengthSeconds
if (timerState == TimerState.Running)
startTimer()
updateButtons()
updateCountdownUI()
}
private fun onTimerFinished() {
var progress_countdown = findViewById<ProgressBar>(R.id.progress_countdown)
timerState = TimerState.Stopped
setNewTimerLength()
progress_countdown.progress = 0
PrefUtil.setSecondsRemaining(timerLengthSeconds, this)
secondsRemaining = timerLengthSeconds
updateButtons()
updateCountdownUI()
}
private fun startTimer() {
timerState = TimerState.Running
timer = object : CountDownTimer((secondsRemaining * 1000).toLong(), 1000) {
override fun onFinish() = onTimerFinished()
override fun onTick(millisUntilFinished: Long) {
secondsRemaining = (millisUntilFinished / 1000).toInt()
updateCountdownUI()
}
}.start()
}
private fun setNewTimerLength() {
var progress_countdown = findViewById<ProgressBar>(R.id.progress_countdown)
var lengthInMinutes = PrefUtil.getTimerLength(this)
timerLengthSeconds = ((lengthInMinutes * 60L).toInt())
progress_countdown.max = timerLengthSeconds.toInt()
}
private fun setPreviousTimerLength() {
var progress_countdown = findViewById<ProgressBar>(R.id.progress_countdown)
timerLengthSeconds = PrefUtil.getPreviousTimerLengthSeconds(this).toInt()
progress_countdown.max = timerLengthSeconds.toInt()
}
private fun updateCountdownUI() {
var progress_countdown = findViewById<ProgressBar>(R.id.progress_countdown)
val textView_Countdown = findViewById<TextView>(R.id.timer_textview)
val minutesUntilFinished = secondsRemaining / 60
val secondsInMinutesUntilFinished = secondsRemaining - minutesUntilFinished * 60
val secondsStr = secondsInMinutesUntilFinished.toString()
textView_Countdown.text = "$minutesUntilFinished:${
if (secondsStr.length == 2) secondsStr
else "0" + secondsStr}"
progress_countdown.progress = (timerLengthSeconds - secondsRemaining).toInt()
}
private fun updateButtons() {
val fab_start = findViewById<Button>(R.id.fab_start)
val fab_pause = findViewById<Button>(R.id.fab_pause)
val fab_stop = findViewById<Button>(R.id.fab_stop)
when (timerState) {
TimerState.Running -> {
fab_start.isEnabled = false
fab_pause.isEnabled = true
fab_stop.isEnabled = true
}
TimerState.Stopped -> {
fab_start.isEnabled = true
fab_pause.isEnabled = false
fab_stop.isEnabled = false
}
TimerState.Paused -> {
fab_start.isEnabled = true
fab_pause.isEnabled = false
fab_stop.isEnabled = true
}
}
}
}
XML:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:background="#drawable/achtergrondnowhite"
tools:context=".NewTimerActivity">
<TextView
android:id="#+id/timer_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:text="30"
android:textColor="#FFF"
android:textSize="70dp" />
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="#style/Theme.KlimaatAmbitieGame.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="#style/Theme.KlimaatAmbitieGame.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="#+id/progress_countdown"
style="#style/Widget.MaterialProgressBar.ProgressBar"
android:layout_width="306dp"
android:layout_height="306dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginStart="52dp"
android:layout_marginTop="215dp"
android:layout_marginEnd="52dp"
android:layout_marginBottom="210dp"
android:minWidth="306dp"
android:minHeight="306dp"
app:mpb_indeterminateTint="#color/white"
app:mpb_progressBackgroundTint="#color/white"
app:mpb_progressTint="#color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/fab_stop"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom|end"
android:layout_marginStart="274dp"
android:layout_marginEnd="27dp"
android:layout_marginBottom="131dp"
android:background="#color/black"
android:minWidth="50dp"
android:minHeight="50dp"
android:text="Stop"
android:textColor="#color/white"
app:srcCompat="#drawable/ic_stop" />
<Button
android:id="#+id/fab_pause"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom|center"
android:layout_marginStart="156dp"
android:layout_marginTop="#dimen/bigger_fab_margin"
android:layout_marginEnd="151dp"
android:layout_marginBottom="131dp"
android:background="#color/black"
android:minWidth="150dp"
android:minHeight="50dp"
android:text="Pause"
android:textColor="#color/white"
app:srcCompat="#drawable/ic_pause" />
<Button
android:id="#+id/fab_start"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_gravity="bottom|start"
android:layout_marginStart="45dp"
android:layout_marginTop="#dimen/bigger_fab_margin"
android:layout_marginEnd="280dp"
android:layout_marginBottom="133dp"
android:background="#color/black"
android:minWidth="100dp"
android:minHeight="50dp"
android:text="Start"
android:textColor="#color/white" />
</RelativeLayout>
PrefUtil
class PrefUtil {
companion object {
fun getTimerLength(context: Context): Double {
//placeholder
return 0.5
}
//private var defValue: Long
private const val PREVIOUS_TIMER_LENGTH_SECONDS_ID = "com.resoconder.timer.previous_timer_length"
fun getPreviousTimerLengthSeconds(context: Context): Long {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
return preferences.getLong(PREVIOUS_TIMER_LENGTH_SECONDS_ID, 0)
}
fun setPreviousTimerLengthSeconds(seconds: Int, context: Context) {
val editor = PreferenceManager.getDefaultSharedPreferences(context).edit()
editor.putLong(PREVIOUS_TIMER_LENGTH_SECONDS_ID, seconds.toLong())
editor.apply()
}
private const val TIMER_STATE_ID = "com.resocoder.timer.timer_state"
fun getTimerState(context: Context): NewTimerActivity.TimerState {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val ordinal = preferences.getInt(TIMER_STATE_ID, 0)
return NewTimerActivity.TimerState.values()[ordinal]
}
fun setTimerState(state: NewTimerActivity.TimerState, context: Context) {
val editor = PreferenceManager.getDefaultSharedPreferences(context).edit()
val ordinal = state.ordinal
editor.putInt(TIMER_STATE_ID, ordinal)
editor.apply()
}
private const val SECONDS_REMAINING_ID = "com.resoconder.timer.previous_timer_length"
fun getSecondsRemaining(context: Context): Long {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
return preferences.getLong(SECONDS_REMAINING_ID, 0)
}
fun setSecondsRemaining(seconds: Int, context: Context) {
val editor = PreferenceManager.getDefaultSharedPreferences(context).edit()
editor.putLong(SECONDS_REMAINING_ID, seconds.toLong())
editor.apply()
}
}
}
And this is my Card activity:
Kotlin:
class StartKaartActivity : AppCompatActivity() {
lateinit var front_anim:AnimatorSet
lateinit var back_anim:AnimatorSet
var isFront = false
#SuppressLint("ResourceType")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_start_kaart)
//Set Backbutton Bar
val display = supportActionBar
display?.title = ""
display?.setDisplayHomeAsUpEnabled(true)
display?.setDisplayUseLogoEnabled(true)
display?.setLogo(R.drawable.logo)
val scale = applicationContext.resources.displayMetrics.density
val tvstartkaart = findViewById<TextView>(com.fotf.klimaatambitiegame.R.id.tvstartkaart)
val tvstartkaartachterkant = findViewById<TextView>(com.fotf.klimaatambitiegame.R.id.tvstartkaartachterkant)
tvstartkaart.cameraDistance = 8000 * scale
tvstartkaartachterkant.cameraDistance = 8000 * scale
front_anim = AnimatorInflater.loadAnimator(applicationContext, R.anim.font_animation) as AnimatorSet
back_anim = AnimatorInflater.loadAnimator(applicationContext, R.anim.back_animation) as AnimatorSet
val Carts = arrayOf("" +
"Random Text",
)
tvstartkaart.setOnClickListener() {
if (isFront) {
front_anim.setTarget(tvstartkaart)
back_anim.setTarget(tvstartkaartachterkant)
front_anim.start()
back_anim.start()
isFront = false
} else {
val random = Carts.random()
tvstartkaart.setText(random)
front_anim.setTarget(tvstartkaartachterkant)
back_anim.setTarget(tvstartkaart)
front_anim.start()
back_anim.start()
isFront = true
}
};
}
}
XML:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#drawable/achtergrondnowhite"
tools:context=".StartKaartActivity">
<TextView
android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="176dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="177dp"
android:paddingHorizontal="10dp"
android:text="Klik op de kaart om een nieuwe kaart te krijgen. \n\nGeef het goede antwoord op de vraag en verdien een houder."
android:textColor="#color/white"
android:textSize="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/tvstartkaart"
android:layout_width="350dp"
android:layout_height="450dp"
android:layout_marginTop="28dp"
android:background="#drawable/kaartstartvoorkant"
android:gravity="left|center"
android:padding="15dp"
android:paddingHorizontal="10dp"
android:text=""
android:textColor="#color/black"
android:textSize="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.491"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView" />
<TextView
android:id="#+id/tvstartkaartachterkant"
android:layout_width="350dp"
android:layout_height="450dp"
android:layout_marginTop="28dp"
android:background="#drawable/kaartstartachterkant"
android:gravity="left|center"
android:padding="15dp"
android:paddingHorizontal="10dp"
android:textColor="#color/black"
android:textSize="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.491"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>

Saving checkbox states in RecyclerView inside Fragment using SharedPreferences or any other method

I'm trying to save checkbox states in RecyclerView inside Fragment to restore these preferences after exit from the app and loading it again.
I have a ConfigActivity for AppWidget in which there are fragments.
Inside of one of the fragments I have a RecyclerView which loads calendars available for the user from Calendar Provider. Based on selected calendars the appwidget will be loading the events from them. Selected calendars should be passed into the appwidget.
I've made saving states of the checkboxes while scrolling of the RecyclerView.
But I don't know how to save selected checkboxes in RecyclerView inside Fragment using SharedPreferences (saving for relaunching of the app).
My data class for calendar items:
data class CalendarItem(
val idCalendar: Long,
val displayNameCalendar: String?,
val accountNameCalendar: String?,
val colorCalendar: Int?
)
Item with checkbox in xml:
<RelativeLayout 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="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp">
<ImageView
android:id="#+id/calendar_color"
android:layout_width="10dp"
android:layout_height="10dp"
android:src="#drawable/color_label_circle"
app:tint="#color/accent_color"
android:layout_alignParentStart="true"
android:layout_alignTop="#+id/text_display_name_calendar"
android:layout_alignBottom="#+id/text_display_name_calendar"/>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="#+id/text_display_name_calendar"
style="#style/basicText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="24dp"
android:layout_marginEnd="4dp"
android:maxLines="1"
android:ellipsize="end"
android:gravity="start|center_vertical"
android:layoutDirection="rtl"
android:text="Display Name" />
<TextView
android:id="#+id/text_account_name"
style="#style/commentText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Account Name"
android:layout_marginEnd="4dp"
android:maxLines="1"
android:ellipsize="end"
android:layout_alignStart="#+id/text_display_name_calendar"
android:layout_below="#+id/text_display_name_calendar" />
</RelativeLayout>
My Fragment getting calendars:
class CalendarsEventsFragment : Fragment() {
// For permissions
private val PERMISSION_REQUEST_CODE = 101
// For RecyclerView - Calendars
private lateinit var calendarItemAdapter: CalendarItemAdapter
private lateinit var recyclerViewCalendars: RecyclerView
// Values for the calendars from the calendar content provider
private val EVENT_PROJECTION = arrayOf(
CalendarContract.Calendars._ID,
CalendarContract.Calendars.CALENDAR_DISPLAY_NAME,
CalendarContract.Calendars.ACCOUNT_NAME,
CalendarContract.Calendars.CALENDAR_COLOR
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view = inflater.inflate(R.layout.fragment_calendars_events, container, false)
recyclerViewCalendars = view.findViewById(R.id.recyclerview_calendars)
// Setup permissions + start getCalendars
setupPermissionsGetCalendars()
return view
}
// Function to get and show calendars
private fun getCalendars() {
// Getting calendars from CalendarProvider
// In practice, should be done in an asynchronous thread instead of on the main thread
calendarItemAdapter = CalendarItemAdapter()
calendarItemAdapter.clearData()
val uri = CalendarContract.Calendars.CONTENT_URI
val cur: Cursor? = context?.contentResolver?.query(
uri,
EVENT_PROJECTION,
null,
null,
null
)
while (cur?.moveToNext() == true) {
val calId = cur.getLong(PROJECTION_ID_INDEX)
val displayName = cur.getString(PROJECTION_DISPLAY_NAME_INDEX)
val accountName = cur.getString(PROJECTION_ACCOUNT_NAME_INDEX)
val color = cur.getInt(PROJECTION_CALENDAR_COLOR_INDEX)
calendarItemAdapter.pushData(
CalendarItem(
idCalendar = calId,
displayNameCalendar = displayName,
accountNameCalendar = accountName,
colorCalendar = color
)
)
}
cur?.close()
// Setup RecyclerView adapter
recyclerViewCalendars.let {
it.layoutManager = LinearLayoutManager(context)
it.adapter = calendarItemAdapter
}
}
// Function to check permission and make request for permission + start getCalendars
private fun setupPermissionsGetCalendars() {
if (checkSelfPermission(requireContext(), Manifest.permission.READ_CALENDAR) !=
PackageManager.PERMISSION_GRANTED
) {
requestPermissions(
arrayOf(Manifest.permission.READ_CALENDAR),
PERMISSION_REQUEST_CODE
)
} else {
getCalendars()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
PERMISSION_REQUEST_CODE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(
requireActivity(),
getText((R.string.toast_permission_granted)),
Toast.LENGTH_SHORT
).show()
getCalendars()
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CALENDAR)) {
Toast.makeText(
requireActivity(),
getText((R.string.toast_permission_denied)),
Toast.LENGTH_SHORT
).show()
showUserRationale()
} else {
askUserOpenAppInfo()
}
}
}
}
}
private fun showUserRationale() {
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.request_permission_rationale_title))
.setMessage(getString(R.string.request_permission_rationale_message))
.setPositiveButton("OK") { dialog, id ->
requestPermissions(
arrayOf(Manifest.permission.READ_CALENDAR),
PERMISSION_REQUEST_CODE
)
}
.create()
.show()
}
private fun askUserOpenAppInfo() {
val appSettingsIntent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", activity?.packageName, null)
)
if (activity?.packageManager?.resolveActivity(
appSettingsIntent,
PackageManager.MATCH_DEFAULT_ONLY
) == null
) {
Toast.makeText(
requireContext(),
getText(R.string.toast_permission_denied_forever),
Toast.LENGTH_SHORT
).show()
} else {
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.request_permission_denied_forever_title))
.setMessage(getString(R.string.request_permission_denied_forever_message))
.setPositiveButton(getString(R.string.open_app_info_dialog_positive_button_text)) { dialog, id ->
startActivity(appSettingsIntent)
requireActivity().finish()
}
.setNegativeButton(getString(R.string.open_app_info_dialog_negative_button_text)) { dialog, id ->
requireActivity().finish()
}
.create()
.show()
}
}
}
My RecyclerView Adapter:
class CalendarItemAdapter() : RecyclerView.Adapter<CalendarItemAdapter.ViewHolder>() {
var data: MutableList<CalendarItem> = mutableListOf()
var checkedCalendarItems = SparseBooleanArray()
fun clearData() {
data.clear()
notifyDataSetChanged()
}
fun pushData(calendarItem: CalendarItem) {
data.add(calendarItem)
notifyDataSetChanged()
}
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageViewColor: ImageView = view.findViewById(R.id.calendar_color)
val displayNameOfCalendar: CheckBox = view.findViewById(R.id.text_display_name_calendar)
val accountName: TextView = view.findViewById(R.id.text_account_name)
init {
displayNameOfCalendar.setOnClickListener {
if(!checkedCalendarItems.get(adapterPosition, false)) {
displayNameOfCalendar.isChecked = true
checkedCalendarItems.put(adapterPosition, true)
} else {
displayNameOfCalendar.isChecked = false
checkedCalendarItems.put(adapterPosition, false)
}
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_calendar, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val datum = data[position]
datum.colorCalendar?.let {
holder.imageViewColor.setColorFilter(it)
}
holder.displayNameOfCalendar.text = datum.displayNameCalendar
holder.displayNameOfCalendar.isChecked = checkedCalendarItems.get(position, false)
holder.accountName.text = datum.accountNameCalendar
}
override fun getItemCount(): Int {
return data.size
}
}
Could you help me, please?
SharedPreferences can only store primitives and String arrays so you'll have to serialise your array somehow. Probably the easiest way is to just get all the indices of the checked items and throw them in a string. And when you pull that back out, split them up and set those to true.
You should probably handle this in the adapter, since really it's an internal implementation detail that only the adapter needs to know about. Something like this maybe:
class CalendarItemAdapter() : RecyclerView.Adapter<CalendarItemAdapter.ViewHolder>() {
var checkedCalendarItems = SparseBooleanArray()
fun saveState(prefs: SharedPreferences) {
// make a list of all the indices that are set to true, join them as a string
val checkedIndices = checkedCalendarItems
.mapIndexedNotNull {index, checked -> if (checked) index else null }
.joinToString(SEPARATOR)
prefs.edit { putString(KEY_CHECKED_INDICES, checkedIndices) }
}
fun restoreState(prefs: SharedPreferences) {
// reset the array - we're clearing the current state
// whether there's anything stored or not
checkedCalendarItems = SparseBooleanArray()
// grab the checked indices and set them - using null as a "do nothing" fallback
val checkedIndices = prefs.getString(KEY_CHECKED_INDICES, null)
?.split(SEPARATOR)
?.map(String::toInt) // or mapNotNull(String::toIntOrNull) to be super safe
?.forEach { checkedCalendarItems[it] = true }
// update the display - onBindViewHolder should be setting/clearing checkboxes
// by referring to the checked array
notifyDataSetChanged()
}
...
companion object {
// making these constants that both functions refer to avoids future bugs
// e.g. someone changing the separator in one function but not the other
const val SEPARATOR = ","
const val KEY_CHECKED_INDICES = "checked indices"
}
}
Then you can call these save/restore state functions on the adapter as appropriate, e.g. in onStop and onStart, passing in your SharedPreferences state object

error: [kapt] An exception occurred: android.databinding.tool.util.LoggedErrorException: Found data binding error(s):?

I am learning databinding with mvvm but I am getting following errors I did not know what is the main problem.
DataBinderMapperImpl.java:9: error: cannot find symbol
import gahfy.net.databinding.ActivityPostListBindingImpl;
^
symbol: class ActivityPostListBindingImpl
location: package gahfy.net.databinding
error: [kapt] An exception occurred: android.databinding.tool.util.LoggedErrorException: Found data binding error(s):
[databinding] {"msg":"cannot find method getLoadingVisibility() in class gahfy.net.ui.post.PostListViewModel","file":"C:\\Users\\Edgar\\Documents\\MVVMPosts\\app\\src\\main\\res\\layout\\activity_post_list.xml","pos":[{"line0":22,"col0":37,"line1":22,"col1":68}]}
error: cannot find symbol
import gahfy.net.databinding.ActivityPostListBindingImpl;
^
symbol: class ActivityPostListBindingImpl
location: package gahfy.net.databinding
cannot find method getLoadingVisibility() in class gahfy.net.ui.post.PostListViewModel
what I have tried invalidate cache restart and rebuild and clean project it did not helped at all
below activity_post_list.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="viewModel"
type="gahfy.net.ui.post.PostListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:mutableVisibility="#{viewModel.getLoadingVisibility()}" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/post_list"
android:layout_width="0dp"
android:layout_height="0dp"
app:adapter="#{viewModel.getPostListAdapter()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
below PostListActivity.kt
import android.os.Bundle
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import gahfy.net.R
import com.google.android.material.snackbar.Snackbar;
import gahfy.net.databinding.ActivityPostListBinding
class PostListActivity: AppCompatActivity() {
private lateinit var binding: ActivityPostListBinding
private lateinit var viewModel: PostListViewModel
private var errorSnackbar: Snackbar? = null
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_post_list)
binding.postList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
viewModel = ViewModelProviders.of(this).get(PostListViewModel::class.java)
viewModel.errorMessage.observe(this, Observer {
errorMessage -> if(errorMessage != null) showError(errorMessage) else hideError()
})
binding.viewModel = viewModel
}
private fun showError(#StringRes errorMessage:Int){
errorSnackbar = Snackbar.make(binding.root, errorMessage, Snackbar.LENGTH_INDEFINITE)
errorSnackbar?.setAction(R.string.retry, viewModel.errorClickListener)
errorSnackbar?.show()
}
private fun hideError(){
errorSnackbar?.dismiss()
}
}
below PostListViewModel.kt
class PostListViewModel:BaseViewModel(){
#Inject
lateinit var postApi: PostApi
private val loadingVisibility: MutableLiveData<Int> = MutableLiveData()
val errorMessage:MutableLiveData<Int> = MutableLiveData()
val errorClickListener = View.OnClickListener { loadPosts() }
private val postListAdapter: PostListAdapter = PostListAdapter()
private lateinit var subscription: Disposable
init{
loadPosts()
}
private fun loadPosts(){
subscription = postApi.getPosts()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe { onRetrievePostListStart() }
.doOnTerminate { onRetrievePostListFinish() }
.subscribe(
// Add result
{ result -> onRetrievePostListSuccess(result) },
{ onRetrievePostListError() }
)
}
private fun onRetrievePostListStart(){
loadingVisibility.value = View.VISIBLE
errorMessage.value = null
}
private fun onRetrievePostListFinish(){
loadingVisibility.value = View.GONE
}
private fun onRetrievePostListSuccess(postList:List<Post>){
postListAdapter.updatePostList(postList)
}
private fun onRetrievePostListError(){
errorMessage.value = R.string.post_error
}
override fun onCleared() {
super.onCleared()
subscription.dispose()
}
}
below BindingAdapters.kt
#BindingAdapter("mutableText")
fun setMutableText(view: TextView, text: MutableLiveData<String>?) {
val parentActivity:AppCompatActivity? = view.getParentActivity()
if(parentActivity != null && text != null) {
text.observe(parentActivity, Observer { value -> view.text = value?:""})
}
#BindingAdapter("mutableVisibility")
fun setMutableVisibility(view: View, visibility: MutableLiveData<Int>?) {
val parentActivity:AppCompatActivity? = view.getParentActivity()
if(parentActivity != null && visibility != null) {
visibility.observe(parentActivity, Observer { value -> view.visibility = value?:View.VISIBLE})
}
}
#BindingAdapter("adapter")
fun setAdapter(view: RecyclerView, adapter: RecyclerView.Adapter<*>) {
view.adapter = adapter
}
}
It's about your databinding usage in xml.
1.Your used variable must be public or a have public getter.
2.If you want use public variable just use it name (without get).
So you must make this changes in this lines.
private val loadingVisibility: MutableLiveData<Int> = MutableLiveData()
private val postListAdapter: PostListAdapter = PostListAdapter()
To
val loadingVisibility: MutableLiveData<Int> = MutableLiveData()
val postListAdapter: PostListAdapter = PostListAdapter()
And
app:mutableVisibility="#{viewModel.getLoadingVisibility()}"
app:adapter="#{viewModel.getPostListAdapter()}"
To
app:mutableVisibility="#{viewModel.loadingVisibility}"
app:adapter="#{viewModel.postListAdapter}"
BindAdapters
class BindAdapters {
companion object {
#BindingAdapter("mutableText")
fun setMutableText(view: TextView, text: MutableLiveData<String>?) {
val parentActivity: AppCompatActivity? = view.getParentActivity()
if (parentActivity != null && text != null) {
text.observe(parentActivity, Observer { value -> view.text = value ?: "" })
}
}
#BindingAdapter("mutableVisibility")
fun setMutableVisibility(view: View, visibility: MutableLiveData<Int>?) {
val parentActivity: AppCompatActivity? = view.getParentActivity()
if (parentActivity != null && visibility != null) {
visibility.observe(
parentActivity,
Observer { value -> view.visibility = value ?: View.VISIBLE })
}
}
#BindingAdapter("adapter")
fun setAdapter(view: RecyclerView, adapter: RecyclerView.Adapter<*>) {
view.adapter = adapter
}
}
}

Admob native ads of UnifiedNativeAdView are not clickable in Kotlin

I faced a strange bug with admob native ads. Everything works fine except that I cannot find url value for an ads on callback and that UnifiedNativeAdView is not clickable, even button inside don't go anywhere as I use admob templates.
Here is the XML view code:
<com.google.android.gms.ads.formats.UnifiedNativeAdView
android:paddingTop="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/native_ad_view"
android:background="#000000"
android:elevation="20dp">
<LinearLayout
android:id="#+id/native_ad_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical">
<TextView
style="#style/AdAttribution"
android:id="#+id/native_ad_attribution"
android:visibility="gone"/>
<LinearLayout
android:id="#+id/native_ad_inside_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="1"
android:orientation="horizontal"
android:visibility="gone">
<ImageView
android:id="#+id/ad_app_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:maxWidth="85dp"
android:scaleType="centerCrop" />
<TextView
android:id="#+id/ad_headline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:maxLines="3"
android:paddingLeft="5dp"
android:paddingRight="5dp"
android:textSize="16sp"
android:textStyle="bold" />
<Button
android:id="#+id/ad_call_to_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:maxWidth="50dp"
android:maxLines="3"
android:textSize="12sp"
/>
</LinearLayout>
</LinearLayout>
</com.google.android.gms.ads.formats.UnifiedNativeAdView>
Code of Viewholder:
inner class TripViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val adView: UnifiedNativeAdView = view.native_ad_view as UnifiedNativeAdView
var layoutView: LinearLayout = view.native_ad_layout
var insideLayoutView: LinearLayout = view.native_ad_inside_layout
var attributionView: TextView = view.native_ad_attribution
var headlineView: TextView = view.ad_headline
var callToActionView: Button = view.ad_call_to_action
var iconView: ImageView = view.ad_app_icon
}
Code of onBindViewHolder:
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = mValues[position]
if (item.nativeAd == null) {
return
}
if (item.nativeAd!!.headline == null) {
return
}
holder.attributionView.setVisibility(View.VISIBLE);
holder.insideLayoutView.setVisibility(View.VISIBLE);
holder.headlineView.text = item.nativeAd!!.headline
if (item.nativeAd!!.callToAction == null) {
holder.callToActionView.visibility = View.INVISIBLE
} else {
holder.callToActionView.visibility = View.VISIBLE
(holder.callToActionView as Button).text = item.nativeAd!!.callToAction
}
if (item.nativeAd!!.icon == null) {
holder.iconView.visibility = View.GONE
} else {
(holder.iconView as ImageView).setImageDrawable(item.nativeAd!!.icon.drawable)
holder.iconView.visibility = View.VISIBLE
}
// Assign native ad object to the native view.
holder.adView.setNativeAd(item.nativeAd!!)
}
And this is where I call adLoader. I call it in the background using BroadcastReceiver:
fun loadNativeAds(context: Context, listener: MyTripsContent.ContentEventsListener?) {
if (!adList.isEmpty()) {
return
}
try {
var unitId: String? = ""
lateinit var adLoader: AdLoader
//test key
unitId = "ca-app-pub-3940256099942544/2247696110"
adLoader = AdLoader.Builder(context, unitId)
.forUnifiedNativeAd { ad: UnifiedNativeAd ->
// Show the ad.
adList.add(ad)
if (adList.size == 5) {
listener?.onLoadedDocuments(0)
}
}
.withAdListener(object : AdListener() {
override fun onAdFailedToLoad(errorCode: Int) {
// Handle the failure by logging, altering the UI, and so on.
if (!adLoader.isLoading) {
}
}
override fun onAdClicked() {
super.onAdClicked()
}
})
.withNativeAdOptions(
NativeAdOptions.Builder()
// Methods in the NativeAdOptions.Builder class can be
// used here to specify individual options settings.
.build()
)
.build()
adLoader.loadAds(AdRequest.Builder().build(), 5)
} catch (exception: Exception) {
exception.printStackTrace()
}
}
So as I mentioned ads work well but they are not clickable. I'm not sure if this is a bug from admob, or my code or something I am missing. I think other developers will face the same issue in the future, so it would be good to solve it.
Thank you in advance.
So due to lack of detailed documentation and examples I found I was missing one line of code. Many other examples do not have this line also. I only added one line in onBindViewHolder
From this:
if (item.nativeAd!!.callToAction == null) {
holder.callToActionView.visibility = View.INVISIBLE
} else {
holder.callToActionView.visibility = View.VISIBLE
(holder.callToActionView as Button).text = item.nativeAd!!.callToAction
}
to this:
if (item.nativeAd!!.callToAction == null) {
holder.callToActionView.visibility = View.INVISIBLE
} else {
holder.callToActionView.visibility = View.VISIBLE
(holder.callToActionView as Button).text = item.nativeAd!!.callToAction
holder.adView.callToActionView = (holder.callToActionView as Button)
}
That means I needed to attach my button as callToActionView

How to crop image rectangle in camera preview on CameraX

I have a custom camera app which has a centered rectangle view, as you can see below:
When I take a picture I want to ignore everything outside the rectangle. And this is my XML layout:
<?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"
android:background="#color/black_50">
<TextureView
android:id="#+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="250dp"
android:layout_margin="16dp"
android:background="#drawable/rectangle"
app:layout_constraintBottom_toTopOf="#+id/cameraBottomView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="#+id/cameraBottomView"
android:layout_width="match_parent"
android:layout_height="130dp"
android:background="#color/black_50"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageButton
android:id="#+id/cameraCaptureImageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/transparent"
android:src="#drawable/ic_capture_image"
app:layout_constraintBottom_toBottomOf="#id/cameraBottomView"
app:layout_constraintEnd_toEndOf="#id/cameraBottomView"
app:layout_constraintStart_toStartOf="#id/cameraBottomView"
app:layout_constraintTop_toTopOf="#id/cameraBottomView"
tools:ignore="ContentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
And this is my kotlin code for the cameraX preview:
class CameraFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_camera, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewFinder.post { setupCamera() }
}
private fun setupCamera() {
CameraX.unbindAll()
CameraX.bindToLifecycle(
this,
buildPreviewUseCase(),
buildImageCaptureUseCase(),
buildImageAnalysisUseCase()
)
}
private fun buildPreviewUseCase(): Preview {
val preview = Preview(
UseCaseConfigBuilder.buildPreviewConfig(
viewFinder.display
)
)
preview.setOnPreviewOutputUpdateListener { previewOutput ->
updateViewFinderWithPreview(previewOutput)
correctPreviewOutputForDisplay(previewOutput.textureSize)
}
return preview
}
private fun updateViewFinderWithPreview(previewOutput: Preview.PreviewOutput) {
val parent = viewFinder.parent as ViewGroup
parent.removeView(viewFinder)
parent.addView(viewFinder, 0)
viewFinder.surfaceTexture = previewOutput.surfaceTexture
}
/**
* Corrects the camera/preview's output to the display, by scaling
* up/down and/or rotating the camera/preview's output.
*/
private fun correctPreviewOutputForDisplay(textureSize: Size) {
val matrix = Matrix()
val centerX = viewFinder.width / 2f
val centerY = viewFinder.height / 2f
val displayRotation = getDisplayRotation()
val (dx, dy) = getDisplayScalingFactors(textureSize)
matrix.postRotate(displayRotation, centerX, centerY)
matrix.preScale(dx, dy, centerX, centerY)
// Correct preview output to account for display rotation and scaling
viewFinder.setTransform(matrix)
}
private fun getDisplayRotation(): Float {
val rotationDegrees = when (viewFinder.display.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> throw IllegalStateException("Unknown display rotation ${viewFinder.display.rotation}")
}
return -rotationDegrees.toFloat()
}
private fun getDisplayScalingFactors(textureSize: Size): Pair<Float, Float> {
val cameraPreviewRation = textureSize.height / textureSize.width.toFloat()
val scaledWidth: Int
val scaledHeight: Int
if (viewFinder.width > viewFinder.height) {
scaledHeight = viewFinder.width
scaledWidth = (viewFinder.width * cameraPreviewRation).toInt()
} else {
scaledHeight = viewFinder.height
scaledWidth = (viewFinder.height * cameraPreviewRation).toInt()
}
val dx = scaledWidth / viewFinder.width.toFloat()
val dy = scaledHeight / viewFinder.height.toFloat()
return Pair(dx, dy)
}
private fun buildImageCaptureUseCase(): ImageCapture {
val capture = ImageCapture(
UseCaseConfigBuilder.buildImageCaptureConfig(
viewFinder.display
)
)
cameraCaptureImageButton.setOnClickListener {
capture.takePicture(
FileCreator.createTempFile(JPEG_FORMAT),
Executors.newSingleThreadExecutor(),
object : ImageCapture.OnImageSavedListener {
override fun onImageSaved(file: File) {
requireActivity().runOnUiThread {
launchGalleryFragment(file.absolutePath)
}
}
override fun onError(
imageCaptureError: ImageCapture.ImageCaptureError,
message: String,
cause: Throwable?
) {
Toast.makeText(requireContext(), "Error: $message", Toast.LENGTH_LONG)
.show()
Log.e("CameraFragment", "Capture error $imageCaptureError: $message", cause)
}
})
}
return capture
}
private fun buildImageAnalysisUseCase(): ImageAnalysis {
val analysis = ImageAnalysis(
UseCaseConfigBuilder.buildImageAnalysisConfig(
viewFinder.display
)
)
analysis.setAnalyzer(
Executors.newSingleThreadExecutor(),
ImageAnalysis.Analyzer { image, rotationDegrees ->
Log.d(
"CameraFragment",
"Image analysis: $image - Rotation degrees: $rotationDegrees"
)
})
return analysis
}
private fun launchGalleryFragment(path: String) {
val action = CameraFragmentDirections.actionLaunchGalleryFragment(path)
findNavController().navigate(action)
}
}
And when I take the picture and send it into new page (GalleryPage), it's show all screen from the camera preview as you can see below:
And this is the kotlin code to get the picture from cameraX preview and display it into ImageView:
class GalleryFragment : Fragment() {
private lateinit var imageView: ImageView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_gallery, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
imageView = view.findViewById(R.id.img)
val imageFilePath = GalleryFragmentArgs.fromBundle(arguments!!).data
val bitmap = BitmapFactory.decodeFile(imageFilePath)
val rotatedBitmap = bitmap.rotate(90)
if (imageFilePath.isBlank()) {
Log.i(
"GalleryFragment",
"Image is Null or Empty"
)
} else {
Glide.with(activity!!)
.load(rotatedBitmap)
.into(imageView)
}
}
private fun Bitmap.rotate(degree:Int):Bitmap{
// Initialize a new matrix
val matrix = Matrix()
// Rotate the bitmap
matrix.postRotate(degree.toFloat())
// Resize the bitmap
val scaledBitmap = Bitmap.createScaledBitmap(
this,
width,
height,
true
)
// Create and return the rotated bitmap
return Bitmap.createBitmap(
scaledBitmap,
0,
0,
scaledBitmap.width,
scaledBitmap.height,
matrix,
true
)
}
}
Can somebody help me how to crop the image properly? Because I already search and research how to do it but still confused and not working for me.
I found a simple and straight forward way of doing this using camerax configuration.
Get the height and width of your rectangle shape of the preview area that you need from the camera preview.
For example
<View
android:background="#drawable/background_drawable"
android:id="#+id/border_view"
android:layout_gravity="center"
android:layout_width="350dp"
android:layout_height="100dp"/>
The width of mine is 350dp and a height of 100dp
Then use ViewPort to get the area you need
val viewPort = ViewPort.Builder(Rational(width, height), rotation).build()
//width = 350, height = 100, rotation = Surface.ROTATION_0
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(preview) //your preview
.addUseCase(imageAnalysis) //if you are using imageAnalysis
.addUseCase(imageCapture)
.setViewPort(viewPort)
.build()
Then bind to LifeCycle of CameraProvider
cameraProvider.bindToLifecycle(this, cameraSelector, useCaseGroup)
Use this link CropRect for more information
If you need any help comment below, I can provide you with the working source code.
Edit
Link to Source Code Sample
I have a solution, I just use this function to cropping the Image after capturing the Image:
private fun cropImage(bitmap: Bitmap, frame: View, reference: View): ByteArray {
val heightOriginal = frame.height
val widthOriginal = frame.width
val heightFrame = reference.height
val widthFrame = reference.width
val leftFrame = reference.left
val topFrame = reference.top
val heightReal = bitmap.height
val widthReal = bitmap.width
val widthFinal = widthFrame * widthReal / widthOriginal
val heightFinal = heightFrame * heightReal / heightOriginal
val leftFinal = leftFrame * widthReal / widthOriginal
val topFinal = topFrame * heightReal / heightOriginal
val bitmapFinal = Bitmap.createBitmap(
bitmap,
leftFinal, topFinal, widthFinal, heightFinal
)
val stream = ByteArrayOutputStream()
bitmapFinal.compress(
Bitmap.CompressFormat.JPEG,
100,
stream
) //100 is the best quality possibe
return stream.toByteArray()
}
Crop an image taking a reference a view parent like a frame and a view child like final reference
param bitmap image to crop
param frame where the image is set it
param reference frame to take reference for a crop the image
return image already cropped
You can see this example: https://github.com/rrifafauzikomara/CustomCamera/tree/custom_camerax
Here is an example of how I'm cropping an image taken by cameraX as you mentioned. I don't know if it the best way to do it and I'm interested to know other solutions.
camerax_version = "1.0.0-alpha07"
CameraFragment.java
Initialize cameraX :
// Views
private PreviewView previewView;
// CameraX
private ProcessCameraProvider cameraProvider;
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
private CameraSelector cameraSelector;
private Executor executor;
private ImageCapture imageCapture;
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
cameraProviderFuture = ProcessCameraProvider.getInstance(getContext());
executor = ContextCompat.getMainExecutor(getContext());
cameraSelector = new CameraSelector.Builder().requireLensFacing(LensFacing.BACK).build();
}
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
previewView = view.findViewById(R.id.preview);
ImageButton btnCapture = view.findViewById(R.id.btn_capture);
// Wait for the view to be properly laid out
previewView.post(() ->{
//Initialize CameraX
cameraProviderFuture.addListener(() -> {
if(cameraProvider != null) cameraProvider.unbindAll();
try {
cameraProvider = cameraProviderFuture.get();
// Set up the preview use case to display camera preview
Preview preview = new Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.setTargetRotation(previewView.getDisplay().getRotation())
.build();
preview.setPreviewSurfaceProvider(previewView.getPreviewSurfaceProvider());
// Set up the capture use case to allow users to take photos
imageCapture = new ImageCapture.Builder()
.setCaptureMode(ImageCapture.CaptureMode.MINIMIZE_LATENCY)
.setTargetRotation(previewView.getDisplay().getRotation())
.setTargetAspectRatio(AspectRatio.RATIO_4_3)
.build();
// Apply declared configs to CameraX using the same lifecycle owner
cameraProvider.bindToLifecycle(this, cameraSelector, preview,imageCapture);
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}, ContextCompat.getMainExecutor(getContext()));
});
btnCapture.setOnClickListener(v -> {
String format = "yyyy-MM-dd-HH-mm-ss-SSS";
SimpleDateFormat fmt = new SimpleDateFormat(format, Locale.US);
String date = fmt.format(System.currentTimeMillis());
File file = new File(getContext().getCacheDir(), date+".jpg");
imageCapture.takePicture(file, executor, imageSavedListener);
});
}
When a photo has been taken, open the gallery fragment passing the path of the photo :
private ImageCapture.OnImageSavedCallback imageSavedListener = new ImageCapture.OnImageSavedCallback() {
#Override
public void onImageSaved(#NonNull File photoFile) {
// Create new fragment and transaction
Fragment newFragment = new GalleryFragment();
FragmentTransaction transaction = getActivity().getSupportFragmentManager().beginTransaction();
// Set arguments
Bundle args = new Bundle();
args.putString("KEY_PATH", Uri.fromFile(photoFile).toString());
newFragment.setArguments(args);
// Replace whatever is in the fragment_container view with this fragment,
transaction.replace(R.id.fragment_container, newFragment,null);
transaction.addToBackStack(null);
// Commit the transaction
transaction.commit();
}
#Override
public void onError(int imageCaptureError, #NonNull String message, #Nullable Throwable cause) {
if (cause != null) {
cause.printStackTrace();
}
}
};
At this moment, the photo has not been cropped, I don't know if it possible to do it directly with cameraX.
GalleryFragment.java
Load the argument passed to the fragment.
#Override
public void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String path = getArguments().getString("KEY_PATH");
sourceUri = Uri.parse(path);
}
Load the Uri with glide in an ImageView and then crop it.
#Override
public void onViewCreated(#NonNull View view, #Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// Initialize the views
ImageView imageView = view.findViewById(R.id.image_view);
View cropArea = view.findViewById(R.id.crop_area);
// Display the image
Glide.with(this).load(sourceUri).listener(new RequestListener<Drawable>() {
#Override
public boolean onLoadFailed(#Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
return false;
}
#Override
public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
// Get original bitmap
sourceBitmap = ((BitmapDrawable)resource).getBitmap();
// Create a new bitmap corresponding to the crop area
int[] cropAreaXY = new int[2];
int[] placeHolderXY = new int[2];
Rect rect = new Rect();
imageView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
#Override
public boolean onPreDraw() {
try {
imageView.getLocationOnScreen(placeHolderXY);
cropArea.getLocationOnScreen(cropAreaXY);
cropArea.getGlobalVisibleRect(rect);
croppedBitmap = Bitmap.createBitmap(sourceBitmap, cropAreaXY[0], cropAreaXY[1] - placeHolderXY[1], rect.width(), rect.height());
// Save the croppedBitmap if you wish
getActivity().runOnUiThread(() -> imageView.setImageBitmap(croppedBitmap));
return true;
}finally {
imageView.getViewTreeObserver().removeOnPreDrawListener(this);
}
}
});
return false;
}
}).into(imageView);
}
fragment_camera.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/black">
<androidx.camera.view.PreviewView
android:id="#+id/preview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="3:4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="#+id/crop_area"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
android:background="#drawable/rectangle_round_corners"
app:layout_constraintBottom_toBottomOf="#+id/preview"
app:layout_constraintDimensionRatio="4.5:3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="#+id/cameraBottomView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="#android:color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/preview" />
<ImageButton
android:id="#+id/btn_capture"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="#drawable/ic_shutter"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#+id/preview" />
</androidx.constraintlayout.widget.ConstraintLayout>
fragment_gallery.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/layout_main"
android:background="#android:color/black"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="#+id/image_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="#+id/crop_area"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="4.5:3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
If you want the image to be cropped to whichever your PreviewView is showing, just do:
val useCaseGroup = UseCaseGroup.Builder()
.addUseCase(preview!!)
.addUseCase(imageCapture!!)
.setViewPort(previewView.viewPort!!)
.build()
camera = cameraProvider.bindToLifecycle(
this, cameraSelector, useCaseGroup)