ReactNative 开发笔记(持续更新...)

2018-12-19 17:44:44 +08:00
 kovli

本文均为 RN 开发过程中遇到的问题、坑点的分析及解决方案,各问题点之间无关联,希望能帮助读者少走弯路,持续更新中... ( 2018 年 12 月 19 日更新)

原文链接: http://www.kovli.com/2018/06/25/rn-anything/

作者:Kovli

- ReactNative 输入框 TextInput 点击弹起键盘,如果键盘遮挡了重要位置,如何让界面自动跟随键盘调整?

使用这个组件KeyboardAvoidingView

本组件用于解决一个常见的尴尬问题:手机上弹出的键盘常常会挡住当前的视图。本组件可以自动根据键盘的位置,调整自身的 position 或底部的 padding,以避免被遮挡。

解决方案:参考如下例子

      <ScrollView style={styles.container}>
        <KeyboardAvoidingView behavior="position" keyboardVerticalOffset={64}>
          ...
          <TextInput />
          ...
        </KeyboardAvoidingView>
      </ScrollView>


- ReactNative 输入框 TextInput 点击弹起键盘,然后点击其他子组件,例如点击提交按钮,会先把键盘收起,再次点击提交按钮才响应提交按钮,得点击两次,如何做到点击提交按钮的同时收起键盘并响应按钮?

这个问题关键在ScrollViewkeyboardShouldPersistTaps属性 ,首先TextInput的特殊性(有键盘弹起)决定了其最好包裹在 ScrollView 里,其次如果当前界面有软键盘,那么点击scrollview后是否收起键盘,取决于keyboardShouldPersistTaps属性的设置。(译注:很多人反应TextInput无法自动失去焦点 /需要点击多次切换到其他组件等等问题,其关键都是需要将TextInput放到ScrollView中再设置本属性)

解决方案:看如下例子

      <ScrollView style={styles.container}
                  keyboardShouldPersistTaps="handled">
        <TextInput />
       ...
      </ScrollView>

    //按钮点击事件注意收起键盘
      _checkAndSubmit = () => {
        Keyboard.dismiss();
      };

- ReactNative 本地图片如何获取其 base64 编码?(一般指采用<Image source={require('./icon.png'.../>这类相对路径地址的图片资源如何获取到绝对路径)

关键是要获取到本地图片的 uri,用到了Image.resolveAssetSource方法,ImageEditor.cropImage方法和ImageStore.getBase64ForTag方法,具体可以查询官方文档

解决方案:看如下代码

      import item from '../../images/avator_upload_icon.png';

      const info = Image.resolveAssetSource(item);

      ImageEditor.cropImage(info.uri, {
        size: {
          width: 126,
          height: 126
        },
        resizeMode: 'cover'
      }, uri => {
        ImageStore.getBase64ForTag(uri, base64ImageData => {
          // 获取图片字节码的 base64 字符串
          this.setState({
            avatarBase64: base64ImageData
          });
        }, err => {
          console.warn("ImageStoreError" + JSON.stringify(err));
        });
      }, err => {
        console.warn("ImageEditorError" + JSON.stringify(err));

      });

- ReactNative 如何读取 iOS 沙盒里的图片?

解决方案:看如下代码

      let RNFS = require('react-native-fs');
        <Image
          style={{width:100, height:100}}
          source={{uri: 'file://' + RNFS.DocumentDirectoryPath + '/myAwesomeSubDir/my.png', scale:1}}


- ReactNative 如何做到图片宽度不变,宽高保持比例,高度自动调整。

RN 图片均需要指定宽高才会显示,如果图片数据的宽高不定,但又希望宽度保持不变、不同图片的高度根据比例动态变化,就需要用到下面这个库,业务场景常用于文章、商品详情的多图展示。

解决方案:使用react-native-scalable-image

- navigor 无法使用的解决办法

从 0.44 版本开始,Navigator 被从 react native 的核心组件库中剥离到了一个名为react-native-deprecated-custom-components的单独模块中。如果你需要继续使用 Navigator,则需要先npm i facebookarchive/react-native-custom-components安装,然后从这个模块中 import,即import { Navigator } from 'react-native-deprecated-custom-components'

如果报错如下参考下面的解决方案

React-Native – undefined is not an object (“ evaluating _react3.default.PropTypes.shape ”)

解决方案

如果已经安装了,先卸载npm uninstall --save react-native-deprecated-custom-components

用下面的命令安装 npm install --save https://github.com/facebookarchive/react-native-custom-components.git

在我们使用 Navigator 的 js 文件中加入下面这个导入包就可以了。

import { Navigator } from'react-native-deprecated-custom-components';(注意最后有一个分号)

就可以正常使用 Navigator 组件了。

- ReactNative 开发的 APP 启动闪白屏问题

由于处理 JS 需要时间,APP 启动会出现一闪而过白屏,可以通过启动页延迟加载方法来避免这类白屏,可以用下面的库 解决方案react-native-splash-screen

- ReactNative 如何做到无感热更新

无论是整包热更新还是差量热更新,均需要最终替换 JSBundle 等文件来完成更新过程,实现原理是 js 来控制启动页的消失时间,等原生把 bundle 包下载(或合并成新 bundle 包)解压到目录以后,通知 js 消失启动页,由于热更新时间一般很短,建议使用差量热更新,一秒左右,所以用户等启动页消失后看到的就是最新的版本。 解决方案(以整包更新为例):

  1. 原生端完成更新及刷新操作,注意里面的[_bridge reload]
//前往更新 js 包
RCT_EXPORT_METHOD(gotoUpdateJS:(NSString *)jsUrl andResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){
  if (!jsUrl) {
    return;
  }
  
  //jsbundle 更新采用静默更新
  //更新
  NSLog(@"jsbundleUrl is : %@",  jsUrl);
  [[LJFileHelper shared] downloadFileWithURLString:jsUrl finish:^(NSInteger status, id data) {
    if(status == 1){
      NSLog(@"下载完成");
      NSError *error;
      NSString *filePath = (NSString *)data;
      NSString *desPath = [NSString stringWithFormat:@"%@",NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]];
      [SSZipArchive unzipFileAtPath:filePath toDestination:desPath overwrite:YES password:nil error:&error];
      if(!error){
        [_bridge reload];

        resolve([NSNumber numberWithBool:true]);
        NSLog(@"解压成功");
        
      }else{
        resolve([NSNumber numberWithBool:false]);
        NSLog(@"解压失败");
      }
    }
  }];

  reject = nil;
}

  1. JS 端
// 原生端通过回调结果通知 JS 热更新情况,JS 端
UpdateModule.gotoUpdateJS(jsUrl).then(resp => {

    if ( resp ) {
       // 成功更新通知隐藏启动页
       DeviceEventEmitter.emit("hide_loading_page",'hide');
    } else {
       // 出问题也要隐藏启动页,用户继续使用旧版本
       DeviceEventEmitter.emit("hide_loading_page",'hide');
       // 其他处理
    }

});
  1. 启动页消失,用户看到的是新版 APP
    async componentWillMount() {

        this.subscription = DeviceEventEmitter.addListener("hide_loading_page", this.hideLoadingPage);
        appUpdateModule.updateJs();


    }

    hideLoadingPage = ()=> {
        SplashScreen.hide();
    };

注意做好容错,例如弱网无网环境下的处理,热更新失败下次保证再次热更新的处理,热更新时间把控,超过时间下次再 reload,是否将热更新 reload 权利交给用户等等都可以扩展。

- ReactNative 如何取消部分警告

debug 模式下调试经常会有黄色的警告,有些警告可能是短时间不需要处理,通过下面的解决方法能忽略部分警告提示

解决方案:使用console.ignoredYellowBox

import { AppRegistry } from 'react-native';
import './app/Common/SetTheme'
import './app/Common/Global'


import App from './App';

console.ignoredYellowBox = ['Warning: BackAndroid is deprecated.  Please use BackHandler instead.',
    'source.uri should not be an empty string','Remote debugger is in a background tab which',
    'Setting a timer',
    'Encountered two children with the same key,',
    'Attempt to read an array index',
];

AppRegistry.registerComponent('ReactNativeTemplate', () => App);

- ReactNative 开发遇到 android 网络图片显示不出来的问题

开发过程中有时会遇到 iOS 图片正常显示,但是安卓却只能显示部分网络图片,造成这个的原因有多种,参考下面的解决方案。

解决方案

  1. 安卓增加 resizeMethod 属性并设置为 resize
<Image style={styles.imageStyle} source={{uri: itemInfo.imageUrl || ''}} resizeMethod={'resize'}/>

resizeMethod 官方解释

resizeMethod  enum('auto', 'resize', 'scale') 

当图片实际尺寸和容器样式尺寸不一致时,决定以怎样的策略来调整图片的尺寸。默认值为 auto。

auto:使用启发式算法来在 resize 和 scale 中自动决定。

resize: 在图片解码之前,使用软件算法对其在内存中的数据进行修改。当图片尺寸比容器尺寸大得多时,应该优先使用此选项。

scale:对图片进行缩放。和 resize 相比,scale 速度更快(一般有硬件加速),而且图片质量更优。在图片尺寸比容器尺寸小或者只是稍大一点时,应该优先使用此选项。

关于 resize 和 scale 的详细说明请参考 http://frescolib.org/docs/resizing-rotating.html.
  1. 如果是 FlatList 或 ScrollView 等包裹图片,尝试设置

removeClippedSubviews={true}//ios set false

  1. 如果还是有问题,尝试配合react-native-image-progress

    还可以谨慎尝试使用react-native-fast-image

- ReactNative 判断及监控网络情况方法总结

提前获取用户的网络情况很有必要,RN 主要靠 NetInfo 来获取网络状态,不过随着 RN 版本的更新也有一些变化。 解决方案:

  1. 较新的 RN 版本(大概是 0.50 及以上版本)
    this.queryConfig();


    queryConfig = ()=> {
        this.listener = NetInfo.addEventListener('connectionChange', this._netChange);
    };
    
        // 网络发生变化时
    _netChange = async(info)=> {
        const {
            type,
            //effectiveType
        } = info;
        const netCanUse = !(type === 'none' || type === 'unknown' || type === 'UNKNOWN' || type === 'NONE');
        if (!netCanUse) {
            this.setState({
                isNetError : true
            });
            this.alertNetError(); //或者其他通知形式

        } else {
            try {
                // 注意这里的 await 语句,其所在的函数必须有 async 关键字声明
                let response = await fetch(CONFIG_URL);
                let responseJson = await response.json();
                const configData = responseJson.result;
                if (response && configData) {
                    this.setState({
                        is_show_tip: configData.is_show_tip,
                        app_bg: CONFIG_HOST + configData.app_bg,
                        jumpUrl: configData.url,
                        isGetConfigData: true
                    }, () => {
                        SplashScreen.hide();
                    })
                } else {
                    // 错误码也去壳
                    if ( responseJson.code === 400 ) {
                        this.setState({
                            isGetConfigData: true
                        }, () => {
                            SplashScreen.hide();
                        })
                    } else {
                        this.setState({
                            isGetConfigData: false
                        }, () => {
                            SplashScreen.hide();
                        })
                    }
                }

            } catch (error) {
                console.log('queryConfig error:' + error);
                this.setState({
                    isGetConfigData: true
                }, () => {
                    SplashScreen.hide();
                })
            }

        }
    };
    
    
    
    
        alertNetError = () => {
        setTimeout(()=> {
            SplashScreen.hide();

        }, 1000);

        if ( ! this.state.is_show_tip &&  this.state.isGetConfigData ) {
            return
        } else {
            Alert.alert(
                'NetworkDisconnected',
                '',
                [
                    {text: 'NetworkDisconnected_OK', onPress: () => {
                        this.checkNetState();
                    }},
                ],
                {cancelable: false}
            );        }


    };
    
    
    
    
        checkNetState = () => {
        NetInfo.isConnected.fetch().done((isConnected) => {
            if ( !isConnected ) {
                this.alertNetError();
            } else {
                this.queryConfig();
            }
        });

    };

  1. 老版本
    async componentWillMount() {
        this.queryConfig();
    }

    checkNetState = () => {
        NetInfo.isConnected.fetch().done((isConnected) => {
            console.log('111Then, is ' + (isConnected ? 'online' : 'offline'));
            if (!isConnected) {
                this.alertNetError();
            } else {
                this.queryConfig();
            }
        });

    };

    alertNetError = () => {
        setTimeout(()=> {
            SplashScreen.hide();

        }, 1000);
        console.log('111111');

        if (!this.state.is_show_tip && this.state.isGetConfigData) {
            console.log('222222');

            return
        } else {
            console.log('33333');

            Alert.alert(
                'NetworkDisconnected',
                '',
                [
                    {
                        text: 'NetworkDisconnected_OK', onPress: () => {
                        this.checkNetState();
                    }
                    },
                ],
                {cancelable: false}
            );
        }


    };


    queryConfig = ()=> {
       
        NetInfo.isConnected.addEventListener(
            'connectionChange',
            this._netChange
        );

    };


    // 网络发生变化时
    _netChange = async(isConnected)=> {
        console.log('Then, is ' + (isConnected ? 'online' : 'offline'));

    
        if (!isConnected) {
            console.log('666');

            this.setState({
                isNetError: true
            });
            this.alertNetError();

        } else {
            try {
                // 注意这里的 await 语句,其所在的函数必须有 async 关键字声明
                let response = await fetch(CONFIG_URL);
                let responseJson = await response.json();
                const configData = responseJson.result;
                if (response && configData) {
                    this.setState({
                        is_show_tip: configData.is_show_tip,
                        app_bg: CONFIG_HOST + configData.app_bg,
                        jumpUrl: configData.url,
                        isGetConfigData: true
                    }, () => {
                        SplashScreen.hide();
                        this.componentNext();
                    })
                } else {
                    this.setState({
                        isGetConfigData: false
                    }, () => {
                        SplashScreen.hide();
                        this.componentNext();
                    })
                }

            } catch (error) {
                console.log('queryConfig error:' + error);
                this.setState({
                    isGetConfigData: true
                }, () => {
                    SplashScreen.hide();
                    this.componentNext();
                })
            }

        }
    };

- ReactNative 版本升级后报错有废弃代码的快速解决方法

使用第三方库或者老版本升级时会遇到报错提示某些方法被废弃,这时候寻找和替换要花不少时间,而且还容易漏掉。

解决方案: 根据报错信息,搜索废弃的代码,例如

报错提示:Use viewPropTypes instead of View.propTypes.

搜索命令:grep -r 'View.propTypes' .

替换搜索出来的代码即可。

这是用于查找项目里的错误或者被废弃的代码的好方法

- 解决 ReactNative 的 TextInput 在 0.55 中文无法输入的问题

此问题主要体现在 iOS 中文输入法无法输入汉字,是 0.55 版 RN 的一个 bug

解决方案:使用下面的MyTextInput替换原TextInput

import React from 'react';
import { TextInput as Input } from 'react-native';

export default class MyTextInput extends React.Component {
    static defaultProps = {
        onFocus: () => { },
    };

    constructor(props) {
        super(props);

        this.state = {
            value: this.props.value,
            refresh: false,
        };
    }

    shouldComponentUpdate(nextProps, nextState) {
        if (this.state.value !== nextState.value) {
            return false;
        }

        return true;
    }

    componentDidUpdate(prevProps) {
        if (prevProps.value !== this.props.value && this.props.value === '') {
            this.setState({ value: '', refresh: true }, () => this.setState({ refresh: false }));
        }
    }

    focus = (e) => {
        this.input.focus();
    };

    onFocus = (e) => {
        this.input.focus();

        this.props.onFocus();
    };

    render() {
        if (this.state.refresh) {
            return null;
        }

        return (
            <Input
                {...this.props}
                ref={(ref) => { this.input = ref; }}
                value={this.state.value}
                onFocus={this.onFocus}
            />
        );
    }
}

ReactNative 集成第三方 DEMO 编译时遇到 RCTSRWebSocket 错误的解决方法

报错信息如下

Ignoring return value of function declared with warn_unused_result attribute

解决方案

StackOverFlow 上的解决方法:

在 navigator 双击 RCTWebSocket project,移除 build settings > custom compiler 下的 flags

版权声明:

转载时请注明作者Kovli以及本文地址: http://www.kovli.com/2018/06/25/rn-anything/


2184 次点击
所在节点    React
2 条回复
kovli
2019-01-26 10:50:10 +08:00
不可以编辑主题,好吧,以后通过回复来更新我的博文。

- 升级旧 RN 版本到目前最新的 0.57.8 如果采用手动升级需要注意如下。
I upgraded from react-naitve 0.55.4 to react-native 0.57.0 and I get this error bundling failed: Error: The 'decorators' plugin requires a 'decoratorsBeforeExport' option, whose value must be a boolean. If you are migrating from Babylon/Babel 6 or want to use the old decorators proposal, you should use the 'decorators-legacy' plugin instead of 'decorators'.

解决方案:参考如下例子

First install the new proposal decorators with npm install @babel/plugin-proposal-decorators --save-dev or yarn add @babel/plugin-proposal-decorators --dev

Then, inside of your .babelrc file, change this:

{
"presets": ["react-native"],
"plugins": ["transform-decorators-legacy"]
}
To this:

{
"presets": [
"module:metro-react-native-babel-preset",
"@babel/preset-flow"
],
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy" : true }]
]
}

EDIT:

After you've updated your .babelrc file, make sure to add preset-flow as well with the command yarn add @babel/preset-flow --dev or npm install @babel/preset-flow --save-dev
kovli
2019-04-28 14:16:26 +08:00
- 如何在原生端( iOS 和 android 两个平台)使用 ReactNative 里的本地图片(路径类似 require('./xxximage.png'))。
在 ReactNative 开发过程中,有时需要在原生端显示 RN 里的图片,这样的好处是可以通过热更新来更新 APP 里的图片,而不需要发布原生版本,而 ReactNative 里图片路径是相对路径,类似'./xxximage.png'的写法,原生端是无法解析这类路径,那么如果将 RN 的图片传递给原生端呢?

解决方案:

1、图片如果用网络图,那只需要将 url 字符串地址传递给原生即可,这种做法需要时间和网络环境加载图片,不属于本地图片,不是本方案所追求的最佳方式。

2、懒人做法是把 RN 的本地图片生成 base64 字符串然后传递给原生再解析,这种做法如果图片太大,字符串会相当长,同样不认为是最佳方案。

其实 RN 提供了相关的解决方法,如下:

RN 端

const myImage = require('./my-image.png');
const resolveAssetSource = require('react-native/Libraries/Image/resolveAssetSource');
const resolvedImage = resolveAssetSource(myImage);
NativeModules.NativeBridge.showRNImage(resolvedImage);

iOS 端

#import <React/RCTConvert.h>


RCT_EXPORT_METHOD(showRNImage:(id)rnImageData){
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *rnImage = [RCTConvert UIImage:rnImageData];
...
});
}

安卓端

第一步,从桥接文件获取到 uri 地址


@ReactMethod
public static void showRNImage(Activity activity, ReadableMap params){
String rnImageUri;
try {
//图片地址
rnImageUri = params.getString("uri");
Log.i("Jumping", "uri : " + uri);

...

} catch (Exception e) {
return;
}
}

第二步,创建 JsDevImageLoader.java


package com.XXX;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.StrictMode;
import android.support.annotation.NonNull;
import android.util.Log;


import com.XXX.NavigationApplication;

import java.io.IOException;
import java.net.URL;

public class JsDevImageLoader {
private static final String TAG = "JsDevImageLoader";
public static Drawable loadIcon(String iconDevUri) {
try {
StrictMode.ThreadPolicy threadPolicy = StrictMode.getThreadPolicy();
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().permitNetwork().build());

Drawable drawable = tryLoadIcon(iconDevUri);

StrictMode.setThreadPolicy(threadPolicy);
return drawable;
} catch (Exception e) {
Log.e(TAG, "Unable to load icon: " + iconDevUri);
return new BitmapDrawable();
}
}

@NonNull
private static Drawable tryLoadIcon(String iconDevUri) throws IOException {
URL url = new URL(iconDevUri);
Bitmap bitmap = BitmapFactory.decodeStream(url.openStream());
return new BitmapDrawable(NavigationApplication.instance.getResources(), bitmap);
}
}
第三步,导入 ResourceDrawableIdHelper.java


package com.xg.navigation.react;// Copyright 2004-present Facebook. All Rights Reserved.

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.net.Uri;

import com.facebook.common.util.UriUtil;

import java.util.HashMap;
import java.util.Map;

import javax.annotation.Nullable;

/**
* Direct copy paste from react-native, because they made that class package scope. -_-"
* Can be deleted in react-native ^0.29
*/
public class ResourceDrawableIdHelper {
public static final ResourceDrawableIdHelper instance = new ResourceDrawableIdHelper();

private Map<String, Integer> mResourceDrawableIdMap;

public ResourceDrawableIdHelper() {
mResourceDrawableIdMap = new HashMap<>();
}

public int getResourceDrawableId(Context context, @Nullable String name) {
if (name == null || name.isEmpty()) {
return 0;
}
name = name.toLowerCase().replace("-", "_");
if (mResourceDrawableIdMap.containsKey(name)) {
return mResourceDrawableIdMap.get(name);
}
int id = context.getResources().getIdentifier(
name,
"drawable",
context.getPackageName());
mResourceDrawableIdMap.put(name, id);
return id;
}

@Nullable
public Drawable getResourceDrawable(Context context, @Nullable String name) {
int resId = getResourceDrawableId(context, name);
return resId > 0 ? context.getResources().getDrawable(resId) : null;
}

public Uri getResourceDrawableUri(Context context, @Nullable String name) {
int resId = getResourceDrawableId(context, name);
return resId > 0 ? new Uri.Builder()
.scheme(UriUtil.LOCAL_RESOURCE_SCHEME)
.path(String.valueOf(resId))
.build() : Uri.EMPTY;
}
}

第四步,创建 BitmapUtil.java


package com.XXX;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;

import com.XXX.NavigationApplication;
import com.XXX.JsDevImageLoader;
import com.XXX.ResourceDrawableIdHelper;

import java.io.IOException;


public class BitmapUtil {

private static final String FILE_SCHEME = "file";

public static Drawable loadImage(String iconSource) {

if (TextUtils.isEmpty(iconSource)) {
return null;
}

if (NavigationApplication.instance.isDebug()) {
return JsDevImageLoader.loadIcon(iconSource);
} else {
Uri uri = Uri.parse(iconSource);
if (isLocalFile(uri)) {
return loadFile(uri);
} else {
return loadResource(iconSource);
}
}
}

private static boolean isLocalFile(Uri uri) {
return FILE_SCHEME.equals(uri.getScheme());
}

private static Drawable loadFile(Uri uri) {
Bitmap bitmap = BitmapFactory.decodeFile(uri.getPath());
return new BitmapDrawable(NavigationApplication.instance.getResources(), bitmap);
}

private static Drawable loadResource(String iconSource) {
return ResourceDrawableIdHelper.instance.getResourceDrawable(NavigationApplication.instance, iconSource);
}

public static Bitmap getBitmap(Activity activity, String uri) {

if (activity == null || uri == null || TextUtils.isEmpty(uri)) {
return null;
}

Uri mImageCaptureUri;
try {
mImageCaptureUri = Uri.parse(uri);
} catch (Exception e) {
e.printStackTrace();
return null;
}

if (mImageCaptureUri == null) {
return null;
}

Bitmap bitmap = null;
try {
bitmap = MediaStore.Images.Media.getBitmap(activity.getContentResolver(), mImageCaptureUri);
} catch (IOException e) {
e.printStackTrace();
return null;
}

return bitmap;
}
}

第五步,使用第一步里的 rnImageUri 地址

...
BitmapUtil.loadImage(rnImageUri)
...

第六步,显示图片


import android.widget.RelativeLayout;
import android.support.v7.widget.AppCompatImageView;
import android.graphics.drawable.Drawable;

...
final RelativeLayout item = (RelativeLayout) mBottomBar.getChildAt(i);
final AppCompatImageView itemIcon = (AppCompatImageView) item.getChildAt(0);
itemIcon.setImageDrawable(BitmapUtil.loadImage(rnImageUri));

...

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/519062

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX