2

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 horizontal LinearLayout with width and height set to match_parent that contains 2 FrameLayouts each with a height of match_parent and layout_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 the FrameLayouts. 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

  1. Enabling drawing cache before the draw call. No fix. It's deprecated anyway.
  2. Generating the bitmap directly from the view's draw cache. No fix. Throws a NullPointerException because the getDrawingCache method returns a null. Still deprecated anyway.
  3. 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 because SurfaceView is essentially a 'hole'. I suspect the PixelCopy API won't suffer from this.
  4. 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
}
copolii
  • 14,208
  • 10
  • 51
  • 80
  • "Nothing but the view background is rendered onto the bitmap" -- there doesn't seem to be anything else. It's two empty `FrameLayout` containers. My guess is that you are filling those in via `setupViewTypeOne()` and `setupViewTypeTwo()`, so perhaps your problem lies there. – CommonsWare Nov 28 '18 at 23:39
  • Those same two (static) methods are used to fill the original views. I've also tried to create a duplicate version of the layout, place it within the dialog view, and call those two methods on the right and left portions. Both cases properly populate view. Those methods may be the source of the problem, but I suspect the cause has something to do with the view being offscreen and not attached to a Window. – copolii Nov 29 '18 at 00:41
  • @CommonsWare The view certainly contains the child views. I set a breakpoint and double checked that it both has the children and that it has received the data to be drawn. – copolii Nov 29 '18 at 06:33

1 Answers1

0

Ok I've just spent few days to figure out how to that. I tried to place your view in my code, and it works:

Result

It also works with my more complex view that use Databinding and contains TextViews, ImageView and custom View.

Here is the code:

val view = layoutInflater.inflate(R.layout.your_layout, null, false)

//view.fill(model) <-- I fill my view before measuring and layout
view.measure(
    MeasureSpec.makeMeasureSpec(300, MeasureSpec.EXACTLY),
    MeasureSpec.makeMeasureSpec(300, MeasureSpec.EXACTLY),
)
view.layout(
    0,
    0,
    view.measuredWidth,
    view.measuredHeight
)

val bitmap = Bitmap.createBitmap(
    view.width, view.height,
    Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
canvas.drawColor(0xFFFFFFFF.toInt())

view.draw(canvas)

// Then save to file or share

As you can see there is really no big difference between what you did, so I guess that, as CommonsWare mentioned above, your problem is somewhere in setupViewTypeOne() or setupViewTypeTwo().

Some more info:

  • Capturing View is not attached to Window or any parent
  • Software layer type is used to make screenshot, so if you have some images you need to convert their hardware bitmaps to software before capturing
  • Because of software rendering, some graphic issues can take place, for example my custom view started to draw outside of CardView round corners
  • PixelCopy cannot be used for this task, because it can dump only things from your real screen (or maybe you can somehow place your layout into Surface or SurfaceView and dump it, or create your own Window but I didn't find the way to do what)
  • You don't have to set to your views explicit layerType="software"
  • View.setDrawingCacheEnabled() really has no effect now