开发类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.
核心功能 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>
Step 2: Permission Handling & Gallery Image Loading
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() } }
Fetch Gallery Images
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
selectedImagesmap 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

