kotlin 隐藏 组件
系列路线图 (Series Roadmap)
Hello there, and welcome back. This is the second part in a three-part series tutorial on using Compound Components in Android. If this is your first time here, you might want to check out the first part here.
您好,欢迎回来。 这是由三部分组成的系列教程的第二部分,该系列教程涉及在Android中使用复合组件。 如果这是您第一次来这里,您可能想在这里签出第一部分。
In the previous part, we successfully displayed our custom view on-screen and we came up with this.
在上一部分中,我们成功地在屏幕上显示了自定义视图,并且我们想到了这一点。
FileDescriptor (FileDescriptor)
In the previous article, we explained the component we would be building, a file descriptor that extracts basic information from a selected file present on the Android File System. So the next step would be adding the functionality to actually select the file.
在上一篇文章中,我们解释了我们将要构建的组件,一个文件描述符,该描述符从Android文件系统上存在的选定文件中提取基本信息。 因此,下一步将是添加功能以实际选择文件。
To that effect, we would be adding a button to the MainActivity
that triggers Android’s default file chooser, which allows the user to select a file of interest. The selected file is then passed into the FileDescriptor
. Let’s jump right in!
为此,我们将向MainActivity
添加一个按钮,该按钮将触发Android的默认文件选择器,该按钮允许用户选择感兴趣的文件。 然后将所选文件传递到FileDescriptor
。 让我们跳进去吧!
<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/select_file_button"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/select_file_text"/> // "Select File"
<dev.olaore.compoundcmpts_final.FileDescriptor
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:layout_marginRight="20dp"
app:layout_constraintTop_toBottomOf="@id/select_file_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
The above layout produces the output below:
上面的布局产生以下输出:
Next, we want to set up a click listener on the button in the MainActivity
, we then define the method that launches the file chooser.
接下来,我们要在MainActivity
的按钮上设置一个单击侦听器,然后定义启动文件选择器的方法。
class MainActivity : AppCompatActivity() {
private val REQUEST_FILE_CODE = 1
private lateinit var fileDescriptor: FileDescriptor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fileDescriptor = findViewById(R.id.file_descriptor);
select_file_button.setOnClickListener {
selectFile()
}
}
}
Below is the implementation of the selectFile()
method invoked on-click of the button.
下面是selectFile()
单击按钮时调用的方法。
// imports...
class MainActivity : AppCompatActivity() {
// ...
private fun selectFile() {
val selectFileIntent = Intent(Intent.ACTION_GET_CONTENT)
selectFileIntent.type = "*/*"
startActivityForResult(selectFileIntent, 1)
}
}
In the implementation above, we essentially construct an Intent
with an action of ACTION_GET_CONTENT
. This action informs the android system that content of some kind is to be retrieved by this intent. Next, we set the type of the content to be gotten. Most times, you might want to get a specific type of file, be it a document, media of some sort, or whatever.
在上面的实现中,我们实质上是使用ACTION_GET_CONTENT
动作构造一个Intent
。 此操作通知android系统此意图将检索某种内容。 接下来,我们设置要获取的内容的类型。 大多数时候,您可能想要获取一种特定类型的文件,无论是文档,某种形式的媒体还是其他。
These are all MIME types, and they do well to differentiate between the file types
and the extensions of the file type. For example, image/*
refers to every type (extension) of image, while image/jpeg
, image/png
and image/gif
refers to specific extensions of the image file type.
这些都是MIME类型 ,它们很好地区分了file types
和file types
的扩展名。 例如, image/*
指image/*
每种类型(扩展名),而image/jpeg
, image/png
和image/gif
指图像文件类型的特定扩展名。
In essence, using */*
says to the android file system: Get me any file type with any extension. So, this dictates to the android file system that this intent is interested in every type of file present on the file system. After setting the type, we can then launch the activity to get the content by calling startActivityForResult
and await the arrival of the content.
本质上,使用*/*
对android文件系统说: 获取具有扩展名的任何文件类型 。 因此,这指示android文件系统此意图对文件系统上存在的每种文件类型都感兴趣。 设置类型之后,我们可以通过调用startActivityForResult
来启动活动以获取内容,然后等待内容到达。
class MainActivity : AppCompatActivity() {
private lateinit var fileDescriptor: FileDescriptor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fileDescriptor = findViewById(R.id.file_descriptor)
file_select_button.setOnClickListener {
selectFile()
}
}
private fun selectFile() {
val selectFileIntent = Intent(Intent.ACTION_GET_CONTENT)
selectFileIntent.type = "*/*"
startActivityForResult(selectFileIntent, 1)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 1 && resultCode == Activity.RESULT_OK) {
if (data != null) {
val fileUri = data.data
}
}
}
}
In onActivityResult
, we perform a check to see if the requestCode
is the same as the one the intent was launched with 1
, and if the result is in good condition. After that, we make sure the data returned is non-null, and if so, we retrieve the URI of the file by getting the data property of the data returned. The data is then stored in our fileUri
local variable.
在onActivityResult
,我们执行一次检查,以查看requestCode
是否与使用1
启动的intent相同,以及结果是否处于良好状态。 之后,我们确保返回的数据为非null,如果是,则通过获取返回数据的data属性来检索文件的URI。 然后将数据存储在我们的fileUri
局部变量中。
Everything looks good, but we have no way to inform the FileDesriptor
of the file that was selected. Well, we can do that by passing the variable into the FileDescriptor
to notify the component of the file’s existence.
一切看起来都不错,但是我们无法将所选文件通知FileDesriptor
。 好吧,我们可以通过将变量传递到FileDescriptor
来通知组件该文件的存在来实现。
// file types
enum class FileType {
IMAGE, TEXT, PDF, DOCX, MP4, MP3, UNKNOWN
}
class FileDescriptor @JvmOverloads
constructor(private val ctx: Context, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0)
: ConstraintLayout(ctx, attributeSet, defStyleAttr) {
var fileUri: Uri? = null
set(value) {
field = value
setUpFileDescriptor()
}
var file: File? = null
var fileType: FileType? = null
private fun setUpFileDescriptor() {
}
}
Firstly, we start by creating an enum class that holds constants for the types of files we would want to manage with our FileDescriptor
. We have constants for most file types and an UNKNOWN
constant for files we would not be accounting for.
首先,我们从创建一个枚举类开始,该枚举类包含要使用FileDescriptor
管理的文件类型的常量。 对于大多数文件类型,我们都有一个常量;对于我们不会考虑的文件,我们有一个UNKNOWN
常量。
We created a mutable fileUri
property that allows us to set the URI of the file we would be describing. A custom setter is then set up to immediately call the setUpFileDescriptor
function once a fileUri
is set. A File
and FileType
object is also created, the former is used to store the file located at the URI set up by the file descriptor locally in our component so every needed piece of data can be gotten at one source, while the former denotes the type of file we have set up. Then the setUpFileDescriptor
method is also declared.
我们创建了一个可变的fileUri
属性,该属性允许我们设置将要描述的文件的URI。 然后设置自定义设置程序,以在设置fileUri
立即调用setUpFileDescriptor
函数。 还创建了一个File
和FileType
对象,前者用于存储位于文件描述符在我们组件中本地设置的URI处的文件,因此每一个需要的数据都可以从一个源获取,而前者表示类型已建立的档案。 然后,还声明了setUpFileDescriptor
方法。
class FileDescriptor @JvmOverloads
constructor(private val ctx: Context, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0)
: ConstraintLayout(ctx, attributeSet, defStyleAttr) {
// ...variable declarations
private fun setUpFileDescriptor() {
setFile()
}
private fun setFile() {
// columns to be retrieved from the media content provider
val columns = arrayOf(MediaStore.Images.Media.DATA)
// query the content resolver for the data associated with the fileUri passed in
val cursor = ctx.contentResolver.query(
fileUri!!, columns, null, null, null
)
cursor?.let {
it.moveToFirst()
val dataColumnIndex = it.getColumnIndex(columns[0])
val filePath = cursor.getString(dataColumnIndex)
it.close()
filePath?.let { path ->
file = File(path)
}
}
}
}
So, we create a new method called setFile
, and call it from the setUpFileDescriptor
method, this method is used to query the Android’s content provider for the whole File object associated with the fileUri
in the FileDescriptor
. This allows us to get accurate data about the File
to be displayed in the component.
因此,我们创建了一个名为setFile
的新方法,并从setUpFileDescriptor
方法中对其进行了setUpFileDescriptor
,该方法用于向Android的内容提供程序查询与FileDescriptor
的fileUri
关联的整个File对象。 这使我们可以获得有关要在组件中显示的File
准确数据。
This method starts by setting up the columns to be queried from the Content Provider, in this case, we would be going with just one — MediaStore.Images.Media.DATA
, this gives us basic descriptive data present in the file. We then run a query on the content provider using the fileUri
passed in the columns of data to be retrieved. We pass null into the rest of the parameters since we would not be providing any selection clauses or arguments into the query. The query then returns a cursor that holds a reference to the File’s data.
此方法首先设置要从Content Provider查询的列,在这种情况下,我们将只处理其中一个— MediaStore.Images.Media.DATA
,这为我们提供了文件中存在的基本描述性数据。 然后,我们使用传递到要检索的数据列中的fileUri
在内容提供程序上运行查询。 我们将null传递给其余参数,因为我们不会在查询中提供任何选择子句或参数。 然后查询将返回一个游标,其中包含对文件数据的引用。
After making sure the cursor isn’t null, we move to the first entry in the cursor. We then need to get the index of the data column we requested for in the whole entry, hence the call to getColumnIndex()
, that should return the index of the column that contains our data. To correctly instantiate our File
object, we need to get the fully-qualified path, the path is present in the DATA
column, so we can get the String value present at the DATA column index in the entry. With that, we get the filePath
and the File
object is then instantiated with the filePath
as a parameter. And that’s it for the File setup.
确保光标不为空后,我们移至光标的第一项。 然后,我们需要获取整个条目中所需的数据列的索引,因此需要调用getColumnIndex()
,该调用应返回包含我们的数据的列的索引。 为了正确地实例化我们的File
对象,我们需要获取完全限定的路径,该路径位于DATA
列中,因此我们可以获取该条目中DATA列索引处存在的String值。 这样,我们得到了filePath
,然后使用filePath
作为参数实例化File
对象。 这就是文件设置。
Now that we have our file, we need to set up the correct image (thumbnail) to be displayed depending on the file type, but let us start by defining the logic for the thumbnail selection.
现在我们有了文件,我们需要根据文件类型设置要显示的正确图像(缩略图),但让我们首先定义缩略图选择的逻辑。
The required images for thumbnails are already in the drawables folder.
缩略图所需的图像已经在drawables文件夹中。
class FileDescriptor @JvmOverloads
constructor(private val ctx: Context, val attributeSet: AttributeSet? = null, defStyleAttr: Int = 0)
: ConstraintLayout(ctx, attributeSet, defStyleAttr) {
// variable declarations
// function declarations
private fun setUpFileTypeImage() {
file_type_image.setImageResource(when(fileType) {
FileType.DOCX -> R.drawable.docx
FileType.IMAGE -> R.drawable.image
FileType.MP3 -> R.drawable.mp3
FileType.MP4 -> R.drawable.video
FileType.PDF -> R.drawable.pdf
FileType.TEXT -> R.drawable.txt
FileType.UNKNOWN -> R.drawable.no_file_selected
else -> R.drawable.no_file_selected
})
}
}
Essentially, we declare a private function called setUpFileTypeImage
and using a when statement, we correctly determine the drawable to be displayed by the compound component depending on the FileType
set by the File
object. And if the FileType
isn’t recognized, we use the same drawable used when it is in an UNKNOWN
state.
本质上,我们声明一个名为setUpFileTypeImage
的私有函数,并使用when语句,根据File
对象设置的FileType
正确确定复合组件要显示的可绘制对象。 如果无法识别FileType
,我们将使用处于UNKNOWN
状态的同一个drawable。
That done, we need to actually retrieve the FileType
. Fortunately, Android’s content resolver API provides a helper getType
method that returns the MIME type associated with our file.
完成后,我们需要实际检索FileType
。 幸运的是,Android的内容解析器API提供了一个助手getType
方法,该方法返回与我们的文件关联的MIME类型。
class FileDescriptor @JvmOverloads
constructor(private val ctx: Context, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0)
: ConstraintLayout(ctx, attributeSet, defStyleAttr) {
// variable declarations
// function declarations
private fun setUpFileType() {
// get a reference to the android's content resolver
val resolver = ctx.contentResolver
// get the type of the file
val type = resolver.getType(fileUri!!)!!
// run check
fileType = if (type.contains("image")) {
FileType.IMAGE
} else if (type.contains("video")) {
FileType.MP4
} else if (type.contains("application/msword") || type.contains("application/vnd.openxmlformats-officedocument.wordprocessingml.document")) {
FileType.DOCX
} else if (type.contains("audio")) {
FileType.MP3
} else if (type.contains("application/pdf")) {
FileType.PDF
} else if (type.contains("text/plain")) {
FileType.TEXT
} else {
FileType.UNKNOWN
}
setUpFileTypeImage()
}
}
We create a new method that starts by getting a reference to the content resolver android provides, then we run getType
on the resolver while passing in the fileUri
to denote what file whose type we want to retrieve, the returned type is then stored in the variable type
.
我们创建一个新方法,首先获取对android提供的内容解析器的引用,然后在解析器上运行getType
,同时传入fileUri
来表示要检索其类型的文件,然后将返回的类型存储在变量中type
。
Using a basic if statement, we run through the MIME types that we support and if the type returned by the content resolver contains any of the MIME types for the supported files, we go on to set the FileType
appropriately.
使用基本的if语句,我们将遍历我们支持的MIME类型,并且如果内容解析器返回的类型包含受支持文件的任何MIME类型,我们将继续适当地设置FileType
。
When that is done, we call setUpFileImage
, which does the job of setting up the mini-image denoting the type of file that was selected, then we can call setUpFileType
from our setFile()
function. So our setFile
function looks like this.
完成后,我们调用setUpFileImage
,它会完成设置表示所选文件类型的微型图像的工作,然后可以从setFile()
函数中调用setUpFileType
。 所以我们的setFile
函数看起来像这样。
private fun setUpFileDescriptor() {
setFile()
setUpFileType()
}
Before we move on, now that we have put up the logic to get the file, process and store it’s reference, we can now link the fileUri
returned by the FileChooser to the FileDescriptor
by doing:
在继续之前,现在我们已经建立了获取文件,处理和存储引用的逻辑,现在我们可以通过执行以下操作将FileChooser返回的fileUri
链接到FileDescriptor
:
class MainActivity : AppCompatActivity() {
// variable declarations
// onCreate callback
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_FILE_CODE && resultCode == Activity.RESULT_OK) {
if (data != null) {
val fileUri = data.data
// set the fileUri retrieved from the FileChooser to the fileUri of the FileDescriptor
fileDescriptor.fileUri = fileUri
}
}
}
}
Since our File
object is set up, we would need to set up the part of our Compound Component that holds description about the selected File. There exists very useful properties on the File
object that provides us with most of the descriptive data that we need. Below is the code that sets up data in the views present in the compound components.
由于已经设置了File
对象,因此我们需要设置复合组件中包含有关所选File的描述的部分。 File
对象上存在非常有用的属性,可为我们提供所需的大多数描述性数据。 以下是在复合组件中存在的视图中设置数据的代码。
class FileDescriptor @JvmOverloads
constructor(private val ctx: Context, val attributeSet: AttributeSet? = null, defStyleAttr: Int = 0)
: ConstraintLayout(ctx, attributeSet, defStyleAttr) {
init {
// layout inflation and setup
// set the visibility of the info section to visible for testing purposes
file_info.visibility = View.VISIBLE
}
private fun setUpFileDescriptor() {
setFile()
setUpFileType()
file?.let {
file_name.text = it.name
file_info.text = """
File Name: ${ it.name }
Path To File: ${ it.path }
Last Modified: ${ it.lastModified() }
Size: ${ it.length() }B
""".trimIndent()
}
}
}
Initially, we start by making sure the File
object is not null, and if so, we construct a multi-line string that describes certain details about the file by calling required methods on the File
object. After running the app, and selecting the file we are interested in, we get this as a result.
最初,我们首先确保File
对象不为null,如果是,我们将通过在File
对象上调用所需的方法来构造多行字符串,以描述有关文件的某些详细信息。 运行该应用程序并选择我们感兴趣的文件后,我们得到了此结果。
Now, our compound component is looking pretty good, but the details in the component don’t really provide optimal user experience, the size of the file can be viewed in bytes and the last modified date is a Long
value. Therefore, we can define extension functions on the Long
data type to modify the data as needed.
现在,我们的复合组件看起来很不错,但是组件中的细节并不能真正提供最佳的用户体验,文件大小可以按字节查看,最后修改日期为Long
值。 因此,我们可以在Long
数据类型上定义扩展函数,以根据需要修改数据。
fun Long.format(format: String): String {
return SimpleDateFormat(format).format(this.absoluteValue)
}
fun Long.valueInKb(): Double {
val kb: Double = this.div(1024).toDouble()
return kb
}
The implementation for the date formatting and bytes conversion is provided above, after doing this, our setUpFileDescriptor
method can be modified as such:
上面提供了日期格式和字节转换的实现,完成此操作后,可以如下修改setUpFileDescriptor
方法:
private fun setUpFileDescriptor() {
// ...
file?.let {
file_name.text = it.name
file_info.text = """
File Name: ${ it.name }
Path To File: ${ it.path }
Last Modified: ${ it.lastModified().format("MMM dd, YYYY hh:mm") }
Size: ${ it.length().valueInKb() }KB
""".trimIndent()
}
}
Running the app, and reproducing the steps for the file selection gives this result:
运行该应用程序,并重新生成文件选择步骤,将得到以下结果:
Next up, we would need to display a small preview of the image (we surely don’t want the big unknown image on all our files right?). To achieve this, there are varying approaches depending on the type of file whose thumbnail we try to retrieve. So as to not add to the complexity of this series, we would be manually getting the thumbnail of images and videos, but just displaying plain images for other types of file. To that effect, we create an object called ThumbnailGenerator
that handles creation of thumbnails for images and videos. Well, for video thumbnail creation, we would need the Glide image loading library, so you can start by adding this dependency in your app-level build.gradle
file.
接下来,我们将需要显示该图像的一个小预览(我们当然不希望所有文件中的未知大图像都正确吗?)。 为实现此目的,根据我们尝试检索其缩略图的文件类型,有不同的方法。 为了不增加本系列的复杂性,我们将手动获取图像和视频的缩略图,而仅显示其他类型文件的纯图像。 为此,我们创建了一个名为ThumbnailGenerator
的对象,该对象处理图像和视频缩略图的创建。 好了,对于创建视频缩略图,我们需要Glide图像加载库,因此您可以从在应用程序级别build.gradle
文件中添加此依赖关系开始。
// Glide dependencyimplementation 'com.github.bumptech.glide:glide:4.11.0'kapt 'com.github.bumptech.glide:compiler:4.11.0'
// Glide dependencyimplementation 'com.github.bumptech.glide:glide:4.11.0'kapt 'com.github.bumptech.glide:compiler:4.11.0'
We can now define our ThumbnailGenerator
object as such:
现在,我们可以这样定义ThumbnailGenerator
对象:
package dev.olaore.compoundcmpts_final
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.provider.MediaStore
import android.widget.ImageView
import com.bumptech.glide.Glide
object ThumbnailGenerator {
fun createVideoThumbnail(context: Context, imageUri: Uri?, imageView: ImageView) {
// load the video thumbnail with glide
Glide.with(context)
.asBitmap()
.load(imageUri)
.into(imageView)
}
fun createImageThumbnail(context: Context, imageUri: Uri?, imageView: ImageView) {
imageView.setImageBitmap(
MediaStore.Images.Media.getBitmap(context.contentResolver, imageUri)
)
}
}
The helper functions simply use the Glide
library in case of videos, and android’s provided getBitmap
method for images.
辅助功能仅在视频情况下使用Glide
库,而android提供了针对图像的getBitmap
方法。
Back in our FileDescriptor
, we need to create the method that effectively retrieves the thumbnail. We then define a retrieveThumbnail
method and call if from the setUpFileDescriptor
method like this:
回到FileDescriptor
,我们需要创建有效检索缩略图的方法。 然后,我们定义一个retrieveThumbnail
方法,并从setUpFileDescriptor
方法中调用if,如下所示:
class FileDescriptor @JvmOverloads
constructor(private val ctx: Context, private val attributeSet: AttributeSet? = null, private val defStyleAttr: Int = 0)
: ConstraintLayout(ctx, attributeSet, defStyleAttr) {
// ...
private fun setUpFileDescriptor() {
setFile()
setUpFileType()
file?.let {
file_name.text = it.name
file_info.text = """
File Name: ${ it.name }
Path To File: ${ it.path }
Last Modified: ${ it.lastModified().format("MMM dd, YYYY hh:mm") }
Size: ${ it.length().valueInKb() }KB
""".trimIndent()
}
// a call to retrieveThumbnail()
retrieveThumbnail()
}
private fun retrieveThumbnail() {
try {
when(fileType) {
FileType.IMAGE -> ThumbnailGenerator.createImageThumbnail(ctx, fileUri, file_preview_image)
FileType.MP4 -> ThumbnailGenerator.createVideoThumbnail(ctx, fileUri, file_preview_image)
FileType.DOCX -> file_preview_image.setImageResource(R.drawable.docx)
FileType.MP3 -> file_preview_image.setImageResource(R.drawable.mp3)
FileType.PDF -> file_preview_image.setImageResource(R.drawable.pdf)
FileType.TEXT -> file_preview_image.setImageResource(R.drawable.txt)
FileType.UNKNOWN -> file_preview_image.setImageResource(R.drawable.no_file_selected)
}
} catch (e: Exception) {
Log.d("FileDescriptor", "Error occured: ${ e.message }")
}
}
// ...
}
What this method does is, depending on the type of file, it either forwards the request to get the thumbnail (image/video) from the ThumbnailGenerator
or sets the provided image if the file type is none of the aforementioned. After running the application, and selecting various file types, we are presented with the following:
此方法的作用是,根据文件类型的不同,它转发请求以从ThumbnailGenerator
获取缩略图(图像/视频)的请求,或者如果文件类型不是上述任何一种,则设置提供的图像。 运行该应用程序并选择各种文件类型之后,我们将看到以下内容:
Finally, we have achieved the main aim of our compound component, and we correctly display details about a selected file no matter the file type, pretty cool!. You do deserve a round of applause for coming this far.
最终,我们达到了复合组件的主要目的,无论文件类型如何,我们都能正确显示有关所选文件的详细信息,非常酷! 您应该为此鼓掌。
👏
👏
Well, you’d notice that we still have two main features to implement: sharing of the file’s data through various media, and toggling the visibility of the information section of the compound component, head on to the third part of this article where we deal with Custom Attributes and Sharing In Android.
好了,您注意到我们仍然要实现两个主要功能:通过各种媒体共享文件数据,以及切换复合组件信息部分的可见性,请转到本文的第三部分。在Android中使用自定义属性和共享。
Big Ups!!
大UPS!
`
`
kotlin 隐藏 组件