You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

开发类WhatsApp的支持多选图片及单图添加字幕的功能

Hey there! Let's walk through building this WhatsApp-style image picker with multi-selection and per-image text input. I'll break down the key parts with practical code snippets to make this straightforward.

实现WhatsApp风格的图片选择与文字添加Demo

核心功能 Overview

First, let's clarify the requirements we're targeting:

  • Tap a button to trigger a bottom sheet that displays album image thumbnails
  • Support multi-selection of images
  • Add custom text to each selected image (either overlayed on the image or below it)

Step 1: UI Layout Setup

We'll start with the basic UI structure, including the main button, bottom sheet container, and individual image item layout.

Main Activity Layout

<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <Button
        android:id="@+id/btnOpenImagePicker"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Select Images & Add Text" />

</LinearLayout>

Bottom Sheet Layout

This will hold our image grid and confirmation button:

<!-- bottom_sheet_image_picker.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Pick Images & Add Custom Text"
        android:textSize="18sp"
        android:textStyle="bold"/>

    <!-- Grid for image thumbnails -->
    <GridView
        android:id="@+id/gridImages"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:numColumns="4"
        android:verticalSpacing="8dp"
        android:horizontalSpacing="8dp"/>

    <Button
        android:id="@+id/btnConfirmSelection"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Confirm Selection"
        android:layout_marginTop="16dp"/>

</LinearLayout>

Individual Image Item Layout

This includes the thumbnail, checkbox, and text input (position can be adjusted):

<!-- item_image_with_text.xml -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/imgThumbnail"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:scaleType="centerCrop"/>

    <!-- Multi-selection checkbox -->
    <CheckBox
        android:id="@+id/cbSelectImage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignTop="@id/imgThumbnail"
        android:layout_alignEnd="@id/imgThumbnail"
        android:background="@drawable/checkbox_selector"
        android:button="@null"/>

    <!-- Text input (placed below image by default) -->
    <EditText
        android:id="@+id/etImageText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/imgThumbnail"
        android:hint="Add text here"
        android:layout_marginTop="4dp"
        android:maxLines="1"/>

    <!-- To overlay text on the image, use this instead:
    <EditText
        android:id="@+id/etImageText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/imgThumbnail"
        android:background="@color/black_50"
        android:textColor="@color/white"
        android:hint="Add text"
        android:padding="4dp"/>
    -->

</RelativeLayout>

First, we need to request access to the user's gallery, then fetch the image URIs.

Add Permissions to Manifest

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- For Android 12 and below -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>

Request Permissions in Code

// MainActivity.kt
private val REQUEST_PERMISSION_CODE = 1001

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

    btnOpenImagePicker.setOnClickListener {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES) == PackageManager.PERMISSION_GRANTED) {
            openImagePickerBottomSheet()
        } else {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_MEDIA_IMAGES), REQUEST_PERMISSION_CODE)
        }
    }
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == REQUEST_PERMISSION_CODE && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
        openImagePickerBottomSheet()
    } else {
        Toast.makeText(this, "Gallery access is required to use this feature", Toast.LENGTH_SHORT).show()
    }
}
private fun getGalleryImages(): List<Uri> {
    val imageUris = mutableListOf<Uri>()
    val projection = arrayOf(MediaStore.Images.Media._ID)
    val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC" // Newest first

    contentResolver.query(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        projection,
        null,
        null,
        sortOrder
    )?.use { cursor ->
        val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
        while (cursor.moveToNext()) {
            val id = cursor.getLong(idColumn)
            val contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
            imageUris.add(contentUri)
        }
    }
    return imageUris
}

Step 3: Bottom Sheet & Multi-Selection Logic

Create a BottomSheetDialogFragment to handle the image picker UI and selection state.

Bottom Sheet Implementation

class ImagePickerBottomSheet : BottomSheetDialogFragment() {

    private lateinit var gridImages: GridView
    private lateinit var btnConfirmSelection: Button
    // Map to track selected images and their associated text
    private var selectedImages = mutableMapOf<Uri, String>()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.bottom_sheet_image_picker, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        gridImages = view.findViewById(R.id.gridImages)
        btnConfirmSelection = view.findViewById(R.id.btnConfirmSelection)

        val imageUris = (activity as MainActivity).getGalleryImages()
        val adapter = ImageWithTextAdapter(requireContext(), imageUris)
        gridImages.adapter = adapter

        btnConfirmSelection.setOnClickListener {
            // Handle the selected data here (e.g., pass back to main activity)
            Toast.makeText(requireContext(), "Selected ${selectedImages.size} images", Toast.LENGTH_SHORT).show()
            dismiss()
        }
    }

    inner class ImageWithTextAdapter(private val context: Context, private val imageUris: List<Uri>) : BaseAdapter() {

        override fun getCount(): Int = imageUris.size

        override fun getItem(position: Int): Any = imageUris[position]

        override fun getItemId(position: Int): Long = position.toLong()

        override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
            val view = convertView ?: LayoutInflater.from(context).inflate(R.layout.item_image_with_text, parent, false)
            val imgThumbnail = view.findViewById<ImageView>(R.id.imgThumbnail)
            val cbSelectImage = view.findViewById<CheckBox>(R.id.cbSelectImage)
            val etImageText = view.findViewById<EditText>(R.id.etImageText)

            val imageUri = imageUris[position]
            // Load thumbnail with Glide (add Glide dependency to your build.gradle)
            Glide.with(context)
                .load(imageUri)
                .centerCrop()
                .into(imgThumbnail)

            // Restore selection state and text
            cbSelectImage.isChecked = selectedImages.containsKey(imageUri)
            etImageText.setText(selectedImages[imageUri])

            // Update selection state on checkbox tap
            cbSelectImage.setOnCheckedChangeListener { _, isChecked ->
                if (isChecked) {
                    selectedImages[imageUri] = etImageText.text.toString()
                } else {
                    selectedImages.remove(imageUri)
                }
            }

            // Update text in map when user types
            etImageText.addTextChangedListener(object : TextWatcher {
                override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

                override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                    if (cbSelectImage.isChecked) {
                        selectedImages[imageUri] = s.toString()
                    }
                }

                override fun afterTextChanged(s: Editable?) {}
            })

            return view
        }
    }
}

Open the Bottom Sheet from Main Activity

private fun openImagePickerBottomSheet() {
    val bottomSheet = ImagePickerBottomSheet()
    bottomSheet.show(supportFragmentManager, "ImagePickerBottomSheet")
}

Step 4: Key Optimizations

  • Image Loading: Always use a library like Glide or Picasso to load thumbnails—this prevents OOM errors and adds caching for better performance.
  • Text Position Toggle: Add a toggle button in the bottom sheet to let users switch between overlayed text and bottom-aligned text by modifying the EditText's layout parameters.
  • State Preservation: The selectedImages map ensures that selection state and text aren't lost when scrolling the grid.
  • Pagination: For large photo libraries, implement pagination to load images in batches instead of all at once.

内容的提问来源于stack exchange,提问作者Rajeshwari Yadav

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 06:42:41