二十二、控制图像显示

到目前为止,本书中的示例还没有在移动屏幕上呈现任何图像。这并不反映移动应用的实际情况。Web 应用显示大量图像。如果有什么区别的话,本地移动应用甚至比 web 应用更依赖图像,因为当您的空间有限时,图像是强大的工具。

在本章中,您将学习如何使用 React NativeImage组件,从加载不同来源的图像开始。然后,您将看到如何使用Image组件调整图像大小,以及如何为惰性图像设置占位符。最后,您将学习如何使用react-native-vector-icons包实现图标。

加载图像

让我们从了解如何实际加载图像开始。您可以渲染<Image>组件,并像其他任何 React 组件一样传递其属性。但是这个特殊的组件需要图像 blob 数据才能使用。让我们看一些代码:

import React, { PropTypes } from 'react'; 
import { 
  AppRegistry, 
  View, 
  Image, 
} from 'react-native'; 

import styles from './styles'; 

// Renders two "<Image>" components, passing the 
// properties of this component to the "source" 
// property of each image. 
const LoadingImages = ({ 
  reactSource, 
  relaySource, 
}) => ( 
  <View style={styles.container}> 
    <Image 
      style={styles.image} 
      source={reactSource} 
    /> 
    <Image 
      style={styles.image} 
      source={relaySource} 
    /> 
  </View> 
); 

// The "source" property can be either 
// an object with a "uri" string, or a number 
// representing a local "require()" resource. 
const sourceProp = PropTypes.oneOfType([ 
  PropTypes.shape({ 
    uri: PropTypes.string.isRequired, 
  }), 
  PropTypes.number, 
]).isRequired; 

LoadingImages.propTypes = { 
  reactSource: sourceProp, 
  relaySource: sourceProp, 
}; 

LoadingImages.defaultProps = { 
  // The "reactSource" image comes from a remote 
  // location. 
  reactSource: { 
    uri: 'https://facebook.github.io/react/img/logo_small_2x.png', 
  }, 

  // The "relaySource" image comes from a local 
  // source. 
  relaySource: require(img/relay.png'), 
}; 

AppRegistry.registerComponent( 
  'LoadingImages', 
  () => LoadingImages 
); 

如您所见,有两种方法可以将 blob 数据加载到<Image>组件中。第一种方法从网络加载图像数据。这是通过将具有uri属性的对象传递给source来完成的。我们在这里渲染的第二个<Image>组件使用本地图像文件,通过调用require()并将结果传递给source

看看我们创建的sourceProp属性类型验证器。这让您了解可以传递给source属性的内容。它要么是一个具有uri字符串属性的对象,要么是一个数字。它需要一个数字,因为require()返回一个数字。

现在,让我们看看渲染结果是什么样子的:

Loading images

以下是用于这些图像的样式:

image: { 
  width: 100, 
  height: 100, 
  margin: 20, 
}, 

请注意,如果没有widthheight样式属性,图像将不会渲染。在下一节中,我们将了解设置了widthheight值时图像大小调整的工作原理。

调整图像大小

Image组件的widthheight样式属性决定了屏幕上呈现内容的大小。例如,您可能需要在某个点处理分辨率比您希望在 React 本机应用中显示的分辨率更高的图像。只需在Image上设置widthheight样式属性就足以正确缩放图像。

让我们看一些代码,这些代码允许您使用控件动态调整图像的尺寸:

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  View, 
  Text, 
  Image, 
  Slider, 
} from 'react-native'; 
import { fromJS } from 'immutable'; 

import styles from './styles'; 

class ResizingImages extends Component { 
  // The initial state of this component includes 
  // a local image source, and the width/height 
  // image dimensions. 
  state = { 
    data: fromJS({ 
      source: require(img/flux.png'), 
      width: 100, 
      height: 100, 
    }), 
  }; 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  render() { 
    // The state values we need... 
    const { 
      source, 
      width, 
      height, 
    } = this.data.toJS(); 

    return ( 
      <View style={styles.container}> 
        { /* The image is rendered using the 
             "source", "width", and "height" 
             state values. */ } 
        <Image 
          source={source} 
          style={{ width, height }} 
        /> 
        { /* The current "width" and "height" 
             values are displayed. */ } 
        <Text>Width: {width}</Text> 
        <Text>Height: {height}</Text> 
        { /* This slider scales the image size 
             up or down by changing the "width" 
             and "height" states. */ } 
        <Slider 
          style={styles.slider} 
          minimumValue={50} 
          maximumValue={150} 
          value={width} 
          onValueChange={(v) => { 
            this.data = this.data 
              .merge({ 
                width: v, 
                height: v, 
              }); 
          }} 
        /> 
      </View> 
    ); 
  } 
} 

AppRegistry.registerComponent( 
  'ResizingImages', 
  () => ResizingImages 
); 

以下是使用默认 100x100 尺寸时图像的外观:

Resizing images

以下是图像的缩小版本:

Resizing images

最后,这是图像的放大版本:

Resizing images

有一个resizeMode属性可以传递给Image组件。这将确定缩放图像如何适合实际零部件的尺寸。您将在本章的最后一节中看到此属性的作用。

延迟图像加载

有时,您不一定希望图像在渲染时加载。例如,您可能正在渲染屏幕上尚不可见的内容。大多数情况下,在图像源实际可见之前从网络中获取图像源是完全正确的。但是,如果您正在微调应用,并且发现通过网络加载大量图像会导致性能问题,则可以延迟加载源。

我认为在移动环境中,更常见的情况是处理这样一种场景:您在可见的位置渲染了一个或多个图像,但网络响应速度较慢。在这种情况下,您可能希望呈现占位符图像,以便用户立即看到某些内容,而不是空白。

为了做到这一点,我们将实现一个抽象,它封装了我们希望在加载后显示的实际图像。代码如下:

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

// The local placeholder image source. 
const placeholder = require(img/placeholder.png'); 

// The mapping to the "loaded" state that gets us 
// the appropriate image component. 
const Placeholder = props => 
  new Map([ 
    [true, null], 
    [false, ( 
      <Image 
        {...props} 
        source={placeholder} 
      /> 
    )], 
  ]).get(props.loaded); 

class LazyImage extends Component { 
  // The "width" and "height" properties 
  // are required. All other properties are 
  // forwarded to the actual "<Image>" 
  // component. 
  static propTypes = { 
    style: PropTypes.shape({ 
      width: PropTypes.number.isRequired, 
      height: PropTypes.number.isRequired, 
    }), 
  }; 

  constructor() { 
    super(); 

    // We assume that the source hasn't finished 
    // loading yet. 
    this.state = { 
      loaded: false, 
    }; 
  } 

  render() { 
    // The props and state this component 
    // needs in order to render... 
    const { 
      props: { 
        style: { 
          width, 
          height, 
        }, 
      }, 
      state: { 
        loaded, 
      }, 
    } = this; 

    return ( 
      <View style={{ width, height }}> 
        { /* The placeholder image is just a standard 
             "<Image>" component with a predefined 
             source. It isn't rendered if "loaded" is 
             true. */ } 
        <Placeholder loaded={loaded} {...this.props} /> 
        { /* The actual image is forwarded props that 
             are passed to "<LazyImage>". The "onLoad" 
             handler ensures the "loaded" state is true, 
             removing the placeholder image. */ } 
        <Image 
          {...this.props} 
          onLoad={() => this.setState({ 
            loaded: true, 
          })} 
        /> 
      </View> 
    ); 
  } 
} 

export default LazyImage; 

该组件呈现一个包含两个Image组件的View。它也有一个loaded状态,它最初是假的。当loaded为 false 时,将呈现占位符图像。调用onLoad()处理程序时,loaded状态设置为 true。这意味着将删除占位符图像,并显示主图像。

现在让我们使用刚刚实现的LazyImage组件。我们将渲染没有源的图像,并且应该显示占位符图像。我们将添加一个按钮,为惰性图像提供一个源,当它加载时,占位符图像应该被替换。以下是主应用模块的外观:

/* eslint-disable global-require */ 
import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  View, 
} from 'react-native'; 

import styles from './styles'; 
import LazyImage from './LazyImage'; 
import Button from './Button'; 

// The remote image to load... 
const remote = 'https://facebook.github.io/react/img/logo_small_2x.png'; 

class LazyLoading extends Component { 
  state = { 
    source: null, 
  } 

  render() { 
    return ( 
      <View style={styles.container}> 
        { /* Renders the lazy image. Since there's 
             no "source" value initially, the placeholder 
             image will be rendered. */ } 
        <LazyImage 
          style={{ width: 200, height: 100 }} 
          resizeMode="contain" 
          source={this.state.source} 
        /> 
        { /* When pressed, this button changes the 
             "source" of the lazy image. When the new 
             source loads, the placeholder image is 
             replaced. */ } 
        <Button 
          label="Load Remote" 
          onPress={() => this.setState({ 
            source: { uri: remote }, 
          })} 
        /> 
      </View> 
    ); 
  } 
} 

AppRegistry.registerComponent( 
  'LazyLoading', 
  () => LazyLoading 
); 

这是屏幕最初的样子:

Lazy image loading

然后,如果你点击加载远程按钮,你最终会看到我们想要的图像:

Lazy image loading

您可能会注意到,根据您的网络速度,即使单击加载远程按钮,占位符图像仍然可见。这是出于设计,因为在确定实际图像已准备好显示之前,您不想删除占位符图像。

渲染图标

在本章的最后一节中,我们将介绍如何在 React 本机组件中呈现图标。使用图标来表示意义使 web 应用更具可用性。那么,为什么本地移动应用应该有所不同呢?

您需要使用react-native-vector-icons包将各种矢量字体包引入到您的 React 原生项目中。安装起来非常简单:

npm install react-native-vector-icons --save
react-native link

现在您可以导入Icon组件并渲染它们。让我们实现一个基于选定图标类别呈现多个FontAwesome图标的示例:

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  View, 
  Picker, 
  ListView, 
  Text, 
} from 'react-native'; 
import Icon from 'react-native-vector-icons/FontAwesome'; 
import { fromJS } from 'immutable'; 

import styles from './styles'; 
import iconNames from './icon-names.json'; 

class RenderingIcons extends Component { 
  // The initial state consists of the "selected" 
  // category, the "icons" JSON object, and the 
  // "listSource" used to render the list view. 
  state = { 
    data: fromJS({ 
      selected: 'Web Application Icons', 
      icons: iconNames, 
      listSource: new ListView.DataSource({ 
        rowHasChanged: (r1, r2) => r1 !== r2, 
      }), 
    }), 
  } 

  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 

  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 

  // Sets the "listSource" state based on the 
  // "selected" icon state. Also sets the "selected" 
  // state. 
  updateListSource = (selected) => { 
    this.data = this.data 
      .update('listSource', listSource => 
        listSource.cloneWithRows( 
          this.data 
            .getIn(['icons', selected]) 
            .toJS() 
        ) 
      ) 
      .set('selected', selected); 
  } 

  // Make sure the "listSource" is populated 
  // before the first render. 
  componentWillMount() { 
    this.updateListSource(this.data.get('selected')); 
  } 

  render() { 
    const { 
      updateListSource, 
    } = this; 

    // Get the state that we need to render the icon 
    // category picker and the list view with icons. 
    const selected = this.data 
      .get('selected'); 
    const categories = this.data 
      .get('icons') 
      .keySeq() 
      .toJS(); 
    const listSource = this.data 
      .get('listSource'); 

    return ( 
      <View style={styles.container}> 
        <View style={styles.picker}> 
          { /* Lets the user select a FontAwesome icon 
               category. When the selection is changed, 
               the list view is changed. */ } 
          <Picker 
            selectedValue={selected} 
            onValueChange={updateListSource} 
          > 
            {categories.map(c => ( 
              <Picker.Item 
                key={c} 
                label={c} 
                value={c} 
              /> 
            ))} 
          </Picker> 
        </View> 
        <ListView 
          style={styles.icons} 
          dataSource={listSource} 
          renderRow={icon => ( 
            <View style={styles.item}> 
              { /* The "<Icon>" component is used 
                   to render the FontAwesome icon */ } 
              <Icon 
                name={icon} 
                style={styles.itemIcon} 
              /> 
              {/* Shows the icon class used */} 
              <Text style={styles.itemText}> 
                {icon} 
              </Text> 
            </View> 
          )} 
        /> 
      </View> 
    ); 
  } 
} 

AppRegistry.registerComponent( 
  'RenderingIcons', 
  () => RenderingIcons 
); 

运行该示例时,您应该看到如下内容:

Rendering icons

如您所见,图标的颜色与通过样式指定文本颜色的方式相同。

总结

在本章中,您学习了如何在本机应用中处理图像。本机应用中的图像在本机移动环境中与在 web 环境中一样重要,它们可以改善用户体验。

您学习了加载图像的不同方法,以及如何调整图像大小。您还学习了如何实现在加载实际图像时使用占位符图像显示的惰性图像。最后,您学习了如何在 React 本机应用中使用图标。

在下一章中,我们将了解 React Native 中的本地存储,这在脱机场景中非常方便。