1

I come from the following question (i asked): Saving the current view as a bitmap

I'll give the maximum details i can in order to ask the question.

My final goal, is to write a view with some information on it (will come later from an API, probably a JSOn object with lots of text). What i've done so far, was: 1) Create a Custom View 2) Draw on this Custom View a Canvas with the information i needed (canvas.drawText() ) 3) Put this CustomView on activity_main.xml (reference it) 4) Instantiate this CustomView on MainActivity.kt (now the problem begins) 5) Convert this CustomView to a Bitmap (Using an extension method. 6) Save the converted CustomView to the SD Card

However, when i try to save it nothing happens. No folder gets created, nothing on the LogCat window also (i'm checking if files \ folders are created using the Device File Explorer on Android Studio).

After reading i understood that i should have an ViewTreeObserver to watch for changes (ex: then the view finishes drawing). I added this to my code as an Extension method (found on SO but can't find the link right now) but changed nothing also.

In order to save the bitmap to internal storage, i got the method from the following link: https://android--code.blogspot.com/2018/04/android-kotlin-save-image-to-internal.html (I just adapted the method since i needed to use a Bitmap no a drawable).

Am i missing something ? As far as i can see i'm doing the correct stuff to save the bitmap to the SD. (Question is big because of the code i posted) Info: Using Android Studio 3.5.1 Kotlin Language

My activity_main.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"
        tools:context=".MainActivity">

        <com.example.desenhanota.CustomView
android:id="@+id/MyCustomview"
            android:layout_width="match_parent"
            android:layout_height="442dp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />


    </RelativeLayout>

ViewTreeObserver Extension Method:

inline fun View.doOnGlobalLayout(crossinline action: (view: View) -> Unit) {
    val vto = viewTreeObserver
    vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        @SuppressLint("ObsoleteSdkInt")
        @Suppress("DEPRECATION")
        override fun onGlobalLayout() {
            action(this@doOnGlobalLayout)
            when {
                vto.isAlive -> {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                        vto.removeOnGlobalLayoutListener(this)
                    } else {
                        vto.removeGlobalOnLayoutListener(this)
                    }
                }
                else -> {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                        viewTreeObserver.removeOnGlobalLayoutListener(this)
                    } else {
                        viewTreeObserver.removeGlobalOnLayoutListener(this)
                    }
                }
            }
        }
    })
}

CustomView file (CustomView.kt)

class CustomView @JvmOverloads constructor(
        context: Context,
        attrs: AttributeSet? = null, 
        defStyleAttr: Int = 0
): View(context, attrs, defStyleAttr) {

    private val  textoGeral = Paint()

    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)

        textoGeral.setColor(Color.BLACK)

        canvas?.drawText("DRAW TEST ON CANVAS TEST TEST ", 0f, 120f, textoGeral)
    }
}

MainActivity

class MainActivity : AppCompatActivity() {

    private val TAG = "MainActivity"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val outraView = CustomView(this)
        outraView.doOnGlobalLayout {
            try {
                val bmp = outraView.fromBitmap()
                val uri: Uri = saveImageToInternalStorage(bmp)

            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

    private fun saveImageToInternalStorage(bitmap :Bitmap):Uri{
        // Get the context wrapper instance
        val wrapper = ContextWrapper(applicationContext)
        // The bellow line return a directory in internal storage
        var file = wrapper.getDir("images", Context.MODE_PRIVATE)
        file = File(file, "${UUID.randomUUID()}.jpg")
        try {
            val stream: OutputStream = FileOutputStream(file)
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
            stream.flush()
            stream.close()
        } catch (e: IOException){ // Catch the exception
            e.printStackTrace()
        }
        // Return the saved image uri
        return Uri.parse(file.absolutePath)
    }
}

EDIT 1: I changed what the user mhedman suggested on the comments. He mentioned i was acessing a new instance of my Custom View, not the one already drawn from Activity Layout. When i tried outside the ViewTreeObsever Event, i had an exception saying "width and height must be > 0". Inside the ViewTreeObserver, nothing happens (no message is show).

Updated code with the suggestion:

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

            val outraView = MyCustomView
        outraView.doOnGlobalLayout {
            val finalBmp = outraView.fromBitmap()
            val uri: Uri = saveImageToInternalStorage(finalBmp)
        }
paboobhzx
  • 109
  • 10
  • 1
    "No folder gets created" -- how are you determining this? Are you using the Device Explorer tool in Android Studio? "no toast is displayed" -- you do not have any code to show a `Toast`, at least in terms of the code in your question. – CommonsWare Oct 24 '19 at 13:00
  • @CommonsWare I removed the toats for now, sorry (i will edit the question). Yeah, i'm using the Device Explorer tool in order to check if any files\folders were created. – paboobhzx Oct 24 '19 at 13:05
  • 1
    There is nothing in your code that is causing your `outraView` to actually get laid out. For example, you are not adding it to the view hierarchy of the activity. If all you want is a bitmap, why not create a bitmap-backed `Canvas` and draw on it, skipping all this custom view stuff? – CommonsWare Oct 24 '19 at 13:14
  • 1
    it seems you didn't try to get the already drown custom view and instead you created new one with this line val outraView = CustomView(this) while you should access the view from the activity layout "com.example.desenhanota.CustomView" – mhemdan Oct 24 '19 at 13:16
  • @mhemdan I understood your point, really makes sense. Let me try here to acccess my created view from the activity layout (like you mentioned) and i'll edit the question with the results. – paboobhzx Oct 24 '19 at 13:20
  • Here is what you want http://hackerseve.com/android-save-view-as-image-and-share-externally/ – hard coder Apr 12 '20 at 05:09

3 Answers3

1

you need two things, first should do measure customView size and second draw into canvas

 private Bitmap loadBitmapFromView(View v, int width, int height)
{
    if (v.getMeasuredHeight() <= 0)
    {
        int specWidth = View.MeasureSpec.makeMeasureSpec((int) convertDpToPixel(BaseApplication.getContext(), width), View.MeasureSpec.UNSPECIFIED);
        int specHeight = View.MeasureSpec.makeMeasureSpec((int) convertDpToPixel(BaseApplication.getContext(), height), View.MeasureSpec.UNSPECIFIED);
        v.measure(specWidth, specHeight);
        Bitmap b = Bitmap.createBitmap(v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(b);
        v.layout(0, 0, (int) convertDpToPixel(BaseApplication.getContext(), width),
                (int) convertDpToPixel(BaseApplication.getContext(), height));
        v.draw(c);
        return b;
    }
    else
    {
        return null;
    }
}
1

Finally i got it working. Thanks to all users who supported and gave their inputs, i figured out what was wrong and got it working. To be honest the key thing was mentioned by mhemdan when he said that i was creating a new view instead of using the one i already had on activitiy_main.xml. From there i just made a few adjustments and finally got it working. Let me share the final code.

activity_main.xml (I added an button to trigger the action of take a screenshot. That's all).

<?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"
    tools:context=".MainActivity" >

    <Button

        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/btnShare"
        android:layout_marginTop="10dp"
        android:text="Share"/>

    <com.example.notasdraw.CustomView
        android:id="@+id/MyCustomview"
        android:layout_width="match_parent"
        android:layout_height="442dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />



</RelativeLayout>

The Custom View code chanded nothing.

MainActivity.kt (where things changed)

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        try
        {
            val button = findViewById<Button>(R.id.btnShare)
            button?.setOnClickListener {
                val bmpFromView = getScreenShot(MyCustomview) //Taking screenshot of the view from activity_main.xml
                val finalPath = saveImageToInternalStorage(bmpFromView) //Saving it to the sd card
                toast(finalPath.toString()) //Debug thing. Just to check the view width (so i can know if its a valid view or 0(just null))
            }

        }
        catch(e: Exception)
        {
            e.printStackTrace()
        }

    }
    private fun saveImageToInternalStorage(bitmap :Bitmap):Uri{
        // Get the context wrapper instance
        val wrapper = ContextWrapper(applicationContext)
        // The bellow line return a directory in internal storage
        var file = wrapper.getDir("images", Context.MODE_PRIVATE)
        file = File(file, "${UUID.randomUUID()}.jpg")
        try {
            val stream: OutputStream = FileOutputStream(file)
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
            stream.flush()
            stream.close()
        } catch (e: IOException){ // Catch the exception
            e.printStackTrace()
        }
        // Return the saved image uri
        return Uri.parse(file.absolutePath)
    }
    fun getScreenShot(view: View): Bitmap { //A few things are deprecated but i kept them anyway
        val screenView = view.rootView
        screenView.isDrawingCacheEnabled = true
        val bitmap = Bitmap.createBitmap(screenView.drawingCache)
        screenView.isDrawingCacheEnabled = false
        return bitmap
    }

    }
    fun Context.toast(message: String) { //Just to display a toast
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
    }
}

I'm gonna still improve this code (remove deprecated stuff, etc). But for now this can do the job. Thanks.

paboobhzx
  • 109
  • 10
0

It is recommended that you use PixelCopy for API 28 and higher and getBitmapDrawingCache for pre-API 28:

Example taken from https://medium.com/@shiveshmehta09/taking-screenshot-programmatically-using-pixelcopy-api-83c84643b02a

// for api level 28
fun getScreenShotFromView(view: View, activity: Activity, callback: (Bitmap) -> Unit) {
    activity.window?.let { window ->
        val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888)
        val locationOfViewInWindow = IntArray(2)
        view.getLocationInWindow(locationOfViewInWindow)
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                PixelCopy.request(
                    window,
                    Rect(
                        locationOfViewInWindow[0],
                        locationOfViewInWindow[1],
                        locationOfViewInWindow[0] + view.width,
                        locationOfViewInWindow[1] + view.height
                    ), bitmap, { copyResult ->
                        if (copyResult == PixelCopy.SUCCESS) {
                            callback(bitmap) }
                        else {

                        }
                        // possible to handle other result codes ...
                    },
                    Handler()
                )
            }
        } catch (e: IllegalArgumentException) {
            // PixelCopy may throw IllegalArgumentException, make sure to handle it
            e.printStackTrace()
        }
    }
}

//deprecated version
/*  Method which will return Bitmap after taking screenshot. We have to pass the view which we want to take screenshot.  */
fun getScreenShot(view: View): Bitmap {
    val screenView = view.rootView
    screenView.isDrawingCacheEnabled = true
    val bitmap = Bitmap.createBitmap(screenView.drawingCache)
    screenView.isDrawingCacheEnabled = false
    return bitmap
}
Jarvis
  • 392
  • 1
  • 6
  • I'm using API level 25. Later i'll switch to a higher level API and use PixelCopy for taking the screenshots. For now i just used an method with a few deprecated things but they do work. – paboobhzx Oct 24 '19 at 17:23
  • This doesnt work when you have a SurfaceView in hierarchy. – Jaswant Singh Dec 09 '21 at 16:13