在接下来的几节课中,我将带你完成一个相当复杂的赢博体育程序的构建。GraphPad赢博体育程序是一个为平板电脑设计的赢博体育程序,它实现了图形绘制程序。
在这个例子中有很多内容,所以我把这个例子的讲义分成了两部分。在这第一节课中,我将带你完成一个初始原型赢博体育程序的构建,它只有最终赢博体育程序的一个子集的功能。
这两个赢博体育程序最重要的功能之一是使用Jetpack Compose Canvas()元素。这个元素使我们能够创建自定义组件,这些组件绘制自己的用户界面,并响赢博体育户事件(如点击和拖动)。这些示例中的图形绘制组件将是一个自定义的Canvas()组件。
Canvas元素的基本结构非常简单:
Canvas(){//绘图代码到这里}
像Jetpack Compose中的赢博体育组件一样,Canvas元素也有一个主体闭包,我们可以在其中放置代码。在Canvas组件的特定情况下,主体闭包将隐式访问DrawScope类型的对象。这个对象提供了一系列绘图方法,例如drawLine()和drawCircle(),我们可以使用这些方法在Canvas中绘制形状。
要使用这些绘图方法,我们还需要了解用于绘图的坐标系,以及一些相关的数据类型。DrawScopes使用的坐标系统的原点在绘图表面的左上角。正x轴向原点右侧,而正y轴从原点向下。位置、大小和距离度量都在Float数据类型中给出。Offset类用于描述绘图表面中的位置。偏移量有两个成员变量,x和y。
关于DrawScope类的更多信息可以在官方文档中找到。
由于我们的最终目标是构建一个允许用户创建自己的图形的组件,因此我们希望在画布中摆脱固定的绘图代码。相反,我们将构建一个赢博体育程序,用户可以在其中创建自己的形状对象,并让Canvas绘制用户想要的任何形状。
这样做的第一步是定义我们自己的形状对象集:
类顶点(var centerX:浮动,var centerX:浮动){val半径= 20.0f fun drawwin (scope: DrawScope){范围。drawCircle(color = color)红色,半径=半径,中心=偏移(x = centerX, y = centerY))} fun containsPoint(pt: Offset): Boolean {val deltaX = (centerX - pt.x) val delay = (centerY - pt.y) val r = 1.5*半径val distSquared = deltaX*deltaX + delay *deltaY返回distSquared < r*r}}类Edge(var origin: Vertex,var dest: Vertex) {fun drawIn(scope: DrawScope) {scope: DrawScope)。drawLine(color = color)绿色,start = Offset(x = origin)。centerX, y = origin.centerY), end = Offset(x = dest.centerX, y = dest.centerY), strokeWidth = 3.0f)}}
关于这些对象需要注意的最重要的事情是,它们包含绘图方法,这些方法将使对象能够在给定DrawScope的情况下绘制自己。
我们创建的形状将被存储在一个模型类中:
类GraphModel: ViewModel() {var vertices = mutableStateListOf<Vertex>() var edges = mutableStateListOf<Edge>() var拖动= mutableStateOf<Vertex?>(null) var start = mutableStateOf<Vertex?>(null) init {val x1 = 200.0f val x2 = 400.0f val v1 = Vertex(centerX = x1, centtery = x1) val v2 = Vertex(centerX = x2, centtery = x2) vertices.add(v1) vertices.add(v2) edges。add(Edge(origin = v1, dest = v2))})} fun makeVertex(center: Offset){顶点。添加顶点(centerX =中心)。x,centerY = center.y))} fun startDrag(pt:Offset) {for (v在顶点){if(v. containspoint (pt)){拖动。value = v}}}}的乐趣continueDrag(pt:Offset){如果(拖动)。Value != null) {val v = drag . Value !!v.c entrx = pt.x v.c entry = pt.y}} fun endDrag(pt:Offset){拖动。value = null} fun startDraw(pt:Offset) {for (v在顶点){if(v. containspoint (pt)){开始。value = v}}}有趣的endDraw(pt:Offset){如果(start. .Value = null) {val = start.value!!for (v in vertex) {if(v. containspoint (pt) && v != s) {val newEdge = Edge(origin = s,dest = v) edges.add(newEdge) break}}} start。值= null}}
注意这两个属性顶点和边缘:这些列表存储了我们将要绘制的对象。
用户将通过在Canvas组件中点击和拖动来创建这些对象。您在shape类和GraphModel类中看到的大部分代码都支持这些用户交互。在下一节中,我将详细解释画布中的交互式绘图是如何工作的。
用户将使用我们的Canvas组件来绘制带有顶点和边的图形。下面是我们的Canvas需要支持的一些用户交互:
为了支持赢博体育这些交互,我们将在Canvas上附加修饰符,使Canvas能够响应点击和拖动事件。下面是包含Canvas的组件的代码,其中包含了此事件处理代码。
@Composable fun CanvasDemo(graph: GraphModel,modifier: modifier = modifier) {val顶点= remember {graph。vertices} val drawingEdge = remember {mutableStateOf(false)} val startOffset = remember {mutableStateOf(offset . zero)} val curOffset = remember {mutableStateOf(offset . zero)} fun startDrag(offset: offset) {startOffset。value = offset。如果(drawingEdge.value) {graph.startDraw(offset)} else {graph.startDrag(offset)}} fun continueDrag(dragAmount: offset) {curOffset. value = offset。value = Offset(x = curOffset.value.x + dragAmount)。x,y = curOffset.value.y + dragAmount.y) if(!drawingEdge.value) {graph.continueDrag(curOffset.value)}} fun endDrag() {if(drawingEdge.value) {graph.endDraw(curOffset.value)} else {graph.endDrag(curOffset.value)} startOffset。value =偏移量。零curOffset。value =偏移量。Column(verticalArrangement =排列。居中,horizontalAlignment =对齐。centerhorizontal, modifier = modifier) {Spacer(modifier = modifier .height(20.dp)) Row(horizontalArrangement = Arrangement。开始,垂直对齐=对齐。{Text("Draw Edges:") spacing (modifier = modifier .width(10.dp)) Switch(checked = drawingEdge。值,onCheckedChange = {drawingEdge。Canvas(modifier = modifier . fillmaxwidth ().height(400.dp).padding(10.dp).border(BorderStroke(10.dp))p, Color.Blue)) . pointerinput (Unit){detectTapGestures(onPress = {startDrag(it)})} . pointerinput (Unit){detectDragGestures(onDrag = {_,amount->continueDrag(amount)}, onDragEnd = {endDrag()})} {for(graph.edges中的边){edge. drawin (this)} for(graph.vertices中的顶点){vertex. drawin (this)} if(startOffset。= curOffset。value && drawingEdge.value) {drawLine(color = color。黑色,start = startOffset。value, end = curOffset。按钮(onClick ={图;makeVertex(Offset(x = 600f,y = 600f))}) {Text("Make New Vertex")}}
拖动事件是一种特殊类型的指针输入事件,Jetpack Compose组件可以对其进行响应。我们通过给Canvas附加两个. pointerinput()修饰符来设置Canvas以响应手势。这些修饰符的主体应该包含手势识别器。由于手势识别器通常相当复杂,我在这里使用了定义一些辅助函数来帮助管理事件处理逻辑的方法。
我们在这里要响应的特定手势是DragGesture,它会响应拖动事件。以下是关于拖动手势如何工作的一些附加信息:
如果您看一下我在这里设置的各种pointerInput修饰符中的代码,您将看到我使用了一组辅助函数来帮助用户响应关键事件。随着事件的进展,这些辅助函数会记录重要的细节,并向模型类发送命令,告诉它移动东西。
响应拖动事件的逻辑有点复杂。一个问题是,在不同的时间,拖拽可能意味着不同的东西。在这个赢博体育中,Canvas可以处于两种模式:一种是用户可以拖动来移动顶点的模式,另一种是用户可以拖动来绘制边缘的模式。drawingEdge状态变量充当这两种模式之间的切换器。
在拖放事件中我们必须管理的另一件事是用户的视觉反馈。我们需要给用户一个视觉指示,表明他们的拖动请求实际上正在做一些事情。为了在拖动过程中提供这种反馈,我在组件中添加了两个状态变量,即startOffset和curOffset变量。这两个变量都使用Offset数据类型来描述绘图表面中的位置。当我们在Canvas主体中运行绘图代码时,我们将在那里添加代码,以便在这两个变量彼此不同时绘制额外的线。在绘制过程中,每当我们拖动绘制新边时,这条线都会提供视觉反馈。
要完全理解响应拖动事件所涉及的逻辑,您应该研究这里的辅助函数中的代码,以及这些辅助函数调用的模型方法中的逻辑。特别地,模型类维护了一些额外的成员变量,拖拽和启动,帮助它跟踪拖拽期间发生的事情。当我们处于顶点拖动模式时,拖动将存储对用户正在拖动的顶点的引用,而当我们处于边缘绘制模式时,start将记录绘图开始的位置。