在第二组关于图形绘制程序的课堂笔记中,我将完成绘图赢博体育程序。该赢博体育程序的最终版本将被设计为在Android平板电脑上运行,因此这也将使我们有机会了解一些最适合平板电脑的设计策略。
下面是完成后的赢博体育程序的样子:
该赢博体育程序将增加以下新功能:
我们的第一个改进是使在数据库中存储图形变得更容易。我们将为此目的建立一个Room数据库。
我从以前的版本中重写了顶点和边缘类,以便更容易将这些对象存储在数据库中。我还引入了一个新的GraphInfo类来表示图形。这将提供创建无限数量的图形并将其存储在数据库中的能力。
下面是图类新版本的代码。
const val半径= 20.0f @Entity(tableName = "vertices")类顶点(@PrimaryKey(autoGenerate=true) val id: Int = 0,var centerX: Float, var centerY: Float, var graph: Int) {fun drawIn(scope: DrawScope) {scope。drawCircle(color = color)红色,=半径,半径中心=抵消(x = centerX, y = centerY))}有趣containsPoint (pt:抵消):布尔{val△= (centerX - pt.x) valδy = (centerY - pt.y) val半径r = 1.5 * val distSquared△= *△+δy *δy返回distSquared < r *}} @ entity(表名=“边缘”)类EdgeInfo (@PrimaryKey (autoGenerate = true) val id: Int = 0, var产地:Int, var桌子:Int, var图:Int)类边缘(var产地:顶点,var桌子:顶点){乐趣港湾式停车站(范围:DrawScope){范围。drawLine(color = color)绿色,start = Offset(x = origin)。centerX, y = origin.centerY), end = Offset(x = dest.centerX, y = dest.centerY), strokeWidth = 3.0f)}} @Entity(tableName = "graphs")类GraphInfo(@PrimaryKey(autoGenerate=true) val id: Int = 0,var name: String)
这段代码的一个重要方面与Edge类有关。Edge类的问题是,边缘包含了对Edge连接的两个顶点对象的对象引用。不幸的是,Room数据库系统不允许我们在数据库中存储对象引用。为了解决这个问题,我引入了一个新的EdgeInfo类,它将一条边表示为一对顶点id。然后,我将添加一些代码,从从数据库中提取的EdgeInfo对象列表中生成Edge对象列表。
还要注意,Vertex类和EdgeInfo类都包含成员变量,这些成员变量存储了这些对象所属的图的id号。当我们从数据库中加载GraphInfo对象并决定使用该图时,我们可以执行查询来查找指向该特定图的赢博体育顶点和边。
设置Room数据库的下一步是设置数据库操作所需的接口。下面是该接口的代码。
@Dao接口GraphDao{@插入suspend fun addGraph(graph: GraphInfo) @插入suspend fun addVertex(顶点:顶点):长@插入suspend fun adddge (edge: EdgeInfo) @更新suspend fun updateVertex(顶点:顶点)@查询(“从图形中选择*”)suspend fun loadGraphs():列表<GraphInfo> @查询(“从图形=:图形的顶点中选择*”)suspend fun loadVertices(图形:Int):列表<顶点> @查询(“从id =:id的顶点中选择*”)suspend fun findVertex(id:Int):顶点@Query("select * from edges where graph =:graph") suspend fun loadEdges(graph:Int): List<EdgeInfo>} @Database(entities = [Vertex::class,EdgeInfo::class,GraphInfo::class], version = 1)抽象类GraphDatabase: RoomDatabase(){抽象fun graphDao(): graphDao}
注意loadVertices()和loadEdges()方法:一旦我们知道了要处理的图的id号,就可以很容易地找到顶点和边。
为了帮助将EdgeInfo对象转换为Edge对象,我还在这里提供了findVertex()方法,该方法将帮助查找具有特定id的顶点。
在这个版本的赢博体育程序中,我们仍然有一个GraphModel类。这一次的主要区别在于GraphModel将与Room数据库一起工作。
下面是新的GraphModel类的代码。
类GraphModel(private val dao: GraphDao): ViewModel() {var graphs = mutableStateOf<List<GraphInfo>>(listOf<GraphInfo>()) var curGraph = mutableIntStateOf(0) var vertices = mutableStateOf<List<Vertex>>(listOf<Vertex>()) var edges = mutableStateOf<List<Edge>>(listOf<Edge>()) var拖拽= mutableStateOf<Vertex?>(null) var start = mutableStateOf<Vertex?>(null) init {viewModelScope。launch {var graphList: List<GraphInfo> withContext(Dispatchers.IO) {graphList = dao.loadGraphs()} withContext(Dispatchers.Main) {GraphInfo . iovalue = graphList}}} fun setGraph(id: Int) {curGraph.intValue = id viewModelScope。launch {var vertexList: List<顶点> withContext(Dispatchers.IO) {vertexList = dao.loadVertices(id)} withContext(Dispatchers.Main){顶点。value = vertexList} var edgeInfoList: List<EdgeInfo> val edgeList = mutableListOf<Edge>() withContext(Dispatchers.IO) {edgeInfoList = dao.loadEdges(id) for(ei in edgeInfoList) {val o = dao.findVertex(ei.origin) val d = dao.findVertex(ei.dest) edgeList.add(Edge(origin=o,dest=d))}} withContext(Dispatchers.Main) {edges。value = edgeList.toList()}}} fun makeGraph(name: String) {val gi = GraphInfo(name = name) viewModelScope。launch {var graphList: List<GraphInfo> withContext(Dispatchers.IO) {dao.addGraph(gi) graphList = dao.loadGraphs()} withContext(Dispatchers.Main) {graphs. iovalue = graphList}}} fun makeVertex(center: Offset) {var v = Vertex(centerX = center。x,centerY =中心。y,图= curGraph.intValue) var id:长viewModelScope。launch {withContext(Dispatchers.IO) {id = dao.addVertex(v) v = dao.findVertex(id. toint ())} withContext(Dispatchers.Main){顶点。value += v}}}} fun startDrag(pt:Offset) {for (v in vertices.value) {if(v. containspoint (pt)){拖动。value = v}}}}的乐趣continueDrag(pt:Offset){如果(拖动)。Value != null) {val v = drag . Value !!v.centerX = pt.x v.centerX = pt.y}} fun endDrag(pt:Offset) {if(Value != null) {val v = drag . Value !!viewModelScope。launch {withContext(Dispatchers.IO) {dao.updateVertex(v)}}拖拽。value = null}} fun startDraw(pt:Offset) {for (v in vertices.value) {if(v. containspoint (pt)){开始。value = v}}}有趣的endDraw(pt:Offset){如果(start. .Value = null) {val = start.value!!for (v in vertices.value) {if(v. containspoint (pt) && v != s) {val newEdge = Edge(origin = s,dest = v) Edge。value += newEdge val ei = EdgeInfo(origin = s.id,dest=v.id,graph=curGraph.intValue) viewModelScope。launch {withContext(Dispatchers.IO) {dao. adddge (ei)}} break}}}值= null}}
这里有一些需要注意的重要变化。
init ()
方法将在赢博体育程序启动时加载GraphInfo对象列表。setGraph ()
方法为我们解决了这个问题。setGraph ()
是为正在讨论的图形加载EdgeInfo对象,然后将每个EdgeInfo对象转换为Edge对象的代码。makeGraph ()
和makeVertex ()
方法处理生成新图形和新顶点。endDraw ()
方法。要创建一条新边,我们创建一个edge对象并将其添加到Edges列表中,然后创建一个EdgeInfo对象,并将其放入数据库。为了充分利用平板电脑平台的优势,我们将开发一个适合该平台的新顶级组件。我们赢博体育中的顶层组件将是一个navigabelistdetailpanescaffold。以下是该组件的主要特性:
下面是顶层视图的代码:
@Composable @OptIn(ExperimentalMaterial3AdaptiveApi::class) fun CanvasApp(gm: GraphModel) {val navigator = rememberListDetailPaneScaffoldNavigator<Any>() NavigableListDetailPaneScaffold(navigator = navigator, listPane = {GraphList(gm, onClick = {gi -> navigator.)navigateTo (ListDetailPaneScaffoldRole。Detail, content = gi)})}, detailPane = {GraphDetailPane(gm,navigator)})}
列表视图和详细视图的角色由GraphList和GraphDetailPane组件处理。下面是这两个的代码:
@Composable fun GraphList(gm: GraphModel,onClick: (GraphInfo)->Unit) {val showDialog = remember {mutableStateOf(false)} Column(verticalArrangement = Arrangement.)中心,modifier = modifier .padding(20.dp)) {gm. graphics .value. foreach {gi -> Text(gi.name, modifier = modifier .padding(10.dp))。clickable(onClick = {onClick(gi)}))} Button(onClick = {showDialog。value = true}) {Text("Make New Graph")}} if(showDialog.value) {NameDialog(onConfirm = {gm.makeGraph(it) showDialog.value = true}) {value = false}, on解散= {showDialog。@Composable @OptIn(ExperimentalMaterial3AdaptiveApi::class) private fun GraphDetailPane(gm: GraphModel,navigator: ThreePaneScaffoldNavigator<Any>) {val graph: GraphInfo?= navigator.currentDestination ?。内容为GraphInfo?if(graph != null) {gm. setgraph (graph.id) CanvasDemo(gm)} else {Text(“做出选择”)}}
这里有一些需要注意的重要事项:
navigabelistdetailpanescaffold的另一个有用的特性是它的适应性。你可以在模拟器中旋转平板电脑,甚至在手机上而不是平板电脑上运行这个新赢博体育程序,看看这个组件对不同配置的反应。在空间有限的情况下,列表窗格将滑出屏幕边缘,为详细信息窗格腾出空间。当用户需要访问列表视图时,可以从边缘向后滑动,将列表视图拉回视图,就像抽屉一样。