`编程`分类下的文章

编程

iOS线性布局?

前几天通过比较了iOS和Android端BLE蓝牙开发时,订阅Characteristic的不同,得出了Android订阅特征相当复杂的结论。但是在结尾我也替Android开发洗了一次白,因为今天说的这个东西,在iOS上非常难搞,而在Android上相当容易!

Context

新的码表产品,是一款为广大骑行爱好者省钱的GPS码表!

这个硬件会记录骑行时候的速度,位置,海拔,心率,踏频,等等等等信息,并在表盘显示实时的状态。所以,手机的用处就是,将骑行记录导出,然后对数据进行备份,为骑行者分析骑行过程。

咱们不仅仅是价格很厉害,在同步方式上也很厉害。因为我们可以通过Wi-Fi同步,而不仅仅是蓝牙同步。Wi-Fi的速度可比蓝牙高得多。不过这些都不是今天的主题,今天是要说app中,展示某一次骑行详情时候的图表展示。

废话不多说,先上图

iOS
Android

这里的图表的表项,是不确定数量的。比如说,这个用户没有踏频器的话,那么数据分析中就会没有踏频的图表。没有心率计的话,就会没有心率的图表。

Android

Android实在是,太方便了吧!

用了LinearLayout之后,我们把所有有可能出线的图表,都写在LinearLayout中,外面再套一个ScrollView。

不需要的图表我们直接把它setVisibility(View.GONE)就完事儿了,后面的图表会紧紧的挨上来,不会留出一个大空白来。

而且ScrollView会随着LinearLayout的大小来确定是否可以滑动,最多可以滑动多少距离。

iOS

没有LinearLayout,只有frame或者auto layout,你说怎么办?

用框架!

是的,有MyLinearLayout,很方便,就是对标Android的LinearLayout做的,其原理也是逃不掉frame和auto layout的。

如果我用框架做,那还写这篇干什么呢~

因为如果不手动写,是完全不会遇到UIScrollView和AutoLayout的一个大坑的!

Step 1

想法很简单,我只要给一个变量叫做lastChart,其实就好了~

@interface ChartViewController () {

    UIView *lastChart; //用了表示上一个图表的指针

    UIScrollView *scrollV;
    UIView *contentView;
    __weak ActivityDetailPresenter *presenterReference;

    ChartBlockView *distanceView;
    ChartBlockView *timeView;
    ChartBlockView *cadenceView;
    ChartBlockView *heartView;

}
@end

这里ChartBlockView表示图表,他是UIView的子类,所以lastChart完完全全可以引用他们。

Step 2

每次从Presenter中reload图表数据的时候,我会一个图表一个图表的去渲染。

比如,我先去检查,有没有速度信息,有的话就搞一个速度的表,没有的话就跳过。

再获取踏频的数据,有数据搞一个踏频的表,没有的话就跳过。

每次在渲染一个表的时候,会去检查lastChart是否为nil,如果为nil,那top就以顶端为参考;如果不为nil,那top就以lastChart的bottom做为参考。

根据这个准则,便很容易实现。

Step 3

当我兴高采烈的run了之后,发现可以显示,但是scrollview完全不能滑动!

查看了view Hierarchy,发现UIScrollView的ContentSize竟然是(0,0)!

因为……autolayout并不能填充scrollview……

所以,后来我又花了些时间,解决了这个问题……

End

所以,iOS没有LinearLayout还是蛮讨厌的……

虽然iOS原生推出了一个新的控件,就是LinearLayout,但是需要iOS 11……

预知后事如何,请听下回分解……

阅读剩下更多

编程

扫码登录(3)

Context

Http是在tcp的基础上做了高级封装,变成了一个被动的,短链接请求.请求完了就把连接断开了,下一次要请求,就需要再一次进行tcp握手,然后通信……在前面第一篇中,我用了定时轮训的方式问服务器,当前这个二维码是否登录,从功能的角度来说,应该是没有问题的,而且写起来也是特别的方便.

不过仔细的想一想,其实还是比较浪费资源的.从信息论的角度来说,中间的N次质询,相当于没有任何的信息量(请不要深究这一句),所以,我们不用http来更新扫码状态!

我们用

Web Socket!

Web Socket

关于websocket的定义,解释什么的,百度一下,你全都知道了,所以我就不在这个地方多说.

只说明一点,websocket屌的飞起,和socket的操作一毛一样,关键是服务器终于可以主动的发消息给客户端啦!

他算是一个长连接,握手之后,就开始了肮脏的通信,通信完了,一方可以发起关闭操作,然后……,最后连接就拆掉了.

Django

websocket怎么说也就只能算是对web服务器的一个扩充,如何在已有的web服务器上快速集成一个websocket的服务,才是比较重要的~

由于之前用的是django做服务器,所以我们就找找给django加上websocket的方法~

在他的官方文档中提到了很多可用的开源app,其中有一个叫做channels的库,他用了asgi,直接可以快速集成在django中,牛的一批.

Channels

这个库的文档,真的是无力吐槽,我真是集成的很辛苦

我们来简化一下他的文档吧!

Router

和django一样,websocket的连接也要有个路由的,我们先建立一个py文件

from channels.routing import route
# 这个文件等会儿解释
from users.webClient import connection_handler, disconnection_handler,subscribe_token

channel_routing = [
    route("websocket.connect", connection_handler, path=r"/ws/$"),
    route("websocket.disconnect", disconnection_handler, path=r"/ws/$"),
    route("websocket.receive", subscribe_token, path=r"/ws/$"),
]

这里的websocket.connect,websocket.disconnect,websocket.receive相当于是websocket的三种事件,已连接、已断开、收到消息

中间那个参数和django里面的那个描述一样,就是该路由对应的处理方法.

Handler

现在来解释一下上一节说的 users.webClient这个文件

# -*- coding: utf-8 -*-

import json
from channels.sessions import channel_session
from users.qrcode_helper import *


@channel_session
def connection_handler(message):
    message.reply_channel.send({"text": json.dumps({"status":False,"msg":"connected"})})


@channel_session
def subscribe_token(message):
    try:
        obj = json.loads(message.content["text"])
        tokens = obj["token"]
        qrcode_record = fetch_qrcode_record_with_token(token=tokens)
        if qrcode_record is not None:
            if is_token_pass_the_authentic(tokens):
                message.reply_channel.send(
                    {"text": json.dumps({"status": True, "msg": "succeed"})})
            else:
                message.reply_channel.send(
                    {"text": json.dumps({"status": False, "msg": "succeed"})})
                qrcode_record.webSocket_session = str(message.content["reply_channel"])
                qrcode_record.save()
                # link token to this channel
        else:
            message.reply_channel.send({"text": json.dumps({"status": "-1", "msg": "token invalid"})})
            message.reply_channel.send({"close": True})
    except Exception as e:
        message.reply_channel.send({"text": e.message})
        message.reply_channel.send({"close": True})


@channel_session
def disconnection_handler(message):
    channel = str(message.content["reply_channel"])
    qrcode_records = fetch_qrcode_record_with_web_socket_channel(channel)
    if qrcode_records is not None:
        for qrcode_record in qrcode_records:
            qrcode_record.webSocket_session = ""
            qrcode_record.save()

很长,也不用仔细看,因为里面主要做了扫码登录里面的事情,如果只是要集成一下websocket的话,不用关心这些方法里面的操作,只要注意这里每个方法的传入参数 message

Message有那些用处呢?

1.通过message.channel或者message.reply_channel可以获取到连接的通道(请暂时不要使用高级操作)
2.message.content其实是一个字典,这个字典遵循了ASGI的消息模型,里面有text,reply_channel等等字段

在我的处理代码里面,我从message.content里面取出了reply_channel,他的value是一个长得很奇怪的字符串(我把它类比成文件描述符把),毕竟要有很多很多很多连接的话,它们的文件描述符不能重复对吧,所以就又长又难看,比如像daphne.response.FSITXWDDzG!IOtDNILqzh

有了这个reply_channel,在后面主动发送消息下去的时候,就可以指定连接发送了~

Attention

注意,这里的websocket.connect的handler里面一定一定,千万千万要给反馈消息回去,不然浏览器(只测试了Chrome的)会以为连接超时的!然后给你一个503 bad gateway,之后就主动断开连接了,留你一脸蒙逼的在那儿傻看着.

Send

现在我们可以看看之前第一章中的app调用的扫码接口了

@csrf_exempt
@require_POST
@pass_auth
@require_parameter(["code"])
def allow_the_qrcode_login(request):
    code = request.POST["code"]
    user = get_user_from_response_session(request)
    qr_record = fetch_qrcode_record_with_code(code)
    if user is not None and qr_record is not None:
        if qr_record.user_id is not None:
            return JsonResponse({"msg": "expired", "status": -1}, status=400)
        qr_record.user_id = user.user_uuid
        qr_record.status = True
        qr_record.save()
        if qr_record.webSocket_session is not None:
            # send new status to the web socket
            channel = Channel(name=qr_record.webSocket_session)
            content = {"text": json.dumps({"status": True, "msg": "succeed"})}
            channel.send(content)
            pass
        return JsonResponse({"msg": "succeed", "status": 0}, status=200)
    return JsonResponse({"msg": "code not existed", "status": -400}, status=400)

这里加了一些些小的改动,我再把他取出来

if qr_record.webSocket_session is not None:
    channel = Channel(name=qr_record.webSocket_session)
    content = {"text": json.dumps({"status": True, "msg": "succeed"})}
    channel.send(content)

websocket.receive的时候,我把websocket的文件描述符记录在了qrcode的记录中,这样扫描某一个qrcode的时候,就可以知道这个二维码是否有websocket正在订阅他,有订阅的话,就可以拿来对指定的通道进行发送了

End

网页端真的没什么好说的,直接贴(里面没有很严密的控制权限,稍微改一改就行,但是我懒)

function listen_for_status() {
    socket = new WebSocket("ws://" + window.location.host + '/ws/');
    socket.onmessage = function (e) {
        var obj = JSON.parse(e.data);
        if (obj.status == true) {
            socket.close();
            window.location="dashboard.html";
        }
    };
    socket.onopen = function () {
        var obj = {};
        obj.token = token;
        socket.send(JSON.stringify(obj));
    };
}

还有不要忘记更新nginx的配置

扫码登录的这个小玩意儿,虽然看起来很是简单,但是也耗了我很多的时间,这个过程中,也是学到了很多新的东西呢!

这次也是达成了一个新的成就:WebSocket(Server&Client)

阅读剩下更多

编程

扫码登录(2)

Context

几天前把扫码登录的服务器总结了一下,由于时间太晚,篇幅太长,而且涉及的内容是两个板块的,所以就没有在那一篇文章中写app端的一些实现。

事实上,app这里并没有简单多少,主要的难点大概就是扫码这里吧~之前虽然是在Alinone中做过扫码的功能,但是当时使用了cocoapods里面的一个库,所以没有怎么深入的理解AVFoundation。这次,我就偏偏不要用开源库,读读官方文档,把AVFoundation的一些功能搞搞明白。

How does it work

好好研究了官方文档一番,我大概明白了一件事儿,整个视频流的工作过程是由一个会话(AVCaptureSession)来控制的,这个会话有输入(AVCaptureInput)和输出(AVCaptureOutput)

当session开始工作的时候,数据就从输入经过会话传到了输出,当然这里说的全都是抽象类,具体到这个app里面的话,输出就是AVCaptureOutput的一个子类AVCaptureMetadataOutput

这玩意儿就是专门用来解析视频流信息,提取其中的metadata的,其中包括BarCode、QRCode等其他code~牛逼的很

Make it

这次没有用什么传说中的设计模式来写这个demo,因为这样会有点儿小题大做的感觉,直接在VC中定义了一切。

@interface MainViewController ()<AVCaptureMetadataOutputObjectsDelegate>
{
    AVCaptureVideoPreviewLayer *previewLayer;
    dispatch_queue_t avProcessQueue;
    IBOutlet UIView *scan_view;
    AVCaptureSession *avCaptureSession;
    AVCaptureDevice *avDevice;
    AVCaptureDeviceInput *avInput;
    AVCaptureMetadataOutput *metaOutput;
    NSMutableArray *capturedList;
}
@property (nonatomic, weak) IBOutlet UIButton *scanCodeButton;

@end

注意这里有个avProcessQueue(dispatch_queue_t),因为视频处理是比较消耗资源和时间的,所以如果让他们在主线程中运行的话,必死,所以要单独搞一个线程出来让它们运行。

AVCaptureVideoPreviewLayer是CALayer的一个子类,可以方方便便的把AVSession里面的内容显示出来,如果没有这个东西的话,就很难想象如何把摄像头捕捉到的东西显示在界面上了~

capturedList是一个用来扫描结果的纪录列表,后面会解释他存在的意义。

Configure AVFoundation Elements

- (void)viewDidLoad {
    [super viewDidLoad];
    // 建立一个新的队列(线程)
    avProcessQueue = dispatch_queue_create( "session queue", DISPATCH_QUEUE_SERIAL );

    // 初始化设备
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

    // 初始化Session
    avCaptureSession=[[AVCaptureSession alloc]init];
    [avCaptureSession setSessionPreset:AVCaptureSessionPresetHigh];

    // 初始化Input
    NSError *error;
    avInput=[[AVCaptureDeviceInput alloc]initWithDevice:device error:&error];
    [avCaptureSession addInput:avInput];

    // 初始化Output
    metaOutput =[[AVCaptureMetadataOutput alloc]init];
    [metaOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
    [avCaptureSession addOutput:metaOutput];
    metaOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];

    // 初始化预览layer
    previewLayer=[AVCaptureVideoPreviewLayer layerWithSession:avCaptureSession];
    previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
    [scan_view.layer addSublayer:previewLayer];
    previewLayer.frame=CGRectMake(0, 0, 250, 250);

}

这里有一个超级超级超级大的坑,我找了大半天才知道原因

metaOutput.metadataObjectTypes = @[AVMetadataObjectTypeQRCode];

就是这个玩意儿,无论我怎么写,都会直接crash,说什么output不支持这种识别方式(我都删的只剩下一种了诶,同学!),结果原来是这个扫描类型的设置必须在该output被加入了session才可以设置,不然就会直接挂掉……也就是

[avCaptureSession addOutput:metaOutput];

这个之后才可以有效的设置识别类型

Please Allow Me To Control Your Camera

不过,摄像头是一个比较敏感的东西,涉及到了隐私,所以在某一代系统之后,AVFoundation要获取视频流是需要权限才行的~

switch ( [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] )
{
    case AVAuthorizationStatusAuthorized:
    {
        // 以前就获取到权限了!
        break;
    }
    case AVAuthorizationStatusNotDetermined:
    {
        // 还不知道有没有获取到权限,我们来问一问
        [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^( BOOL granted ) {
            // 索取权限完毕,处理结果
        }];
        break;
    }
    default:
    {
        // 以前就没有获取到权限!
        break;
    }
}

这里已经注释的很清楚了,直接在适当的位置复制粘贴进去就好啦!

I got it

在前面也看到了

[metaOutput setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];

这一行设置,意思是AVCaptureMetadataOutput扫描到有效的信息之后,会通过代理的形式调用一个回调函数(方法),顺便把扫描结果给返回出来,所以这里我就让我的MainViewController遵从了AVCaptureMetadataOutputObjectsDelegate协议,并且实现了接口需要的方法

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection{
    if (metadataObjects.count>0) {
        for (AVMetadataObject *data in metadataObjects){
            if ([data isKindOfClass:[AVMetadataMachineReadableCodeObject class]]) {
                AVMetadataMachineReadableCodeObject *result=(AVMetadataMachineReadableCodeObject*)data;
                if (![capturedList containsObject:result.stringValue]) {
                    // 这里出现了capturedList!
                    [capturedList addObject:result.stringValue];
                    // 扫描的Raw内容就是这么简单
                    NSLog(@"scanned code is %@",result.stringValue);
                    [NetworkHelper permit:result.stringValue andSucceed:nil andFail:nil];
                }
            }
        }
    }
}

好了,现在可以说明一下,为什么要用capturedList啦~

因为,这个- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection方法是不会中断的,是会在每个周期内都执行的(只要摄像头下有符合要求的Code),

换句话说,就是如果你的摄像头指着一个QRCode,系统就会使劲的调用这个代理方法,假设没有做任何的判断,那么很有可能对一个QRCode的处理会在一瞬间做N多次,在例子中我用到了一个网络请求,如果没有加前面的那个判断的话,服务器可能会哭~(哈哈)

End

好像只要这么多就可以把扫码功能搞定了呢!我当年为啥要偷懒去用开源库呢~

(要不然我也把这个东西整理一下,开个源,骗一些Star?)

阅读剩下更多

返回顶部