0

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.

  • 3
    You're saving data to `outState` without calling the `super.onSaveInstanceState(outState)`. – glagarto Jul 05 '18 at 02:38
  • Ok. So I added the super line to the method, but it still doesn't save the file's absolute path. When I rotate the device, the `File` is still presumably `null`. – AstroNoodles Jul 09 '18 at 18:18

1 Answers1

0

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)
}
Cobus Kruger
  • 8,338
  • 3
  • 61
  • 106