react native结合Android原生实现调用相机或图库选择图片设置头像

这种功能网上已经有了许多开源组件,虽然是现成的,不过也是需要学习如何使用的,集成到自己项目的过程中可能也会遇到一些问题,而且开源的功能也不一定能够完全的符合自己的需求,所以试着自己实现一下,欢迎大家分享自己的观点和意见。

实现思路很简单,就是原生部分调用相机将拍照的图片保存到sd卡,js部分负责显示。将图片保存的sd卡是为了下次启动应用的时候头像依然有效,也可以上传到服务器。编写代码使用webStorm 和 androidStudio。

下面开始实现:
首先执行命令 react-native init HeadImage 创建一个名为HeadImage的工程,创建好工程后执行 react-native run-android 看看是否能编译运行

然后找一张图片 head_default.png(命名随意)作为默认头像,放到工程的 HeadImage\android\app\src\main\res\drawable 目录下,rn默认工程没有drawable可以自己新建一个,然后webstorm选择工程根目录打开,修改index.android.js代码

export default class HeadImage extends Component {
    render() {
        return (
            style={styles.container}>
                onPress={this._clickImage}>
                    source={{uri: 'head_default'}} style={{width:50,height:50}}/>
                
            
        );
    }

    _clickImage(){
        console.log("click image...");
    }
}

修改了render方法,t添加_clickImage方法,TouchableOpacity和Image组件别忘了import

注意Image的uri要与自己放在HeadImage\android\app\src\main\res\drawable目录下的默认头像图片命名一致,之后编译运行(必须编译运行,因为加了资源图片,只是reload是不显示图片的),运行结果将显示默认头像,点击打出log


接下来实现与原生的交互,参考 http://reactnative.cn/docs/0.31/native-modules-android.html#content

androidstudio打开工程的android目录


新建两个类,HeadImageModule.java和HeadImagePackage.java,分别继承ReactContextBaseJavaModule和ReactPackage,之后在MainApplication.java里面注册,各部分代码如下:


HeadImageModule.java

public class HeadImageModule extends ReactContextBaseJavaModule {

    public HeadImageModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "HeadImageModule"; //注意这里的返回值
    }

    @ReactMethod
    public void callCamera() { // 调用相机的方法
        Log.d("","call camera...");
    }
}

HeadImagePackage.java

public class HeadImagePackage implements ReactPackage {
    @Override
    public List createNativeModules(ReactApplicationContext reactContext) {
        List modules = new ArrayList<>();
        modules.add(new HeadImageModule(reactContext));
        return modules;
    }

    @Override
    public Listextends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }

    @Override
    public List createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

MainApplication.java

@Override
protected List getPackages() {
    return Arrays.asList(
            new MainReactPackage(),
            new HeadImagePackage()  //注册模块
    );
}

java代码写好后,编写js代码调用HeadImageModule.java里面的callCamera方法,在index.android.js的_clickImage方法添加一行代码

_clickImage(){
    console.log("click image...");
    NativeModules.HeadImageModule.callCamera()
}
用到了 NativeModules 别忘了import { NativeModules } from 'react-native';

现在可以编译运行一下了(修改了java代码必须重新编译,reload无效),然后点击头像,应该可以打印log:call camera...

到这里已经实现了js与原生的交互


接下来就可以在HeadImageModule.java中实现调用相机的具体代码了,先定义几个常量

// 保存图片的sd卡路径
private static final String HEAD_IMAGE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/HeadImage/";
// 保存图片的名称
private static final String HEAD_IMAGE_NAME = "head_image.png";

// startActivityForResult  requestCode
private static final int REQUEST_CODE_CAMERA = 0;
private static final int REQUEST_CODE_GALLERY = 1;
private static final int REQUEST_CODE_CROP = 2;

接下来实现callCamera方法,注意要让js可以调用必须加@ReactMethod,Promise是与js交互有关的,参考:http://reactnative.cn/docs/0.31/native-modules-android.html#content

@ReactMethod
public void callCamera(Promise promise) {
    recursionDeleteFile(); // 删除目录下除了头像图片的其他临时图片
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//启动相机的intent
    if (isPathExists()) { // 判断常量定义的路径是否存在,不存在就创建,然后返回true
        mFullPath = HEAD_IMAGE_PATH + System.currentTimeMillis() + ".png"; // 临时图片
        mUri = Uri.fromFile(new File(mFullPath));
        intent.putExtra(MediaStore.EXTRA_OUTPUT, mUri);
        Activity activity = getCurrentActivity();
        if (activity != null) {
            mPromise = promise;
            activity.startActivityForResult(intent, REQUEST_CODE_CAMERA);
        }
    }
}
执行完这个方法就可以启动相机了,方法中使用临时图片是因为拍照->裁剪->完成这个过程用户可能在某一步取消操作,避免原来的头像被替换,但是这样每次调用相机都会创建一个临时图片,为了不使sd卡存头像图片的文件夹越来越大,所以编写了recursionDeleteFile()方法每次做一次递归删除,删除临时图片,代码就不贴了,后面会给出源码地址。

拍照点击完成之后,就该去onActivityResult里面处理了,rn提供了一个接口实现监听onActivityResult,还是看http://reactnative.cn/docs/0.31/native-modules-android.html#content  (或者直接在MainActivity里面重写onActivityResult方法应该也是可以的,有兴趣的可以尝试一下

HeadImageModule.java构造方法里面添加如下代码

 
  
reactContext.addActivityEventListener(new BaseActivityEventListener() {
    @Override
    public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CODE_CAMERA) { // 调用相机回调
            if (resultCode == Activity.RESULT_OK) { // *************1.拍照完成,将进入裁剪界面
                activity.startActivityForResult(cropImage(mUri), REQUEST_CODE_CROP);// 启动裁剪界面
            } else if (resultCode == Activity.RESULT_CANCELED) { // 拍照界面点击取消
                mPromise.resolve(null);
                // mFullPath就是callCamera里面定义的临时图片路径
                // 如果没有取消拍照,那么就不执行这里,临时图片的删除将在下次调用相机的时候,所以与recursionDeleteFile()不重复
                new File(mFullPath).delete();
            }
        } else if (requestCode == REQUEST_CODE_CROP) { // ************2.裁剪完成
            if (resultCode == Activity.RESULT_OK) {
                // uri存的是临时图片路径,返回给js代码,这里有个问题,稍后再说
                mPromise.resolve(mUri.toString());
                // 将临时图片复制一份,保存为最终的头像图片
                saveHeadImage();
            } else if (resultCode == Activity.RESULT_CANCELED) {
                mPromise.resolve(null);
                new File(mFullPath).delete();
            }
        }
    }
});
拍照完成将调用系统的裁剪功能
activity.startActivityForResult(cropImage(mUri), REQUEST_CODE_CROP);

cropImage(mUri)方法的实现

private Intent cropImage(Uri uri) {
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.putExtra("crop", "true");
    intent.putExtra("aspectX", 1);
    intent.putExtra("aspectY", 1);
    intent.putExtra("outputX", 800);
    intent.putExtra("outputY", 800);
    intent.putExtra("return-data", false);
    intent.putExtra("scale", true);
    intent.putExtra("scaleUpIfNeeded", true);
    intent.putExtra(MediaStore.EXTRA_OUTPUT,
            Uri.fromFile(new File(mFullPath)));
    intent.putExtra("outputFormat", "png");
    return intent;
}
裁剪完成基本就结束了,上面说到有个问题,就是下面代码裁剪完成的时候返回给js的图片是临时图片,而不是saveHeadImage()保存最终图片之后返回最终的图片head_image.png

           if (resultCode == Activity.RESULT_OK) {
                // uri存的是临时图片路径,返回给js代码,这里有个问题,稍后再说
                mPromise.resolve(mUri.toString());
                // 将临时图片复制一份,保存为最终的头像图片
                saveHeadImage();
            } 
看js代码
source={{uri: 'head_default'}} style={{width:50,height:50}}/>

如果uri设置的是最终头像head_image.png,那么显示到界面之后,替换sd卡上的最终头像图片,命名不变,这时候刷新界面,图片还是显示替换之前的头像,按返回键退出,再启动应用,也是显示之前的头像,除非杀死进程再启动,猜测这个应该跟android的内存机制有关,这就是Java部分就返回临时图片的原因。


到这里,头像图片已经成功的保存到sd卡上了,接下来就是js显示的实现了,先理一下,js需要处理的图片包括三个:默认头像,sd卡存的临时头像,sd卡存的最终头像。很容易想到,先判断sd卡的最终头像是否存在,不存在就用默认头像,也就是最终头像优先级高于默认头像,至于临时头像,通过上面的介绍知道只有拍照并且完成裁剪之后才会有临时头像传给js,而这时候临时的和最终的一样,其他时候在js里面都是空,所以可以把临时的优先级看成最高,结果就是先判断临时图片的存在,再判断最终图片的存在,都不存在的话使用默认图片。

逻辑理顺了就开始写代码,可以新建一个自己的组件,webstorm中在项目的根目录下新建MyImage.js(命名和路径随意),代码如下:

import React, {Component, PropTypes} from 'react';
import {
    View,
    StyleSheet,
    Image,
    NativeModules,
} from 'react-native';

export default class MyImage extends Component {

    constructor(props) {
        super(props);
        this.state = {
            uri: null,
        };
    }

    static defaultProps = {
        uri: null,
    };

    static propTypes = {
        uri: PropTypes.string,
        imageStyle: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
    }

    async componentWillReceiveProps() {
        let isExists = await NativeModules.HeadImageModule.isImageExists();
        if (this.props.uri !== null) {
            this.setState({
                uri: this.props.uri
            });
        } else if (isExists) {
            this.setState({
                uri: await NativeModules.HeadImageModule.getImageUri()
            });
        } else {
            this.setState({
                uri: 'head_default'
            });
        }
    }

    render() {
        return (
            source={{uri: this.state.uri}} style={this.props.imageStyle}/>
        );
    }

}
需要的文件操作可以使用java然后js调用(仿照camera),自己的组件写好就可以去用了,修改index.android.js代码:

首先import自己的组件

import MyImage from './MyImage'
export default class HeadImage extends Component {

    constructor(props) {
        super(props);
        this.state = {
            headImageUri: null,
        };
    }

    render() {
        return (
            style={styles.container}>
                onPress={this._clickImage.bind(this)}>
                    uri={this.state.headImageUri} imageStyle={{width: 100,height: 100}}/>
                
            
        );
    }

    async _clickImage() {
        this.setState({
            headImageUri: await NativeModules.HeadImageModule.callCamera() // 相机拍照
            // headImageUri: await NativeModules.HeadImageModule.callGallery() // 相册选择图片
        });
    }

    componentDidMount() {
        this.setState({
            code: this.props.code
        });
    }
}
相册部分和相机相似,到这里就差不多结束了,代码还有一些地方可以完善,这里就不继续了,简单列出一下:

  js部分可以把负责点击事件处理的TouchableOpacity 写到自定义组件MyImage里面,当然相应的处理逻辑也在MyImage里面,这样可以让自定义组件使用起来更方便

  可以给MyImage增加一个设置头像存储路径的属性,然后传给Java部分,java就不用使用常量将头像的存储路径写死了,可以更加灵活

  可以在头像的存储路径下创建一个.nomedia文件,避免头像图片被系统图库扫描到


源码地址:

http://download.csdn.net/download/simple_simple_simple/9749571

https://github.com/developerzjy/react_native_android_headImage






你可能感兴趣的:(react native结合Android原生实现调用相机或图库选择图片设置头像)