Android中处理Touch Icon的方案

关注微信号:javalearns   随时随地学Java

或扫一扫

随时随地学Java


苹果的Touch Icon相对我们都比较熟悉,是苹果为了支持网络应用(或者说网页)添加到桌面需要的图标,有了这些Touch Icon的网页链接更加和Native应用更相像了。由于苹果设备IPod,IPhone,IPad等设备广泛,很多网页都提供了touch icon这种图标资源。由于Android中并没有及早的有一份这样的标准,当我们想把网页添加到桌面时,仍然需要使用苹果的Touch Icon。

Touch Icon

当我们想让一个网页比较完美地添加到桌面,通常情况下我们需要设置一个png图片文件作为apple-touch-icon。比如

<link rel="apple-touch-icon" href="/custom_icon.png">

如果想支持IPhone和IPad,我们需要使用sizes属性来制定多个图片,默认sizes的值为60 x 60。

<link rel="apple-touch-icon" href="touch-icon-iphone.png">
<link rel="apple-touch-icon" sizes="76x76" href="touch-icon-ipad.png">
<link rel="apple-touch-icon" sizes="120x120" href="touch-icon-iphone-retina.png">
<link rel="apple-touch-icon" sizes="152x152" href="touch-icon-ipad-retina.png">

在IOS7之前,苹果系统会对添加到桌面的图标进行圆角化等视觉上的处理,为了不让其处理,我们可以使用apple-touch-icon-precomposed来作为rel的值实现。

更多关于Touch Icon的信息,可以访问水果开发者网站了解更多。

Android中有缺陷的实现

在Android WebView提供了处理Touch Icon的回调,onReceivedTouchIconUrl(WebView view, String url,boolean precomposed)该方法返回了对我们有用的touch icon的url,和是否为预组合(在IOS中不需要进行视觉处理)。虽然有这些数据,我们可以进行处理,但是这其中是有问题的,就是我们不好确定文件的大小,来选择适合的图片。

举个例子,如下一个网页的源码,其中sizes的顺序不规律

<link rel="apple-touch-icon-precomposed" sizes="72x72" href="http://www.qiyipic.com/20130423143600/fix/H5-72x72.png">
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="http://www.qiyipic.com/20130423143600/fix/H5-114x114.png">
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="http://www.qiyipic.com/20130423143600/fix/H5-57x57.png">
<link rel="apple-touch-icon-precomposed"  href="http://www.qiyipic.com/20130423143600/fix/H5-0x0.png">

加载网页,onReceivedTouchIconUrl输出的日志

I/MainActivity( 6995): onReceivedTouchIconUrl url=http://www.qiyipic.com/20130423143600/fix/H5-0x0.png;precomposed=true
I/MainActivity( 6995): onReceivedTouchIconUrl url=http://www.qiyipic.com/20130423143600/fix/H5-57x57.png;precomposed=true
I/MainActivity( 6995): onReceivedTouchIconUrl url=http://www.qiyipic.com/20130423143600/fix/H5-114x114.png;precomposed=true
I/MainActivity( 6995): onReceivedTouchIconUrl url=http://www.qiyipic.com/20130423143600/fix/H5-72x72.png;precomposed=true

从上面的输出来看,基本上是后面(书写)的元素先打印出来,所以这个回调的缺陷如下

  • 由于Touch Icon url地址没有硬性规定,不能根据url包含某些尺寸来判断使用哪个icon
  • 由于网页编写touch icon元素相对随意,不能根据onReceivedTouchIconUrl调用先后来决定使用哪个icon
  • 回调中没有sizes属性值,不好确定使用哪个icon
  • 如果我们选取质量最高的图片,然后进行适当压缩处理或许可以解决问题,但是将全部icon下载下来或者根据Head头信息总感觉不怎么好。

改进方法

既然WebView没有现成的方法满足我们的需求,只好自己来实现。其实实现方法还是比较简单地就是js脚本注入检测网页元素中得touch icon,返回json数据。

JavaScript方法

下面的JS代码所做的功能为查找所有为touch icon的link元素,包含正常的还标记为precomposed。然后将这些link元素的属性存入json数据,最后返回给Java代码中对应的回调。

var touchIcons = [];
function gatherTouchIcons(elements) {
  var normalTouchIconLength = elements.length;
  var currentElement;
  for (var i =0; i < normalTouchIconLength;i++) {
      currentElement = elements[i];
      var size;
      if (currentElement.hasAttribute('sizes')) {
          size = currentElement.sizes[0];
      } else {
          size = '';
      }
      var info = {'sizes':size, 'rel': currentElement.rel, 'href': currentElement.href};
      touchIcons.push(info);
  }
}

function obtainTouchIcons() {
  normalElements = document.querySelectorAll("link[rel='apple-touch-icon']");
  precomposedElements = document.querySelectorAll("link[rel='apple-touch-icon-precomposed']");
  gatherTouchIcons(normalElements);
  gatherTouchIcons(precomposedElements);
  var info = JSON.stringify(touchIcons);
  window.app_native.onReceivedTouchIcons(document.URL, info);
}
obtainTouchIcons();

Java代码

这里为了便于理解还是全部贴出了demo的源码,demo中当网页加载完成之后注入上面的js代码获取touch icon信息,然后返回给java的回调方法中。如果不清楚Java和JavaScript交互,可以访问Android中Java和JavaScript交互了解更多。

package com.example.obtaintouchicon;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class MainActivity extends Activity {

  protected String LOGTAG = "MainActivity";

  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      WebView webView = new WebView(this);
      webView.getSettings().setJavaScriptEnabled(true);
      webView.setWebViewClient(new WebViewClient() {
          @Override
          public void onPageFinished(WebView view, String url) {
              super.onPageFinished(view, url);
              final String touchIconJsCode = getTouchIconJsCode();
              Log.i(LOGTAG , "onPageFinished url = " + url + ";touchIconJsCode=" + touchIconJsCode);
              view.loadUrl("javascript:" + touchIconJsCode);
          }
      });
      webView.addJavascriptInterface(new JsObject(), "app_native");
      webView.loadUrl("http://192.168.1.5:8000/html/touchicon.html");
  }

  private class JsObject {

      @JavascriptInterface
      public void onReceivedTouchIcons(String url, String json) {
          Log.i(LOGTAG, "onReceivedTouchIcons url=" + url + ";json=" + json);
      }
  }

  private String getTouchIconJsCode() {
      StringBuilder total = new StringBuilder();
      InputStream inputStream = null;
      BufferedReader bufferReader = null;
      try {
          inputStream = getAssets().open("touchicon.js");
          bufferReader = new BufferedReader(new InputStreamReader(inputStream));
          String line;
          while ((line = bufferReader.readLine()) != null) {
              total.append(line);
          }
      } catch (FileNotFoundException e) {
          e.printStackTrace();
      } catch (IOException e) {
          e.printStackTrace();
      } finally {
          if (null != inputStream) {
              try {
                  inputStream.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      }
      return total.toString();
  }
}

返回的JSON数据

[
  {
      "sizes":"72x72",
      "rel":"apple-touch-icon-precomposed",
      "href":"http://www.qiyipic.com/20130423143600/fix/H5-72x72.png"
  },
  {
      "sizes":"114x114",
      "rel":"apple-touch-icon-precomposed",
      "href":"http://www.qiyipic.com/20130423143600/fix/H5-114x114.png"
  },
  {
      "sizes":"57x57",
      "rel":"apple-touch-icon-precomposed",
      "href":"http://www.qiyipic.com/20130423143600/fix/H5-57x57.png"
  },
  {
      "sizes":"",
      "rel":"apple-touch-icon-precomposed",
      "href":"http://www.qiyipic.com/20130423143600/fix/H5-0x0.png"
  }
]

我们可以对得到的JSON数据按照需要处理。

Google会改进么

答案是会,而且已经改进,但Google修改的不是onReceivedTouchIconUrl这个方法,而是Google正在推行自己的一套规则。

在Chrome上,Google增加了这样一个元素,这是Google提供的为网页程序定义元数据的方法。

<link rel="manifest" href="manifest.json">

在元数据json中,你可以自定义title,起始页,程序是横屏还是竖屏展示。一个简单地json实例如下,这里我们可以看到其中icons中存在多个类似touch icon的图标,src代表图标路径,sizes代表大小,type就是mimetype,density指的是Android中的屏幕密度(这样更加Android化了)。

{
  "name": "Web Application Manifest Sample",
  "icons": [
    {
      "src": "launcher-icon-0-75x.png",
      "sizes": "36x36",
      "type": "image/png",
      "density": "0.75"
    },
    {
      "src": "launcher-icon-1x.png",
      "sizes": "48x48",
      "type": "image/png",
      "density": "1.0"
    },
    {
      "src": "launcher-icon-1-5x.png",
      "sizes": "72x72",
      "type": "image/png",
      "density": "1.5"
    },
    {
      "src": "launcher-icon-2x.png",
      "sizes": "96x96",
      "type": "image/png",
      "density": "2.0"
    },
    {
      "src": "launcher-icon-3x.png",
      "sizes": "144x144",
      "type": "image/png",
      "density": "3.0"
    },
    {
      "src": "launcher-icon-4x.png",
      "sizes": "192x192",
      "type": "image/png",
      "density": "4.0"
    }
  ],
  "start_url": "index.html",
  "display": "standalone",
  "orientation": "landscape"
}

关于Google这套新的标准,可以参考Add to Homescreen

但是由于目前,这种标准实施率相对比较低,所以我们还是需要使用苹果的touch icon。

.................... 【.........阅读全文】

Java免费学习   Java自学网 http://www.javalearns.com

关注微信号:javalearns   随时随地学Java

或扫一扫

随时随地学Java


<think>我们面临一个挑战:在Service中显示SurfaceView。SurfaceView通常用于在Activity中显示高性能图形,但Service没有界面。不过,我们可以通过以下方法实现在Service中显示SurfaceView: ### 核心思路 1. **使用系统窗口**:在Service中创建`SurfaceView`并添加到系统窗口管理器(`WindowManager`)。 2. **权限声明**:需要声明`SYSTEM_ALERT_WINDOW`权限,因为添加系统窗口属于特殊权限。 3. **生命周期管理**:确保在Service销毁时移除SurfaceView,避免内存泄漏。 ### 实现步骤 #### 1. 添加权限 在`AndroidManifest.xml`中添加权限: ```xml <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> ``` > **注意**:从Android 6.0 (API 23)开始,`SYSTEM_ALERT_WINDOW`是危险权限,需要动态申请。对于Android 10及更高版本,使用`TYPE_APPLICATION_OVERLAY`代替`TYPE_SYSTEM_ALERT`。 #### 2. 创建Service并添加SurfaceView ```java public class OverlayService extends Service implements SurfaceHolder.Callback { private SurfaceView surfaceView; private WindowManager windowManager; @Override public void onCreate() { super.onCreate(); // 初始化WindowManager windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); // 创建SurfaceView surfaceView = new SurfaceView(this); surfaceView.getHolder().addCallback(this); // 设置回调 // 设置窗口参数 WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT, Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, PixelFormat.TRANSLUCENT); // 添加SurfaceView到窗口 windowManager.addView(surfaceView, params); } @Override public void surfaceCreated(SurfaceHolder holder) { // Surface已创建,可以开始绘制 new Thread(() -> { Canvas canvas = holder.lockCanvas(); if (canvas != null) { // 在canvas上绘制内容,例如背景色 canvas.drawColor(Color.BLUE); holder.unlockCanvasAndPost(canvas); } }).start(); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Surface尺寸变化时处理 } @Override public void surfaceDestroyed(SurfaceHolder holder) { // Surface销毁时处理 } @Override public void onDestroy() { super.onDestroy(); if (surfaceView != null) { // 移除SurfaceView windowManager.removeView(surfaceView); } } @Override public IBinder onBind(Intent intent) { return null; } } ``` #### 3. 动态申请权限(针对Android 6.0+) 在启动Service前,检查并申请权限: ```java // 在Activity中 private static final int REQUEST_CODE = 1; private void requestOverlayPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!Settings.canDrawOverlays(this)) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); startActivityForResult(intent, REQUEST_CODE); } else { startService(new Intent(this, OverlayService.class)); } } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Settings.canDrawOverlays(this)) { startService(new Intent(this, OverlayService.class)); } } } } ``` ### 关键点说明 - **窗口类型**:根据Android版本选择`TYPE_APPLICATION_OVERLAY`(Android O+)或`TYPE_SYSTEM_ALERT`(旧版)。 - **绘制线程**:在`surfaceCreated()`中启动新线程进行绘制,避免阻塞主线程。 - **权限处理**:动态申请`SYSTEM_ALERT_WINDOW`权限,否则无法添加窗口。 ### 注意事项 1. **用户可见性**:系统窗口会覆盖在其他应用之上,可能影响用户体验,需谨慎使用。 2. **性能影响**:持续绘制可能增加耗电,建议在不需要时及时停止绘制。 3. **兼容性**:不同Android版本对悬浮窗权限的管理策略不同,需充分测试。 ### 替代方案 如果仅需显示简单视图(如按钮、文字),考虑使用`Toast`或`Snackbar`,但它们在Service中显示受限。对于复杂场景,建议仍使用Activity,通过透明主题实现类似效果。 §§ 相关问题 §§ 1. 如何动态申请`SYSTEM_ALERT_WINDOW`权限? 2. 在Service中显示SurfaceView时,如何实现触摸事件传递? 3. Android不同版本对悬浮窗的限制有哪些变化? 4. 如何优化SurfaceView的绘制性能以减少耗电?</think>在 Android Service 中创建和显示 SurfaceView 需要特殊处理,因为 Service 本身没有 UI 上下文。以下是完整实现方案: ### 核心实现原理 1. **使用系统窗口**:通过 `WindowManager` 添加 SurfaceView 到系统级窗口 2. **前台服务 + 权限**:需要 `SYSTEM_ALERT_WINDOW` 权限和前台服务通知 3. **SurfaceHolder 回调**:处理 Surface 生命周期 ### 完整实现步骤 #### 1. 添加必要权限 (AndroidManifest.xml) ```xml <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application> <service android:name=".OverlayService" /> </application> ``` #### 2. 创建 Service 实现类 ```java public class OverlayService extends Service implements SurfaceHolder.Callback { private SurfaceView surfaceView; private WindowManager windowManager; @Override public void onCreate() { super.onCreate(); showAsOverlay(); startForeground(1, createNotification()); // 前台服务要求 } private void showAsOverlay() { windowManager = (WindowManager) getSystemService(WINDOW_SERVICE); // 创建SurfaceView surfaceView = new SurfaceView(this); surfaceView.getHolder().addCallback(this); // 设置回调监听器 // 设置窗口参数 WindowManager.LayoutParams params = new WindowManager.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, 500, // 高度 Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, PixelFormat.TRANSLUCENT); params.gravity = Gravity.TOP; // 显示在顶部 // 添加到窗口 windowManager.addView(surfaceView, params); } @Override public void surfaceCreated(SurfaceHolder holder) { // Surface已创建,可以开始绘制 new Thread(this::renderContent).start(); } private void renderContent() { Canvas canvas = surfaceView.getHolder().lockCanvas(); if (canvas != null) { try { // 示例:绘制红色背景和文字 canvas.drawColor(Color.RED); Paint paint = new Paint(); paint.setColor(Color.WHITE); paint.setTextSize(48); canvas.drawText("Service SurfaceView", 50, 150, paint); } finally { surfaceView.getHolder().unlockCanvasAndPost(canvas); } } } // 其他回调方法 @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {} @Override public void surfaceDestroyed(SurfaceHolder holder) {} @Override public void onDestroy() { super.onDestroy(); if (windowManager != null && surfaceView != null) { windowManager.removeView(surfaceView); // 必须移除视图 } } private Notification createNotification() { // 创建前台服务通知 return new NotificationCompat.Builder(this, "channel_id") .setContentTitle("SurfaceView Service") .setSmallIcon(R.drawable.ic_notification) .build(); } @Override public IBinder onBind(Intent intent) { return null; } } ``` #### 3. 动态权限申请 (在Activity中) ```java // 检查悬浮窗权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (!Settings.canDrawOverlays(this)) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); startActivityForResult(intent, REQUEST_CODE); } else { startService(new Intent(this, OverlayService.class)); } } ``` ### 关键注意事项 1. **版本兼容性**: - Android 8.0+ 必须使用 `TYPE_APPLICATION_OVERLAY` - 旧版使用 `TYPE_SYSTEM_ALERT` ```java int type = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; ``` 2. **性能优化技巧**: - 在独立线程中进行绘制操作 - 使用双缓冲技术减少闪烁 - 及时调用 `unlockCanvasAndPost()` 3. **触摸事件处理**: ```java surfaceView.setOnTouchListener((v, event) -> { // 处理触摸事件 return true; }); ``` 4. **高级用法(视频播放)**: 结合 MediaPlayer 实现视频播放: ```java public void surfaceCreated(SurfaceHolder holder) { MediaPlayer mediaPlayer = new MediaPlayer(); mediaPlayer.setDisplay(holder); mediaPlayer.setDataSource("video.mp4"); mediaPlayer.prepareAsync(); mediaPlayer.setOnPreparedListener(MediaPlayer::start); } ``` ### 替代方案比较 | **方法** | **优点** | **缺点** | |------------------------|-----------------------------|-----------------------------------| | 系统窗口+SurfaceView | 真正的后台渲染 | 需要特殊权限,系统限制较多 | | 透明Activity | 不需要特殊权限 | 不是真正的后台服务 | | TextureView + Service | 支持动画和变换 | 需要配合Activity使用 | ### 常见问题解决 1. **权限问题**:`SYSTEM_ALERT_WINDOW` 必须在设置中手动开启 2. **Android 12+ 限制**: - 添加 `<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>` - 在服务中检查 `canDrawOverlays()` 3. **SurfaceView 不显示**: - 确保调用了 `unlockCanvasAndPost()` - 检查 WindowManager 参数是否正确 - 确认服务已启动并显示前台通知 > 对于需要复杂交互的场景,建议使用透明 Activity 替代 Service 方案[^2][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值