#####介绍:
首先来看一下TextInputLayout,TextInputLayout 是EditText(或者EditText子类)的一个包装类,它主要用于在用户输入文本的时候显示一个浮动标签,也支持显示错误信息和字符计数等功能。同样它也支持密码可见切换按钮,通过setPasswordVisibilityToggleEnabled(boolean)API 或者 xml 中的属性,如果设置该属性为可用(enable),那么当EditText 显示设置的密码时,会显示一个切换密码可见和隐藏的按钮。
#####TextInputLayout 重要方法和属性:

  • app:counterEnabled 字符计数是否可用
    代码中对应的方法为:setCounterEnabled(boolean)
  • app:counterMaxLength 计数最大的长度
    代码中对应的方法为:setCounterMaxLength(int )
  • app:counterOverflowTextAppearance 计数超过最大长度时显示的文本样式
  • app:counterTextAppearance 显示的计数的文本样式。
  • app:errorEnabled 显示错误信息是否可用
  • app:errorTextAppearance 显示错误信息的文本样式
  • android:hint 浮动标签
    代码对应方法:setHint(CharSequence)
  • app:hintAnimationEnabled 控制是否需要浮动标签的动画
    代码对应方法:setHintAnimationEnabled(boolean)
  • app:hintEnabled 控制是否显示浮动标签
    代码对应方法:setHintEnabled(boolean)
  • app:hintTextAppearance 浮动标签的文本样式
    代码对应方法:setHintTextAppearance(int)
  • app:passwordToggleDrawable 密码可见切换图标
    代码对应方法:setPasswordVisibilityToggleDrawable(int)或者setPasswordVisibilityToggleDrawable(Drawable)
  • app:passwordToggleEnabled 控制是否显示密码可见切换图标
    代码对应方法:setPasswordVisibilityToggleEnabled(boolean)
  • app:counterOverflowTextAppearance 设置超出字符数后提示文字的颜色,如果不设置默认为@color/colorAccent的颜色

#####基础用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:hint="账号"
android:paddingLeft="16dp"
android:scrollbarAlwaysDrawHorizontalTrack="true"
app:counterMaxLength="8"
app:counterOverflowTextAppearance="@style/TextOverCount">

<android.support.design.widget.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="@color/black"
android:textColorHint="@color/gray"
android:textSize="14dp"/>
</android.support.design.widget.TextInputLayout>

######修改默认样式:
改变TextInputLayout默认下划线颜色和点击时颜色,默认hint颜色,在TextInputLayout控件设置textColorHint设置默认hint颜色
在EditText控件中设置android:theme属性设置 style

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
<android.support.design.widget.TextInputLayout
android:id="@+id/login_textinput_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColorHint="@color/mintcream">
<!--android:textColorHint" 默认hint字体及下划线颜色-->
<EditText
android:id="@+id/login_userId"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入账号"
android:inputType="number"
android:maxLines="1"
android:singleLine="true"
android:textColor="@color/darkorange"
android:theme="@style/TextAppTheme" />
<!--textColor设置输入时字体颜色-->
</android.support.design.widget.TextInputLayout>

<!--改变TextInputLayout 里面的EditText默认下划线和点击时下划线的颜色-->
<style name="TextAppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorControlNormal">@color/gray</item><!--默认显示下划线的颜色-->
<item name="colorControlActivated">@color/gray</item><!--点击后下划线的颜色-->
<item name="colorAccent">@color/qmui_config_color_red</item>
</style>

######超出字符数后的提示样式:

1
2
3
<style name="TextOverCount" parent="Base.TextAppearance.AppCompat.Light.Widget.PopupMenu.Small">
<item name="android:textColor">@android:color/holo_red_light</item>
</style>

#####设置错误提示文字

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
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_marginTop="20dp"
app:errorEnabled="true" //设置为true
android:id="@+id/textinputlayout_email"
android:layout_height="wrap_content">
<EditText
android:layout_width="match_parent"
android:hint="请输入邮箱"
android:id="@+id/et_email"
android:layout_height="wrap_content" />
</android.support.design.widget.TextInputLayout>


editText_email=findViewById(R.id.et_email);
textInputLayout =findViewById(R.id.textinputlayout_email);
editText_email.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {

}

@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
if(!RegexUtils.isEmail(charSequence)){
textInputLayout.setError("邮箱格式错误");
textInputLayout.setErrorEnabled(true);
}else {
textInputLayout.setErrorEnabled(false);
}
}

@Override
public void afterTextChanged(Editable editable) {

}
});

image
######设置密码是否可见

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_marginTop="20dp"
app:errorEnabled="true"
app:passwordToggleEnabled="true" //设置为true
android:id="@+id/textinputlayout_password"
android:layout_height="wrap_content">
<EditText
android:layout_width="match_parent"
android:hint="请输入密码"
android:id="@+id/et_password"
android:inputType="textPassword"
android:layout_height="wrap_content" />
</android.support.design.widget.TextInputLayout>

image

在Rxjava使用之前记得在Gradle中添加依赖引入

1
2
implementation "io.reactivex.rxjava2:rxjava:2.1.12"
implementation "io.reactivex.rxjava2:rxandroid:2.0.2"
  • ###观察者的回调方法:
  • onSubscribe() 当观察者被订阅时回调
  • onNext() 当观察者受到onNext事件时回调
  • onComplete() 当观察者收到onNext事件时回调
  • onError()当观察者受到onError事件时回调
  • ###Rxjava简述

rxjava是一个在java虚拟机上,使用可观察的序列构成基于事件的,异步的程序库
白话:类似于Google提供 给我我们使用的AsyncTask,它能在异步线程处理后传递到UI线程中处理,现在基本上已经由Rxjava替换了AsyncTask来使用

  • ###RxAndroid简述

RxAndroid是基于RxJava开发出的一套适用于Android开发的辅助库
白话:RxJava通过RxAndroid的辅助,使得原本RxJava只在Java上运行的程序也能在Android上开发

  • ###设计模式中的观察者模式

观察者模式就是RxJava使用的核心点,掌握这个模式,可以理解RxJava更简单,观察者模式简单的说就是:订阅-发布;的模式,举一个例子说,当你订阅某家牛奶店的早餐奶(订阅过程),只要牛奶店生成牛奶,便会给你送过去(发布过程)。这里的牛奶店只有一家,但是订阅的人可以很多。这是一种一对多的关系,只要牛奶店发布牛奶,那么订阅的人就会收到牛奶。换成RxJava里面的话,牛奶店就是被观察者(ObServable),订阅的人就是观察者(ObServer),根据这个例子,我们通过带啊来实现这逻辑
#####1. 创建观察者和被观察者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//被观察者
public interface ObServable{
//订阅
public subscribe(ObServer obServer);
//取消订阅
void cancel(ObServer obServer);
//发布
void onNext();
}
//观察者
public interface ObServer{
//接收通知
void onNotify();
}

#####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
46
47
48
49
50
51
52
  //牛奶店
public class MilkFactory implements ObServable{
private List<ObServer> ObServers;
public MilkFactory(){
this.ObServers=new ArrayList<>();
}
@Override
public void subscribe(ObServer obServer){
ObServers.add(obServer);
}

@Override
public void cancel(ObServer obServer) {
ObServers.remove(obServer);
}

@Override
public void onNext() {
for (ObServer obServer:obServables) {
obServer.onNotify();
}
}
}
//顾客
public class MilkConsumer implements Observer{
private String name;
public MilkConsumer(String name){
this.name=name;
}
@Override
public void onNotify(){
System.out.println(this.name+"收到牛奶");
}
}
//观察者订阅被观察者
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ObServable milkFactory=new MilkFactory();
ObServer milkConsumer1=new MilkConsumer("顾客1");
ObServer milkConsumer2=new MilkConsumer("顾客2");
//订阅过程
milkFactory.subscribe(milkConsumer1);
milkFactory.subscribe(milkConsumer2);
//发布过程
System.out.println("此时,牛奶店已经产出早餐奶,通知并发送给各位顾客");
milkFactory.onNext();
}
//查看程序输出的结果
//此时,牛奶店已经产出早餐奶,通知并发送给各位顾客
//顾客1 收到牛奶
//顾客2 收到牛奶
  • ###Rxjava中的观察者模式

RxJava中的观察者模式也是跟例子中的大同小异,也是通过创建出观察者和被观察者,然后进行订阅事件,只不过这里的观察者和被观察者已经不在是我们自定义的两个接口类,而是通过RxJava提供好的观察者和被观察者

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
 @Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//拿到被观察者
ObServable<String> obServable=getObServable();
//拿到观察者
ObServer<String> obServer=getObServer();
//订阅
obServable.subscribe(obServer);
}
public static Observable<String> getObServable(){
return Observable.create(new ObServableOnSubscribe<String>(){
@Override
public void subscribe(@NonNull ObServableEmitter<String> e) throws Exception{
e.onNext("事件1");
e.onNext("事件2");//onNext()类似于下一步
e.onComplete();//onComplete()完成
}
});
}
public static Observer<String> getObServer(){
return new ObServer<String>(){
Disposable disposable=null;
@Override
public void onSubscribe(Disposable d){
disposable=d;
}
@Override
public void onNext(String s){
System.out.println("onNext="+s);
if (s.equals("取消关注")){
//断开订阅
disposable.dispose();
}
}
@Override
public void onError(){

}
@Override
public void onComplete(){
System.out.println("onComplete");
}
};
}
//执行结果:
//onNext=事件1
//onNext=事件2
//onComplete

Flutter 支持作为 android Moudle 出现在项目中.这样就可以在 已有的项目中 使用.
虽然现在Flutter 比较受关注,但是和weex 一样 ,大部分都只是在观望 不是真正的进行使用.所以 如果用还是混合开发 原生+Flutter 方式比较合适(自我感觉).
写一个demo 进行Android及Flutter 交互.(IOS 方法基本一致).
Flutter 调用 android 硬件的插件还比较匮乏 比如 各种传感器, 自定义相机 所以就会用到 Flutter 调用android 及android 原生调用 Flutter的方法.

#####本例子中会实现.

  • 原有的android 应用程序嵌入 FlutterView
  • Flutter 代码调用Android 原生方法进行页面跳转及传值
  • Android原生 调用 Flutter 方法 进行传值

#####android引入flutter moudle
在当前project下 运行命令
flutter create -t module my_flutter(my_flutter为生成的flutter module名称)

  1. 在工程的settings.gradle增加以下配置
    1
    2
    3
    4
    5
    6
    include ':app'
    setBinding(new Binding([gradle: this])) // new
    evaluate(new File( // new
    settingsDir.parentFile, // new
    'ToyamaFinance/my_flutter/.android/include_flutter.groovy' //ToyamaFinance项目名称 my_flutter FlutterMoudle名称
    ))
    2.在app (假如你的Android应用名称为app)的build.gradle文件下dependencies增加如下依赖
    implementation project(':flutter')
    其中flutter工程为创建Flutter module过程自动生成的,注意就是flutter
    注意 其中 minSdkVersion 需要至少为16,否则会报错

######生成的工程结构如下:

#####android端:

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
package com.guoshikeji.toyamafinance.flutter_activitys;

import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Window;
import android.widget.RelativeLayout;

import com.guoshikeji.toyamafinance.R;
import com.guoshikeji.toyamafinance.activity.launch.LoginActivity;
import com.guoshikeji.toyamafinance.activity.user.ShareDialogActivity;
import com.guoshikeji.toyamafinance.application.MyApplication;
import com.guoshikeji.toyamafinance.control.utils.SerializeUtils;
import com.guoshikeji.toyamafinance.mode.bean.UserBean;
import com.guoshikeji.toyamafinance.mode.constants.Constants;
import com.qmuiteam.qmui.util.QMUIStatusBarHelper;
import io.flutter.facade.Flutter;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.view.FlutterView;
public class FlutterProjectsDetails extends AppCompatActivity {
//发送与接收的key,需保证唯一,不一定是包名,并且android和flutter的key需一致 不然会报错
public static final String FlutterToAndroidCHANNEL = "com.guoshikeji.toandroid/project_details";
public static final String AndroidToFlutterCHANNEL= "com.guoshikeji.toflutter/project_details";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//requestWindowFeature(Window.FEATURE_NO_TITLE);
//MyApplication.getInstance().addActivity(this);
//QMUIStatusBarHelper.translucent(this, Color.WHITE);
//MyApplication.getInstance().addActivity(this);
setContentView(R.layout.activity_flutter_default);
RelativeLayout rl_flutter_defalut = findViewById(R.id.rl_flutter_defalut);
Intent intent = getIntent();
int project_id = intent.getIntExtra("project_id", 0);
Log.e("tyl","project_id1="+project_id);
//加载flutter界面 getLifecycle方法需要activity继承至AppCompatActivity
FlutterView flutterView = Flutter.createView(this, getLifecycle(), "route1");
//将view添加到原生界面中
rl_flutter_defalut.addView(flutterView);
//android向flutter发送消息
new EventChannel(flutterView, AndroidToFlutterCHANNEL)
.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
String androidParmas = project_id+"";
eventSink.success(androidParmas);
}
@Override
public void onCancel(Object o) {

}
});
//接收flutter发送过来的消息
new MethodChannel(flutterView, FlutterToAndroidCHANNEL).setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
Log.e("tyl","flutter_back="+methodCall.method);
//接收来自flutter的指令oneAct
if (methodCall.method.equals("finish")) {
MyApplication.getInstance().removeActivity(FlutterProjectsDetails.this);
result.success("success");
}
else if (methodCall.method.equals("share")) {
Object object = SerializeUtils.readServiceListFromFile(Constants.USER_INFO);
if (object != null) {
Intent intent = new Intent(FlutterProjectsDetails.this, ShareDialogActivity.class);
intent.putExtra(ShareDialogActivity.MODE_ORIENTATION, 1);
startActivity(intent);
} else {
startActivity(new Intent(FlutterProjectsDetails.this, LoginActivity.class));
}
result.success("success");
} else {
result.notImplemented();
}
}
});
}
}

#####Flutter端:

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
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:async';

void main() => runApp(new MyApp());

Widget _widgetForRoute(String route) {
switch (route) {
case 'route1':
return MyHomePage(title: 'Flutter Demo Home Page1');
case 'route2':
return MyHomePage(title: 'Flutter Demo Home Page2');
default:
return MyHomePage(title: 'Flutter Demo Home Page2');
}
}

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: _widgetForRoute(window.defaultRouteName),
);
}
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
//获取到插件与原生的交互通道 需要和android的key一致
static const toAndroidPlugin = const MethodChannel('com.guoshikeji.toandroid/project_details');
static const fromAndroiPlugin = const EventChannel('com.guoshikeji.toflutter/project_details');
StreamSubscription _fromAndroiSub;
var _nativeParams;
@override
void initState() {
// TODO: implement initState
super.initState();
_startfromAndroiPlugin();//在initState中初始化
}
//注册从android获取消息的监听
void _startfromAndroiPlugin(){
if(_fromAndroiSub == null){
_fromAndroiSub = fromAndroiPlugin.receiveBroadcastStream()
.listen(_onfromAndroiEvent,onError: _onfromAndroiError);
}
}
//获取成功的方法
void _onfromAndroiEvent(Object event) {
setState(() {
_nativeParams = event;
});
}
//获取失败的方法
void _onfromAndroiError(Object error) {
setState(() {
_nativeParams = "error";
print(error);
});
}
//发送消息
Future<Null> _jumpToNative() async {
String result = await toAndroidPlugin.invokeMethod('withoutParams');

print(result);
}
//发送带参数的消息
Future<Null> _jumpToNativeWithParams() async {
Map<String, String> map = { "flutter": "这是一条来自flutter的参数" };
String result = await toAndroidPlugin.invokeMethod('withParams', map);
print(result);
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
child: new ListView(
children: <Widget>[
new Padding(
padding: const EdgeInsets.only(left: 10.0, top: 10.0, right: 10.0),
child: new RaisedButton(
textColor: Colors.black,
child: new Text('跳转到原生界面'),
onPressed: () {
_jumpToNative();
}),
),
new Padding(
padding: const EdgeInsets.only(
left: 10.0, top: 10.0, right: 10.0),
child: new RaisedButton(
textColor: Colors.black,
child: new Text('跳转到原生界面(带参数)'),
onPressed: () {
_jumpToNativeWithParams();
}),
),

new Padding(
padding: const EdgeInsets.only(
left: 10.0, top: 10.0, right: 10.0),
child: new Text('这是一个从原生获取的参数:$_nativeParams'),
)
, new Padding(
padding: const EdgeInsets.only(
left: 10.0, top: 10.0, right: 10.0),
child: new Text('Flutter floatingActionButton 点击次数$_counter'),
)

],
)
),
);
}
}

#####1 下载flutter开发包

windows下载地址
官方文档

######安装注意:

  • 安装AndroidStudio3.0版本或更新版本
  • 操作系统:Windows 7 SP1或更高版本(64位)
  • 磁盘空间:400 MB(不包括IDE /工具的磁盘空间)
  • 解压缩zip文件并将其包含flutter在Flutter SDK的所需安装位置(例如C:\src\flutter;不要将Flutter安装在C:\Program Files\需要提升权限的目录
    中)。
  • flutter_console.bat在flutter目录中找到该文件。双击启动它。

flutter文件
#####2 配置环境变量
在系统环境变量path中添加刚刚下载的flutter的路径。将bin文件路径添加至path中
win10添加效果:
flutter的路径
#####3 flutter 文件目录中双击flutter_console.bat输入flutter doctor命令
flutter doctor 会自动检测当前开发环境配置。//这时还会报几个错误,暂时不予理会 还需在studio中配置dart和flutter插件
#####4 Android Studio安装Dart插件
打开Android Studio -> File -> Settings -> Plugins
搜索Dart插件 在右侧点击Install即可。
安装后效果
同样的步骤搜索Flutter插件安装即可
安装完成效果

  • 安装flutter时,因网络延迟可能会导致下载失败,我是多次下载后才成功的!

#####注意:对于国内开发者创建项目可能会卡在Create flutter .. 底部显示 building Symbols…状态。
查看Console会提示Got socket error trying to find package at http://pub.dartlang.org
######解决办法:
在系统环境变量中新建名为PUB_HOSTED_URL 值为https://pub.flutter-io.cn 的环境变量和新建名为FLUTTER_STORAGE_BASE_URL值为
https://storage.flutter-io.cn的环境变量。然后重新打开项目即可
image.png
#####5 studio中配置dart和flutter的路径
dart
flutter
#####6 创建Flutter项目
插件安装完成后,重启Android Studio新建项目会发现增加了创建Flutter项目的选项。
配置成功

MaterialApp 代表使用纸墨设计(Material Design)风格的应用。里面包含了纸墨设计风格应用所需要的基本控件。

MaterialApp 主要属性如下:

  • title : 在任务管理窗口中所显示的应用名字
  • theme : 应用各种 UI 所使用的主题颜色
  • color : 应用的主要颜色值(primary color),也就是安卓任务管理窗口中所显示的应用颜色
  • home : 应用默认所显示的界面 Widget
  • routes : 应用的顶级导航表格,这个是多页面应用用来控制页面跳转的,类似于网页的网址
  • initialRoute :第一个显示的路由名字,默认值为 Window.defaultRouteName
  • onGenerateRoute : 生成路由的回调函数,当导航的命名路由的时候,会使用这个来生成界面
  • onLocaleChanged : 当系统修改语言的时候,会触发å这个回调
  • navigatorObservers : 应用 Navigator 的监听器
  • debugShowMaterialGrid : 是否显示 纸墨设计 基础布局网格,用来调试 UI 的工具
  • showPerformanceOverlay : 显示性能标签,https://flutter.io/debugging/#performanceoverlay
  • checkerboardRasterCacheImages 、showSemanticsDebugger、debugShowCheckedModeBanner 各种调试开关

下面将介绍重要的几个属性。

title

这个和启动图标名字是不一样的,和当前 Activity 的名字也是不一样的。 这个 Title 是用来定义任务管理窗口界面所看到应用名字的。在原生 Android 系统中点击圆圈 Home 按钮右边的方块按钮就会打开多任务切换窗口。

theme

定义应用所使用的主题颜色,在纸墨设计中定义了 primaryColor、accentColor、hintColor 等颜色值。可以通过这个来指定一个 ThemeData 定义应用中每个控件的颜色。

home

这个是一个 Widget 对象,用来定义当前应用打开的时候,所显示的界面。

color

定义系统中该应用的主要颜色

routes

定义应用中页面跳转规则。 该对象是一个 Map<String, WidgetBuilder>。
当使用 Navigator.pushNamed 来路由的时候,会在 routes 查找路由名字,然后使用 对应的 WidgetBuilder 来构造一个带有页面切换动画的 MaterialPageRoute。如果应用只有一个界面,则不用设置这个属性,使用 home 设置这个界面即可。

如果 home 不为 null,当 routes 中包含 Navigator.defaultRouteName('/') 的时候会出错,两个都是 home 冲突了。

如果所查找的路由在 routes 中不存在,则会通过 onGenerateRoute 来查找。

initialRoute

指定默认显示的路由名字,默认值为 Window.defaultRouteName

onGenerateRoute

路由回调函数

#####1 继续关系:
继续关系

  • BoxDecoration:实现边框、圆角、阴影、形状、渐变、背景图像
  • ShapeDecoration:实现四个边分别指定颜色和宽度、底部线、矩形边色、圆形边色、体育场(竖向椭圆)、 角形(八边角)边色
  • FlutterLogoDecoration:实现Flutter图片
  • UnderlineTabindicator:下划线

#####2 介绍:
一个背景装饰对象,相当于Android中的shape.xml,定制各种各样的背景(边框、圆角、阴影、形状、渐变、背景图像)。
#####3 BoxDecoration例子:

1
2
3
4
5
6
7
8
9
10
11
//构造方法:
const BoxDecoration({
this.color, // 底色
this.image, // 图片
this.border, 边色
this.borderRadius, // 圆角度
this.boxShadow, // 阴影
this.gradient, // 渐变
this.backgroundBlendMode, // 混合Mode
this.shape = BoxShape.rectangle, // 形状
})

######1.边框+圆角:

1
2
3
4
5
6
decoration: new BoxDecoration(
border: new Border.all(color: Color(0xFFFF0000), width: 0.5), // 边色与边宽度
color: Color(0xFF9E9E9E), // 底色
// borderRadius: new BorderRadius.circular((20.0)), // 圆角度
borderRadius: new BorderRadius.vertical(top: Radius.elliptical(20, 50)), // 也可控件一边圆角大小
),

######2.阴影:

1
2
3
4
5
decoration: new BoxDecoration(
border: new Border.all(color: Color(0xFFFF0000), width: 0.5), // 边色与边宽度
// 生成俩层阴影,一层绿,一层黄, 阴影位置由offset决定,阴影模糊层度由blurRadius大小决定(大就更透明更扩散),阴影模糊大小由spreadRadius决定
boxShadow: [BoxShadow(color: Color(0x99FFFF00), offset: Offset(5.0, 5.0), blurRadius: 10.0, spreadRadius: 2.0), BoxShadow(color: Color(0x9900FF00), offset: Offset(1.0, 1.0)), BoxShadow(color: Color(0xFF0000FF))],
),

######3.渐变(环形、扫描式、线性):

1
2
3
4
5
6
7
8
9
decoration: new BoxDecoration(
border: new Border.all(color: Color(0xFFFFFF00), width: 0.5), // 边色与边宽度
// 环形渲染
gradient: RadialGradient(colors: [Color(0xFFFFFF00), Color(0xFF00FF00), Color(0xFF00FFFF)],radius: 1, tileMode: TileMode.mirror)
//扫描式渐变
// gradient: SweepGradient(colors: [Color(0xFFFFFF00), Color(0xFF00FF00), Color(0xFF00FFFF)], startAngle: 0.0, endAngle: 1*3.14)
// 线性渐变
// gradient: LinearGradient(colors: [Color(0xFFFFFF00), Color(0xFF00FF00), Color(0xFF00FFFF)], begin: FractionalOffset(1, 0), end: FractionalOffset(0, 1))
),

######4.背景图像:

1
2
3
4
5
6
7
8
9
decoration: new BoxDecoration(
border: new Border.all(color: Color(0xFFFFFF00), width: 0.5), // 边色与边宽度
image: new DecorationImage(
image: new NetworkImage('https://avatar.csdn.net/8/9/A/3_chenlove1.jpg'), // 网络图片
// image: new AssetImage('graphics/background.png'), 本地图片
fit: BoxFit.fill // 填满
// centerSlice: new Rect.fromLTRB(270.0, 180.0, 1360.0, 730.0),// 固定大小
),
),

######5.ShapeDecoration例子:

1
2
3
4
5
6
7
8
//构造方法:
const ShapeDecoration({
this.color,
this.image,
this.gradient,
this.shadows,
@required this.shape,
})

除了shape,其他与BoxDecoration一致,shape研究:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
decoration: new ShapeDecoration(
color: Color(0xFFFF00FF), // 底色
// 统一四边颜色和宽度
shape: Border.all(color: Color(0xFF00FFFF),style: BorderStyle.solid,width: 2)
// 四个边分别指定颜色和宽度, 当只给bottom时与UnderlineInputBorder一致效果
// shape: Border(top: b, bottom: b, right: b, left: b)
// 底部线
// shape: UnderlineInputBorder( borderSide:BorderSide(color: Color(0xFFFFFFFF), style: BorderStyle.solid, width: 2))
// 矩形边色
// shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10)), side: BorderSide(color: Color(0xFFFFFFFF), style: BorderStyle.solid, width: 2))
// 圆形边色
// shape: CircleBorder(side: BorderSide(color: Color(0xFFFFFF00), style: BorderStyle.solid, width: 2))
// 体育场(竖向椭圆)
// shape: StadiumBorder(side: BorderSide(width: 2, style: BorderStyle.solid, color: Color(0xFF00FFFF))
// 角形(八边角)边色
// shape: BeveledRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10)), side: BorderSide(color: Color(0xFFFFFFFF), style: BorderStyle.solid, width: 2))
),

######6.FlutterLogoDecoration例子:

1
2
3
4
5
6
7
8
//构造方法
const FlutterLogoDecoration({
this.lightColor = const Color(0xFF42A5F5), // Colors.blue[400]
this.darkColor = const Color(0xFF0D47A1), // Colors.blue[900]
this.textColor = const Color(0xFF616161),
this.style = FlutterLogoStyle.markOnly,
this.margin = EdgeInsets.zero,
})

######7. UnderlineTabindicator例子:

1
2
3
4
5
//构造方法:
const UnderlineTabIndicator({
this.borderSide = const BorderSide(width: 2.0, color: Colors.white),
this.insets = EdgeInsets.zero,
})

这个比较简单就是加下划线,可以设置Insets值(控制下划高底,左右边距)

1
2
3
4
decoration: new UnderlineTabIndicator(
borderSide: BorderSide(width: 2.0, color: Colors.white),
insets: EdgeInsets.fromLTRB(0,0,0,10)
),

#####目录结构

当使用flutter create myapp 创建项目后,会自动生成初始化代码。

下面介绍一下代码的目录。

1
2
3
4
5
6
7
8
9
10
11
myapp

├ android - 包含 Android 特定的文件。
├ build - 存储 iOS 和 Android 构建文件。
├ ios - 包含 iOS 特定的文件。
├ lib - 应用源文件。

└ src - 包含额外的源文件。
└ main.dart - 程序运行入口文件。
├ test - 测试文件。
└ pubspec.yaml - 包含 Flutter 应用程序的包数据。

image.png
新建Flutter项目的结构和原生android的工程结构不一样,我们不能用android那种多module 多lib的结构去创建module和lib,因为我们的代码都是在lib目录里面完成的,除非要用到原生交互的代码,你可以在android目录里面去写,然后在lib目录里面去引用这些交互的代码。
######android目录
这里存放的是Flutter与android原生交互的一些代码,这个路径的文件和创建单独的Android项目的基本一样的。不过里面的代码配置跟单独创建Android项目有些不一样。
######ios目录
这里存放的是Flutter与ios原生交互的一些代码。
######lib目录
这里存放的是Dart语言编写的代码,这里是核心代码。不管是Android平台,还是ios平台,安装配置好环境,可以把dart代码运行到对应的设备或模拟器上面。刚才的示例中,就是运行的lib目录下的main.dart这个文件。
我们可以在这个lib目录下面创建不同的文件夹,里面存放不同的文件,使用Dart语言写我们的自己的代码。
######pubspec.yaml文件

这个是配置依赖项的文件,比如配置远程pub仓库的依赖库,或者指定本地资源(图片、字体、音频、视频等)。
例如刚才创建的项目的pubspec.yaml里面的:cupertino_icons: ^0.1.2,表示项目要依赖cupertino_icons这个库,版本号为0.1.2。
#####资源
像图片、视频、文字等这些资源文件,在 Flutter 里是可以直接引用的,不过需要对资源进行声明式说明。
在 pubspec.yaml 里进行声明。

1
2
3
4
flutter:
assets:
- assets/my_icon.png
- assets/background.png

在代码里这样进行引用。

1
new Image(image: new AssetImage('assets/background.png'));

如果资源是来至网络的而不是本地的,则需要使用 image.network。

1
new Image.network('https://flutter.io/images/owl.jpg');

Scaffold 实现了基本的纸墨设计布局结构。在示例应用中,MyHomePage 所返回的就是一个 Scaffold。也就是说, MaterialApp 的 child 是 Scaffold Widget。

在纸墨设计中定义的单个界面上的各种布局元素,在 Scaffold 中都有支持,比如 左边栏(Drawers)snack bars、以及 bottom sheets

Scaffold 有下面几个主要属性:

  • appBar:显示在界面顶部的一个 AppBar,也就是 Android 中的 ActionBar 、Toolbar
  • body:当前界面所显示的主要内容 Widget
  • floatingActionButton:纸墨设计中所定义的 FAB,界面的主要功能按钮
  • persistentFooterButtons:固定在下方显示的按钮,比如对话框下方的确定、取消按钮
  • drawer:侧边栏控件
  • backgroundColor: 内容的背景颜色,默认使用的是 ThemeData.scaffoldBackgroundColor 的值
  • bottomNavigationBar: 显示在页面底部的导航栏
  • resizeToAvoidBottomPadding:类似于 Android 中的 android:windowSoftInputMode=”adjustResize”,控制界面内容 body 是否重新布局来避免底部被覆盖了,比如当键盘显示的时候,重新布局避免被键盘盖住内容。默认值为 true。

显示 snackbar 或者 bottom sheet 的时候,需要使用当前的 BuildContext 参数调用 Scaffold.of 函数来获取 ScaffoldState 对象,然后使用 ScaffoldState.showSnackBar 和 ScaffoldState.showBottomSheet 函数来显示。

移动应用的一个必不可少的环节就是与用户的交互,在Android中提供了手势检测,并为手势检测提供了相应的监听。本文将介绍Flutter中手势检测GestureDetector。
Flutter中的手势系统分为两层,第一层是触摸原事件(指针),有相应的四种事件类型。

  1. PointerDownEvent :用户与屏幕接触产生了联系。
  2. PointerMoveEvent :手指已从屏幕上的一个位置移动到另一个位置。
  3. PointerUpEvent :用户已停止接触屏幕。
  4. PointerCancelEvent :此指针的输入不再指向此应用程序。
    第二层就是我们可以检测到的手势,主要分为三大类,包括轻击,拖动和缩放,下面是具体的手势监听方法。

#####点击(一次):

  • onTapDown :点击屏幕立即触发此方法。
  • onTapUp :手指离开屏幕。
  • onTap :点击屏幕。
  • onTapCancel:此次点击事件结束,onTapDown不会在产生点击事件。

#####双击:
onDoubleTap :用户快速连续两次在同一位置点击屏幕。
#####长按:
onLongPress :长时间保持与相同位置的屏幕接触
#####垂直拖动:

  • onVerticalDragStart: 与接触屏幕,可能会开始垂直移动。
  • onVerticalDragUpdate:与屏幕接触并垂直移动的指针在垂直方向上移动
  • onVerticalDragEnd :之前与屏幕接触并垂直移动的指针不再与屏幕接触,并且在停止接触屏幕时以特定速度移动

#####水平拖动:

  • onHorizontalDragStart :与接触屏幕,可能开始水平移动
  • onVerticalDragUpdate:与屏幕接触并水平移动的指针在水平方向上移动
  • onVerticalDragEnd :先前与屏幕接触并且水平移动的指针不再与屏幕接触,并且当它停止接触屏幕时以特定速度移动
    上面介绍的就是Flutter的GestureDetector提供的三种手势检测相应的监听方法。来看个Demo。
    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
    import 'package:flutter/material.dart';
    void main() =>
    runApp(new MaterialApp(
    home: new MyApp()
    ));
    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return new GestureDetector(
    /*点击*/
    onTap: () {
    print("文本点击了");
    },
    /*长按*/
    onLongPress: () {
    print("长按文本");
    },
    /*横向拖动的开始状态*/
    onHorizontalDragStart: (startDetails) {
    print(startDetails.globalPosition);
    },

    /*横向拖动的结束状态*/
    onHorizontalDragEnd: (endDetails) {
    print(endDetails);
    },
    /*缩放开始状态*/
    onScaleStart: (startScaleDetails) {
    print(startScaleDetails);
    },
    /*缩放结束状态*/
    onScaleEnd: (endScaleDetails) {
    print(endScaleDetails);
    },
    child: new Container(
    color: Color.fromARGB(255, 220, 220, 220),
    child: new Center(
    child: new Text("Flutter手势"),
    )
    ),
    );
    }
    }