There are a variety of questions about saving an offscreen view to a bitmap, and I've followed all to no avail, which is why I don't think this is a duplicate of either.
Background
My app offers two different views of the same data. Both of these views are presented using custom views that work as designed (One uses a SurfaceView, which is a pain in the butt for this kind of thing). There is a requirement to share this data as an image that includes both views (Only one view is visible to the user at a time, they can switch between the views). For an improved user experience and so that the same image is generated for the same data regardless of the device used, this data is to be contained in a bitmap (well, PNG) of fixed size, every time.
Method
In order to accomplish the above, I've
- Created
fragment_screenshot_generator.xml
layout below: (A horizontalLinearLayout
with width and height set tomatch_parent
that contains 2FrameLayouts
each with a height ofmatch_parent
andlayout_weight
of 1. Each takes half the screen. For testing purposes they have a translucent background (red & green respectively) - Created a
DialogFragment
(progress dialog) that is supposed to do this work in the background and automatically dismiss once the screenshot is saved to file (returning the file to the parent fragment) - While the dialog is being created, I inflate
fragment_screenshot_generator.xml
and setup the two view types in each of theFrameLayout
s. The exact same code is used here as what generates the original user visible views. - The View is then rendered onto a canvas, backed by a bitmap
- The bitmap is compressed to a file
Problem
Nothing but the view background is rendered onto the bitmap. I suspect I'm missing a call somewhere to tell the root of fragment_screenshot_generator.xml
that "it's all good" so that the views inside it would render. I've tried posting to the layout root, but it's very unpredictable, takes very long at times, and still draws nothing but transparency. Do I have to attach this layout to a window? Can I attach it to the current window without showing it? Do you see anything missing in my code?
Notes
I've tried
- Enabling drawing cache before the draw call. No fix. It's deprecated anyway.
- Generating the bitmap directly from the view's draw cache. No fix. Throws a
NullPointerException
because thegetDrawingCache
method returns anull
. Still deprecated anyway. - Rendering the exact same view onscreen (as a part of the dialog layout) and saving that to bitmap. It works, but the
SurfaceView
portion is empty. This is expected becauseSurfaceView
is essentially a 'hole'. I suspect thePixelCopy
API won't suffer from this. - I've removed background execution from the code below for simplicity. So don't yell at me for running this all on the main thread! :)
Code
fragment_screenshot_generator.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/view_1"
android:background="#4f00"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:id="@+id/view_2"
android:background="#40f0"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
ScreenshotGeneratorDialogFragment#onCreateDialog
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
viewModel = ViewModelProviders.of(activity!!).get(DetailViewModel::class.java)
val parameter1 = viewModel.dataTypeOne.value!!.parameter1
val parameter2 = viewModel.dataTypeOne.value!!.parameter2
screenshotView = View.inflate(context, R.layout.fragment_screenshot_generator, null).apply {
measure(View.MeasureSpec.makeMeasureSpec(SCREENSHOT_WIDTH, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(SCREENSHOT_HEIGHT, View.MeasureSpec.EXACTLY))
layout(0, 0, measuredWidth, measuredHeight)
setupViewTypeOne(findViewById(R.id.view_1)!!, parameter1, parameter2)
setupViewTypeTwo(findViewById(R.id.view_2)!!, parameter1, parameter2, viewModel.plotData.value)
layout(0, 0, measuredWidth, measuredHeight)
}
dialogView = View.inflate(context, R.layout.dialog_screenshot_generator, null).apply {
findViewById<TextView>(R.id.progress_label)!!.setText(R.string.preparing_screenshots)
}
val dialog = AlertDialog.Builder(context!!)
.setView(dialogView)
.create()
dialog.setOnShowListener { capture() }
return dialog
}
ScreenshotGeneratorDialogFragment#capture
private fun capture():File {
var bitmap = Bitmap.createBitmap(screenshotView.measuredWidth, screenshotView.measuredHeight, Bitmap.Config.ARGB_8888)
var canvas = Canvas(bitmap)
canvas.drawColor(0xffffffff.toInt())
screenshotView.draw(canvas)
// Saving to SDcard for testing. This will move to cache dir.
val file = File.createTempFile("share-", ".png", Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES))
file.outputStream().use {
bitmap.compress(Bitmap.CompressFormat.PNG, 0, it)
}
return file
}