【问题标题】:Save a CustomView as Bitmap将 CustomView 保存为位图
【发布时间】:2019-10-24 12:57:43
【问题描述】:

我来自以下问题(我问过): Saving the current view as a bitmap

为了提出这个问题,我会提供尽可能多的细节。

我的最终目标是编写一个包含一些信息的视图(稍后将来自 API,可能是包含大量文本的 JSOn 对象)。 到目前为止,我所做的是: 1) 创建自定义视图 2)使用我需要的信息在此自定义视图上绘制画布(canvas.drawText()) 3)把这个CustomView放在activity_main.xml上(参考它) 4) 在 MainActivity.kt 上实例化这个 CustomView (现在问题开始了) 5)将此CustomView转换为位图(使用扩展方法。 6)将转换后的CustomView保存到SD卡

但是,当我尝试保存它时,什么也没有发生。没有创建文件夹,LogCat 窗口上也没有任何内容(我正在检查文件\文件夹是否是使用 Android Studio 上的设备文件资源管理器创建的)。

阅读后我明白我应该有一个 ViewTreeObserver 来观察变化(例如:然后视图完成绘制)。我将此作为扩展方法添加到我的代码中(在 SO 上找到但现在找不到链接)但也没有改变。

为了将位图保存到内部存储,我从以下链接获得了方法: https://android--code.blogspot.com/2018/04/android-kotlin-save-image-to-internal.html (我只是调整了该方法,因为我需要使用 Bitmap 而不是可绘制对象)。

我错过了什么吗?据我所见,我正在做正确的事情来将位图保存到 SD。 (由于我发布的代码,问题很大) 信息: 使用 Android Studio 3.5.1 Kotlin 语言

我的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 扩展方法:

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.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)
    }
}

主活动

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)
    }
}

编辑 1:我更改了用户 mhedman 在 cmets 上的建议。他提到我正在访问我的自定义视图的一个新实例,而不是已经从活动布局中绘制的那个。当我在 ViewTreeObsever 事件之外尝试时,我有一个异常说“宽度和高度必须> 0”。在 ViewTreeObserver 内部,什么也没有发生(不显示任何消息)。

使用建议更新代码:

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)
        }

【问题讨论】:

  • “没有创建文件夹”——你是如何确定的?您是否在 Android Studio 中使用设备资源管理器工具? “没有显示吐司”——您没有任何代码可以显示Toast,至少就您问题中的代码而言。
  • @CommonsWare 我暂时删除了 toats,抱歉(我将编辑问题)。是的,我正在使用设备资源管理器工具来检查是否创建了任何文件\文件夹。
  • 您的代码中没有任何内容会导致您的outraView 实际布局。例如,您没有将其添加到活动的视图层次结构中。如果你想要的只是一个位图,为什么不创建一个支持位图的Canvas 并在其上绘制,跳过所有这些自定义视图的内容?
  • 您似乎没有尝试获取已经淹没的自定义视图,而是使用此行 val outraView = CustomView(this) 创建了新视图,而您应该从活动布局“com”访问该视图.example.desenhanota.CustomView"
  • @mhemdan 我明白你的意思,真的很有道理。让我在这里尝试从活动布局中访问我创建的视图(就像你提到的那样),我将使用结果编辑问题。

标签: android kotlin bitmap android-view


【解决方案1】:

你需要做两件事,首先应该测量 customView 的大小,然后绘制到画布中

 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;
    }
}

【讨论】:

  • 我找到了一种将 dp 转换为像素的方法,但我无法访问 BaseApplication.getContext() - 无需导入、添加等。
  • 你可以将上下文作为参数传递给这个方法
【解决方案2】:

我终于让它工作了。 感谢所有支持并提供意见的用户,我找出了问题所在并使其正常工作。 老实说,当 mhemdan 说我正在创建一个新视图而不是使用我在 activitiy_main.xml 上已有的视图时,他提到了关键的事情。从那里我只是做了一些调整,终于让它工作了。让我分享最终代码。

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" >

    <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>

自定义视图代码没有任何变化。

MainActivity.kt(发生变化的地方)

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()
    }
}

我仍将改进此代码(删除已弃用的内容等)。但就目前而言,这可以完成这项工作。 谢谢。

【讨论】:

    【解决方案3】:

    对于 API 28 及更高版本,建议您使用 PixelCopy,对于 pre-API 28 使用 getBitmapDrawingCache:

    示例取自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
    }
    

    【讨论】:

    • 我正在使用 API 级别 25。稍后我将切换到更高级别的 API 并使用 PixelCopy 进行截图。现在我只使用了一些已弃用的方法,但它们确实有效。
    • 当您在层次结构中有 SurfaceView 时,这不起作用。
    猜你喜欢
    • 1970-01-01
    • 2015-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-02-08
    • 1970-01-01
    • 2010-10-13
    相关资源
    最近更新 更多