分类目录归档:移动端

react-native仿微信通讯录右侧边栏快速定位功能

代码地址 -> ReactNativeCountrySelect

1. 界面

SectionList把数据渲染出来

右边的A-ZText组件即可,这里为了做滑动定位,没有选择Touchable组件

import * as React from 'react';
import {
  Text,
  View,
  StyleSheet,
  SectionList,
  SafeAreaView,
} from 'react-native';

import countries from './countryCode.json';
const sectionMapArr = [
  ['A', 0],
  ['B', 1],
  ['C', 2],
  ['D', 3],
  ['E', 4],
  ['F', 5],
  ['G', 6],
  ['H', 7],
  ['I', 8],
  ['J', 9],
  ['K', 10],
  ['L', 11],
  ['M', 12],
  ['N', 13],
  ['O', 14],
  ['P', 15],
  ['Q', 16],
  ['R', 17],
  ['S', 18],
  ['T', 19],
  ['U', 20],
  ['V', 21],
  ['W', 22],
  ['X', 23],
  ['Y', 24],
  ['Z', 25],
];
export default class App extends React.Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <SectionList
          containerStyle={{ flex: 1, justifyContent: 'center' }}
          ItemSeparatorComponent={() => (<View style={{ borderBottomColor: '#F8F8F8', borderBottomWidth: 1, }} />)}
          renderItem={({ item, index, section }) => (
            <View style={styles.itemContainer}>
              <Text style={styles.itemText} key={index}>
                {item.countryName}
              </Text>
            </View>
          )}
          renderSectionHeader={({ section: { key } }) => (
            <View style={styles.headerContainer}>
              <Text style={styles.headerText}>{key}</Text>
            </View>
          )}
          sections={countries}
          keyExtractor={(item, index) => item + index}
        />
        <View
          style={{ width: 16, justifyContent: 'center' }}
        >
          {sectionMapArr.map((item, index) => {
            return (
              <Text
                key={index}
              >
                {item[0]}
              </Text>
            );
          })}
        </View>
      </SafeAreaView>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#FFFFFF',
    flexDirection: 'row',
  },
  headerContainer: {
    padding: 5,
    backgroundColor: '#F8F8F8',
  },
  headerText: {
    fontWeight: 'bold',
  },
  itemContainer: {
    paddingHorizontal: 5,
    paddingVertical: 10,
  },
  itemText: {
    fontSize: 16,
  },
});

这时候界面已经完成了,然后就是增加触摸滑动定位的功能了。

2. 使用Gesture Responder System监听触摸事件

给右侧的View上启用手势


<View style={{ width: 16, justifyContent: 'center' }} onStartShouldSetResponder={() => true} onMoveShouldSetResponder={() => true} > {sectionMapArr.map((item, index) => { return ( <Text key={index} > {item[0]} </Text> ); })} </View>

这样我们就可以在触摸滑动的时候,获得滑动到的位置

3. 使用onLayout找到每个字母对应的X、Y

首先在constructor里声明一个实例属性,用来记录每个字母的信息:

this.ps = [];

然后在Text组件上利用onLayout获得每个字母的位置,并且存到this.ps里:

<Text
  key={index}
  onLayout={({
    nativeEvent: {
      layout: { x, y, width, height },
    },
  }) => {
    this.ps = this.ps.filter(i => i.key !== item[0]); 
    this.ps.push({
      key: item[0],     // 对应的字母 A-Z
      min: y,           // 字母顶部Y坐标
      max: y + height,  // 字母底部Y坐标
      index: item[1],   // 字母对应SectionList的index
    });
  }}
>
  {item[0]}
</Text>

4. 根据滑动找到滑到哪个字母上

<View
  style={{ width: 16, justifyContent: 'center' }}
  onStartShouldSetResponder={() => true}
  onMoveShouldSetResponder={() => true}
  onResponderMove={({ nativeEvent: { pageY } }) => {
    const offsetY = pageY - this.offsetY;
    const find = this.ps.find(
      i => i.min < offsetY && i.max > offsetY
    );
    if (find) {
      console.log(find) // 滑动到的字母
    }
  }}
>

5. 根据触摸的字母,SectionList跳到对应的位置

  1. 先在constructor里创建ref
this.sectionlist = React.createRef();
  1. 在SectionList上绑定ref
ref={this.sectionlist}
  1. 调用SectionList的scrollToLocation
onResponderMove={({ nativeEvent: { pageY } }) => {
  const offsetY = pageY - this.offsetY;
  const find = this.ps.find(
    i => i.min < offsetY && i.max > offsetY
  );
  if (find) {
    this.sectionlist.current.scrollToLocation({
      sectionIndex: find.index,
      itemIndex: 0,
      animated: false,
    });
  }
}}

完工

react native改变WebView背景颜色

react-native的WebView背景颜色是无法改变的,不过我们可以用另一个View盖住WebView,达到改变颜色的效果。

import React from 'react';
import {View, StyleSheet, WebView, ActivityIndicator} from 'react-native';

export default class YourComponent extends React.PureComponent{

    state = {
        loading: true,
    };

    render() {
        return (
            <View>
                <WebView
                    source={html}
                    onLoadEnd={() => {
                        this.setState({loading: false});
                    }}
                />
                {
                    this.state.loading && (
                        <View style={{
                            ...StyleSheet.absoluteFillObject,
                            backgroundColor: '#000000',  // your color 
                            alignItems: 'center',
                            justifyContent: 'center',
                        }}>
                            <ActivityIndicator />
                        </View>
                    )
                }
            </View>
        )

    }
}

iOS开发:在Swift中使用Alamofire发送HTTP请求

创建项目

  1. 打开Xcode,点击Create a new Xcode project

  2. 选择Single View App,点击Next

  3. 输入Product Name:demo,Language选择Swift,点击Next

  4. 选择一个目录存放你的项目

使用Pod安装Alamofire

  1. 关掉Xcode

  2. 在你的项目目录里,创建一个文件Podfile

  3. 输入

    source 'https://github.com/CocoaPods/Specs.git'
    platform :ios, '10.0'
    use_frameworks!
    
    target 'demo' do
        pod 'Alamofire', '~> 4.7'
    end
    
  4. 在终端中输入:pod install

  5. 打开demo目录,双击demo.xcworkspace打开项目

使用Alamofire发送HTTP请求

  1. 打开文件ViewController.swift

  2. 在viewDidLoad函数内调用Alamofire

    Alamofire.request("https://api.github.com/gists").responseJSON { response in
        print("Request: \(String(describing: response.request))")   // original url request
        print("Response: \(String(describing: response.response))") // http url response
        print("Result: \(response.result)")                         // response serialization result
    
        if let json = response.result.value {
            print("JSON: \(json)") // serialized json response
        }
    
        if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
            print("Data: \(utf8Text)") // original server data as UTF8 string
        }
    }
    
  3. ⌘+R启动APP,可以看到输出

react-native中使用SafeAreaView保证iPhoneX兼容性

react-native从0.50.1开始,提供了SafeAreaView来确保iPhone X的兼容性,效果如下:

代码如下:

import {
  ...
  SafeAreaView
} from 'react-native';
class Main extends React.Component {
  render() {
    return (
      <SafeAreaView style={styles.safeArea}>
        <App />
      </SafeAreaView>
    )
  }
}
const styles = StyleSheet.create({
  ...,
  safeArea: {
    flex: 1,
    backgroundColor: '#ddd'
  }
})

并且,SafeAreaView会在接打电话等需要调整状态栏高度的时候自动调整状态栏的高度:

从0开始创建微信小程序

首先需要注册一个小程序的账号,然后才能创建微信小程序。

注册账号

打开https://mp.weixin.qq.com/wxopen/waregister?action=step1,填写表单,提交成功并且验证邮件之后你就拥有了小程序的账号,可以创建小程序了。
注意:这里的邮箱不能是公众号的邮箱。

填写小程序的信息

打开https://mp.weixin.qq.com/wxopen/initprofile,点击第一步最右边的连接,填写小程序的信息,就能创建小程序了。

下载开发工具

打开https://mp.weixin.qq.com/debug/wxadoc/dev/devtools/download.html, 下载对应操作系统的开发工具。下载完成之后安装。

运行Hello World

  1. 打开开发工具,新建一个项目

  2. 项目目录选择一个空的文件夹,app id填写你创建的app的id

  3. 输入项目名称

  4. 选择模板,我这里选择的是腾讯云Node.js模版

  5. 点击确定,即可使用模板创建一个小程序。


这个时候小程序可以运行了。

上传到服务器

我们可以通过上传一个体验版,来测试手机上的效果
点击上传:

提示上传成功之后,点击预览即可生成一个二维码,用微信扫一扫就能看到体验版的小程序。

注意:这个二维码只有https://mp.weixin.qq.com/wxopen/authprofile这个页面添加过的成员,并且有体验者权限的用户才能打开预览。
效果如下:

上传后端代码

  1. 点击右上角的“腾讯云”按钮,选择上传测试代码

  2. 选择“智能上传”并且勾选“部署后自动安装依赖”

  3. 等待部署完成

  4. 点击测试登录接口

说明我们的后端代码部署成功了。
这样就创建了一个最简单的小程序。

react native实现拖拽/拖放

react native实现拖拽/拖放

首先初始化一个react native的项目,cd到你的目录下运行

react-native init rndd

创建一个新的组件./app/components/Draggable.js

import React, {
    Component,
} from 'react';
import {
    StyleSheet,
    Image,
    PanResponder,
    Animated,
} from 'react-native';
class Draggable extends Component{
    constructor(props){
        super(props);
    }
    render(){
        return (
            <Animated.View style={styles.container}>
                <Image style={{width:80,height:80}} source={require('../assets/react-native.jpg')}/>
            </Animated.View>
        )
    }
}
export default Draggable;
const styles = StyleSheet.create({
    container: {
        position: 'absolute',
        left: 100,
        top: 100,
    }
});

加上PanResponder

PanResponder的api可以上官网查看PanResponder

componentWillMount() {
    this._panResponder = PanResponder.create({
        onMoveShouldSetResponderCapture: () => true,
        onMoveShouldSetPanResponderCapture: () => true,
        onPanResponderGrant: (e, gestureState) => {
        },
        onPanResponderMove: Animated.event([
        ]),
        onPanResponderRelease: (e, {vx, vy}) => {
        }
    });
}
render(){
    return (
        <Animated.View style={styles.container} {...this._panResponder.panHandlers}>
            <Image style={{width:80,height:80}} source={require('../assets/react-native.jpg')}/>
        </Animated.View>
    )
}

响应拖拽事件

先在state中创建一个对象来记录拖拽的记录。

constructor(props) {
    super(props);
    this.state = {
        pan: new Animated.ValueXY()
    };
}

更新panHandler:

componentWillMount() {
    this._panResponder = PanResponder.create({
        onMoveShouldSetResponderCapture: () => true,
        onMoveShouldSetPanResponderCapture: () => true,
        // 设置初始位置
        onPanResponderGrant: (e, gestureState) => {
            this.state.pan.setValue({x: 0, y: 0});
        },
        // 使用拖拽的偏移量来定位
        onPanResponderMove: Animated.event([
            null, {dx: this.state.pan.x, dy: this.state.pan.y},
        ]),
        onPanResponderRelease: (e, {vx, vy}) => {
        }
    });
}

更新render方法

render(){
    // 从state中取出pan
    const { pan } = this.state;
    // 从pan里计算出偏移量
    const [translateX, translateY] = [pan.x, pan.y];
    // 设置transform为偏移量
    const imageStyle = {transform: [{translateX}, {translateY}]};
    return (
        <Animated.View style={[styles.container,imageStyle]} {...this._panResponder.panHandlers}>
            <Image style={{width:80,height:80}} source={require('../assets/react-native.jpg')}/>
        </Animated.View>
    )
}

这个时候我们刷新用CMD+R刷新,发现已经可以拖动这个图片了,但是有个问题,第二次拖拽的时候又从原点开始移动,我们接下来解决这个问题。
我们有几个选择:

  1. 让拖动的元素在释放的时候回到原来的位置:
    修改PanResponder的onPanResponderRelease方法

    onPanResponderRelease: (e, {vx, vy}) => {
        Animated.spring(
            this.state.pan,
            {toValue: {x: 0, y: 0}}
        ).start();
    }
    
  2. 让拖动的元素停留在拖拽到的位置:
    修改PanResponder的onPanResponderGrant和onPanResponderRelease方法

    this._panResponder = PanResponder.create({
        onMoveShouldSetResponderCapture: () => true,
        onMoveShouldSetPanResponderCapture: () => true,
        // 设置初始位置
        onPanResponderGrant: (e, gestureState) => {
            this.state.pan.setOffset({
                x: this.state.pan.x._value,
                y: this.state.pan.y._value
            });
            this.state.pan.setValue({x: 0, y: 0});
        },
        // 使用拖拽的偏移量来定位
        onPanResponderMove: Animated.event([
            null, {dx: this.state.pan.x, dy: this.state.pan.y},
        ]),
        onPanResponderRelease: (e, {vx, vy}) => {
            this.state.pan.flattenOffset();
        }
    });
    

我们再来加上拖拽时候放大的效果

先在state里加上一个scale用来记录放大的倍数

this.state = {
    pan: new Animated.ValueXY(),
    scale: new Animated.Value(1)
};

我们要在render里使用放大的值

render(){
    // 从state中取出pan和scale
    const { pan, scale } = this.state;
    // 从pan里计算出偏移量
    const [translateX, translateY] = [pan.x, pan.y];
    // 设置transform为偏移量
    const imageStyle = {transform: [{translateX}, {translateY},  {scale}]};
    return (
        <Animated.View style={[styles.container,imageStyle]} {...this._panResponder.panHandlers}>
            <Image style={{width:80,height:80}} source={require('../assets/react-native.jpg')}/>
        </Animated.View>
    )
}

在拖拽的时候变大:

onPanResponderGrant: (e, gestureState) => {
    this.state.pan.setOffset({
        x: this.state.pan.x._value,
        y: this.state.pan.y._value
    });
    this.state.pan.setValue({x: 0, y: 0});
    Animated.spring(
        this.state.scale,
        { toValue: 1.3, friction: 3 }
    ).start();
},

在释放的时候缩小到原来的样子:

onPanResponderRelease: (e, {vx, vy}) => {
    this.state.pan.flattenOffset();
    Animated.spring(
        this.state.scale,
        { toValue: 1, friction: 3 }
    ).start();
}

加上拖拽的旋转效果:

先在state中加上一个rotate用来记录旋转的角度

this.state = {
    pan: new Animated.ValueXY(),
    scale: new Animated.Value(1),
    rotate: new Animated.Value(0)
};

在拖拽的时候旋转,需要在onPanResponderGrant里加上

Animated.timing(this.state.rotate, {
    toValue: 25, // 旋转25%,render里interpolate出deg的值
    duration: 300
}).start();

在释放的时候恢复原状,需要在onPanResponderRelease中加上

Animated.timing(this.state.rotate, {
    toValue: 0,
    duration: 300
}).start();

render函数里设置transform:

render(){
    // 从state中取出pan
    const { pan, scale } = this.state;
    // 从pan里计算出偏移量
    const [translateX, translateY] = [pan.x, pan.y];
    // 计算旋转
    const rotate = this.state.rotate.interpolate({
        inputRange: [0, 100],
        outputRange: ['0deg', '360deg']
    });
    // 设置transform为偏移量
    const imageStyle = {transform: [{translateX}, {translateY},  {scale}, {rotate}]};
    return (
        <Animated.View style={[styles.container,imageStyle]} {...this._panResponder.panHandlers}>
            <Image style={{width:80,height:80}} source={require('../assets/react-native.jpg')}/>
        </Animated.View>
    )
}

完整代码:

https://github.com/shengoo/rndd

import React, {
    Component,
} from 'react';
import {
    StyleSheet,
    Image,
    PanResponder,
    Animated,
} from 'react-native';
class Draggable extends Component{
    constructor(props){
        super(props);
        this.state = {
            pan: new Animated.ValueXY(),
            scale: new Animated.Value(1),
            rotate: new Animated.Value(0)
        };
    }
    componentWillMount() {
        this._panResponder = PanResponder.create({
            onMoveShouldSetResponderCapture: () => true,
            onMoveShouldSetPanResponderCapture: () => true,
            // 设置初始位置
            onPanResponderGrant: (e, gestureState) => {
                this.state.pan.setOffset({
                    x: this.state.pan.x._value,
                    y: this.state.pan.y._value
                });
                this.state.pan.setValue({x: 0, y: 0});
                Animated.spring(this.state.scale, {
                    toValue: 1.3,
                    friction: 3 }
                ).start();
                Animated.timing(this.state.rotate, {
                    toValue: 25,
                    duration: 300
                }).start();
            },
            // 使用拖拽的偏移量来定位
            onPanResponderMove: Animated.event([
                null, {dx: this.state.pan.x, dy: this.state.pan.y},
            ]),
            onPanResponderRelease: (e, {vx, vy}) => {
                this.state.pan.flattenOffset();
                // Animated.spring(
                //     this.state.pan,
                //     {toValue: {x: 0, y: 0}}
                // ).start();
                Animated.spring(
                    this.state.scale,
                    { toValue: 1, friction: 3 }
                ).start();
                Animated.timing(this.state.rotate, {
                    toValue: 0,
                    duration: 300
                }).start();
            }
        });
    }
    render(){
        // 从state中取出pan
        const { pan, scale } = this.state;
        // 从pan里计算出偏移量
        const [translateX, translateY] = [pan.x, pan.y];
        // 计算旋转
        const rotate = this.state.rotate.interpolate({
            inputRange: [0, 100],
            outputRange: ['0deg', '360deg']
        });
        // 设置transform为偏移量
        const imageStyle = {transform: [{translateX}, {translateY},  {scale}, {rotate}]};
        return (
            <Animated.View style={[styles.container,imageStyle]} {...this._panResponder.panHandlers}>
                <Image style={{width:80,height:80}} source={require('../assets/react-native.jpg')}/>
            </Animated.View>
        )
    }
}
export default Draggable;
const styles = StyleSheet.create({
    container: {
        position: 'absolute',
        left: 100,
        top: 100,
    }
});

iOS原生app中集成react-native组件

在iOS原生app中增加ReactNative

  1. 新建一个iOS的项目,打开XCode,新建一个项目,选择Tabbed App

  2. 运行一下看看效果

  3. 新建一个react-native项目,和iOS的项目中同一个目录下

    brew install node
    brew install watchman
    npm install -g react-native-cli
    react-native init ReactNativeProject
    
  4. 启动react-native服务器
    react-native start
    
  5. 安装CocoaPods
    brew install cocoapods
    
  6. 初始化CocoaPods,在你的iOS项目根目录里运行
    pod init
    
  7. 编辑生成的Podfile,增加react-native的pod
    # Uncomment the next line to define a global platform for your project
    # platform :ios, '9.0'
    target 'demo' do
    # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
    use_frameworks!
    # Pods for demo
    pod 'React', :path => ‘../ReactNativeProject/node_modules/react-native', :subspecs => [
    'Core',
    'CxxBridge', # Include this for RN >= 0.47
    'DevSupport', # Include this to enable In-App Devmenu if RN >= 0.43
    'RCTText',
    'RCTNetwork',
    'RCTWebSocket', # needed for debugging
    # Add any other subspecs you want to use in your project
    ]
    pod "yoga", :path => "../ReactNativeProject/node_modules/react-native/ReactCommon/yoga"
    pod 'DoubleConversion', :podspec => '../ReactNativeProject/node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
    pod 'GLog', :podspec => '../ReactNativeProject/node_modules/react-native/third-party-podspecs/GLog.podspec'
    pod 'Folly', :podspec => '../ReactNativeProject/node_modules/react-native/third-party-podspecs/Folly.podspec'
    target 'demoTests' do
    inherit! :search_paths
    # Pods for testing
    end
    target 'demoUITests' do
    inherit! :search_paths
    # Pods for testing
    end
    end
    
  8. 运行pod install安装依赖,得到以下输出:
    $ pod install
    Setting up CocoaPods master repo
    $ /usr/bin/git clone https://github.com/CocoaPods/Specs.git master --progress
    Cloning into 'master'...
    remote: Counting objects: 1799117, done.
    remote: Compressing objects: 100% (377/377), done.
    remote: Total 1799117 (delta 157), reused 35 (delta 35), pack-reused 1798692
    Receiving objects: 100% (1799117/1799117), 500.73 MiB | 320.00 KiB/s, done.
    Resolving deltas: 100% (981561/981561), done.
    Checking out files: 100% (203691/203691), done.
    Setup completed
    Analyzing dependencies
    Fetching podspec for `DoubleConversion` from `../ReactNativeProject/node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`
    Fetching podspec for `Folly` from `../ReactNativeProject/node_modules/react-native/third-party-podspecs/Folly.podspec`
    Fetching podspec for `GLog` from `../ReactNativeProject/node_modules/react-native/third-party-podspecs/GLog.podspec`
    Fetching podspec for `React` from `../ReactNativeProject/node_modules/react-native`
    Fetching podspec for `yoga` from `../ReactNativeProject/node_modules/react-native/ReactCommon/yoga`
    Downloading dependencies
    Installing DoubleConversion (1.1.5)
    Installing Folly (2016.09.26.00)
    Installing GLog (0.3.4)
    Installing React (0.51.0)
    Installing boost (1.59.0)
    Installing yoga (0.51.0.React)
    Generating Pods project
    Integrating client project
    
  9. 打开iOS项目目录下的demo.xcworkspace

  10. 在AppDelegate.swift文件中引入React

    import React
    
  11. 声明react-native组件的UIViewController并加入到tab中
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -&gt; Bool {
    // Override point for customization after application launch.
    var tab = self.window?.rootViewController as! UITabBarController
    let jsCodeLocation = URL(string: "http://localhost:8081/index.bundle?platform=ios")
    let rootView = RCTRootView(
    bundleURL: jsCodeLocation,
    moduleName: "ReactNativeProject",
    initialProperties: nil,
    launchOptions: nil
    )
    let vc = UIViewController()
    vc.view = rootView
    vc.title = "rn"
    tabbar.viewControllers?.append(vc)
    return true
    }
    
  12. 设置允许localhost的http访问

  13. 运行看看效果

代码: https://github.com/shengoo/iOS-rn-h5

Android service 自动重启

当Android的service被停止(内存不够、被其他app杀掉)的时候,加入以下代码到你的service里,就可以马上重新启动了。

@Override
public void onDestroy() {
  super.onDestroy();
  // Restart service in 500 ms
  ((AlarmManager) getSystemService(Context.ALARM_SERVICE))
  .set(AlarmManager.RTC,
    System.currentTimeMillis() + 500,
    PendingIntent.getService(this, 3, new Intent(this, TaskService.class), 0));
}