ui组件也许是最令人着迷的部分。这部分最复杂的要数文字排版系统,比较重要的部分分别是属性集部件,信号槽部件,布局系统。相对复杂的部分是图片顶点数据拼装,和系统事件处理。
仓库:https://bitbucket.org/mm_longcheng/mm-vulkan
扣扣交流群:554329899
一、统一布局坐标系
为了可以适配动态变化的窗体大小,我参考了cegui的“The Unified Co-ordinate System”。它使用两个量来表达一个维度(offset, scale)。offset代表标量,scale代表给予父节点的附加量。当我们定位一个距离右边缘10单位的位置时,就可以使用(-10, 1)来表示。当父窗口的大小发生变动时可以自适应调整。
对于移动设备的屏幕适配也同样好用。
屏幕适配方案
类似unity的自动适配,我们会以设计宽高为基准设计ui布局,在实际设备上产生基于相对最小边的部分不动,执行等宽高缩放,长边会拓宽到实际像素。
举个例子:
Window ----------------------------+----------------------------------
Window pWindowName: org.mm.nwsi.vkApp
Window hDisplayDensity: 2.750000
Window hDisplayFrameSize: (1080.00, 1920.00)
Window hDesignDensity: 2.000000
Window hAbsoluteDensity: 1.000000
Window hDesignCanvasSize: (720.00, 1280.00)
Window hPhysicalViewSize: (392.73, 698.18)
Window hPhysicalSafeRect: (0.00, 0.00, 392.73, 698.18)
Window hActuallyViewSize: (720.00, 1280.00)
Window hActuallySafeRect: (0.00, 0.00, 720.00, 1280.00)
Window hTransformCanvas: 1.833333
Window hTransformWindow: 0.666667
Window hViewSizeAspectRatio: 0.562500
Window hSafeRectAspectRatio: 0.562500
Window ----------------------------+----------------------------------
hDisplayDensity 为px / pt 的比值,这个是从os层获取的。通常在视网膜屏上会大于1.
hDisplayFrameSize 这是全屏幕的像素px的宽高
hDesignDensity 设计的缩放比例,如目标屏幕pt宽高为360x640, 设计资源为720x1280,值为2
hAbsoluteDensity 这是自适应调整的最大阈值,通常是1
hDesignCanvasSize这是设计画布的宽高这里是720x1280
hPhysicalViewSize 这是实际屏幕的pt宽高
hPhysicalSafeRect 这是安全区的pt矩形位置
hActuallyViewSize 设计尺寸的ui逻辑尺寸,不完全与设计尺寸相等,部分设备高度会略大。
hActuallySafeRect 设计尺寸的安全区,在全面屏上会略微小于视图尺寸。
我们按720x1280为设计尺寸做布局,就可以在不同设备上保证相似的效果,部分设备下,逻辑高度会稍微高一点,通过统一坐标系统,我们就可以让相对大的屏幕呈现更多内容,自动适配。
安全区是为了ui不会被全面屏的缺角遮挡而设计的,我们的页面可以挂在在以安全区为基准的页面上就可以避免意外裁剪。安全区的数据我们通过nwsi系统,使用os原生接口获取。
二、视图对象
mmUIView是对标ios里UIView的的组件。它提供以下功能
1.拥有位置,缩放,选择参量
2.可以挂载子节点
3.拥有属性集,我们可以使用属性名字来设置视图的属性数值
4.信号槽,可以挂载事件处理函数,比如按钮的按下回调函数
5.纵横比模式,我们可以使用这个模式自适应调整基于父节点的宽高
6.视图裁剪,裁剪本视图携带的可绘制体和子视图,仅支持普通正交视图的裁剪
7.视图派生,和基于视图元数据的工厂注册。ui系统使用字符串构建具体控件能解耦代码。
如何派生一个视图
extern const struct mmMetaAllocator mmUIMetaAllocatorViewMaster;
extern const struct mmUIMetadata mmUIMetadataViewMaster;
struct mmUIViewMaster
{
struct mmUIView view;
/* UIView weak reference. */
struct mmUIView* MyImage;
struct mmUIView* MyText;
struct mmUIView* MyEditBox;
struct mmUIView* MyButton;
};
const struct mmMetaAllocator mmUIMetaAllocatorViewMaster =
{
"mmUIViewMaster",
sizeof(struct mmUIViewMaster),
&mmUIViewMaster_Produce,
&mmUIViewMaster_Recycle,
};
const struct mmUIMetadata mmUIMetadataViewMaster =
{
&mmUIMetaAllocatorViewMaster,
/*- Widget -*/
&mmUIWidgetOperableViewMaster,
NULL,
NULL,
/*- Responder -*/
NULL,
NULL,
NULL,
NULL,
};
void mmUIViewFactoryAddTypes(struct mmUIViewFactory* pViewFactory)
{
/* other view type */
mmUIViewFactory_AddType(pViewFactory, &mmUIViewMaster);
}
void mmUIViewFactoryRmvTypes(struct mmUIViewFactory* pViewFactory)
{
/* other view type */
mmUIViewFactory_RmvType(pViewFactory, &mmUIViewMaster);
}
第一个对象必须是view,是作为主类继承的约定。同时,我们定义了相应的metadata。我们会把metadata注册进视图工厂,通过视图加载器,我们就可以使用"mmUIViewMaster"名字来创建派生视图对象。
如何给派生视图配置属性
// 代码方式配置
mmUIView_SetValueCStr(pView, "View.Anchor", "(0.5f, 0.0f, 0.0f)");
mmUIView_SetValueCStr(pView, "View.Size", "((183.0f, 0.0f), (200.0f, 0.0f), (0.0f, 0.0f))");
mmUIView_SetValueCStr(pView, "View.Position", "((0.0f, 0.0f), (90.0f, 0.0f), (0.0f, 0.0f))");
mmUIView_SetValueCStr(pView, "Drawable.Alignment", "(1, 1, 1)");
mmUIView_SetValueCStr(pView, "Drawable.AspectMode", "3");
// 使用json配置文件
{
"Type": "mmUIViewImage",
"Property":
{
"View.Name": "MyImage",
"View.Anchor": "(0.5f, 0.0f, 0.0f)",
"View.Size": "((183.0f, 0.0f), (200.0f, 0.0f), (0.0f, 0.0f))",
"View.Position": "((0.0f, 0.0f), (90.0f, 0.0f), (0.0f, 0.0f))",
"View.Alignment": "(1, 0, 0)",
"View.Clipping": "1",
"Drawable.Alignment": "(1, 1, 1)",
"Drawable.AspectMode": "3",
"Image.SheetName": "imagesets/Image_2.sheet",
"Image.FrameName": "gz_zhuangshi.png"
}
}
由于我们有属性集系统,所以可以使用基类,通过属性名字来设置对应的对象参数。这种方法统一了属性设置接口,也便于做文件配置系统。
属性集的实现参看我的一篇文章组件:c语言版本事件集EventSet_mm_longcheng的专栏-CSDN博客
如何给控件挂接回调处理函数
void
mmUIViewMaster_OnPrepare(
struct mmUIViewMaster* p)
{
struct mmUIViewLoader* pViewLoader = p->view.context->loader;
// 使用配置文件构建视图
mmUIViewLoader_PrepareViewFromFile(
pViewLoader,
&p->view,
"view/mmUIViewMaster.view.json");
// 获取子控件的弱引用
p->MyImage = mmUIView_GetChildByCStr(&p->view, "MyImage");
p->MyText = mmUIView_GetChildByCStr(&p->view, "MyText");
p->MyEditBox = mmUIView_GetChildByCStr(&p->view, "MyEditBox");
p->MyButton = mmUIView_GetChildByCStr(&p->view, "MyButton");
// 将一个按钮的处理函数注册进按钮的信号槽
mmEventSet_SubscribeEvent(
&p->MyButton->events,
mmUIView_EventClicked,
&mmUIViewMaster_OnEventClicked,
p);
}
我们的信号槽可以按控件句柄好事件名字注册回调函数,注意需要在结束时解注册。
信号槽的实现可以参看我的一篇文章组件:c语言版本事件集EventSet_mm_longcheng的专栏-CSDN博客
三、ui可渲染对象
mmUIDrawable 保留了vulkan绘制需要的句柄,如管线和描述符。并且提供了对标unity(ImageType)的几种常用图片分解模式.
1.普通模式,就是直接将图片顶点数据分解为两个三角形
2.切片模式,这是九宫格切分方式,顶点数据按井字形切割
3.平铺模式,图片资源为单图非图集,可以采用平铺贴砖模式
4.画刷模式,对标unity的filled模式,在这个模式下,我们有三种填充方式
画刷模式的填充方式
1.线性方式,这个方式包含横向填充和纵向填充,对标Horizontal和Vertical
2.环形方式,对标unity的Radial360
顶点数据填充函数在mmUIQuad文件里面,想参考的可以看这个。
可渲染对象可配置的属性列表
mmPropertyRemove(propertys, "Drawable.Amount"); // 画刷进度[0, 1]
mmPropertyRemove(propertys, "Drawable.Opacity"); // 不透明度(r,g,b,a)
mmPropertyRemove(propertys, "Drawable.Color"); // 叠加颜色(r,g,b,a)
mmPropertyRemove(propertys, "Drawable.Flipped"); // 图片翻转(x,y)
mmPropertyRemove(propertys, "Drawable.AspectMode"); // 纵横缩放模式
mmPropertyRemove(propertys, "Drawable.Alignment"); // 对齐方式(x,y,z)
mmPropertyRemove(propertys, "Drawable.ImageMode"); // 图片分解模式
mmPropertyRemove(propertys, "Drawable.BrushMode"); // 画刷填充方式
mmPropertyRemove(propertys, "Drawable.Direction"); // 画刷起点朝向(上下左右)
mmPropertyRemove(propertys, "Drawable.Reverse"); // 画刷是否反向或逆时针
mmPropertyRemove(propertys, "Drawable.PipelineName");// 管线名字
mmPropertyRemove(propertys, "Drawable.PoolName"); // 描述符池
四、ui文字排版
文字排版的接口主要使用nwsi抽象的系统接口。这样有很多好处,首先我们无需管理大体积的字库资源,这在中文字库上尤其突出,并且不用关心字库缺字问题。但是可以使用的默认字体没办法调整,在不同系统上表现会略有不同。
mmTextLayout 是主要的部件。我们抽象的文字排版尽可能多的保留os层的可用参数,基本的字号,颜色,下划线,删除线,文字背景色等都是支持的,并且直接支持富文本。文字排版接口仅支持android,windows,ios和macos。
在静态文字的基础上,我们对接了os层的输入法事件,通过mmUIEditBox来管理当前游标位置。我们得到了一份可用的输入框控件。
文本具体参数可以看mmTextFormat,段落格式参数可以看mmTextParagraphFormat
文字组件可配置的属性列表
/*- MasterFormat -*/
mmPropertyRemove(propertys, "Text.MasterFontFamilyName"); // 字体名字
mmPropertyRemove(propertys, "Text.MasterFontSize"); // 字号
mmPropertyRemove(propertys, "Text.MasterFontStyle"); // 字体风格 黑体斜体
mmPropertyRemove(propertys, "Text.MasterFontWeight"); // 字重400是普通
mmPropertyRemove(propertys, "Text.MasterForegroundColor");// 背景颜色
mmPropertyRemove(propertys, "Text.MasterBackgroundColor");// 字体颜色
mmPropertyRemove(propertys, "Text.MasterStrokeWidth"); // 描边正数镂空负数附描边
mmPropertyRemove(propertys, "Text.MasterStrokeColor"); // 描边颜色
mmPropertyRemove(propertys, "Text.MasterUnderlineWidth"); // 下划线厚度
mmPropertyRemove(propertys, "Text.MasterUnderlineColor"); // 下划线颜色
mmPropertyRemove(propertys, "Text.MasterStrikeThruWidth");// 删除线厚度
mmPropertyRemove(propertys, "Text.MasterStrikeThruColor");// 删除线颜色
/*- ParagraphFormat -*/
mmPropertyRemove(propertys, "Text.TextAlignment"); // 水平对齐方式
mmPropertyRemove(propertys, "Text.ParagraphAlignment"); // 段落对齐方式
mmPropertyRemove(propertys, "Text.LineBreakMode"); // 断行模式
mmPropertyRemove(propertys, "Text.WritingDirection"); // 书写方向
mmPropertyRemove(propertys, "Text.FirstLineHeadIndent"); // 首行缩进
mmPropertyRemove(propertys, "Text.HeadIndent"); // 前置缩进
mmPropertyRemove(propertys, "Text.TailIndent"); // 后置缩进
mmPropertyRemove(propertys, "Text.LineSpacingAddition"); // 行距附加标量
mmPropertyRemove(propertys, "Text.LineSpacingMultiple"); // 行距附加乘量
mmPropertyRemove(propertys, "Text.ShadowOffset"); // 阴影偏移
mmPropertyRemove(propertys, "Text.ShadowBlur"); // 阴影模糊半径
mmPropertyRemove(propertys, "Text.ShadowColor"); // 阴影颜色
mmPropertyRemove(propertys, "Text.LineCap"); // 线封口模式
mmPropertyRemove(propertys, "Text.LineJoin"); // 线连接模式
mmPropertyRemove(propertys, "Text.LineMiterLimit"); // 折线阈值
mmPropertyRemove(propertys, "Text.WindowSize"); // 文本宽高
mmPropertyRemove(propertys, "Text.LayoutRect"); // 布局区域
mmPropertyRemove(propertys, "Text.DisplayDensity"); // 缩放比例
/*- EditBox -*/
mmPropertyRemove(propertys, "Text.CaretIndex"); // 当前游标
mmPropertyRemove(propertys, "Text.Selection"); // 当前选中位置
mmPropertyRemove(propertys, "Text.CaretWrapping"); // 右边是否处于换行位置
mmPropertyRemove(propertys, "Text.CaretStatus"); // 光标是否显示
mmPropertyRemove(propertys, "Text.SelectionStatus"); // 是否高亮显示选中文本
mmPropertyRemove(propertys, "Text.CaretWidth"); // 光标宽
mmPropertyRemove(propertys, "Text.CaretColor"); // 光标颜色
mmPropertyRemove(propertys, "Text.SelectionCaptureColor");// 高亮获取焦点颜色
mmPropertyRemove(propertys, "Text.SelectionReleaseColor");// 高亮失去焦点颜色
/*- Text -*/
mmPropertyRemove(propertys, "Text.String"); // 文本(utf16格式)
五、ui基础控件
实际上我的ui库基础控件只有四种
1.图片 mmUIViewImage 图片可以使用图集资源,也可以使用单图资源
2.文本 mmUIViewText 文本通常为静态文本,不可编辑
3.输入框 mmUIViewEditBox 输入框在文本的基础上对接了输入法事件处理,可以进行编辑
4.按钮 mmUIViewButton 按钮有五种展现状态,普通,失活,悬停,按下,按下拖拽
图片控件,拥有的属性
mmPropertyRemove(propertys, "Image.ImageName"); // 单图资源路径
mmPropertyRemove(propertys, "Image.SheetName"); // 图集名字
mmPropertyRemove(propertys, "Image.FrameName"); // 图集帧名字
mmPropertyRemove(propertys, "Image.Frame"); // 单图资源的包围盒
mmPropertyRemove(propertys, "Image.Rotated"); // 单图资源是否旋转
mmPropertyRemove(propertys, "Image.Filter"); // 采样器筛选参数
mmPropertyRemove(propertys, "Image.MipmapMode"); // 采样器mipmap模式
mmPropertyRemove(propertys, "Image.AddressMode");// 采样器寻址方式
mmPropertyRemove(propertys, "Image.BorderColor");// 采样器边缘颜色
页面层次管理
mmUIViewDirector 不是控件,它可以简单的管理页面关系,比如将mmUIViewRoot设置为根节点,并且在这个节点上挂载mmUIViewMaster作为第一个页面,可以使用名字构建:
mmUIViewDirector_SetRootWindowFromType(&p->vViewDirector, "mmUIViewRoot", "mmUIViewRoot");
mmUIViewDirector_SceneExclusiveForeground(&p->vViewDirector, "mmUIViewMaster", "mmUIViewMaster");
图片在视图中的对齐和缩放方式
图片有时候并不能将视图填满,那么我们需要相应的对齐和缩放方式。
对齐方式
1.mmUIAlignmentMinimum 按最小值为基准,对于x轴就是左对齐
2.mmUIAlignmentMiddled 中部对齐
3.mmUIAlignmentMaximum 按最大值为基准,对于x轴就是右对齐
缩放方式
1.mmUIAspectModeIgnore 忽略图片缩放
2.mmUIAspectModeShrink 缩小缩放,图片会尽量显示在视图中,并保持宽高比
3.mmUIAspectModeExpand 拓展缩放,图片会尽可能铺满视图,并保持快高比,会产生裁剪
4.mmUIAspectModeEntire 铺满缩放,图片宽高会直接拉伸到视图宽高,会产生变形
六、ui事件系统
os层会将鼠标,键盘,触点,输入法,帧更新等事件通过nwsi传过来。
mmUIViewContext 会得到首先得到事件,并使用HitTest得到第一响应链的处理者,之后事件会一次向父节点传递。不同的事件传递方式略有区别。
键盘事件
// Keypad content.
struct mmEventKeypad
{
/* modifier mask */
mmUInt32_t modifier_mask;
/* Key code. */
int key;
/* Text length. */
int length;
/* Text buffer. */
mmUInt16_t text[4];
/* Return value.
* 0 Need to handle keyboard event.
* 1 Event has been processed.
*/
mmUInt32_t handle;
};
鼠标事件
struct mmEventCursor
{
/* modifier mask */
mmUInt32_t modifier_mask;
/* button mask */
mmUInt32_t button_mask;
/* button id */
mmUInt32_t button_id;
/* Absolute position x */
double abs_x;
/* Absolute position y */
double abs_y;
/* Relative position x */
double rel_x;
/* Relative position y */
double rel_y;
/* wheel scrolling delta x */
double wheel_dx;
/* wheel scrolling delta y */
double wheel_dy;
/* The UTC time of this fix, in milliseconds since January 1, 1970.
* The time (in ms) when event trigger.
*/
mmUInt64_t timestamp;
/* Return value.
* 0 Need to handle keyboard pop up.
* 1 Event has been processed.
*/
mmUInt32_t handle;
};
触点事件
// Touch point.
struct mmEventTouch
{
// Motion event weak ref.
uintptr_t motion_id;
// The number of times the finger was tapped for this given touch.
int tap_count;
// The phase of the touch.TouchPhase touch began, moved, ended, or was canceled.
int phase;
// modifier mask
mmUInt32_t modifier_mask;
// button mask
mmUInt32_t button_mask;
// Absolute position x.
double abs_x;
// Absolute position y.
double abs_y;
// Relative position x.
double rel_x;
// Relative position y.
double rel_y;
// The force of the touch, where a value of 1.0 represents the
// force of an average touch (predetermined by the system, not user-specific).
double force_value;
// The maximum possible force for a touch.
double force_maximum;
// The major radius (in points) of the touch.
double major_radius;
// The minor radius (in points) of the touch.
double minor_radius;
// The area size value, (0, 1) an abstract metric.
double size_value;
// The UTC time of this fix, in milliseconds since January 1, 1970.
// The time (in ms) when event trigger.
mmUInt64_t timestamp;
};
// Touch point set.
struct mmEventTouchs
{
// The touch vector.
struct mmVectorValue context;
};
输入法事件
// Text content data.
struct mmEventEditText
{
// text utf16 character weak reference pointer.
mmUInt16_t* t;
// c string utf32 character size.
size_t size;
// c string utf32 character capacity.
size_t capacity;
// current cursor position.
size_t c;
// lift Selection position.
size_t l;
// right Selection position.
size_t r;
// mmEventTextType.
int v;
};
// String content data.
struct mmEventEditString
{
// string pointer.
mmUInt16_t* s;
// string lenght.
size_t l;
// Replace character offset. Replace API only.
size_t o;
// Replace character number. Replace API only.
size_t n;
};
软键盘事件
struct mmEventKeypadStatus
{
// The surface pointer.
void* surface;
// Layer:
// screen <- window trigger window attach to screen.
// extern extern screen brothers.
//
// Rect(x, y, w, h)
double screen_rect[4];
double safety_area[4];
double keypad_rect[4];
double window_rect[4];
// Animation duration for this event.
double animation_duration;
// enum mmKeypadAnimationCurve.
// Some time the value can be out of enum.
int animation_curve;
// Status type. mmSurfaceKeypadStatusType.
mmUInt32_t state;
// Return value.
// 0 Need to handle keyboard occlusion.
// 1 Event has been processed.
mmUInt32_t handle;
};
系统视图窗口变化事件
// Surface metrics.
struct mmEventMetrics
{
// The surface pointer.
void* surface;
// canvas size.
//
// Size(w, h)
double canvas_size[2];
// window size.
//
// Size(w, h)
double window_size[2];
// safety area.
//
// Rect(x, y, w, h)
double safety_area[4];
};
帧更新事件
// Surface frame update.
struct mmEventUpdate
{
// frame number.
mmUInt64_t number;
// average fps.
double average;
// index [0x00, 0x3F].
mmUInt8_t index;
// Time for frame interval, second(s).
double interval;
};