Я исхожу из следующего вопроса (я задал): Сохранение текущего представления как растрового изображения

Я дам максимум подробностей, чтобы задать вопрос.

Моя конечная цель - написать представление с некоторой информацией о нем (позже будет получено из API, возможно, объект JSOn с большим количеством текста). То, что я сделал до сих пор, было: 1) Создание настраиваемого представления 2) Нарисовать в этом настраиваемом представлении холст с необходимой мне информацией (canvas.drawText ()) 3) Поместить этот CustomView в activity_main.xml (сослаться на него) 4) Создайте экземпляр этого CustomView на MainActivity.kt (теперь проблема начинается) 5) Преобразуйте этот CustomView в растровое изображение (используя метод расширения. 6) Сохраните преобразованный CustomView на SD-карту

Однако, когда я пытаюсь его сохранить, ничего не происходит. Папка не создается, в окне LogCat ничего не создается (я проверяю, создаются ли файлы \ папки с помощью проводника файлов устройства в Android Studio).

После прочтения я понял, что у меня должен быть ViewTreeObserver, чтобы следить за изменениями (например, тогда представление завершает рисование). Я добавил это в свой код как метод расширения (нашел на SO, но не могу найти ссылку прямо сейчас), но также ничего не изменил.

Чтобы сохранить растровое изображение во внутреннем хранилище, я получил метод по следующей ссылке: https: // android-- code.blogspot.com/2018/04/android-kotlin-save-image-to-internal.html (Я просто адаптировал метод, так как мне нужно было использовать растровое изображение, а не рисовать).

Я что-то упускаю ? Насколько я понимаю, я делаю все правильно, чтобы сохранить растровое изображение на SD. (Вопрос большой из-за кода, который я опубликовал) Информация: Использование Android Studio 3.5.1 Kotlin Language

Моя деятельность_основная.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 (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 предложил в комментариях. Он упомянул, что я открываю новый экземпляр своего пользовательского представления, а не тот, который уже был нарисован из макета действий. Когда я попытался выйти за пределы события 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)
        }
1
paboobhzx 24 Окт 2019 в 15:57
1
«Папка не создается» - как вы это определяете? Вы используете инструмент Device Explorer в Android Studio? "тост не отображается" - у вас нет кода для отображения Toast, по крайней мере, с точки зрения кода в вашем вопросе.
 – 
CommonsWare
24 Окт 2019 в 16:00
Я пока удалил toats, извините (отредактирую вопрос). Да, я использую инструмент Device Explorer, чтобы проверить, были ли созданы какие-либо файлы \ папки.
 – 
paboobhzx
24 Окт 2019 в 16:05
1
В вашем коде нет ничего, что заставляло бы ваш outraView фактически выкладываться. Например, вы не добавляете его в иерархию представления действия. Если все, что вам нужно, это растровое изображение, почему бы не создать Canvas на основе растрового изображения и не рисовать на нем, пропуская все эти настраиваемые представления?
 – 
CommonsWare
24 Окт 2019 в 16:14
1
Похоже, вы не пытались получить уже утопленный пользовательский вид, а вместо этого вы создали новый с этой строкой val outraView = CustomView (this), в то время как вы должны получить доступ к представлению из макета действия "com.example.desenhanota.CustomView"
 – 
mhemdan
24 Окт 2019 в 16:16
Я понял вашу точку зрения, действительно имеет смысл. Позвольте мне здесь попытаться получить доступ к моему созданному представлению из макета активности (как вы упомянули), и я отредактирую вопрос с результатами.
 – 
paboobhzx
24 Окт 2019 в 16:20

3 ответа

Вам нужны две вещи: во-первых, нужно измерить размер 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;
    }
}
1
Alireza Tizfahm Fard 24 Окт 2019 в 16:45
Я нашел способ преобразовать dp в пиксель, но у меня нет доступа к BaseApplication.getContext () - нечего импортировать, добавлять и т. Д.
 – 
paboobhzx
24 Окт 2019 в 17:00
Вы можете передать контекст в качестве параметров этому методу
 – 
Alireza Tizfahm Fard
25 Окт 2019 в 17:52

Рекомендуется использовать PixelCopy для API 28 и выше и getBitmapDrawingCache для pre-API 28:

Пример взят из https://medium.com/@shiveshmehta09 / take-screenshot-programatically-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
}
1
Jarvis 24 Окт 2019 в 18:01
Я использую API уровня 25. Позже я перейду на API более высокого уровня и буду использовать PixelCopy для создания снимков экрана. На данный момент я просто использовал метод с несколькими устаревшими вещами, но они работают.
 – 
paboobhzx
24 Окт 2019 в 20:23

Наконец-то у меня все заработало. Спасибо всем пользователям, которые поддержали и внесли свой вклад, я понял, что не так, и все заработало. Честно говоря, ключевой момент был упомянут 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>

Код Custom View ничего не изменил.

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

Я все еще собираюсь улучшить этот код (удалить устаревшие вещи и т. Д.). Но пока это может сработать. Спасибо.

0
paboobhzx 24 Окт 2019 в 20:22