整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

谷歌发布安卓折叠屏手机应用设计规范

ndroid 的覆盖范围在递增,体验也在变得越来越好,现已有超过 2.5 亿台大屏设备搭载了 Android 系统,包括平板电脑、可折叠设备以及 Chrome OS 设备。如何适配不同的屏幕尺寸并保障良好的体验,一直以来都是开发者的一大难题。尤其随着可折叠设备等新兴产品的涌现,适配工作也愈发迫切。

谷歌官方发文,将重点介绍 Material Design 指南中更新的相关内容,并提供一些建议来帮助开发者按照自适应界面的原则来构建应用,从而解决在平板电脑和可折叠设备上的适配问题。

如果您更喜欢通过视频了解本文内容,请点击下方:

△ 折叠屏上应用设计规范

Compose

https://developer.android.google.cn/jetpack/compose/nav-adaptive

设计指南

2021 年年初,我们在 Material Design 网站上发布了针对大屏设备的指南文档。Android 开发者峰会期间我们更新了一些内容,以帮助开发者为可折叠设备等更多其他类型的设备做好准备。

https://m3.material.io/foundations/adaptive-design/overview

深入理解布局

深入理解布局指南介绍了布局容器的相关概念,它提供了一个整体框架,可帮助开发者思考如何在屏幕上排列导航栏、工具栏和内容等界面元素。

https://material.io/design/layout/understanding-layout.html#principles

△ 布局的三个主要区域

指南中的组合部分带您了解如何充分利用屏幕空间以保障可读性,并且以尊重用户心智模型的方式在不同的场景下合理排布重要内容和操作选项。包括适当缩放以展示更多内容,如示例中的副标题和日期,以及较小的组合技术,例如在紧凑型的布局中对内容进行视觉分组并保持其相关性等。

https://material.io/design/layout/understanding-layout.html#composition

△ 组合指南中涉及的部分布局方式

以 Fortnightly 示例应用为例,它在平板电脑上的界面布局十分均衡,这得益于它遵从了指南里对容器的建议。而且可以看到,Fortnightly 使用了视觉分隔线 (Visual Divider) 用于分隔最新新闻,在屏幕的另一边,则利用留白和排版对不同类别的新闻报道进行分组。

△ Fortnightly 遵循指南对内容进行分隔和分组

网格系统

现在,许多应用将屏幕视作一个大画布或单栏,以水平和垂直的方式按相互关系绘制元素,有些应用也会在一侧整体留出边距。这一做法在小屏上或许行得通,当屏幕尺寸较大时就会出现明显的问题。网格系统则将您的布局划分为一系列栏,从而帮助您在规范网格中设计更具表现力的布局。在布局中使用栏式网格 (如下图),能够让大屏设备的体验呈现更贴心,更组织有序的印象,使得设备和内容更自然地融为一体。

△ 栏式网格

您可以通过这些栏将屏幕划分为不同区域,用于容纳相关的信息和操作,进而改善信息层次结构。如下图所示,这里分了三个区域,这些区域将按照设计者期望用户阅读的顺序,把用户的注意力吸引到这些区域对应在屏幕的主要信息片段或信息组上。最重要的一点是,栏式网格提供了一种合理的方式来思考当屏幕尺寸变大或变小时如何将内容进行重排,从而帮助您对不同的屏幕尺寸作出一致响应。

△ 使用栏式网格将屏幕划分为三个主要区域

在本例中,三个主要区域通过重排来保持相同的信息层次结构,但以更加人性化的方式在小屏幕上显示。

△ 使用栏式网格在不同屏幕尺寸中对内容进行重排

记住网格系统有助于您选择组件行为,在不同的布局中,以对设备尺寸和场景最有意义的方式决定替换还是更改组件。例如,在大屏设备上,您可使用 Navigation rail (左侧边栏导航条) 代替底部导航 (Bottom navigation),两者功能相同,视觉表现方式也类似,但 Navigation rail 能够更加人性化地排布页面。手机上的全屏对话框 (Full-screen dialog) 在大屏幕上可以采用简单对话框 (Simple dialog) 替代,以保持用户当前操作的上下文。

△ 在大屏上使用简单对话框 (右) 代替全屏对话框 (左)

Navigation rail

https://m3.material.io/components/navigation-rail/overview

底部导航 (Bottom navigation)

https://m3.material.io/components/bottom-navigation

尺寸类别

请记住,替换组件时,首先要满足用户的功能性和人性化需求。找到调整界面的正确阈值,这是实现响应式界面的重要步骤。因此我们定义了新断点值,这有助于将设备划分到预设的尺寸类别中,这些尺寸代表了市场上实际设备的尺寸。它们有助于将应用版面的原始尺寸转换为离散的标准化组,您可以据此做出更高层次的界面决策。例如,几乎所有标准手机在竖屏模式下都采用了较小 (Compact) 宽度和中等 (Medium) 高度的组合,由于普遍使用垂直滚动,对大多数应用而言,根据宽度的尺寸类别进行适配就已足够。

△ 基于宽度的尺寸类别

△ 基于高度的尺寸类

这些尺寸类将作为新的 API 出现在 1.1 版 Jetpack Window Manager 库中。从 Android Studio Bumblebee 开始,我们还以参考设备 (Reference devices) 的形式,将尺寸类别整合到工具中,在此基础上实现界面有利于保持一致性,操作也更加简单。而且开发者不需要去检查实际物理尺寸或屏幕方向,或其他容易出错的标识。您在设计和构建不同的尺寸类别时,请想想人们会如何手持和触摸这些类别所代表的设备。关注设备的形状和尺寸,有助于您打造出更加人性化的体验。例如,在平板电脑或大屏手机上,如果不完全调整握持姿势,人们可能很难触及屏幕的顶部区域,因此请将重要操作和内容放在容易触及的区域中。

尺寸类

https://developer.android.google.cn/guide/topics/large-screens/support-different-screen-sizes#window_size_classes

Window Manager

https://developer.android.google.cn/jetpack/androidx/releases/window

Android Studio Bumblebee

https://developer.android.google.cn/studio

规范布局

规范布局提供了一系列通用布局方案,对设计大屏幕应用非常有帮助。第一种是列表 / 详情,或列表网格视图的简单组合,同时在开始展示内容的屏幕起始侧,设置 / 不设置导航容器。

△ 列表 / 详情布局

支持面板可用于人们需要集中精力的体验中,例如文档。在屏幕尾侧或底部添加一块面板,以便于使用工具或上下文控件。

△ 支持面板

信息流是新闻或社交类应用中的常见模式,模板采用图块 (Tile) 的形式来吸引用户发现更多内容。这种交互与移动手机一样 —— 打开一项即表示打开一个新页面,但这种体验更具沉浸感,而且专为大屏幕尺寸而设计。

△ 信息流

主页横幅优先将内容排列在屏幕顶部,并在内容周围和下方设计了支持元素,这对以媒体为中心的应用来说,是非常棒的体验。

△ 主页横幅

规范布局实践

采用响应式界面不仅仅是为不同屏幕尺寸提供并行结构,应用还要足够灵活,这样才能根据各种需要调整尺寸,例如旋转设备、多窗口模式以及折叠和非折叠姿态。因此在运行期间,应用可从一个尺寸类别过渡到另一个尺寸类别,并再次过渡回去。重要的是,不要将尺寸类别视作完全独立的桶,应用也需保证连续性 (即不中断用户体验),所以应用状态或数据不能丢失。

△ 响应式界面可根据屏幕尺寸变化而调整内容布局

设想一下,当您调整浏览器窗口大小时,如果浏览器回退了一个页面,或者重定向到另一个页面,又或者修改了历史记录,这种体验非常奇怪。因此,每个页面都应足够灵活,而且应当能够在尺寸过渡期间保持状态不变,这个时候规范布局就能发挥重要作用。针对每个页面,您可以思考一下,当屏幕尺寸变大时,可以添加什么内容。当屏幕尺寸变小时,可以删除哪些内容。然后再选择合适的策略。这可能意味着您需要重新审视导航图,尤其是当您目前的设计以手机为主时更应如此。

如需构建响应式界面,我们应该优先考虑界面中长驻元素的位置,例如导航元素。遵循 Material 指南,我们可以根据宽度的尺寸类别提供替代布局,将导航调整到最方便使用的位置。例如,小屏幕采用底部导航视图,中等屏幕采用 Navigation rail,大屏幕采用完整导航视图。请大家注意,这些布局采用的是宽度限定符 "-w",而非最小宽度限定符 "-sw"。剩余空间用于排列内容,我们可以在这些空间应用规范布局。

列表 / 详情

对列表 / 详情而言,AndroidX 中有个名为 SlidingPaneLayout 的专用控件,使用前需为它的两个子元素指定 layout_width,在运行期间,SlidingPaneLayout 会判断是否有足够空间同时展示两个窗格:

<SlidingPaneLayout …>
      <FragmentCOntainerView
              android : id=”@+id/list_pane”
              android : layout_width=”300dp”
              android : layout_weight=”1”
              …  />

      <FragmentCOntainerView
              android : id=”@+id/detail_pane”
              android : layout_width=”360dp”
              android : layout_weight=”2”

<SlidingPaneLayout …>

△ SlidingPaneLayout 布局示例

当屏幕空间足够,则两个窗格至少都要达到指定的宽度,剩余空间可通过 layout_weight 分配,如左图所示;如果空间不足,如右图所示,则每个窗格都使用父视图的全宽,详情窗格将被滑到一边,或直接覆盖第一个窗格。

△ SlidingPaneLayout 中空间分配结果

viewModel.selectedItemFlow.collect { item ->
// 更新详情窗格的内容
detailPane.showItem(item)
// 将详细信息窗格滑动到视图中
// 如果并排放置两个窗格
// 并不会产生实际效果
slidingPaneLayout.openPane()
}

如上代码所示,您可以通过代码控制滑动窗格,当用户从列表中选择一个项目,我们从 ViewModel 的 Kotlin 流中接收到该项目,然后更新详情窗格的内容,并通过调用 openPane 将其滑入视图。在 Trackr 应用中效果如下图所示:

关于如何使用 SlidingPaneLayout 实现双窗格布局的相关内容,请参阅 Android 开发者网站: 创建双窗格布局,该页面还介绍了其他内容,例如集成系统返回按钮以实现侧滑回退窗格等。

Trackr 应用

https://github.com/android/trackr

创建双窗格布局

https://developer.android.google.cn/guide/topics/ui/layout/twopane

信息流

我们可以通过信息流沉浸式地展示一个数据集,因此 RecyclerView 是非常适合的选择,我们可以通过改变 RecyclerView 使用的 LayoutManager 来改变其展现形式。LinearLayoutManager 适合用于较小型宽度,但在中等宽度和展开型宽度场景下,页面内容则会出现过度拉伸和变形的情况,这时改用 GridLayoutManager,或 StaggeredGridLayoutManager 甚至 FlexBoxLayoutManager,可能会更合适。

△ 通过更换 RecyclerView 的 LayoutManager 来改变其展现形式

主页横幅

我们还可以改变单项布局,使某些项比其他项更高或更宽,以此凸显其重要性,打造更有趣的视觉效果。在主页横幅布局中,我们强调某个特定元素,重新排布它周围的其他支持元素。当然我们有很多方法可以实现这一点,但 ConstraintLayout 的灵活性最大,因为它提供了很多种方式来约束子元素的尺寸,以及相对于其他子元素的位置。在如下媒体类示例应用,它的首图限制在 16:9 的宽高比内,描述窗格占 60% 宽度,剩余空间留给其他元素。约束条件可以改变甚至还可以用 MotionLayout 设置动画,它是一个特殊的 ConstraintLayout。

△ 主页横幅示例

对于支持面板而言,从 LinearLayout 到 ConstraintLayout 的任何布局控件,都可以当作容器来定位面板。如下图所示,我们考虑一件事,当过渡到小屏幕尺寸时,面板上的内容应该放在哪里。我们有许多可选方案,比如使用屏幕尾侧的侧边抽屉式导航栏,或者使用上滑式底部动作条,或者使用选项菜单,甚至可以将内容完全隐藏起来。

适配可折叠设备

可折叠设备不仅配备了更大的屏幕,它们还可以根据设备的折叠方式和用户的使用方式调整设备的方向 / 姿势。

目前有三种常见的设备形态: 折叠、未折叠和桌面模式 (悬停)。另外,我们稍后也将看到其他理论上存在的状态,例如书本模式。

△ 折叠设备的三种常见姿态

与其他大屏幕设备一样,我们需要多想想用户会怎样握持未折叠设备?如平板电脑,部分屏幕区域难以用大拇指触及,用户也很难腾出整只手来自由操控屏幕。用户轻易就能触及屏幕的底部角落,但可能无法触及屏幕最顶端,尤其是在竖屏模式下。这意味着如果您使用 Navigation rail 这类组件,将导航按钮居中或固定在屏幕底部,这会更便于用户的操作。

△ 大屏设备中的用户操作热区

同时,我们还需要考虑铰链位置对交互的影响。铰链会带来明显的触觉差异,甚至两个屏幕会存在物理分离。因此,请您避免将按钮和其他重要操作项直接放在铰链区域。大多数设备上的铰链区域宽度约为 48 dp,在桌面模式下也请避免将界面元素放在铰链区域,因为在这种设备模式下,用户几乎无法使用该区域的任何功能。

△ 铰链区域

当设备从折叠模式转换到非折叠模式时,有两种主要的技术方案可用于设计布局。第一种是扩大屏幕,该方案采用了一种简单的响应式布局,在该布局下应用会扩展内容并填充到屏幕上。通常情况下,我们会根据前面提到的 Material 指南来扩展栏式网格:

https://m3.material.io/foundations/adaptive-design/foldables/compositions

第二种是增加另一个页面,根据您构建的应用不同,可以采用与列表 / 详情或者以另一个面板补充主面板功能相同的方案。

△ 情境 1: 扩大屏幕 (图左) 情境 2: 增加页面 (图右)

在这两种情况下,根据 material.io 的指南,您需要创建一个平均分布在铰链区域两侧的八栏网格,当添加 Navigation rail 等导航容器时,屏幕起始侧会被压缩以容纳导航容器。

△ 平均分布在铰链两侧的八栏网格 (蓝背景)

适配示例

现在我们来看如何在运行期间利用好折叠状态。Jetpack Window Manager 库提供了相应的 API,可以检测应用窗口是否存在折叠。任何 Activity 都可以获得一个 WindowInfoRepository 实例。然后,在 Started 和 Stopped 这两种生命周期状态之间,我们可以安全地从窗口布局信息流中收集信息。每当流发射一个值时,我们都可以检查 displayFeature,然后有针对性地寻找 FoldingFeature。

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val windowInfoRepo = windowInfoRepository()

        // 在 STARTED 和 STOPPED 这两种生命周期状态之间安全地从 windowInfoRepo 中收集数据
        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                windowInfoRepo.windowLayoutInfo.collect { info ->
                    for (feature in info.displayFeatures) {
                        val fold = feature as? FoldingFeature ?: continue
                        // 使用 FoldingFeature
                    }
                }
            }
        }
    }

△ 识别折叠姿态

掌握了折叠姿态的相关信息后,我们可以通过一些方法来查看设备是否处于前面提及的某种姿态。在书本模式下,设备的状态为 HALF_OPENED,且其方向为 VERTICAL;在桌面模式下的状态为 HALF_OPENED,且其方向为 HORIZONTAL。

// 书本模式是半打开的垂直折叠模式
fun FoldingFeature.isBookMode() =
    state == FoldingFeature.State.HALF_OPENED &&
        orientation == FoldingFeature.Orientation.VERTICAL

// 桌面模式是半打开的水平折叠模式
fun FoldingFeature.isTableTopMode() =
    state == FoldingFeature.State.HALF_OPENED &&
        orientation == FoldingFeature.Orientation.HORIZONTAL

△ 书本模式于桌面模式的判定条件

FoldingFeature 中还包含窗口中的折叠位置,当折叠导致内容视图被割裂时,我们应该及时更新布局参数。您可以做些调整,比如将支持面板置于一侧,或者在折叠的上半部分展示主页横幅。首先,我们需要知道内容视图在窗口中的位置,通过 getLocationInWindow 可以获取位置信息。我们将使用这些坐标以及宽度和高度创建一个 Rect 对象,这样我们便得到了窗口坐标空间中的视图边界。

FoldingFeature 给出了在窗口的坐标空间中的折叠边界,因此我们可以直接检查这两个区域是否相交,如果相交,我们可以将 featureRect 的边界转换为视图的坐标空间并将其返回。顺便说一下,如果您使用 SlidingPaneLayout 来实现列表 / 详情布局,您会自动获得对书本模式的支持。只要两个窗格都能容纳进去,SlidingPaneLayout 会将窗格置于折叠姿态的另一侧。

fun getFoldBoundsInView(
       foldingFeature: FoldingFeature,
       view: View
): Rect? {
       // 获取视图在窗口坐标空间中的边界
       val viewLocation = IntArray(2)
       view.getLocationInWindow(viewLocation)

       val (viewX, viewY) = viewLocation
       val viewRect = Rect(
           left = viewX, top = viewY
           right = viewX + view.width, bottom = view + view.height
       )
    …

       //显示功能的边界已经在窗口的坐标空间中
       // 检查 view 的边界和显示功能的边界是否相交
       val featureRect = Rect(foldingFeature.bounds)
       val intersects = featureRect. intersect (viewRect)

       if (featureRect.isEmpty || ! intersects)
           return null
       }

       // 将功能的边界坐标转换为 view 的坐标空间
       featureRect.offset(-viewX, -viewY)
       return featureRect
}

△ 获取折叠的位置信息

测试

如果您的应用存在与折叠状态相关的特殊行为,您需要为此编写单元测试。Jetpack Window Manager 里面有一条测试规则,支持在插桩测试期间模拟 FoldingFeature。由于测试需用到视图,我们添加了 WindowLayoutInfoPublisherRule,以及 ActivityScenarioRule,两者一起组成了一个测试规则链。在该测试方法中,我们通过 activityRule 获取 Activity,然后创建窗口特性来模拟桌面模式,构建 WindowLayoutInfo 对象并使用 publisherRule 发布该对象。之后,我们可以使用 Espresso 和 JUnit 断言来检查 Activity 在桌面模式下能否正常运行。

private val publisherRule = WindowLayoutInfoPublisherRule()
private val activityRule = ActivityScenarioRule (MyActivity: :class.java)

@get :Rule
val testRule = RuleChain.outerRule (publisherRule) .around(activityRule)

@Test
fun testDeviceOpen_TableTop(): Unit = testScope.runBlockingTest {
    activityRule.scenario.onActivity { activity ->
        val feature = FoldingFeature (activity, HALF_OPENED, HORIZONTAL)
        val testWindowInfo = WindowLayoutInfo.Builder( )
                .setDisplayFeatures (listOf (feature))
                .build()

        publisherRule.overrideWindowLayoutInfo(testWindowInfo)
    }
        // 编写基于桌面模式的断言
}

△ 测试折叠状态

界面测试存在一定难度,因为有些测试须在特定设备上进行。为此,Android Studio 正在增加对 Gradle 托管的虚拟设备的支持。您可以使用 7.1 及以上版本的 Android Gradle 插件来体验该功能。

在应用级的 build.gradle 文件中的 testOptions 模块下,指定虚拟设备配置文件,就像您平时在 Android Studio 管理和运行虚拟设备那样。例如,这里使用的是 Pixel C 平板电脑镜像,接下来 Gradle 会创建能够在指定设备上执行测试的目标,甚至还能根据需要下载设备镜像。

android {
    testoptions {
        devices {
            pixelCapi30 (ManagedVirtualDevice) {
                device = "Pixel C" // 平板电脑设备
                apilevel = 30
                systemImageSource = "aosp" // 如需 GooglePlay 服务,使用“google”
                abi = "x86”
            }
        }
    }
}

#Gradle target = {device name} + {build variant} + "AndroidTest"
./gradlew pixelCapi30debugAndroidTest

△ 虚拟设备配置

为便于区分哪些测试是针对哪些设备的,我们将创建自定义注解 LargeScreenTest,并用该注解来标记测试函数。运行前面的 Gradle 命令时,我们会为 AndroidTestRunner 添加一项参数,确保只运行具有此注释的测试。若您不使用注释,也可以使用 TestRunner 的其他过滤选项,比如运行特定类中的测试。将这些特性加以组合,我们可以为测试设置一致运行配置。

annotation class LargeScreenTest

@RunWith(AndroidJUnit4: :class)
class MyActivityTest {

    @Test @LargeScreenTest
    fun largeScreenDeviceTest() {
        // 在平板电脑设备上测试界面
    }
}

# 只运行带有指定注解的测试
. /gradlew pixelCapi30debugAndroidTest \
    -Pandroid.testInstrumentationRunnerArguments.annotation=com.mypkg.LargeScreenTest

△ 使用自定义注解为指定设备编写测试

更多信息

除了让屏幕上的内容看起来更大之外,大屏幕还带来了一些其他机会,帮助您的应用大放异彩。在多窗口模式下,您的应用可以与其他应用并排使用,除了响应式调整之外,还可以考虑如何让应用在这种模式下发挥更大作用,比如支持拖拽等。这种小功能可以提高用户的工作效率,用户便更乐意使用您的应用。

△ 多窗口模式效果

多窗口模式

https://developer.android.google.cn/guide/topics/ui/multi-window

除了通过触摸进行交互外,大屏幕设备还支持其他交互形式。设备的屏幕尺寸越大,用户就越有可能使用键盘、手写笔、鼠标、游戏手柄或其他外接设备。如果您想提高应用在这些情况下的易用性,可以计划支持其中一些输入方式,如需了解更多详情,请参阅文章《是时候为各式设备适配完善的输入支持了》。

在如此多样化的硬件生态系统中,您可能很难拥有各种形状和尺寸的设备,如今 Android SDK 为可折叠设备提供了模拟器图像,这些模拟器允许您随时将折叠状态更改为铰链的角度。即将推出的 Android Studio Chipmunk 也会配备可调整尺寸的模拟器,允许您自由改变应用窗口的尺寸,每个开发者都可以在几乎任何类型的设备中试用他们的应用。

△ Android Studio Chipmunk 中的可调整尺寸的模拟器

Android Studio Chipmunk

https://developer.android.google.cn/studio/preview

我们也一直在 Android Studio 中开发新工具,希望为大家开发大屏幕应用提供支持。新的 Layout Validation 工具可以在覆盖了各种尺寸类别的参考设备上预览布局,并提示问题区域 (例如文本使用了长行),以及为不同断点推荐不同界面组件。

△ Android Studio 中的 Layout Validation

最后,我们在 Android 开发者网站上列出了针对大屏幕的应用质量指南,指南中的前面部分介绍的是基本兼容性预期,比如应用是否同时支持横屏和竖屏模式,后面几部分重点介绍支持各种屏幕类型和状态,并使用特定屏幕类型或状态打造不同的体验。

大屏幕的应用质量指南

https://developer.android.google.cn/docs/quality-guidelines/large-screens-app-quality

我们希望大家都能够利用今天分享的内容,并参考新的质量指南,构建出在各种屏幕尺寸下都能让用户心动的应用。

、传统布局

盒状模型结合 display 属性、float 浮动以及 position 定位属性设计的各式传统布局形式。

2、说再多不如动手实践,下面举三个例子

html 部分代码:

 <section>
 2 <!-- 传统布局-例1结构:盒子模型 -->
 3 <div class="layout-one">
 4 <div class="header">header</div>
 5 <div class="banner">banner</div>
 6 <div class="content">main-content</div>
 7 <div class="footer">footer</div>
 8 </div>
 9 
10 <!-- 传统布局-例2结构:盒子模型 + float -->
11 <div class="layout-two">
12 <div class="header">header</div>
13 <div class="banner">banner</div>
14 <div class="content">
15 <div class="content-left">content-left</div>
16 <div class="content-right">content-right</div>
17 </div>
18 <div class="footer">footer</div>
19 </div>
20 
21 <!-- 传统布局-例3结构 + float + position -->
22 <div class="layout-three">
23 <div class="header">header</div>
24 <div class="main-banner">banner</div>
25 <div class="main-content">
26 <div class="content1">content1</div>
27 <div class="content2">content2</div>
28 </div>
29 <div class="footer">footer</div>
30 </div>
31 </section>

css样式部分代码:

1 /* 基本样式 */
 2 section {
 3 width: 1200px;
 4 height: 300px;
 5 margin: 0 auto;
 6 padding: 10px;
 7 }
 8 .layout-one, .layout-two, .layout-three {
 9 float: left;
10 margin-left: 20px;
11 }
12 div {
13 width: 300px;
14 }
15 
16 /* 可复用样式 */
17 .header {
18 height: 25px;
19 text-align: center;
20 background-color: bisque;
21 line-height: 25px;
22 }
23 .banner {
24 height: 50px;
25 text-align: center;
26 line-height: 50px;
27 background-color: aquamarine;
28 }
29 .footer {
30 height: 25px;
31 text-align: center;
32 line-height: 25px;
33 background-color: black;
34 color: #ffffff;
35 }
36 
37 /* 结构1 基础样式 */
38 .layout-one .content {
39 height: 60px;
40 text-align: center;
41 line-height: 60px;
42 background-color: aqua;
43 }
44 
45 /* 结构2 基础样式 */
46 .layout-two .content {
47 height: 60px;
48 text-align: center;
49 line-height: 60px;
50 background-color: aqua;
51 }
52 .layout-two .content-left {
53 width: 100px;
54 float: left;
55 border-right: 1px solid #000000;
56 }
57 .layout-two .content-right {
58 width: 199px;
59 float: left;
60 }
61 
62 /* 结构3 基础样式 */
63 .main-banner {
64 width: 200px;
65 height: 50px;
66 margin: 0 auto;
67 background-color: aquamarine;
68 text-align: center;
69 line-height: 50px;
70 }
71 .main-content {
72 position: relative;
73 width: 200px;
74 height: 60px;
75 margin: 0 auto;
76 text-align: center;
77 line-height: 60px;
78 }
79 .content1 {
80 width: 60px;
81 height: 60px;
82 position: absolute;
83 top: 0px;
84 left: 30px;
85 background-color: aqua;
86 }
87 .content2 {
88 width: 60px;
89 height: 60px;
90 position: absolute;
91 top: 0px;
92 right: 30px;
93 background-color: aqua;
94 }

页面效果:

通过上述的三个例子,我们可以发现:

  • 盒子的多重嵌套,虽然让我们可以方便的划分区域,但却增加了网页结构的复杂性,这将使后期网站的维护变得十分的困难,从而增加维护成本;
  • 合理的网页布局可以让我们在网站开发的过程中得到事半功倍的效果
  • 浮动的应用以及结合定位属性的使用,使得我们的网页布局花样变得丰富多彩,但同时也带来了不少问题,比如:在定位的过程中对“距离”的要求变得十分精确等等
  • 网站设计与布局的其中一个也是很重要的要求便是能在不同的设备上较好的展现对应的内容,但如上的传统布局在一定程度上在这方面不是很好,无法很好的进行响应屏幕分辨率的变化!

为了更近时代的进步,所以我们需要更深入的去学习,如:响应式设计中的,流式布局,弹性布局等等!加油吧,骚年!

每天进步一点点,相信积累的力量!

web前端其实很简单!

如何学习呢?看下下面

领取方法:

关注“IT金匀” 然后私信回复“前端”即可

请记得给金匀先来个“评论+转发”

我们的开发工程中经常会使用到各种图,所谓的图就是由节点和节点之间的连接所形成的系统,数学上专门有一个分支叫图论(Graph Theroy)。利用图我们可以做很多工具,比如思维导图,流程图,状态机,组织架构图,等等。今天我要做的是用开源的HTML5工具来快速构造一个做图的工具。

工具选择

工预善其事,必先利其器。第一件事是选择一件合适的工具,开源时代,程序员还是很幸福的,选择很多。

  • flowchart.js http://adrai.github.io/flowchart.js/ , 基于SVG创建Flow Chart
  • go.js http://www.gojs.net/latest/index.html go.js 提供一整套的JS工具 ,支持各种交互式图表的创建。有免费版和收费版
  • joint.js http://www.jointjs.com/ joint.js 是另一个创建图标库的工具,也提供免费版和商业版
  • jsPlumb http://www.jsplumb.org/ jsPlumb是一套开源的流程图创建工具 ,小巧精悍,使用简单
  • d3 http://d3js.org 在html5领域,d3可谓是最好的可视化基础库,提供方面的DOM操作,非常强大。

最终,我选择了jsPlumb,因为它完全开源,使用很简单,用D3的话可能会多花很多功夫。joint.js也不错。大家可以根据自己的需要选择。

构建静态应用

下面我们一步一步的来使用jsPlumb来创建我们的流程图工具。

第一步是等待DOM和jsPlumb初始化完毕,类似document.ready()和jquery.ready(), 要使用jsPlumb, 需要把代码放在这个函数里:

jsPlumb.ready(function() {
    // ... your code goes here ...
}


创建一个jsPlumb的实例,并初始化jsPlumb的配置参数:

//Initialize JsPlumb
var color = "#E8C870";
var instance = jsPlumb.getInstance({
    // notice the 'curviness' argument to this Bezier curve.  the curves on this page are far smoother
    // than the curves on the first demo, which use the default curviness value.      
    Connector : [ "Bezier", { curviness:50 } ],
    DragOptions : { cursor: "pointer", zIndex:2000 },
    PaintStyle : { strokeStyle:color, lineWidth:2 },
    EndpointStyle : { radius:5, fillStyle:color },
    HoverPaintStyle : {strokeStyle:"#7073EB" },
    EndpointHoverStyle : {fillStyle:"#7073EB" },
    Container:"container-id"
 });


这里给给出了一些配置包括,连接线(这里配置了一个贝塞尔曲线),线的风格,连接点得风格。Container需要配置一个对应的DIV容器的id。(这里也可以使用setContainer的方法)

下面我们要创建一个节点(node),每一个节点可以用一个DIV来实现。我这里提供了一个函数来创建节点。

function addNode(parentId, nodeId, nodeLable, position) {
  var panel = d3.select("#" + parentId);
  panel.append('div').style('width','120px').style('height','50px')
    .style('position','absolute')
    .style('top',position.y).style('left',position.x)
    .style('border','2px #9DFFCA solid').attr('align','center')
    .attr('id',nodeId).classed('node',true)
    .text(nodeLable);

  return jsPlumb.getSelector('#' + nodeId)[0];
}


这里做的事情就是创建了一个DIV元素,并放在对应的容器的制定位置上,注意为了支持拖拽的功能,必须使用position:absolute 。

我使用D3来操作DOM,大家可能会更习惯JQuery,这纯属个人喜好的问题。

最后返回创建节点的实例引用,这是的selector使用了jsPlumb.getSelector()方法,它和JQuery的selector是一样的,这样用的好处是你可以使用不同的DOM操作库,例如Vanilla

下面我使用一个函数来创建端点/锚点(anchor),锚点就是节点上的连接点,用于连接不同的节点。

function addPorts(instance, node, ports, type) {
  //Assume horizental layout
  var number_of_ports = ports.length;
  var i = 0;
  var height = $(node).height();  //Note, jquery does not include border for height
  var y_offset = 1 / ( number_of_ports + 1);
  var y = 0;

  for ( ; i < number_of_ports; i++ ) {
    var anchor = [0,0,0,0];
    var paintStyle = { radius:5, fillStyle:'#FF8891' };
    var isSource = false, isTarget = false;
    if ( type === 'output' ) {
      anchor[0] = 1;
      paintStyle.fillStyle = '#D4FFD6';
      isSource = true;
    } else {
      isTarget =true;
    }

    anchor[1] = y + y_offset;
    y = anchor[1];

    instance.addEndpoint(node, {
      uuid:node.getAttribute("id") + "-" + ports[i],
      paintStyle: paintStyle,
      anchor:anchor,
      maxConnections:-1,
      isSource:isSource,
      isTarget:isTarget
    });
  }
}


instance是jsPlumb的实例

node是我们用addNode方法创建的Node实例

ports,是一个string的数组,指定端点的个数和名字

type,可能是output或者input,指定端点的种类,一个节点的输出端口可以连接另一个节点的输入端口。

这里anchor是一个四维数组,0维和1维分别是锚点在节点x轴和y轴的偏移百分比。我这里希望把端口画在节点的左右两侧,并按照端口的数量均匀分布。

最后使用instance.addEndpoint来创建端点。注意这里只要指定isSource和isTarget就可以用drag&drop的方式来连接端点,非常方便。

下面一步我们提供一个函数来连接端点:

function connectPorts(instance, node1, port1, node2 , port2) {
  // declare some common values:
  var color = "gray";
  var arrowCommon = { foldback:0.8, fillStyle:color, width:5 },
  // use three-arg spec to create two different arrows with the common values:
  overlays = [
    [ "Arrow", { location:0.8 }, arrowCommon ],
    [ "Arrow", { location:0.2, direction:-1 }, arrowCommon ]
  ];

  var uuid_source = node1.getAttribute("id") + "-" + port1;
  var uuid_target = node2.getAttribute("id") + "-" + port2;

  instance.connect({uuids:[uuid_source, uuid_target]});
}


node1和node2是源节点和目标节点的引用,port1和port2是源端口和目标端口的名字。

使用instance.connect方法来创建连接。 overlays用来添加连接线的箭头效果或者其他风格,我这里没有使用,因为觉得都不是很好看。大家如果要用,只要把overlays加入到instance.connect的方法参数就可以了。

调用以上方法来创建节点,端点和连接线。

var node1 = addNode('container-id','node1', 'node1', {x:'80px',y:'20px'});
var node2 = addNode('container-id','node2', 'node2', {x:'280px',y:'20px'});

addPorts(instance, node1, ['out1','out2'],'output');
addPorts(instance, node2, ['in','in1','in2'],'input');

connectPorts(instance, node1, 'out2', node2, 'in');


这里我们创建了两个节点,第一个节点有两个输出端口,第二个节点有三个输入端口,然后把第一个节点的out2端口连接到第二个端点的in端口。效果如下:

最后我们给节点增加drag&drop的功能,这样我们就可以拖动这些节点来改变图的布局了。

instance.draggable($('.node'));


这里似乎依赖于JQuery-UI,我还不是很清楚。

交互式创建节点

我们已经初步具有了创建图的功能,可是节点的创建必须通过程序,我们希望用交互的方式来创建节点。

通常我们希望有一个tree view的控件,让后通过拖拽来创建对应类型的节点。这里我使用了这个开源的tree view,基于bootstrap https://github.com/jonmiles/bootstrap-treeview

我们先创建一个tree view:

function getTreeData() {
  var tree = [
    {
      text: "Nodes",
      nodes: [
        {
          text: "Node1",
        },
        {
          text: "Node2"
        }
      ]
    }
  ]; 

  return tree;
}
//Initialize Control Tree View
$('#control-panel').treeview({data: getTreeData()});


树上有两个节点:

然后我实现从树上拖拽对应的节点,到流程图上的逻辑。

//Handle drag and drop
$('.list-group-item').attr('draggable','true').on('dragstart', function(ev){
  //ev.dataTransfer.setData("text", ev.target.id);
  ev.originalEvent.dataTransfer.setData('text',ev.target.textContent);
  console.log('drag start');
});

$('#container-id').on('drop', function(ev){
  //avoid event conlict for jsPlumb
  if (ev.target.className.indexOf('_jsPlumb') >= 0 ) {
    return;
  }

  ev.preventDefault();
  var mx = '' + ev.originalEvent.offsetX + 'px';
  var my = '' + ev.originalEvent.offsetY + 'px';

  console.log('on drop : ' + ev.originalEvent.dataTransfer.getData('text'));
  var uid = new Date().getTime();
  var node = addNode('flow-panel','node' + uid, 'node', {x:mx,y:my});
  addPorts(instance, node, ['out'],'output');
  addPorts(instance, node, ['in1','in2'],'input');
  instance.draggable($(node));
}).on('dragover', function(ev){
  ev.preventDefault();
  console.log('on drag over');
});


这里要注意的是要避免和jsPlumb拖拽端点的逻辑冲突,当检测到target是jsPlumb对象是需要直接从drop方法中退出以执行对应的jsPlumb的drop逻辑。

好了,一个绘制流程图的软件工具初步完工。

我把代码放在oschina的代码托管服务上了, 大家有兴趣可以去试试。