我已经为我的应用程序实现了拍照的功能。为什么当我尝试在本地保存图像时文件路径为 null?

I have implemented the ability for my app to take a picture. Why is the file path null when I try to save the image locally?

提问人:Chùnky 提问时间:6/2/2022 最后编辑:Chùnky 更新时间:6/7/2022 访问量:78

问:

这是我的第一个简单且几乎完整的 Android 应用程序。

我的目标

我想用设备的内置摄像头拍照,将其显示在同一个 Fragment 的 ImageButton 上,并将其保存到本地设备,以便我可以引用其文件路径并将其添加到我的自定义 SQLite 数据库中,以在另一个 Fragment 上的 RecyclerView 中显示。

背景信息

我的应用程序建立在自定义 SQLite 数据库后端之上,其中包含两个表、用户及其关联的书籍,他们已将其添加到应用程序中以显示在 RecyclerView 中。该应用程序完全使用 Kotlin 构建。导航是使用 Android Jetpack 的带有导航图的导航图(目前不使用 SafeArgs)完成的,整个应用从包含多个 Fragment 的单个 MainActivity 运行。我有一个包含 RecyclerView 的 Fragment,这个 RecyclerView 包含一个 ImageView,我希望在其中显示用户使用其设备拍摄的图片的缩略图。为了显示图像,我使用的是第三方毕加索库。我的应用面向的最低 API 是 API 23。我正在使用已弃用的相机 API,因为 Camera2 和 CameraX 几乎没有改善我的问题,所以我退缩了,它也没有帮助。

我的清单包含以下用于写入访问权限和相机访问权限的代码:

<uses-feature android:name="android.hardware.camera"
        android:required="false"
        />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>

我的文件提供程序设置如下:

<provider
     android:authorities="<my_package_authority>.fileprovider"
     android:name="androidx.core.content.FileProvider"
     android:exported="false"
     android:grantUriPermissions="true">
     <meta-data
         android:name="android.support.FILE_PROVIDER_PATHS"
         android:resource="@xml/file_paths"
         />

</provider>

我的file_paths.xml文件在这里:

<?xml version="1.0" encoding="utf-8" ?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path
        name="my_images"
        path="Android/data/<my_package_authority>/files/Pictures">
    </external-files-path>
    <external-files-path
        name="my_debug_images"
        path="/storage/emulated/0/Android/data/<my_package_authority>/files/Pictures">
    </external-files-path>
    <external-files-path
        name="my_root_images"
        path="/">
    </external-files-path>
</paths>

这是我的 MainActivity.kt,我基本上用它来存储请求代码以及用户及其书籍的用户 ID 和 bookID,以上传到我的数据库(修补解决方案将在以后更好地实现):

class MainActivity : AppCompatActivity() {

    companion object {
        val REQUEST_CAMERA_PERMISSIONS_CODE = 1
        val REQUEST_CAMERA_USAGE = 2

        var userID: Int? = null
        var bookID: Int? = null
    }


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

    }
}

.xml 是完全不变的,并且是默认的。

下面是 CreateBookFragment 的 .xml 和 .kt 文件,用户将在其上将书籍添加到其个人库的 Fragment。

fragment_create_book.xml(我知道一些底部按钮会从屏幕上剪出,具体取决于屏幕大小。我计划在实现相机功能后修复)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/clCreateBookRoot"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/grey"
    tools:context=".CreateBookFragment">

    <TextView
        android:id="@+id/tvCreateBookTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:fontFamily="cursive"
        android:shadowColor="@color/deep_red"
        android:shadowDx="1.5"
        android:shadowDy="1.5"
        android:shadowRadius="1.5"
        android:text="@string/add_book"
        android:textColor="@color/deep_red"
        android:textSize="55sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.024" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="3dp"
        android:orientation="vertical"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tvCreateBookTitle">

        <ImageButton
            android:id="@+id/ibCreateBookImage"
            android:layout_width="75dp"
            android:layout_height="75dp"
            android:layout_gravity="center"
            android:backgroundTint="@color/deep_red"
            android:contentDescription="@string/add_a_picture_to_the_book"
            android:src="@android:drawable/ic_menu_camera" />

        <EditText
            android:id="@+id/etCreateBookTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/title"
            android:importantForAutofill="no"
            android:inputType="textPersonName"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <EditText
            android:id="@+id/etCreateBookAuthor"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/author"
            android:importantForAutofill="no"
            android:inputType="textPersonName"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <EditText
            android:id="@+id/etCreateBookPages"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/total_pages"
            android:importantForAutofill="no"
            android:inputType="number"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tvCreateBookGenre"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                android:layout_marginStart="4dp"
                android:layout_marginEnd="10dp"
                android:text="@string/genre"
                android:textColor="@color/black"
                android:textSize="18sp" />

            <Spinner
                android:id="@+id/spinCreateBookGenre"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:ems="10"
                android:hint="@string/genre"
                android:importantForAutofill="no"
                android:inputType="textPersonName"
                android:minHeight="48dp"
                android:textColorHint="@color/black"
                tools:ignore="TextContrastCheck,SpeakableTextPresentCheck" />
        </LinearLayout>

        <EditText
            android:id="@+id/etCreateBookPublisher"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/publisher"
            android:importantForAutofill="no"
            android:inputType="textPersonName"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <EditText
            android:id="@+id/etCreateBookYearPublished"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/year_published"
            android:importantForAutofill="no"
            android:inputType="number"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <EditText
            android:id="@+id/etCreateBookISBN"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/isbn_code"
            android:importantForAutofill="no"
            android:inputType="textPersonName"
            android:minHeight="48dp"
            android:textColorHint="@color/black" />

        <EditText
            android:id="@+id/etCreateBookStarRating"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="50dp"
            android:layout_marginTop="3dp"
            android:layout_marginEnd="50dp"
            android:layout_marginBottom="5dp"
            android:ems="10"
            android:hint="@string/star_rating_1_5"
            android:importantForAutofill="no"
            android:inputType="number"
            android:minHeight="48dp"
            android:textColorHint="@color/black"
            tools:ignore="TextContrastCheck" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_marginTop="3dp"
            android:gravity="center"
            android:orientation="horizontal">

            <Button
                android:id="@+id/btnCancelCreateBook"
                style="@style/cancel_button_style"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@android:string/cancel" />

            <Button
                android:id="@+id/btnSaveCreateBook"
                style="@style/save_button_style"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/add" />
        </LinearLayout>

    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

创建BookFragment.kt

import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.navigation.NavController
import androidx.navigation.Navigation
import com.squareup.picasso.MemoryPolicy
import com.squareup.picasso.NetworkPolicy
import com.squareup.picasso.Picasso
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*

class CreateBookFragment : Fragment(), View.OnClickListener, AdapterView.OnItemSelectedListener {

    private var photoFilePath: String? = ""  // This is returning null!
    private var navigationController: NavController? = null

    private lateinit var etCreateBookTitle: EditText
    private lateinit var etCreateBookAuthor: EditText
    private lateinit var etCreateBookPages: EditText
    private lateinit var spinCreateBookGenre: Spinner
    private lateinit var etCreateBookPublisher: EditText
    private lateinit var etCreateBookYearPublished: EditText
    private lateinit var etCreateBookISBN: EditText
    private lateinit var etCreateBookStarRating: EditText
    private lateinit var ibCreateBookImage: ImageButton
    private lateinit var btnCancelCreateBook: Button
    private lateinit var btnSaveCreateBook: Button
    private lateinit var genres: Array<out String>
    private lateinit var spinnerText: String

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment

        return inflater.inflate(R.layout.fragment_create_book, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        navigationController = Navigation.findNavController(view)

        initialiseUIElements(view)

        setUpButtonClickListeners()

        setUpGenreSpinner()

    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == MainActivity.REQUEST_CAMERA_USAGE && resultCode == Activity.RESULT_OK) {
            val photoUri: Uri = Uri.parse(photoFilePath)

            Picasso.with(requireContext())
                .load(photoUri)
                .memoryPolicy(MemoryPolicy.NO_CACHE)
                .networkPolicy(NetworkPolicy.NO_CACHE)
                .error(R.drawable.ic_custom_bookshelf)
                .fit()
                .centerInside()
                .noFade()
                .into(ibCreateBookImage)
        }
    }

    private fun initialiseUIElements(view: View) {
        etCreateBookTitle = view.findViewById(R.id.etCreateBookTitle)
        etCreateBookAuthor = view.findViewById(R.id.etCreateBookAuthor)
        etCreateBookPages = view.findViewById(R.id.etCreateBookPages)
        spinCreateBookGenre = view.findViewById(R.id.spinCreateBookGenre)
        etCreateBookPublisher = view.findViewById(R.id.etCreateBookPublisher)
        etCreateBookYearPublished = view.findViewById(R.id.etCreateBookYearPublished)
        etCreateBookISBN = view.findViewById(R.id.etCreateBookISBN)
        etCreateBookStarRating = view.findViewById(R.id.etCreateBookStarRating)
        ibCreateBookImage = view.findViewById(R.id.ibCreateBookImage)
        btnCancelCreateBook = view.findViewById(R.id.btnCancelCreateBook)
        btnSaveCreateBook = view.findViewById(R.id.btnSaveCreateBook)
    }

    private fun setUpButtonClickListeners() {
        ibCreateBookImage.setOnClickListener(this)
        btnCancelCreateBook.setOnClickListener(this)
        btnSaveCreateBook.setOnClickListener(this)
    }

    private fun setUpGenreSpinner() {
        genres = resources.getStringArray(R.array.book_genres)
        val spinnerAdapter: ArrayAdapter<CharSequence> = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, genres)
        spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
        spinCreateBookGenre.adapter = spinnerAdapter
        spinCreateBookGenre.onItemSelectedListener = this
    }

    private fun addBook(view: View) {
        if (checkEmpty(etCreateBookTitle) || checkEmpty(etCreateBookAuthor) ||
            (checkEmpty(etCreateBookPages) || !isNumeric(etCreateBookPages.text.toString().trim())) ||
            spinnerText.isEmpty() || checkEmpty(etCreateBookPublisher) ||
            (checkEmpty(etCreateBookYearPublished) || !isNumeric(etCreateBookYearPublished.text.toString().trim())) ||
            checkEmpty(etCreateBookISBN) ||
            (checkEmpty(etCreateBookStarRating) || !isNumeric(etCreateBookStarRating.text.toString().trim()))) {
            Toast.makeText(requireContext(), "Fields cannot be blank", Toast.LENGTH_SHORT).show()
        }
        else {
            val bookTitle: String = etCreateBookTitle.text.toString().trim()
            val bookAuthor: String = etCreateBookAuthor.text.toString().trim()
            val bookPages: Int = etCreateBookPages.text.toString().trim().toInt()
            val bookGenre: String = spinnerText
            val bookPublisher: String = etCreateBookPublisher.text.toString().trim()
            val bookYearPublished: Int = etCreateBookYearPublished.text.toString().trim().toInt()
            val ISBN: String = etCreateBookISBN.text.toString().trim()
            val bookStarRating: Float = etCreateBookStarRating.text.toString().trim().toFloat()
            val bookImage: String? = if (photoFilePath != "")
                photoFilePath
            else
                null

            val dbHandler: DBHandler = DBHandler(requireContext())

            val status = dbHandler.addBook(BookModelClass(null, bookTitle, bookAuthor, bookPages,
                bookGenre, bookPublisher, bookYearPublished, ISBN, bookStarRating, 0, bookImage, MainActivity.userID!!))

            if (status > -1) {
                Toast.makeText(requireContext(), "Successfully added book to list", Toast.LENGTH_SHORT).show()
                etCreateBookTitle.text.clear()
                etCreateBookAuthor.text.clear()
                etCreateBookPages.text.clear()
                etCreateBookPublisher.text.clear()
                etCreateBookYearPublished.text.clear()
                etCreateBookISBN.text.clear()
                etCreateBookStarRating.text.clear()
            }
        }
    }

    private fun checkEmpty(editText: EditText): Boolean {
        if (editText.text.toString().trim() == "") {
            return true
        }
        return false
    }

    override fun onClick(p0: View?) {
        when (p0) {
            ibCreateBookImage -> {
                if (ContextCompat.checkSelfPermission(requireContext(), android.Manifest.permission.CAMERA)
                == PackageManager.PERMISSION_GRANTED)
                    startCamera()
                else {
                    ActivityCompat.requestPermissions(requireActivity(), arrayOf(android.Manifest.permission.CAMERA),
                        MainActivity.REQUEST_CAMERA_PERMISSIONS_CODE)
                }
            }
            btnCancelCreateBook -> {
                navigationController!!.navigate(R.id.action_createBookFragment_to_bookListFragment)
                Toast.makeText(requireContext(), "Changes discarded", Toast.LENGTH_SHORT).show()
            }
            btnSaveCreateBook -> {
                addBook(p0)

                navigationController!!.navigate(R.id.action_createBookFragment_to_bookListFragment)
            }
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == MainActivity.REQUEST_CAMERA_USAGE) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)
                startCamera()
            else {
                Toast.makeText(requireContext(), "Oops! Camera permission denied", Toast.LENGTH_SHORT).show()
                return
            }
        }
    }

    private fun startCamera() {
        val cameraIntent: Intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        var photoFile: File? = null
        try {
            photoFile = createPictureFile()
        } catch (exception: IOException) {
            Toast.makeText(requireContext(), "Error: Cannot save photo", Toast.LENGTH_SHORT).show()
            return
        }
        if (photoFile != null) {
            val photoUri = FileProvider.getUriForFile(requireContext(), requireActivity().packageName + ".fileprovider", photoFile)
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
            startActivityForResult(cameraIntent, MainActivity.REQUEST_CAMERA_USAGE)
        }
    }

    private fun createPictureFile(): File {
        val timeStamp: String = SimpleDateFormat("ddMMyyyy_HHmmss", Locale.UK).format(Date().time)
        val photoFileName: String = "IMG_" + timeStamp + "_"
        val storageDirectory: File = requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
        return File.createTempFile(photoFileName, ".jpg", storageDirectory).apply {
            photoFilePath = absolutePath
        }
    }

    private fun isNumeric(string: String): Boolean {
        return string.all { char -> char.isDigit() }
    }

    override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
        spinnerText = genres[p2].toString()
    }

    override fun onNothingSelected(p0: AdapterView<*>?) {
        spinnerText = ""
    }
}

问题

实际上,当尝试将图片保存到设备时,Android Studio 会将图片/路径注册为“null”,在我开始使用 Picasso 之前,我曾经在尝试将图片加载到 RecyclerView 上时收到 NullPointerException,即使我可以看到图片确实保存到模拟器 (API 30) 和物理设备 (API 24) 上的本地存储中,路径设置为: “/storage/emulated/0/Android/data/<my_package_authority>/files/Pictures/IMG_01062022_164449_8511882897552656984.jpg”,以及物理设备的类似路径(下面包含file_paths.xml)。我通过使用一个名为 DBBrowser for Sqlite 的应用程序获得了此路径,我用它来确认我的数据库确实正在获取该路径

毕加索只显示我为缩略图设置的错误图像,尽管尝试了许多不同的来源,例如官方 Android 文档和其他一些论坛、视频和其他类似主题的 StackOverflow 问题/答案。当用户在最初创建图片时尝试将图片添加到他们的书中时,在他们拍摄相机照片后,它甚至没有实际显示图片。似乎发生的事情是,该文件从未“正式”创建,但该文件确实存在于设备内存上的指定位置。我目前正在使用已弃用的 startActivityForResult() 函数进行测试,并尝试在 Fragment 内的 onActivityResult() 中设置内容,但它不起作用。我正在使用已弃用的函数,因为替换它的较新函数也无济于事,所以这是最后的手段。StackOverflow 上的一个答案表明,startActivityForResult() 函数实际上是将数据发送到 MainActivity,而不是将该数据本地化到 Fragment 中,这几乎引起了最大的麻烦。这个问题已经好几天了,如果不是整整一周,请发送帮助......

我不需要任何帮助来尝试显示图像,甚至不需要任何尝试从我的数据库中获取图像路径的帮助,我只需要帮助弄清楚如何阻止文件路径返回 null,以便 Picasso 可以在用户首次在 CreateBookFragment 上添加图像时显示图像预览。如果文件路径可以停止返回 null,我就可以让整个功能继续运行。我争论过将我的数据库与两个模型类放在一起,但它就像 1000 行代码,而 StackOverflow 不允许超过 40000 个字符

一些来源(绝对不是全部)

https://developer.android.com/training/camera/photobasics

未在 Fragment 中调用 onActivityResult

我看过的许多Youtube视频之一https://www.google.com/search?q=how+to+save+an+image+from+camera+onto+device+android+studio&rlz=1C1CHBF_enZA979ZA979&oq=how+to+save+an+image+from+camera+onto+device+android+studio&aqs=chrome..69i57j0i22i30l2j0i390.10856j0j7&sourceid=chrome&ie=UTF-8#kpvalbx=_fW2YYqCeCJq6gAbewp2gCA22

编辑: 作为一个快速而肮脏的解决方案,我最终将捕获的图像保存为SQLite数据库中的blob,并在ImageView上显示它们时将它们转换为位图。我仍在寻找一种更有效的方法来做到这一点,因为在SQLite数据库中将图像存储为blob并不是最佳实践。

android-studio kotlin file-io nullpointerexception android-相机

评论


答: 暂无答案