Flutter:WahtApp_Clones使用Camera&TabBar&ListView

wahtApp.gif

新建项目、新建包,再新建一个dart文件,不同于Java文件会自动填充类目,这是一个空白文件,其次dart中文件名不需要和类型相同。因为同一个文件中可以存在多个一级类,而Java多以匿名内部类的形式存在。类的创建方式就不做赘述了,需要注意的是笔者用到的时候需要自己手动导包,当然如果你记得完整类名也可以用快捷键导包,该类是此App的主界面,通常来说直接继承自`StatelessWidget或者StatefulWidget,Flutter中所有都是Weight(组件),包括padding、color等。而在Flutter中Weight大致可以分为三类,也有认为分两类的都可以吧。

  • 第一类:StatelessWidget以及继承自该类的其他Widget如Text、ButtonBar等。此类为无状态组件,必须实现其构造方法,不需要维护其状态,内容通常无法动态更改。
  • 第二类:StatefulWidget以及继承自该类的其他Widget如Image、Scaffold等。此类为有状态的组件,必须实现createState()方法,以创建需要维护的State,可以用来更改控件的内容或者状态。
  • 第三类:RenderObjectWidget以及直接或者间接继承自该类的Widget如Padding、Align(对齐)等。改类多位一些小部件,用来修饰其他Widget。

还有就是上面涉及到的导包操作,格式如下

//import '路径'
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/widgets.dart';

这里用到的Widget为StatefulWidget,导入上述文件中任何一个都可以,根据不同平台进行选择。这三个文件包含内容的大小从上到下依次减少,前两个文件包含了第三个文件。第一个主要为Material Design风格UI等(Android平台主流风格),第二个主要包含Cupertino风格UI等(IOS平台主流风格),第三个即常用Widget等。

import 'package:flutter/material.dart';
//import 'package:flutter/widgets.dart';
//import 'package:flutter/cupertino.dart';

//规范:同Java,驼峰命名不推荐下划线等特殊符号
class WhatsappHome extends StatefulWidget {

    @override
    State createState() {
        return null;
    }
}

接下来为createState()方法创建一个返回值,这种情况可以考虑直接new一个State,但是会报错因为State是一个抽象类,所以这里我们自定义一个类,继承State。代码如下

class _WhatsAppHomeState extends State{
  @override
  Widget build(BuildContext context) {
    return null;
  }
}

上面代码依旧在同一个文件中,必须实现build方法,同时该方法要求返回一个Widget,前面提到的任何Widget都可以在这里作为返回值,但这里我们选择使用Scaffold作为返回值,Scaffold是一个组合Widget,由Flutter内部帮我们将多个Widget组合到一起,实现了基本的Material Design布局结构(Android),通过阅读源码可以查看该Widget由那些Widget组合而来,请自行查阅,Scaffold是及其常用的,其子Widget中又包含了其他组合Widget比如AppBar等,了解各个Widget有助于以后自定义(组合)布局结构。

接下来往Scaffold中填充元素,根据上面的效果图可以分析出来当前页面需要哪些Widget,填充后完整代码如下

 @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("WhatApp"),
        elevation: 0.7,//阴影
        bottom: new TabBar(//注意这里bottom是指Appbar的bottom
          //硬性规定:TabBar必须配合TabController一起使用(或者TabController的实现类),用于控制tab切换,否则程序报错
          controller: new TabController(length: 4, vsync: this),
          indicatorColor: Colors.white,
          //选中时颜色
          tabs: <Widget>[
            new Tab(icon: new Icon(Icons.camera_alt)),
            new Tab(text: "CHATS"),
            new Tab(text: "STATUS"),
            new Tab(text: "CALLS")
          ],
        ),
        //右侧按钮
        actions: <Widget>[
          new Icon(Icons.search),
          new Padding(padding: const EdgeInsets.symmetric(horizontal: 5.0)),
          //常量建议加上const关键字(非强制),EdgeInsets表示边缘插入,symmetric为对称插入方向的描述之一
          new Icon(Icons.more_vert)
        ],
      ),
    );
  }
}

但是如果使用hot load功能程序会出现如下错误后来发现图片丢了......

看到关键点即可,可以看到错误中提到了一个叫SingleTickerProviderStateMixin的类,并指明了_WhatsAppHomeState需要使用它,代码中有注释明确表示需要配合TickerProvider来使用Tabbar,而错误中提到的类也包含这个单词,到这里大致就可以推断出来SingleTickerProviderStateMixinTickerProvider的一个子类。那替换成它要求的类即可。

在动手前先看下SingleTickerProviderStateMixin的具体实现,这里算是难点了
点击controller属性进入源码中查看,可以看到源码中要求使用TabController,而这里直接new一个对象的话会出现如下错误

The argument type 'SingleTickerProviderStateMixin<StatefulWidget>' can't be assigned to the parameter type 'TabController'

大意为SingleTickerProviderStateMixin不能转换为TabController,点开SingleTickerProviderStateMixin源码如下

@optionalTypeArgs
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
  Ticker _ticker;
    ...

多的就不看了,第一行就够,很明显该类实现了TickerProvider在Dart语法中可以使用implements实现其他类不一定是抽象类,如下

abstract class A{

}
abstract class B{

}
class D{

}
abstract class C extends A implements B,D{

}

因为这里是实现关系,并非继承所以对象不能转换。其他该类的声明过程中还出现了几个关键词mixinon,通常和前两个关键词配合使用的还有一个with,受限于篇幅这里就不详述了,看下如下代码应该就差不多明白了,说明在注释中
class A {
void a() {
print("A");
}
}

class B {
  void b() {
    print("B");
  }
}

class D {
  void b() {
    print("D");
  }
}

mixin E on D {}//该行表示类E允许被with多继承,但是受限于类D,再直白点的说法就是:再直白点的说法就是:某X使用with多继承E,则X必须是继承D

//with用于实现多继承,若有同名方法优先级从右往左依次从高到低
class C extends A with B, D, E {
  void text() {
    this.a();
    this.b(); //可以调用到BD中方法,且输出D,因为D中b()覆盖了B中b().
  }
}
//以下下写法将出错
//class F with E{}因为F和D并无继承关系

这里需要着重理解一下从事移动开发的对多继承的概念可能比较薄弱,其次Dart低(高)版本中该关键字使用方法可能有差异。

确保上面的三个关键字理解了,再回头看SingleTickerProviderStateMixin得使用就比较明了了。完整代码如下

import 'package:flutter/material.dart';
import 'package:flutter/src/foundation/diagnostics.dart';
import 'package:flutter_teaching/whatsapp/pages/call_screen.dart';
import 'package:flutter_teaching/whatsapp/pages/camera_screen.dart';
import 'package:flutter_teaching/whatsapp/pages/chat_screen.dart';
import 'package:flutter_teaching/whatsapp/pages/status_screen.dart';

//import 'package:flutter/widgets.dart';
//import 'package:flutter/cupertino.dart';

//规范:同Java,驼峰命名不推荐下划线等特殊符号
class WhatsappHome extends StatefulWidget {
  @override
  State createState() {
    return new _WhatsAppHomeState();
  }
}

class _WhatsAppHomeState extends State with SingleTickerProviderStateMixin {
  TabController _tabController;

  @override
  void initState() {
    super.initState();
    _tabController = new TabController(length: 4, vsync: this, initialIndex: 1);
  }

  @override
  Widget build(BuildContext context) {
    // TODO: implement build
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("WhatApp"),
        elevation: 0.7, //阴影
        bottom: new TabBar(
          controller: _tabController,
          //硬性规定:TabBar必须配合TabController一起使用
          indicatorColor: Colors.white,
          //选中时颜色
          tabs: <Widget>[
            new Tab(icon: new Icon(Icons.camera_alt)),
            new Tab(text: "CHATS"),
            new Tab(text: "STATUS"),
            new Tab(text: "CALLS")
          ],
        ),
        actions: <Widget>[
          new Icon(Icons.search),
          new Padding(padding: const EdgeInsets.symmetric(horizontal: 5.0)),
          //常量建议加上const关键字(非强制),EdgeInsets表示边缘插入,symmetric为对称插入方向的描述之一
          new Icon(Icons.more_vert)
        ],
      ),
      //TabBarView表示为TabBar的页面,该空间会自动按顺序关联TabBar,这里简单创建了4个类
      // 每个类写法完全相同如下,记得导入文件
      //class CallsScreen extends StatelessWidget {
      //  @override
      //  Widget build(BuildContext context) {
      //    return new Center(
      //      child: new Text(
      //        "Calls",
      //        style: new TextStyle(fontSize: 20.0),
      //      ),
      //    );
      //  }
      //}
      //注意这是Scaffold中属性
      body: new TabBarView(
        controller: _tabController,
        children: <Widget>[
          new CameraScreen(),
          new ChatScreen(),
          new StatusScreen(),
          new CallsScreen(),
        ],
      ),
      floatingActionButton: new FloatingActionButton(
          backgroundColor: Theme.of(context).accentColor,
          child: new Icon(
            Icons.message,
            color: Colors.white,
          ),
          onPressed: () => print("open chat")),
    );
  }
}

这里说明一下Dart的语法是嵌套+构造的写法,如果遇到不清楚的Widget不知道有些啥属性那么点进去看一下它的构造方法就明白了。这里代码可能看起来不清晰,但在AS中会自动填充标记所嵌套的层次。
到目前为止整个功能剩余对摄像头的支持了,目前效果图如下

Gif图片

然而并没有图

接下来对camera_screen.dart文件改造下。Flutter中摄像头使用的官方链接,大致看一下知道大概哪些步骤。
前面为了方便对四个View都是使用的StatelessWidget,也就是说类容固定,而要使用摄像头必须使用StatefulWidget,还需要添加依赖。其次这里还涉及到异步,文档第二步中的使用到的await,async,Future就是Flutter中异步操作需要用到的关键词,常用于网络请求,以及耗时任务等阻塞线程的操作中。之后初始化CameraController这玩意就是Android中的CameraManager,不初始化无法使用拍照等功能。最后创建一个Widget用于显示摄像头获取到的画面,这里指定使用CameraPreview来展示画面。剩余的两部分别是拍照和展示照片。
第一步:按照文档中的第一步,添加依赖。添加后pubspec.yaml文件如下,添加之后点击左上角的Packages get同步完成后进行下一步

# 注意这里的依赖缩进需要注意 缩进错误会导致找不到包报错
dependencies:
  flutter:
    sdk: flutter
  camera:
  path_provider:
  path:

第二步:获取可用Camera列表,并从列表中获取第一个可用的摄像头代码如下

// Obtain a list of the available cameras on the device.
final cameras = await availableCameras();

// Get a specific camera from the list of available cameras.
final firstCamera = cameras.first; 

重点来了,前面讲到了mixinwithon三个关键字以及组合使用,这里依旧是三个相互关联的关键字分别是awaitFutureasync

await用在调用的异步方法时作为前置修饰。若某方法体内使用await关键字,则该方法必定需要使用async修饰,若由await修饰的部分为返回值,则返回值类型必须声明为Future类型

  Future getCamera() async {
    // 该方法调用异步方法availableCameras(),需要使用await来调用,且该方法需要使用async修饰,返回值为Future
    final cameras = await availableCameras();
    // Get a specific camera from the list of available cameras.
    final firstCamera = cameras.first;
  }

上面代码中异步方法availableCameras()来自文件'package:camera/camera.dart';,

第三步:初始化CameraController

class TakePictureScreen extends StatefulWidget {
  final CameraDescription camera;

  const TakePictureScreen({
    Key key,
    @required this.camera,
  }) : super(key: key);

  @override
  TakePictureScreenState createState() => TakePictureScreenState();
}

class TakePictureScreenState extends State<TakePictureScreen> {
  // Add two variables to the state class to store the CameraController and
  // the Future.
  CameraController _controller;
  Future<void> _initializeControllerFuture;

  @override
  void initState() {
    super.initState();
    // To display the current output from the camera,
    // create a CameraController.
    _controller = CameraController(
      // Get a specific camera from the list of available cameras.
      widget.camera,
      // Define the resolution to use.
      ResolutionPreset.medium,
    );

    // Next, initialize the controller. This returns a Future.
    _initializeControllerFuture = _controller.initialize();
  }

  @override
  void dispose() {
    // Dispose of the controller when the widget is disposed.
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Fill this out in the next steps.第四步中完成该方法
  }
}

第四步:使用CameraPreview显示摄像头获取到的画面

  //第三部中的build方法
  @override
  Widget build(BuildContext context) {
    if (!controller.value.isInitialized) {
      return new Container();
    }

    return new AspectRatio(
      aspectRatio: controller.value.aspectRatio,
      child: new CameraPreview(controller),
    );
  }

最后,在main函数中异步获取可用camera,并将该对象以构造的方式传入到camera_screen()中。继续按照官方文档中的第五六步可以实现拍照且,将照片显示出来。

注意:前面讲过Flutter类名和文件名无关,且可以有多个一级类比如前面讲到的WhatsappHome中的_WhatsAppHomeState,那么要在_WhatsAppHomeState中调用到WhatsappHome中的变量可以使用如下写法

class _WhatsAppHomeState extends State<WhatsappHome> with SingleTickerProviderStateMixin {

通过查看State类的源码可得知该类支持泛型,而State通常是作为StatefulWidget中createState的返回值出现,故State中泛型通常传入与之关联的StatefulWidget。

@optionalTypeArgs
abstract class State<T extends StatefulWidget> extends Diagnosticable {...}

最后是Flutter中ListView的使用
在Flutter中滑动控件也叫ListView,其相关的列表控件包括ScrollView以及直接或间接继承自该类的子控件如CustomScrollView、GridView等
ListView官方文档
通过阅读文档或者查看源码可以发现,ListView共四种用法,原文如下

///  1. The default constructor takes an explicit [List<Widget>] of children. This
///     constructor is appropriate for list views with a small number of
///     children because constructing the [List] requires doing work for every
///     child that could possibly be displayed in the list view instead of just
///     those children that are actually visible.默认构造方法,WIdget数组即可,适用数少量,固定的列表
///
///  2. The [ListView.builder] constructor takes an [IndexedWidgetBuilder], which
///     builds the children on demand. This constructor is appropriate for list views
///     with a large (or infinite) number of children because the builder is called
///     only for those children that are actually visible.根据需要构建子项。此构造函数适用于具有大量(或无限)子项数的列表视图
///
///  3. The [ListView.separated] constructor takes two [IndexedWidgetBuilder]s:
///     `itemBuilder` builds child items on demand, and `separatorBuilder`
///     similarly builds separator children which appear in between the child items.
///     This constructor is appropriate for list views with a fixed number of children.
///     按需构建子项,而separatorBuilder类似地构建出现在子项之间的子项(分割线或者多层次的ListView)。此构造函数适用于具有固定数量子项的列表视图。
///
///  4. The [ListView.custom] constructor takes a [SliverChildDelegate], which provides
///     the ability to customize additional aspects of the child model. For example,
///     a [SliverChildDelegate] can control the algorithm used to estimate the
///     size of children that are not actually visible.
///     采用SliverChildDelegate,它提供了自定义子模型的其他方面的功能。例如,SliverChildDelegate可以控制用于估计实际上不可见的子项大小的算法。
///     这个涉及到Sliver以及其衍生子类,动画效果都很好,渐变渐隐之类的如SliverAppBar等。

下面是改造后的ChatScreen,其中方向由构造方法中的参数决定,默认竖向Axis scrollDirection = Axis.vertical,

class ChatScreen extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new ChatScreenState();
  }
}

class ChatScreenState extends State<ChatScreen> {
  final List<String> entries = <String>["A", "B", "C", "A", "B", "C"];
  final List<int> background = <int>[50, 100, 200, 300, 400, 500];//颜色深度有一张表,在一个叫Gallery的项目中找到的

  @override
  Widget build(BuildContext context) {
      //不同构造方法
//    ListView.separated(itemBuilder: null, separatorBuilder: null, itemCount: null);
//    ListView.builder(itemBuilder: null);
//    ListView.custom(childrenDelegate: null);

    return new ListView.separated(
        padding: const EdgeInsets.all(5.0),
        itemCount: entries.length,
        separatorBuilder: (BuildContext context, int index) => const Divider(),//默认分隔线
        itemBuilder: (BuildContext context, int index) {
          return Container(
            height: 100,
            color: Colors.red[background[index]],
            child: new Center(
              child: new Text("Item_${entries[index]}"),//支持字符串中直接拼接
            ),
          );
        });
  }
}

最终完成最上面图中展示的效果。此项目来源于Flutter Example Apps,由于Flutter的跟新某些Api更新了所以重写了下。

Demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容