跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaWeChatjava

Android 实现类似朋友圈发说说功能:拍照、选图与多图上传(Retrofit2)

Android 开发中实现类似朋友圈的图文发布功能,涵盖图片选择器集成、相机调用及多图上传逻辑。通过 PhotoPicker 库简化交互,利用 Retrofit2 配合 Multipart 请求处理后端文件传输,同时提供 SpringMVC 与 Struts2 服务端接收示例,解决移动端图片压缩与批量上传的实际问题。

极客工坊发布于 2025/1/19更新于 2026/8/1837 浏览
Android 实现类似朋友圈发说说功能:拍照、选图与多图上传(Retrofit2)

项目背景

最近在做 Android 项目时,需要实现一个类似微信朋友圈的'发说说'功能。核心需求包括:支持拍照、从图库选择图片、以及多张图片批量上传。市面上很多 App 都有类似功能,但具体实现细节往往各有不同。下面分享一下基于 Retrofit2.0 和 PhotoPicker 库的实现过程。

一、集成图片选择器

本项目使用了一个名为 photopicker 的开源库,它封装了图片选择和预览的功能,支持是否调用相机、选中后预览等操作。

1. 初始化 Intent

在 Activity 中简单配置 PhotoPickerIntent:

PhotoPickerIntent intent = new PhotoPickerIntent(MainActivity.this);
intent.setSelectModel(SelectModel.MULTI); // 多选模式
// intent.setShowCamera(true);            // 是否显示拍照按钮
intent.setMaxTotal(6);                   // 最多选择照片数量,默认为 6
intent.setSelectedPaths(imagePaths);     // 已选中的照片地址,用于回显选中状态
startActivityForResult(intent, REQUEST_CAMERA_CODE);
2. 处理返回结果

重写 onActivityResult 方法,根据请求码区分是选择了照片还是预览了照片:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == RESULT_OK) {
        switch (requestCode) {
            case REQUEST_CAMERA_CODE:
                loadAdapter(data.getStringArrayListExtra(PhotoPickerActivity.EXTRA_RESULT));
                break;
            case REQUEST_PREVIEW_CODE:
                loadAdapter(data.getStringArrayListExtra(PhotoPreviewActivity.EXTRA_RESULT));
                break;
        }
    }
}

二、UI 适配:GridAdapter

在展示图片列表时,我们需要一个特殊的入口来触发选择器。这里采用在路径集合中默认添加一个特殊标识(如 "000000")的方式。

1. 适配器逻辑

当图片数量达到上限(例如 7 张,含占位符)时,移除最后一个占位符,保证 UI 整洁。

private class GridAdapter extends BaseAdapter {
    private ArrayList<String> listUrls;
    private LayoutInflater inflater;

    public GridAdapter(ArrayList<String> listUrls) {
        this.listUrls = listUrls;
        if (listUrls.size() == 7) {
            listUrls.remove(listUrls.size() - 1);
        }
        inflater = LayoutInflater.from(MainActivity.this);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder = null;
        if (convertView == null) {
            holder = new ViewHolder();
            convertView = inflater.inflate(R.layout.item_image, parent, false);
            holder.image = (ImageView) convertView.findViewById(R.id.imageView);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        final String path = listUrls.get(position);
        if (path.equals("000000")) {
            // 显示添加图标
            holder.image.setImageResource(R.mipmap.ic_launcher);
        } else {
            Glide.with(MainActivity.this)
                    .load(path)
                    .placeholder(R.mipmap.default_error)
                    .error(R.mipmap.default_error)
                    .centerCrop()
                    .crossFade()
                    .into(holder.image);
        }
        return convertView;
    }

    static class ViewHolder {
        ImageView image;
    }
}

三、上传管理:Retrofit2 实现

这是核心部分。我们定义了一个接口来处理单张或多张图片的上传。注意,Retrofit 的 @Multipart 注解配合 RequestBody 可以灵活处理文件流。

1. 接口定义

为了兼容不同的上传场景,提供了三种重载方法:

public interface FileUploadService {
    /**
     * 上传一张图片
     */
    @Multipart
    @POST("/upload")
    Call<String> uploadImage(@Part("fileName") String description,
                             @Part("file\"; filename=\"image.png\"") RequestBody imgs);

    /**
     * 上传 6 张图片(固定参数)
     */
    @Multipart
    @POST("/upload")
    Call<String> uploadImage(@Part("description") String description,
                             @Part("file\"; filename=\"image.png\"") RequestBody imgs1,
                             @Part("file\"; filename=\"image.png\"") RequestBody imgs2,
                             @Part("file\"; filename=\"image.png\"") RequestBody imgs3,
                             @Part("file\"; filename=\"image.png\"") RequestBody imgs4,
                             @Part("file\"; filename=\"image.png\"") RequestBody imgs5,
                             @Part("file\"; filename=\"image.png\"") RequestBody imgs6);

    /**
     * 简便写法:使用 Map 动态传入多个文件
     */
    @Multipart
    @POST("/upload")
    Call<String> uploadImage(@Part("description") String description,
                             @PartMap Map<String, RequestBody> imgs);
}
2. 执行上传

实际调用时,建议使用 @PartMap 方式,这样代码更简洁,不需要为每个文件写单独参数。

public static void uploadMany(ArrayList<String> paths, String desc) {
    Map<String, RequestBody> photos = new HashMap<>();
    if (paths.size() > 0) {
        for (int i = 0; i < paths.size(); i++) {
            String filename = paths.get(i).substring(paths.get(i).lastIndexOf("/") + 1);
            String key = "file\"; filename=" + filename;
            photos.put(key, RequestBody.create(MediaType.parse("multipart/form-data"), new File(paths.get(i))));
        }
    }
    Call<String> stringCall = apiManager.uploadImage(desc, photos);
    stringCall.enqueue(new Callback<String>() {
        @Override
        public void onResponse(Call<String> call, Response<String> response) {
            Log.d(TAG, "onResponse: " + response.body());
        }

        @Override
        public void onFailure(Call<String> call, Throwable t) {
            Log.e(TAG, "onFailure", t);
        }
    });
}

四、主界面逻辑整合

在 MainActivity 中,我们将上述组件串联起来。注意清理掉了一些过时的包引用和作者信息,保持代码纯净。

public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_CAMERA_CODE = 10;
    private static final int REQUEST_PREVIEW_CODE = 20;
    private ArrayList<String> imagePaths = new ArrayList<>();
    private GridView gridView;
    private GridAdapter gridAdapter;
    private Button mButton;
    private EditText textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        gridView = findViewById(R.id.gridView);
        mButton = findViewById(R.id.button);
        textView = findViewById(R.id.et_context);

        // 设置列数自适应
        int cols = getResources().getDisplayMetrics().widthPixels / getResources().getDisplayMetrics().densityDpi;
        cols = cols < 3 ? 3 : cols;
        gridView.setNumColumns(cols);

        // 点击事件:判断是添加图片还是预览
        gridView.setOnItemClickListener((parent, view, position, id) -> {
            String imgPath = (String) parent.getItemAtPosition(position);
            if ("000000".equals(imgPath)) {
                PhotoPickerIntent intent = new PhotoPickerIntent(MainActivity.this);
                intent.setSelectModel(SelectModel.MULTI);
                intent.setShowCamera(true);
                intent.setMaxTotal(6);
                intent.setSelectedPaths(imagePaths);
                startActivityForResult(intent, REQUEST_CAMERA_CODE);
            } else {
                PhotoPreviewIntent intent = new PhotoPreviewIntent(MainActivity.this);
                intent.setCurrentItem(position);
                intent.setPhotoPaths(imagePaths);
                startActivityForResult(intent, REQUEST_PREVIEW_CODE);
            }
        });

        // 初始化列表,预留一个添加按钮位置
        imagePaths.add("000000");
        gridAdapter = new GridAdapter(imagePaths);
        gridView.setAdapter(gridAdapter);

        // 提交按钮
        mButton.setOnClickListener(v -> {
            String desc = textView.getText().toString().trim();
            if (desc.isEmpty()) desc = "随便聊聊";
            new Thread(() -> FileUploadManager.uploadMany(imagePaths, desc)).start();
        });
    }

    // ... onActivityResult 和 loadAdapter 方法同上 ...
}

五、服务端接收示例

为了让整个流程跑通,后端也需要相应的处理逻辑。这里提供 SpringMVC 和 Struts2 两种常见框架的接收示例。

1. SpringMVC 接收
@RequestMapping("/upload")
public String addUser(@RequestParam("file") CommonsMultipartFile[] files, HttpServletRequest request) {
    for (CommonsMultipartFile file : files) {
        System.out.println("fileName---------->" + file.getOriginalFilename());
        if (!file.isEmpty()) {
            try {
                FileOutputStream os = new FileOutputStream("f:/img/" + new Date().getTime() + ".jpg");
                FileInputStream in = file.getInputStream();
                byte[] buffer = new byte[1024];
                int b;
                while ((b = in.read(buffer)) != -1) {
                    os.write(buffer, 0, b);
                }
                os.flush();
                os.close();
                in.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    return "success";
}
2. Struts2 接收
@Controller
public class UploadFile extends ActionSupport {
    private File[] file;
    private String description;

    public File[] getFile() { return file; }
    public void setFile(File[] file) { this.file = file; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }

    @Action("/upload")
    public String upload() {
        System.out.println("上传的文件=" + Arrays.toString(file));
        System.out.println("说说内容=" + description);
        return SUCCESS;
    }
}

六、效果说明

完成上述步骤后,应用即可实现以下功能:

  1. 点击网格中的加号图标,唤起系统相册或相机。
  2. 支持多选图片,并在界面上实时预览缩略图。
  3. 输入文字描述后点击发送,数据通过 Retrofit 异步上传至服务器。
  4. 服务端可正常接收文件流并保存。

目录

  1. 项目背景
  2. 一、集成图片选择器
  3. 1. 初始化 Intent
  4. 2. 处理返回结果
  5. 二、UI 适配:GridAdapter
  6. 1. 适配器逻辑
  7. 三、上传管理:Retrofit2 实现
  8. 1. 接口定义
  9. 2. 执行上传
  10. 四、主界面逻辑整合
  11. 五、服务端接收示例
  12. 1. SpringMVC 接收
  13. 2. Struts2 接收
  14. 六、效果说明
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 6 款开源视觉模型推荐:M2FP 多人人体解析与 WebUI 实践
  • Python 爬取财富中国 500 强数据示例
  • Python + Bright Data MCP 实时抓取 Google 搜索结果实战
  • GitHub Copilot 权限管理与安全合规实践
  • 新能源汽车实训系统:数字孪生与 VR/AR 技术架构
  • Spring Cloud Nacos 微服务核心实战:注册、配置与负载均衡
  • OpenArm 开源机械臂技术框架与开发指南
  • 网络安全入门教程:基础原理与实战指南
  • hacdias/webdav WebDAV 服务器部署与安全配置指南
  • Higress MCP Server 插件:REST API 转 AI 工具配置指南
  • Go 项目中使用 Casbin 实现 RBAC 权限管理
  • C++ 引用折叠规则与模板推导应用详解
  • Linux 网络:数据链路层详解
  • 基于 Rokid 眼镜的 AI 天气与旅游规划应用实战
  • Higress 将 REST API 转换为 MCP Server 工具配置
  • 磁力链接、BT 种子与 DHT 网络原理及 Go 实现
  • 使用 Higress 将 REST API 转换为 MCP Server 工具
  • Kurator 云边协同与多集群治理实操指南
  • 从零构建并训练基于 BERT 架构的生成式大模型
  • Python 移动应用开发实战:Kivy 与 BeeWare 框架详解

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online