显示标签为“mobile”的博文。显示所有博文
显示标签为“mobile”的博文。显示所有博文

2018年4月15日星期日

移动网页的 iPhone X 适配

Instagram web without iPhone X fix (portrait)

一个月前我在 iPhone X 的 Mobile Safari 中打开 Instagram web,发现页面底下的导航栏跟 iPhone 的 home indicator 重叠在一起不方便使用。我想既然 Apple 为 iPhone X 专门更新的 Human Interface Guidelines 并为 native app 引入了 safe area 和 inset 等概念,那 Mobile Safari 应该有对应的 web 概念吧。搜索了一下,发现 Apple 确实对 Mobile Safari 增加了对应的功能。既然 Instagram 是我们公司的产品,那就动手去改吧。

改造的第一步是对页面加上这一句:

<meta name='viewport' content='initial-scale=1, viewport-fit=cover'>

因为大多数移动页面都已经有类似的声明,所以只要加上 viewport-fit=cover 就行了。不加的话,下面所有的 CSS inset 声明都不会生效。

第二部是把竖屏(portrait mode)时的页面底部导航栏往上挪。这时候我们可以把导航栏到屏幕底部的距离设置为 env(safe-area-inset-bottom),然后浏览器自动会使用正确的数值来进行布局。(在 Safari 显示自己的工具栏时,这个值会神器地变为 0,使得页面底部导航栏紧贴 Safari 工具栏。)假设我们使用 padding-bottom 把导航栏往上挪,那么我们可以写 padding-bottom: env(safe-area-inset-bottom)。(当然 Instagram web 的实际情况比这个复杂,如果你想研究的话可以用 Safari remote debugger + iPhone X Simulator 来看。)这样竖屏的问题就修复了。

Instagram web with iPhone X fix (portrait)

如果这是个 native app 的话,问题可能到此就结束了,因为 native app 可以选择不支持横屏(landscape mode)。然后网页必须支持横屏,因为浏览器本身可以横屏。(当然你也可以很霸道地在浏览器横屏时只显示一句提示让用户把屏幕直过来,这样就可以不支持横屏了。)因为 iPhone X 屏幕顶上的那个缺口(notch),Mobile Safari 在横屏时默认会在页面两侧加白边,确保任何没对 iPhone X 做修改的页面能够正常显示。

Instagram web without iPhone X fix (landscape)

这两侧的白边很不好看,因为会让原本应该贯穿全屏的横线终止在屏幕内。在加上 viewport-fit=cover 后,两侧的白边会消失掉,因为 Mobile Safari 把这看作开发者愿意对 iPhone X 布局负责,之后如何处理横屏一侧缺口就是开发者的责任了。之前对 Instagram web 的竖屏调整一旦放到横屏就会发现新问题。

Instagram web with some iPhone X fix (landscape)

页面顶部标题栏两侧的按钮太靠近屏幕边缘了。因为 iPhone X 屏幕边缘有圆角,所以按钮放在那里并不好按。此外那也在 Apple 定义的 safe area 之外,本来就不应该放可点击元素。为此我们必须使用 env(safe-area-inset-left)env(safe-area-inset-right) 把这两个按钮往页面中间挪。假设我们使用 margin-leftmargin-right 来控制布局的话,我们可以这样写:

.leftButton {
  margin-left: env(safe-area-inset-left);
}
.rightButton {
  margin-right: env(safe-area-inset-right);
}

这样子横屏是修复了,但又会给竖屏引入新的问题。在原本的竖屏设计中,按钮离两侧屏幕边缘 16px。在我们把 16px 替换成 env(safe-area-inset-left)env(safe-area-inset-right) 之后,竖屏时这两个按钮就贴着屏幕边缘了。为此我们要引入 max() 来保证按钮离屏幕边缘至少有 16px

.leftButton {
  margin-left: max(16px, env(safe-area-inset-left));
}
.rightButton {
  margin-right: max(16px, env(safe-area-inset-right));
}

这时候竖屏横屏都没问题了,唯一问题是 Safari 以外的浏览器都被弄晕了,这 maxenv 都是什么呀?我们还没支持呢,而且是否会被标准化也很难说。幸好大多数浏览器都支持 @support,我们可以用它来进行筛选,把专门写给 Safari 看的 CSS 留给 Safari 看。

.leftButton {
  margin-left: 16px;
}
.rightButton {
  margin-right: 16px;
}

@supports (margin: max(16px)) {
  .leftButton {
    margin-left: max(16px, env(safe-area-inset-left));
  }
  .rightButton {
    margin-right: max(16px, env(safe-area-inset-right));
  }
}

到此所有的问题都解决了,Instagram web 也能在横屏中正常显示了。王子和公主从此幸福地生活在一起。

Instagram web with all iPhone X fix (landscape)

故事当然不会到这里就结束了。首先,Instagram web 可不止这一个页面。这些页面的竖屏都不会有问题,但横屏就很难说了,有可能某些元素在使用 viewport-fit=cover 之后被布局到了 safe area 之外,需要把它们挪回来。这些问题我见到一个修一个,但永远也不知道是否有遗留的。当然这个问题在 native app 里面也存在,除非从零开始设计一个新的 app 并在设计原则和布局框架上对 safe area 作出考虑,否则一个 app 无论怎么改都无法证明改全了,而且开发新功能时一不小心没测 iPhone X 就可能出现不兼容的问题。

其次,Mobile Safari 在横屏模式时如果显示地址栏就会导致页面底部导航栏处于半隐藏状态,而非原来的全隐藏状态。

iPhone X Safari Apple bug (landscape)

为什么会发生这样的事情呢?因为在显示地址栏时 Safari 会把整个 viewport 往屏幕下方挪动地址栏的高度。这时候 viewport 高度是不会改变的,因此 viewport 的一部分就跑到屏幕外去了。(但 viewport 的定义不就是屏幕内可见区域么?Apple 你自己发明了这个概念,现在说改就改。)Apple 对此的解释是,显示地址栏的 animation 必须保持 60 FPS,但 viewport 高度变化过程受页面布局速度影响而无法做到 60 FPS,所以这是 feature 不是 bug。(Chrome for iOS 在显示地址栏时会调整 viewport 高度,但因为不是 60 FPS animation 所以会看到页面闪烁。)

我觉得 Apple 要把 viewport 偷偷隐藏掉一部分也不是问题,但在隐藏的时候至少应该把 env(safe-area-inset-bottom) 自动变会 0 吧?这样子底部导航栏至少可以完全隐藏掉。这个问题已经有其他人写过,并且那篇文章的作者已经给 Apple 开 bug。

最后一个问题,为什么 env(safe-area-inset-top) 没有被用到?因为 Mobile Safari 总会在屏幕顶部显示状态栏,所以网页永远都不需要自己想办法避让屏幕顶部的缺口。(那使用 <meta name="apple-mobile-web-app-capable" content="yes"> 强行进入全屏模式呢?iPhone X 会很恶心地在屏幕上方留下一个黑色区域。)估计唯一的例外是你自己写一个 app 并在里面放一个全屏的 WebView,这时候 WebView 内的网页就需要使用 env(safe-area-inset-top) 了。我没有试过做这样的事情,但可以参考别人的文章

总的来说,iPhone X 适配不是一个很难的技术问题,尤其是只做竖屏模式的话。

2010年1月29日星期五

能承载移动 Web 应用的唯一浏览器: Mobile Safari

最近拿 iPhone 、 Android 、 Windows Mobile 这三个平台上的内置浏览器来做了一番对比,结果是只有 iPhone 的 Mobile Safari 能够承载现代化的移动 Web 应用,其他移动浏览器的设计思路还停留在上个世纪──能看网页就行,不存在移动应用一说。

我用来做对比的平台是 iPhone 2.0 、 Android 2.0 、 Windows Mobile 6.5 。 iPhone 之所以选择 2.0 ,是因为 3.0 的浏览器跟 2.0 是一样的,尽管我的测试是在 3.0 上做的,但对 2.0 来说也是适用的。 Windows Mobile 6.5 则是在 Windows Mobile 系列中的唯一选择,因为只有它的浏览器是 IE6 内核的,再往前的 Windows Mobile 6.1 都是 IE4 内核的。

拖放

iPhone 对拖放的支持是完美的,使用 touch 系列事件监控触击及拖动,然后使用 preventDefault 禁用浏览器的默认行为(也就是拖动页面显示区域),这就搞掂了。

Android 也支持拖放,具体方案我没有研究过,估计是改用 mouse 系列事件监控触击及拖动,然后使用 preventDefault 禁用浏览器的默认行为。

Windows Mobile 让人很绝望,连拖放也不支持。估计因为这是一个对 IE6 的封装,封装的时候就把浏览器默认行为写死了,所以你不能够通过 mouse 事件监控到任何东西,也无法禁用浏览器默认行为。

手势

最简单的手势,就是通过两个手指靠进和远离来实现放大和缩小,如果这个手势都实现不了,别的手势也就不用测试了。

iPhone 对手势的支持也是完美的,你可以直接使用 gesture 系列事件监控手势操作,也可以用 touch 系列事件监控手势中每一个触点的活动。 preventDefault 同样可以禁用浏览器默认行为(也就是缩放页面显示区域)。

Android 的浏览器不支持多点触击,尽管 Android 的另一些内置应用是支持的,这估计是因为 Google 和 Apple 纠缠不清的多点触击专利官司。因此,如果你要在 Web 应用上支持缩放,就只能在界面上放两个按钮来实现了。

Window Mobile 就不用说了,你不能期望一个缩小版的 IE6 能做什么针对移动 Web 应用的优化。

定位

iPhone 可以使用 W3C 的 Geolocation API 进行定位,而且可以跟踪定位(也就是定位变化时通过事件调用你的函数)。

Android 内置的地图可以定位,但是浏览器就是不提供这个接口,这只能说 Android 就设计为不考虑移动 Web 应用。

Windows Mobile 在安装 Google Gears 后,理论上可以支持定位。这个 ActiveX 嘛,能让你扩展浏览器,实现任何其他浏览器上难以实现的东西。唯一的问题是,这会让人产生疑问──我到底是在 Web 应用开发还是在做客户端应用开发? Microsoft 背负了太多的历史包袱,使得开发者要从客户端应用开发转向 Web 应用开发也不容易。

绘图

iPhone 同时支持 SVG 和 Canvas 。如果你需要面向对象的图形组合方式,你可以选择 SVG 。如果你需要命令式(类似 GDI )的绘图接口,你可以选择 Canvas 。

Android 只支持 Canvas ,这能够满足大多数绘图需求,唯一的问题是你不能把已经绘制的形状如同对象般删除,需要的话只能全部重绘,这就是 Canvas 跟 SVG 在使用上的主要区别。

至于 Windows Mobile ,不仅仅 SVG 和 Canvas 这样的公开标准不支持,连 VML 这样的私有标准也不支持,基本上也就无法绘图了。

样式

iPhone 支持大部分的 CSS3 特性。这是因为 iPhone 1.0 并不支持客户端应用, Apple 为了让开发者能在 Web 上实现跟客户端应用一样的视觉效果, Mobile Safari 必须做到跟 Safari 一样,支持大多数的 CSS3 特性。

Android 支持的 CSS3 特性要少一些。尽管它的浏览器用的也是 Webkit 内核,但 Webkit 管的是布局而不是渲染,在样式渲染方面 Google 的实现总是比 Apple 要慢一步。

Windows Mobile 的话……跟一个 IE6 核心的浏览器讨论 CSS3 ,这不是纯粹浪费口水吗?

体验

上面列举了一大堆的功能,但这一切只是为了实现特定的体验,所以我们真正应该关注的是一款移动浏览器设计为满足怎样的体验目标。

iPhone 上的 Mobile Safari 拥有最高的体验目标设定。在 iPhone 1.0 的时候, Apple 禁止开发人员开发本地应用,只允许开发 Web 应用,因此 Apple 必须为开发人员提供最好的 Web 应用开发平台。

此外, Apple 拥有业界最好的 HIG (人机交互指引),对比之下 Microsoft 的 UX Guide 连 Windows Mobile 都覆盖不到,而 Android 的 UI Guildlines 则显得十分山寨。由于 Apple 鼓励开发者按照 HIG 进行 iPhone 应用程序的交互设计,所以 Mobile Safari 当初就设计为帮助开发者实践 HIG 。至于其他移动浏览器,它们的设计目标则简单得多──只要用户能够在上面流畅使用面向桌面设计的网站就可以了。

正是由于设计目标的差异,使得最终产出的产品存在着巨大的差异。只有 Mobile Safari 能够承载移动 Web 应用,其他移动浏览器只是桌面浏览器在移动设备上的延伸。

2009年11月24日星期二

编写 iPhone Friendly 的 Web 应用程序 (Part 7 - 多点触击)

这个系列的上一篇文章差不多是两年之前的事情了,在这两年里Mobile Safari并非停滞不前,从iPhone 2.0开始Mobile Safari就加入了对多点触击的支持,现在我们就来看一下我们可以利用它来干什么。

相信很多人都看过WPF为Surface设备做的一个简单demo,也就是在桌面上显示若干张照片,你可以通过单点触击拖放,也可以通过多点触击缩放和旋转。这在iPhone上能够做到,甚至在Mobile Safari里面也能做到,因为Mobile Safari提供了一套专门用于多点触击的JavaScript接口。现在我们就来看看如何利用这套接口吧。

我们都知道,Mobile Safari自身会处理多点触击,默认行为包括滚动和缩放。我们可以接管相应的事件,同时使用e.preventDefault()禁用浏览器默认行为,使得我们的Web应用程序能够如同WPF桌面应用一样处理多点触击。下面我们来深入看看Mobile Safari提供的多点触击事件。

单点触击

首先我们要处理的是单点触击事件,禁用浏览器的滚动行为,同时为我们的照片(一个img元素)增加拖动行为。在这里,我们需要用到touchstarttouchmovetouchend事件。在这三个事件里,我们可以通过e.targetTouches获取到用户点击的坐标,从而计算相对的位置变化。

首先,我们要在touchstart事件里面记录下初始坐标:

var transform = {
  x: 0,
  y: 0,
  rotation: 0,
  scale: 1
};

var startX;
var startY;
var touching = false;

element.addEventListener("touchstart", function(e){
  e.preventDefault();
  startX = e.targetTouches[0].clientX;
  startY = e.targetTouches[0].clientY;
  touching = true;
});


接着,我们要在touchmove事件里面计算相对位置变化,并且更新element坐标:

element.addEventListener("touchmove", function(e){
  e.preventDefault();
  if (!touching) return;
  transform.x += e.targetTouches[0].clientX - startX;
  transform.y += e.targetTouches[0].clientY - startY;
  updateTransform();
  startX = e.targetTouches[0].clientX;
  startY = e.targetTouches[0].clientY;
});
updateTransform做了什么?现在先不讨论,我们只要把事件相关数据正确地更新到transform的四个属性即可,如何把这些属性反映到界面上稍后再说。

最后,我们还要在touchend事件里面处理一下标志位:

element.addEventListener("touchend", function(e){
  e.preventDefault();
  touching = false;
)};


就这么简单?是的。关键点也就在于touchmove时跟踪e.targetTouches的变化,并更新transform里面的信息。

CSS3变换

接下来我们看看如何将transform里面的信息作用到界面上。在没有CSS3的时代,这是极之痛苦的事情,我们需要修改元素的多个样式属性才能实现这部分的功能,并且还没办法实现旋转。现在有了CSS3,只需要修改一下transform属性就可以了:

var updateTransform = function(){
  element.style.webkitTransform
    = "translate(" + transform.x + "px, "
    + transform.y + "px) "
    + "rotate(" + transform.rotation + "deg) "
    + "scale(" + transform.scale + ")";
}


一句代码就把位置、旋转、缩放都设置好了!尽管我们现在还没用到旋转和缩放属性,那就让它们保持默认值吧,我们在多点触击的事件里面会设置它们的。

多点触击

多点触击涉及到三个事件:gesturestartgesturechangegestureend。这三个事件跟单点触击的三个事件非常类似,使用起来甚至可以说是更简单一些:

var startRotation;
var startScale
var gesturing = false;

element.addEventListener("gesturestart", function(e){
  e.preventDefault();
  startRotation = transform.rotation;
  startScale = transform.scale;
  gesturing = true;
});

element.addEventListener("gesturechange", function(e){
  e.preventDefault();
  if (!gesturing) return;
  transform.rotation = startRotation + e.rotation;
  transform.scale = startScale * e.scale;
  updateTransform();
});

element.addEventListener("gestureend", function(e){
  e.preventDefault();
  gesturing = false;
});


代码确实比之前的还要少一些,重点就是正确设置transform的两个属性,随后调用一下updateTransform就能把最近的状态更新的界面上。

小结

在这篇文章里,我们了解到了Mobile Safari的6个特有事件,以及如何利用这6个特有事件处理多点触击。

如果你直接使用我的代码去实现开头所说的照片拖放应用,你会发现一个小问题——在进行多点触击操作时,旋转与缩放都是很自然的,就是拖动不自然,好像拖动只跟随第一个触点似的。原因很简单,在多点触击时,管理触点移动的还是touchmove事件,但上述代码只处理e.targetTouches[0],所以拖动只跟随第一个触点。

如果需要同时跟随两个触点,你需要对代码稍作改动,使得移动距离为e.targetTouches[0]e.targetTouches[1]的平均值。为什么呢?如果一个触点往上移动30px,另一个触点往下移动10px,除去旋转与缩放效果外,照片的中点应该是往上移动10px的,也就是两个移动的平均值。那么我如何知道当前有多少个触点呢?看看e.targetTouches.length就知道了。

最后,如果你关注移动设备上的Web开发,欢迎订阅我的博客: