Android 上传图片到服务器(单文件上传)

一、Android端选择图片并显示

1.设计item

我们将使用RecyclerView来展示我们选择好的图片。因此我们首先要设计item,item有两种:添加按钮、图片。

添加按钮item:add_image_button_item.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"
    android:layout_width="110dp"
    android:layout_height="115dp">

    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:scaleType="centerCrop"
        android:src="@mipmap/img_pick"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

图片:image_item.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"
    android:layout_width="110dp"
    android:layout_height="110dp">

    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:scaleType="centerCrop"
        android:src="@mipmap/img_pick"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <ProgressBar
        android:id="@+id/pb_bar"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <ImageView
        android:id="@+id/iv_delete"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_alignParentRight="true"
        android:src="@mipmap/delete"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

这个item包括了显示图片的ImageView,带一个右上角的删除按钮,底部带一个进度条。
当item显示图片时,删除按钮,进度条都应该设置为可见。当item是一个添加按钮时,它们就要隐藏。图片与添加按钮本来是可以分开的,但是为了方便,我将其写在一起。
在这里插入图片描述activity_main.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_img"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

2.设计数据类

数据类中有我们的图片数据:ImageFileBean.java。

public class ImageFileBean {
    private File file; // 图片文件
    private boolean isUpload = false; //标识该文件是否上传
    private Bitmap bitmap;// 图片
    private boolean startUpload; // 标识图片是否开始上传,以此控件ProgressBar的显示

    public ImageFileBean(File file, int pg) {
        this.file = file;
    }

    public ImageFileBean(File file, Bitmap bitmap,boolean isUpload) {
        this.file = file;
        this.isUpload = isUpload;
        this.bitmap = bitmap;
    }
	...
}

将图片数据与添加按钮统一处理的数据类:ItemBean.java。

public class ItemBean {
    private boolean isButton; // 是否是添加图片的按钮
    private ImageFileBean imageFileBean;

    public ItemBean(){}
    public ItemBean(ImageFileBean bean,boolean isButton){
        this.imageFileBean = bean;
        this.isButton = isButton;
    }
	...
}

3.编写适配器,将数据装配到视图上

LoadImageAdapter.java:

package com.wong.imageupload;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;

public class LoadImageAdapter extends RecyclerView.Adapter<LoadImageAdapter.ButtonViewHolder> {


    private final static int BUTTON_TYPE = 100;
    private final static int IMAGE_TYPE = 200;
    private List<ItemBean> list = null;

    private OnImageItemClickListener onImageItemClickListener;

    public LoadImageAdapter(List<ItemBean> list) {
        this.list = list;
    }

    @NonNull
    @Override
    public ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        switch (viewType) {
            case BUTTON_TYPE:
                View buttonView = LayoutInflater.from(parent.getContext()).inflate(R.layout.add_image_button_item, parent, false);
                return new ButtonViewHolder(buttonView);
            default:
                View imageView = LayoutInflater.from(parent.getContext()).inflate(R.layout.image_item, parent, false);
                return new ImageViewHolder(imageView);
        }

    }

    @Override
    public void onBindViewHolder(@NonNull ButtonViewHolder holder, final int position) {
        if (!list.get(position).isButton()) {
            ImageViewHolder imageViewHolder = (ImageViewHolder)holder;
            // 显示图片
            imageViewHolder.mIVImg.setImageBitmap(list.get(position).getImageFileBean().getBitmap());
            boolean startUpload = list.get(position).getImageFileBean().isStartUpload();
            boolean isUpload = list.get(position).getImageFileBean().isUpload();
            if(startUpload && !isUpload){
                imageViewHolder.mPB.setVisibility(View.VISIBLE);
            }else{
                imageViewHolder.mPB.setVisibility(View.GONE);
            }
            // 点击删除按钮
            imageViewHolder.mIVDel.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (onImageItemClickListener != null) {
                        onImageItemClickListener.onDelete(v, list.get(position), position);
                    }
                }
            });
        }

        holder.mIVImg.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (onImageItemClickListener != null) {
                    onImageItemClickListener.onClick(v, list.get(position), position);
                }
            }
        });

    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    @Override
    public int getItemViewType(int position) {
        if (list.get(position).isButton()) {
            return BUTTON_TYPE;
        } else {
            return IMAGE_TYPE;
        }
    }

    public void setOnImageItemClickListener(OnImageItemClickListener onImageItemClickListener) {
        this.onImageItemClickListener = onImageItemClickListener;
    }

    static class ButtonViewHolder extends RecyclerView.ViewHolder {

        ImageView mIVImg;
        View view;

        public ButtonViewHolder(@NonNull View itemView) {
            super(itemView);
            view = itemView;
            mIVImg = itemView.findViewById(R.id.iv_img);
        }
    }

    static class ImageViewHolder extends ButtonViewHolder {
        ImageView mIVDel;
        ProgressBar mPB;

        public ImageViewHolder(@NonNull View itemView) {
            super(itemView);
            mIVDel = itemView.findViewById(R.id.iv_delete);
            mPB = itemView.findViewById(R.id.pb_bar);
        }
    }

    public interface OnImageItemClickListener {
        void onClick(View view, ItemBean itemBean, int position);

        void onDelete(View view, ItemBean itemBean, int position);
    }
}


4.核心代码

4.1打开相册的方式

// 这种方式是通过action方式打开android的其他app来完成的
Intent galleryIntent = new Intent(Intent.ACTION_PICK); // 系统默认的图片选择程序
galleryIntent.setType("image/*");
startActivityForResult(galleryIntent,REQUEST_GALLERY);

获取选中的图片:

Uri uri = data.getData(); 

4.2打开拍照的方式

// 这种方式是通过action方式打开android的其他app来完成的
// MediaStore.ACTION_IMAGE_CAPTURE 即android.media.action.IMAGE_CAPTURE
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);// 系统的相机程序
// 准备图片名称
String imageName = UUID.randomUUID().toString().replace("-","")+".jpg";
// 创建应用内缓存目录cache/images
CacheUtils.createImagesCacheFolder(MainActivity.this);
File cameraFile = new File(CacheUtils.getCacheImagesFolder(MainActivity.this).getPath()+"/"+imageName);
// 创建好图片文件接收拍照的数据
if(!cameraFile.exists()){
	try {
		cameraFile.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
	// 在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException。7.0以上的系统需要使用FileProvider兼容拍照
	cameraUri = FileProvider.getUriForFile(MainActivity.this, "com.wong.camera.fileprovider", cameraFile);
}else{
    cameraUri = Uri.fromFile(cameraFile);
}
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT,cameraUri);
startActivityForResult(cameraIntent,REQUEST_CAMERA);
4.2.1兼容7.0后的拍照

在官方7.0的以上的系统中,尝试传递 file://URI可能会触发FileUriExposedException。7.0以上的系统需要使用FileProvider兼容拍照:
第一步:在AndroidManifest.xml加入以下内容。

<provider
	android:authorities="com.wong.camera.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/fileprovider"/>
</provider>

第二步:新建资源目录xml,并配置FileProvider的资源目录。
fileprovider.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!--name,此值会隐藏您要共享的子目录的名称,path,实际的子目录名称-->
    <!--设备根目录new File("/")-->
    <root-path name="root" path="" />
    <!--代表context.getFilesDir()-->
    <files-path name="my_files" path="files" />
    <!--代表context.getCacheDir()-->
    <cache-path name="cache" path="images" />
    <!--代表Environment.getExternalStorageDirectory(),如下代表的目录即为:Environment.getExternalStorageDirectory()/upload,其他同理。-->
    <external-path name="external" path="upload" />
    <!--代表context.getExternalFilesDirs()-->
    <external-files-path name="name1" path="path1" />
    <!--代表getExternalCacheDirs()-->
    <external-cache-path name="name2" path="path2" />
</paths>

通过URI获取图片:

Bitmap bitmap = null;
if(uri != null){
    try {
         BitmapFactory.Options options = new BitmapFactory.Options();
         options.inSampleSize = 2; // 图片宽高都为原来的二分之一,即图片为原来的四分之一
         bitmap = BitmapFactory.decodeStream(this.getContentResolver().openInputStream(uri), null, options);
         String filePath = uri.getEncodedPath();;
         File file = new File(filePath);
     }catch (Exception e){

     }
}

4.3获取图片


    /**
     * 将Uri图片类型转换成File,BitMap类型
     * 在界面上显示BitMap图片,以防止内存溢出
     * 上传可选择File文件上传
     *
     * @param uri
     */
    private void saveUriToFile(Uri uri,int from) {
        Bitmap bitmap = null;
        if (uri != null) {
            try {
                BitmapFactory.Options options = new BitmapFactory.Options();
                options.inSampleSize = 2; // 图片宽高都为原来的二分之一,即图片为原来的四分之一
                bitmap = BitmapFactory.decodeStream(this.getContentResolver().openInputStream(uri), null, options);
                File file = null;
                switch (from){
                    case REQUEST_GALLERY:
                        String filePath = FileUtils.getRealFilePath(this,uri);
                        File oldFile = new File(filePath);
                        // 修改文件名
                        String newFileName = UUID.randomUUID().toString().replace("-","")+".jpg";
                        String newFilePath = oldFile.getParent()+"/"+newFileName;
                        file = new File(newFilePath);
                        oldFile.renameTo(file);

                        break;
                    case REQUEST_CAMERA:
                        file = cameraFile;
                        break;
                }

                if(file == null || !file.exists()){
                    Log.i("异常:","文件不存在!");
                }
                list.remove(addImgButton); // 先删除
                if (list.size() < DEFAULT_NUM) {
                    ItemBean itemBean = new ItemBean(new ImageFileBean(file, bitmap, false), false);
                    list.add(itemBean);
                    if (list.size() < DEFAULT_NUM) {
                        // 如果图片数量还没有达到最大值,则将添加按钮添加到list后面
                        list.add(addImgButton);
                    }
                }

                adapter.notifyDataSetChanged();
            } catch (Exception e) {

            }
        }
    }

4.4.单文件上传图片核心代码

在form表单中enctype属性规定了form表单在发送到服务器时候编码方式,它有如下的三个值:
①application/x-www-form-urlencoded:默认的编码方式。但是在用文本的传输和MP3等大型文件的时候,使用这种编码就显得 效率低下。
②multipart/form-data:指定传输数据为二进制类型,比如图片、mp3、文件。
③text/plain:纯文体的传输。空格转换为 “+” 加号,但不对特殊字符编码。

    private void uploadImage(final ImageFileBean fileBean) {
        File file = fileBean.getFile();
        if (file == null) return;
        if(!file.exists()){
            Toast.makeText(this, "文件不存在!", Toast.LENGTH_SHORT).show();
            return;
        }
        viewHolder.mTVText.setText("HashCode#"+SHA256.getSHA256(file));
        Log.i("文件HashCode:",SHA256.getSHA256(file));

        // 准备Body
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart("name",file.getName())// 其他信息
                .addFormDataPart("id","12,13,14")// 其他信息
                .addFormDataPart("type","2")// 其他信
                .addFormDataPart("file", file.getName(),
                        RequestBody.create(MediaType.parse("multipart/form-data"), file))//文件
                .build();
        Request request = new Request.Builder()
                .url(Global.UPLOAD_URL).post(requestBody)
                .addHeader("user-agent", "PDA")
                .addHeader("x-userid", "752332")// 添加x-userid请求头
                .addHeader("x-sessionkey", "kjhsfjkaskfashfuiwf")// 添加x-sessionkey请求头
                .addHeader("x-tonce", Long.valueOf(System.currentTimeMillis()).toString())// 添加x-tonce请求头
                .addHeader("x-timestamp", Long.valueOf(System.currentTimeMillis()).toString())// 添加x-timestamp请求头
                .build();

        OkHttpClient okHttpClient = new OkHttpClient();
       final Message msg = myHandler.obtainMessage();
        okHttpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                msg.obj = fileBean;
                msg.what =0;
                myHandler.sendMessage(msg);
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                String result = response.body().string();
                Log.i("上传图片结果:", result);
                msg.obj = fileBean;
                if (!response.isSuccessful()) {
                    Log.i("响应失败:", response.code() + "");
                    msg.what =1;
                    return;
                }
                msg.what = 3;
                myHandler.sendMessage(msg);

            }
        });
    }

4.5.多文件上传图片核心代码

请参考《Android 上传图片到服务器(多文件上传)》

二、服务器端

服务端我们选择用SpringBoot来实现,非常简单:

  /**
     * 单文件上传
     *
     * @param file
     * @param model
     * @param request
     * @return
     */
    @PostMapping("/api/upload")
    public String fileUpload(@RequestParam(value = "file") MultipartFile file, Model model, HttpServletRequest request, @RequestParam(value = "type") int type, @RequestHeader(value = "user-agent") String userAgent) {
        if (file.isEmpty()) {
            System.out.println("文件为空空");
        }

        logger.info("获得的其他参数type=" + type);

        logger.info("获得的Header user-agent=" + userAgent.toString());

        // 如果参数比较少可以直接在方法上使用注解@RequestParam来映射到不同的名称上获得,当然如果不用此注解,也可以定义一个与传过来的参数名一样的形参来获得
        // 蒜从客户端传过来的其他参数
        Enumeration names = request.getParameterNames();
        while (names.hasMoreElements()) {
            String key = names.nextElement().toString();
            logger.info("客户端传过来的参数#key=" + key + ",value=" + request.getParameterValues(key).toString());
        }
        Enumeration headers = request.getHeaderNames();
        while (headers.hasMoreElements()) {
            String key = headers.nextElement().toString();
            String info = "客户端传过来的Header参数:key=" + key + ",value=" + request.getHeader(key);
            logger.info(info);
        }

        // BMP、JPG、JPEG、PNG、GIF
        String fileName = file.getOriginalFilename();  // 文件名
        logger.info("上传文件名:" + fileName);
        String suffixName = fileName.substring(fileName.lastIndexOf("."));  // 后缀名
        // 验证上传的文件是否图片
        if (!".bmp".equalsIgnoreCase(suffixName) && !".jpg".equalsIgnoreCase(suffixName)
                && !".jpeg".equalsIgnoreCase(suffixName)
                && !".png".equalsIgnoreCase(suffixName)
                && !".gif".equalsIgnoreCase(suffixName)) {
            return "上传失败,请选择BMP、JPG、JPEG、PNG、GIF文件!";
        }

        fileName = UUID.randomUUID() + suffixName; // 新文件名
        File dest = new File(fileName);
        // 如果文件的父路径不存在,则创建
        if (fileName.startsWith("/") && !dest.getParentFile().exists()) {
            dest.getParentFile().mkdirs();
        }
        // 开始存放文件到指定目录去
        try {
            file.transferTo(dest);
            return "上传成功";
        } catch (IOException e) {
            e.printStackTrace();
            return "上传失败";

        }
    }

同时在application.yml配置文件配置上传的目录:

spring:
  servlet:
    multipart:
      location: /home/kyun/Downloads/recFiles

Android客户端
文件上传服务器

附:HTTP请求报文

用WireShark软件对Android发起请求时的数据进行拦截,得到的HTTP请求的报文:


Hypertext Transfer Protocol
    POST /api/upload HTTP/1.1\r\n
        [Expert Info (Chat/Sequence): POST /api/upload HTTP/1.1\r\n]
            [POST /api/upload HTTP/1.1\r\n]
            [Severity level: Chat]
            [Group: Sequence]
        Request Method: POST
        Request URI: /api/upload
        Request Version: HTTP/1.1
    user-agent: PDA\r\n
    x-userid: 752332\r\n
    x-sessionkey: kjhsfjkaskfashfuiwf\r\n
    x-tonce: 1591980645278\r\n
    x-timestamp: 1591980645278\r\n
    Content-Type: multipart/form-data; boundary=27fba8d3-a15a-40f6-8880-e0af1932914b\r\n
    Content-Length: 48558\r\n
        [Content length: 48558]
    Host: 192.168.43.120:8080\r\n
    Connection: Keep-Alive\r\n
    Accept-Encoding: gzip\r\n
    \r\n
    [Full request URI: http://192.168.43.120:8080/api/upload]
    [HTTP request 1/1]
    [Response in frame: 334]
    File Data: 48558 bytes
MIME Multipart Media Encapsulation, Type: multipart/form-data, Boundary: "27fba8d3-a15a-40f6-8880-e0af1932914b"
    [Type: multipart/form-data]
    First boundary: --27fba8d3-a15a-40f6-8880-e0af1932914b\r\n
    Encapsulated multipart part: 
        Content-Disposition: form-data; name="name"\r\n
        Content-Length: 36\r\n\r\n
        Data (36 bytes)
            Data: 353636386333306432636638346534613834323364383837[Length: 36]
    Boundary: \r\n--27fba8d3-a15a-40f6-8880-e0af1932914b\r\n
    Encapsulated multipart part: 
        Content-Disposition: form-data; name="id"\r\n
        Content-Length: 8\r\n\r\n
        Data (8 bytes)
            Data: 31322c31332c3134
            [Length: 8]
    Boundary: \r\n--27fba8d3-a15a-40f6-8880-e0af1932914b\r\n
    Encapsulated multipart part: 
        Content-Disposition: form-data; name="type"\r\n
        Content-Length: 1\r\n\r\n
        Data (1 byte)
            Data: 32
            [Length: 1]
    Boundary: \r\n--27fba8d3-a15a-40f6-8880-e0af1932914b\r\n
    Encapsulated multipart part:  (multipart/form-data)
        Content-Disposition: form-data; name="file"; filename="5668c30d2cf84e4a8423d887527fd069.jpg"\r\n
        Content-Type: multipart/form-data\r\n
        Content-Length: 47952\r\n\r\n
        The multipart dissector could not find a required parameter.
            [Expert Info (Error/Protocol): The multipart dissector could not find a required parameter.]
                [The multipart dissector could not find a required parameter.]
                [Severity level: Error]
                [Group: Protocol]
        Data (47952 bytes)
            Data: ffd8ffe12ee245786966000049492a00080000000b001001…
            [Length: 47952]
    Last boundary: \r\n--27fba8d3-a15a-40f6-8880-e0af1932914b--\r\n

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值