Recently, I was creating a test app to familiarize myself with RecyclerViewand the Android Palette library when I came across this semantic error in my fragment that deals with Palette. When I take a picture in the fragment, it saves the photo in the File for the current orientation, for the landscape orientation but when I rotate my phone back to portrait, the File resets back to null. I have discovered this based off my Log tests and reading stack traces.
Currently I've wrapped the null absolute path in a null check to prevent further errors but I'm not sure how to proceed. Below is my Kotlin file.
class PicFragment : Fragment() {
private var imgFile: File? = null
private lateinit var cameraPic: ImageView
private lateinit var cycleLayout: View
private var swatchIndex: Int = 0
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view: View? = inflater?.inflate(R.layout.camera_fragment, container, false)
// init
val cameraButton: ImageButton = view!!.findViewById(R.id.click_pic)
val colorCycler: ImageButton = view.findViewById(R.id.color_clicker)
cameraPic = view.findViewById(R.id.camera_pic)
cycleLayout = view.findViewById(R.id.color_selector)
val swatchDisplay: ImageView = view.findViewById(R.id.main_color)
val swatchName: TextView = view.findViewById(R.id.main_color_name)
// restoring the picture taken if it exists
if(savedInstanceState != null){
val path: String? = savedInstanceState.getString("imageFile")
swatchIndex = savedInstanceState.getInt("swatchIndex")
if(path != null) {
val bm: Bitmap = BitmapFactory.decodeFile(path)
cameraPic.setImageBitmap(bm)
animateColorSlides(cycleLayout, duration = 500)
}
}
// taking the picture (full size)
cameraButton.setOnClickListener { _ ->
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
if (intent.resolveActivity(context.packageManager) != null){
imgFile = createFileName()
val photoURI = FileProvider.getUriForFile(context, "com.github.astronoodles.provider", imgFile)
grantUriPermissions(intent, photoURI)
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
startActivityForResult(intent, 3)
}
}
// Palette Button (click to go through color values)
colorCycler.setOnClickListener { _ ->
if(cameraPic.drawable is BitmapDrawable){
val img: Bitmap = (cameraPic.drawable as BitmapDrawable).bitmap
Palette.from(img).generate { palette ->
val swatches = palette.swatches
Log.d(MainActivity.TAG, "Swatch Size: ${swatches.size}")
Log.d(MainActivity.TAG, "Counter: $swatchIndex")
val hexCode = "#${Integer.toHexString(swatches[swatchIndex++ % swatches.size].rgb)}"
swatchName.text = hexCode
animateColorDrawableFade(context, swatchDisplay, hexCode)
}
} else Log.e(MainActivity.TAG, "No bitmap found! Cannot cycle images...")
}
return view
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
outState?.putString("imageFile", imgFile?.absolutePath)
outState?.putInt("swatchIndex", swatchIndex)
}
/**
* Animates the color of an ImageView using its image drawable
* #author Michael + StackOverflow
* #since 6/24/18
* #param ctx Context needed to load the animations
* #param target Target ImageView for switching colors
* #param hexCode The hex code of the colors switching in
*/
private fun animateColorDrawableFade(ctx: Context, target: ImageView, hexCode: String){
val fadeOut = AnimationUtils.loadAnimation(ctx, android.R.anim.fade_out)
val fadeIn = AnimationUtils.loadAnimation(ctx, android.R.anim.fade_in)
fadeOut.setAnimationListener(object: Animation.AnimationListener {
override fun onAnimationStart(animation: Animation?) {}
override fun onAnimationRepeat(animation: Animation?) {}
override fun onAnimationEnd(animation: Animation?) {
target.setImageDrawable(ColorDrawable(Color.parseColor(hexCode)))
target.startAnimation(fadeIn)
}
})
target.startAnimation(fadeOut)
}
/**
* Helper method for animating a layout's visibility from invisible and visible
* #author Michael
* #param layout The layout to animate
* #param duration The length of the alpha animation.
*/
private fun animateColorSlides(layout: View, duration: Long){
layout.alpha = 0f
layout.visibility = View.VISIBLE
layout.animate().alpha(1f).setListener(null).duration = duration
}
/**
* Creates an unique name for the file as suggested here using a SimpleDateFormat
* #author Michael
* #returns A (temporary?) file linking to where the photo will be saved.
*/
private fun createFileName(): File {
val timeStamp: String = SimpleDateFormat("yyyyMd_km", Locale.US).format(Date())
val jpegTitle = "JPEG_${timeStamp}_"
val directory: File = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
try {
return File.createTempFile(jpegTitle, ".png", directory)
} catch (e: IOException) {
e.printStackTrace()
}
return File(directory, "$jpegTitle.jpg")
}
/**
* Grants URI permissions for the file provider to successfully save the full size file. <br>
* Code borrowed from https://stackoverflow.com/questions/18249007/how-to-use-support-fileprovider-for-sharing-content-to-other-apps
* #param intent The intent to send the photo
* #param uri The URI retrieved from the FileProvider
* #author Michael and Leszek
*/
private fun grantUriPermissions(intent: Intent, uri: Uri){
val intentHandleList: List<ResolveInfo> = context.packageManager.queryIntentActivities(intent,
PackageManager.MATCH_DEFAULT_ONLY)
intentHandleList.forEach {
val packageName: String = it.activityInfo.packageName
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if(requestCode == 3 && resultCode == Activity.RESULT_OK){
val bitmap: Bitmap = BitmapFactory.decodeFile(imgFile!!.absolutePath)
cameraPic.setImageBitmap(bitmap)
animateColorSlides(cycleLayout, duration = 2000)
}
}
}
I also have my WRITE_EXTERNAL_STORAGE permission in the manifest if that helps.
Thanks.
From the Android activity lifecycle documentation, this is the relevant part:
If you override onSaveInstanceState(), you must call the superclass implementation if you want the default implementation to save the state of the view hierarchy
Which will give you something like this:
override fun onSaveInstanceState(outState: Bundle?) {
outState?.putString("imageFile", imgFile?.absolutePath)
outState?.putInt("swatchIndex", swatchIndex)
// Always call the superclass so it can save the view hierarchy state
super.onSaveInstanceState(outState)
}
Related
I clicked the download button in the song list activity, but after a few seconds, the system recycled my current activity, and did not generate an error, but entered the onDestroy() method.
Specific steps: Start a service, download it in the service, and call back to the page to update the progress through EventBus. I haven't started that activity.
-I want to know why I recycled my current activity
my code is as following:
fun download(){
if (downloadList.size == 0)return
if (current >= downloadList.size){
ToastUtil.setSuccessToast(HomePageActivity.MA,"下载完成!")
current = 0
downloadList.clear()
return
}
DownloadUtils.startDownload(downloadList[current],object : DownloadUtils.FileDownloaderCallback{
override fun pending(task: BaseDownloadTask) {
//status = 6
EventBus.getDefault().postSticky(DownloadingBean(task.status,"",percentage,"", downloadList[current]))
}
override fun start(task: BaseDownloadTask) {
//已经进入下载队列,正在等待下载
//status = 6
EventBus.getDefault().postSticky(DownloadingBean(task.status,"",percentage,"", downloadList[current]))
}
override fun running(task: BaseDownloadTask, speed: Int, current: Int, total: Int) {
//status = 3
// kb/s-> KB/s
Log.d("downloadTAG","running:$speed")
percentage = ((current*1.0 /total)*100).toInt()
EventBus.getDefault().postSticky(DownloadingBean(task.status,"${remainDigit(speed/8.0)}KB/s",percentage,"", downloadList[DownloadBinder.current]))
}
override fun pause(task: BaseDownloadTask) {
Log.d("downloadTAG","pause:${task.status}")
EventBus.getDefault().postSticky(DownloadingBean(task.status,"",percentage,"", downloadList[current]))
}
override fun completed(task: BaseDownloadTask) {
//status = -3
/**
* 除2个1024的到大小MB
* 记得最后保留一位小数*/
val primary = "${downloadList[current].songId}${downloadList[current].songName}"
/**
* 下载完成之后,更新数据库字段*/
PlayListDataBase.getDBInstance().downloadDao().updateComplete(primary,true)
PlayListDataBase.getDBInstance().downloadDao().updatePath(primary,task.path)
PlayListDataBase.getDBInstance().downloadDao().updateUrl(primary,task.url)
val size = remainDigit(task.smallFileTotalBytes*1.0/1024/1024)
PlayListDataBase.getDBInstance().downloadDao().updateSize(primary,"${size}MB")
EventBus.getDefault().postSticky(DownloadingBean(task.status,"",percentage,"", downloadList[current]))
current++
download()
}
override fun failed(task: BaseDownloadTask, message: String?) {
// error = -1
Log.d("downloadTAG","failed:${task.status}")
Log.d("downloadTAG","failed:$message")
EventBus.getDefault().postSticky(DownloadingBean(task.status,"",percentage,message!!, downloadList[current]))
}
override fun exist(task: BaseDownloadTask) {
/**
* 不会进入此处
* 因为外面已经判断过重复项*/
Log.d("downloadTAG","exist:${task.status}")
EventBus.getDefault().postSticky(DownloadingBean(task.status,"",percentage,"", downloadList[current]))
}
})
}
I have added CameraX to my ongoing development app a while ago. I know it was in alpha but I was ready to make the change when beta or final release will be available.
So I started working on it today. I have updated from
implementation 'androidx.camera:camera-core:1.0.0-alpha04'
implementation 'androidx.camera:camera-camera2:1.0.0-alpha04'
to this:
implementation 'androidx.camera:camera-core:1.0.0-beta01'
implementation 'androidx.camera:camera-camera2:1.0.0-beta01'
implementation 'androidx.camera:camera-lifecycle:1.0.0-beta01'
My Previous Working Code (alpha-04):
class ScannerX : AppCompatActivity() {
private lateinit var context: Context
var isOtpAuthCode = true
private val immersiveFlagTimeout = 500L
private val flagsFullscreen = View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
private var preview: Preview? = null
private var lensFacing = CameraX.LensFacing.BACK
private var imageAnalyzer: ImageAnalysis? = null
private lateinit var analyzerThread: HandlerThread
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scanner_x)
context = this
btnCancel.setOnClickListener {
finish()
}
analyzerThread = if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) {
HandlerThread("BarcodeFirebaseAnalyzer").apply { start() }
} else {
HandlerThread("BarcodeZxingAnalyzer").apply { start() }
}
Dexter.withActivity(this)
.withPermissions(Manifest.permission.CAMERA)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
textureView.post {
val metrics = DisplayMetrics().also { textureView.display.getRealMetrics(it) }
val screenAspectRatio = Rational(metrics.widthPixels, metrics.heightPixels)
val previewConfig = PreviewConfig.Builder().apply {
setLensFacing(lensFacing)
// We request aspect ratio but no resolution to let CameraX optimize our use cases
setTargetAspectRatio(screenAspectRatio)
// Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
setTargetRotation(textureView.display.rotation)
}.build()
val analyzerConfig = ImageAnalysisConfig.Builder().apply {
setLensFacing(lensFacing)
// Use a worker thread for image analysis to prevent preview glitches
setCallbackHandler(Handler(analyzerThread.looper))
// In our analysis, we care more about the latest image than analyzing *every* image
setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
// Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
setTargetRotation(textureView.display.rotation)
}.build()
preview = AutoFitPreviewBuilder.build(previewConfig, textureView)
imageAnalyzer = ImageAnalysis(analyzerConfig).apply {
analyzer = if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) {
BarcodeFirebaseAnalyzer { qrCode ->
if (isOtpAuthCode) {
if (qrCode.startsWith("otpauth")) {
toAddAuth(qrCode)
}
} else {
toAddAuth(qrCode)
}
}
} else {
BarcodeZxingAnalyzer { qrCode ->
if (isOtpAuthCode) {
if (qrCode.startsWith("otpauth")) {
toAddAuth(qrCode)
}
} else {
toAddAuth(qrCode)
}
}
}
}
// Apply declared configs to CameraX using the same lifecycle owner
CameraX.bindToLifecycle(this#ScannerX, preview, imageAnalyzer)
}
}
override fun onPermissionRationaleShouldBeShown(permissions: MutableList<PermissionRequest>?, token: PermissionToken?) {
//
}
}).check()
}
override fun onStart() {
super.onStart()
// Before setting full screen flags, we must wait a bit to let UI settle; otherwise, we may
// be trying to set app to immersive mode before it's ready and the flags do not stick
textureView.postDelayed({
textureView.systemUiVisibility = flagsFullscreen
}, immersiveFlagTimeout)
}
override fun onDestroy() {
analyzerThread.quit()
super.onDestroy()
}
private fun toAddAuth(scannedCode: String) {
if (CameraX.isBound(imageAnalyzer)) {
CameraX.unbind(imageAnalyzer)
}
val intent = Intent()
intent.putExtra("scanResult", scannedCode)
setResult(RESULT_OK, intent)
finish()
}
companion object {
private const val RESULT_OK = 666
}
}
And the code I have changed is as follows (beta-01):
class ScannerX : AppCompatActivity() {
private lateinit var context: Context
var isOtpAuthCode = true
private val immersiveFlagTimeout = 500L
private val flagsFullscreen = View.SYSTEM_UI_FLAG_LOW_PROFILE or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
private var preview: Preview? = null
private var lensFacing = CameraSelector.DEFAULT_BACK_CAMERA
private var imageAnalyzer: ImageAnalysis? = null
private lateinit var analysisExecutor: ExecutorService
private lateinit var processCameraProvider: ListenableFuture<ProcessCameraProvider>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scanner_x)
context = this
btnCancel.setOnClickListener {
finish()
}
Dexter.withActivity(this)
.withPermissions(Manifest.permission.CAMERA)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
textureView.post {
analysisExecutor = Executors.newSingleThreadExecutor()
processCameraProvider = ProcessCameraProvider.getInstance(context)
preview = Preview.Builder()
.setTargetAspectRatio(AspectRatio.RATIO_16_9)
.setTargetRotation(textureView.display.rotation)
.build()
imageAnalyzer = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetRotation(textureView.display.rotation)
.build()
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS) {
imageAnalyzer?.apply {
setAnalyzer(analysisExecutor, BarcodeFirebaseAnalyzer { qrCode ->
if (isOtpAuthCode) {
if (qrCode.startsWith("otpauth")) {
toAddAuth(qrCode)
}
} else {
toAddAuth(qrCode)
}
})
}
} else {
imageAnalyzer?.apply {
setAnalyzer(analysisExecutor, BarcodeZxingAnalyzer { qrCode ->
if (isOtpAuthCode) {
if (qrCode.startsWith("otpauth")) {
toAddAuth(qrCode)
}
} else {
toAddAuth(qrCode)
}
})
}
}
processCameraProvider.get().bindToLifecycle(this#ScannerX, lensFacing, imageAnalyzer)
}
}
override fun onPermissionRationaleShouldBeShown(permissions: MutableList<PermissionRequest>?, token: PermissionToken?) {
//
}
}).check()
}
override fun onStart() {
super.onStart()
// Before setting full screen flags, we must wait a bit to let UI settle; otherwise, we may
// be trying to set app to immersive mode before it's ready and the flags do not stick
textureView.postDelayed({
textureView.systemUiVisibility = flagsFullscreen
}, immersiveFlagTimeout)
}
override fun onDestroy() {
if (!analysisExecutor.isShutdown) {
analysisExecutor.shutdown()
}
super.onDestroy()
}
private fun toAddAuth(scannedCode: String) {
/*if (CameraX.isBound(imageAnalyzer)) {
CameraX.unbind(imageAnalyzer)
}*/
val intent = Intent()
intent.putExtra("scanResult", scannedCode)
setResult(RESULT_OK, intent)
finish()
}
companion object {
private const val RESULT_OK = 666
}
}
After I upgraded there were so many changes in library and now I cant make it work.
I also cant use Google Provided AutoFitPreview Class along with initial alpha release of this library. This was not necessary even with alpha04 since the only problem without this class was camera view little bit stretched out but scanning and analyzing worked properly.
/**
* Builder for [Preview] that takes in a [WeakReference] of the view finder and [PreviewConfig],
* then instantiates a [Preview] which automatically resizes and rotates reacting to config changes.
*/
class AutoFitPreviewBuilder private constructor(config: PreviewConfig, viewFinderRef: WeakReference<TextureView>) {
/** Public instance of preview use-case which can be used by consumers of this adapter */
val useCase: Preview
/** Internal variable used to keep track of the use case's output rotation */
private var bufferRotation: Int = 0
/** Internal variable used to keep track of the view's rotation */
private var viewFinderRotation: Int? = null
/** Internal variable used to keep track of the use-case's output dimension */
private var bufferDimens: Size = Size(0, 0)
/** Internal variable used to keep track of the view's dimension */
private var viewFinderDimens: Size = Size(0, 0)
/** Internal variable used to keep track of the view's display */
private var viewFinderDisplay: Int = -1
/** Internal reference of the [DisplayManager] */
private lateinit var displayManager: DisplayManager
/**
* We need a display listener for orientation changes that do not trigger a configuration
* change, for example if we choose to override config change in manifest or for 180-degree
* orientation changes.
*/
private val displayListener = object : DisplayManager.DisplayListener {
override fun onDisplayAdded(displayId: Int) = Unit
override fun onDisplayRemoved(displayId: Int) = Unit
override fun onDisplayChanged(displayId: Int) {
val viewFinder = viewFinderRef.get() ?: return
if (displayId == viewFinderDisplay) {
val display = displayManager.getDisplay(displayId)
val rotation = getDisplaySurfaceRotation(display)
updateTransform(viewFinder, rotation, bufferDimens, viewFinderDimens)
}
}
}
init {
// Make sure that the view finder reference is valid
val viewFinder = viewFinderRef.get() ?:
throw IllegalArgumentException("Invalid reference to view finder used")
// Initialize the display and rotation from texture view information
viewFinderDisplay = viewFinder.display.displayId
viewFinderRotation = getDisplaySurfaceRotation(viewFinder.display) ?: 0
// Initialize public use-case with the given config
useCase = Preview(config)
// Every time the view finder is updated, recompute layout
useCase.onPreviewOutputUpdateListener = Preview.OnPreviewOutputUpdateListener {
val viewFinderI = viewFinderRef.get() ?: return#OnPreviewOutputUpdateListener
Log.d(TAG, "Preview output changed. " +
"Size: ${it.textureSize}. Rotation: ${it.rotationDegrees}")
// To update the SurfaceTexture, we have to remove it and re-add it
val parent = viewFinderI.parent as ViewGroup
parent.removeView(viewFinderI)
parent.addView(viewFinderI, 0)
// Update internal texture
viewFinderI.surfaceTexture = it.surfaceTexture
// Apply relevant transformations
bufferRotation = it.rotationDegrees
val rotation = getDisplaySurfaceRotation(viewFinderI.display)
updateTransform(viewFinderI, rotation, it.textureSize, viewFinderDimens)
}
// Every time the provided texture view changes, recompute layout
viewFinder.addOnLayoutChangeListener { view, left, top, right, bottom, _, _, _, _ ->
val viewFinderII = view as TextureView
val newViewFinderDimens = Size(right - left, bottom - top)
Log.d(TAG, "View finder layout changed. Size: $newViewFinderDimens")
val rotation = getDisplaySurfaceRotation(viewFinderII.display)
updateTransform(viewFinderII, rotation, bufferDimens, newViewFinderDimens)
}
// Every time the orientation of device changes, recompute layout
// NOTE: This is unnecessary if we listen to display orientation changes in the camera
// fragment and call [Preview.setTargetRotation()] (like we do in this sample), which will
// trigger [Preview.OnPreviewOutputUpdateListener] with a new
// [PreviewOutput.rotationDegrees]. CameraX Preview use case will not rotate the frames for
// us, it will just tell us about the buffer rotation with respect to sensor orientation.
// In this sample, we ignore the buffer rotation and instead look at the view finder's
// rotation every time [updateTransform] is called, which gets triggered by
// [CameraFragment] display listener -- but the approach taken in this sample is not the
// only valid one.
displayManager = viewFinder.context
.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
displayManager.registerDisplayListener(displayListener, null)
// Remove the display listeners when the view is detached to avoid holding a reference to
// it outside of the Fragment that owns the view.
// NOTE: Even though using a weak reference should take care of this, we still try to avoid
// unnecessary calls to the listener this way.
viewFinder.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(view: View?) =
displayManager.registerDisplayListener(displayListener, null)
override fun onViewDetachedFromWindow(view: View?) =
displayManager.unregisterDisplayListener(displayListener)
})
}
/** Helper function that fits a camera preview into the given [TextureView] */
private fun updateTransform(textureView: TextureView?, rotation: Int?, newBufferDimens: Size, newViewFinderDimens: Size) {
// This should not happen anyway, but now the linter knows
val textureViewI = textureView ?: return
if (rotation == viewFinderRotation &&
Objects.equals(newBufferDimens, bufferDimens) &&
Objects.equals(newViewFinderDimens, viewFinderDimens)) {
// Nothing has changed, no need to transform output again
return
}
if (rotation == null) {
// Invalid rotation - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
viewFinderRotation = rotation
}
if (newBufferDimens.width == 0 || newBufferDimens.height == 0) {
// Invalid buffer dimens - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
bufferDimens = newBufferDimens
}
if (newViewFinderDimens.width == 0 || newViewFinderDimens.height == 0) {
// Invalid view finder dimens - wait for valid inputs before setting matrix
return
} else {
// Update internal field with new inputs
viewFinderDimens = newViewFinderDimens
}
val matrix = Matrix()
Log.d(TAG, "Applying output transformation.\n" +
"View finder size: $viewFinderDimens.\n" +
"Preview output size: $bufferDimens\n" +
"View finder rotation: $viewFinderRotation\n" +
"Preview output rotation: $bufferRotation")
// Compute the center of the view finder
val centerX = viewFinderDimens.width / 2f
val centerY = viewFinderDimens.height / 2f
// Correct preview output to account for display rotation
matrix.postRotate(-viewFinderRotation!!.toFloat(), centerX, centerY)
// Buffers are rotated relative to the device's 'natural' orientation: swap width and height
val bufferRatio = bufferDimens.height / bufferDimens.width.toFloat()
val scaledWidth: Int
val scaledHeight: Int
// Match longest sides together -- i.e. apply center-crop transformation
if (viewFinderDimens.width > viewFinderDimens.height) {
scaledHeight = viewFinderDimens.width
scaledWidth = (viewFinderDimens.width * bufferRatio).roundToInt()
} else {
scaledHeight = viewFinderDimens.height
scaledWidth = (viewFinderDimens.height * bufferRatio).roundToInt()
}
// Compute the relative scale value
val xScale = scaledWidth / viewFinderDimens.width.toFloat()
val yScale = scaledHeight / viewFinderDimens.height.toFloat()
// Scale input buffers to fill the view finder
matrix.preScale(xScale, yScale, centerX, centerY)
// Finally, apply transformations to our TextureView
textureViewI.setTransform(matrix)
}
companion object {
private val TAG = AutoFitPreviewBuilder::class.java.simpleName
/** Helper function that gets the rotation of a [Display] in degrees */
fun getDisplaySurfaceRotation(display: Display?) = when(display?.rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> null
}
/**
* Main entry point for users of this class: instantiates the adapter and returns an instance
* of [Preview] which automatically adjusts in size and rotation to compensate for
* config changes.
*/
fun build(config: PreviewConfig, viewFinder: TextureView) =
AutoFitPreviewBuilder(config, WeakReference(viewFinder)).useCase
}
}
Please Help
I'm working on an ExoPlayer based media player for Android, and I'm attempting to write my own Equalizer.
I've looked fairly deeply into ExoPlayer, and I believe the best place to manipulate samples in order to apply Equalier changes, is in a custom AudioProcessor.
I've used ChannelMappingAudioProcessor as a starting point, and cloned what I think are the relevant aspects:
class EqualizerAudioProcessor : BaseAudioProcessor() {
private lateinit var outputChannels: IntArray
override fun configure(sampleRateHz: Int, channelCount: Int, encoding: Int): Boolean {
outputChannels = IntArray(channelCount)
for (i in 0 until channelCount) {
outputChannels[i] = i
}
return true
}
override fun isActive(): Boolean {
return true
}
override fun getOutputChannelCount(): Int {
return outputChannels.size
}
override fun queueInput(inputBuffer: ByteBuffer) {
var position = inputBuffer.position()
val limit = inputBuffer.limit()
val frameCount = (limit - position) / (2 * channelCount)
val outputSize = frameCount * outputChannels.size * 2
val buffer = replaceOutputBuffer(outputSize)
while (position < limit) {
for (element in outputChannels) {
var sample = inputBuffer.getShort(position + 2 * element)
// Todo: Manipulate sample
buffer.putShort(sample)
}
position += channelCount * 2
}
inputBuffer.position(limit)
buffer.flip()
}
override fun onReset() {
}
}
It seems that if I enable this AudioProcessor, playback doesn't occur (it seems stuck in a 'paused state', as if the samples aren't being passed along, and interestingly, queueInput() is not called. If I disable the AudioProcessor, playback works fine.
I'm hoping someone can help me understand if I'm making a mistake here, and how to get this working.
For reference, the ExoPlayer instance is initialised like so:
private fun initPlayer(context: Context): ExoPlayer {
val audioProcessor = EqualizerAudioProcessor()
val renderersFactory = object : DefaultRenderersFactory(context) {
override fun buildAudioProcessors(): Array<AudioProcessor> {
return arrayOf(audioProcessor)
}
}
val player: SimpleExoPlayer = ExoPlayerFactory.newSimpleInstance(
context,
renderersFactory,
DefaultTrackSelector(),
DefaultLoadControl()
)
player.addListener(object : Player.EventListener {
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
callback?.onPlayStateChanged(playWhenReady)
}
})
return player
}
Thanks in advance
The problem is that you must call setInputFormat() in configure() of the AudioProcessor, or queueInput() will not be called.
I would like to implement ARCore with Twilio's video call. The documentation says this is possible but I could not figure out how to do it. Can Anyone tell me what I'm doing wrong?
This is my activity:
class MixActivity : AppCompatActivity() {
private lateinit var mArFragment: ArFragment
private lateinit var mVideoView: ArSceneView
private var mScreenVideoTrack: LocalVideoTrack? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_mix)
mArFragment = ar_fragment as ArFragment
mVideoView = mArFragment.arSceneView
mScreenVideoTrack = LocalVideoTrack.create(this, true,
ViewCapturer(mVideoView)
)
} }
This is view:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/ar_fragment"
android:name="com.google.ar.sceneform.ux.ArFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
And VideoCapture:
internal class ViewCapturer(private val view: View) : VideoCapturer, PixelCopy.OnPixelCopyFinishedListener {
private val handler = Handler(Looper.getMainLooper())
private var videoCapturerListener: VideoCapturer.Listener? = null
private val started = AtomicBoolean(false)
private lateinit var mViewBitmap: Bitmap
private val viewCapturer = object : Runnable {
override fun run() {
val dropFrame = view.width == 0 || view.height == 0
// Only capture the view if the dimensions have been established
if (!dropFrame) {
// Draw view into bitmap backed canvas
val measuredWidth = View.MeasureSpec.makeMeasureSpec(
view.width,
View.MeasureSpec.EXACTLY
)
val measuredHeight = View.MeasureSpec.makeMeasureSpec(
view.height,
View.MeasureSpec.EXACTLY
)
view.measure(measuredWidth, measuredHeight)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
mViewBitmap = Bitmap.createBitmap(
view.width, view.height,
Bitmap.Config.ARGB_8888
)
val viewCanvas = Canvas(mViewBitmap)
view.draw(viewCanvas)
// Extract the frame from the bitmap
val bytes = mViewBitmap.byteCount
val buffer = ByteBuffer.allocate(bytes)
mViewBitmap.copyPixelsToBuffer(buffer)
val array = buffer.array()
val captureTimeNs = TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime())
// Create video frame
val dimensions = VideoDimensions(view.width, view.height)
val videoFrame = VideoFrame(
array,
dimensions, VideoFrame.RotationAngle.ROTATION_0, captureTimeNs
)
// Notify the listener
if (started.get()) {
videoCapturerListener!!.onFrameCaptured(videoFrame)
}
}
// Schedule the next capture
if (started.get()) {
handler.postDelayed(this, VIEW_CAPTURER_FRAMERATE_MS.toLong())
}
}
}
/**
* Returns the list of supported formats for this view capturer. Currently, only supports
* capturing to RGBA_8888 bitmaps.
*
* #return list of supported formats.
*/
override fun getSupportedFormats(): List<VideoFormat> {
val videoFormats = ArrayList<VideoFormat>()
val videoDimensions = VideoDimensions(view.width, view.height)
val videoFormat = VideoFormat(videoDimensions, 30, VideoPixelFormat.RGBA_8888)
videoFormats.add(videoFormat)
return videoFormats
}
/**
* Returns true because we are capturing screen content.
*/
override fun isScreencast(): Boolean {
return true
}
/**
* This will be invoked when it is time to start capturing frames.
*
* #param videoFormat the video format of the frames to be captured.
* #param listener capturer listener.
*/
override fun startCapture(videoFormat: VideoFormat, listener: VideoCapturer.Listener) {
// Store the capturer listener
this.videoCapturerListener = listener
this.started.set(true)
// Notify capturer API that the capturer has started
val capturerStarted = handler.postDelayed(
viewCapturer,
VIEW_CAPTURER_FRAMERATE_MS.toLong()
)
this.videoCapturerListener!!.onCapturerStarted(capturerStarted)
}
/**
* Stop capturing frames. Note that the SDK cannot receive frames once this has been invoked.
*/
override fun stopCapture() {
this.started.set(false)
handler.removeCallbacks(viewCapturer)
}
override fun onPixelCopyFinished(i: Int) {
// Extract the frame from the bitmap
val bytes = mViewBitmap.getByteCount()
val buffer = ByteBuffer.allocate(bytes)
mViewBitmap.copyPixelsToBuffer(buffer)
val array = buffer.array()
val captureTimeNs = TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime())
// Create video frame
val dimensions = VideoDimensions(view.width, view.height)
val videoFrame = VideoFrame(
array,
dimensions, VideoFrame.RotationAngle.ROTATION_0, captureTimeNs
)
// Notify the listener
if (started.get()) {
videoCapturerListener?.onFrameCaptured(videoFrame)
}
if (started.get()) {
handler.postDelayed(viewCapturer, VIEW_CAPTURER_FRAMERATE_MS.toLong())
}
}
companion object {
private val VIEW_CAPTURER_FRAMERATE_MS = 100
}
}
The ARCore part works but the Twilio part does not work.
I referred to another post that talked about it but it was incomplete:
Streaming CustomView ARcore with Twilio video
I have an activity with a product list fragment and many other fragments and I am trying to use architecture component navigation controller.
The problem is: it replaces the (start destination) product list fragment and I don't want the list to be reloaded when user click back button.
How to make the fragment transaction as add not replace?
Android navigation component just replace but you want to add fragment instead of replace like dialog you can use this but need to min. "Version 2.1.0" for navigation component.
Solution
and you can see "Dialog destinations"
I faced the same problem, while waiting on add and other options for fragment transactions I implemented this work around to preserve the state when hitting back.
I just added a check if the binding is present then I just restore the previous state, the same with the networking call, I added a check if the data is present in view model then don't do the network refetching. After testing it works as expected.
EDIT:
For the recycler view I believe it will automatically return to the same sate the list was before you navigated from the fragment but storing the position in the onSavedInstanceSate is also possible
private lateinit var binding: FragmentSearchResultsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel =
ViewModelProviders.of(this, mViewModelFactory).get(SearchResultsViewModel::class.java)
return if (::binding.isInitialized) {
binding.root
} else {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_results, container, false)
with(binding) {
//some stuff
root
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//reload only if search results are empty
if (viewModel.searchResults.isEmpty()) {
args.searchKey.let {
binding.toolbarHome.title = it
viewModel.onSearchResultRequest(it)
}
}
}
You have to override NavHostFragment's createFragmentNavigator method and return YourFragmentNavigator.
YourFragmentNavigator must override FragmentNavigator's navigate method.
Copy and paste FragmentNavigator's navigate method to your YourFragmentNavigator.
In navigate method, change the line ft.replace(mContainerId, frag); with
if (fragmentManager.fragments.size <= 0) {
ft.replace(containerId, frag)
} else {
ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
ft.add(containerId, frag)
}
The solution will look like this:
class YourNavHostFragment : NavHostFragment() {
override fun createFragmentNavigator(): Navigator<...> {
return YourFragmentNavigator(...)
}}
....
class YourFragmentNavigator(...) : FragmentNavigator(...) {
override fun navigate(...){
....
if (fragmentManager.fragments.size <= 0) {
ft.replace(containerId, frag)
} else {
ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
ft.add(containerId, frag)
}
....
}}
in your xml use YourNavHostFragment.
I was facing the same issue but in my case I updated my code to use livedata and viewmodel.
when you press back the viewmodel is not created again and thus your data is retained.
make sure you do the api call in init method of viewmodel, so that it happens only once when viewmodel is created
just copy the FragmentNavigator's code (300 lines) and replace replace() with add(). this is the best solution for me at the moment.
#Navigator.Name("fragment")
public class CustomFragmentNavigator extends
Navigator<...> {
...
public NavDestination navigate(...) {
...
ft.add(mContainerId, frag);
...
}
...
}
#Rainmaker is right in my opinion, I did the same thing.
We can also save the recycler view position/state in onSaveInstanceState
in order to return to the same recycler view position when navigating back to the list fragment.
You can use these classes as your custom NavHostFragment and Navigator
NavHostFragment
class YourNavHostFragment : NavHostFragment() {
override fun onCreateNavHostController(navHostController: NavHostController) {
/**
* Done this on purpose.
*/
if (false) {
super.onCreateNavHostController(navHostController)
}
val containerId = if (id != 0 && id != View.NO_ID) id else R.id.nav_host_fragment_container
navController.navigatorProvider += YourFragmentNavigator(requireContext(), parentFragmentManager, containerId)
navController.navigatorProvider += DialogFragmentNavigator(requireContext(), childFragmentManager)
}
}
Navigator
#Navigator.Name("fragment")
class YourFragmentNavigator(private val context: Context, private val fragmentManager: FragmentManager, private val containerId: Int) : Navigator<YourFragmentNavigator.Destination>() {
private val savedIds = mutableSetOf<String>()
/**
* {#inheritDoc}
*
* This method must call
* [FragmentTransaction.setPrimaryNavigationFragment]
* if the pop succeeded so that the newly visible Fragment can be retrieved with
* [FragmentManager.getPrimaryNavigationFragment].
*
* Note that the default implementation pops the Fragment
* asynchronously, so the newly visible Fragment from the back stack
* is not instantly available after this call completes.
*/
override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
if (fragmentManager.isStateSaved) {
Log.i(TAG, "Ignoring popBackStack() call: FragmentManager has already saved its state")
return
}
if (savedState) {
val beforePopList = state.backStack.value
val initialEntry = beforePopList.first()
// Get the set of entries that are going to be popped
val poppedList = beforePopList.subList(
beforePopList.indexOf(popUpTo),
beforePopList.size
)
// Now go through the list in reversed order (i.e., started from the most added)
// and save the back stack state of each.
for (entry in poppedList.reversed()) {
if (entry == initialEntry) {
Log.i(TAG, "FragmentManager cannot save the state of the initial destination $entry")
} else {
fragmentManager.saveBackStack(entry.id)
savedIds += entry.id
}
}
} else {
fragmentManager.popBackStack(popUpTo.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
state.pop(popUpTo, savedState)
}
override fun createDestination(): Destination {
return Destination(this)
}
/**
* Instantiates the Fragment via the FragmentManager's
* [androidx.fragment.app.FragmentFactory].
*
* Note that this method is **not** responsible for calling
* [Fragment.setArguments] on the returned Fragment instance.
*
* #param context Context providing the correct [ClassLoader]
* #param fragmentManager FragmentManager the Fragment will be added to
* #param className The Fragment to instantiate
* #param args The Fragment's arguments, if any
* #return A new fragment instance.
*/
#Suppress("DeprecatedCallableAddReplaceWith")
#Deprecated(
"""Set a custom {#link androidx.fragment.app.FragmentFactory} via
{#link FragmentManager#setFragmentFactory(FragmentFactory)} to control
instantiation of Fragments."""
)
fun instantiateFragment(context: Context, fragmentManager: FragmentManager, className: String, args: Bundle?): Fragment {
return fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
}
/**
* {#inheritDoc}
*
* This method should always call
* [FragmentTransaction.setPrimaryNavigationFragment]
* so that the Fragment associated with the new destination can be retrieved with
* [FragmentManager.getPrimaryNavigationFragment].
*
* Note that the default implementation commits the new Fragment
* asynchronously, so the new Fragment is not instantly available
* after this call completes.
*/
override fun navigate(entries: List<NavBackStackEntry>, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?) {
if (fragmentManager.isStateSaved) {
Log.i(TAG, "Ignoring navigate() call: FragmentManager has already saved its state")
return
}
for (entry in entries) {
navigate(entry, navOptions, navigatorExtras)
}
}
private fun navigate(entry: NavBackStackEntry, navOptions: NavOptions?, navigatorExtras: Navigator.Extras?) {
val backStack = state.backStack.value
val initialNavigation = backStack.isEmpty()
val restoreState = (navOptions != null && !initialNavigation && navOptions.shouldRestoreState() && savedIds.remove(entry.id))
if (restoreState) {
// Restore back stack does all the work to restore the entry
fragmentManager.restoreBackStack(entry.id)
state.push(entry)
return
}
val destination = entry.destination as Destination
val args = entry.arguments
var className = destination.className
if (className[0] == '.') {
className = context.packageName + className
}
val frag = fragmentManager.fragmentFactory.instantiate(context.classLoader, className)
frag.arguments = args
val ft = fragmentManager.beginTransaction()
var enterAnim = navOptions?.enterAnim ?: -1
var exitAnim = navOptions?.exitAnim ?: -1
var popEnterAnim = navOptions?.popEnterAnim ?: -1
var popExitAnim = navOptions?.popExitAnim ?: -1
if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
enterAnim = if (enterAnim != -1) enterAnim else 0
exitAnim = if (exitAnim != -1) exitAnim else 0
popEnterAnim = if (popEnterAnim != -1) popEnterAnim else 0
popExitAnim = if (popExitAnim != -1) popExitAnim else 0
ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim)
}
if (fragmentManager.fragments.size <= 0) {
ft.replace(containerId, frag)
} else {
ft.hide(fragmentManager.fragments[fragmentManager.fragments.size - 1])
ft.add(containerId, frag)
}
#IdRes val destId = destination.id
// TODO Build first class singleTop behavior for fragments
val isSingleTopReplacement = (navOptions != null && !initialNavigation && navOptions.shouldLaunchSingleTop() && backStack.last().destination.id == destId)
val isAdded = when {
initialNavigation -> {
true
}
isSingleTopReplacement -> {
// Single Top means we only want one instance on the back stack
if (backStack.size > 1) {
// If the Fragment to be replaced is on the FragmentManager's
// back stack, a simple replace() isn't enough so we
// remove it from the back stack and put our replacement
// on the back stack in its place
fragmentManager.popBackStack(entry.id, FragmentManager.POP_BACK_STACK_INCLUSIVE)
ft.addToBackStack(entry.id)
}
false
}
else -> {
ft.addToBackStack(entry.id)
true
}
}
if (navigatorExtras is Extras) {
for ((key, value) in navigatorExtras.sharedElements) {
ft.addSharedElement(key, value)
}
}
ft.setReorderingAllowed(true)
ft.commit()
// The commit succeeded, update our view of the world
if (isAdded) {
state.push(entry)
}
}
override fun onSaveState(): Bundle? {
if (savedIds.isEmpty()) {
return null
}
return bundleOf(KEY_SAVED_IDS to ArrayList(savedIds))
}
override fun onRestoreState(savedState: Bundle) {
val savedIds = savedState.getStringArrayList(KEY_SAVED_IDS)
if (savedIds != null) {
this.savedIds.clear()
this.savedIds += savedIds
}
}
/**
* NavDestination specific to [FragmentNavigator]
*
* Construct a new fragment destination. This destination is not valid until you set the
* Fragment via [setClassName].
*
* #param fragmentNavigator The [FragmentNavigator] which this destination will be associated
* with. Generally retrieved via a [NavController]'s [NavigatorProvider.getNavigator] method.
*/
#NavDestination.ClassType(Fragment::class)
open class Destination
constructor(fragmentNavigator: Navigator<out Destination>) : NavDestination(fragmentNavigator) {
/**
* Construct a new fragment destination. This destination is not valid until you set the
* Fragment via [setClassName].
*
* #param navigatorProvider The [NavController] which this destination
* will be associated with.
*/
//public constructor(navigatorProvider: NavigatorProvider) : this(navigatorProvider.getNavigator(FragmentNavigator::class.java))
#CallSuper
public override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
context.resources.obtainAttributes(attrs, R.styleable.FragmentNavigator).use { array ->
val className = array.getString(R.styleable.FragmentNavigator_android_name)
if (className != null) setClassName(className)
}
}
/**
* Set the Fragment class name associated with this destination
* #param className The class name of the Fragment to show when you navigate to this
* destination
* #return this [Destination]
*/
fun setClassName(className: String): Destination {
_className = className
return this
}
private var _className: String? = null
/**
* The Fragment's class name associated with this destination
*
* #throws IllegalStateException when no Fragment class was set.
*/
val className: String
get() {
checkNotNull(_className) { "Fragment class was not set" }
return _className as String
}
override fun toString(): String {
val sb = StringBuilder()
sb.append(super.toString())
sb.append(" class=")
if (_className == null) {
sb.append("null")
} else {
sb.append(_className)
}
return sb.toString()
}
override fun equals(other: Any?): Boolean {
if (other == null || other !is Destination) return false
return super.equals(other) && _className == other._className
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + _className.hashCode()
return result
}
}
/**
* Extras that can be passed to FragmentNavigator to enable Fragment specific behavior
*/
class Extras internal constructor(sharedElements: Map<View, String>) :
Navigator.Extras {
private val _sharedElements = LinkedHashMap<View, String>()
/**
* The map of shared elements associated with these Extras. The returned map
* is an [unmodifiable][Map] copy of the underlying map and should be treated as immutable.
*/
val sharedElements: Map<View, String>
get() = _sharedElements.toMap()
/**
* Builder for constructing new [Extras] instances. The resulting instances are
* immutable.
*/
class Builder {
private val _sharedElements = LinkedHashMap<View, String>()
/**
* Adds multiple shared elements for mapping Views in the current Fragment to
* transitionNames in the Fragment being navigated to.
*
* #param sharedElements Shared element pairs to add
* #return this [Builder]
*/
fun addSharedElements(sharedElements: Map<View, String>): Builder {
for ((view, name) in sharedElements) {
addSharedElement(view, name)
}
return this
}
/**
* Maps the given View in the current Fragment to the given transition name in the
* Fragment being navigated to.
*
* #param sharedElement A View in the current Fragment to match with a View in the
* Fragment being navigated to.
* #param name The transitionName of the View in the Fragment being navigated to that
* should be matched to the shared element.
* #return this [Builder]
* #see FragmentTransaction.addSharedElement
*/
fun addSharedElement(sharedElement: View, name: String): Builder {
_sharedElements[sharedElement] = name
return this
}
/**
* Constructs the final [Extras] instance.
*
* #return An immutable [Extras] instance.
*/
fun build(): Extras {
return Extras(_sharedElements)
}
}
init {
_sharedElements.putAll(sharedElements)
}
}
private companion object {
private const val TAG = "YourFragmentNavigator"
private const val KEY_SAVED_IDS = "androidx-nav-fragment:navigator:savedIds"
}
}
Usage
In your activity/fragment your FragmentContainerView should look like this.
<androidx.fragment.app.FragmentContainerView
android:id="#+id/navHost"
android:name="in.your.android.core.platform.navigation.YourNavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph" />
After searching a bit, it's not possible, but the problem itself can be solved with viewmodel and livedata or rxjava.
So fragment state is kept after transactions and my product list will not reload each time