Android 垃圾分类APP(四)垃圾分类之图像输入

本文涉及的产品
图像搜索,7款服务类型 1个月
简介: Android 垃圾分类APP(四)垃圾分类之图像输入

图像输入


前言


在上一篇文章中完成了语音输入,这一篇来写图像输入


正文


 图像输入无非就是图片识别嘛,再通俗一点就是识别手机中的照片,分析里面的物品,然后进行垃圾分类。图像识别还是有很多的SDK可以使用的,这里面我目前用过的就是百度的图像识别,感觉还是蛮好的,而且有我之前的文章做普遍,那么本文是属于APP功能编写,这与单独写介绍SDK使用的文章完全是两回事。那么就来看看实践中怎么插入这个图像识别了。


如果你还有时间的话,不妨先去看看Android 百度图像识别(详细步骤+源码)


因为毕竟是写过一次的东西了,只是应用环境不同,所以下面就只是介绍业务逻辑和贴代码,不再去详细讲解。


一、创建平台应用


既然要用百度的SDK,自然要先去百度智能云注册登录,登录之后呢。点击管理控制台,然后点击左侧产品服务箭头左侧展开,找到图像识别点进去。


20210407104805977.png


点击创建应用


20210407105003726.png


输入相关的信息就可以了。


20210407105213254.png


填写好资料后点击立即创建。


20210407105432965.png


查看应用详情。


20210407105512324.png


这里有三个关键的信息:AppID、API Key、Secret Key,这三个值在后面会用到,请使用自己创建应用时生成的值。现在先把它们放到常量里面,打开Constant,这里的四个常量,对应的值就是你在平台上申请应用产生的,记得使用自己的。


20210607112017561.png


二、新建图像识别页面


在ui包下新建一个ImageInputActivity,对应的xml为activity_image_input.xml,创建好之后,再MainActivity页面中写一个按钮,点击之后进入刚才创建的这个图像识别页面。

修改activity_main.xml,在语音输入的下面加一个图像输入的按钮,代码如下:


  <!--图像输入-->
    <com.google.android.material.button.MaterialButton
        style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dp_60"
        android:layout_margin="@dimen/dp_16"
        android:gravity="center"
        android:insetTop="@dimen/dp_0"
        android:insetBottom="@dimen/dp_0"
        android:onClick="jumpImageInput"
        android:text="图像输入"
        android:textSize="@dimen/sp_16"
        android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar"
        app:backgroundTint="@color/colorPrimaryDark"
        app:cornerRadius="@dimen/dp_12"
        app:icon="@mipmap/icon_image_input"
        app:iconGravity="textStart"
        app:iconSize="@dimen/dp_24" />


进入到MainActivity中,新增一个方法jumpImageInput。


  /**
     * 进入图像输入页面
     */
    public void jumpImageInput(View view) {
        gotoActivity(ImageInputActivity.class);
    }


下面来写ImageInputActivity页面的代码,写代码之前,先完成布局编写,修改activity_image_input,里面的代码如下:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <!--标题-->
    <com.google.android.material.appbar.MaterialToolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/white"
        android:elevation="@dimen/dp_2"
        app:navigationIcon="@mipmap/icon_back">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:text="图像输入"
            android:textColor="@color/black"
            android:textSize="18sp" />
    </com.google.android.material.appbar.MaterialToolbar>
    <!--滑动控件-->
    <androidx.core.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center_horizontal"
            android:orientation="vertical">
            <ImageView
                android:id="@+id/iv_picture"
                android:layout_width="@dimen/dp_200"
                android:layout_height="@dimen/dp_300"
                android:layout_marginTop="@dimen/dp_12"
                android:visibility="gone" />
            <EditText
                android:id="@+id/et_image_url"
                android:layout_width="match_parent"
                android:layout_height="@dimen/dp_50"
                android:background="@drawable/shape_et_bg"
                android:hint="网络图片Url"
                android:layout_margin="@dimen/dp_1"
                android:textCursorDrawable="@drawable/cursor_style"
                android:paddingStart="@dimen/dp_12"
                android:paddingEnd="@dimen/dp_12"
                android:singleLine="true"
                android:imeOptions="actionGo"
                android:textSize="@dimen/sp_14"
                android:visibility="gone" />
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal"
                android:padding="@dimen/dp_12">
                <com.google.android.material.button.MaterialButton
                    android:id="@+id/btn_web_picture"
                    style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
                    android:layout_width="0dp"
                    android:layout_height="@dimen/dp_50"
                    android:layout_marginEnd="@dimen/dp_6"
                    android:layout_weight="1"
                    android:gravity="center"
                    android:insetTop="@dimen/dp_0"
                    android:insetBottom="@dimen/dp_0"
                    android:text="网络图片"
                    android:textSize="@dimen/sp_16"
                    android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar"
                    app:backgroundTint="@color/colorPrimaryDark"
                    app:cornerRadius="@dimen/dp_12"
                    app:iconGravity="textStart"
                    app:iconSize="@dimen/dp_24" />
                <com.google.android.material.button.MaterialButton
                    android:id="@+id/btn_open_album"
                    style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
                    android:layout_width="0dp"
                    android:layout_height="@dimen/dp_50"
                    android:layout_marginStart="@dimen/dp_6"
                    android:layout_marginEnd="@dimen/dp_6"
                    android:layout_weight="1"
                    android:gravity="center"
                    android:insetTop="@dimen/dp_0"
                    android:insetBottom="@dimen/dp_0"
                    android:text="相册图片"
                    android:textSize="@dimen/sp_16"
                    android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar"
                    app:backgroundTint="@color/colorPrimaryDark"
                    app:cornerRadius="@dimen/dp_12"
                    app:iconGravity="textStart"
                    app:iconSize="@dimen/dp_24" />
                <com.google.android.material.button.MaterialButton
                    android:id="@+id/btn_take_photo"
                    style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
                    android:layout_width="0dp"
                    android:layout_height="@dimen/dp_50"
                    android:layout_marginStart="@dimen/dp_6"
                    android:layout_weight="1"
                    android:gravity="center"
                    android:insetTop="@dimen/dp_0"
                    android:insetBottom="@dimen/dp_0"
                    android:text="拍照图片"
                    android:textSize="@dimen/sp_16"
                    android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar"
                    app:backgroundTint="@color/colorPrimaryDark"
                    app:cornerRadius="@dimen/dp_12"
                    app:iconGravity="textStart"
                    app:iconSize="@dimen/dp_24" />
            </LinearLayout>
            <!--图像识别结果-->
            <LinearLayout
                android:id="@+id/lay_recognition_result"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:orientation="vertical"
                android:visibility="gone">
                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:gravity="center_vertical">
                    <View
                        android:layout_width="@dimen/dp_30"
                        android:layout_height="@dimen/dp_1"
                        android:background="@color/line_color" />
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:padding="@dimen/dp_12"
                        android:text="识别结果" />
                    <View
                        android:layout_width="@dimen/dp_30"
                        android:layout_height="@dimen/dp_1"
                        android:background="@color/line_color" />
                </LinearLayout>
                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/rv_recognition_result"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:overScrollMode="never" />
            </LinearLayout>
            <!--垃圾分类结果-->
            <LinearLayout
                android:id="@+id/lay_classification_result"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_horizontal"
                android:orientation="vertical"
                android:visibility="gone">
                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:gravity="center_vertical">
                    <View
                        android:layout_width="@dimen/dp_30"
                        android:layout_height="@dimen/dp_1"
                        android:background="@color/line_color" />
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:padding="@dimen/dp_12"
                        android:text="分类结果" />
                    <View
                        android:layout_width="@dimen/dp_30"
                        android:layout_height="@dimen/dp_1"
                        android:background="@color/line_color" />
                </LinearLayout>
                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/rv_classification_result"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:overScrollMode="never" />
            </LinearLayout>
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>
</LinearLayout>


然后回到ImageInputActivity页面,再写代码之前,先想一下这个页面要做什么?首先是获取百度的鉴权Token,然后进行图片识别,最后进行物品的垃圾分类。那么就需要三次网络请求,这里需要重写一个订阅。


三、网络订阅


这里需要增加一个网络访问地址,因为使用的是百度的API,而本身有一个天行的API地址,这里需要对两个地址进行一个控制。打开NetworkApi,在里面增加如下方法。


  /**
     * 修改访问地址
     * @param type
     */
    private static void getBaseUrl(int type) {
        switch (type) {
            case 0://天行API地址
                mBaseUrl = "http://api.tianapi.com";
                break;
            case 1://百度SDK地址
                mBaseUrl = "https://aip.baidubce.com";
                break;
            default:
                break;
        }
    }


这个方法根据传入类型的不同,使用不同的网络地址,之前写博客时疏忽了,写的嗨了,漏掉了这一部分,现在补上。然后在createService方法中增加一个type参数,之后调用getBaseUrl方法获取访问地址。


20210607110840692.png


现在你的这个createService方法改动了,那么其他调用了这个方法的地方也要做相应的改动,比如之前在做文字输入进行垃圾分类识别时,TextContract中的调用,之前是没有type的,现在你加一个0就可以了,0表示就是访问天行API。


20210607111050790.png


其他的地方记也要修改,否则会报错的。改好之后,就可以来写这个图像识别到的订阅器了,如下:


在contract包下新建一个ImageContract类,里面的代码如下:


package com.llw.goodtrash.contract;
import android.annotation.SuppressLint;
import com.llw.goodtrash.api.ApiService;
import com.llw.goodtrash.model.GetDiscernResultResponse;
import com.llw.goodtrash.model.GetTokenResponse;
import com.llw.goodtrash.model.TrashResponse;
import com.llw.mvplibrary.base.BasePresenter;
import com.llw.mvplibrary.base.BaseView;
import com.llw.mvplibrary.network.NetworkApi;
import com.llw.mvplibrary.network.observer.BaseObserver;
import static com.llw.goodtrash.utils.Constant.*;
/**
 * 图像输入页面访问网络
 *
 * @author llw
 * @date 2021/3/30 15:28
 */
public class ImageContract {
    public static class ImagePresenter extends BasePresenter<ImageView> {
        /**
         * 获取鉴权Token
         */
        @SuppressLint("CheckResult")
        public void getToken() {
            ApiService service = NetworkApi.createService(ApiService.class, 1);
            service.getToken(GRANT_TYPE, API_KEY, API_SECRET)
                    .compose(NetworkApi.applySchedulers(new BaseObserver<GetTokenResponse>() {
                        @Override
                        public void onSuccess(GetTokenResponse getTokenResponse) {
                            if (getView() != null) {
                                getView().getTokenResponse(getTokenResponse);
                            }
                        }
                        @Override
                        public void onFailure(Throwable e) {
                            if (getView() != null) {
                                getView().getTokenFailed(e);
                            }
                        }
                    }));
        }
        /**
         * 获取图像识别结果
         *
         * @param token 鉴权Token
         * @param image 图片base64
         * @param url   网络图片url
         */
        @SuppressLint("CheckResult")
        public void getDiscernResult(String token, String image, String url) {
            ApiService service = NetworkApi.createService(ApiService.class, 1);
            service.getDiscernResult(token, image, url)
                    .compose(NetworkApi.applySchedulers(new BaseObserver<GetDiscernResultResponse>() {
                        @Override
                        public void onSuccess(GetDiscernResultResponse getTokenResponse) {
                            if (getView() != null) {
                                getView().getDiscernResultResponse(getTokenResponse);
                            }
                        }
                        @Override
                        public void onFailure(Throwable e) {
                            if (getView() != null) {
                                getView().getDiscernResultFailed(e);
                            }
                        }
                    }));
        }
        /**
         * 搜索物品
         *
         * @param word 物品名
         */
        @SuppressLint("CheckResult")
        public void searchGoods(String word) {
            ApiService service = NetworkApi.createService(ApiService.class, 0);
            service.searchGoods(word).compose(NetworkApi.applySchedulers(new BaseObserver<TrashResponse>() {
                @Override
                public void onSuccess(TrashResponse groupResponse) {
                    if (getView() != null) {
                        getView().getSearchResponse(groupResponse);
                    }
                }
                @Override
                public void onFailure(Throwable e) {
                    if (getView() != null) {
                        getView().getSearchResponseFailed(e);
                    }
                }
            }));
        }
    }
    public interface ImageView extends BaseView {
        /**
         * 获取鉴权Token
         *
         * @param response GetTokenResponse
         */
        void getTokenResponse(GetTokenResponse response);
        /**
         * 获取鉴权Token异常返回
         *
         * @param throwable 异常
         */
        void getTokenFailed(Throwable throwable);
        /**
         * 获取图像识别结果
         *
         * @param response GetDiscernResultResponse
         */
        void getDiscernResultResponse(GetDiscernResultResponse response);
        /**
         * 获取图像识别结果失败
         *
         * @param throwable 异常
         */
        void getDiscernResultFailed(Throwable throwable);
        /**
         * 搜索物品返回
         *
         * @param response TrashResponse
         */
        void getSearchResponse(TrashResponse response);
        /**
         * 搜索物品异常返回
         *
         * @param throwable 异常
         */
        void getSearchResponseFailed(Throwable throwable);
    }
}


鉴权方法中的几个全局变量在Constant中定义,


  /**
     * 鉴权Token
     */
    public static final String TOKEN = "accessToken";
    /**
     * 获取Token的时间
     */
    public static final String GET_TOKEN_TIME = "getTokenTime";
    /**
     * Token有效期
     */
    public static final String TOKEN_VALID_PERIOD = "tokenValidPeriod";
    /**
     * 百度鉴权认证参数值
     */
    public static final String GRANT_TYPE = "client_credentials";
    /**
     * 百度图像识别 APP ID  GoodTrash
     */
    public static final String APP_ID = "23943795";
    /**
     * 百度图像识别 APP Key  GoodTrash
     */
    public static final String API_KEY = "PAUCX7vSAd4ZBwv897GAfhEQ";


请注意,这里的值是我在百度开放平台上注册应用时生成的,请替换为自己的。


下面回到ImageInputActivity,修改代码后如下:


package com.llw.goodtrash.ui;
import android.os.Bundle;
import com.llw.goodtrash.R;
import com.llw.goodtrash.contract.ImageContract;
import com.llw.goodtrash.model.GetDiscernResultResponse;
import com.llw.goodtrash.model.GetTokenResponse;
import com.llw.goodtrash.model.TrashResponse;
import com.llw.mvplibrary.mvp.MvpActivity;
/**
 * 图像输入物品进行垃圾分类
 *
 * @author llw
 * @date 2021/4/7 11:04
 */
public class ImageInputActivity extends MvpActivity<ImageContract.ImagePresenter> implements ImageContract.ImageView {
    @Override
    public void initData(Bundle savedInstanceState) {
    }
    @Override
    public int getLayoutId() {
        return R.layout.activity_image_input;
    }
    @Override
    protected ImageContract.ImagePresenter createPresenter() {
        return new ImageContract.ImagePresenter();
    }
    @Override
    public void getTokenResponse(GetTokenResponse response) {
    }
    @Override
    public void getTokenFailed(Throwable throwable) {
    }
    @Override
    public void getDiscernResultResponse(GetDiscernResultResponse response) {
    }
    @Override
    public void getDiscernResultFailed(Throwable throwable) {
    }
    @Override
    public void getSearchResponse(TrashResponse response) {
    }
    @Override
    public void getSearchResponseFailed(Throwable throwable) {
    }
}


这里使用了MVP,通过P来处理M和V,三个网络请求对应六个返回,下面进行页面的初始化


四、编写页面代码


先声明一些变量


  private static final String TAG = "ImageInputActivity";
  /**
     * 打开相册
     */
    private static final int OPEN_ALBUM_CODE = 100;
    /**
     * 打开相机
     */
    private static final int TAKE_PHOTO_CODE = 101;
    /**
     * 鉴权Toeken
     */
    private String accessToken;
    private Toolbar toolbar;
    private ImageView ivPicture;
    private EditText etImageUrl;
    private LinearLayout layRecognitionResult,layClassificationResult;
    private RecyclerView rvRecognitionResult,rvClassificationResult;
    private RxPermissions rxPermissions;
    private File outputImage;


然后新增一个initView的方法。


  /**
     * 初始化
     */
    private void initView() {
        toolbar = findViewById(R.id.toolbar);
        ivPicture = findViewById(R.id.iv_picture);
        etImageUrl = findViewById(R.id.et_image_url);
        findViewById(R.id.btn_web_picture).setOnClickListener(this);
        findViewById(R.id.btn_open_album).setOnClickListener(this);
        findViewById(R.id.btn_take_photo).setOnClickListener(this);
        layRecognitionResult = findViewById(R.id.lay_recognition_result);
        layClassificationResult = findViewById(R.id.lay_classification_result);
        rvRecognitionResult = findViewById(R.id.rv_recognition_result);
        rvClassificationResult = findViewById(R.id.rv_classification_result);
        //设置页面状态栏
        setStatubar(this, R.color.white, true);
        back(toolbar, true);
        rxPermissions = new RxPermissions(this);
    }


然后在initData中调用它。


  @Override
    public void initData(Bundle savedInstanceState) {
        initView();
    }


然后继承控件的点击监听回调


2021041510591051.png


重写onClick方法。


  @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_web_picture://网络图片
                break;
            case R.id.btn_open_album://相册图片
                break;
            case R.id.btn_take_photo://拍照图片
                break;
            default:
                break;
        }
    }


由于Token存在有效期的关系,因此不需要每一次都去获取,所以可以在第一次获取之后存入缓存,只要Token在有效期内都可以直接从缓存中获取,这样就可以少请求一次网络。


下面先写一个缓存的工具类。


在utils包下新增一个SPUtils类,里面的代码如下:


package com.llw.goodtrash.utils;
import android.content.Context;
import android.content.SharedPreferences;
/**
 * SharedPreferences工具类
 *
 * @author llw
 */
public class SPUtils {
    private static final String NAME = "config";
    public static void putBoolean(String key, boolean value, Context context) {
        SharedPreferences sp = context.getSharedPreferences(NAME,
                Context.MODE_PRIVATE);
        sp.edit().putBoolean(key, value).commit();
    }
    public static boolean getBoolean(String key, boolean defValue, Context context) {
        SharedPreferences sp = context.getSharedPreferences(NAME,
                Context.MODE_PRIVATE);
        return sp.getBoolean(key, defValue);
    }
    public static void putString(String key, String value, Context context) {
        SharedPreferences sp = context.getSharedPreferences(NAME,
                Context.MODE_PRIVATE);
        sp.edit().putString(key, value).commit();
    }
    public static String getString(String key, String defValue, Context context) {
        if (context != null) {
            SharedPreferences sp = context.getSharedPreferences(NAME,
                    Context.MODE_PRIVATE);
            return sp.getString(key, defValue);
        }
        return "";
    }
    public static void putInt(String key, int value, Context context) {
        SharedPreferences sp = context.getSharedPreferences(NAME,
                Context.MODE_PRIVATE);
        sp.edit().putInt(key, value).commit();
    }
    public static int getInt(String key, int defValue, Context context) {
        SharedPreferences sp = context.getSharedPreferences(NAME,
                Context.MODE_PRIVATE);
        return sp.getInt(key, defValue);
    }
    public static void putLong(String key, long value, Context context) {
        SharedPreferences sp = context.getSharedPreferences(NAME,
                Context.MODE_PRIVATE);
        sp.edit().putLong(key, value).commit();
    }
    public static long getLong(String key, long defValue, Context context) {
        SharedPreferences sp = context.getSharedPreferences(NAME,
                Context.MODE_PRIVATE);
        return sp.getLong(key, defValue);
    }
    public static void remove(String key, Context context) {
        SharedPreferences sp = context.getSharedPreferences(NAME,
                Context.MODE_PRIVATE);
        sp.edit().remove(key).commit();
    }
}


然后在ImageInputActivity中写一个获取Token的方法,代码如下:


  /**
     * 获取鉴权Token
     */
    private String getAccessToken() {
        String token = SPUtils.getString(Constant.TOKEN, null, this);
        if (token == null) {
            //访问API获取接口
            mPresenter.getToken();
        } else {
            //则判断Token是否过期
            if (isTokenExpired()) {
                //过期
                mPresenter.getToken();
            } else {
                accessToken = token;
            }
        }
        return accessToken;
    }


这里你的isTokenExpired()方法会报红,这是一个用来判断Token是否过期的方法。里面的代码如下:


  /**
     * Token是否过期
     *
     * @return
     */
    private boolean isTokenExpired() {
        //获取Token的时间
        long getTokenTime = SPUtils.getLong(Constant.GET_TOKEN_TIME, 0, this);
        //获取Token的有效时间
        long effectiveTime = SPUtils.getLong(Constant.TOKEN_VALID_PERIOD, 0, this);
        //获取当前系统时间
        long currentTime = System.currentTimeMillis() / 1000;
        return (currentTime - getTokenTime) >= effectiveTime;
    }


刚才在获取Token方法中,通过mPresenter.getToken();发起了一个网络请求,返回的结果有成功和失败,成功后会有Token返回,失败了会有失败原因返回。


修改如下方法:


  /**
     * 获取鉴权Token成功返回
     * @param response GetTokenResponse
     */
    @Override
    public void getTokenResponse(GetTokenResponse response) {
        if (response != null) {
            //鉴权Token
            accessToken = response.getAccess_token();
            //过期时间 秒
            long expiresIn = response.getExpires_in();
            //当前时间 秒
            long currentTimeMillis = System.currentTimeMillis() / 1000;
            //放入缓存
            SPUtils.putString(Constant.TOKEN, accessToken, this);
            SPUtils.putLong(Constant.GET_TOKEN_TIME, currentTimeMillis, this);
            SPUtils.putLong(Constant.TOKEN_VALID_PERIOD, expiresIn, this);
        } else {
            showMsg("Token为null");
        }
    }
    /**
     * 获取Token失败返回
     * @param throwable 异常
     */
    @Override
    public void getTokenFailed(Throwable throwable) {
        Log.d(TAG, "Token获取失败:"+throwable.toString());
    }


我已经写了注释了,那么你就知道这个方法是做什么的了。


五、识别网络图片


我的想法是当我点击这个网络图片的按钮时,页面出现一个输入框,当我输入完成之后,点击键盘的回车直接识别,虽后隐藏这个输入框,嗯,就是这样。


首先来写点击网络图片时的业务逻辑代码。


      case R.id.btn_web_picture://网络图片
                etImageUrl.setVisibility(View.VISIBLE);
                etImageUrl.setOnKeyListener((view, keyCode, keyEvent) -> {
                    if (keyCode == KeyEvent.KEYCODE_ENTER && keyEvent.getAction() == KeyEvent.ACTION_UP) {
                        String webImageUrl = etImageUrl.getText().toString().trim();
                        String defaultWebImageUrl = "https://bce-baiyu.cdn.bcebos.com/14ce36d3d539b6004ef2e45fe050352ac65cb71e.jpeg";
                        String imageUrl = "".equals(webImageUrl) ?defaultWebImageUrl : webImageUrl;
                        //识别网络图片Url
                        showLoadingDialog();
                        Glide.with(context).load(imageUrl).into(ivPicture);
                        mPresenter.getDiscernResult(accessToken,null,imageUrl);
                        etImageUrl.setVisibility(View.GONE);
                    }
                    return false;
                });
                break;


在这里发起了一个图片识别的请求,下面来看返回的方法处理。


  /**
     * 图片识别成功返回
     * @param response GetDiscernResultResponse
     */
    @Override
    public void getDiscernResultResponse(GetDiscernResultResponse response) {
        if(response == null){
            hideLoadingDialog();
            showMsg("未获得相应的识别结果");
            return;
        }
        ivPicture.setVisibility(View.VISIBLE);
        List<GetDiscernResultResponse.ResultBean> result = response.getResult();
        if (result != null && result.size() > 0) {
            //显示识别结果
            showDiscernResult(result);
        } else {
            hideLoadingDialog();
            showMsg("未获得相应的识别结果");
        }
    }
    /**
     * 图片识别成功失败
     * @param throwable 异常
     */
    @Override
    public void getDiscernResultFailed(Throwable throwable) {
        Log.d(TAG, "图片识别失败:"+throwable.toString());
    }


返回成功之后,如果数据不为空则显示要识别的图片,然后通过列表展示识别结果数据,

首先得有一个识别的结果列表item布局,item_distinguish_result_rv.xml,代码如下:


<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/item_distinguish_rv"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="1dp"
    android:background="#FFF"
    android:foreground="?attr/selectableItemBackground"
    android:padding="16dp">
    <TextView
        android:id="@+id/tv_keyword"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#000"
        android:textSize="16sp" />
    <TextView
        android:id="@+id/tv_root"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tv_keyword"
        android:layout_marginTop="@dimen/dp_4"
        android:textSize="14sp" />
    <TextView
        android:id="@+id/tv_score"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true" />
</RelativeLayout>


下面写适配器代码,在adapter下新建一个DiscernResultAdapter类,代码如下:


package com.llw.goodtrash.adapter;
import androidx.annotation.Nullable;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.BaseViewHolder;
import com.llw.goodtrash.R;
import com.llw.goodtrash.model.GetDiscernResultResponse;
import java.util.List;
/**
 * 识别结果列表适配器
 * @author llw
 */
public class DiscernResultAdapter extends BaseQuickAdapter<GetDiscernResultResponse.ResultBean, BaseViewHolder> {
    public DiscernResultAdapter(int layoutResId, @Nullable List<GetDiscernResultResponse.ResultBean> data) {
        super(layoutResId, data);
    }
    @Override
    protected void convert(BaseViewHolder helper, GetDiscernResultResponse.ResultBean item) {
        helper.setText(R.id.tv_keyword,item.getKeyword())
                .setText(R.id.tv_root,item.getRoot())
                .setText(R.id.tv_score,String.valueOf(item.getScore()))
                .addOnClickListener(R.id.item_distinguish_rv);
    }
}


适配器和列表item都写好了,下面回到ImageInputActivity中新增方法showDiscernResult(),代码如下:


  /**
     * 显示识别的结果列表
     *
     * @param result
     */
    private void showDiscernResult(List<GetDiscernResultResponse.ResultBean> result) {
        DiscernResultAdapter adapter = new DiscernResultAdapter(R.layout.item_distinguish_result_rv, result);
        rvRecognitionResult.setLayoutManager(new LinearLayoutManager(this));
        rvRecognitionResult.setAdapter(adapter);
        //隐藏加载
        hideLoadingDialog();
        //显示弹窗
        layRecognitionResult.setVisibility(View.VISIBLE);
        layClassificationResult.setVisibility(View.GONE);
    }


下面来运行一下:


20210415153440432.gif


不一定第一次就能识别出来,看你的人品。下面识别相册图片


六、识别相册图片


下面写点击这个相册图片按钮的业务逻辑,如下:


      case R.id.btn_open_album://相册图片
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    rxPermissions.request(
                            Manifest.permission.READ_EXTERNAL_STORAGE,
                            Manifest.permission.WRITE_EXTERNAL_STORAGE)
                            .subscribe(grant -> {
                                if (grant) {
                                    //获得权限
                                    openAlbum();
                                } else {
                                    showMsg("未获取到权限");
                                }
                            });
                } else {
                    openAlbum();
                }
                break;


打开相册的openAlbum()方法,代码如下:


  /**
     * 打开相册
     */
    private void openAlbum() {
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_PICK);
        intent.setType("image/*");
        startActivityForResult(intent, OPEN_ALBUM_CODE);
    }


下面就是从相册返回的处理


  @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            showLoadingDialog();
            if (requestCode == OPEN_ALBUM_CODE) {
                //打开相册返回
                String[] filePathColumns = {MediaStore.Images.Media.DATA};
                final Uri imageUri = Objects.requireNonNull(data).getData();
                Cursor cursor = getContentResolver().query(imageUri, filePathColumns, null, null, null);
                cursor.moveToFirst();
                int columnIndex = cursor.getColumnIndex(filePathColumns[0]);
                //获取图片路径
                String imagePath = cursor.getString(columnIndex);
                cursor.close();
                //识别
                localImageDiscern(imagePath);
            }
        } else {
            showMsg("什么都没有");
        }
    }


通过相册图片获取图片的路径,通过localImageDiscern方法将这个路径去转字节,再转base64。代码如下:


  /**
     * 本地图片识别
     */
    private void localImageDiscern(String imagePath) {
        try {
            String token = getAccessToken();
            //通过图片路径显示图片
            Glide.with(this).load(imagePath).into(ivPicture);
            //按字节读取文件
            byte[] imgData = FileUtil.readFileByBytes(imagePath);
            //字节转Base64
            String imageBase64 = Base64Util.encode(imgData);
            //本地图片识别
            mPresenter.getDiscernResult(token, imageBase64, null);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


这里面还有两个工具类FileUtil和Base64Util。下面在utils包下新建一个FileUtil类,里面的代码如下:


package com.llw.goodtrash.utils;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/**
 * 文件读取工具类
 */
public class FileUtil {
    /**
     * 读取文件内容,作为字符串返回
     */
    public static String readFileAsString(String filePath) throws IOException {
        File file = new File(filePath);
        if (!file.exists()) {
            throw new FileNotFoundException(filePath);
        } 
        if (file.length() > 1024 * 1024 * 1024) {
            throw new IOException("File is too large");
        } 
        StringBuilder sb = new StringBuilder((int) (file.length()));
        // 创建字节输入流  
        FileInputStream fis = new FileInputStream(filePath);  
        // 创建一个长度为10240的Buffer
        byte[] bbuf = new byte[10240];  
        // 用于保存实际读取的字节数  
        int hasRead = 0;  
        while ( (hasRead = fis.read(bbuf)) > 0 ) {  
            sb.append(new String(bbuf, 0, hasRead));  
        }  
        fis.close();  
        return sb.toString();
    }
    /**
     * 根据文件路径读取byte[] 数组
     */
    public static byte[] readFileByBytes(String filePath) throws IOException {
        File file = new File(filePath);
        if (!file.exists()) {
            throw new FileNotFoundException(filePath);
        } else {
            ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length());
            BufferedInputStream in = null;
            try {
                in = new BufferedInputStream(new FileInputStream(file));
                short bufSize = 1024;
                byte[] buffer = new byte[bufSize];
                int len1;
                while (-1 != (len1 = in.read(buffer, 0, bufSize))) {
                    bos.write(buffer, 0, len1);
                }
                byte[] var7 = bos.toByteArray();
                return var7;
            } finally {
                try {
                    if (in != null) {
                        in.close();
                    }
                } catch (IOException var14) {
                    var14.printStackTrace();
                }
                bos.close();
            }
        }
    }
}


然后是Base64Util类,代码如下:


package com.llw.goodtrash.utils;
/**
 * Base64 工具类
 */
public class Base64Util {
    private static final char last2byte = (char) Integer.parseInt("00000011", 2);
    private static final char last4byte = (char) Integer.parseInt("00001111", 2);
    private static final char last6byte = (char) Integer.parseInt("00111111", 2);
    private static final char lead6byte = (char) Integer.parseInt("11111100", 2);
    private static final char lead4byte = (char) Integer.parseInt("11110000", 2);
    private static final char lead2byte = (char) Integer.parseInt("11000000", 2);
    private static final char[] encodeTable = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'};
    public Base64Util() {
    }
    public static String encode(byte[] from) {
        StringBuilder to = new StringBuilder((int) ((double) from.length * 1.34D) + 3);
        int num = 0;
        char currentByte = 0;
        int i;
        for (i = 0; i < from.length; ++i) {
            for (num %= 8; num < 8; num += 6) {
                switch (num) {
                    case 0:
                        currentByte = (char) (from[i] & lead6byte);
                        currentByte = (char) (currentByte >>> 2);
                    case 1:
                    case 3:
                    case 5:
                    default:
                        break;
                    case 2:
                        currentByte = (char) (from[i] & last6byte);
                        break;
                    case 4:
                        currentByte = (char) (from[i] & last4byte);
                        currentByte = (char) (currentByte << 2);
                        if (i + 1 < from.length) {
                            currentByte = (char) (currentByte | (from[i + 1] & lead2byte) >>> 6);
                        }
                        break;
                    case 6:
                        currentByte = (char) (from[i] & last2byte);
                        currentByte = (char) (currentByte << 4);
                        if (i + 1 < from.length) {
                            currentByte = (char) (currentByte | (from[i + 1] & lead4byte) >>> 4);
                        }
                }
                to.append(encodeTable[currentByte]);
            }
        }
        if (to.length() % 4 != 0) {
            for (i = 4 - to.length() % 4; i > 0; --i) {
                to.append("=");
            }
        }
        return to.toString();
    }
}


下面可以直接运行了。


20210415155545866.gif


下面该识别拍照图片


七、识别拍照图片


点击拍照图片按钮的业务逻辑代码,


      case R.id.btn_take_photo://拍照图片
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    rxPermissions.request(
                            Manifest.permission.CAMERA)
                            .subscribe(grant -> {
                                if (grant) {
                                    //获得权限
                                    turnOnCamera();
                                } else {
                                    showMsg("未获取到权限");
                                }
                            });
                } else {
                    turnOnCamera();
                }
                break;


turnOnCamera是用来打开相机的方法,里面的代码如下:


  /**
     * 打开相机
     */
    private void turnOnCamera() {
        SimpleDateFormat timeStampFormat = new SimpleDateFormat("HH_mm_ss");
        String filename = timeStampFormat.format(new Date());
        //创建File对象
        outputImage = new File(getExternalCacheDir(), "takePhoto" + filename + ".jpg");
        Uri imageUri;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            imageUri = FileProvider.getUriForFile(this,
                    "com.llw.goodtrash.fileprovider", outputImage);
        } else {
            imageUri = Uri.fromFile(outputImage);
        }
        //打开相机
        Intent intent = new Intent();
        intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
        startActivityForResult(intent, TAKE_PHOTO_CODE);
    }


然后同样要在onActivityResult方法中添加相机返回的分支判断。


  else if (requestCode == TAKE_PHOTO_CODE) {
                //拍照返回
                String imagePath = outputImage.getAbsolutePath();
                //识别
                localImageDiscern(imagePath);
            }


添加位置如下:


20210415160603521.png


下面就可以直接运行了。


20210415161041244.gif


OK,现在就来进行垃圾分类了。


八、垃圾分类


刚才通过图像识别已经拿到物品结果了,下面通过点击这个物品去进行垃圾分类。添加adapter的适配器,在showDiscernResult方法中添加如下代码


    //添加列表Item点击
        adapter.setOnItemChildClickListener((adapter1, view, position) -> {
            showLoadingDialog();
            //垃圾分类
            mPresenter.searchGoods(result.get(position).getKeyword());
        });


添加位置如下:


20210415162525488.png


下面进行搜索物品的返回数据处理。


  /**
     * 搜索物品进行垃圾分类成功返回
     *
     * @param response TrashResponse
     */
    @Override
    public void getSearchResponse(TrashResponse response) {
        //请求成功  进行数据的渲染
        if (response.getCode() == Constant.SUCCESS_CODE) {
            List<TrashResponse.NewslistBean> result = response.getNewslist();
            if (result != null && result.size() > 0) {
                //显示分类结果
                showClassificationResult(result);
            } else {
                showMsg("触及到了知识盲区");
            }
        } else {
            hideLoadingDialog();
            showMsg(response.getMsg());
        }
    }
    @Override
    public void getSearchResponseFailed(Throwable throwable) {
        Log.d(TAG, "搜索物品进行垃圾分类失败:" + throwable.toString());
    }


通过showClassificationResult方法进行物品垃圾分类结果显示,代码如下


  /**
     * 显示物品分类结果
     * @param result
     */
    private void showClassificationResult(List<TrashResponse.NewslistBean> result) {
        SearchGoodsAdapter adapter = new SearchGoodsAdapter(R.layout.item_search_rv,result);
        rvClassificationResult.setLayoutManager(new LinearLayoutManager(context));
        rvClassificationResult.setAdapter(adapter);
        //隐藏加载
        hideLoadingDialog();
        //显示弹窗
        layClassificationResult.setVisibility(View.VISIBLE);
    }


下面运行一下:


20210415164509861.gif


下面再优化一下,就是让数据显示之后,滑动到屏幕底部,

修改activity_image_input.xml

给NestedScrollView控件添加一个id。


android:id="@+id/nestedScrollView"


然后进入ImageInputActivity中,初始化。


20210415170712261.png


写一个方法


  /**
     * 滑动到屏幕底部
     */
    private void scrollToEnd() {
        nestedScrollView.post(() -> {
            nestedScrollView.fullScroll(View.FOCUS_DOWN);//滚到底部
            //nestedScrollView.fullScroll(ScrollView.FOCUS_UP);//滚到顶部
        });
    }


在显示列表数据时后调用,有两处。


第一处,图像识别的结果列表显示之后


2021041517100579.png


第二处,显示物品垃圾分类结果显示之后


20210415171056346.png


运行一下:


20210415171327167.gif


那么这个页面的功能就写完了。

相关文章
|
1月前
|
XML Java 数据库
安卓项目:app注册/登录界面设计
本文介绍了如何设计一个Android应用的注册/登录界面,包括布局文件的创建、登录和注册逻辑的实现,以及运行效果的展示。
140 0
安卓项目:app注册/登录界面设计
|
2月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
123 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
|
2月前
|
存储 开发工具 Android开发
使用.NET MAUI开发第一个安卓APP
【9月更文挑战第24天】使用.NET MAUI开发首个安卓APP需完成以下步骤:首先,安装Visual Studio 2022并勾选“.NET Multi-platform App UI development”工作负载;接着,安装Android SDK。然后,创建新项目时选择“.NET Multi-platform App (MAUI)”模板,并仅针对Android平台进行配置。了解项目结构,包括`.csproj`配置文件、`Properties`配置文件夹、平台特定代码及共享代码等。
162 2
|
2月前
|
XML Android开发 数据格式
🌐Android国际化与本地化全攻略!让你的App走遍全球无障碍!🌍
在全球化背景下,实现Android应用的国际化与本地化至关重要。本文以一款旅游指南App为例,详细介绍如何通过资源文件拆分与命名、适配布局与方向、处理日期时间及货币格式、考虑文化习俗等步骤,完成多语言支持和本地化调整。通过邀请用户测试并收集反馈,确保应用能无缝融入不同市场,提升用户体验与满意度。
103 3
|
2月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android应用开发中的多线程编程,涵盖基本概念、常见实现方式及最佳实践。主要内容包括主线程与工作线程的作用、多线程的多种实现方法(如 `Thread`、`HandlerThread`、`Executors` 和 Kotlin 协程),以及如何避免内存泄漏和合理使用线程池。通过有效的多线程管理,可以显著提升应用性能和用户体验。
69 10
|
1月前
|
安全 网络安全 Android开发
深度解析:利用Universal Links与Android App Links实现无缝网页至应用跳转的安全考量
【10月更文挑战第2天】在移动互联网时代,用户经常需要从网页无缝跳转到移动应用中。这种跳转不仅需要提供流畅的用户体验,还要确保安全性。本文将深入探讨如何利用Universal Links(仅限于iOS)和Android App Links技术实现这一目标,并分析其安全性。
224 0
|
2月前
|
XML 数据库 Android开发
10分钟手把手教你用Android手撸一个简易的个人记账App
该文章提供了使用Android Studio从零开始创建一个简单的个人记账应用的详细步骤,包括项目搭建、界面设计、数据库处理及各功能模块的实现方法。
|
3月前
|
API Android开发
Android P 性能优化:创建APP进程白名单,杀死白名单之外的进程
本文介绍了在Android P系统中通过创建应用进程白名单并杀死白名单之外的进程来优化性能的方法,包括设置权限、获取运行中的APP列表、配置白名单以及在应用启动时杀死非白名单进程的代码实现。
62 1
|
3月前
|
Android开发 iOS开发 C#
Xamarin:用C#打造跨平台移动应用的终极利器——从零开始构建你的第一个iOS与Android通用App,体验前所未有的高效与便捷开发之旅
【8月更文挑战第31天】Xamarin 是一个强大的框架,允许开发者使用单一的 C# 代码库构建高性能的原生移动应用,支持 iOS、Android 和 Windows 平台。作为微软的一部分,Xamarin 充分利用了 .NET 框架的强大功能,提供了丰富的 API 和工具集,简化了跨平台移动应用开发。本文通过一个简单的示例应用介绍了如何使用 Xamarin.Forms 快速创建跨平台应用,包括设置开发环境、定义用户界面和实现按钮点击事件处理逻辑。这个示例展示了 Xamarin.Forms 的基本功能,帮助开发者提高开发效率并实现一致的用户体验。
151 0
|
3月前
|
存储 XML Linux
深入理解操作系统:进程管理与调度策略探索安卓应用开发:从零开始构建你的第一个App
【8月更文挑战第28天】在数字世界里航行,操作系统是掌控一切的舵手。本文将带你领略操作系统的精妙设计,特别是进程管理和调度策略这两大核心领域。我们将从基础概念出发,逐步深入到复杂的实现机制,最后通过实际代码示例,揭示操作系统如何高效协调资源,确保多任务顺畅运行的秘密。准备好了吗?让我们启航,探索那些隐藏在日常电脑使用背后的奥秘。 【8月更文挑战第28天】在这个数字时代,拥有一款自己的移动应用程序不仅是技术的展示,也是实现创意和解决问题的一种方式。本文将引导初学者了解安卓开发的基础知识,通过一个简单的待办事项列表App项目,逐步介绍如何利用安卓开发工具和语言来创建、测试并发布一个基本的安卓应用