以一个卡片的实现做下讲解
简单讲就是需要开发用代码一步一步进行布局,这个过程需要开发全程参与。
UIView *cardView = [[UIView alloc] init];
cardView.backgroundColor = [UIColor whiteColor];
cardView.layer.cornerRadius = 16;
cardView.clipsToBounds = YES;
[self.view addSubview:cardView];
[cardView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(16);
make.right.mas_offset(-16);
make.height.mas_equalTo(116);
make.top.mas_equalTo(100);
}];
NSString *imgUrl = @"https://ke-image.ljcdn.com//110000-inspection//pc1_nBllrJgGj_1.jpg.280x210.jpg";
UIImageView *imgView = [[UIImageView alloc] init];
imgView.backgroundColor = [UIColor lightGrayColor];
[imgView sd_setImageWithURL:[NSURL URLWithString:imgUrl]];
[cardView addSubview:imgView];
[imgView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.mas_offset(0);
make.left.mas_equalTo(0);
make.width.mas_equalTo(107);
}];
UILabel *titleLbl = [[UILabel alloc] init];
titleLbl.font = [UIFont systemFontOfSize:14 weight:UIFontWeightBold];
titleLbl.textColor = [UIColor blackColor];
titleLbl.text = @"万柳书院新一区 南北向满五唯一";
[cardView addSubview:titleLbl];
[titleLbl mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(imgView.mas_right).mas_offset(12);
make.right.mas_offset(-12);
make.top.mas_equalTo(16);
}];
UILabel *subTitleLbl = [[UILabel alloc] init];
subTitleLbl.textColor = [UIColor blackColor];
subTitleLbl.font = [UIFont systemFontOfSize:12 weight:UIFontWeightRegular];
subTitleLbl.text = @"4室2厅/278.35㎡/南 北/万柳书院";
[cardView addSubview:subTitleLbl];
[subTitleLbl mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.right.mas_equalTo(titleLbl);
make.top.mas_equalTo(titleLbl.mas_bottom).mas_offset(8);
}];
UILabel *priceLbl = [[UILabel alloc] init];
priceLbl.font = [UIFont systemFontOfSize:14 weight:UIFontWeightBold];
priceLbl.textColor = [UIColor redColor];
priceLbl.text = @"4238万";
[cardView addSubview:priceLbl];
[priceLbl mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(titleLbl);
make.bottom.mas_offset(-16);
}];
UILabel *avgPriceLbl = [[UILabel alloc] init];
avgPriceLbl.textColor = [UIColor lightGrayColor];
avgPriceLbl.font = [UIFont systemFontOfSize:12 weight:UIFontWeightRegular];
avgPriceLbl.text = @"155,445元/平";
[cardView addSubview:avgPriceLbl];
[avgPriceLbl mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(priceLbl.mas_right).mas_offset(2);
make.right.mas_lessThanOrEqualTo(titleLbl.mas_right);
make.bottom.mas_equalTo(priceLbl);
}];
声明式则是由开发使用语言描述UI页面长什么样子,之后全权交给引擎去做
比如上面的卡片可以进行如下的拆分
<View
style={{
borderRadius: 8,
marginHorizontal: 16,
flexDirection: 'row',
backgroundColor: 'white',
overflow: 'hidden',
height: 116,
}}>
<Image
source={{
uri: 'https://ke-image.ljcdn.com//110000-inspection//pc1_nBllrJgGj_1.jpg.280x210.jpg',
}}
style={{width: 107, backgroundColor: '#eee'}}
/>
<View
style={{
marginVertical: 16,
marginHorizontal: 12,
flex: 1,
justifyContent: 'space-between',
}}>
<View>
<Text style={{fontSize: 14, color: '#222', fontWeight: '500'}}>
万柳书院新一区 南北向满五唯一
</Text>
<Text style={{fontSize: 11, color: '#222', marginTop: 8}}>
4室2厅/278.35㎡/南 北/万柳书院
</Text>
</View>
<View
style={{flexDirection: 'row', marginTop: 8, alignItems: 'flex-end'}}>
<Text
style={{
fontSize: 17,
color: '#E62222',
fontWeight: 'bold',
}}>
4238万
</Text>
<Text style={{fontSize: 11, color: '#999', marginLeft: 6}}>
155,445元/平
</Text>
</View>
</View>
</View>
HStack(spacing:0) {
AsyncImage(url: URL(string: "https://ke-image.ljcdn.com//110000-inspection//pc1_nBllrJgGj_1.jpg.280x210.jpg"))
.frame(width:107)
.aspectRatio(contentMode: .fill)
.clipped()
VStack(alignment: .leading,
spacing:0) {
VStack(alignment: .leading,
spacing:0) {
Text("万柳书院新一区 南北向满五唯一")
.lineLimit(1)
.font(.system(size: 14))
.foregroundColor(.black)
.fontWeight(.bold)
Text("4室2厅/278.35㎡/南 北/万柳书院")
.lineLimit(1)
.font(.system(size: 12))
.foregroundColor(.black)
.padding(.top, 8)
}
Spacer()
HStack(alignment: .bottom,
spacing:2) {
Text("4238万")
.font(.system(size: 14))
.foregroundColor(.red)
.fontWeight(.bold)
Text("155,445元/平")
.font(.system(size: 12))
.foregroundColor(.secondary)
.padding(.leading, 2)
}
}
.padding(.vertical, 16)
.padding(.horizontal, 12)
Spacer()
}
.frame(height: 116)
.background(.white)
.clipShape(RoundedRectangle(cornerRadius: 8))
.padding(.horizontal, 16)
}
Row() {
Row() {
Image("https://ke-image.ljcdn.com//110000-inspection//pc1_nBllrJgGj_1.jpg.280x210.jpg")
.width(107)
.height("100%")
.objectFit(ImageFit.Cover)
Column() {
Column() {
Text("柳书院新一区 南北向满五唯一")
.fontSize(16)
.fontColor("#222")
.maxLines(1)
Text("4室2厅/278.35㎡/南 北/万柳书院")
.fontSize(14)
.fontColor("#222")
.maxLines(1)
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
Row() {
Text("4238万")
.fontSize(15)
.fontColor("#E62222")
.fontWeight(FontWeight.Bold)
Text("155,445元/平")
.fontSize(13)
.fontColor("#222")
.margin({ left: 2 })
}
.justifyContent(FlexAlign.Start)
.alignItems(VerticalAlign.Bottom)
}
.width("100%")
.height("100%")
.padding({ top: 16, bottom: 16, left: 12, right: 12 })
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.SpaceBetween)
}
.borderRadius(8)
.margin({ left: 16, right: 16 })
.backgroundColor(Color.White)
.justifyContent(FlexAlign.Start)
.clip(true)
}
.height(116)
.width("100%")
Flutter中Widget的布局原理如下图所示。想了解更多Flutter的布局原理可以查看 深入理解 Flutter 布局约束
SwiftUI中的布局原理可以参考下图。想了解细节,可参考 SwiftUI 中布局的工作原理
声明式布局想要布局子视图都会经历由上到下的一个过程,只有知道了子视图的大小之后才能根据对齐方式将子视图放置在准确的位置。
声明式布局几乎都是下面这个套路
ArkUI官方链接
方舟开发框架(简称ArkUI)是鸿蒙开发的UI框架,提供如下两种开发范式,我们 只学声明式开发范式
我们使用ArkTS写完页面描述后,交给语言运行时进行语法解析,再之后由C++编写的后端引擎将UI转换为渲染指令交给渲染引擎绘制到屏幕上
ArkUI的渲染分为两大情况
① 通过devEco将源码编译成带类型标识的字节码文件,同时携带创建这个结构所需信息的指令流
② 通过跨语言调用生成C++层的Component树。这一步只是把ArkTS描述转变成了使用C++描述
③ 通过Component树生成Element树,Element是Component的实例,用于表示一个具体的组件节点。界面在运行时的树形结构就是通过Element树来维持的,同时自动更新的diff算法也是依赖Element树来减少复杂度的
④ 对于每个可显示的Element都会为其创建对应的RenderNode。RenderNode负责一个节点的显示信息,它形成的Render树维护着整个界面渲染需要用到的信息,包括位置、大小、绘制命令等。后续的布局、绘制都是在Render树上进行的
⑤ 实现真正的渲染并显示绘制结果
⑥ 点击事件传递到组件,组件的onClick事件方法被触发执行
⑦ 由于onClick事件方法中@State注解过的变量改变了,相应getter/setter函数会被触发
⑧ 状态管理模块定位出关联的UI组件
⑨ 状态管理模块更新相应的Element树的信息
⑩ 更新相应的UI组件的渲染信息
⑪ 界面显示,与⑤类似
上面我们说的布局原理,子视图上报给父视图自身大小的值是指 组件内容区的大小
如何选择使用哪种布局
主轴方向:垂直向下
Column() {
...
}.justifyContent(FlexAlign.Start)
主轴方向:水平向右
Row() {
...
}.justifyContent(FlexAlign.Start)
Column() {
...
}.alignItems(HorizontalAlign.Start)
Row() {
...
}.alignItems(VerticalAlign.Top)
Stack({ alignContent: Alignment.BottomStart })
Flex({ direction: FlexDirection.Row })
主轴为水平方向的Flex容器示意图
通过justifyContent参数设置在主轴方向的对齐方式,和Row、Column的主轴对齐方式行为一样
可以通过Flex组件的alignItems参数设置子组件在交叉轴的对齐方式,子组件默认使用Flex组件的对齐方式。但也可以通过alignSelf单独设置对齐方式
Flex({ alignItems: ItemAlign.Start })
ItemAlign.Auto:使用Flex容器中默认配置。
ItemAlign.Start:交叉轴方向首部对齐
ItemAlign.Center:交叉轴方向居中对齐
ItemAlign.End:交叉轴方向底部对齐
子组件通过 alignSelf 设置在父容器交叉轴的对齐格式,覆盖Flex布局容器中alignItems配置
alignRules
指定相对布局规则锚点的对齐位置示意图
一个示例
@Entry
@Component
struct Index {
build() {
Row() {
RelativeContainer() {
Row()
.width(100)
.height(100)
.backgroundColor('#FF3333')
.alignRules({
top: { anchor: '__container__', align: VerticalAlign.Top }, //以父容器为锚点,竖直方向顶头对齐
middle: { anchor: '__container__', align: HorizontalAlign.Center } //以父容器为锚点,水平方向居中对齐
})
.id('row1') //设置锚点为row1
Row() {
Image($r('app.media.icon'))
}
.height(100).width(100)
.alignRules({
top: { anchor: 'row1', align: VerticalAlign.Bottom }, //以row1组件为锚点,竖直方向低端对齐
left: { anchor: 'row1', align: HorizontalAlign.Start } //以row1组件为锚点,水平方向开头对齐
})
.id('row2') //设置锚点为row2
Row()
.width(100)
.height(100)
.backgroundColor('#FFCC00')
.alignRules({
top: { anchor: 'row2', align: VerticalAlign.Top }
})
.id('row3') //设置锚点为row3
Row()
.width(100)
.height(100)
.backgroundColor('#FF9966')
.alignRules({
top: { anchor: 'row2', align: VerticalAlign.Top },
left: { anchor: 'row2', align: HorizontalAlign.End },
})
.id('row4') //设置锚点为row4
Row()
.width(100)
.height(100)
.backgroundColor('#FF66FF')
.alignRules({
top: { anchor: 'row2', align: VerticalAlign.Bottom },
middle: { anchor: 'row2', align: HorizontalAlign.Center }
})
.id('row5') //设置锚点为row5
}
.width(300).height(300)
.border({ width: 2, color: '#6699FF' })
}
.height('100%').margin({ left: 30 })
}
}
List() {
ListItem() {
}
ListItem() {
}
ListItemGroup() {
}
LazyForEach(this.dataSource, item => {
ListItem() {
...
}
})
}
// 设置垂直方向
.listDirection(Axis.Vertical)
// 粘性header
.sticky(StickyStyle.Header)
// 两列
.lanes(2)
容器内每一个条目对应一个GridItem组件
Grid() {
...
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 2fr 1fr')
通过设置GridItem的rowStart、rowEnd、columnStart和columnEnd可以实现单个网格横跨多行或多列的场景
Grid() {
GridItem() {}
GridItem() {}
GridItem() {}
.columnStart(1)
.columnEnd(2)
GridItem() {}
.rowStart(1)
.rowEnd(2)
GridItem() {}
GridItem() {}
GridItem() {}
GridItem() {}
.columnStart(1)
.columnEnd(3)
}
.rowsTemplate('1fr 1fr 1fr')
.columnsTemplate('1fr 1fr 1fr 1fr')
.columnsGap(8)
.rowsGap(8)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。