0%


一转眼来到国庆假期,核酸未停疫情还在,假期是个好好充电的日子,休息之余总结之前的工作度过一个充实的假期,但愿疫情快点过去,大家能回归到正常生活吧
上一篇写到了flutter的开发框架封装,其中用到了DioManager这个类,这个类主要是利用flutter框架提供的dio对网络层http请求做一个封装,另外由于业务需要,也封装了WebSocket请求,这里整理了一下具体的封装流程,欢迎一起交流讨论~


http请求封装

封装数据传输Bean基础类

这里以WanAndroid的Api为例

  • data是泛型T,返回的数据内容
  • errorCode是后端返回的int类型的业务状态码
  • errorMsg是错误信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    class BaseResult<T> {
    BaseResult({
    this.data,
    this.errorCode,
    this.errorMsg,
    });

    factory BaseResult.fromJson(Map<String, dynamic> json) {
    return BaseResult(
    data: json['data'],
    errorCode: json['errorCode'],
    errorMsg: json['errorMsg']);
    }

    /**
    * 不显示错误toast
    */
    bool isSuccess() {
    return errorCode == 0;
    }

    T? data;
    int? errorCode;
    String? errorMsg;

    Map<String, dynamic> toJson() {
    final map = <String, dynamic>{};
    if (data != null) {
    map['data'] = data;
    }
    map['errorCode'] = errorCode;
    map['errorMsg'] = errorMsg;
    return map;
    }
    }

    单例模式

    构造函数私有化,懒汉式构造单例
    1
    2
    3
    4
    5
    6
    7
    8
    static DioManager? instance;

    static DioManager? getInstance() {
    instance ??= DioManager._internal();
    return instance;
    }

    DioManager._internal();

    声明baseUrl和全局Header

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static String getAppUrl() {
    return "https://www.wanandroid.com";
    }

    static Map<String, dynamic> baseHeaders = {
    "X-SYSTEM": "Android",
    "LANG": "zh_CN",
    "x-app-token": "",
    "X-VERSION": ""
    };

    初始化DioManager

    dio对象全局唯一,DioManager的构造函数中需要对options进行相关配置,全局header和baseUrl以及连接超时,接收超时,发送超时,添加日志拦截器,设置数据的返回格式为Json
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Dio dio = Dio();

    DioManager() {
    dio.options.headers = baseHeaders;
    dio.options.baseUrl = getAppUrl();
    dio.options.connectTimeout = 15000;
    dio.options.receiveTimeout = 15000;
    dio.options.sendTimeout = 30000;
    // 是否开启请求日志
    if (Global.isDebug) {
    dio.interceptors.addAll([LogInterceptor()]);
    }
    dio.options.responseType = ResponseType.json;
    }

    封装get和post请求

  • post请求有表单和body两种方式,isForm为true时为表单请求默认为false
  • _request方法中统一处理,调用dio的request方法传path,data,queryParameters和options发起请求,根据post和get请求配置options的method,根据post表单和body两种请求方式配置contentType,如果是get请求携带的参数传queryParameters,post请求携带的参数传data
  • dio的request需要try-catch捕获异常,如果捕获到异常且needToast为true时通过toast展示异常code
  • 获取到data数据后,如果是string则直接反序列化,否则调用Bean基础类BaseResult的fromJson反序列化,反序列化操作需要try-catch捕获异常,如果成功直接返回结果,否则返回序列化失败,如果当前是错误状态码且needToast为true时通过toast展示toast
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    Future<BaseResult> getRequest(String url, {Map<String, dynamic>? params}) {
    return _request(
    url,
    queryParameters: params,
    options: Options(method: "GET"),
    );
    }

    Future<BaseResult> postRequest(String url,
    {dynamic params, bool isForm = false}) {
    final contentType =
    isForm ? Headers.formUrlEncodedContentType : Headers.jsonContentType;
    final options = Options(
    method: "POST",
    extra: {"dio_content_type": contentType},
    contentType: contentType);
    return _request(url, data: params, options: options);
    }

    Future<BaseResult> _request(String path,
    {data,
    Map<String, dynamic>? queryParameters,
    Options? options,
    bool needToast = true}) async {
    final Response response;
    try {
    response = await dio.request(path,
    data: data, queryParameters: queryParameters, options: options);
    } on DioError catch (error, stackTrace) {
    var checkedError = _checkError(error);
    final statusCode =
    checkedError.response?.statusCode ?? ResultCode.NO_NETWORK;
    if (needToast) {
    showToast("network_error(${statusCode.toString()})");
    }
    if (Global.isDebug) Future.error(checkedError, stackTrace);
    return BaseResult(errorCode: statusCode, errorMsg: error.message);
    }

    try {
    //防止解析失败
    var data = response.data;
    if (data is String) {
    data = json.decode(data);
    }
    var baseResult = BaseResult.fromJson(data);
    if (!baseResult.isSuccess() && needToast) {
    showToast(baseResult.errorMsg ?? "");
    }
    return baseResult;
    } catch (ex, stackTrace) {
    if (Global.isDebug) Future.error(ex, stackTrace);
    return BaseResult(
    errorCode: ResultCode.JSON_ERROR, errorMsg: ex.toString());
    }
    }

    错误码统一处理

    如果返回的DioError的response为空或statusCode为空统一处理为没有网络,其余的错误类型转换为自定义的错误码便于混合开发时统计
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    DioError _checkError(DioError error) {
    // 如果response为空构造没有网络的response
    // 如果statusCode为空直接赋值为没有网络
    /// 统一认为是没有网络
    Response errorResponse = error.response ??
    Response(
    statusCode: ResultCode.NO_NETWORK,
    requestOptions: error.requestOptions);

    errorResponse.statusCode ??= ResultCode.NO_NETWORK;

    /// 自定义请求超时错误码
    if (error.type == DioErrorType.connectTimeout) {
    errorResponse.statusCode = ResultCode.CONNECT_TIMEOUT;
    } else if (error.type == DioErrorType.receiveTimeout) {
    errorResponse.statusCode = ResultCode.RECEIVE_TIMEOUT;
    } else if (error.type == DioErrorType.sendTimeout) {
    errorResponse.statusCode = ResultCode.SEND_TIMEOUT;
    }
    error.response = errorResponse;
    return error;
    }

    class ResultCode {
    /// 业务错误
    static const BIZ_ERROR = -111;

    /// json解析异常
    static const JSON_ERROR = -100;

    /// 没有网络
    static const NO_NETWORK = -101;

    ///连接超时
    static const CONNECT_TIMEOUT = -102;

    ///It occurs when receiving timeout.
    static const RECEIVE_TIMEOUT = -103;

    ///写数据超时
    static const SEND_TIMEOUT = -104;
    }
    最后附上DioManager完整的源码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    class DioManager {
    static DioManager? instance;

    factory DioManager() {
    instance ??= DioManager();
    return instance!;
    }

    static String getAppUrl() {
    return "https://www.wanandroid.com";
    }

    static Map<String, dynamic> baseHeaders = {
    "X-SYSTEM": "Android",
    "LANG": "zh_CN",
    "x-app-token": "",
    "X-VERSION": ""
    };

    Dio dio = Dio();

    DioManager._internal() {
    dio.options.headers = baseHeaders;
    dio.options.baseUrl = getAppUrl();
    dio.options.connectTimeout = 15000;
    dio.options.receiveTimeout = 15000;
    dio.options.sendTimeout = 30000;
    // 是否开启请求日志
    if (Global.isDebug) {
    dio.interceptors.addAll([LogInterceptor()]);
    }
    dio.options.responseType = ResponseType.json;
    }

    Future<BaseResult> getRequest(String url, {Map<String, dynamic>? params}) {
    return _request(
    url,
    queryParameters: params,
    options: Options(method: "GET"),
    );
    }

    Future<BaseResult> postRequest(String url,
    {dynamic params, bool isForm = false}) {
    final contentType =
    isForm ? Headers.formUrlEncodedContentType : Headers.jsonContentType;
    final options = Options(
    method: "POST",
    extra: {"dio_content_type": contentType},
    contentType: contentType);
    return _request(url, data: params, options: options);
    }

    Future<BaseResult> _request(String path,
    {data,
    Map<String, dynamic>? queryParameters,
    Options? options,
    bool needToast = true}) async {
    final Response response;
    try {
    response = await dio.request(path,
    data: data, queryParameters: queryParameters, options: options);
    } on DioError catch (error, stackTrace) {
    var checkedError = _checkError(error);
    final statusCode =
    checkedError.response?.statusCode ?? ResultCode.NO_NETWORK;
    if (needToast) {
    showToast("network_error(${statusCode.toString()})");
    }
    if (Global.isDebug) Future.error(checkedError, stackTrace);
    return BaseResult(errorCode: statusCode, errorMsg: error.message);
    }

    try {
    //防止解析失败
    var data = response.data;
    if (data is String) {
    data = json.decode(data);
    }
    var baseResult = BaseResult.fromJson(data);
    if (!baseResult.isNotShowNetErrorToast() && needToast) {
    showToast(baseResult.errorMsg ?? "");
    }
    return baseResult;
    } catch (ex, stackTrace) {
    if (Global.isDebug) Future.error(ex, stackTrace);
    return BaseResult(
    errorCode: ResultCode.JSON_ERROR, errorMsg: ex.toString());
    }
    }

    DioError _checkError(DioError error) {
    /// 统一认为是没有网络
    // 如果response为空构造没有网络的response
    // 如果statusCode为空直接赋值为没有网络
    Response errorResponse = error.response ??
    Response(
    statusCode: ResultCode.NO_NETWORK,
    requestOptions: error.requestOptions);

    errorResponse.statusCode ??= ResultCode.NO_NETWORK;

    /// 自定义请求超时错误码
    if (error.type == DioErrorType.connectTimeout) {
    errorResponse.statusCode = ResultCode.CONNECT_TIMEOUT;
    } else if (error.type == DioErrorType.receiveTimeout) {
    errorResponse.statusCode = ResultCode.RECEIVE_TIMEOUT;
    } else if (error.type == DioErrorType.sendTimeout) {
    errorResponse.statusCode = ResultCode.SEND_TIMEOUT;
    }
    error.response = errorResponse;
    return error;
    }
    }

    /// 跟宿主定义的http网络错误码保持一致
    class ResultCode {
    /// 业务错误
    static const BIZ_ERROR = -111;

    /// json解析异常
    static const JSON_ERROR = -100;

    /// 没有网络
    static const NO_NETWORK = -101;

    ///连接超时
    static const CONNECT_TIMEOUT = -102;

    ///It occurs when receiving timeout.
    static const RECEIVE_TIMEOUT = -103;

    ///写数据超时
    static const SEND_TIMEOUT = -104;
    }

    WebSocket请求封装

    封装数据传输Bean基础类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class WsBaseBean {
    Decimal? changeRate;
    Decimal? lastTradedPrice;
    String? symbolCode;
    Decimal? volValue;

    WsBaseBean(
    {this.changeRate, this.lastTradedPrice, this.symbolCode, this.volValue});

    factory WsBaseBean.fromJson(Map<String, dynamic> json) {
    return WsBaseBean(
    changeRate: Decimal.tryParse("${json["changeRate"] ?? 0.0}"),
    lastTradedPrice: Decimal.tryParse("${json["lastTradedPrice"] ?? 0.0}"),
    symbolCode: json["symbolCode"],
    volValue: Decimal.tryParse("${json["volValue"] ?? 0.0}"));
    }
    }

    单例模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /// 单例对象
    static WebSocketUtility? _socket;

    /// 内部构造方法,可避免外部暴露构造函数,进行实例化
    WebSocketUtility._internal();

    /// 获取单例内部方法
    factory WebSocketUtility() {
    // 只能有一个实例
    _socket ??= WebSocketUtility._internal();
    return _socket!;
    }

    声明Websocket地址和连接状态

    1
    2
    3
    4
    5
    6
    7
    const String _SOCKET_URL = 'ws://192.168.3.123:8181/test';

    enum SocketStatus {
    socketStatusConnected, // 已连接
    socketStatusFailed, // 失败
    socketStatusClosed, // 连接关闭
    }

    声明变量

  • 定义回调,接连错误,接收消息,开始连接
  • 定义IOWebSocketChannel实现WebSocket通信
  • 定义心跳定时器和心跳间隔,每隔固定时长发送心跳
  • 定义重连定时器和当前重连次数和最大重连次数,未达到最大重连次数时每隔固定时长重新连接
  • 定义当前socket的连接状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    IOWebSocketChannel? _webSocket; // WebSocket
    SocketStatus? _socketStatus; // socket状态
    Timer? _heartBeat; // 心跳定时器
    final int _heartTimes = 3000; // 心跳间隔(毫秒)
    final int _reconnectMaxCount = 60; // 重连次数,默认60次
    int _reconnectTimes = 0; // 重连计数器
    Timer? _reconnectTimer; // 重连定时器
    late Function onError; // 连接错误回调
    late Function onOpen; // 开始连接回调
    late Function onMessage; // 接收消息回调

    开始连接

  • 初始化IOWebSocketChannel开始连接
  • 设置_socketStatus连接状态
  • 重置重连计数和定时器
  • 调用onOpen回调
  • 接收消息回调处理,收到消息调onMessage回调,连接错误调onError回调,连接推出时尝试重连
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /// 开启WebSocket连接
    void openSocket() {
    closeSocket();
    _webSocket = IOWebSocketChannel.connect(_SOCKET_URL);
    print('WebSocket连接成功: $_SOCKET_URL');
    // 连接成功,返回WebSocket实例
    _socketStatus = SocketStatus.socketStatusConnected;
    // 连接成功,重置重连计数器
    _reconnectTimes = 0;
    if (_reconnectTimer != null) {
    _reconnectTimer?.cancel();
    _reconnectTimer = null;
    }
    onOpen();
    // 接收消息
    _webSocket?.stream.listen((data) => onMessage(data), onError: (e) {
    WebSocketChannelException ex = e;
    _socketStatus = SocketStatus.socketStatusFailed;
    onError(ex.message);
    closeSocket();
    }, onDone: () {
    print('closed');
    reconnect();
    });
    }

    销毁心跳

    取消心跳定时器
    1
    2
    3
    4
    5
    6
    void destroyHeartBeat() {
    if (_heartBeat != null) {
    _heartBeat?.cancel();
    _heartBeat = null;
    }
    }

    初始化并开启心跳

    如果心跳定时器已经初始化则销毁,重新初始化并开启心跳发送任务
    1
    2
    3
    4
    5
    6
    void initHeartBeat() {
    destroyHeartBeat();
    _heartBeat = Timer.periodic(Duration(milliseconds: _heartTimes), (timer) {
    sendMessage('{"module": "HEART_CHECK", "message": "请求心跳"}');
    });
    }

    发送WebSocket消息

    如果当前的连接状态是已连接则发送消息,否则打印当前状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    /// 发送WebSocket消息
    void sendMessage(message) {
    if (_webSocket != null) {
    switch (_socketStatus) {
    case SocketStatus.socketStatusConnected:
    print('发送中:' + message);
    _webSocket?.sink.add(message);
    break;
    case SocketStatus.socketStatusClosed:
    print('连接已关闭');
    break;
    case SocketStatus.socketStatusFailed:
    print('发送失败');
    break;
    default:
    break;
    }
    }
    }

    重连机制

    如果当前未达到最大连接次数则重连次数+1,开启重连定时器,执行WebSocket连接,否则如果重连定时器不为空则销毁
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /// 重连机制
    void reconnect() {
    if (_reconnectTimes < _reconnectMaxCount) {
    _reconnectTimes++;
    _reconnectTimer =
    Timer.periodic(Duration(milliseconds: _heartTimes), (timer) {
    openSocket();
    });
    } else {
    if (_reconnectTimer != null) {
    print('重连次数超过最大次数');
    _reconnectTimer?.cancel();
    _reconnectTimer = null;
    }
    return;
    }
    }
    最后附上WebSocketUtility完整的源码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    /// WebSocket地址
    const String _SOCKET_URL = 'ws://192.168.3.123:8181/test';

    /// WebSocket状态
    enum SocketStatus {
    socketStatusConnected, // 已连接
    socketStatusFailed, // 失败
    socketStatusClosed, // 连接关闭
    }

    class WebSocketUtility {
    /// 单例对象
    static WebSocketUtility? _socket;

    /// 内部构造方法,可避免外部暴露构造函数,进行实例化
    WebSocketUtility._internal();

    /// 获取单例内部方法
    factory WebSocketUtility() {
    // 只能有一个实例
    _socket ??= WebSocketUtility._internal();
    return _socket!;
    }

    IOWebSocketChannel? _webSocket; // WebSocket
    SocketStatus? _socketStatus; // socket状态
    Timer? _heartBeat; // 心跳定时器
    final int _heartTimes = 3000; // 心跳间隔(毫秒)
    final int _reconnectMaxCount = 60; // 重连次数,默认60次
    int _reconnectTimes = 0; // 重连计数器
    Timer? _reconnectTimer; // 重连定时器
    late Function onError; // 连接错误回调
    late Function onOpen; // 连接开启回调
    late Function onMessage; // 接收消息回调

    /// 初始化WebSocket
    void initWebSocket(
    {required Function onOpen,
    required Function onMessage,
    required Function onError}) {
    this.onOpen = onOpen;
    this.onMessage = onMessage;
    this.onError = onError;
    openSocket();
    }

    /// 开启WebSocket连接
    void openSocket() {
    closeSocket();
    _webSocket = IOWebSocketChannel.connect(_SOCKET_URL);
    print('WebSocket连接成功: $_SOCKET_URL');
    // 连接成功,返回WebSocket实例
    _socketStatus = SocketStatus.socketStatusConnected;
    // 连接成功,重置重连计数器
    _reconnectTimes = 0;
    if (_reconnectTimer != null) {
    _reconnectTimer?.cancel();
    _reconnectTimer = null;
    }
    onOpen();
    // 接收消息
    _webSocket?.stream.listen((data) => onMessage(data), onError: (e) {
    WebSocketChannelException ex = e;
    _socketStatus = SocketStatus.socketStatusFailed;
    onError(ex.message);
    closeSocket();
    }, onDone: () {
    print('closed');
    reconnect();
    });
    }

    /// 初始化心跳
    void initHeartBeat() {
    destroyHeartBeat();
    _heartBeat = Timer.periodic(Duration(milliseconds: _heartTimes), (timer) {
    sendMessage('{"module": "HEART_CHECK", "message": "请求心跳"}');
    });
    }

    /// 销毁心跳
    void destroyHeartBeat() {
    if (_heartBeat != null) {
    _heartBeat?.cancel();
    _heartBeat = null;
    }
    }

    /// 关闭WebSocket
    void closeSocket() {
    if (_webSocket != null) {
    print('WebSocket连接关闭');
    _webSocket?.sink.close();
    destroyHeartBeat();
    _socketStatus = SocketStatus.socketStatusClosed;
    }
    }

    /// 发送WebSocket消息
    void sendMessage(message) {
    if (_webSocket != null) {
    switch (_socketStatus) {
    case SocketStatus.socketStatusConnected:
    print('发送中:' + message);
    _webSocket?.sink.add(message);
    break;
    case SocketStatus.socketStatusClosed:
    print('连接已关闭');
    break;
    case SocketStatus.socketStatusFailed:
    print('发送失败');
    break;
    default:
    break;
    }
    }
    }

    /// 重连机制
    void reconnect() {
    if (_reconnectTimes < _reconnectMaxCount) {
    _reconnectTimes++;
    _reconnectTimer =
    Timer.periodic(Duration(milliseconds: _heartTimes), (timer) {
    openSocket();
    });
    } else {
    if (_reconnectTimer != null) {
    print('重连次数超过最大次数');
    _reconnectTimer?.cancel();
    _reconnectTimer = null;
    }
    return;
    }
    }
    }

    在Widget中使用

    在initState中初始化WebSocket并在连接后开启心跳发送任务与服务端保持通信连接,在消息接收中反序列化实时刷新页面数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    class SocketTestWidget extends StatefulWidget {
    const SocketTestWidget({Key? key}) : super(key: key);

    @override
    State<SocketTestWidget> createState() => _SocketTestWidgetState();
    }

    class _SocketTestWidgetState extends State<SocketTestWidget> {
    dynamic data;

    @override
    void initState() {
    super.initState();
    WebSocketUtility().initWebSocket(onOpen: () {
    WebSocketUtility().initHeartBeat();
    }, onMessage: (data) {
    WsBaseBean wsBaseBean = WsBaseBean.fromJson(jsonDecode(data));
    setState(() {
    this.data = wsBaseBean.changeRate;
    });
    }, onError: (e) {
    print("WebSocketUtility" + e);
    });
    }

    @override
    void dispose() {
    // TODO: implement dispose
    super.dispose();
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: const Text("SocketTest"),
    ),
    body: Center(
    child: ListView(
    children: [Text('收到数据: ${data}')],
    ),
    ),
    );
    }
    }
    看下flutter客户端的控制台输出效果
    效果图

最后我们可以用python写个简单的服务端脚本测试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import asyncio
import websockets
import time
import json
import random
websocket_users = set()

# 接收客户端消息并处理
async def recv_user_msg(websocket):
while True:
recv_text = await websocket.recv()
print("recv_text:", recv_text)
changeRate = random.randint(2000, 3000) + random.random()
lastTradedPrice = random.randint(1000, 2000) + random.random()
volValue = random.randint(4000, 5000) + random.random()
recv_text = {'changeRate' : changeRate, 'lastTradedPrice' : lastTradedPrice, 'symbolCode' : "BTC-USDT", 'volValue' : volValue}
data = json.dumps(recv_text)
print(data)
response_text = data
print("response_text:", f"Server return: {data}")
await websocket.send(response_text)
time.sleep(5)

# 服务器端主逻辑
async def run(websocket, path):
while True:
try:
await recv_user_msg(websocket)
except websockets.ConnectionClosed:
print("ConnectionClosed...", path) # 链接断开
print("websocket_users old:", websocket_users)
websocket_users.remove(websocket)
print("websocket_users new:", websocket_users)
break
except websockets.InvalidState:
print("InvalidState...") # 无效状态
break
except Exception as e:
print("Exception:", e)


if __name__ == '__main__':
print("192.168.3.123:8181 websocket...")
asyncio.get_event_loop().run_until_complete(websockets.serve(run, "192.168.3.123", 8181))
asyncio.get_event_loop().run_forever()

看下服务端的控制台输出效果
效果图


  • 今年的互联网寒冬真是冰冷刺骨,高温+限电+地震+疫情让所有人都感觉到2022年活下来就好,既然疫情下的经济衰退无法避免,那就只能多准备点保暖的装备过冬了,作为菜鸟的我焦虑是无法避免的而学习可以平复心中的不安,大环境无法改变只能抓紧时间充电了,期待破局的那一天。
  • 移动端开发趋近于饱和已经很久了,传统的开发方式需要ios和Android的两端的人力,为了达到双端一致的效果少不了沟通和UI交互校对的成本,如果项目以UI交互为主利用跨平台开发是有优势的,一套代码打造各端UI一致的体验,性能接近原生,提升人效受各大企业青睐,可能这是客户端最终的归宿吧,前有facebook的React后有google的flutter,连compose都有跨平台发展的迹象,随着flutter3.0的发布已经稳定支持所有终端,作为Android开发者面对这种强大的开发框架必须要赶紧学习起来,保持新技术的学习热情,顺应互联网时代的快速发展。
  • 公司的项目中已经用上了flutter,对于二级页面采用混合式的开发方式,集成flutter到原生的项目当中,目前性能和稳定性表现都是非常不错的,由于hot reload开发效率也提升了不少,开发了一段时间后,我利用Android开发中的MVI架构思想和GetX状态管理封装了flutter的开发架构,目前集成到项目中在线上稳定运行,欢迎交流和讨论~

架构图

效果图

  • 先放上一张整体的架构图~
  • 借鉴Android的MVI架构思想,整理一下大概思路:
  1. ViewModel 会存储并公开界面要使用的状态。界面状态是经过 ViewModel 转换的应用数据
  2. 界面会向 ViewModel 发送用户事件通知
  3. ViewModel 会处理用户操作并更新状态
  4. 更新后的状态将反馈给界面呈现
  5. ViewModel的数据源是两种,本地数据源和服务端数据源,本地数据源来自于文件和数据库,服务端数据源是web服务端接口提供的数据

Repository

LocalDataSource本地存储

引入shared_preferences到pubspec.yaml

工具类封装

对shared_preferences进行封装,调用shared_preferences

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Future<bool> saveString2Sp(String key, String value) async {
final prefs = await SharedPreferences.getInstance();
return prefs.setString(key, value);
}

Future<String?> getStringFromSp(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(key);
}

Future<bool> saveBool2Sp(String key, bool value) async {
final prefs = await SharedPreferences.getInstance();
return prefs.setBool(key, value);
}

Future<bool?> getBoolFromSp(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(key);
}

Future<bool> removeSp(String key) async {
final prefs = await SharedPreferences.getInstance();
return prefs.remove(key);
}

本地存储基类

调用封装的工具类实现本地数据的操作

  • 本地数据缓存
  • 本地数据获取
  • 删除本地数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    abstract class BaseLocalDataSource {
    Future<bool> saveCache(String cacheData, String key) async {
    return saveString2Sp(key, cacheData);
    }

    Future<String?> readCache(String key) async {
    return getStringFromSp(key);
    }

    Future<bool?> deleteCache(String key) async {
    return removeSp(key);
    }
    }

    网络请求基类

    引入dio到pubspec.yaml,初始化网络请求DioManager对象供实现类使用,封装了网络请求的Dio API
    1
    2
    3
    abstract class BaseRemoteDataSource {
    final DioManager dioManager = DioManager.getInstance()!;
    }

    Repository基类

    暴露本地数据操作接口
  • 本地数据缓存
  • 本地数据获取
  • 删除本地数据
    1
    2
    3
    4
    5
    abstract class BaseRepository {
    Future<bool> saveCache(String cacheData, String key);
    Future<String?> readCache(String key);
    Future<bool?> deleteCache(String key);
    }

    Repository基础实现类

  • 需外部传入本地数据,网络请求操作对象
  • 方便外部自定义BaseLocalDataSource,替换shared_preferences,实现本地数据操作方法
  • 方便外部自定义BaseRemoteDataSource, 定义网络请求的相关接口
  • 网络请求的相关接口在外部Repository实现类中实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    abstract class BaseRepositoryImp<Local extends BaseLocalDataSource,
    Remote extends BaseRemoteDataSource> implements BaseRepository {
    final Local? localDataSource;
    final Remote remoteDataSource;

    BaseRepositoryImp({this.localDataSource, required this.remoteDataSource});

    @override
    Future<bool> saveCache(String cacheData, String key) async {
    var isSuccess = await localDataSource?.saveCache(cacheData, key);
    return isSuccess ?? false;
    }

    @override
    Future<String?> readCache(String key) async {
    return localDataSource?.readCache(key);
    }

    @override
    Future<bool?> deleteCache(String key) async {
    var isSuccess = await localDataSource?.deleteCache(key);
    return isSuccess ?? false;
    }
    }

    Binding基类

    注入repository实现类
    1
    2
    3
    4
    // 页面binding封装,注入repository
    abstract class BaseBinding<R extends BaseRepository> extends Bindings {
    R provideRepository();
    }

    封装页面基类

    继承StatelessWidget,暴露viewModel的get接口,重写build方法
    1
    2
    3
    4
    5
    6
    7
    abstract class BaseView<T extends BaseViewModel> extends StatelessWidget {
    const BaseView({Key? key}) : super(key: key);
    T get viewModel => GetInstance().find<T>();

    @override
    Widget build(BuildContext context);
    }

    封装页面基类ViewModel

  1. 引入GetX到pubspec.yaml
  2. FullLifeCycleController的生命周期
  • onInit Controller初始化
  • onReady 处理异步事件,网络请求
  • onResumed 应用可见且页面回到前台
  • onInactive 应用在前台但页面不可见
  • onPaused 应用不可见且页面到后台
  • onDetach 页面视图销毁
  • onClose 关闭流对象,动画,释放内存,数据持久化
  1. 继承FullLifeCycleController混入FullLifeCycleMixin重写生命周期方法,传入Repository的实现类定义Repository对象的具体类型,初始化数据埋点收集对象TrackPageViewHelper,通过桥调用native SDK中的API实现的,传入Repository对象,在生命周期做数据埋点,onInit和onResumed调用数据埋点的onPageShow,onInactive和onClose调用数据埋点的onPageHide
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    abstract class BaseViewModel<Repository extends BaseRepository?>
    extends FullLifeCycleController with FullLifeCycleMixin {
    final Repository? repository;

    BaseViewModel({this.repository});

    TrackPageViewHelper? trackPageViewHelper;

    String pageId() => '';

    @override
    @mustCallSuper
    void onInit() {
    if (pageId().isNotEmpty) {
    trackPageViewHelper = TrackPageViewHelper(pageId());
    trackPageViewHelper?.onPageShow();
    }

    /// viewModel的初始化工作,例如一些成员属性的初始化
    super.onInit();
    }

    @override
    void onReady() {
    /// 处理异步事件,比如网络请求
    super.onReady();
    }

    @override
    @mustCallSuper
    void onClose() {
    if (pageId().isNotEmpty) {
    trackPageViewHelper?.onPageHide();
    }
    super.onClose();
    }

    @override
    @mustCallSuper
    void onResumed() {
    if (pageId().isNotEmpty) {
    trackPageViewHelper?.onPageShow();
    }
    }

    @override
    @mustCallSuper
    void onInactive() {
    if (pageId().isNotEmpty) {
    trackPageViewHelper?.onPageHide();
    }
    }

    @override
    void onPaused() {}

    @override
    void onDetached() {}
    }

    通用页面基类

    根据当前封装的页面基类和ViewModel基类封装普通/列表/下拉刷新列表页,分别封装它们对应的UIState/Page/ViewModel

    普通页基类

    封装UIState

    UIState只定义与UI刷新相关的变量,结合GetX的Obx机制刷新使用obs变量,普通页的UIState封装内容View的显示状态,根据当前的几个状态加载中/空页面/错误页来展示页面当前View
    1
    2
    3
    4
    5
    6
    7
    // 只定义UI显示所需要的state,其他和界面显示无关的state,请放在ViewModel
    // 普通页面UIState
    abstract class BasePageUIState {
    bool isLoading = true;
    bool isEmpty = false;
    bool isError = false;
    }

    封装ViewModel

    传入UIState的实例化对象和Repository的实例化对象,继承自前面封装的基类ViewModel,暴露状态传入的接口并利用GetX的GetBuilder刷新状态视图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    abstract class BasePageViewModel<UIState extends BasePageUIState,
    Repository extends BaseRepository?> extends BaseViewModel<Repository?> {
    BasePageViewModel({Repository? repository, required this.uiState})
    : super(repository: repository);

    final UIState uiState;

    void activeStatus(
    {bool isLoading = false, bool isEmpty = false, bool isError = false}) {
    uiState.isLoading = isLoading;
    uiState.isEmpty = isEmpty;
    uiState.isError = isError;
    update(['status']);
    }
    }

    封装普通页面

  • 定义页面背景色,appBar背景色
  • 重写build方法,设置页面背景色和appBar背景色
  • 标题居中,设置左侧返回按钮,右侧操作按钮
  • 暴露标题设置,左侧/右侧按钮设置,空界面,网络错误界面,重试,点击返回,内容界面绘制接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    abstract class BasePage<T extends BasePageViewModel> extends BaseView<T> {
    const BasePage({Key? key}) : super(key: key);

    Color get backgroundColor => ColorConfig.background;

    Color get appBarBackgroundColor => ColorConfig.background;

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    backgroundColor: backgroundColor,
    resizeToAvoidBottomInset: false,
    appBar: appbar(context),
    body: body(context),
    );
    }

    @protected
    PreferredSizeWidget? appbar(BuildContext buildContext) {
    return AppBar(
    brightness: Global.isDark! ? Brightness.dark : Brightness.light,
    backgroundColor: appBarBackgroundColor,
    elevation: 0,
    centerTitle: true,
    leading: leading(buildContext),
    title: title(),
    actions: actions(),
    );
    }

    /// 返回按钮
    @protected
    Widget? leading(BuildContext context) {
    return IconButton(
    splashColor: Colors.transparent,
    highlightColor: Colors.transparent,
    icon: Image.asset(
    'resource/images/icon_back.png',
    width: 24,
    height: 24,
    color: ColorConfig.emphasis,
    ),
    onPressed: () {
    onBackClick(context);
    },
    );
    }

    /// 标题字符串
    @protected
    String titleString() => '';

    /// 标题
    @protected
    Widget? title() {
    return Container(
    padding: EdgeInsetsDirectional.only(bottom: 5),
    child: Text(
    titleString(),
    overflow: TextOverflow.ellipsis,
    style: TextStyle(
    color: ColorConfig.emphasis,
    fontSize: 18,
    ),
    textAlign: TextAlign.center,
    ),
    );
    }

    List<Widget>? actions() {
    return null;
    }

    Widget body(BuildContext context) {
    return GetBuilder<T>(
    id: 'status',
    builder: (viewModel) {
    if (viewModel.uiState.isLoading) {
    return loading();
    } else if (viewModel.uiState.isEmpty) {
    return empty();
    } else if (viewModel.uiState.isError) {
    return networkError();
    } else {
    return content(context);
    }
    });
    }

    /// 子类实现该方法,创建自己的UI
    @protected
    Widget content(BuildContext context);

    @protected
    Widget loading() {
    return const PageLoading();
    }

    @protected
    Widget empty() {
    return const Padding(
    padding: EdgeInsetsDirectional.only(
    top: 152,
    ),
    child: PageEmpty(),
    );
    }

    @protected
    Widget networkError() {
    return PageNetworkError(
    onRetry: onRetry,
    );
    }

    /// 点击重试时回调
    @protected
    void onRetry() {}

    @protected
    void onBackClick(BuildContext context) {
    if (Navigator.canPop(context)) {
    Navigator.pop(context);
    } else {
    Global.bridge.pop();
    }
    }
    }

    列表页基类

    封装列表UIState

    继承普通页的UIState增加dataList变量
    1
    2
    3
    4
    5
    6
    /**
    * 普通列表UIState
    */
    class BaseListPageUIState<T> extends BasePageUIState {
    final List<T> dataList = [];
    }

    封装列表ViewModel

    传入列表UIState实现类,列表Repository实现类,重写onReady方法调数据刷新接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    abstract class BaseListPageViewModel<UIState extends BaseListPageUIState,
    Repository extends BaseRepository>
    extends BasePageViewModel<UIState, Repository> {
    BaseListPageViewModel(
    {required Repository repository, required UIState uiState})
    : super(repository: repository, uiState: uiState);

    @override
    void onReady() {
    refreshData(isFirstLoading: true);
    super.onReady();
    }

    Future<void> refreshData({bool isFirstLoading = false});
    }

    封装列表页

    继承普通页面基类,传入列表ViewModel,重写content方法返回带下拉刷新的listview,暴露itemExtent接口设置固定的高度重写可提升性能,设置itemCount为viewModel内置UIState中的dataList的数据长度,暴露listview的item接口给外部定义视图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    abstract class BaseListPage<T extends BaseListPageViewModel>
    extends BasePage<T> {
    const BaseListPage({Key? key}) : super(key: key);

    @override
    Widget content(BuildContext context) {
    return GetBuilder<T>(
    id: 'listview',
    builder: (viewModel) {
    return EasyRefresh(
    onRefresh: () async {
    await viewModel.refreshData();
    },
    child: ListView.builder(
    scrollDirection: Axis.vertical,
    itemExtent: itemExtent(),
    itemCount: itemCount(),
    shrinkWrap: true,
    itemBuilder: (BuildContext context, int index) {
    return item(context, index);
    },
    ),
    );
    });
    }

    /// 已知item固定高度时设置itemExtent值,提升列表滚动性能
    @protected
    double? itemExtent() => null;

    @protected
    int? itemCount() => viewModel.uiState.dataList.length;

    Widget item(BuildContext context, int index);
    }

    下拉刷新列表页基类

    封装UIState

    继承列表UIState增加isEmptyList变量用于控制是否展示空列表视图
    1
    2
    3
    class BaseLoadMoreListPageUIState<T> extends BaseListPageUIState<T> {
    bool isEmptyList = false;
    }

    封装ViewModel

    继承列表ViewModel传入列表UIState实现类,列表Repository实现类,定义EasyRefreshController对象并在onInit中初始化,定义当前页数和是否有下一页变量支持分页加载,暴露加载下一页数据和刷新列表的接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    abstract class BaseLoadMoreListViewModel<
    UIState extends BaseLoadMoreListPageUIState,
    Repository extends BaseRepository>
    extends BaseListPageViewModel<UIState, Repository> {
    late final EasyRefreshController controller;
    @protected
    int currentPage = 1;
    @protected
    bool hasNext = false;

    BaseLoadMoreListViewModel(
    {required Repository repository, required UIState uiState})
    : super(repository: repository, uiState: uiState);

    @override
    void onInit() {
    controller = EasyRefreshController();
    super.onInit();
    }

    // 加载下一页数据时,后端某些分页api采用偏移量计算下一页数据;某些是currentPage自增;
    Future<void> loadNextPageData();

    @override
    void onClose() {
    controller.dispose();
    super.onClose();
    }

    void updatePageList() {
    update(['listView']);
    }
    }

    封装加载更多列表页

    继承列表页基类,传入加载更多列表的ViewModel,重写content方法,利用GetX的GetBuilder设置列表的id,设置列表的下拉刷新和加载更多回调,根据UIState的isEmptyList设置通用空白页面的显示,设置列表的通用底部视图,如果列表不为空加载下一页否则不加载数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    abstract class BaseLoadMoreListPage<T extends BaseLoadMoreListViewModel>
    extends BaseListPage<T> {
    const BaseLoadMoreListPage({Key? key}) : super(key: key);

    @override
    Widget content(BuildContext context) {
    return GetBuilder<T>(
    id: 'listView',
    builder: (viewModel) {
    return createContentWidget(viewModel);
    },
    );
    }

    @protected
    Widget createContentWidget(viewModel) {
    return EasyRefresh(
    enableControlFinishLoad: false,
    enableControlFinishRefresh: false,
    taskIndependence: true,
    topBouncing: false,
    behavior: null,
    bottomBouncing: false,
    footer: PageFooter(),
    controller: viewModel.controller,
    emptyWidget: viewModel.uiState.isEmptyList ? empty() : null,
    onRefresh: () async {
    await viewModel.refreshData();
    },
    onLoad:
    viewModel.uiState.isEmptyList || viewModel.uiState.dataList.isEmpty
    ? null
    : () async {
    await viewModel.loadNextPageData();
    },
    child: ListView.builder(
    scrollDirection: Axis.vertical,
    itemExtent: itemExtent(),
    itemCount: itemCount(),
    shrinkWrap: true,
    itemBuilder: (BuildContext context, int index) {
    return item(context, index);
    },
    ),
    );
    }

    @override
    Widget empty() {
    return const Padding(
    padding: EdgeInsetsDirectional.only(
    top: 40,
    ),
    child: PageEmpty(),
    );
    }
    }

    封装通用视图

    空界面

    通用的空界面视图,中间展示图标和文本,适配黑白模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    class PageEmpty extends StatelessWidget {
    const PageEmpty({Key? key}) : super(key: key);

    @override
    Widget build(BuildContext context) {
    return Container(
    alignment: Alignment.topCenter,
    child: Column(
    mainAxisSize: MainAxisSize.min,
    crossAxisAlignment: CrossAxisAlignment.center,
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
    Image.asset(
    _provideImagePath(),
    width: 180,
    height: 180,
    ),
    const Padding(
    padding: EdgeInsetsDirectional.only(top: 12),
    ),
    Text(
    'empty',
    style: TextStyle(
    fontSize: 14,
    color: ColorConfig.emphasis60,
    ),
    ),
    ],
    ),
    );
    }

    String _provideImagePath() {
    bool isDark = Global.isDark ?? false;
    if (isDark) {
    return "resource/images/ic_no_record_dark.png";
    } else {
    return "resource/images/ic_no_record.png";
    }
    }
    }

    列表底部视图

    通用的列表底部视图,居中展示文本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    class PageFooter extends Footer {
    final LinkFooterNotifier linkNotifier = LinkFooterNotifier();
    PageFooter({bool safeArea = true}) : super(safeArea: safeArea);

    @override
    Widget contentBuilder(
    BuildContext context,
    LoadMode loadState,
    double pulledExtent,
    double loadTriggerPullDistance,
    double loadIndicatorExtent,
    AxisDirection axisDirection,
    bool float,
    Duration? completeDuration,
    bool enableInfiniteLoad,
    bool success,
    bool noMore) {
    linkNotifier.contentBuilder(
    context,
    loadState,
    pulledExtent,
    loadTriggerPullDistance,
    loadIndicatorExtent,
    axisDirection,
    float,
    completeDuration,
    enableInfiniteLoad,
    success,
    noMore);
    return SafeArea(
    child: Container(
    height: 32,
    child: Center(
    child: Text(
    noMore ? 'no_more_data' : 'loading',
    textAlign: TextAlign.center,
    style: TextStyle(fontSize: 12, color: ColorConfig.emphasis40),
    ),
    ),
    ),
    );
    }
    }

    加载视图

    封装LoadingView

    引入Lottie到pubspec.yaml,传入文本并展示加载图标
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    class LoadingView extends StatefulWidget {
    final String tips;
    final Size size;

    const LoadingView({
    Key? key,
    this.tips = '',
    this.size = const Size.square(32),
    }) : super(key: key);

    @override
    State<LoadingView> createState() => _LoadingViewState();
    }

    class _LoadingViewState extends State<LoadingView> {
    @override
    Widget build(BuildContext context) {
    return UnconstrainedBox(
    child: Column(
    children: [
    Container(
    alignment: Alignment.center,
    width: widget.size.width,
    height: widget.size.height,
    child: Lottie.asset(
    'resource/lottie/loading.json',
    animate: true,
    ),
    ),
    widget.tips.isEmpty
    ? SizedBox()
    : Text(
    widget.tips,
    textAlign: TextAlign.center,
    style: TextStyle(
    fontSize: 12,
    fontFamily: 'URWDIN',
    fontWeight: FontWeight.w400,
    color: ColorConfig.emphasis,
    ),
    ),
    ],
    ),
    );
    }
    }
    通用的加载视图,中间展示LoadingView
    1
    2
    3
    4
    5
    6
    7
    8
    class PageLoading extends StatelessWidget {
    const PageLoading({Key? key}) : super(key: key);

    @override
    Widget build(BuildContext context) {
    return const Center(child: LoadingView(size: Size.square(50)));
    }
    }

    网络错误视图

    通用的网络错误,中间展示图标和文字暴露重试接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    class PageNetworkError extends StatelessWidget {
    final OnRetry? onRetry;

    const PageNetworkError({
    Key? key,
    this.onRetry,
    }) : super(key: key);

    @override
    Widget build(BuildContext context) {
    return GestureDetector(
    child: Container(
    alignment: Alignment.center,
    margin: EdgeInsetsDirectional.only(top: 50),
    child: Column(
    children: <Widget>[
    Image.asset(
    'resource/images/candy_net_error.png',
    width: 250,
    height: 100,
    ),
    Text(
    'network_error',
    style: TextStyle(fontSize: 14, color: ColorConfig.emphasis60),
    ),
    ],
    ),
    ),
    onTap: () => onRetry,
    );
    }
    }

    typedef OnRetry = void Function();

    应用场景

    普通页面-登录页面

    这里用wanandroid的api来举例

    登录页面LoginApi

    返回登录和注册的url
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class LoginApi {
    static LoginApi? _instance;

    LoginApi._internal();

    factory LoginApi.getInstance() {
    _instance ??= LoginApi._internal();
    return _instance!;
    }

    String getLoginUrl() {
    return "/user/login";
    }

    String getRegisterUrl() {
    return "/user/register";
    }
    }

    接口返回的bean类

    所有字段均可空,实现fromJson方法接收map实例返回实例和toJson方法返回序列化后的map对象,可以用插件JsonToDart也可以用在线转换[0][传送门]再拷贝过来
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    class LoginBean {
    LoginBean({
    this.admin,
    this.chapterTops,
    this.coinCount,
    this.collectIds,
    this.email,
    this.icon,
    this.id,
    this.nickname,
    this.password,
    this.publicName,
    this.token,
    this.type,
    this.username,
    });

    factory LoginBean.fromJson(Map<String, dynamic> json) {
    return LoginBean(
    admin: json['admin'],
    chapterTops:
    (json['chapterTops'] as List?)?.map((e) => e as String).toList(),
    coinCount: json['coinCount'],
    collectIds: (json['collectIds'] as List?)?.map((e) => e as int).toList(),
    email: json['email'],
    icon: json['icon'],
    id: json['id'],
    nickname: json['nickname'],
    password: json['password'],
    publicName: json['publicName'],
    token: json['token'],
    type: json['type'],
    username: json['username']);
    }

    bool? admin;
    List<dynamic>? chapterTops;
    int? coinCount;
    List<dynamic>? collectIds;
    String? email;
    String? icon;
    int? id;
    String? nickname;
    String? password;
    String? publicName;
    String? token;
    int? type;
    String? username;

    Map<String, dynamic> toJson() {
    final map = <String, dynamic>{};
    map['admin'] = admin;
    if (chapterTops != null) {
    map['chapterTops'] = chapterTops?.map((v) => v.toJson()).toList();
    }
    map['coinCount'] = coinCount;
    map['collectIds'] = collectIds?.map((v) => v.toJson()).toList();
    map['email'] = email;
    map['icon'] = icon;
    map['id'] = id;
    map['nickname'] = nickname;
    map['password'] = password;
    map['publicName'] = publicName;
    map['token'] = token;
    map['type'] = type;
    map['username'] = username;
    return map;
    }
    }

    定义BasePageViewModel实现类LoginLogic

    调用LoginRepository的login方法发起登录请求,onReady方法中可执行loadData执行请求任务列表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class LoginLogic extends BasePageViewModel<LoginUIState, LoginRepository> {
    LoginLogic({required super.uiState, required super.repository});

    @override
    String pageId() => LoginTrack.pageIdLogin;

    @override
    void onReady() {
    // TODO: implement onReady
    super.onReady();
    loadData();
    }

    void loadData() async {
    activeStatus(isLoading: true);
    await Future.wait<dynamic>([]);
    activeStatus(isLoading: false);
    }

    void login(String username, String password) async {
    LoginBean? loginBean = await repository?.login(username, password);
    showToast(loginBean != null ? '登录成功' : '登录失败');
    }
    }

    定义BaseBinding实现类LoginBinding

    初始化LoginRemoteDataSource实例,重写provideRepository方法注入LoginRepositoryImpl的实例和LoginUIState的实例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class LoginLogicBinding extends BaseBinding {
    @override
    void dependencies() {
    Get.lazyPut<LoginLogic>(() =>
    LoginLogic(repository: provideRepository(), uiState: LoginUIState()));
    }

    @override
    LoginRepository provideRepository() => LoginRepositoryImpl.getInstance(LoginRemoteDataSource.getInstance());
    }

    定义UIState实现类LoginUIState

    定义UI相关的变量,是否展示输入密码交互,当前是否可以登录
    1
    2
    3
    4
    class LoginUIState extends BasePageUIState {
    RxBool protect = false.obs;
    RxBool loginEnable = false.obs;
    }

    定义RemoteDataSource实现类LoginRemoteDataSource

    继承BaseRemoteDataSource,实现调用服务端login接口的方法并实现单例,利用封装的DioManager发起post请求
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class LoginRemoteDataSource extends BaseRemoteDataSource {
    static LoginRemoteDataSource? _instance;

    LoginRemoteDataSource._internal();

    factory LoginRemoteDataSource.getInstance() {
    _instance ??= LoginRemoteDataSource._internal();
    return _instance!;
    }

    Future<LoginBean?> login(String username, String password) async {
    var requestParams = {
    'username': username,
    'password': password
    };
    BaseResult result = await dioManager.postRequest(
    LoginApi.getInstance().getLoginUrl(),
    params: requestParams,
    isForm: true);
    logPrint(LoginApi.getInstance().getLoginUrl(), result);
    return result.data == null ? null : LoginBean.fromJson(result.data);
    }
    }

    定义BaseRepository子类LoginRepository

    定义登录接口
    1
    2
    3
    abstract class LoginRepository extends BaseRepository {
    Future<LoginBean?> login(String username, String password);
    }

    定义RepositoryImpl子类LoginRepositoryImpl

    实现LoginRepository接口的登录方法调用服务端数据处理对象请求接口,传入LoginRemoteDataSource服务端数据处理对象类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class LoginRepositoryImpl
    extends BaseRepositoryImp<BaseLocalDataSource, LoginRemoteDataSource>
    implements LoginRepository {
    static LoginRepositoryImpl? _instance;

    LoginRepositoryImpl._internal({required super.remoteDataSource});

    factory LoginRepositoryImpl.getInstance(
    LoginRemoteDataSource loginRemoteDataSource) {
    _instance ??=
    LoginRepositoryImpl._internal(remoteDataSource: loginRemoteDataSource);
    return _instance!;
    }

    @override
    Future<LoginBean?> login(String username, String password) => remoteDataSource.login(username, password);
    }

    定义BasePage的子类LoginPage

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    class LoginPage extends BasePage<LoginLogic> {
    LoginLogic? loginLogic;
    String? username;
    String? password;

    LoginPage({Key? key}) : super(key: key) {
    loginLogic = GetInstance().find<LoginLogic>();
    }

    @override
    String titleString() {
    return "登录";
    }

    @override
    PreferredSizeWidget? appbar(BuildContext buildContext) {
    // TODO: implement appbar
    return AppBar(
    brightness: Global.isDark! ? Brightness.dark : Brightness.light,
    backgroundColor: appBarBackgroundColor,
    elevation: 0,
    centerTitle: true,
    titleSpacing: 0,
    leading: leading(buildContext),
    title: title(),
    actions: actions(),
    );
    }

    @override
    List<Widget>? actions() {
    return [
    GestureDetector(
    child: Container(
    height: 44,
    margin: EdgeInsetsDirectional.only(start: 12, end: 12),
    padding: EdgeInsetsDirectional.only(bottom: 5),
    alignment: AlignmentDirectional.center,
    child: Image.asset('resource/images/newcomer_home_rule.png',
    width: 24, height: 24, color: ColorConfig.emphasis),
    ),
    onTap: () {
    print('explainIcon');
    },
    ),
    ];
    }

    @override
    Widget content(BuildContext context) {
    return Container(
    child: ListView(
    children: [
    Obx(() => LoginEffect(protect: viewModel.uiState.protect.value)),
    LoginInput('用户名', '请输入用户名', onChanged: (text) {
    username = text;
    checkInput();
    }),
    LoginInput(
    '密码',
    '请输入密码',
    obscureText: true,
    onChanged: (text) {
    password = text;
    checkInput();
    },
    focusChanged: (focus) {
    viewModel.uiState.protect.value = focus;
    },
    ),
    Obx(() => Padding(
    padding: EdgeInsets.only(left: 20, right: 20, top: 10),
    child: LoginButton(
    '登录',
    enable: viewModel.uiState.loginEnable.value,
    onPressed: send,
    ),
    ))
    ],
    ),
    );
    }

    void checkInput() {
    bool enable;
    if (username?.isNotEmpty == true && password?.isNotEmpty == true) {
    enable = true;
    } else {
    enable = false;
    }
    viewModel.uiState.loginEnable.value = enable;
    }

    void send() async {
    if (username != null && password != null) {
    loginLogic?.login(username!, password!);
    }
    }
    }

    封装登录按钮

    封装编辑框下面的圆角登录按钮
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class LoginButton extends StatelessWidget {
    final String title;
    final bool enable;
    final VoidCallback? onPressed;

    const LoginButton(this.title, {Key? key, this.enable = true, this.onPressed})
    : super(key: key);

    @override
    Widget build(BuildContext context) {
    return FractionallySizedBox(
    widthFactor: 1,
    child: MaterialButton(
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
    height: 45,
    onPressed: enable ? onPressed : null,
    disabledColor: ColorConfig.primary60,
    color: ColorConfig.primary,
    child: Text(title, style: TextStyle(color: Colors.white, fontSize: 16)),
    ),
    );
    }
    }

    封装登录页面顶部交互头

    传入密码输入时是否展示遮挡交互的标记
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    class LoginEffect extends StatefulWidget {
    final bool protect;

    const LoginEffect({Key? key, required this.protect}) : super(key: key);

    @override
    State<LoginEffect> createState() => _LoginEffectState();
    }

    class _LoginEffectState extends State<LoginEffect> {
    @override
    Widget build(BuildContext context) {
    return Container(
    padding: EdgeInsets.only(top: 10),
    decoration: BoxDecoration(
    color: Colors.grey[100],
    border: Border(bottom: BorderSide(color: Colors.grey[300]!)),
    ),
    child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    image(true),
    Image(
    height: 90,
    width: 90,
    image: AssetImage('resource/images/logo.png'),
    ),
    image(false)
    ],
    ),
    );
    }

    /**
    * 密码保护的交互图片
    */
    image(bool left) {
    var leftImg = widget.protect
    ? 'resource/images/head_left_protect.png'
    : 'resource/images/head_left.png';

    var rightImg = widget.protect
    ? 'resource/images/head_right_protect.png'
    : 'resource/images/head_right.png';

    return Image(height: 90, image: AssetImage(left ? leftImg : rightImg));
    }
    }

    封装登录用户名密码编辑框

    传入标题和提示文本,暴露文字/焦点改变的回调,是否有左边距,传入密码是否模糊
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    class LoginInput extends StatefulWidget {
    final String title;
    final String hint;
    final ValueChanged<String>? onChanged;
    final ValueChanged<bool>? focusChanged;
    final bool lineStretch;
    final bool obscureText;
    final TextInputType? keyboardType;

    const LoginInput(this.title, this.hint,
    {Key? key,
    this.onChanged,
    this.focusChanged,
    this.lineStretch = false,
    this.obscureText = false,
    this.keyboardType})
    : super(key: key);

    @override
    State<LoginInput> createState() => _LoginInputState();
    }

    class _LoginInputState extends State<LoginInput> {
    final focusNode = FocusNode();

    @override
    void initState() {
    super.initState();
    focusNode.addListener(() {
    print('Has focus: ${focusNode.hasFocus}');
    if (widget.focusChanged != null) {
    widget.focusChanged!(focusNode.hasFocus);
    }
    });
    }

    @override
    void dispose() {
    focusNode.dispose();
    super.dispose();
    }

    @override
    Widget build(BuildContext context) {
    return Column(
    children: [
    Row(
    children: [
    Container(
    padding: EdgeInsets.only(left: 15),
    width: 100,
    child: Text(
    widget.title,
    style: TextStyle(fontSize: 16),
    ),
    ),
    input()
    ],
    ),
    Padding(
    padding: EdgeInsets.only(left: !widget.lineStretch ? 15 : 0),
    child: Divider(
    height: 1,
    thickness: 0.5,
    )),
    ],
    );
    }

    input() {
    return Expanded(
    child: TextField(
    focusNode: focusNode,
    onChanged: widget.onChanged,
    obscureText: widget.obscureText,
    keyboardType: widget.keyboardType,
    autofocus: !widget.obscureText,
    cursorColor: ColorConfig.primary,
    style: TextStyle(fontSize: 16, fontWeight: FontWeight.w300),
    decoration: InputDecoration(
    contentPadding: EdgeInsets.only(left: 20, right: 20),
    border: InputBorder.none,
    hintText: widget.hint,
    hintStyle: TextStyle(fontSize: 15, color: Colors.grey)),
    ));
    }
    }

    加载更多列表页面-WanAndroid

    首页HomeApi

    返回获取文章列表的url
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class HomeApi {
    static HomeApi? _instance;
    HomeApi._internal();
    factory HomeApi.getInstance() {
    _instance ??= HomeApi._internal();
    return _instance!;
    }

    String getArticleList(int currentPage) {
    return '/article/list/$currentPage/json';
    }
    }

    BaseLoadMoreListPageUIState的子类HomeUIState

    暂时没用到与UI显示有关的变量
    1
    class HomeUIState extends BaseLoadMoreListPageUIState<ArticleBean> {}

    BaseBinding的实现类首页Binding

    初始化HomeRemoteDataSource的实例,重写provideRepository方法,注入HomeRepositoryImpl的实例和HomeUIState的实例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class HomeBinding extends BaseBinding{
    @override
    void dependencies() {
    Get.lazyPut<HomeLogic>(() =>
    HomeLogic(repository: provideRepository(), uiState: HomeUIState()));
    }

    @override
    HomeRepository provideRepository() => HomeRepositoryImpl.getInstance(HomeRemoteDataSource.getInstance());

    }

    BaseLoadMoreListViewModel的实现类HomeLogic

    重写loadData方法调用repository的获取文章列表接口添加到BaseListPageUIState的dataList中,刷新是否有下一页标记,刷新下一次请求的页数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class HomeLogic extends BaseLoadMoreListViewModel<HomeUIState, HomeRepository> {
    HomeLogic({required super.repository, required super.uiState});

    @override
    Future<bool> loadData(bool isRefresh) async {
    var result = await repository?.getArticleList(currentPage);
    if (result == null) {
    return false;
    }
    if (isRefresh) {
    uiState.dataList.clear();
    }
    currentPage = result.curPage ?? 0;
    if (result.articles != null) {
    var list = result.articles?.map((e) => e as ArticleBean).toList();
    uiState.dataList.addAll(list ?? []);
    }
    hasNext = result.curPage != result.pageCount;
    return true;
    }
    }

    BaseRemoteDataSource的实现类HomeRemoteDataSource

    调用dioManager对象发起get请求获取文章列表
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class HomeRemoteDataSource extends BaseRemoteDataSource {
    static HomeRemoteDataSource? _instance;
    HomeRemoteDataSource._internal();

    factory HomeRemoteDataSource.getInstance(){
    _instance ??= HomeRemoteDataSource._internal();
    return _instance!;
    }

    Future<ArticleList?> getArticleList(int currentPage) async{
    String requestUrl = HomeApi.getInstance().getArticleList(currentPage);
    BaseResult result = await dioManager.getRequest(requestUrl);
    logPrint(requestUrl, result);
    return result.data == null ? null : ArticleList.fromJson(result.data);
    }

    logPrint(String api, BaseResult result){
    if (Global.isDebug) {
    print("Login >> $api \n ${jsonEncode(result)}");
    }
    }
    }

    BaseRepository的子类HomeRepository

    定义获取文章列表接口
    1
    2
    3
    abstract class HomeRepository extends BaseRepository{
    Future<ArticleList?> getArticleList(int currentPage);
    }

    BaseRepositoryImp的子类HomeRepositoryImpl实现HomeRepository接口

    重写getArticleList方法调用服务端数据处理对象请求文章列表接口,传入HomeRemoteDataSource服务端数据处理对象类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class HomeRepositoryImpl
    extends BaseRepositoryImp<BaseLocalDataSource, HomeRemoteDataSource>
    implements HomeRepository {
    static HomeRepositoryImpl? _instance;

    HomeRepositoryImpl._internal({required super.remoteDataSource});

    factory HomeRepositoryImpl.getInstance(
    HomeRemoteDataSource homeRemoteDataSource) {
    _instance ??=
    HomeRepositoryImpl._internal(remoteDataSource: homeRemoteDataSource);
    return _instance!;
    }

    @override
    Future<ArticleList?> getArticleList(int currentPage) =>
    remoteDataSource.getArticleList(currentPage);
    }

    封装文章列表的Bean对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    class ArticleList {
    ArticleList({
    this.curPage,
    this.articles,
    this.offset,
    this.over,
    this.pageCount,
    this.size,
    this.total,
    });

    factory ArticleList.fromJson(Map<String, dynamic> json) {
    return ArticleList(
    curPage: json['curPage'],
    articles: (json['datas'] as List?)?
    .map((e) => ArticleBean.fromJson(e))
    .toList(),
    offset: json['offset'],
    over: json['over'],
    pageCount: json['pageCount'],
    size: json['size'],
    total: json['total']);
    }

    int? curPage;
    List<dynamic>? articles;
    int? offset;
    bool? over;
    int? pageCount;
    int? size;
    int? total;

    Map<String, dynamic> toJson() {
    final map = <String, dynamic>{};
    map['curPage'] = curPage;
    if (articles != null) {
    map['datas'] = articles?.map((v) => v.toJson()).toList();
    }
    map['offset'] = offset;
    map['over'] = over;
    map['pageCount'] = pageCount;
    map['size'] = size;
    map['total'] = total;
    return map;
    }
    }

    class ArticleBean {
    ArticleBean({
    this.adminAdd,
    this.apkLink,
    this.audit,
    this.author,
    this.canEdit,
    this.chapterId,
    this.chapterName,
    this.collect,
    this.courseId,
    this.desc,
    this.descMd,
    this.envelopePic,
    this.fresh,
    this.host,
    this.id,
    this.isAdminAdd,
    this.link,
    this.niceDate,
    this.niceShareDate,
    this.origin,
    this.prefix,
    this.projectLink,
    this.publishTime,
    this.realSuperChapterId,
    this.selfVisible,
    this.shareDate,
    this.shareUser,
    this.superChapterId,
    this.superChapterName,
    this.tags,
    this.title,
    this.type,
    this.userId,
    this.visible,
    this.zan,
    });

    factory ArticleBean.fromJson(Map<String, dynamic> json) {
    return ArticleBean(
    adminAdd: json['adminAdd'],
    apkLink: json['apkLink'],
    audit: json['audit'],
    author: json['author'],
    canEdit: json['canEdit'],
    chapterId: json['chapterId'],
    chapterName: json['chapterName'],
    collect: json['collect'],
    courseId: json['courseId'],
    desc: json['desc'],
    descMd: json['descMd'],
    envelopePic: json['envelopePic'],
    fresh: json['fresh'],
    host: json['host'],
    id: json['id'],
    isAdminAdd: json['isAdminAdd'],
    link: json['link'],
    niceDate: json['niceDate'],
    niceShareDate: json['niceShareDate'],
    origin: json['origin'],
    prefix: json['prefix'],
    projectLink: json['projectLink'],
    publishTime: json['publishTime'],
    realSuperChapterId: json['realSuperChapterId'],
    selfVisible: json['selfVisible'],
    shareDate: json['shareDate'],
    shareUser: json['shareUser'],
    superChapterId: json['superChapterId'],
    superChapterName: json['superChapterName'],
    tags: (json['tags'] as List?)?.map((e) => Tag.fromJson(e)).toList(),
    title: json['title'],
    type: json['type'],
    userId: json['userId'],
    visible: json['visible'],
    zan: json['zan']);
    }

    bool? adminAdd;
    String? apkLink;
    int? audit;
    String? author;
    bool? canEdit;
    int? chapterId;
    String? chapterName;
    bool? collect;
    int? courseId;
    String? desc;
    String? descMd;
    String? envelopePic;
    bool? fresh;
    String? host;
    int? id;
    bool? isAdminAdd;
    String? link;
    String? niceDate;
    String? niceShareDate;
    String? origin;
    String? prefix;
    String? projectLink;
    int? publishTime;
    int? realSuperChapterId;
    int? selfVisible;
    dynamic shareDate;
    String? shareUser;
    int? superChapterId;
    String? superChapterName;
    List<dynamic>? tags;
    String? title;
    int? type;
    int? userId;
    int? visible;
    int? zan;

    Map<String, dynamic> toJson() {
    final map = <String, dynamic>{};
    map['adminAdd'] = adminAdd;
    map['apkLink'] = apkLink;
    map['audit'] = audit;
    map['author'] = author;
    map['canEdit'] = canEdit;
    map['chapterId'] = chapterId;
    map['chapterName'] = chapterName;
    map['collect'] = collect;
    map['courseId'] = courseId;
    map['desc'] = desc;
    map['descMd'] = descMd;
    map['envelopePic'] = envelopePic;
    map['fresh'] = fresh;
    map['host'] = host;
    map['id'] = id;
    map['isAdminAdd'] = isAdminAdd;
    map['link'] = link;
    map['niceDate'] = niceDate;
    map['niceShareDate'] = niceShareDate;
    map['origin'] = origin;
    map['prefix'] = prefix;
    map['projectLink'] = projectLink;
    map['publishTime'] = publishTime;
    map['realSuperChapterId'] = realSuperChapterId;
    map['selfVisible'] = selfVisible;
    map['shareDate'] = shareDate;
    map['shareUser'] = shareUser;
    map['superChapterId'] = superChapterId;
    map['superChapterName'] = superChapterName;
    if (tags != null) {
    map['tags'] = tags?.map((v) => v.toJson()).toList();
    }
    map['title'] = title;
    map['type'] = type;
    map['userId'] = userId;
    map['visible'] = visible;
    map['zan'] = zan;
    return map;
    }
    }

    class Tag {
    Tag({
    this.name,
    this.url,
    });

    factory Tag.fromJson(Map<String, dynamic> json) {
    return Tag(name: json['name'], url: json['url']);
    }

    String? name;
    String? url;

    Map<String, dynamic> toJson() {
    final map = <String, dynamic>{};
    map['name'] = name;
    map['url'] = url;
    return map;
    }
    }

    BasePage的实现类HomePage

    重写首页展示标题,重写item方法返回列表项视图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class HomePage extends BaseLoadMoreListPage<HomeLogic> {
    HomeLogic? homeLogic;

    HomePage({Key? key}) : super(key: key) {
    homeLogic = GetInstance().find<HomeLogic>();
    }

    @override
    String titleString() {
    return '首页';
    }

    @override
    Widget item(BuildContext context, int index) {
    return ArticleItemView(
    articleBean: viewModel.uiState.dataList[index],
    onClickCollect: (collect) {
    print('click collect');
    });
    }
    }

    封装文章列表项

    外部Widget传入ViewModel中的UIState中的dataList中的某条ArticleBean数据项,传入回调方法onClickCollect,定义collect通过GetX的Obx机制结合obs变量刷新局部视图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    class ArticleItemView extends StatefulWidget {
    final ArticleBean articleBean;
    final OnClickCollect onClickCollect;
    RxBool collect = false.obs;

    ArticleItemView({
    Key? key,
    required this.articleBean,
    required this.onClickCollect,
    });

    @override
    State<ArticleItemView> createState() => _ArticleItemViewState();
    }

    class _ArticleItemViewState extends State<ArticleItemView> {
    void itemClick(ArticleBean articleBean) {}

    void itemCollect(ArticleBean articleBean) {}

    @override
    Widget build(BuildContext context) {
    String authorTitle;
    String author;

    if (widget.articleBean.author == null ||
    widget.articleBean.author!.isEmpty) {
    authorTitle = "分享人:";
    author = widget.articleBean.shareUser ?? "";
    } else {
    authorTitle = "作者:";
    author = widget.articleBean.author ?? "";
    }
    Row row = Row(
    mainAxisAlignment: MainAxisAlignment.start,
    children: <Widget>[
    Expanded(
    child: Text(
    "$authorTitle$author",
    style: TextStyle(color: ColorConfig.emphasis, fontSize: 13),
    )),
    Text(
    widget.articleBean.niceDate ?? "",
    style: TextStyle(color: ColorConfig.emphasis, fontSize: 13),
    ),
    ],
    );
    Row title = Row(
    children: <Widget>[
    Expanded(
    child: Text.rich(
    TextSpan(text: widget.articleBean.title ?? ""),
    softWrap: true,
    style: TextStyle(fontSize: 16.0, color: ColorConfig.emphasis),
    textAlign: TextAlign.left,
    )),
    ],
    );
    Row chapterName = Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
    Container(
    padding: EdgeInsets.only(left: 4, right: 4, top: 1, bottom: 2),
    decoration: BoxDecoration(
    color: ColorConfig.darkOverLay,
    borderRadius: BorderRadius.circular(2)),
    child: Text(
    widget.articleBean.chapterName ?? "",
    softWrap: true,
    style: TextStyle(color: ColorConfig.emphasis60, fontSize: 12),
    textAlign: TextAlign.left,
    ),
    ),
    Obx(() => GestureDetector(
    child: Icon(
    widget.collect.value ? Icons.favorite : Icons.favorite_border,
    color: widget.collect.value ? Colors.red : null,
    ),
    onTap: () {
    widget.articleBean.collect =
    !widget.articleBean.collect.notNull();
    widget.collect.value = widget.articleBean.collect.notNull();
    widget.onClickCollect
    .call(widget.articleBean.collect.notNull());
    },
    ))
    ],
    );

    Column column = Column(
    children: [
    title,
    SizedBox(
    height: 5,
    ),
    row,
    SizedBox(
    height: 5,
    ),
    chapterName
    ],
    );

    return InkWell(
    onTap: () {
    itemClick(widget.articleBean);
    },
    child: Container(
    padding: EdgeInsets.symmetric(vertical: 8, horizontal: 12),
    decoration: BoxDecoration(
    color: Colors.white,
    borderRadius: BorderRadius.circular(4),
    ),
    margin: EdgeInsets.all(4),
    child: column,
    ));
    }
    }

    typedef OnClickCollect = void Function(bool collect);


又是很长一段时间没有更新blog,最近疫情又严重了,公司开启了WFH模式,闲暇之余有更多的时间来做技术总结和沉淀,最近在项目中遇到一个需求,绘制一个六位的密码输入框,需要用到自定义View的相关知识,中间还出了一点小插曲,在某些机型引发了一个bug在此做一个记录,欢迎交流和讨论~


最终效果图:
效果图

自定义LinearLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class PassWordView extends LinearLayout {

public PassWordView(@NonNull Context context) {
super(context);
init();
}

public PassWordView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public PassWordView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init(){

}
}

添加EditText和TextView

  • 在LinearLayout中添加1个EditText在底部,EditText的输入格式只能是数字或密码且输入长度为6,当前LinearLayout点击后触发EditText获取焦点并弹出软键盘
  • 监听输入过程把输入的文本替换为”●”
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    private int defaultSize = 6;
    private EditText editText;

    // 清空当前输入的密码
    public void clearPassWord() {
    editText.setText(null);
    }

    @SuppressLint("CheckResult")
    private void init() {
    editText = new EditText(getContext());
    editText.setTextSize(12f);
    editText.setLayoutParams(new FrameLayout.LayoutParams(1, ViewGroup.LayoutParams.WRAP_CONTENT));
    editText.setCursorVisible(false);
    editText.setBackgroundDrawable(null);
    editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
    addView(editText);
    editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(defaultSize)});
    setOnClickListener(v -> {
    if (editText.requestFocus()) {
    InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    if (imm != null) {
    imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
    }
    }
    });

    editText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
    TextView childAt = (TextView) getChildAt(start + 1); // 获取当前聚焦的TextView
    if (before == 0) {
    childAt.setText("●");
    } else {
    childAt.setText("");
    }
    }

    @Override
    public void afterTextChanged(Editable s) {

    }
    });
    }

    // 获取当前密码
    public String getPwd() {
    return editText.getText().toString();
    }


    public void setFocusShape(boolean showFocusShape) {
    this.showFocusShape = showFocusShape;
    }

    添加TextView

  • 添加6个正方形的TextView在顶部,来实现我们的六位密码框输入布局,自定义TextView在onMeasure中设置宽等于高
  • 定义单个密码框之间间隔为6,第一个密码框marginStart为0,marginEnd为5/6 * space,第二个密码框marginStart为1/6 * space,marginEnd为4/6 * space,第三个密码框marginStart为2/6 * space,marginEnd为3/6 * space,前一个密码框的marginEnd + marginStart = space,所以marginStart = space * i / defaultSize,marginEnd = space * (defaultSize - 1 - i) / defaultSize
  • 设置当前输入的密码框聚焦背景

    小插曲

    这里最开始的实现思路有问题,把TextView的weight设置为并动态添加,对当前LinearLayout的onMeasure方法做了处理,获取每个TextView的宽度再设置其高度导致应用到底部弹窗布局时,小米机型上由于TextView的高度做了调整比调整前的高度更高,使得软件盘顶上去的高度不够,下面的文本被遮挡。但是在华为等机型上又表现正常,可能跟Rom的处理有关(这些机型上又自动刷新了一次)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    private boolean showFocusShape = true; // 是否设置聚焦背景
    @SuppressLint("CheckResult")
    private void init(){
    ...
    double space = SizeUtils.dp2px(6);
    for (int i = 0; i < defaultSize; i++) {
    TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.single_pwd_editext, null);
    LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1);
    layoutParams.setMarginStart((int) (space * i / defaultSize));
    layoutParams.setMarginEnd((int) (space * (defaultSize - i - 1) / defaultSize));
    addView(textView, layoutParams);
    }
    editText.setOnFocusChangeListener((v, hasFocus) -> {
    if (!showFocusShape) {
    return;
    }
    for (int i = 1; i < getChildCount(); i++) {
    getChildAt(i).setBackgroundResource(hasFocus ? R.drawable.shape_editext_border_green_2r : R.drawable.shape_edittext_border_background_with_error);
    }
    });
    ...
    }

    public static class SquareTextView extends AppCompatTextView {

    public SquareTextView(Context context) {
    super(context);
    }

    public SquareTextView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    if (widthMode == MeasureSpec.UNSPECIFIED) {
    throw new RuntimeException("mode is UNSPECIFIED");
    } else {
    setMeasuredDimension(widthSize, widthSize);
    }
    }
    }
  • shape_editext_border_green_2r.xml
    1
    2
    3
    4
    5
    6
    7
    8
    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/emphasis4" />
    <stroke
    android:width="@dimen/line_width"
    android:color="@color/primary" />
    <corners android:radius="2dp" />
    </shape>
  • shape_edittext_border_background_with_error.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_activated="true">
    <shape>
    <solid android:color="@color/emphasis4" />
    <stroke android:width="@dimen/line_width" android:color="@color/secondary" />
    <corners android:radius="6dp" />
    </shape>
    </item>
    <item android:state_activated="false">
    <shape>
    <solid android:color="@color/emphasis4" />
    <corners android:radius="6dp" />
    </shape>
    </item>
    </selector>
  • single_pwd_editext中引用自定义TextView
    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="utf-8"?>
    <view class="com.resources.widget.PassWordView$SquareTextView" xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/shape_edittext_border_background_with_error"
    android:gravity="center"
    android:textColor="@color/c_text60" />

    定义回调返回密码输入完成的数据

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    private PasswordCallback passwordCallback;
    interface PassWordCallback {
    void complete(String password)
    }

    public void setPasswordCallback(PasswordCallback passwordCallback) {
    this.passwordCallback = passwordCallback;
    }

    @SuppressLint("CheckResult")
    private void init(){
    ...
    editText.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {

    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
    TextView childAt = (TextView) getChildAt(start + 1);
    if (before == 0) { // 新增情况下
    childAt.setText("●");
    } else {
    childAt.setText("");
    }
    if (!TextUtils.isEmpty(s) && s.length() == defaultSize) {
    if (passwordCallback != null) {
    try {
    passwordCallback.complete(s.toString());
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }
    }

    @Override
    public void afterTextChanged(Editable s) {

    }
    });
    ...
    }

最后附上完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
public class PassWordView extends LinearLayout {

private int defaultSize = 6;
private PasswordCallback passwordCallback;
interface PassWordCallback {
void complete(String password)
}

public void setPasswordCallback(PasswordCallback passwordCallback) {
this.passwordCallback = passwordCallback;
}
private boolean showFocusShape = true;
private EditText editText;

public PassWordView(@NonNull Context context) {
super(context);
init();
}

public PassWordView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public PassWordView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

public void clearPassWord() {
editText.setText(null);
}

@SuppressLint("CheckResult")
private void init() {
editText = new EditText(getContext());
editText.setTextSize(12f);
editText.setLayoutParams(new FrameLayout.LayoutParams(1, ViewGroup.LayoutParams.WRAP_CONTENT));
editText.setCursorVisible(false);
editText.setBackgroundDrawable(null);
editText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);
addView(editText);
editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(defaultSize)});
setOnClickListener(v -> {
if (editText.requestFocus()) {
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) {
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
}
}
});

double space = SizeUtils.dp2px(6);
for (int i = 0; i < defaultSize; i++) {
TextView textView = (TextView) LayoutInflater.from(getContext()).inflate(R.layout.single_pwd_editext, null);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1);
layoutParams.setMarginStart((int) (space * i / defaultSize));
layoutParams.setMarginEnd((int) (space * (defaultSize - i - 1) / defaultSize));
addView(textView, layoutParams);
}
editText.setOnFocusChangeListener((v, hasFocus) -> {
if (!showFocusShape) {
return;
}
for (int i = 1; i < getChildCount(); i++) {
getChildAt(i).setBackgroundResource(hasFocus ? R.drawable.shape_editext_border_green_2r : R.drawable.shape_edittext_border_background_with_error);
}
});
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {

}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
TextView childAt = (TextView) getChildAt(start + 1);
if (before == 0) {
childAt.setText("●");
} else {
childAt.setText("");
}
if (!TextUtils.isEmpty(s) && s.length() == defaultSize) {
if (passwordCallback != null) {
try {
passwordCallback.complete(s.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

@Override
public void afterTextChanged(Editable s) {

}
});
}

public String getPwd() {
return editText.getText().toString();
}


public void setFocusShape(boolean showFocusShape) {
this.showFocusShape = showFocusShape;
}

public static class SquareTextView extends AppCompatTextView {

public SquareTextView(Context context) {
super(context);
}

public SquareTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthMode == MeasureSpec.UNSPECIFIED) {
throw new RuntimeException("mode is UNSPECIFIED");
} else {
setMeasuredDimension(widthSize, widthSize);
}
}
}
}

在xml中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/c_large"
android:paddingStart="16dp"
android:paddingTop="12dp"
android:paddingEnd="16dp">

<TextView
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/input_pwd"
android:textColor="@color/c_text"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/iv_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingBottom="12dp"
android:src="@mipmap/icon_close"
app:tint="@color/emphasis60"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<com.resources.widget.PassWordView
android:id="@+id/password_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/tv1"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv1"
app:layout_goneMarginTop="20dp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/password_view"
android:paddingTop="10dp"
android:paddingBottom="12dp"
android:text="@string/tips_input"
android:textColor="@color/secondary"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password_view" />

<TextView
android:id="@+id/tv_forget_pwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/password_view"
android:paddingTop="10dp"
android:paddingBottom="12dp"
android:text="@string/forget_pwd"
android:textColor="@color/c_text60"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/password_view" />

</androidx.constraintlayout.widget.ConstraintLayout>


又是更新blog的一天,最近项目中遇到一个需求,绘制一个渐变色的仪表盘去展示某个行情当前的热度,为用户提供当前市场热度更直观的一个大概参考,需要用到自定义View的相关知识,在这里做一个总结,欢迎一起学习和讨论~


最终效果图:
效果图

变量声明

  • 声明画笔,颜色,刻度数,属性动画,进度条宽度,表盘矩形区域,文本大小,必要的偏移量,表盘的开始和过渡角度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    var progressColor = intArrayOf() // 渐变色开始颜色
    var progressBackgroundColor = R.color.emphasis16 // 进度条背景颜色
    var textColor = R.color.primary // 文字颜色
    var tickScaleColor = R.color.emphasis8 // 普通刻度线颜色
    var groupScaleColor = R.color.emphasis38 // 分组刻度线颜色
    var progressStrokeWidth = 24f // 进度条宽度
    var paintProgressBackground = Paint() // 进度条背景画笔
    private var paintProgress = Paint() // 进度条画笔
    var paintText = Paint() // 文字画笔
    private var paintNum = Paint() // 刻度画笔
    var rect = RectF() // 表盘矩形区域
    private var viewWidth = 0 // 宽度
    private var viewHeight = 0 // 高度
    var percent = 0f // 百分比
    var oldPercent = 0f // 过去的百分比
    var textSize = 100f // 文本大小
    private var valueAnimator: ValueAnimator? = null // 属性动画
    var animatorDuration = 0L // 动画时长
    private var groupNum = 5 // 分组数
    private var ticksNum = 6 // 每组刻度数
    var pointerWidth = 15f // 指针的宽度
    private var ticksCount = groupNum * ticksNum + 1// 总刻度数

    companion object {
    var OFFSET = 30f // 偏移量
    var START_ARC = 150f // 开始角度
    var DURING_ARC = 240f // 过渡调度
    }

初始化逻辑

  • 获取xml的配置进行变量初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    init {
    setLayerType(LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.dashboard)
    progressBackgroundColor = typedArray.getColor(
    R.styleable.dashboard_progressBackgroundColor,
    ContextCompat.getColor(context, progressBackgroundColor)
    )
    progressStrokeWidth = typedArray.getDimension(
    R.styleable.dashboard_progressStrokeWidth,
    progressStrokeWidth
    )

    textSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    textSize
    )
    textColor = typedArray.getColor(R.styleable.dashboard_textColor, textColor)
    tickScaleColor =
    typedArray.getColor(R.styleable.dashboard_tickScaleColor, tickScaleColor)
    groupScaleColor =
    typedArray.getColor(R.styleable.dashboard_groupScaleColor, groupScaleColor)
    groupNum = typedArray.getInt(R.styleable.dashboard_groupNum, groupNum)
    ticksNum = typedArray.getInt(R.styleable.dashboard_ticksNum, ticksNum)
    pointerWidth =
    typedArray.getDimension(R.styleable.dashboard_pointerWidth, pointerWidth)
    val colorsId = typedArray.getResourceId(R.styleable.dashboard_progressColors, 0)
    progressColor =typedArray.resources.getIntArray(colorsId)
    ticksCount = groupNum * ticksNum + 1
    typedArray.recycle()
    OFFSET = progressStrokeWidth + 10f
    initPaint()
    }

    /**
    * 初始化画笔
    */
    private fun initPaint() {
    paintProgressBackground.isAntiAlias = true
    paintProgressBackground.strokeWidth = progressStrokeWidth
    paintProgressBackground.style = Paint.Style.STROKE
    paintProgressBackground.color = progressBackgroundColor
    paintProgressBackground.isDither = true
    paintProgress.isAntiAlias = true
    paintProgress.strokeWidth = progressStrokeWidth
    paintProgress.style = Paint.Style.STROKE
    paintProgress.isDither = true
    paintText.isAntiAlias = true
    paintText.color = textColor
    paintText.strokeWidth = 1F
    paintText.style = Paint.Style.FILL
    paintText.isDither = true
    paintNum.isAntiAlias = true
    paintNum.strokeWidth = 3f
    paintNum.style = Paint.Style.FILL
    paintNum.isDither = true
    }

    声明xml配置属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <declare-styleable name="dashboard">
    <attr name="progressStrokeWidth" format="dimension" />
    <attr name="progressBackgroundColor" format="color" />
    <attr name="textColor" format="color" />
    <attr name="tickScaleColor" format="color" />
    <attr name="groupScaleColor" format="color" />
    <attr name="groupNum" format="integer" />
    <attr name="ticksNum" format="integer" />
    <attr name="pointerWidth" format="dimension" />
    <attr name="textSize" format="dimension" />
    <attr name="progressColors" format="reference"/>
    </declare-styleable>

    获取自定义View宽高,初始化渐变进度条

    回顾一下View的生命周期

    1
    activity.onCreate -> onFinishInflate -> activity.onStart -> activity.onResume -> onAttachedToWindow -> onWindowVisibilityChanged -> onVisibilityChanged -> onMeasure -> onSizeChanged -> onLayout -> onDraw -> onWindowFocusChanged -> activity.onPause -> onWindowFocusChanged -> onWindowVisibilityChanged -> activity.onStop -> onVisibilityChanged -> activity.onDestroy -> onDetachedFromWindow
  • 重写onSizeChanged,此时自定义View已经完成测量可以拿到当前自定义View的宽高
  • 初始化指针宽度,当前变盘的矩形区域,以0,0点为原点,左边上角的坐标,x为-(View宽度 / 2)+ 偏移量 + paddingLeft,y为- (view高度 / 2) + 偏移量 + paddingTop,右下角的坐标,x为(View宽度 / 2)- 偏移量 - paddingRight,y为(view高度 / 2) - 偏移量 - paddingBottom,渐变色采用以原点为中心的90度渐变,初始化Shader的子类SweepGradient并设置一个以原点为中心的90度旋转操作后的矩阵,再设置这个Shader为画笔的Shader
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    viewWidth = width
    viewHeight = height
    pointerWidth = (viewWidth / 40).toFloat()
    initShader()
    }

    /**
    * 初始化渐变颜色
    */
    private fun initShader() {
    rect.set(
    (-viewWidth / 2) + OFFSET + paddingLeft, paddingTop - (viewHeight / 2) + OFFSET,
    (viewWidth / 2) - paddingRight - OFFSET,
    (viewWidth / 2) - paddingBottom - OFFSET
    )
    val shader = SweepGradient(
    0f,
    0f,
    progressColor,
    null
    )
    val rotate = 90f
    val gradientMatrix = Matrix()
    gradientMatrix.preRotate(rotate, 0f, 0f)
    shader.setLocalMatrix(gradientMatrix)
    paintProgress.shader = shader
    }

    测量View的宽高

  • 根据当前的模式和大小决定View大小,如果是明确指明宽高的大小就用当前声明的值,否则就设置一个固定值使得宽高一致防止自定义View形变
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val realWidth = startMeasure(widthMeasureSpec)
    val realHeight = startMeasure(heightMeasureSpec)
    setMeasuredDimension(realWidth, realHeight)
    }

    fun startMeasure(msSpec: Int): Int {
    val mode = MeasureSpec.getMode(msSpec)
    val size = MeasureSpec.getSize(msSpec)
    return if (mode == MeasureSpec.EXACTLY) {
    size
    } else {
    Util.dp2px(200)
    }
    }

    重写绘制方法

  • 当前的仪表盘的绘制逻辑分为绘制表盘刻度,绘制进度条,绘制文本,绘制指针,canvas的原点为左上角,为了方便计算在绘制前需要把原点移动到canvas的中心,所以x方向平移(View宽度 / 2),y方向平移(View高度 / 2)
    1
    2
    3
    4
    5
    6
    7
    8
    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    canvas?.translate((viewWidth / 2).toFloat(), (viewHeight / 2).toFloat())
    drawPanel(canvas)
    drawProgress(canvas, percent)
    drawText(canvas, percent)
    drawPointer(canvas, percent)
    }

    绘制表盘刻度

  • 以0,0点为圆心,绘制起始点逆时针旋转-120度,计算刻度的y坐标为 -(View高度 / 2) + 偏移量 + 进度条宽度,计算每个刻度的旋转角度为过渡角度 / (总刻度数 - 1),再以总刻度数进行循环绘制刻度线条,判断当前下标是否为新一组开始刻度的位置,区分普通刻度和分割刻度并设置不同的颜色
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /**
    * 绘制刻度
    * @param canvas Canvas?
    */
    fun drawPanel(canvas: Canvas?) {
    canvas?.save()
    canvas?.rotate(-(180 - START_ARC + 90), 0f, 0f)
    val numY = -viewHeight / 2 + OFFSET + progressStrokeWidth
    val angle = DURING_ARC / ((ticksCount - 1) * 1.0f)
    for (i in 0 until ticksCount) {
    canvas?.save()
    canvas?.rotate(angle * i, 0f, 0f)
    if (i == 0 || i % groupNum == 0) {
    paintNum.color = groupScaleColor
    } else {
    paintNum.color = tickScaleColor
    }
    canvas?.drawLine(0f, numY + 2, 0f, numY + (pointerWidth * 2) + 5, paintNum)
    canvas?.restore()
    }
    canvas?.restore()
    }

    绘制进度条

  • 先绘制背景进度条,绘制一个圆弧,绘制区域为表盘矩形区域,设置开始角度和过渡角度
  • 绘制进度条,判断当前的进度百分比如果大于1则为1,过滤掉其他异常情况,如果百分比大于0绘制一个渐变色圆弧,根据当前百分比和过渡角度计算当前需要绘制的渐变色过渡角度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    /**
    * 绘制进度
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawProgress(canvas: Canvas?, percent: Float) {
    canvas?.drawArc(rect, START_ARC, DURING_ARC, false, paintProgressBackground)
    var curPercent = percent
    if (curPercent > 1.0f) {
    curPercent = 1.0f
    }
    if (curPercent > 0.0f) {
    canvas?.drawArc(rect, START_ARC, percent * DURING_ARC, false, paintProgress)
    }
    }

    绘制文本

  • 根据当前的进度百分比设置文本的内容和颜色,根据当前的View宽度计算一个Y方向的偏移量,如果百分比小于0.2展示“Very Cold”文本,渐变色数组中取第一个颜色,绘制文本的x坐标为文本宽度/2,第一个词y坐标为1.4倍偏移量,第二个词为2.4倍偏移量,如果百分比大于等于0.2并且小于0.4展示“Cold”文本,y坐标为2.2倍偏移量,如果百分比大于等于0.4并且小于0.6展示“Normal”文本,y坐标为2.2倍偏移量,如果百分比大于等于0.6并且小于0.8展示“Hot”文本,y坐标为2.2倍偏移量,如果是剩下的情况展示“Very Hot”文本,第一个词y坐标为1.4倍偏移量,第二个词为2.4倍偏移量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    /**
    * 绘制文本
    * @param canvas Canvas?
    * @param percent Float
    */
    @SuppressLint("RestrictedApi")
    fun drawText(canvas: Canvas?, percent: Float) {
    val offsetY = viewWidth / 8
    paintText.textSize = textSize
    if (percent < 0.2f) {
    val subTitle1 = "Very"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Cold"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    } else if (percent >= 0.2f && percent < 0.4f) {
    val subTitle = "Cold"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.4f && percent < 0.6f) {
    val subTitle = "Normal"
    paintText.color = progressColor[2]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.6f && percent < 0.8f) {
    val subTitle = "Hot"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else {
    val subTitle1 = "Very"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Hot"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    }

    paintText.textSize = (textSize * 1.3).toFloat()
    val text = (percent * 100).toInt().toString()
    canvas?.drawText(
    text,
    -paintText.measureText(text) / 2,
    - textSize / 5f,
    paintText
    )
    }

    绘制指针

  • 绘制一个三角形的指针,根据当前的百分比计算指针所在位置的旋转角度,再计算指针所在位置需要绘制三角形的路径,从(0, view的高度/2 - 偏移量 - 进度条宽度)为起点,绘制一个倒三角形,绘制右边的线段和左边的线段再绘制顶部的线段,以垂直向下为0度,旋转范围是-300到-60并设置填充模式绘制这条路径
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    /**
    * 绘制指针
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawPointer(canvas: Canvas?, percent: Float) {
    canvas?.save()
    val angle = DURING_ARC * (percent - 0.5f) - 180
    canvas?.rotate(angle, 0f, 0f)
    val pointer = Path()
    pointer.moveTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.lineTo(
    pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(
    -pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.close()
    pointer.fillType = Path.FillType.EVEN_ODD
    canvas?.drawPath(pointer, paintText)
    canvas?.restore()
    }

设置百分比和动画

  • 传入当前的百分比范围0~1,初始化属性动画设置属性值变化监听刷新当前百分比,根据上一次的百分比和当前百分比计算动画时间,设置动画开始结束的百分比数值,设置动画结束监听刷新上一次百分比数值并做边界判断。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    * 设置当前百分比
    * @param curPercent Float 范围0~1
    */
    fun setProgress(curPercent: Float) {
    if (valueAnimator?.isRunning == true) {
    valueAnimator?.cancel()
    }
    animatorDuration = (abs(curPercent - oldPercent) * 20).toLong()
    valueAnimator = ValueAnimator.ofFloat(oldPercent, curPercent).setDuration(animatorDuration)
    valueAnimator?.addUpdateListener {
    percent = it.animatedValue as Float
    invalidate()
    }
    valueAnimator?.interpolator = LinearInterpolator()
    valueAnimator?.addListener(onEnd = {
    oldPercent = curPercent
    if (percent < 0.0f) {
    percent = 0.0f
    invalidate()
    }
    if (percent > 1f) {
    percent = 1f
    invalidate()
    }
    })
    valueAnimator?.start()
    }

    使用

  • xml声明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <com.example.testapp.DashboardView
    android:id="@+id/view_dashboard"
    android:layout_width="200dp"
    android:layout_height="200dp"
    app:layout_constraintTop_toTopOf="parent"
    android:layout_marginStart="6dp"
    android:layout_marginTop="8dp"
    app:groupNum="5"
    app:groupScaleColor="@color/emphasis38"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:pointerWidth="2dp"
    app:progressBackgroundColor="@color/emphasis8"
    app:progressStrokeWidth="6dp"
    app:progressColors="@array/dashboardColors"
    app:textSize="24sp"
    app:tickScaleColor="@color/emphasis8"
    app:ticksNum="6" />
  • 设置当前百分比
    1
    dashboardView.setProgress((float) i / 100)
    最后附上完整代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    288
    289
    290
    291
    292
    293
    294
    295
    296
    297
    298
    299
    300
    301
    302
    303
    304
    305
    306
    307
    308
    309
    310
    311
    312
    313
    314
    315
    316
    317
    318
    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    package com.example.testapp

    import android.animation.ValueAnimator
    import android.annotation.SuppressLint
    import android.content.Context
    import android.graphics.*
    import android.util.AttributeSet
    import android.view.View
    import android.view.animation.LinearInterpolator
    import androidx.core.animation.addListener
    import androidx.core.content.ContextCompat
    import kotlin.math.abs

    @SuppressLint("CustomViewStyleable")
    class DashboardView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
    ) :
    View(context, attrs, defStyleAttr) {
    var progressColor = intArrayOf() // 渐变色开始颜色
    var progressBackgroundColor = R.color.emphasis16 // 进度条背景颜色
    var textColor = R.color.primary // 文字颜色
    var tickScaleColor = R.color.emphasis8 // 普通刻度线颜色
    var groupScaleColor = R.color.emphasis38 // 分组刻度线颜色
    var progressStrokeWidth = 24f // 进度条宽度
    var paintProgressBackground = Paint() // 进度条背景画笔
    private var paintProgress = Paint() // 进度条画笔
    var paintText = Paint() // 文字画笔
    private var paintNum = Paint() // 刻度画笔
    var rect = RectF() // 表盘矩形区域
    private var viewWidth = 0 // 宽度
    private var viewHeight = 0 // 高度
    var percent = 0f // 百分比
    var oldPercent = 0f // 过去的百分比
    var textSize = 100f // 文本大小
    private var valueAnimator: ValueAnimator? = null // 属性动画
    var animatorDuration = 0L // 动画时长
    private var groupNum = 5 // 分组数
    private var ticksNum = 6 // 每组刻度数
    var pointerWidth = 15f // 指针的宽度
    private var ticksCount = groupNum * ticksNum + 1// 总刻度数

    companion object {
    var OFFSET = 30f // 偏移量
    var START_ARC = 150f // 开始角度
    var DURING_ARC = 240f // 过渡调度
    }

    init {
    setLayerType(LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.dashboard)
    progressBackgroundColor = typedArray.getColor(
    R.styleable.dashboard_progressBackgroundColor,
    ContextCompat.getColor(context, progressBackgroundColor)
    )
    progressStrokeWidth = typedArray.getDimension(
    R.styleable.dashboard_progressStrokeWidth,
    progressStrokeWidth
    )

    textSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    textSize
    )
    textColor = typedArray.getColor(R.styleable.dashboard_textColor, textColor)
    tickScaleColor =
    typedArray.getColor(R.styleable.dashboard_tickScaleColor, tickScaleColor)
    groupScaleColor =
    typedArray.getColor(R.styleable.dashboard_groupScaleColor, groupScaleColor)
    groupNum = typedArray.getInt(R.styleable.dashboard_groupNum, groupNum)
    ticksNum = typedArray.getInt(R.styleable.dashboard_ticksNum, ticksNum)
    pointerWidth =
    typedArray.getDimension(R.styleable.dashboard_pointerWidth, pointerWidth)
    val colorsId = typedArray.getResourceId(R.styleable.dashboard_progressColors, 0)
    progressColor =typedArray.resources.getIntArray(colorsId)
    ticksCount = groupNum * ticksNum + 1
    typedArray.recycle()
    OFFSET = progressStrokeWidth + 10f
    initPaint()
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    val realWidth = startMeasure(widthMeasureSpec)
    val realHeight = startMeasure(heightMeasureSpec)
    setMeasuredDimension(realWidth, realHeight)
    }

    fun startMeasure(msSpec: Int): Int {
    val mode = MeasureSpec.getMode(msSpec)
    val size = MeasureSpec.getSize(msSpec)
    return if (mode == MeasureSpec.EXACTLY) {
    size
    } else {
    Util.dp2px(200)
    }
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    viewWidth = width
    viewHeight = height
    pointerWidth = (viewWidth / 40).toFloat()
    initShader()
    }

    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    canvas?.translate((viewWidth / 2).toFloat(), (viewHeight / 2).toFloat())
    drawPanel(canvas)
    drawProgress(canvas, percent)
    drawText(canvas, percent)
    drawPointer(canvas, percent)
    }

    /**
    * 绘制刻度
    * @param canvas Canvas?
    */
    fun drawPanel(canvas: Canvas?) {
    canvas?.save()
    canvas?.rotate(-(180 - START_ARC + 90), 0f, 0f)
    val numY = -viewHeight / 2 + OFFSET + progressStrokeWidth
    val angle = DURING_ARC / ((ticksCount - 1) * 1.0f)
    for (i in 0 until ticksCount) {
    canvas?.save()
    canvas?.rotate(angle * i, 0f, 0f)
    if (i == 0 || i % groupNum == 0) {
    paintNum.color = groupScaleColor
    } else {
    paintNum.color = tickScaleColor
    }
    canvas?.drawLine(0f, numY + 2, 0f, numY + (pointerWidth * 2) + 5, paintNum)
    canvas?.restore()
    }
    canvas?.restore()
    }

    /**
    * 绘制进度
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawProgress(canvas: Canvas?, percent: Float) {
    canvas?.drawArc(rect, START_ARC, DURING_ARC, false, paintProgressBackground)
    var curPercent = percent
    if (curPercent > 1.0f) {
    curPercent = 1.0f
    }
    if (curPercent > 0.0f) {
    canvas?.drawArc(rect, START_ARC, percent * DURING_ARC, false, paintProgress)
    }
    }

    /**
    * 绘制指针
    * @param canvas Canvas?
    * @param percent Float
    */
    private fun drawPointer(canvas: Canvas?, percent: Float) {
    canvas?.save()
    val angle = DURING_ARC * (percent - 0.5f) - 180
    canvas?.rotate(angle, 0f, 0f)
    val pointer = Path()
    pointer.moveTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.lineTo(
    pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(
    -pointerWidth,
    viewHeight / 2 - OFFSET - progressStrokeWidth - (pointerWidth * 2)
    )
    pointer.lineTo(0f, viewHeight / 2 - OFFSET - progressStrokeWidth)
    pointer.close()
    pointer.fillType = Path.FillType.EVEN_ODD
    canvas?.drawPath(pointer, paintText)
    canvas?.restore()
    }

    /**
    * 绘制文本
    * @param canvas Canvas?
    * @param percent Float
    */
    @SuppressLint("RestrictedApi")
    fun drawText(canvas: Canvas?, percent: Float) {
    val offsetY = viewWidth / 8
    paintText.textSize = textSize
    if (percent < 0.2f) {
    val subTitle1 = "Very"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Cold"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    } else if (percent >= 0.2f && percent < 0.4f) {
    val subTitle = "Cold"
    paintText.color = progressColor[0]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.4f && percent < 0.6f) {
    val subTitle = "Normal"
    paintText.color = progressColor[2]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else if (percent >= 0.6f && percent < 0.8f) {
    val subTitle = "Hot"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle,
    -paintText.measureText(subTitle) / 2,
    offsetY * 2.2f,
    paintText
    )
    } else {
    val subTitle1 = "Very"
    paintText.color = progressColor[4]
    canvas?.drawText(
    subTitle1,
    -paintText.measureText(subTitle1) / 2,
    offsetY * 1.4f,
    paintText
    )

    val subTitle2 = "Hot"
    canvas?.drawText(
    subTitle2,
    -paintText.measureText(subTitle2) / 2,
    offsetY * 2.4f,
    paintText
    )
    }

    paintText.textSize = (textSize * 1.3).toFloat()
    val text = (percent * 100).toInt().toString()
    canvas?.drawText(
    text,
    -paintText.measureText(text) / 2,
    - textSize / 5f,
    paintText
    )
    }

    /**
    * 设置当前百分比
    * @param curPercent Float 范围0~1
    */
    fun setProgress(curPercent: Float) {
    if (valueAnimator?.isRunning == true) {
    valueAnimator?.cancel()
    }
    animatorDuration = (abs(curPercent - oldPercent) * 20).toLong()
    valueAnimator = ValueAnimator.ofFloat(oldPercent, curPercent).setDuration(animatorDuration)
    valueAnimator?.addUpdateListener {
    percent = it.animatedValue as Float
    invalidate()
    }
    valueAnimator?.interpolator = LinearInterpolator()
    valueAnimator?.addListener(onEnd = {
    oldPercent = curPercent
    if (percent < 0.0f) {
    percent = 0.0f
    invalidate()
    }
    if (percent > 1f) {
    percent = 1f
    invalidate()
    }
    })
    valueAnimator?.start()
    }

    /**
    * 设置进度条宽度
    * @param dp Int
    */
    fun setProgressStroke(dp: Int) {
    progressStrokeWidth = Util.dp2px(dp).toFloat()
    paintProgress.strokeWidth = progressStrokeWidth
    paintProgressBackground.strokeWidth = progressStrokeWidth
    invalidate()
    }



    /**
    * 初始化画笔
    */
    private fun initPaint() {
    paintProgressBackground.isAntiAlias = true
    paintProgressBackground.strokeWidth = progressStrokeWidth
    paintProgressBackground.style = Paint.Style.STROKE
    paintProgressBackground.color = progressBackgroundColor
    paintProgressBackground.isDither = true
    paintProgress.isAntiAlias = true
    paintProgress.strokeWidth = progressStrokeWidth
    paintProgress.style = Paint.Style.STROKE
    paintProgress.isDither = true
    paintText.isAntiAlias = true
    paintText.color = textColor
    paintText.strokeWidth = 1F
    paintText.style = Paint.Style.FILL
    paintText.isDither = true
    paintNum.isAntiAlias = true
    paintNum.strokeWidth = 3f
    paintNum.style = Paint.Style.FILL
    paintNum.isDither = true
    }

    /**
    * 初始化渐变颜色
    */
    private fun initShader() {
    rect.set(
    (-viewWidth / 2) + OFFSET + paddingLeft, paddingTop - (viewHeight / 2) + OFFSET,
    (viewWidth / 2) - paddingRight - OFFSET,
    (viewWidth / 2) - paddingBottom - OFFSET
    )
    val shader = SweepGradient(
    0f,
    0f,
    progressColor,
    null
    )
    val rotate = 90f
    val gradientMatrix = Matrix()
    gradientMatrix.preRotate(rotate, 0f, 0f)
    shader.setLocalMatrix(gradientMatrix)
    paintProgress.shader = shader
    }
    }


最近工作比较忙,项目压力大,又是很长一段时间没有更新blog了。今天虽然是放五一长假,但是由于疫情仍然没有结束,所以没有出游的计划,正好可以写点东西总结一下最近项目中遇到的自定义View相关的内容,实现一个总量一定,支持和反对人数对比的指示条来反映某个问题下大多数人的看法,欢迎交流和讨论~


最终效果图:
效果图

变量声明

  • 声明支持和反对的绘制画笔,数量以及颜色,定义指示条的宽度,中间间隔的宽度,以及三角分割的宽度,圆角大小,指示条坐标对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private var downPaint = Paint()
    private var upPaint = Paint()
    private var upCount = 80
    private var downCount = 0
    private var downColor = ContextCompat.getColor(context, R.color.secondary)
    private var upColor = ContextCompat.getColor(context, R.color.primary)
    private var lineWidth = Util.dp2px(6)
    private var attachWidth = Util.dp2px(2)
    private var spanWidth = Util.dp2px(9)
    private var roundSize = lineWidth - Util.dp2px(2)
    private val indicator by lazy { Indicator() }

    指示条坐标对象

  • 整个指示条包含8个坐标,支持段和反对段各4个坐标,所以封装为一个Indicator对象,把支持和反对的绘制路径封装成两个IndicatorPath对象,每个IndicatorPath包含4个坐标,每个坐标包含x,y的数值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    data class Indicator(
    var upPath: IndicatorPath = IndicatorPath(),
    var downPath: IndicatorPath = IndicatorPath()
    )

    data class IndicatorPath(
    var startTop: Pos = Pos(),
    var startBottom: Pos = Pos(),
    var endTop: Pos = Pos(),
    var endBottom: Pos = Pos()
    )

    data class Pos(var x: Float = 0f, var y: Float = 0f)

    初始化逻辑

  • 获取xml的配置进行变量初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    init {
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.compareindicator)
    downColor = typedArray.getColor(R.styleable.compareindicator_down_color, downColor)
    upColor = typedArray.getColor(R.styleable.compareindicator_up_color, upColor)
    lineWidth = typedArray.getDimension(
    R.styleable.compareindicator_lineWidth,
    lineWidth.toFloat()
    ).toInt()
    attachWidth = typedArray.getDimension(
    R.styleable.compareindicator_attachWidth,
    lineWidth.toFloat()
    ).toInt()
    spanWidth = typedArray.getDimension(
    R.styleable.compareindicator_spanWidth,
    lineWidth.toFloat()
    ).toInt()
    textSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    textSize
    )
    typedArray.recycle()
    upPaint = Paint()
    upPaint.color = upColor
    downPaint = Paint()
    downPaint.color = downColor
    }

    重写onDraw自定义绘制逻辑

  • 当支持和反对数量均不为0时,根据当前View宽度和支持数量百分比计算支持指示条的长度,根据当前View宽度和反对数量百分比计算反对指示条的长度,再计算支持和反对指示条的绘制路径。如果支持数不为0反对数为0则只绘制支持指示条,设置支持绘制画笔的圆角,直接绘制一根长度为View宽度的支持指示条,如果支持数为0反对数不为0则只绘制反对指示条,设置反对绘制画笔的圆角,直接绘制一根长度为View宽度的反对指示条。如果支持和反对数都为0,则支持和反对指示条长度各占View宽度的一半进行绘制
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    val upWidth: Int
    val downWidth: Int
    if (upCount != 0 && downCount != 0) {
    upWidth = width / (downCount + upCount) * upCount
    downWidth = width / (downCount + upCount) * downCount
    getPath(downWidth)
    drawIndicator(canvas)
    } else if (upCount != 0 && downCount == 0) {
    upWidth = width
    upPaint.strokeWidth = lineWidth.toFloat()
    upPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (upWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    upPaint
    )
    } else if (upCount == 0 && downCount != 0) {
    downWidth = width
    downPaint.strokeWidth = lineWidth.toFloat()
    downPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (downWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    downPaint
    )
    } else {
    upWidth = width / 2
    downWidth = width / 2
    getPath(downWidth)
    drawIndicator(canvas)
    }
    drawText(canvas)
    }

    计算绘制路径

  • 分别计算支持指示条和反对指示条的绘制路径,各自计算开始和结束的上下坐标,这里考虑到圆角,所以左边的反对指示条x开始坐标为View的paddingStart+圆角的大小,x结束坐标为View的paddingStart+圆角的大小+反对指示条的长度+三角形分割的宽度-间隔的宽度,y开始坐标为View的paddingTop,y结束坐标为View的paddingTop+指示条的宽度。右边的支持指示条x开始坐标为反对指示条的x坐标+间隔宽度,结束坐标为View的宽度-View的paddingEnd-圆角大小,y开始坐标为paddingTop,结束坐标为paddingTop+指示条的宽度
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    private fun getPath(downWidth: Int) {
    val paddingSpanStart = paddingStart + roundSize
    // 反对指示条
    indicator.downPath.startTop.x = paddingSpanStart.toFloat()
    indicator.downPath.startTop.y = paddingTop.toFloat()
    indicator.downPath.startBottom.x = paddingSpanStart.toFloat()
    indicator.downPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.downPath.endTop.x =
    (paddingSpanStart + downWidth + attachWidth - spanWidth).toFloat()
    indicator.downPath.endTop.y = paddingTop.toFloat()
    indicator.downPath.endBottom.x = (paddingSpanStart + downWidth - spanWidth).toFloat()
    indicator.downPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    val paddingSpanEnd = paddingEnd + roundSize
    // 支持指示条
    indicator.upPath.startTop.x = indicator.downPath.endTop.x + spanWidth
    indicator.upPath.startTop.y = paddingTop.toFloat()
    indicator.upPath.startBottom.x = indicator.downPath.endBottom.x + spanWidth
    indicator.upPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.upPath.endTop.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endTop.y = paddingTop.toFloat()
    indicator.upPath.endBottom.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    }

    绘制指示条逻辑

  • 设置支持和反对的绘制画笔为填充模式,绘制反对指示条的路径,从开始的上坐标到结束的上坐标,从结束的下坐标到开始的下坐标都是绘制直线,开始的下坐标到开始的上坐标是绘制圆滑的曲线即贝塞尔曲线,控制点为x的位置是开始的下坐标x-圆角大小,y是开始的下坐标y-指示线的宽度/2,结束点为开始的上坐标。同理,绘制支持指示条的路径,从结束的上坐标到开始的上坐标,从开始的下坐标到结束的下坐标都是绘制直线,结束的下坐标到结束的上坐标同样是绘制贝塞尔曲线,控制点为x的位置是结束的下坐标x-圆角大小,y是结束的下坐标y-指示线的宽度/2,结束点为结束的上坐标。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    private fun drawIndicator(canvas: Canvas?) {
    downPaint.strokeWidth = 1F
    upPaint.strokeWidth = 1F
    downPaint.strokeCap = Paint.Cap.BUTT
    upPaint.strokeCap = Paint.Cap.BUTT
    drawLine(canvas, indicator.downPath, downPaint, true)
    drawLine(canvas, indicator.upPath, upPaint, false)
    }

    private fun drawLine(
    canvas: Canvas?,
    indicatorPath: IndicatorPath,
    paint: Paint,
    down: Boolean
    ) {
    val path = Path()
    if (down) {
    path.moveTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.quadTo(
    indicatorPath.startBottom.x - roundSize,
    indicatorPath.startBottom.y - (lineWidth / 2),
    indicatorPath.startTop.x,
    indicatorPath.startTop.y
    )
    } else {
    path.moveTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.quadTo(
    indicatorPath.endBottom.x + roundSize,
    indicatorPath.endBottom.y - (lineWidth / 2),
    indicatorPath.endTop.x,
    indicatorPath.endTop.y
    )
    }
    path.close()
    paint.strokeWidth = 1f
    paint.style = Paint.Style.FILL
    path.fillType = Path.FillType.WINDING
    canvas?.drawPath(path, paint)
    }

    绘制文本逻辑

  • 左边是反对指示条在开始位置绘制Down的数量文本,x的坐标为反对指示条的开始x坐标,y坐标为反对指示条的开始下坐标y+间隔宽度+文字大小,右边是支持指示条在结束位置绘制Up的数量文本,x的坐标为支持指示条的结束下坐标的x-文本宽度,y坐标为支持指示条的结束下坐标y+间隔宽度+文字大小
    1
    2
    3
    4
    5
    6
    7
    8
    private fun drawText(canvas: Canvas?){
    val downText = "Down: $downCount"
    downPaint.textSize = textSize
    canvas?.drawText(downText, indicator.downPath.startBottom.x, indicator.downPath.startBottom.y + spanWidth + textSize, downPaint)
    val upText = "Up: $upCount"
    upPaint.textSize = textSize
    canvas?.drawText(upText, indicator.upPath.endBottom.x - upPaint.measureText(upText), indicator.upPath.endBottom.y + spanWidth + textSize, upPaint)
    }

    设置支持和反对数量

  • 根据设置的支持反对数量刷新视图
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    fun updateView(
    up: Int,
    down: Int,
    downColor: Int = ContextCompat.getColor(context, R.color.secondary),
    upColor: Int = ContextCompat.getColor(context, R.color.primary)
    ) {
    this.upCount = up
    this.downCount = down
    this.downColor = downColor
    downPaint.color = downColor
    this.upColor = upColor
    upPaint.color = upColor
    postInvalidate()
    }

    使用

  • xml中声明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <com.example.testapp.CompareIndicator
    android:id="@+id/indicator_compare"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginStart="12dp"
    android:layout_marginTop="200dp"
    android:layout_marginEnd="12dp"
    app:lineWidth="6dp"
    app:textSize="12sp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
  • 设置支持反对数量
    1
    compareIndicator.updateView(80,20, ContextCompat.getColor(this, R.color.secondary), ContextCompat.getColor(this, R.color.primary))
    最后附上完整代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    package com.example.testapp

    import android.content.Context
    import android.graphics.Canvas
    import android.graphics.Paint
    import android.graphics.Path
    import android.util.AttributeSet
    import android.view.View
    import androidx.core.content.ContextCompat

    class CompareIndicator @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
    ) : View(context, attrs, defStyleAttr) {
    private var downPaint = Paint()
    private var upPaint = Paint()
    private var upCount = 80
    private var downCount = 0
    private var downColor = ContextCompat.getColor(context, R.color.secondary)
    private var upColor = ContextCompat.getColor(context, R.color.primary)
    private var lineWidth = Util.dp2px(6)
    private var attachWidth = Util.dp2px(2)
    private var spanWidth = Util.dp2px(9)
    private var labelSize = 40f
    private var roundSize = lineWidth - Util.dp2px(2)
    private val indicator by lazy { Indicator() }

    init {
    setLayerType(View.LAYER_TYPE_SOFTWARE, null)
    val typedArray = context.obtainStyledAttributes(attrs, R.styleable.compareindicator)
    downColor = typedArray.getColor(R.styleable.compareindicator_down_color, downColor)
    upColor = typedArray.getColor(R.styleable.compareindicator_up_color, upColor)
    lineWidth = typedArray.getDimension(
    R.styleable.compareindicator_lineWidth,
    lineWidth.toFloat()
    ).toInt()
    attachWidth = typedArray.getDimension(
    R.styleable.compareindicator_attachWidth,
    lineWidth.toFloat()
    ).toInt()
    spanWidth = typedArray.getDimension(
    R.styleable.compareindicator_spanWidth,
    lineWidth.toFloat()
    ).toInt()
    labelSize = typedArray.getDimension(
    R.styleable.dashboard_textSize,
    labelSize
    )
    typedArray.recycle()
    upPaint = Paint()
    upPaint.color = upColor
    downPaint = Paint()
    downPaint.color = downColor
    }

    fun updateView(
    up: Int,
    down: Int,
    downColor: Int = ContextCompat.getColor(context, R.color.secondary),
    upColor: Int = ContextCompat.getColor(context, R.color.primary)
    ) {
    this.upCount = up
    this.downCount = down
    this.downColor = downColor
    downPaint.color = downColor
    this.upColor = upColor
    upPaint.color = upColor
    postInvalidate()
    }

    override fun onDraw(canvas: Canvas?) {
    super.onDraw(canvas)
    val upWidth: Int
    val downWidth: Int
    if (upCount != 0 && downCount != 0) {
    upWidth = width / (downCount + upCount) * upCount
    downWidth = width / (downCount + upCount) * downCount
    getPath(downWidth)
    drawIndicator(canvas)
    } else if (upCount != 0 && downCount == 0) {
    upWidth = width
    upPaint.strokeWidth = lineWidth.toFloat()
    upPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (upWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    upPaint
    )
    } else if (upCount == 0 && downCount != 0) {
    downWidth = width
    downPaint.strokeWidth = lineWidth.toFloat()
    downPaint.strokeCap = Paint.Cap.ROUND
    canvas?.drawLine(
    (paddingStart + roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    (downWidth - paddingEnd - roundSize).toFloat(),
    (paddingTop + roundSize).toFloat(),
    downPaint
    )
    } else {
    upWidth = width / 2
    downWidth = width / 2
    getPath(downWidth)
    drawIndicator(canvas)
    }
    drawText(canvas)
    }

    private fun drawText(canvas: Canvas?){
    val downText = "Down: $downCount"
    downPaint.textSize = labelSize
    canvas?.drawText(downText, indicator.downPath.startBottom.x, indicator.downPath.startBottom.y + spanWidth + labelSize, downPaint)
    val upText = "Up: $upCount"
    upPaint.textSize = labelSize
    canvas?.drawText(upText, indicator.upPath.endBottom.x - upPaint.measureText(upText), indicator.upPath.endBottom.y + spanWidth + labelSize, upPaint)
    }

    private fun drawIndicator(canvas: Canvas?) {
    downPaint.strokeWidth = 1F
    upPaint.strokeWidth = 1F
    downPaint.strokeCap = Paint.Cap.BUTT
    upPaint.strokeCap = Paint.Cap.BUTT
    drawLine(canvas, indicator.downPath, downPaint, true)
    drawLine(canvas, indicator.upPath, upPaint, false)
    }

    private fun drawLine(
    canvas: Canvas?,
    indicatorPath: IndicatorPath,
    paint: Paint,
    down: Boolean
    ) {
    val path = Path()
    if (down) {
    path.moveTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.quadTo(
    indicatorPath.startBottom.x - roundSize,
    indicatorPath.startBottom.y - (lineWidth / 2),
    indicatorPath.startTop.x,
    indicatorPath.startTop.y
    )
    } else {
    path.moveTo(indicatorPath.endTop.x, indicatorPath.endTop.y)
    path.lineTo(indicatorPath.startTop.x, indicatorPath.startTop.y)
    path.lineTo(indicatorPath.startBottom.x, indicatorPath.startBottom.y)
    path.lineTo(indicatorPath.endBottom.x, indicatorPath.endBottom.y)
    path.quadTo(
    indicatorPath.endBottom.x + roundSize,
    indicatorPath.endBottom.y - (lineWidth / 2),
    indicatorPath.endTop.x,
    indicatorPath.endTop.y
    )
    }
    path.close()
    paint.strokeWidth = 1f
    paint.style = Paint.Style.FILL
    path.fillType = Path.FillType.WINDING
    canvas?.drawPath(path, paint)
    }

    private fun getPath(downWidth: Int) {
    val paddingSpanStart = paddingStart + roundSize
    // 支持指示条
    indicator.downPath.startTop.x = paddingSpanStart.toFloat()
    indicator.downPath.startTop.y = paddingTop.toFloat()
    indicator.downPath.startBottom.x = paddingSpanStart.toFloat()
    indicator.downPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.downPath.endTop.x =
    (paddingSpanStart + downWidth + attachWidth - spanWidth).toFloat()
    indicator.downPath.endTop.y = paddingTop.toFloat()
    indicator.downPath.endBottom.x = (paddingSpanStart + downWidth - spanWidth).toFloat()
    indicator.downPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    val paddingSpanEnd = paddingEnd + roundSize
    // 反对指示条
    indicator.upPath.startTop.x = indicator.downPath.endTop.x + spanWidth
    indicator.upPath.startTop.y = paddingTop.toFloat()
    indicator.upPath.startBottom.x = indicator.downPath.endBottom.x + spanWidth
    indicator.upPath.startBottom.y = (paddingTop + lineWidth).toFloat()
    indicator.upPath.endTop.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endTop.y = paddingTop.toFloat()
    indicator.upPath.endBottom.x = (width - paddingSpanEnd).toFloat()
    indicator.upPath.endBottom.y = (paddingTop + lineWidth).toFloat()
    }

    data class Indicator(
    var upPath: IndicatorPath = IndicatorPath(),
    var downPath: IndicatorPath = IndicatorPath()
    )

    data class IndicatorPath(
    var startTop: Pos = Pos(),
    var startBottom: Pos = Pos(),
    var endTop: Pos = Pos(),
    var endBottom: Pos = Pos()
    )

    data class Pos(var x: Float = 0f, var y: Float = 0f)
    }


裁剪是Android开发中一个常见的需求,虽然Android的ImageView的scaleType属性提供了多种图片的展示方式,其中包括一些裁剪方式,以及Glide自带的Transformation也提供了常见的圆角,圆形,居中裁剪,但有的时候仍然不能满足我们的需求,比如说为了版本迭代老版本apk能兼容新版本的图片资源,UI迭代的情况下产品会提出裁剪图片保留图片底部区域的规则,这时候ImageView和Glide自带的裁剪就不能满足需求了,需要我们自定义Transformation实现新的裁剪规则


裁剪类型

定义枚举:顶部裁剪,居中裁剪,底部裁剪

1
2
3
enum class CropType {
TOP, CENTER, BOTTOM
}

实线BitmapTransformation接口

默认居中裁剪,支持传入自定义的裁剪像素宽高,重写必要的方法,updateDiskCacheKey,transform,equals,hashCode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private val TAG: String = CropTransformation::class.java.name

class CropTransformation(var width: Int = 0, var height: Int = 0, var cropType: CropType = CropType.CENTER) : BitmapTransformation() {

override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(TAG.toByteArray(CHARSET))
}

override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
...
}

override fun equals(other: Any?): Boolean {
return other is CropTransformation && other.width == width && other.height == height && other.cropType == cropType
}

override fun hashCode(): Int {
return TAG.hashCode()
}
}

实现核心方法transform

  • transform的入参为,图片复用线程池pool,下载后要处理的图片toTransform,目标View的宽高

  • 当宽或高为0时默认使用图片的宽或高

  • 图片格式默认采用要处理图片的格式,否则使用ARGB_8888

  • 从当前图片缓存池中取得一张复用的图片进行处理,打开alpha通道

  • 计算裁剪的宽和图片的宽之比,以及高之比,取大的那个

  • 计算缩放后的宽和高

  • 计算绘制的开始位置,x和y坐标,x坐标为(当前裁剪的宽度 - 缩放后的宽度)/ 2,从水平居中区域开始绘制,y坐标根据当前的裁剪类型而定,TOP从0开始,CENTER为(当前裁剪的高度 - 缩放后的高度)/ 2,从垂直居中区域开始,BOTTOM直接裁剪底部区域,结束位置的x坐标为开始位置的x坐标 + 缩放后的宽度,y坐标为开始位置的y坐标 + 缩放后的高度,最后计算出绘制的矩形区域,设置从当前图片缓存池中取得的图片密度为要处理的图片toTransform的密度,利用复用的图片创建canvas,drawBitmap把要处理的图片toTransform绘制到canvas的目标矩形区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
width = if (width == 0) toTransform.width else width
height = if (height == 0) toTransform.height else height

val config = if (toTransform.config != null) toTransform.config else Bitmap.Config.ARGB_8888
val bitmap = pool[width, height, config]

bitmap.setHasAlpha(true)

val scaleX = width.toFloat() / toTransform.width
val scaleY = height.toFloat() / toTransform.height
val scale = max(scaleX, scaleY)

val scaledWidth = scale * toTransform.width
val scaledHeight = scale * toTransform.height
val left = (width - scaledWidth) / 2
val top: Float = getTop(scaledHeight)
val targetRect = RectF(left, top, left + scaledWidth, top + scaledHeight)

bitmap.density = toTransform.density
val canvas = Canvas(bitmap)
canvas.drawBitmap(toTransform, null, targetRect, null)

return bitmap
}

private fun getTop(scaledHeight: Float): Float {
return when (cropType) {
CropType.TOP -> 0f
CropType.CENTER -> (height - scaledHeight) / 2
CropType.BOTTOM -> height - scaledHeight
}
}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.kubi.kucoin.utils

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.RectF
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.security.MessageDigest
import kotlin.math.max

/**
* @description: 图片裁剪
* @author: Jessie.Li
* @email: jessie.li@corp.kucoin.com
* @create: 2022-01-05 17:47
**/
private val TAG: String = CropTransformation::class.java.name

class CropTransformation(var width: Int = 0, var height: Int = 0, var cropType: CropType = CropType.CENTER) : BitmapTransformation() {
enum class CropType {
TOP, CENTER, BOTTOM
}

override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(TAG.toByteArray(CHARSET))
}

override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
width = if (width == 0) toTransform.width else width
height = if (height == 0) toTransform.height else height

val config = if (toTransform.config != null) toTransform.config else Bitmap.Config.ARGB_8888
val bitmap = pool[width, height, config]

bitmap.setHasAlpha(true)

val scaleX = width.toFloat() / toTransform.width
val scaleY = height.toFloat() / toTransform.height
val scale = max(scaleX, scaleY)

val scaledWidth = scale * toTransform.width
val scaledHeight = scale * toTransform.height
val left = (width - scaledWidth) / 2
val top: Float = getTop(scaledHeight)
val targetRect = RectF(left, top, left + scaledWidth, top + scaledHeight)

bitmap.density = toTransform.density
val canvas = Canvas(bitmap)
canvas.drawBitmap(toTransform, null, targetRect, null)

return bitmap
}

private fun getTop(scaledHeight: Float): Float {
return when (cropType) {
CropType.TOP -> 0f
CropType.CENTER -> (height - scaledHeight) / 2
CropType.BOTTOM -> height - scaledHeight
}
}

override fun equals(other: Any?): Boolean {
return other is CropTransformation && other.width == width && other.height == height && other.cropType == cropType
}

override fun hashCode(): Int {
return TAG.hashCode()
}
}


最近工作忙项目比较急,很少有时间更新blog了,不过好习惯还是应该保持,时间挤挤还是有的。项目中遇到一个需求,实现一个拉环动画,有一根拉伸可以往下拉动并在放手的时候回弹然后上下抖动,就跟我们生活中拖拽一根带了拉环的橡皮筋效果是一样的。最终我是用属性动画去实现这个需求,感觉不算复杂效果也还行,在此记录实现过程,欢迎相互交流~


最终效果图gif:
image

自定义View

实现AnimatorUpdateListener和Animator.AnimatorListener接口

1
2
3
4
5
6
7
8
9
class RingView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), AnimatorUpdateListener, Animator.AnimatorListener{
override fun onAnimationUpdate(animation: ValueAnimator) {}
override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationCancel(animation: Animator) {}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {}
}

初始化

初始化paint,animator,设置1秒后开始动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
init {
paint.color = resources.getColor(R.color.colorPrimary)
paint.isAntiAlias = true
postDelayed({ startAnimation() }, 1000)
}
private fun initAnimator(start: Int, end: Int) {
animator = ValueAnimator.ofInt(start, end)
animator?.duration = 1000
animator?.interpolator = accelerateInterpolator
animator?.addUpdateListener(this)
animator?.addListener(this)
}

private fun startAnimation() {
initAnimator(startY, endY)
isDown = true
animator?.start()
}

绘制

绘制拉环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private val bgRing = BitmapFactory.decodeResource(resources, R.mipmap.bg_ring)
// 拉环绘制尺寸
private val targetWidth = (bgRing.width * 1.4).toInt()
private val targetHeight = (bgRing.height * 1.4).toInt()
private val radius = targetWidth / 2
// 拉环坐标
private var xPos = 0
private var yPos = 0
private val srcRingRect = Rect(0, 0, bgRing.width, bgRing.height)
private val dstRingRect = Rect()

private fun drawRing(canvas: Canvas) {
dstRingRect.left = xPos
dstRingRect.top = yPos
dstRingRect.right = xPos + targetWidth
dstRingRect.bottom = yPos + targetHeight
canvas.drawBitmap(bgRing, srcRingRect, dstRingRect, paint)
}

绘制弹力绳

1
2
3
4
5
6
private val strokeWidth = 6f  // 绳子粗细
private var ratio = 0f // 弹性变化比例
private fun drawLine(canvas: Canvas) {
paint.strokeWidth = strokeWidth - 3 * ratio
canvas.drawLine((xPos + radius).toFloat(), 0f, (xPos + radius).toFloat(), (yPos + 2).toFloat(), paint)
}

重写onDraw

初始化上下轻微抖动的范围和最大拖拽长度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 上下轻微抖动的范围
private var startY = 0
private var endY = 0
private var maxDragY = 0 // 最大拖拽长度
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (animator == null) {
startY = canvas.height / 2 - radius / 2
endY = canvas.height / 2
yPos = startY
maxDragY = endY + endY / 2
xPos = canvas.width / 2 - radius
}
drawLine(canvas)
drawRing(canvas)
}

下拉后自动回弹

利用加速插值器和减速插值器实现惯性向下运动,减速向上回弹,监听动画结束事件,更新运动方向标志位,下落时设置为加速插值器,回弹时设置为减速插值器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private var isDown = true
private var stopAnimate = false // 是否停止动画
private val accelerateInterpolator = AccelerateInterpolator()
private val decelerateInterpolator = DecelerateInterpolator()
override fun onAnimationEnd(animation: Animator) {
if (!stopAnimate) {
val vAnimation = animation as ValueAnimator
isDown = !isDown
if (isDown) {
vAnimation.setIntValues(startY, endY)
vAnimation.interpolator = accelerateInterpolator
} else {
vAnimation.setIntValues(endY, startY)
vAnimation.interpolator = decelerateInterpolator
}
vAnimation.start()
}
}

拖拽逻辑

重写onTouchEvent,监听ACTION_DOWN/ACTION_UP/ACTION_MOVE事件
手指落下时:判断当前触摸位置坐标是否在拉环矩形区域内,如果是则记录开始拖拽位置的Y坐标,停止当前动画,记录上一次手指移动的Y坐标
手指移动时:记录手指当前Y坐标,如果是拖拽状态且拖拽距离大于10,计算当前拖拽的插值,如果插值大于0则为向下拖拽,继续判断当前Y坐标是否小于最大拖拽Y坐标位置,如果是计算拉环当前运动的目标位置,计算当前拖拽位置到最大拖拽位置的百分比,修正拉环的Y坐标,实现拖拽时移动缓慢的阻尼效果,达到最大拖拽位置时百分比为1,更新拖拽状态为false,刷新视图,更新最后一次拖拽的Y坐标
手指抬起时:更新拖拽状态为false,更新最后一次拖拽的Y坐标为0,触发回弹动画,设置为减速插值器,刷新动画标志位,向下标志位为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isDrag = dstRingRect.plus(padding).contains(event.x.toInt(), event.y.toInt())
if (isDrag) {
lastDragY = event.y
if (animator != null && animator!!.isRunning) {
stopAnimate = true
animator?.cancel()
}
lastMoveY = (yPos + radius).toFloat()
}
}
MotionEvent.ACTION_UP -> {
// 弹回去
back()
lastMoveY = 0f
isDrag = false
}
MotionEvent.ACTION_MOVE -> {
curDragY = event.y
if (isDrag && curDragY - lastDragY > 10) {
val diffY = curDragY - lastMoveY
Log.i(TAG, "diffY: $diffY" )
if (diffY > 0){
if (curDragY < maxDragY){
val targetY = curDragY.toInt()
Log.i(TAG, "maxLength: ${maxDragY - lastDragY}" )
ratio = (targetY - lastDragY) / (maxDragY - lastDragY)
yPos = (yPos + diffY * (1 - ratio)).toInt()
Log.i(TAG, "add: ${diffY * (1 - ratio)}, diffY:${diffY} ratio:${(1 - ratio)}" )
} else {
ratio = 1f
isDrag = false
}

invalidate()
}
lastMoveY = curDragY
}
}
}
return true
}

private fun back() {
if (animator != null && !animator!!.isRunning) {
animator?.setIntValues(yPos, startY)
animator?.interpolator = decelerateInterpolator
stopAnimate = false
animator?.start()
isDown = false
}
}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
class RingView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr), AnimatorUpdateListener, Animator.AnimatorListener {
private val bgRing = BitmapFactory.decodeResource(resources, R.mipmap.bg_ring)
// 拉环绘制尺寸
private val targetWidth = (bgRing.width * 1.4).toInt()
private val targetHeight = (bgRing.height * 1.4).toInt()
private val radius = targetWidth / 2
// 拉环坐标
private var xPos = 0
private var yPos = 0
private val strokeWidth = 6f // 绳子粗细
private val paint = Paint()
private var animator: ValueAnimator? = null
private var isDown = true

// 上下轻微抖动的范围
private var startY = 0
private var endY = 0
private var maxDragY = 0// 最大拖拽长度
private val accelerateInterpolator = AccelerateInterpolator()
private val decelerateInterpolator = DecelerateInterpolator()
private val srcRingRect = Rect(0, 0, bgRing.width, bgRing.height)
private val dstRingRect = Rect()
private var isDrag = false// 是否拖拽
private var curDragY = 0f // 当前拖拽Y坐标
private var lastDragY = 0f // 开始拖拽Y坐标
private var stopAnimate = false // 是否停止动画
private var lastMoveY = 0f // 上一次手指移动Y坐标
private var ratio = 0f // 弹性变化比例
private var padding = 20 // 触摸范围增量
init {
paint.color = resources.getColor(R.color.colorPrimary)
paint.isAntiAlias = true
postDelayed({ startAnimation() }, 1000)
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (animator == null) {
startY = canvas.height / 2 - radius / 2
endY = canvas.height / 2
yPos = startY
maxDragY = endY + endY / 2
xPos = canvas.width / 2 - radius
}
drawLine(canvas)
drawRing(canvas)
}

private fun drawRing(canvas: Canvas) {
dstRingRect.left = xPos
dstRingRect.top = yPos
dstRingRect.right = xPos + targetWidth
dstRingRect.bottom = yPos + targetHeight
canvas.drawBitmap(bgRing, srcRingRect, dstRingRect, paint)
}

override fun onAnimationUpdate(animation: ValueAnimator) {
yPos = animation.animatedValue as Int
ratio = (yPos - startY).toFloat() / (maxDragY - startY)
invalidate()
}

private fun drawLine(canvas: Canvas) {
paint.strokeWidth = strokeWidth - 3 * ratio
canvas.drawLine((xPos + radius).toFloat(), 0f, (xPos + radius).toFloat(), (yPos + 2).toFloat(), paint)
}

private fun initAnimator(start: Int, end: Int) {
animator = ValueAnimator.ofInt(start, end)
animator?.duration = 1000
animator?.interpolator = accelerateInterpolator
animator?.addUpdateListener(this)
animator?.addListener(this)
}

private fun startAnimation() {
initAnimator(startY, endY)
isDown = true
animator?.start()
}

override fun onAnimationRepeat(animation: Animator) {}
override fun onAnimationStart(animation: Animator) {}
override fun onAnimationEnd(animation: Animator) {
if (!stopAnimate) {
val vAnimation = animation as ValueAnimator
isDown = !isDown
if (isDown) {
vAnimation.setIntValues(startY, endY)
vAnimation.interpolator = accelerateInterpolator
} else {
vAnimation.setIntValues(endY, startY)
vAnimation.interpolator = decelerateInterpolator
}
vAnimation.start()
}
}

override fun onAnimationCancel(animation: Animator) {}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
isDrag = dstRingRect.plus(padding).contains(event.x.toInt(), event.y.toInt())
if (isDrag) {
lastDragY = event.y
if (animator != null && animator!!.isRunning) {
stopAnimate = true
animator?.cancel()
}
lastMoveY = (yPos + radius).toFloat()
}
}
MotionEvent.ACTION_UP -> {
// 弹回去
back()
lastMoveY = 0f
isDrag = false
}
MotionEvent.ACTION_MOVE -> {
curDragY = event.y
if (isDrag && curDragY - lastDragY > 10) {
val diffY = curDragY - lastMoveY
Log.i(TAG, "diffY: $diffY" )
if (diffY > 0){
if (curDragY < maxDragY){
val targetY = curDragY.toInt()
Log.i(TAG, "maxLength: ${maxDragY - lastDragY}" )
ratio = (targetY - lastDragY) / (maxDragY - lastDragY)
yPos = (yPos + diffY * (1 - ratio)).toInt()
Log.i(TAG, "add: ${diffY * (1 - ratio)}, diffY:${diffY} ratio:${(1 - ratio)}" )
} else {
ratio = 1f
isDrag = false
}

invalidate()
}
lastMoveY = curDragY
}
}
}
return true
}

private fun back() {
if (animator != null && !animator!!.isRunning) {
animator?.setIntValues(yPos, startY)
animator?.interpolator = decelerateInterpolator
stopAnimate = false
animator?.start()
isDown = false
}
}

}

xml中引入

1
2
3
4
<com.example.testapp.RingView
android:layout_width="match_parent"
android:layout_height="match_parent"
/>


PPTP协议在没有配置智能分流的情况下,上网需要来回切换略显麻烦,有没有什么办法可以和ssr客户端和V2ray客户端那样实现分流呢?解决这个问题以后又会发现一个新的问题PPTP用的是google的dns,解析到的ip地址未必是适合的ip导致某些网站访问速度很慢,如果一个账号能让任意设备共享网络,省去折腾各个终端安装和配置那就太好了,最近利用家里的树莓派实现了这个需求,下面是我的折腾记录,有更好的方法欢迎留言交流~


安装PPTP的linux客户端

  1. apt-get安装pptp-linux
    sudo apt-get install pptp-linux

  2. 修改pptp-linux的配置文件

    sudo vi /etc/ppp/peers/pptpconf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    pty "你的服务端地址 --nolaunchpppd"
    name 账号
    password 密码
    remotename PPTP
    require-mppe-128
    require-mschap-v2
    refuse-eap
    refuse-pap
    refuse-chap
    refuse-mschap
    noauth
    persist
    maxfail 0
    defaultroute
    replacedefaultroute
    usepeerdns
  3. 启动/关闭PPTP
    sudo pon pptpconf
    开启后,如果连接正常,ifconfig可以看到PPTP的连接ppp0

  4. 设置开机启动服务 sudo vi /lib/systemd/system/pptp.service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [Unit]
    Description=PPTP Service
    After=network.target

    [Service]
    Type=forking
    ExecStart=/usr/bin/pon pptpconf

    [Install]
    WantedBy=multi-user.target
  5. 刷新并启动服务

    1
    2
    3
    sudo systemctl daemon-reload
    sudo systemctl enable pptp
    sudo systemctl start pptp

    chnroutes分流策略

    根据请求ip分流,某些网站强制转发到ppp0使用PPTP访问,某些网站不使用,这里记录PPTP下的使用方法,其它协议的使用方法可以查看官网:https://github.com/fivesheep/chnroutes

1
2
3
git clone https://github.com/fivesheep/chnroutes.git
cd chnroutes
sudo python chnroutes.py -p linux; sudo chmod a+x ip-pre-up; sudo cp ip-pre-up /etc/ppp; sudo chmod a+x ip-down; sudo cp ip-down /etc/ppp/ip-down.d/

clash开启http/https/socks5代理

  1. 安装clash
  • 下载安装包

    sudo wget https://github.com/Dreamacro/clash/releases/download/v1.6.5/clash-linux-armv7-v1.6.5.gz

  • 解压

    sudo gunzip clash-linux-armv7-v1.6.5.gz

  • 移动到系统目录

    sudo mv clash-linux-armv7-v1.6.5 /usr/local/bin/clash

  • 设置可执行权限

    sudo chmod +x /usr/local/bin/clash

  1. 设置配置文件
    sudo vi ~/.config/clash/config.yaml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # port of HTTP
    port: 7890

    ## port of SOCKS5
    socks-port: 7891

    # `allow-lan` must be true in your config.yml
    allow-lan: true

    # set log level to stdout (default is info)
    # info / warning / error / debug / silent
    log-level: info

    # A RESTful API for clash
    #使用0.0.0.0可以使用局域网设备访问
    external-controller: 0.0.0.0:8080

    mode: Rule

    sudo clash启动,这样我们就拥有http/https/socks5的代理服务器了,任意设备只要配置树莓派的ip地址和对应协议的端口号即可代理请求,如果有公网ip或内网穿透,有域名+ddns解析服务器那么在外网也可以代理

  2. 设置开机启动服务

    sudo vi /etc/systemd/system/clash.service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [Unit]
    Description=Clash Service
    After=network.target

    [Service]
    Restart=on-abort
    LimitNOFILE=1048576
    ExecStart=/usr/local/bin/clash -d /home/pi/.config/clash

    [Install]
    WantedBy=multi-user.target
  3. 转发所有请求 sudo vi /etc/sysctl.conf

  • ipv4的请求,修改

    net.ipv4.ip_forward=1

  • ipv6的请求,修改

    net.ipv6.conf.all.forwarding = 1
    刷新设置

    sudo sysctl -p
    流量强制转发到ppp0
    sudo iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE

    dnsmasq + dnsmasq-china-list 本地DNS分流策略

  1. apt-get安装dnsmasq

    sudo apt install dnsmasq

  2. 设置配置文件

    sudo vi /etc/dnsmasq.conf

    1
    2
    3
    no-resolv
    server=8.8.8.8
    server=8.8.4.4
  3. 用dnsmasq-china-list设置白名单,用运营商dns去解析,其它用google的dns解析,否则解析到的ip并不是访问速度快的最适合的ip,导致网站和App的访问速度太慢了,所以太干净的dns解析也不好

  • 运营商分配的DNS地址假设为223.5.5.5,切换到su用户,拉取不需要走PPTP的地址用运营商分配的DNS地址去解析
    1
    curl -s https://raw.githubusercontent.com/felixonmars/dnsmasq-china-list/master/accelerated-domains.china.conf|sed 's/114.114.114.114/223.5.5.5/g' >/etc/dnsmasq.d/accelerated-domains.china.223.5.5.5.conf
  1. 重启dnsmasq

    service dnsmasq restart

  2. 测试分流效果

  • 未使用本地dns解析
    dig google.com @223.5.5.5
  • 使用本地dns解析
    dig google.com @127.0.0.1
  1. 添加自定义域名到白名单
    echo 'server=/你需要的域名/223.5.5.5' >>/etc/dnsmasq.d/accelerated-domains.china.223.5.5.5.conf

    DDNS动态域名解析

    如果家里是公网IP,可以使用DDNS给域名绑定动态ip,no-ip是ddns解析服务,免费赠送域名无需备案,速度还不错,每个月需要点一次邮件续期,官网的ip更新方式在树莓派下更新失败,折腾一段时间后,发现用ddclient可以正常更新,但是注意如果当前状态是pptp开启的情况下,获取到的外网ip可能不是运营商的公网ip,在配置了智能路由的情况下需使用无需PPTP访问的网站获取外网ip
    1
    sudo apt-get install ddclient
    设置配置文件 /etc/ddclient.conf
    1
    2
    3
    4
    5
    6
    protocol=noip
    use=web, web=获取外网ip的网站
    server=dynupdate.no-ip.com
    login=用户名(邮箱)
    password='密码'
    你的域名


C语言是面向过程的,函数+结构体组成,C++是在C语言的基础上增加面向对象的能力,兼容C语言但是C语言不兼容C++,是Android中NDK开发的主要语言,对于学习NDK开发而言重要性是不言而喻的,接下来总结C++的学习,欢迎一起学习和交流~


打印日志

1
2
3
4
5
6
void print() {
// c的打印方式
printf("c++ 语言的学习!\n");
// c++的打印方式,endl == \n
cout << "c++ 语言的学习!" << endl;
}

交换两个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// c的交换方式
void numberChange(int *num1, int *num2){
int temp;
temp = *num1;
*num1 = *num2;
*num2 = temp;
}
// c++的交换方式
void numberChange2(int &number1,int &number2){
// 内存地址与外部一致
cout << "n1内存地址:" << &number1 << ", n2内存地址:" << &number2 <<endl;

int temp;
temp = number1;
number1 = number2;
number2 = temp;
}
int main(){
int num1 = 10;
int num2 = 20;
// numberChange(&num1, &num2);
// cout << "n1:" << num1 << ", n2:" << num2 << endl;
cout << "n1内存地址:" << &num1 << ", n2内存地址:" << &num2<<endl;
numberChange2(num1, num2);
cout << "n1:" << num1 << ", n2:" << num2 << endl;
return 0;
}

通过内存地址修改某个值

1
2
3
4
5
6
7
8
9
10
11
12
int n1 = 999;
int n2 = n1;
// n1 n2内存地址不同
cout << &n1 << "---" << &n2 << endl;

int n3 = 999;
int &n4 = n3;
// n3 n4内存地址相同
cout << &n3 << "---" << &n4 << endl;
n4 = 777;
// n3 n4都改为777
cout << n3 << "---" << n4 << endl;

定义结构体

1
2
3
4
5
6
7
8
typedef struct {
char name[20];
int age;
}Student;
int main(){
Student student = {"小明", 30};
return 0;
}

函数重载

1
2
3
4
5
6
7
8
9
10
11
12
int add(int number1,int number2 ){
return number1 + number2;
}

int add(int number1,int number2,int number3){
return number1 + number2 + number3;
}

// 支持默认形参,直接重载1-4个参数方法
//int add(int number1 = 1,int number2 = 2,int number3 = 3, int number4 = 4){
// return number1 + number2;
//}

类的定义

  • Teacher.h中声明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    #include <iostream>
    using namespace std;
    class Person{
    private:
    char *name;
    int age;
    public:
    Person(char * name, int age):name(name){
    this -> age = age;
    cout << "Person 构造函数" << endl;
    }
    void print(){
    cout << this -> name << "," << this -> age << endl;
    }
    };

    // 默认私有继承,子类中可以访问父类的成员,类外不行
    // 公开继承,子类在类外也可以访问父类的成员 class Student:public Person
    class Student: Person{
    private:
    char * course;
    Student(char * name, int age, char * course): Person(name, age),course(course){
    cout << "Student 构造函数" << endl;
    }
    public:
    void test(){
    print();
    }
    // 构造函数顺序:Person,Student,析构函数顺序Student,Person
    ~Student(){
    cout << "Student 析构函数" << endl;
    }
    };
    class Teacher{
    private:
    char *name;
    int age;
    public:
    void setAge(int age);
    void setName(char *name);
    int getAge();
    char* getName();
    // 静态变量声明再实现
    static int id;
    // 友元函数可以访问/修改所有私有成员
    friend void updateAge(Teacher *teacher, int age);
    };
  • Teacher.cpp中定义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    #include "Teacher.h"
    #include <string.h>
    using namespace std;

    public:
    int Teacher::id = 9
    // 不需要::
    void updateAge(Teacher *teacher, int age){
    teacher -> age = age;
    }
    void Teacher::setAge(int age){
    this->age = age;
    }
    void Teacher::setName(char *name){
    this -> name = name;
    }
    int Teacher::getAge(){
    return this -> age;
    }
    char* Teacher::getName(){
    return this -> name;
    }
    // 先调两个参数的构造函数,再调用一个参数的构造函数
    Teacher(char *name):Teacher(name, 87){
    this.name = name;
    }
    // 拷贝构造函数,被const修饰只能写在类中访问私有成员
    Teacher(const Teacher &teacher){
    this -> age = teacher.age;
    // 浅拷贝
    // this -> name = teacher.name;
    // 如果存在堆成员采用深拷贝
    this -> name = (char *)malloc(sizeof(char*) * 10);
    strcpy(this -> name, teacher.name);
    }
    Teacher(char *name, int age){
    // 堆区创建的name
    this -> name = (char *)malloc(sizeof(char*) * 10);
    strcpy(this -> name, name);
    this.age = age;
    }
    ~Teacher(){
    // 释放堆区创建的属性
    if (this -> name){
    free(this -> name);
    this -> name = NULL;
    }
    }


    int main(){
    // 栈空间分配内存
    Teacher teacher;
    teacher.setAge(99);
    teacher.setName("李华");
    cout << "name:" << teacher.getName() << ", age:" << teacher.getAge() << endl;

    // free 不会调析构函数,malloc不会调构造函数
    // 堆空间分配内存
    Teacher *teacher2 = new Teacher(); //堆区 手动释放
    teacher2 ->setAge(88);
    teacher2 ->setName("李华成");
    // 堆空间的内存需要手动释放
    if (teacher2){
    delete teacher2; // 析构函数一定执行
    teacher2 = NULL;
    }

    Teacher teacher1("张三",34); //栈区 弹栈自动释放
    // 不会调用拷贝构造函数
    // Teacher teacher2;
    // teacher2 = teacher1;
    Teacher teacher2 = teacher1; // 调拷贝构造函数
    Teacher *teacher1 = new Teacher("李四",35);
    Teacher *teacher2 = teacher1; // 不会调拷贝构造函数
    int number = 9;
    int number2 = 8;
    // 常量指针
    const int *numberP1 = &number;
    *numberP1 = 100; // 不允许修改常量指针存放地址对应的值
    numberP1 = &number2; // 允许修改常量指针存放的地址
    // 指针常量
    int* const numberP2 = &number;
    *numberP2 = 100; // 允许修改指针常量存放地址对应的值
    numberP2 = &number2; // 不允许修改指针常量存放的地址
    // 常量指针常量
    const int* const numberP3 = &number;
    *numberP3 = 100; // 不允许修改常量指针常量存放地址对应的值
    numberP3 = &number2; // 不允许修改常量指针常量存放的地址
    return 0;
    }

    自定义命名空间

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    namespace MyNameSpace1{
    namespace MyNameSpace2{
    namespace MyNameSpace3{
    void out(){
    cout << "" << endl;
    }
    }
    }
    }
    int main(){
    // 第一种调用方式
    using namespace MyNameSpace1::MyNameSpace2::MyNameSpace3;
    out();
    // 第二种调用方式
    // MyNameSpace1::MyNameSpace2::MyNameSpace3::out();
    return 0;
    }

    可变参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <stdarg.h> // 支持可变参数
    using namespace std;
    void sum(int count, ...){
    va_list vp; // 可变参数的动作
    // count 内部需要一个存储地址参考值,否则无法处理存放参数的信息,也用于循环遍历
    va_start(vp, count);
    int number = va_arg(vp, int);
    cout << number << endl;

    number = va_arg(vp, int);
    cout << number << endl;

    number = va_arg(vp, int);
    cout << number << endl;

    va_end(vp);
    }

    int main(){
    sum(54,6,7,8)
    return 0;
    }

    友元类

  • java反射的实现原理 native层利用友元类访问私有成员
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # include<iostream>
    using namespace std;

    class ImageView{
    private int viewSize;
    friend class Class; // 声明Class为友元类
    };
    class Class{
    public ImageView imageView;
    void changeViewSize(int size){
    imageView.viewSize = size;
    }
    int getViewSize(){
    return imageView.viewSize
    }
    };

    int main(){
    Class imageViewClass;
    imageViewClass.changeViewSize(600);
    return 0;
    }

    运算符重载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    class Position{
    private:
    int x, y;
    public:
    Position(int x, int y):x(x), y(y) {}
    void setX(int x){
    this -> x = x;
    }
    void setY(int y){
    this -> y = y;
    }
    int getX(){
    return this -> x;
    }
    int getY(){
    return this -> y;
    }

    // 如果没有&会创建副本,影响性能
    Position operator + (Position &position) {
    int x = this -> x + position.x; // 类里可以访问私有成员
    int y = this -> y + position.y;
    return Position(x, y);
    }

    void operator ++(){ //++对象
    this -> x = this -> x + 1;
    this -> y = this -> y + 1;
    }

    void operator ++ (int) { //对象++
    this -> x = this -> x + 1;
    this -> y = this -> y + 1;
    }

    friend void operator << (ostream &_START, Position position){
    _START << position.x << "," << position.y << endl;
    }

    friend ostream & operator >> (ostream &_START, Position position){
    _START << position.x << "," << position.y << endl;
    return _START; // 可多次打印
    }

    friend istream & operator >> (istream &_START, Position position){
    _START >> position.x;
    _START >> position.y;
    return _START; // 可多次打印
    }
    };

    class ArrayClass {
    private:
    int size = 0;
    int *arrayValue;
    public:
    void set(int index, int value){
    arrayValue[index] = value;
    size += 1;
    }
    int getSize(){
    return this -> size;
    }
    int operator[](int index){
    return this -> arrayValue[index];
    }
    void printfArrayClass(ArrayClass arrayClass){
    for (int i = 0; i < arrayClass.getSize(); ++i){
    cout << arrayClass[i] << endl;
    }
    }
    };


    int main(){
    Position position1(100, 200);
    Position position2(200, 300);
    Position res = position1 + position2;
    cout << res.getX() << "," << res.getY() << endl;
    Position pos(1, 2);
    pos ++;
    ++ pos;
    cout << pos.getX() << "," << pos.getY() << endl;
    cout >> position2 >> position2 >> position2;
    Position position;
    cin >> position;
    cout << position.getX() << "," << position.getY() << endl;

    ArrayClass arrayClass;
    arrayClass.set(0, 1000);
    arrayClass.set(1, 2000);
    arrayClass.set(2, 3000);
    printfArrayClass(arrayClass);
    return 0;
    }

    多继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    #include <iostream>
    using namespace std;
    class BaseActivity1{
    public:
    void onCreate(){
    cout << "BaseActivity1 onCreate" << endl;
    }
    void onStart(){
    cout << "BaseActivity1 onStart" << endl;
    }
    };
    class BaseActivity2{
    public:
    void onCreate(){
    cout << "BaseActivity2 onCreate" << endl;
    }
    void onStart(){
    cout << "BaseActivity2 onStart" << endl;
    }
    };
    class BaseActivity3{
    public:
    void onCreate(){
    cout << "BaseActivity3 onCreate" << endl;
    }
    void onStart(){
    cout << "BaseActivity3 onStart" << endl;
    }
    };
    class MainActivity1: public BaseActivity1,BaseActivity2,BaseActivity3{
    void onCreate(){
    cout << "MainActivity1 onCreate" << endl;
    }

    };
    int main(){
    MainActivity1 mainActivity1;
    mainActivity1.onCreate();
    // mainActivity1.onStart(); 有歧义,不知道调哪个父类的onStart,子类重写onStart就没问题
    mainActivity1.BaseActivity1::onStart();
    mainActivity1.BaseActivity2::onStart();
    mainActivity1.BaseActivity3::onStart();

    return 0;
    }

    class Object{
    public:
    int number;
    };

    // 虚继承解决歧义问题
    class BaseActivity1:virtual public Object{

    };

    // 虚继承解决歧义问题,原理是把多个变量化成一份
    class BaseActivity2:virtual public Object{

    };

    class Son :public BaseActivity1,public BaseActivity2{
    public:
    int number; // 覆盖父类number
    }
    int main(){
    Son son;
    son.BaseActivity1::number = 1;
    son.BaseActivity2::number = 1;
    son.number = 1;
    return 0;
    }

    多态

  • 重写和重载
  • 同一个方法有不同的实现,父类指向子类
  • 动态多态:程序在运行期间才确定调用哪个类的函数
  • C++默认关闭多态,在父类上给方法增加virtual关键字开启多态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    #include <iostream>
    using namespace std;
    class BaseActivity{
    public:
    virtual void onStart(){
    cout << "BaseActivity onStart" << endl;
    }
    };
    class HomeActivity: public BaseActivity{
    public:
    void onStart(){
    cout << "HomeActivity onStart" << endl;
    }
    };
    class LoginActivity: public BaseActivity{
    public:
    void onStart(){
    cout << "LoginActivity onStart" << endl;
    }
    };
    void startToActivity(BaseActivity *baseActivity){
    baseActivity -> onStart();
    }

    void add(int number1, int number2){
    cout << number1 + number2 << endl;
    }

    void add(float number1, float number2){
    cout << number1 + number2 << endl;
    }

    void add(double number1, double number2){
    cout << number1 + number2 << endl;
    }
    int main(){
    BaseActivity *homeActivity = new HomeActivity();
    BaseActivity *loginActivity = new LoginActivity();
    startToActivity(homeActivity);
    startToActivity(loginActivity);
    if (homeActivity && loginActivity) delete homeActivity; delete loginActivity;

    add(100, 100);
    add(1.1f, 2.1f);
    add(23.2, 21.2);
    cout << endl;
    }

    纯虚函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #include <iostream>
    using namespace std;
    class BaseActivity{
    private:
    void setContentView(String layoutId){
    cout << "解析布局文件,反射" << endl;
    }
    public :
    void onCreate(){
    setContentView(getLayoutId());
    initView();
    initData();
    initListener();
    }
    //纯虚函数就是抽象函数,必须实现,虚函数virtual String getLayoutId();不是必须实现的
    virtual String getLayoutId() = 0;
    virtual void initView() = 0;
    virtual void initData() = 0;
    virtual void initListener() = 0;
    };
    // 如果子类没有实现纯虚函数就是抽象类,不能实例化
    class MainActivity: public BaseActivity{
    String getLayoutId(){
    return "R.layout.activity_main";
    }
    void initView(){

    }
    void initData(){

    }
    void initListener(){

    }
    };
    int main(){
    MainActivity mainActivity;
    return 0;
    }

    全纯虚函数

  • C++没有接口,所有的函数都是纯虚函数就是C++的接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    #include<iostream>
    using namespace std;
    Class Student{
    int _id;
    string name;
    int age;
    };
    // 定义接口
    class Student_DB{
    virtual void insertStudent(Student student) = 0;
    virtual void deleteStudent(int id) = 0;
    virtual void updateStudent(int id, Student student) = 0;
    virtual void queryStudent(Student student) = 0;
    }
    // 接口的实现类
    class Student_DBImpl: public Student_DB{
    public:
    void insertStudent(Student student){

    }
    void deleteStudent(int id){

    }
    void updateStudent(int id, Student student){

    }
    Student queryStudent(Student student){

    }
    }
    int main(){
    Student_DBImpl student_DBImpl;

    return 0;
    }

    回调

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    // 返回的数据对象
    class SuccessBean {
    public:
    String username;
    String userpwd;
    SuccessBean(String username, String userpwd):username(username),userpwd(userpwd){}
    }
    // 回调接口
    class ILoginResponse{
    public:
    virtual void loginSuccess(int code, string message, SuccessBean successBean) = 0;
    virtual void loginError(int code, string message) = 0;
    }
    // 登录操作
    void loginAction(String name, String pwd, ILoginResponse loginResponse){
    if (name.empty() || pwd.empty()){
    cout << "用户名或密码为空" << endl;
    return;
    }
    if ("admin" == name && "123" == pwd){
    loginResponse.loginSuccess(200, "登录成功", SuccessBean(name, "恭喜你成功登入"));
    loginResponse.loginError(404, "用户名或密码错误");
    }
    }
    // 接口的实现类
    class ILoginResponseImpl: public ILoginResponse{
    public:
    void loginSuccess(int code, string message, SuccessBean successBean){
    cout << "登录成功" << "code:" << code << "message" << message << "successBean" << successBean.username << "," << successBean.userpwd << endl;
    }
    void loginError(int code, string message){
    cout << "登录失败" << "code:" << code << "message" << endl;
    }
    }
    int main(){
    string username;
    cout << "请输入用户名.." << endl;
    cin >> username;
    string userpwd;
    cout << "请输入密码.." << endl;
    cin >> userpwd;
    ILoginResponseImpl iLoginResponseImpl;
    loginAction(username, userpwd, iLoginResponseImpl);
    return 0;
    }

    泛型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template <typename TT>
    void addAction(TT n1, TT n2){
    cout << "模版函数:" << n1 + n2 << endl;
    }

    int main(){
    addAction(1, 2);
    addAction(10.2f, 20.3f);
    addAction(545.34, 324.3);
    return 0;
    }


随着工作年限的增长,越来越意识到C语言的重要性,Android的底层是C和linux内核,Android中为提高安全性,防止反编译,防止二次打包,提升程序的执行效率都是用C去实现的,作为Android开发者,掌握C语言才能进行NDK开发,提高自己的核心竞争力,拓宽职业道路


指针与地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(){
int i = 100;
double d = 200;
printf("i的值是:%d\n", i); // 100
printf("d的值是:%lf\n", d); // 200

printf("i的值是:%d\n", *(&i)); // 100 取该地址的值
printf("d的值是:%lf\n", *(&d)); // 200 取该地址的值

int *intP = &i;
double *doubleP = &d;

*intP = 220; // 修改内存地址对应的值为220
printf("i的值是:%d\n", *intP); // 220 取该地址的值
printf("d的值是:%lf\n", *doubleP); // 200 取该地址的值
}

交换两个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void change(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
int main(){
int a = 100;
int b = 200;
printf("a的值为:%d\n", a); // 100
printf("b的值为:%d\n", b); // 200

change(&a, &b);

printf("a的值为:%d\n", a); // 200
printf("b的值为:%d\n", b); // 100
return 0;
}

多级指针

  • 指针变量存放的是内存地址,指针变量自己也有地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 多级指针
    void test1(){
    int num = 999;
    int * p1 = &num;
    int ** p2 = &p1;
    int *** p3 = &p2;
    printf("p1的值是:%p, p2的值是:%p, p3的值是:%p\n", p1, p2, p3);// p1存放num内存地址,p2存放p1内存地址,p3存放p2内存地址
    printf("p1的值是:%p, p2的值是:%p, p3的值是:%p\n", &p1, &p2, &p3);// p1,p2,p3自己的内存地址都不一样
    printf("p2的内存地址对应的值是:%d\n",**p2); // 999
    printf("p3的内存地址对应的值是:%d\n",***p3); // 999
    }

    数组指针

  • 指针变量在32位下占4个字节,64位下占8个字节,指针类型决定了sizeof,获取元素时的偏移
  • 数组变量就是一个指针,存放的是第一个元素的内存地址,也等于它自己的内存地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 数组指针
    void test2(){
    int arr[] = {1,2,3,4};
    int *arr_p = arr;
    int i = 0;
    for (i = 0; i < sizeof arr / sizeof(int); ++i){
    printf("下标为%d的值是%d\n", i,*(arr_p + i));
    printf("下标%d的内存地址是%p\n",i, arr_p + i); // 地址间隔4个字节
    }
    // 同一个地址
    printf("arr = %p\n", arr);
    printf("&arr = %p\n", &arr);
    printf("&arr[0] = %p\n", &arr[0]);

    // 取数组第二个值
    arr_p ++;
    printf("%d\n", *arr_p);

    // 超出范围,野指针
    arr_p += 200;
    printf("%d\n", *arr_p);
    }

    内存静态开辟和动态开辟

  • 动态开辟需要手动释放,手动释放后如果不赋值为NULL,就是悬空指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void dynamicAction(int num){
    // 堆区动态开辟1M内存
    int *arr = malloc(num * sizeof(int));
    printf("dynamicAction函数,arr自己的内存地址:%p,堆区开辟的内存地址:%p\n",&arr,arr);
    // dynamicAction函数,arr自己的内存地址:0x7ffee47e1480,堆区开辟的内存地址:0x7fdb5dd00000
    // 释放堆区开辟的内存
    if(arr){
    free(arr);
    arr = NULL; // 如果不赋值为NULL,就是悬空指针
    printf("dynamicAction函数2,arr自己的内存地址:%p,堆区开辟的内存地址:%p\n",&arr,arr);
    // dynamicAction函数2,arr自己的内存地址:0x7ffee47e1480,堆区开辟的内存地址:0x0
    }
    }
  • 静态开辟,使用栈内存,自动释放
    1
    2
    3
    4
    void staticAction(){
    int arr[6];
    printf("staticAction函数,arr自己的内存地址:%p,堆区开辟的内存地址:%p\n",&arr,arr);
    }

    realloc重新开辟内存

  • 扩容内存时,地址不一定连续,物理内存被其它占用会返回新的地址,所以内存重新开辟时需要传入指针和总大小进行拷贝
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    int main(){
    int num;
    printf("请输入新的个数:");
    // 获取用户输入的值
    scanf("%d", &num);

    int *arr = (int *)malloc(arr, sizeof(int) * num);
    for (int i = 0; i < num; ++i){
    arr[i] = i + 10001;
    }
    printf("开辟的内存地址:%p\n", arr);
    // 开辟新的内存空间
    int newNum;
    printf("请输入新增加的个数");
    scanf("%d", &newNum);
    int *newArr = (int *)realloc(arr, sizeof(int) * (num + newNum));
    if (newArr){
    int j = num;
    for (;j < (num + newNum); ++j){
    arr[j] = j + 10001;
    }
    printf("新开辟的内存地址:%p\n", newArr);
    }
    // 释放内存操作
    if (newArr){
    free(newArr);
    newArr = NULL;
    arr = NULL;
    }
    else{
    free(arr);
    arr = NULL;
    }
    return 0;
    }

    函数指针

  • 使用函数指针实现回调,相当于Java的接口
  • 函数指针和它自己的内存地址相同
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void add(int num1, int num2){
    printf("num1 + num2 = %d\n", (num1 + num2));
    }

    void mins(int num1, int num2){
    printf("num1 - num2 = %d\n", (num1 - num2));
    }

    // 传递函数指针
    void operate(void(*method) (int,int),int num1, int num2){
    method(num1,num2);
    }

    void test4(){
    operate(add, 10, 20);
    void(*method)(int,int) = mins;
    operate(method, 100, 20);
    // 函数指针和它自己的内存地址相同
    printf("%p, %p\n", add, &add);
    }

    生成随机数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdlib.h>
    #include <time.h>
    void test5(){
    srand((unsigned) time(NULL));
    int i;
    for (i = 0; i < 10; ++i) {
    printf("随机数%d\n", rand() % 100);
    }
    }

    复制字符串

    1
    2
    3
    4
    5
    6
    7
    #include <string.h>
    void test6(){
    char string[10];
    char* str1 = "abcdefghi";
    strcpy(string, str1);
    printf("%s\n", string);
    }

    字符串获取长度

  • 也可以直接使用strLen
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    void getLen(int *resultLen, char *str){
    int count = 0;
    while(*str){
    str ++;
    count ++;
    }
    *resultLen = count;
    }
    /**
    * C/C++会把数组优化成指针提高效率,导致长度计算错误
    int getLen(int arr[]){
    int len = sizeof(arr) / sizeof(int);
    return len;
    }
    */
    void test7{
    char str1[] = {'H','e','l','l','o','\0'}; // 遇到\0停下来
    str1[2] = 'z'; // 栈空间,允许修改
    printf("第一种方式:%s\n", str1);
    int count;
    getLen(&count, str1);
    printf("长度为:%d\n", count);
    char *str2 = "Hello"; // 结尾隐式增加\0
    // str2[2] = 'z'; 会报错,不允许修改全局区的字符串
    printf("第二种方式:%s\n", str2);
    }

    字符串转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void convertInt(){
    char *num = "1";
    int res = atoi(num);
    printf("转换结果:%d\n", res);
    }

    void convertDouble(){
    double resD = atof(num);
    printf("转换结果:%lf\n", resD);
    }

    字符串比较

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void test8(){
    char *str1 = "Hello";
    char *str2 = "hello";
    // int res = strcmp(str1, str2); // 区分大小写
    int res = strcmpi(str1, str2); // 不区分大小写
    if (!res){
    printf("相等");
    }else{
    printf("不相等")
    }
    }

    字符串查找子串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void test9(){
    char *text = "hello world";
    char *subtext = "w";
    // 从搜索到子串的下标位置截取到末尾
    char *pop = strstr(text, subtext);
    if (pop){
    printf("查找到了,pop的值是%s\n",pop);
    }
    else{
    printf("没有查找到,subtext的值是%s\n",subtext);
    }
    int index = pop - text;
    printf("%s第一次出现的位置是:%d\n",subtext, index);
    }

    字符串拼接

    1
    2
    3
    4
    5
    6
    7
    8
    void test10(){
    char destination[25];
    char *blank = "--到--", *CPP = "C++", *Java = "Java";
    strcpy(destination, CPP); // 先拷贝到数组
    strcat(destination, blank); // 拼接blank
    strcat(destination, Java); // 拼接Java
    printf("拼接后的结果%s\n", destination);
    }

    字符串截取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    void substring1(char *res, char *str, int start, int end){
    char *temp = str;
    int index = 0; // 当前截取的位置
    while(*temp){
    if (index > start && index < end){
    *res = *temp;
    res ++;
    }
    temp ++;
    index ++;
    }
    }
    void substring2(char **res, char *str, int start, int end){
    char *temp = str;
    char resArr[end - start]; // 方案1. 临时变量在栈中分配内存,方法结束会被释放
    // char *resArr = malloc(end - start); // 方案2. 堆中开辟内存
    int index = 0;
    for (int i = start, i < end; ++i){
    resArr[index] = *(temp + i);
    index ++;
    }
    // 二级指针的一级指针等于test11的res一级指针
    // *res = resArr; // 方案2. 结果指向堆中的数组,方法结束后也不能释放所以不推荐
    strcpy(*res, resArr); // 方案1. 拷贝到数组
    printf("截取后的结果:%s\n", resArr);
    }
    void substring3(char *res, char *str, int start, int end){
    for (int i = start; i < end; ++i){
    *(result++) = *(str + i);
    }
    }
    void substring4(char *res, char *str, int start, int end){
    strncpy(result, str + start, end - start);
    }
    void test11(){
    char *str = "hello";
    char *res;
    substring1(res, str, 1, 4);
    // substring2(&res, str, 1, 4);
    printf("截取后的内容是:%s",res);
    }

    大小写转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    void lower(char *dest, char *text){
    int *temp = text;
    while(*temp){
    *dest = tolower(*temp);
    temp ++;
    dest ++;
    }
    *dest = '\0'; // 结尾增加\0避免打出系统值
    printf("name:%s\n", text);
    }
    void test12(){
    char *text = "hello";
    char dest[20];
    lower(dest, text);
    printf("小写转换后的结构是:%s\n", dest);
    }

    结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    struct Dog{
    char name[10];
    int age;
    char sex;
    }
    struct Person{
    char *name;
    int age;
    char sex;
    } person1 = {"小明", 21, 'M'}
    struct Study{
    char *studyContent;
    }
    struct Student{
    char name[10];
    int age;
    char sex;
    struct Study study; // 引用外部结构体,声明结构体对象
    // 定义结构体并声明结构体对象
    struct Wan {
    char *wanContent;
    }wan;
    }
    // 匿名结构体定义别名
    typedef struct {
    char *name;
    int age;
    char sex;
    } Cat;
    int main(){
    struct Dog dog;
    strcpy(dog.name, "旺财");
    dog.age = 2;
    dog.sex = 'M';
    printf("name:%s, age:%d, sex:%c \n", dog.name, dog.age, dog.sex);

    struct Student student = {"小红", 18, 'F', {"学习C"}, {"王者荣耀"}};
    printf("name:%s, age:%d, sex:%c, study:%s, wan:%s \n", student.name, student.age, student.sex, student.study.studyContent, student.wan.wanContent);

    struct Dog dog2 = {"旺财2"4, 'M'};
    struct Dog *dogp = &dog2;
    dogp -> age = 3;
    dogp -> sex = 'F';
    strcpy(dogp->name, "旺财3");
    printf("name:%s, age:%d, sex:%c \n", dogp->name, dogp->age, dogp->sex);
    free(dogp);
    dogp = NULL;

    struct Dog dogArr[10] = {
    {"旺财4"4, 'M'},
    {"旺财5"5, 'M'},
    {"旺财6"6, 'M'},
    {},
    {},
    {},
    {},
    {},
    {},
    {}
    };
    struct Dog dog9 = {"旺财9"9, 'M'};
    // dogArr[9] = dog9;
    *(dogArr + 9) = dog9;
    printf("name:%s, age:%d, sex:%c \n", dog9.name, dog9.age, dog9.sex
    // 动态申请内存
    struct Dog *dogArr2 = malloc(sizeof(struct Dog) * 10);
    strcpy(dogArr2->name, "大黄1");
    dogArr2 -> age = 2;
    dogArr2 -> sex = 'M';
    printf("name:%s, age:%d, sex:%c \n", dogArr2->name, dogArr2->age, dogArr2->sex);
    // 指针移到第8个元素
    dogArr2 += 7;
    strcpy(dogArr2 -> name, "大黄8");
    dogArr2 -> age = 3;
    dogArr2 -> sex = 'M';
    printf("name:%s, age:%d, sex:%c \n", dogArr2->name, dogArr2->age, dogArr2->sex);
    free(dogArr2);
    dogArr2 = NULL;

    Cat *cat = malloc(sizeof(Cat)); // 结构体指针
    return 0;
    }

    枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    enum CommentType{
    TEXT = 10,
    TEXT_IMAGE,
    IMAGE
    };
    typedef enum {
    TEXT1 = 10,
    TEXT_IMAGE1,
    IMAGE1
    } CommentType1;
    int main(){
    enum CommentType commentType = TEXT;
    printf("%d\n", commentType);

    CommentType1 commentType1 = TEXT1;
    printf("%d\n", commentType1);
    return 0;
    }

    文件读写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    void readFile(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test.txt";
    FILE *file = fopen(fileName,"r"); // 此文件必须存在
    if (!file){
    printf("文件打开失败,请查看路径");
    exit(0);
    }

    char buffer[10]; // 创建buffer读取
    while(fgets(buffer, 10, file)){
    printf("%s", buffer);
    }
    // 关闭文件
    fclose(file);
    }

    void writeFile(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test1.txt";
    FILE *file = fopen(fileName, "w"); // 此文件可以不存在
    if (!file){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    fputs("这是我写入的测试内容", file);
    fclose(file);
    }

    文件复制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void copyFile(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test.txt";
    char *fileNameCopy = "/Users/JessieKate/CLionProjects/TestProject/testCopy.txt";
    FILE *file = fopen(fileName, "rb");
    FILE *fileCopy = fopen(fileNameCopy, "wb");
    if (!file || !fileCopy){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    int buffer[514]; // 缓存数组
    int len; // 每次读取的长度

    // fread 读入缓存buffer, 偏移数量, 读取字节数写入缓存
    while((len = fread(buffer, 1, 514, file)) > 0){
    fwrite(buffer, len, 1, fileCopy);
    }

    fclose(file);
    fclose(fileCopy);
    }

    文件大小

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void getSize(){
    char *fileName = "/Users/JessieKate/CLionProjects/TestProject/test.txt";
    FILE *file = fopen(fileName,"r");
    if (!file){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    fseek(file, 0, SEEK_END); //从0开始挪动到文件结束
    long fileSize = ftell(file); //读取file的信息
    printf("%s文件的字节大小是:%ld",fileName, fileSize);
    }

    文件加密解密

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    void encrypt(){
    char * fileName = "/Users/JessieKate/CLionProjects/TestProject/Image.jpg";
    char * fileNameEncode = "/Users/JessieKate/CLionProjects/TestProject/Image_encode.jpg";
    FILE * file = fopen(fileName, "rb");
    FILE * fileEncode = fopen(fileNameEncode,"wb");

    if (!file || !fileEncode){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    char *password = "123456";
    // 加密,破坏文件,解密,还原文件
    int c;
    int index = 0;
    int pass_len = strlen(password); // 获取密码的长度

    // fgetc 返回EOF = end of file
    while((c = fgetc(file)) != EOF){
    // 循环获取密码的每个字符,1,2,3,4,5,6,1,2,3...
    char item = password[index % pass_len];
    printf("item:%c%\n",item);
    fputc( c ^ item, fileEncode);
    index++;
    }
    fclose(file);
    fclose(fileEncode);
    }

    void decrypt(){
    char * fileNameEncode = "/Users/JessieKate/CLionProjects/TestProject/Image_encode.jpg";
    char * fileNameDecode = "/Users/JessieKate/CLionProjects/TestProject/Image_decode.jpg";
    FILE * file = fopen(fileNameEncode, "rb");
    FILE * fileDecode = fopen(fileNameDecode,"wb");
    if (!file || !fileDecode){
    printf("文件打开失败,请查看路径");
    exit(0);
    }
    char *password = "123456";
    int c;
    int index = 0;
    int pass_len = strlen(password);

    // fgetc 返回EOF = end of file
    while((c = fgetc(file)) != EOF){
    char item = password[index % pass_len];
    fputc( c ^ item, fileDecode);
    index++;
    }
    fclose(file);
    fclose(fileDecode);
    }