2024年2月

accent-color
是从 Chrome 93 开始被得到支持的一个不算太新属性。之前一直没有好好介绍一下这个属性。直到最近在给一些系统整体切换主题色的时候,更深入的了解了一下这个属性。

简单而言,CSS
accent-color
支持使用几行简单的 CSS 为
表单元素
着色,是的,只需几行代码就可以将主题颜色应用到页面的表单输入。

表单元素一直被吐槽
很难自定义
。而
accent-color
就是规范非常大的一个改变,我们开始能更多的自定义原生的表单的样式了!

如何使用
accent-color

OK,我们一起来学习一下,我们应该如何使用
accent-color

首先,我们来实现这么一个简单的表单界面:

<div class="wrapper">
	<form action="">
		<fieldset>
			<legend>Accent-color Demo</legend>

			<label>
				Strawberries
				<input type="checkbox" id="berries_1" value="strawberries">
			</label>

			<label>
				Radio Buttons
				<div>
					<input type="radio" name="accented-demo" checked>
					<input type="radio" name="accented-demo">
					<input type="radio" name="accented-demo">
				</div>
			</label>

			<label>
				Range Slider
				<input type="range">
			</label>

			<label>
				Progress element
				<progress max="100" value="50">50%</progress>
			</label>
		</fieldset>
	</form>
</div>

只需要最简单的布局 CSS,与
accent-color
关系不大,我就不列出来了,这样,我们的 DEMO 大致如下:

可以看到,表单控件的主题颜色是
蓝色
,在之前,我们是没办法修改这个颜色的。

而现在,我们可以简单的借助
accent-color
,修改表单的主题色:

:root {
	accent-color: rgba(250, 15, 117);
}

其中,
rgba(250, 15, 117)
表示粉色,此时,整体的效果就变成了:

当然,这个
accent-color
也支持传入 CSS 变量,配合更多的其他颜色一起进行修改。

我们可以对上述的 DEMO 再简单改造:

:root {
	--brand: rgba(250, 15, 117);
	accent-color: var(--brand);
}
fieldset {
	border: 1px solid var(--brand);
}
legend {
	color: var(--brand);
}

我们设置了一个 CSS 变量
--brand: rgba(250, 15, 117)
,除了把这个颜色赋值给表单的
accent-color
,还能赋值给其它更多元素。譬如这里,我们赋值给了
<fieldset>
的边框色和
<legend>
的文字颜色。

这样,当我们修改 CSS 变量值时,整个主题色会一起发生变化:

完整的 DEMO,你可以戳这:
CodePen Demo -- Accent-color with custom property

通常而言,更多的时候,我们会将
accent-color
应用于:


color-scheme
配合使用,适配日间夜间模式

还有一个容易忽略的细节点。
accent-color
还支持和
color-scheme
一起使用。

OK,什么是 color-scheme 呢?color-scheme 是 CSS 的一个属性,用于指定网页的颜色方案或主题。它定义了网页元素应该使用哪种颜色方案来呈现内容。

color-scheme 属性有以下几个可能的取值:

  • auto:表示使用用户代理(浏览器)的默认颜色方案。这通常是浏览器自动根据操作系统或用户设置选择的方案。
  • light:表示使用浅色颜色方案。这通常包括浅色背景和深色文本。
  • dark:表示使用深色颜色方案。这通常包括深色背景和浅色文本。

通过指定适当的 color-scheme 值,开发者可以为网页提供不同的颜色方案,以适应用户的偏好或操作系统的设置。这有助于提供更好的可访问性和用户体验。

譬如,我们可以将页面的
color-schema
设置为
light dark

body {
  color-scheme: light dark;
}

上述代码表示页面将同时支持浅色和深色颜色方案。它告诉浏览器,网页希望适应用户代理(浏览器)的默认颜色方案,并同时支持浅色和深色模式。

当使用
color-scheme: light dark
时,浏览器会根据用户代理的默认颜色方案来选择适当的颜色方案。如果用户代理处于浅色模式,网页将使用浅色颜色方案来呈现内容;如果用户代理处于深色模式,网页将使用深色颜色方案来呈现内容。

此时,我们的代码可以这样改造:

:root {
	--brand: rgba(250, 15, 117, 1);
	accent-color: var(--brand);
}
@media (prefers-color-scheme: dark) {
	:root {
		--brand: rgba(3, 169, 244, 1);
	}
	
	body {
		background: #000;
		color: #fff;
	}
}
body {
	color-scheme: light dark;
}

下面是我在手机上调整日间模式和夜间模式的效果图:

通过
@media (prefers-color-scheme: dark) {}
媒体查询,在黑夜模式下,展示不同的
accent-color

可能有人对
@media (prefers-color-scheme: dark)
还不太了解,可以看看我的这篇文章 --
使用 CSS prefers-* 规范,提升网站的可访问性与健壮性

完整的 DEMO,你可以戳这:
CodePen Demo -- Accent-color with custom property

最后

怎样,学会了吗。并且,根据规范描述,后续
accent-color
将会应用于更多的元素。将未来的 CSS 中会逐渐变得更加重要。早点掌握不是坏事。


好了,本文到此结束,希望本文对你有所帮助

至于未来会怎样,要走下去才知道反正路还很长,天总会亮。

1. 面向对象

1.1 什么是面向对象(OOP)

面向对象 Object Oriented Programming。在软件开发中,我们虽然用的是面向对象的语言,但我相信绝大多数入门或者工作经验不长的同学敲出来的代码依然是大段的面向过程的思想,我们只是把面向对象来当做 OC 语言的一个特性而已,具体是什么估计自己也说不明白到底是什么。那么到底该怎么去理解面向对象编程呢?

面向对象是一种程序设计的范型,同时也是一种程序开发的方法。面向对象是将现实世界中的事物抽象成对象,现实世界中的关系抽象成类、继承,帮助人们实现对现实世界的抽象与数字建模,用更利于人的理解方式,对复杂的系统进行分析、设计与编程。

现代的程序开发几乎都是以面向对象为基础。而在面向对象广泛流行之前,软件行业中使用最广泛的设计模式是面向过程方式。面向过程的操作是以程序的基本功能实现为主,开发过程中只针对问题本身的实现,并没有很好的模块化设计,所以在代码维护的时候较为麻烦。而面向对象,采用的更多的是进行子模块化的设计,每一个模块都需要单独的存在,并且可以被重复利用。所以面向对象开发更像是一个具备标准模式的编程开发,每一个单独设计的模块都可以单独存在,需要时候只要通过简单的组装就可以使用。但是,面向对象的底层还是面向过程,这两种程序设计思想是可以互相依存的,也是贯穿我们整个程序开发周期的思想指导,我们在设计程序、开发程序中应该牢记“面向对象”、“面向过程”。面向对象中类和对象是最基本、最重要的组成单元。下面会讲到该怎么理解类和对象。

1.2 面向对象的三大特征

继承:
继承是子类继承父类非私有数据结构和方法的机制,是类之间的一种关系。它是面向对象语言特有的特征,面向过程的语言不具有继承特性,而 OC 是单继承。继承提供了类的规范等级结构,使公共的特性能够共享,提高的软件的重用性。类的继承性使所建的软件具有开发性、可扩充性,简化了对象、类的创建工作量,提高了代码的重用性。

封装:
在面向对象的语言中,对象、类、方法都是一种封装,对象是封装的最基本单位。

类的封装体现在每个类都有 .h 和 .m 两个文件,将定义与实现分开,.h 声明(用户可见的外部接口),.m实现(用户不可见的内部实现)。

方法的封装,是最常见的,每个方法中封装了一个小的功能,这是单一职责的很好体现,第三方框架和代码也是一种封装。

封装使程序的结构更加清晰,将实现的信息封装隐藏,用的时候直接调用封装好的方法或类,提高效率。此外,可以减少程序间的相互依赖。

多态:
不同对象以自己的方式响应相同的消息的能力叫做多态。假设有一个类包含一个方法,由这个类派生出两个子类或者两个对象,其各自实现各自的方法,也就是不同的对象以自己的方式响应了相同的消息。多态增强了软件的灵活性和重用性。

1.3 对象的理解

什么是对象?世界万物皆对象,所有看到的看不到的都是对象,把对象引入到编程中,那就是面向对象编程(Object Oriented Programming,OOP,面向对象程序设计),在百度百科中有关于 OOP 详细的介绍。简单来说就是它将对象作为控件的基本元素, 利用对象和对象之间的相互作用来设计程序,说白了,一款软件的运行就是控件之间的相互作用。说了那么多,那到底啥是对象,对象是你,是我,是万物,我认为在程序中能
alloc 出来的都是对象
。就拿人来说,你是人,我也是人,为啥咱俩不一样,要是放到程序里来说,是因为 alloc 分配的内存地址不一样。人有头发、眼睛、手鼻等,这些在 OC 里面称之为属性,人能跑、跳、投,等动作这些在 OC 里面称之为方法。其实,你在做项目的时候已经在用对象这个概念了,只不过你不知道罢了。举个简单的例子:点击 tableview 的 cell 让 cell 里面的控件变换颜色,我相信大家都能实现这个效果,是怎么做的呢?肯定是定位点击的哪个 cell,拿到当前 cell,那这个 cell 不就是
对象
,找到 cell 中的控件,控件不就是 cell 的属性吗?颜色也是控件的属性,控件即是对象也是属性,控件间的相互作用完成了这个功能,说到底也就是操作的对象。

1.4 类的理解

面向对象编程中,具体的事物是对象,将具有相同或相似性质的对象的属性或方法抽象出来便是类,类是对象的抽象化,对象便是类的具体实现。估计这不好理解,再拿网络请求数据来讲,数据都有相同或相似的数据,将这段数据的相同点抽离出来便是我们所建的 model 类,比如 person 类,属性有:name、height、weight 等,他们具有相同的属性将属性他们通过一个方法将数据进行转化,那这个方法不就是封装吗?一般都是在 .h 文件中声明方法,在 .m 中去实现,方法一般都是隐藏内部实现,预留一个稳定外部接口。面向对象程序设计中的方法可分为两种,一为上述的实体(对象)方法,二为类方法,主要的差异在于实体方法需要有一对象去引发,而类别方法可以由类别名称调用。

2. Objective-C 类

2.1 类概念

如同所有其他的面向对象语言,类是 Objective-C 用来封装数据,以及操作数据的行为的基础结构。对象就是类的运行期间实例,它包含了类声明的实例变量自己的内存拷贝,以及类成员的指针。Objective-C 的类规格说明包含了两个部分:定义(interface)与实现(implementation)。定义(interface)部分包含了类声明和实例(成员)变量的定义,以及类相关的方法。实现(implementation)部分包含了方法的实现,以及定义私有(private)变量及方法。类的定义文件遵循 C 语言之惯例以 .h 为后缀,实现文件以 .m 为后缀。

2.2 类定义(Interface)

2.2.1 定义

定义文件遵循 C 语言之惯例以 .h 为后缀。定义部分,清楚定义了类的名称、数据成员和方法。 以关键字 @interface 作为开始,@end 作为结束。

下面定义一个叫做 XBCar 的类的语法,这个类继承自 NSObject 基础类。类名之后的(用冒号分隔的)是父类的名字。类的实例(成员)变量声明在被大括号包含的代码块中。实例变量块后面就是类声明的方法的列表。每个实例变量和方法声明都以分号结尾。

@interface XBCar : NSObject {
    // 成员变量
    float maxVelocity;
    double money;
  	// 实例变量
    NSString *name;
}

+ (void)class_method; // 类方法
- (void)instance_method1; // 实例方法
- (void)instance_method2:(int)p1;

@end

​ 类定义具体内容包括:

  1. 类的声明,类声明总是由 @interface 编译选项开始,由 @end 编译选项结束;
  2. 实例(成员)变量的定义(用大括号包含的);
  3. 类相关方法的定义:类方法和实例方法;

2.2.2 实例变量和成员变量的理解

实例(Instance)是针对类(class)而言的。实例是指类的声明; 由此推理,实例变量(Instance Variable) 是指由类声明的对象。成员变量就是基本类型声明的变量。

严格来说
@interface{}
里定义的实例(成员)变量,它是这个类内部真正的全局变量。然而这个 instance variable 是不对外公开的,因此我们还需要一个对外公开的东西来调用,就是属性,关键字 @property。它其实是告诉大家,我这个类里,有一个变量的 seter/geter 方法。比如,@property NSString* string;就是说,本类里有一个 string/setString 供你们调用。属性具体的使用后面会讲到,这里就不展开了。

2.3 类实现(Implementation)

实现文件以 .m 为后缀。实现区块则包含了公开方法的实现,以及定义私有(private)变量及方法。 以关键字 @implementation 作为区块起头,@end结尾。

@implementation XBCar {
    int private; // 私有成员变量
}

+ (void)class_method {
    
}

- (void)instance_method1 {
    
}
- (void)instance_method2:(int)p1{
    
}

值得一提的是不只 Interface 区块可定义实例(成员)变量,Implementation 区块也可以定义实例(成员)变量,两者的差别在于访问权限的不同,Interface 区块内的实体变量默认权限为 protected,implementation 区块的实例(成员)变量则默认为 private,故在 implementation 区块定义私有成员更匹配面向对象之封装原则,因为如此类别之私有信息就不需曝露于公开 interface(.h文件)中。在程序开发中我们会发现在实现文件(.m)中,有如下这样的代码:

@interface XBCar () // Class Extension

@end

那么问题来了,为什么 .h 文件和 .m 文件里各有1个 @interface 它们分别有什么用呢?

定义文件(.h 文件)里面的
@interface
,不用说,是典型的头文件,用来定义(声明)类的。

实现文件(.m 文件)里面的
@interface
,在 OC 里叫作 Class Extension,是 .h文件中
@interface
声明类的补充扩展。但是 .m 文件里的
@interface
,对外是不开放的,只在 .m 文件里可见。

2.4 创建对象

Objective-C 创建对象需通过 alloc 以及 init 两个消息。alloc 的作用是分配内存,init 则是初始化对象。 init 与 alloc 都是定义在 NSObject 里的方法,父对象收到这两个消息并做出正确回应后,新对象才创建完毕。

XBCar *car = [[XBCar alloc] init];

Objective-C 还可以通过 new 关键字来创建对象。

XBCar *blueCar = [XBCar new];

那么 alloc/init 和 new 有什么区别呢?首先功能上他两几乎是一致的,都是分配内存并完成初始化。差别在于,采用 new 的方式创建对象只能采用默认的 init 方法完成初始化,而通过 alloc 的方式可以采用其他定制的初始化方法完成初始化。另外 alloc 分配内存的时候使用了 zone。它是给对象分配内存的时候,把关联的对象分配到一个相邻的内存区域内,以便于调用时消耗很少的代价,提升了程序处理速度。

2.5 方法声明

1. 方法声明语法.png

当你想调用一个方法,你传递消息到对应的对象。这里消息就是方法标识符,以及传递给方法的参数信息。发送给对象的所有消息都会动态分发,这样有利于实现 Objective-C 类的多态行为。也就是说,如果子类定义了跟父类的具有相同标识符的方法,那么子类首先收到消息,然后可以有选择的把消息转发(也可以不转发)给他的父类。

消息被中括号( [ 和 ] )包括。中括号中间,接收消息的对象在左边,消息(包括消息需要的任何参数)在右边。例如,给 myArray 变量传递消息insertObject:atIndex: 消息,你需要使用如下的语法:

[myArray insertObject:anObj atIndex:0];

为了避免声明过多的本地变量保存临时结果,Objective-C 允许你使用嵌套消息。每个嵌套消息的返回值可以作为其他消息的参数或者目标。例如,你可以用任何获取这种值的消息来代替前面例子里面的任何变量。所以,如果你有另外一个对象叫做 myAppObject 拥有方法,可以访问数组对象,以及插入对象到一个数组,你可以把前面的例子写成如下的样子:

[[myAppObject getArray] insertObject:[myAppObject getObjectToInsert] atIndex:0];

虽然前面的例子都是传递消息给某个类的实例,但是你也可以传递消息给类本身。当给类发消息,你指定的方法必须被定义为类方法,而不是实例方法。你可以认为类方法跟 C++ 类里面的静态成员有点像(但是不是完全相同的)。

类方法的典型用途是用做创建新的类实例的工厂方法,或者是访问类相关的共享信息的途径。类方法声明的语法跟实例方法的几乎完全一样,只有一点小差别。与实例方法使用减号作为方法类型标识符不同,类方法使用加号( + )。

下面的例子演示了一个类方法如何作为类的工厂方法。在这里,arrayWithCapacity 是 NSMutableArray 类的类方法,为类的新实例分配内容并初始化,然后返回给你。

NSMutableArray* myArray = nil; // nil 基本上等同于 NULL
// 创建一个新的数组,并把它赋值给 myArray 变量
myArray = [NSMutableArray arrayWithCapacity:0];

2.6 属性

属性(property)用于封装对象中的数据,iOS 开发中最常用最方便的变量声明方式,允许我们用点语法来访问对象的实例变量。

2.6.1 属性的实质是什么

@property 是声明属性的语法。可以快速为实例变量创建存取器。允许我们通过点语法使用存取器。

	属性(@property) = 实例变量定义 + setter方法 + getter方法。

当我们声明一个属性属性
name
的时候,在编译阶段,编译器会自动给对象添加一个实例变量
name
和它的存取方法
- (void)setName:(NSString *)name

- (NSString *)name
。这个过程由于是在编译阶段自动合成的,所以我们在编辑阶段是看不到的。添加实例变量是有一个前提的,就是对象还没有同名的成员变量,就是如果已经有 _name 了,就不再添加了。我们可以用运行时验证一下:

- (void)propertyTest {
    unsigned int count = 0;
    Ivar *varList = class_copyIvarList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        const char *varName = ivar_getName(varList[i]);
        printf("成员变量----%s\n", varName);
    }
    
    Method *methodList = class_copyMethodList([self class], &count);
    for (unsigned int i = 0; i < count; i++) {
        SEL methodName = method_getName(methodList[i]);
        NSLog(@"方法----%@",NSStringFromSelector(methodName));
    }
}

打印的日志:
成员变量----_name
2021-07-20 16:55:59.492166+0800 001 - Class[772:380998] 方法----propertyTest
2021-07-20 16:55:59.492321+0800 001 - Class[772:380998] 方法----name
2021-07-20 16:55:59.492551+0800 001 - Class[772:380998] 方法----setName:
2021-07-20 16:55:59.492664+0800 001 - Class[772:380998] 方法----viewDidLoad

2.6.2 @synthesize

@synthesize 属于编译器指令,用来告诉编译器要做什么。

@property (strong,nonatomic) NSString *name;
@synthesize name = myname;
@dynamic name;

@synthesize
关键字主要有两个作用,在 ARC 下已经很少用了。

  • 在 MRC 下,
    @synthesize name
    ,用在实现文件中告诉编译器自动实现实例变量
    name
    的存取(访问器 getter/setter)方法。不过在 ARC 下就不必了,无论你是否
    @synthesize name
    ,编译器都会自动合成 name 的存取方法。
  • 如果你声明的属性是 name,系统自动给你添加的成员变量是
    _name
    ,如果你对这个变量名字不满,可以这样
    @synthesize name = myname
    ;自己给个名字。这样系统给添加的成员变量就是 myname,而不是_name,但是变量的存取方法没有变化。不过我建议最好不要这么办,因为都按照约定成俗的方式来命名变量,代码的可读性较高,大家都理解,所以我建议大家最好不要用这个关键字。

2.6.3 @dynamic

@dynamic 关键字主要是告诉编译器不用为我们自动合成变量的存取方法, 我们会自己实现。即使我们没有实现,编译器也不会警告,因为它相信在运行阶段会实现。如果我们没有实现还调用了,就会报这个错误
'-[ViewController setName:]: unrecognized selector sent to instance 0x10040af10'

2.6.4 属性的特性(关键字)

1)属性特性概述

原子性:

  • atomic(默认):atomic 意为操作是原子的,意味着只有一个线程访问实例变量(生成的 setter 和 getter 方法是一个原子操作)。atomic 是线程安全的,至少在当前的存取器上是安全的。它是一个默认的特性,但是很少使用,因为比较影响效率;
  • nonatomic:意为操作是非原子的,可以被多个线程访问。它的效率比atomic 快。但不能保证在多线程环境下的安全性,开发中常用,所以我们在写代码的时候要尽量避非免线程安全的代码出现;

读写权限(存取器控制):

  • readwrite(默认):readwrite 是默认值,表示该属性同时拥有 getter 和 setter;
  • readonly: readonly 表示只有 getter 没有 setter;
  • 有时候为了语意更明确可能需要自定义访问器的名字;

内存管理语义:

  • retain(MRC)/strong(ARC)

    内存管理语义:强引用

    系统默认:ARC 情况下是默认

    作用:只能修饰对象。对象会更改引用计数,那么每次被引用,引用计数都会+1,释放后都会-1;即使对象本身被释放了,只要还有对象在引用,就会持有不会造成什么问题;只有当引用计数为0时,就被dealloc析构函数回收内存了。对应变量权限修饰符为__strong。

  • weak

    内存管理语义:弱引用

    系统默认:否

    作用:只能修饰对象。不改变对象的引用计数,当其指向对象被销毁时,它会自动置为nil;变量权限修饰符为__weak,常用于容易造成循环引用的地方不改变对象的引用计数。

  • assign

    内存管理语义:赋值

    系统默认:MRC 情况下是

    作用:主要用于修饰值类型,如 int、float、double 和 CGPoint、CGFloat 等表示单纯的复制。还包括不存在所有权关系的对象,比如常见的 delegate。值类型变量的内存由编译器自动管理;修饰对象属性时,其指向一个对象后,不改变该对象的引用计数。即只引用已创建的对象,而不持有对象;assign 修饰的属性不持有对象,当其指向对象在别处释放后,该指针变为悬挂指针也叫野指针。对应的变量权限修饰符为
    __unsafe_unretained

  • unsafe_unretained

    用来修饰属性的时候,和 assing 修饰对象的时候是一模一样的。为属性设置新值的时候。唯一的区别就是当属性所指的对象释放的时候,属性不会被置为 nil,这就会产生野指针,所以是不安全的。对应的变量权限修饰符为
    __unsafe_unretained

  • copy

    内存管理语义:复制/拷贝

    系统默认:否

    作用:只能修饰对象。一般情况下属性会持有该对象的一份拷贝,建立一个索引计数为1的新对象,然后释放旧对象。
    copy 一般用在修饰有可变对应类型的不可变对象上,如 NSString, NSArray, NSDictionary

2)strong 和 copy 修饰属性的区别

不可变字符串:

@property (nonatomic,strong)NSString* strongedString;
@property (nonatomic,copy)NSString* copyedString;

// 当tempString为不可变字符串时候
NSString *tempString = @"Text";
self.strongedString = tempString;
self.copyedString = tempString;
NSLog(@"tempString : %@ %p %p",tempString, tempString, &tempString);
NSLog(@"strongedString : %@ %p %p",_strongedString, _strongedString, &_strongedString);
NSLog(@"copyedString : %@ %p %p",_copyedString, _copyedString, &_copyedString);

tempString = @"Change OK";
NSLog(@"tempString : %@ %p %p",tempString, tempString, &tempString);
NSLog(@"strongedString : %@ %p %p",_strongedString, _strongedString, &_strongedString);
NSLog(@"copyedString : %@ %p %p",_copyedString, _copyedString, &_copyedString);

// 打印结果:
BaseGrammar[40394:185339] tempString : Text 0x10d858e68 0x7ffee23adbf8
BaseGrammar[40394:185339] strongedString : Text 0x10d858e68 0x7ff634530440
BaseGrammar[40394:185339] copyedString : Text 0x10d858e68 0x7ff634530448
// 改变后:
BaseGrammar[40394:185339] tempString : Change OK 0x10d858ee8 0x7ffee23adbf8
BaseGrammar[40394:185339] strongedString : Text 0x10d858e68 0x7ff634530440
BaseGrammar[40394:185339] copyedString : Text 0x10d858e68 0x7ff634530448

结论:当 tempString 为不可变字符串时

  • 不管是 strong 还是 copy 属性的对象,其指向的地址都是同一个,即为tempString 指向的地址。
  • 如果我们换作 MRC 环境,打印 tempString 的引用计数的话,会看到其引用计数值是3,即 strong 操作和 copy 操作都使原字符串对象的引用计数值加了1。
  • 当 tempString 的值发生改变时,两个对象的值也保持原来的值。

可变字符串:

// 可变字符串
NSMutableString *tempString = [NSMutableString stringWithFormat:@"Text"];
self.strongedString = tempString;
self.copyedString = tempString;
NSLog(@"tempString : %@ %p %p",tempString, tempString, &tempString);
NSLog(@"strongedString : %@ %p %p",_strongedString, _strongedString, &_strongedString);
NSLog(@"copyedString : %@ %p %p",_copyedString, _copyedString, &_copyedString);

// 改变tempString的值
[tempString appendString:@"Change OK"];
NSLog(@"tempString : %@ %p %p",tempString, tempString, &tempString);
NSLog(@"strongedString : %@ %p %p",_strongedString, _strongedString, &_strongedString);
NSLog(@"copyedString : %@ %p %p",_copyedString, _copyedString, &_copyedString);

// 打印结果:
BaseGrammar[42008:192582] tempString : Text 0x60000279c960 0x7ffee5983bf8
BaseGrammar[42008:192582] strongedString : Text 0x60000279c960 0x7fe20fe14380
BaseGrammar[42008:192582] copyedString : Text 0xb96aa7441ad7466e 0x7fe20fe14388
// 改变后:
BaseGrammar[42008:192582] tempString : TextChange OK 0x60000279c960 0x7ffee5983bf8
BaseGrammar[42008:192582] strongedString : TextChange OK 0x60000279c960 0x7fe20fe14380
BaseGrammar[42008:192582] copyedString : Text 0xb96aa7441ad7466e 0x7fe20fe14388
  

结论:当 tempString 为可变字符串时

  • 此时 copy 属性字符串已不再指向 tempString 字符串对象,而是深拷贝了 tempString 字符串,并让 copyedString 对象指向这个字符串。

  • strongString 与 tempString 是指向同一对象,所以 strongString 的值也会跟随着改变(需要注意的是,此时 strongString 的类型实际上是NSMutableString,而不是 NSString);而 copyedString 是指向另一个对象的,所以并不会改变。

  • 在 MRC 环境下,打印两者的引用计数,可以看到 tempString 对象的引用计数是2,而 copyedString 对象的引用计数是1。

由上面分析我们能得到一个结论:
copy 对于不可变对象是浅拷贝,对于可变对象是深拷贝
。在声明 NSString 属性时,到底是选择 strong 还是 copy,可以根据实际情况来定。不过,一般我们将对象声明为 NSString 时,都不希望它改变,所以大多数情况下,我们建议用 copy,以免因可变字符串的修改导致的一些非预期问题。即对于可变字符串以得到新的内存分配,而不只是原来的引用。

3)assign 和 weak 的区别

assign 的特点:

  • 修饰基本数据类型和原子类类型。
  • 修饰对象类型时,不改变其引用计数。
  • 会产生悬挂指针(野指针),用 assign 修饰的对象被释放之后,assign指针仍指向原对象的地址内存,继续使用 assign 指针访问原对象,会产生悬挂指针导致内存泄漏。

weak 的特点:

  • 只能修饰对象。
  • 不改变修饰对象的引用计数。
  • 所指对象在被释放之后会自动置为 nil。
4)深浅拷贝

浅拷贝:就是对内存地址的复制,让目标对象指针和原对象指针指向同一片内存空间。浅拷贝增加了被拷贝对象的引用计数,并没有产生新的内存分配空间。

深拷贝:就是让对象指针和原对象指针指向两片内容相同的内存空间。深拷贝没有增加被拷贝对象的引用计数,产生新的内存分配控件。

总结:

copy:对于可变对象为深拷贝,对于不可变对象为浅拷贝。拷贝出来的是不可变对象。

mutableCopy:始终是深拷贝。拷贝出来的是可变对象。

总的来说在 Objective-C 里面只有一种情况是浅拷贝,那就是不可变对象的copy,其它的都是深拷贝(包括不可变对象mutableCopy、可变对象的的 copy 和mutableCopy)。

3. 引用关键字

关键字说明:

  • #import
    是 Objective-C 导入头文件的关键字。确保一个头文件只能被导入一次,这使你在递归包含中不会出现问题,所以 #import 比起#include 的好处是不会引起交叉编译。


    • import<>
      代表导入系统自带的框架;

    • import""
      代表导入我们自己创建的头文件;

  • #include
    是C/C++导入头文件的关键字。

  • @class
    一般用于声明某个字符串作为类名使用,它只是声明了一个类名,没有导入 .h 文件中的内容,不会引起交叉编译问题。

import 和 @class 的区别总结:

  • Import 会包含这个类的所有信息,包括实体变量和方法(.h 文件中),而 @class 只是告诉编译器,其后边的声明的名称是类的名称,至于类是如何定义的,后边再说,这边不关心。或者说 @class 创建了一个前向引用,就是在告诉编译器,“相信我,以后你会知道这个类到底是什么,但是现在,你只需要知道这些就好”,如果有循环依赖关系;
  • 在头文件中,一般只需要知道被引用的类的名称就可以了,不需要知道其内部的实体变量和方法,所以在头文件中一般使用 @class 来声明这个名称是类的名称,而在实现类里边,因为会用到这个引用类的内部的实体变量和方法,所以需要使用 #import 来包含这个被引用类的头文件;

使用总结:

  1. 如果不是 C/C++,尽量使用 #import;

  2. 能在实现文件中 #import,就不在头文件中 #import;

  3. 能在头文件中 @class 实现文件中 #import,就不再头文件中 #import;

在常用的后台管理系统中,通常都会有访问权限控制的需求,用于限制不同人员对于接口的访问能力,如果用户不具备指定的权限,则不能访问某些接口。

本文将用 waynboot-mall 项目举例,给大家介绍常见后管系统如何引入权限控制框架 Spring Security。大纲如下,

image

一、什么是 Spring Security

Spring Security 是一个基于 Spring 框架的开源项目,旨在为 Java 应用程序提供强大和灵活的安全性解决方案。Spring Security 提供了以下特性:

  • 认证:支持多种认证机制,如表单登录、HTTP 基本认证、OAuth2、OpenID 等。
  • 授权:支持基于角色或权限的访问控制,以及基于表达式的细粒度控制。
  • 防护:提供了多种防护措施,如防止会话固定、点击劫持、跨站请求伪造等攻击。
  • 集成:与 Spring 框架和其他第三方库和框架进行无缝集成,如 Spring MVC、Thymeleaf、Hibernate 等。

二、如何引入 Spring Security

在 waynboot-mall 项目中直接引入 spring-boot-starter-security 依赖,

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
        <version>3.1.0</version>
    </dependency>
</dependencies>

三、如何配置 Spring Security

在 Spring Security 3.0 中要配置 Spring Security 跟以往是有些不同的,比如不在继承 WebSecurityConfigurerAdapter。在 waynboot-mall 项目中,具体配置如下,

@Configuration
@EnableWebSecurity
@AllArgsConstructor
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {
    private UserDetailsServiceImpl userDetailsService;
    private AuthenticationEntryPointImpl unauthorizedHandler;
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // cors启用
                .cors(httpSecurityCorsConfigurer -> {})
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(httpSecuritySessionManagementConfigurer -> {
                    httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
                })
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer -> {
                    httpSecurityExceptionHandlingConfigurer.authenticationEntryPoint(unauthorizedHandler);
                })
                // 过滤请求
                .authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> {
                    authorizationManagerRequestMatcherRegistry
                            .requestMatchers("/favicon.ico", "/login", "/favicon.ico", "/actuator/**").anonymous()
                            .requestMatchers("/slider/**").anonymous()
                            .requestMatchers("/captcha/**").anonymous()
                            .requestMatchers("/upload/**").anonymous()
                            .requestMatchers("/common/download**").anonymous()
                            .requestMatchers("/doc.html").anonymous()
                            .requestMatchers("/swagger-ui/**").anonymous()
                            .requestMatchers("/swagger-resources/**").anonymous()
                            .requestMatchers("/webjars/**").anonymous()
                            .requestMatchers("/*/api-docs").anonymous()
                            .requestMatchers("/druid/**").anonymous()
                            .requestMatchers("/elastic/**").anonymous()
                            .requestMatchers("/message/**").anonymous()
                            .requestMatchers("/ws/**").anonymous()
                            // 除上面外的所有请求全部需要鉴权认证
                            .anyRequest().authenticated();
                })
                .headers(httpSecurityHeadersConfigurer -> {
                    httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable);
                });
                // 处理跨域请求中的Preflight请求(cors),设置corsConfigurationSource后无需使用
                // .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                // 对于登录login 验证码captchaImage 允许匿名访问

        httpSecurity.logout(httpSecurityLogoutConfigurer -> {
            httpSecurityLogoutConfigurer.logoutUrl("/logout");
            httpSecurityLogoutConfigurer.logoutSuccessHandler(logoutSuccessHandler);
        });
        // 添加JWT filter
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 认证用户时用户信息加载配置,注入springAuthUserService
        httpSecurity.userDetailsService(userDetailsService);
        return httpSecurity.build();
    }
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

这里详细介绍下 SecurityConfig 配置类,

  • filterChain(HttpSecurity httpSecurity) 方法是访问控制的核心方法,这里面可以针对 url 设置是否需要权限认证、cors 配置、csrf 配置、用户信息加载配置、jwt 过滤器拦截配置等众多功能。
  • authenticationManager(AuthenticationConfiguration authenticationConfiguration) 方法适用于启用认证接口,需要手动声明,否则启动报错。
  • bCryptPasswordEncoder() 方法用户定义用户登录时的密码加密策略,需要手动声明,否则启动报错。

四、如何使用 Spring Security

要使用 Spring Security,只需要在需要控制访问权限的方法或类上添加相应的 @PreAuthorize 注解即可,如下,

@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("system/role")
public class RoleController extends BaseController {

    private IRoleService iRoleService;

    @PreAuthorize("@ss.hasPermi('system:role:list')")
    @GetMapping("/list")
    public R list(Role role) {
        Page<Role> page = getPage();
        return R.success().add("page", iRoleService.listPage(page, role));
    }
}

我们在 list 方法上加了
@PreAuthorize("@ss.hasPermi('system:role:list')")
注解表示当前登录用户拥有 system:role:list 权限才能访问 list 方法,否则返回权限错误。

五、获取当前登录用户权限

在 SecurityConfig 配置类中我们定义了 UserDetailsServiceImpl 作为我们的用户信息加载的实现类,从而通过读取数据库中用户的账号、密码与前端传入的账号、密码进行比对。代码如下,

@Slf4j
@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private IUserService iUserService;

    private IDeptService iDeptService;

    private PermissionService permissionService;

    public static void main(String[] args) {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        System.out.println(bCryptPasswordEncoder.encode("123456"));
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 读取数据库中当前用户信息
        User user = iUserService.getOne(new QueryWrapper<User>().eq("user_name", username));
        // 2. 判断该用户是否存在
        if (user == null) {
            log.info("登录用户:{} 不存在.", username);
            throw new UsernameNotFoundException("登录用户:" + username + " 不存在");
        }
        // 3. 判断是否禁用
        if (Objects.equals(UserStatusEnum.DISABLE.getCode(), user.getUserStatus())) {
            log.info("登录用户:{} 已经被停用.", username);
            throw new DisabledException("登录用户:" + username + " 不存在");
        }
        user.setDept(iDeptService.getById(user.getDeptId()));
        // 4. 获取当前用户的角色信息
        Set<String> rolePermission = permissionService.getRolePermission(user);
        // 5. 根据角色获取权限信息
        Set<String> menuPermission = permissionService.getMenuPermission(rolePermission);
        return new LoginUserDetail(user, menuPermission);
    }
}

针对 UserDetailsServiceImpl 的代码逻辑进行一个讲解,大家可以配合代码理解。

  1. 读取数据库中当前用户信息
  2. 判断该用户是否存在
  3. 判断是否禁用
  4. 获取当前用户的角色信息
  5. 根据角色获取权限信息

总结一下

本文给大家讲解了后管系统如何引入权限控制框架 Spring Security 3.0 版本以及代码实战。相信能帮助大家对权限控制框架 Spring Security 有一个清晰的理解。后续大家可以按照本文的使用指南一步一步将 Spring Security 引入到的自己的项目中用于访问权限控制。

想要获取 waynboot-mall 项目源码的同学,可以关注我公众号【程序员wayn】,回复 waynboot-mall 即可获得。

如果觉得这篇文章写的不错的话,不妨点赞加关注,我会更新更多技术干货、项目教学、实战经验分享的文章。

我们在《
SqlSugar开发框架
》中,Winform界面开发部分往往也用到了自定义的用户控件,对应一些特殊的界面或者常用到的一些局部界面内容,我们可以使用自定义的用户控件来提高界面的统一性,同时也增强了使用的便利性。如我们Winform界面中用到的分页控件、附件显示内容、以及一些公司、部门、菜单的下拉框列表等等,由于重复多处使用,因此一处封装多处收益。

1、回顾Winform界面中自定义的用户控件的处理场景

其实我的关于Winform的开发随笔,介绍了不少的控件使用、以及自定义控件的使用例子,如随笔《
在Winform界面使用自定义用户控件及TabelPanel和StackPanel布局控件
》中介绍到模仿牙医管家的软件界面的部分,来创建一些自定义部分的内容。

根据其中显示的内容部分,制作了一个用户控件,在其中添加一个LayoutControl方便控制布局,添加一些标签以及设置了一些图标,得到下图所示。

其中定义的用户控件的源码部分,继承自XtraUserControl用户控件基类(如果是传统样式的Winform界面,可以继承自UserControl),修改其中源码增加对应的属性,方便动态设置用户控件的相关属性,如颜色块,项目背景色,以及绑定的对象信息等内容。

然后通过自定义控件的事件或者方法对界面内容进行更新处理即可。完成后我们看界面的效果如下所示,较为符合实际的效果即可。

一般来说,一个窗体用户控件不多的情况下,Winform界面的效果还是挺好的,如果界面的用户控件很多,如超过几千个,那么可能会有性能问题,之前在随笔《
使用Winform开发自定义用户控件,以及实现相关自定义事件的处理
》中介绍的关于动态展现大量历史号码信息的自定义控件的时候,就会出现一些句柄创建错误的问题。

控件集合可以通过布局TableLayoutPanel(表格布局)或者FlowLayoutPanel(顺序流布局)来添加即可。如果利用利用TableLayoutPanel来展示,那么需要设置好每列的宽度或者比例,如下界面所示。

表格的行列定义如下所示。

由于自定义控件,我们需要跟踪用户的单击处理,并且需要把这个逻辑逐步推动到顶级界面上去进行处理,因此需要定义一个事件信息,如下所示。

        /// <summary>
        ///事件处理/// </summary>
        public EventHandler<ClickEventData> ClickEventHandler { get; set; }

控件的动态添加处理,可以同时指定它的匿名事件的处理逻辑,从而对控件的事件进行更新。

    var controlList = new List<LotteryItemControl2>();foreach (var info inlist)
{
var control = newLotteryItemControl2();

control.Qi
= info.LineNo.ToString("D2");var numberList = new List<string>()
{
info.No1.ToString(
"D2"),
info.No2.ToString(
"D2"),
info.No3.ToString(
"D2"),
info.No4.ToString(
"D2"),
info.No5.ToString(
"D2"),
info.No6.ToString(
"D2"),
info.No7.ToString(
"D2"),
};
control.NumberList
=numberList;
control.BindData();

control.ClickEventHandler
+= (s, data) =>{//遍历所有的控件统一处理样式 foreach (var subCtrl inpanel.Controls)
{
if (subCtrl isLotteryItemControl2 lottery)
{
lottery.SetSelected(data);
}
}
};
controlList.Add(control);
}
this.panel.Controls.AddRange(controlList.ToArray());

如果我们不喜欢每个控件都对事件进行一个层层的处理,我们也可以引入MediatR来实现事件总线的处理,如我随笔介绍《
在Winform系统开发中,使用MediatR来实现类似事件总线的消息处理
》,

MediatR的GitHub项目地址:
https://github.com/jbogard/MediatR

我们在程序启动的时候,注入对应的接口服务IMediator,那么我们就可以通过该静态类的 GetService<T>() 方法获取对应的注入接口IMediator,我们需要利用该接口来发送Send请求/应答命令或者发布Publish消息的处理。

public partial classTestMediatR : BaseForm
{
private readonlyIMediator_mediator;publicTestMediatR()
{
InitializeComponent();

_mediator
= ServiceLocator.GetService<IMediator>();
}

MediatR是一个跨平台通过一种进程内消息传递机制,进行请求/响应、命令、查询、通知和事件的消息传递,并通过C#泛型来支持消息的智能调度,其目的是消息发送和消息处理的解耦。它支持以单播和多播形式使用同步或异步的模式来发布消息,创建和侦听事件。它主要的几个对象:

IMediator
:主要提供Send与Publish方法,需要执行的命令都是通过这两个方法实现

IRequest、IRequest<T>

命令查询 | 处理类
所继承的接口,一个有返回类型,一个无返回类型,一个查询对应一个处理类,程序集只认第一个扫描到的类。

IRequestHandler<in TRequest,TResponse>
(实现Handle方法) :
命令处理接口。命令
查询 | 处理
类继承它,也可以继承

AsyncRequestHandler(实现抽象Handle方法)、RequestHandler(实现抽象Handle方法)接口

INotification

命令查询 | 处理类
所继承的接口这个没有返回,与IRequest不通的是可以对于多个处理类。

INotificationHandler<in TNotification>
:与IRequestHandler一样的只不过这是INotification的处理接口。

例如我们发送消息后,收到应答消息结果,展示在界面中的如下所示。

/// <summary>
///使用请求、应答的消息进行测试,获得返回结果后输出显示/// </summary>
private async void btnSend_Click(objectsender, EventArgs e)
{
//应答处理 var outputMessage = await _mediator.Send(newRetrieveInfoCommandRequest
{
Text
= this.txtSend.Text
});
Console.WriteLine(outputMessage.OutputMessage);
this.txtReceived.AppendText(outputMessage.OutputMessage +Environment.NewLine);
}

如果控件比较多,处理的时候,刷新的时候记得移除面板上已经添加的控件。

//清空界面
while (panel.Controls.Count > 0)
{
var controltoremove = panel.Controls[0];
panel.Controls.RemoveAt(
0);
controltoremove.Dispose();
}
panel.Controls.Clear();

2、《
SqlSugar开发框架
》Winform界面中的自定义的用户控件的一些处理

例如我们在附件管理的时候,对于一些窗体的信息,我们需要了解该业务对应的附件信息有几个,并且提供入口可以查看或者管理附件列表,那么我们可以根据需要封装一个自定义的附件管理的自定义用户控件。

在实际界面应用的时候,由于附件管理的自定义控件已经封装好了,所以在使用的时候,拖动到界面即可,如下界面所示。

我们在做病历管理的时候,就需要大量用到不同的分类的附件信息的展示,如下界面效果所示。

还有就是有时候,对于权限管理里面,部门信息在不少的地方用到,如果每次对原始的下拉列表处理,那么增加不少工作量,如果把它封装为自定义控件,和常规的控件一样使用即可,就会很方便,如下界面所示。

它的实际展示效果如下所示。

单击下拉列表后,展示部门的列表信息。

同理,用户控件一旦创建后,我们可以在很多需要的地方直接使用,省却初始化的一些代码操作。

我们在初始化的时候,显示相关的部门列表,选择后获得部门的ID,也可以设置部门的ID。

/// <summary>
///部门显示控件/// </summary>
public partial classDeptControl : XtraUserControl
{
public string ParentOuID = "-1";/// <summary> ///选择的值发生变化的时候/// </summary> public eventEventHandler EditValueChanged;publicDeptControl()
{
InitializeComponent();
this.txtDept.EditValueChanged+= newEventHandler(cmbUpperOU_EditValueChanged);
}
void cmbUpperOU_EditValueChanged(objectsender, EventArgs e)
{
if (EditValueChanged != null)
{
EditValueChanged(sender, e);
}
}
private async void DeptControl_Load(objectsender, EventArgs e)
{
if (!this.DesignMode)
{
//限定用户的选择级别 var list = awaitPortal.gc.GetMyTopGroup();foreach (OuInfo OuInfo inlist)
{
if (OuInfo != null)
{
this.ParentOuID =OuInfo.Id.ToString();
}
}

Init();
}
}

需要可以响应相关的编辑事件,用来触发关联的信息变化,如下所示是自定义控件的使用代码。

public partial classFrmEditUser : BaseEditForm
{
publicFrmEditUser()
{
InitializeComponent();
this.txtDept.EditValueChanged+= newEventHandler(txtDept_EditValueChanged);
}
void txtDept_EditValueChanged(objectsender, EventArgs e)
{
if (!string.IsNullOrEmpty(txtDept.Value))
{
InitManagers(txtDept.Value.ToInt32());
}
}

在介绍一个场景,我们在一些选择用户的界面中,如CRM中对于分配用户、工作流中选择流程用户的操作中,往往需要选择系统的人员列表,可以多个选择,那么我们可以设计界面如下所示。

其中选择的人员用红色方框标识,这个部分的用户和移除图标是自定义控件,界面如下所示。

主要就是方便对用户进行显示和移除设置的一些简单的封装。

在比如在工作流的创建入口中,我们展示相关可以创建流程的快速入口,通过一些图片、文字来展示工作流程的信息,单击事件进行弹出不同的流程对话框处理过程。

这个过程主要就是美观性的要求,是相对于全部文本信息的单调有一些改善的效果。

链接附注

如对我们的代码生成工具有兴趣,可以到官网下载使用《
代码生成工具Database2Sharp
》。

如需了解我们官网对《SqlSugar开发框架》的介绍,可以参考《
SqlSugar开发框架
》了解。

如需阅读我们对于《SqlSugar开发框架》文章介绍,可以参考博客园的随笔标签《
SqlSugar随笔
,
WPF随笔
》学习了解。

好处

Compose 编译后不是转化为原生的 Android 上的 View 去显示
,而是依赖于平台的Canvas ,在这点上和 Flutter 有点相似,简单地说可以理解为 Compose 是全新的一套 View 。

声明式 UI,通过对比可以看到 Kotin DSL 有诸多好处:

  • 有着近似 XML 的结构化表现力
  • 较少的字符串,更多的强类型,更安全
  • 可提取 linearLayoutParams 这样的对象方便复用
  • 在布局中同步嵌入 onClick 等事件处理
  • 如需要还可以嵌入 if ,for 这样的控制语句
  • 减少 findViewById 等函数遍历树
  • 加速开发

    View 与 Compose 之间可以相互调用
    ,兼容现有的所有代码。借助 AS 可以实时预览界面,轻松执行界面检查。
  • 另外 Compose 里的代码基本都是可以被混淆的
    ,所以开启混淆之后代码的压缩率也很高。
  • 手动操纵视图会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以出人意料的方式发生冲突时,也很容易造成异常状态。例如,某项更新可能会尝试设置刚刚从界面中移除的节点的值。一般来说,软件维护的复杂性会随着需要更新的视图数量而增长。

入门

Jetpack Compose 中的 match_parent 相当于什么?

Compose 编程思想  |  Jetpack Compose  |  Android Developers

Compose 布局基础知识  |  Jetpack Compose  |  Android Developers

原创:写给初学者的Jetpack Compose教程,基础控件和布局

原创:写给初学者的Jetpack Compose教程,Modifier

原创:写给初学者的Jetpack Compose教程,使用State让界面动起来

原创:写给初学者的Jetpack Compose教程,Lazy Layout

Composable

告诉编译器:此函数旨在将数据转换为界面。
所有的 Composable 函数还有一个约定俗成的习惯,就是函数的命名首字母需要大写。
@Preview 注解,这个注解表示这个函数是用来快速预览 UI 样式的。

@Composable 注解用于标记一个函数为可组合函数
。可组合函数是一种特殊的函数,不需要返回任何 UI 元素,因为
可组合函数描述的是所需的屏幕状态,而不是构造界面 widget
;而如果按我们以前的 XML 编程方式,必须在方法中返回 UI 元素才能使用它(如返回 View 类型)。

@Composable 注解的函数之间可以相互调用,因为这样 Compose 框架才能正确处理依赖关系。另外,@Composable 函数中也可以调用普通函数,而普通函数中却不能直接调用@Composable 函数。
这里可以类比下 kotlin 中 suspend 挂起函数的用法,其用法是相似的

布局

Compose 通过只测量一次子项来实现高性能。单遍测量对性能有利,使 Compose 能够高效地处理较深的界面树。
image.png
父节点会在其子节点之前进行测量,但会在其子节点的尺寸和放置位置确定之后再对自身进行调整

其他组件

CollapsingToolbarScaffold
stickyHeader
HorizontalPager
BottomNavigationBar
Scaffold
PullRefreshIndicator
TopAppBar

列表

列表和网格  |  Jetpack Compose  |  Android Developers

verticalScroll

我们可以使用
verticalScroll()
修饰符使
Column
可滚动

@Composable
fun MessageList(messages: List<Message>) {
    Column {
        messages.forEach { message ->
            MessageRow(message)
        }
    }
}

延迟列表

使用 Compose 的
LazyColumn

LazyRow
。这些可组合项只会呈现屏幕上显示的元素,因此,对于较长的列表,使用它们会非常高效。

import androidx.compose.foundation.lazy.items

@Composable
fun MessageList(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageRow(message)
        }
    }
}

还有一个名为 [
itemsIndexed ()
](
https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/package-summary?hl=zh-cn#
(androidx. compose. foundation. lazy. LazyListScope). itemsIndexed (kotlin. collections. List, kotlin. Function2, kotlin. Function2, kotlin. Function3)) 的
items ()
扩展函数的变体,用于提供索引

内容内边距

LazyColumn(
    contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
    // ...
}

如需在列表项之间添加间距,可以使用
Arrangement.spacedBy ()
。以下示例在每个列表项之间添加了
4.dp
的间距:

verticalArrangement = Arrangement.spacedBy(4.dp),

性能

早期 Lazy Layout 的性能很差,滚动的时候巨卡无比,确实很难让人用得下去。
但是在 Compose 1.5版本中,Google 做了大量的性能优化工作,所以如果你现在再来尝试一次,你会发现性能已经不是什么问题了。

修饰符

借助修饰符,您可以修饰或扩充可组合项。您可以使用修饰符来执行以下操作:

  • 更改可组合项的大小、布局、行为和外观
  • 添加信息,如无障碍标签
  • 处理用户输入
  • 添加高级互动,如使元素可点击、可滚动、可拖动或可缩放

修饰符是标准的 Kotlin 对象。您可以通过调用某个
Modifier
类函数来创建修饰符:

@Composable
fun ArtistCard(/*...*/) {
    val padding = 16.dp
    Column(
        Modifier
            .clickable(onClick = onClick)
            .padding(padding)
            .fillMaxWidth()
    ) {
        // rest of the implementation
    }
}
  • 修饰符顺序很重要
  • 提取和重复使用修饰符
  • clickable
    使可组合项响应用户输入,并显示涟漪。
  • padding
    在元素周围留出空间。
  • fillMaxWidth
    使可组合项填充其父项为它提供的最大宽度。
  • size()
    指定元素的首选宽度和高度。

偏移量

要相对于原始位置放置布局,请添加
offset
修饰符,并在
x
轴和
y
轴中设置偏移量。偏移量可以是正数,也可以是非正数。
padding

offset
之间的区别在于,向可组合项添加
offset
不会改变其测量结果:

@Composable
fun ArtistCard(artist: Artist) {
    Row(/*...*/) {
        Column {
            Text(artist.name)
            Text(
                text = artist.lastSeenOnline,
                modifier = Modifier.offset(x = 4.dp)
            )
        }
    }
}

offset
修饰符根据布局方向水平应用。在
从左到右
的上下文中,正
offset
会将元素向右移,而在
从右到左
的上下文中,它会将元素向左移。
image.png

requiredSize

请注意,如果指定的尺寸不符合来自布局父项的约束条件,则可能不会采用该尺寸。如果您希望可组合项的尺寸固定不变,而不考虑传入的约束条件,请使用
requiredSize
修饰符:

@Composable
fun ArtistCard(/*...*/) {
    Row(
        modifier = Modifier.size(width = 400.dp, height = 100.dp)
    ) {
        Image(
            /*...*/
            modifier = Modifier.requiredSize(150.dp)
        )
        Column { /*...*/ }
    }
}

在此示例中,即使父项的
height
设置为
100.dp

Image
的高度还是
150.dp
,因为
requiredSize
修饰符优先级较高。

滚动

在 View 中的话,通常可以在需要滚动的内容之外再嵌套一层 ScrollView 布局,这样 ScrollView 中的内容就可以滚动了。
而 Compose 则不需要再进行额外的布局嵌套,只需要借助 modifier 参数即可,代码如下所示:

@Composable  
fun SimpleWidgetColumn() {  
    Row(  
        modifier = Modifier  
            .fillMaxSize()  
            .horizontalScroll(rememberScrollState()),  
        verticalAlignment = Alignment.CenterVertically,  
    ) {  
        ...  
    }  
}

添加间距Spacer

Spacer(modifier = Modifier.width(8.dp))

Button

如何才能给 Button 指定文字内容呢?它可以和 Text 配合在一起使用。

Button(
    onClick = { /* ... */ },
    // Uses ButtonDefaults.ContentPadding by default
    contentPadding = PaddingValues(
        start = 20.dp,
        top = 12.dp,
        end = 20.dp,
        bottom = 12.dp
    )
) {
    // Inner content including an icon and a text label
    Icon(
        Icons.Filled.Favorite,
        contentDescription = "Favorite",
        modifier = Modifier.size(ButtonDefaults.IconSize)
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

Context

要想弹出 Toast 需要有 Context 参数才行。在 Composable 函数当中获取 Context 对象,可以调用 LocalContext. current 获得。

@Composable  
fun SimpleWidgetColumn() {  
    Column {  
        ...  
        val context = LocalContext.current  
        Button(onClick = {  
            Toast.makeText(context, "This is Toast", Toast.LENGTH_SHORT).show()  
        }) {  
            Text(  
                text = "This is Button",  
                color = Color.White,  
                fontSize = 26.sp  
            )  
        }  
    }  
}

文字图片

Compose 中的文字  |  Jetpack Compose  |  Android Developers

自定义图片  |  Jetpack Compose  |  Android Developers

val imageModifier = Modifier
    .size(150.dp)
    .border(BorderStroke(1.dp, Color.Black))
    .background(Color.Yellow)
Image(
    painter = painterResource(id = R.drawable.dog),
    contentDescription = stringResource(id = R.string.dog_content_description),
    contentScale = ContentScale.Fit,
    modifier = imageModifier
)

TextField

@Composable  
fun SimpleWidgetColumn() {  
    Column {  
        ...  
        TextField(  
            value = "",  
            onValueChange = {},  
            placeholder = {  
                Text(text = "Type something here")  
            },  
            colors = TextFieldDefaults.textFieldColors(  
                backgroundColor = Color.White  
            )  
        )  
    }  
}

重组

  • 为了跟踪这种状态变化,您必须使用
    remember

    mutableStateOf
    函数。
  • remember 和 mutableStateOf 在 Composable 函数中几乎永远都是配套使用的。
  • 使用 by 关键字替代了之前的等号,用委托的方式来为 count 变量赋值。count 的类型是 MutableState<Int>,而改用 by 关键字赋值之后,count 的类型就变成了 Int。既然都是 Int 了,那么我们就可以直接对这个值进行读写操作了,而不用像之前那样再调用它的 getValue ()和 setValue ()函数,是不是代码变得更简单了?
    注意导包
import androidx. compose. runtime. getValue
import androidx. compose. runtime. setValue
  • rememberSaveable 函数是 remember 函数的一个1增强版
    ,它唯一和 remember 不同的地方就是在于其包裹的数据在手机横竖屏旋转时会被保留下来。

实例

import androidx. compose. foundation. clickable
import androidx. compose. runtime. getValue
import androidx. compose. runtime. mutableStateOf
import androidx. compose. runtime. remember
import androidx. compose. runtime. setValue

class MainActivity : ComponentActivity () {
   override fun onCreate (savedInstanceState: Bundle?) {
       super.onCreate (savedInstanceState)
       setContent {
           ComposeTutorialTheme {
               Conversation (SampleData. conversationSample)
           }
       }
   }
}

@Composable
fun MessageCard (msg: Message) {
    Row (modifier = Modifier.padding (all = 8. dp)) {
        Image (
            painter = painterResource (R.drawable. profile_picture),
            contentDescription = null,
            modifier = Modifier
                .size (40. dp)
                .clip (CircleShape)
                .border (1.5. dp, MaterialTheme. colors. secondaryVariant, CircleShape)
        )
        Spacer (modifier = Modifier.width (8. dp))
		
        var isExpanded by remember { mutableStateOf (false) }

        Column (modifier = Modifier. clickable { isExpanded = !isExpanded }) {
            Text (
                text = msg. author,
                color = MaterialTheme. colors. secondaryVariant,
                style = MaterialTheme. typography. subtitle2
            )

            Spacer (modifier = Modifier.height (4. dp))

            Surface (
                shape = MaterialTheme. shapes. medium,
                elevation = 1. dp,
            ) {
                Text (
                    text = msg. body,
                    modifier = Modifier.padding (all = 4. dp),
                    maxLines = if (isExpanded) Int. MAX_VALUE else 1,
                    style = MaterialTheme. typography. body2
                )
            }
        }
    }
}

状态提升

以下是你应该考虑的状态提升最少应该到达哪个层级的关键因素:

  1. 如果有多个 Composable 函数需要读取同一个 State 对象,那么至少要将 State 提升到这些 Composable 函数共有的父级函数当中。

  2. 如果有多个 Composable 函数需要对同一个 State 对象进行写入,那么至少要将 State 提升到所有执行写入的 Composable 函数里调用层级最高的那一层。

  3. 如果某个事件的触发会导致两个或更多的 State 发生变更,那么这些 State 都应该提升到相同的层级。

viewmodel

首先我们要引入如下两个库,这是 Compose 为了适配 ViewModel 和 LiveData 而专门设计的库:

dependencies {
    implementation "androidx. lifecycle: lifecycle-viewmodel-compose: 2.6.2"
    implementation "androidx. compose. runtime: runtime-livedata: 1.5.1"
}

传统 LiveData 的用法在 Compose 中并不好使,因为传统 LiveData 依赖于监听某个值的变化,并对相应的界面进行更新,而 Compose 的界面更新则依赖于重组。
因此,我们需要将 LiveData 转换成 State 才行,observeAsState ()函数就是用来做这个事情的,参数中传入的0表示它的初始值。

import androidx. lifecycle. viewmodel. compose. viewModel

@Composable
fun CallCounter (modifier: Modifier = Modifier, viewModel: MainViewModel = viewModel ()) {
    val count by viewModel.count.observeAsState (0)
    val doubleCount by viewModel.doubleCount.observeAsState (0)
    Column {
        Counter (
            count = count,
            onIncrement = { viewModel.incrementCount () },
            modifier.fillMaxWidth ()
        )
        Counter (
            count = doubleCount,
            onIncrement = { viewModel.incrementDoubleCount () },
            modifier.fillMaxWidth ()
        )
    }
}

互相调用

Interoperability API  |  Jetpack Compose  |  Android Developers

Jetpack Compose和View的互操作性 - 圣骑士wind - 博客园

ComposeView

setContent (content: @Composable () -> Unit)
方法只有一个
content
参数,而这个参数是一个添加了
@Composable
注解的匿名函数,也就是说,在其中我们可以正常的使用 compose 了。

bind. jointGifPreviewRecyclerView. setContent {
	Test ()
}

Android View

@Composable
fun CustomView () {
    val state = remember { mutableStateOf (0) }
 
    //widget. Button
    AndroidView (
        factory = { ctx ->
            //Here you can construct your View
            android.widget.Button (ctx). apply {
                text = "My Button"
                layoutParams = LinearLayout.LayoutParams (MATCH_PARENT, WRAP_CONTENT)
                setOnClickListener {
                    state. value++
                }
            }
        },
        modifier = Modifier.padding (8. dp)
    )
    //widget. TextView
    AndroidView (factory = { ctx ->
        //Here you can construct your View
        TextView (ctx). apply {
            layoutParams = LinearLayout.LayoutParams (MATCH_PARENT, WRAP_CONTENT)
        }
    }, update = {
        it. text = "You have clicked the buttons: " + state.value.toString () + " times"
    })
}

这里的桥梁是
AndroidView
, 它是一个 composable 方法:

@Composable
fun <T : View> AndroidView (
    factory: (Context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
)

Compose 和 View 的结合, 主要是靠两个桥梁.
还挺有趣的:

  • ComposeView
    其实是个 Android View.
  • AndroidView
    其实是个 Composable 方法.

Compose 和 View 可以互相兼容的特点保证了项目可以逐步迁移, 并且也给够了安全感, 像极了当年 java 项目迁移 kotlin,至于什么学习曲线, 经验不足, 反正早晚都要学的, 整点新鲜的也挺好

项目学习



fmtjava/Compose_Eyepetizer: 一款基于 Jetpack Compose 实现的精美仿开眼视频App(提供Kotlin、Flutter、React Native、小程序版本