提问人:Chùnky 提问时间:6/2/2022 最后编辑:Chùnky 更新时间:6/7/2022 访问量:78
我已经为我的应用程序实现了拍照的功能。为什么当我尝试在本地保存图像时文件路径为 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?
问:
这是我的第一个简单且几乎完整的 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
编辑: 作为一个快速而肮脏的解决方案,我最终将捕获的图像保存为SQLite数据库中的blob,并在ImageView上显示它们时将它们转换为位图。我仍在寻找一种更有效的方法来做到这一点,因为在SQLite数据库中将图像存储为blob并不是最佳实践。
答: 暂无答案
评论