大势所趋,应用如何适配Android P HEIF图片格式

介绍HEIF图片格式的优势,包括更高的压缩效率、支持多张图片和动态图片等特性,并提供了在Android平台上适配HEIF格式的指导建议。

谷歌公布,Android P 版本支持HEIF格式图片。HEIF 全称为 High Efficiency Image Format,即高效率图片格式。相比于 JPEG 等目前互联网通用的图片格式,HEIF 图片有何价值?在HEIF图片的适配过程中,应用会遭遇哪些典型性问题?该如何进行适配?

一. 什么是HEIF图片

1.HEIF(High Efficiency Image Format),即高效率图片格式。2015年由 ISO 批准发布,由 MPEG 标准组织制定,采用 HEVC 编码格式。

2.苹果自2017.9.20发布的 iOS11 开始拍照保存支持HEIF格式(iPhone 7及以上机型)。

3.谷歌 O 版本 MR1 支持 HEIF 静态图的软件解码,Android P 支持 HEIF 软件解码、软件编码。

二. HEIF图片价值:

1.与 JPEG 同等图片质量的压缩率是 JPEG 的2.39倍,可节省约50%空间,节省网络传输流量。

2.支持存储多张图片(图片集合、序列图等,如连拍)。

3.支持动态图片(类似 Gif 动图)。

4.支持图片深度信息、透明度信息。

目标:替代 JPEG,成为主流图片类型

三. 适配HEIF图片格式典型问题

1.在支持 HEIF 编解码的手机无法打开HEIF格式文件。

2.尽管手机支持 HEIF 编解码,应用仍无法扫描到用户手机本地的HEIF格式图片,无法分享或者发送本地 HEIF 格式图片给其他用户。

3.将 HEIF 格式图片,通过支持 HEIF 编解码的手机发送到不支持 HEIF 编解码的手机,会出现图片无法查看的问题。

四. 适配指导

1.判断手机是否支持 HEIF 格式

谷歌未提供统一接口,推荐应用通过手机系统版本来判断是否支持 HEIF 格式编解码。目前,Android O 版本 MR1 支持 HEIF 静态图的软件解码,Android P 支持 HEIF 软件解码、软件编码。

以华为 Android P 版本手机HEIF格式编解码为例,参考实现代码

tic boolean isSupportHeif() {
   Log.e(TAG, "Build.MANUFACTURER:" + Build.MANUFACTURER + ", Build.VERSION.SDK_INT:" + Build.VERSION.SDK_INT);
if("HUAWEI".equals(Build.MANUFACTURER) && Build.VERSION.SDK_INT>27) {
        return true;
    }
    return false;
}
复制代码

2.如何读取和显示HEIF图片文件

①通过 Android P 版本新增的类 ImageDecoder 加载图片。

参考实现代码

public static Drawable getHeifImageFromSdcardUseImageDecoder(String path) throws IOException {
    File file = new File(path);
    ImageDecoder.Source source = ImageDecoder.createSource(file);
    return ImageDecoder.decodeDrawable(source);
}

mImageHeif = findViewById(R.id.imageView2);
if (HeifUtils.isSupportHeif()) {
    try {
        Drawable drawble = HeifUtils.getHeifImageFromSdcardUseImageDecoder("/sdcard/bird_burst.heic");
        mImageHeif.setImageDrawable(drawble);
        textHeif.setText("load heif image use ImageDecoder");
    } catch (IOException e) {
        e.printStackTrace();
    }
}
复制代码

显示效果:

②通过历史版本已有类 BitmapFactory 加载图片。

该方法工作量最小,只需放开 Android P 版本手机 HEIF 图片文件加载,无需修改图片加载代码。需要注意的是,应用需默认支持 HEIF 图片的加载查看。

参考实现代码

public static Bitmap getHeifImageFromSdcardUseBitmapFactory(String path) {
    return BitmapFactory.decodeFile(path);
}

if (HeifUtils.isSupportHeif()) {
    Bitmap bmp = HeifUtils.getHeifImageFromSdcardUseBitmapFactory("/sdcard/bird_burst.heic");
    mImageHeif.setImageBitmap(bmp);
    textHeif.setText("load heif image use BitmapFactory");
}
复制代码

显示效果:

3.图片扫描推荐方式

方式一(推荐):通过 ContentProvider 扫描

实现本地图片的上传、分享或者是发送功能,需要扫描手机本地的图片。如果应用通过自身扫描的方式,需根据手机版本判断手机支持的图片编解码格式,所以推荐使用 ContentProvider 扫描手机中的图片。通过这种方法扫描,系统会将支持的所有解码格式的图片文件返回给应用,不需要应用自身再去做格式判断。

参考实现代码

public static void getAllImagesLocal(final Context context) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            Cursor cursor = context.getContentResolver().query(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);
            while (cursor.moveToNext()) {
                String path = cursor.getString(cursor
                        .getColumnIndex(MediaStore.Images.Media.DATA));
                Log.e(TAG, "image path:" + path);
            }
        }
    }).start();
}
复制代码

通过抓取日志,能够看到应用如果在支持 HEIF 编解码的手机执行,系统会将HEIF格式图片扫描结果返回给应用,如果在不支持 HEIF 格式的手机执行,系统则不会将 HEIF 格式图片扫描结果返回。

支持 HEIF 格式的手机执行抓取的日志

Line 4653: 06-09 11:14:37.976 17127 17173 E HeifUtils: image path:/storage/emulated/0/Pictures/autumn_1440x960.heic
	Line 4654: 06-09 11:14:37.976 17127 17173 E HeifUtils: image path:/storage/emulated/0/Pictures/old_bridge_1440x960.heic
	Line 4655: 06-09 11:14:37.976 17127 17173 E HeifUtils: image path:/storage/emulated/0/tencent/MicroMsg/wallet/luckyMoneyEffect/1/package/input.png
	Line 4656: 06-09 11:14:37.976 17127 17173 E HeifUtils: image path:/storage/emulated/0/tencent/MicroMsg/wallet/luckyMoneyEffect/1/package/list.png
	Line 4657: 06-09 11:14:37.976 17127 17173 E HeifUtils: image path:/storage/emulated/0/tencent/MicroMsg/wallet/luckyMoneyEffect/1/package/cover.png
	Line 4658: 06-09 11:14:37.976 17127 17173 E HeifUtils: image path:/storage/emulated/0/tencent/MicroMsg/wallet/luckyMoneyEffect/1/package/chat.png
	Line 4659: 06-09 11:14:37.976 17127 17173 E HeifUtils: image path:/storage/emulated/0/bird_burst.heic
	Line 4660: 06-09 11:14:37.976 17127 17173 E HeifUtils: image path:/storage/emulated/0/Hydrangeas.jpg
	Line 4661: 06-09 11:14:37.976 17127 17173 E HeifUtils: image path:/storage/emulated/0/Download/bird_burst.heic
	Line 4662: 06-09 11:14:37.976 17127 17173 E HeifUtils: image path:/storage/emulated/0/Download/Hydrangeas.jpg
复制代码

方式二(不推荐):应用自身进行扫描

应用通过自身扫描,判断哪些是可支持的图片文件,需要增加对 HEIF 格式的判断, HEIF 格式文件的后缀有两种:.heif和.heic

4.HEIF 格式转 JPEG

使用场景:

HEIF 格式转成 JPEG 格式主要考虑向下兼容的问题。通过支持 HEIF 格式的手机向不支持此格式的手机发送 HEIF 图片,如果直接发送原始 HEIF 格式,会出现图片无法使用的情况。因此,为了避免这种情况,可以考虑在发送之前将 HEIF 格式图片转换成 JPEG 格式再发送。应用可以选择使用谷歌原生的接口来进行转换。

参考实现代码

try {
    Bitmap bmp = HeifUtils.getHeifImageFromSdcardUseBitmapFactory("/sdcard/bird_burst.heic");
    File file = new File("/sdcard/download/bird_burst1.jpg");
    FileOutputStream out = new FileOutputStream(file);
    bmp.compress(Bitmap.CompressFormat.JPEG, 100, out);
    out.flush();
    out.close();
    Uri uri = Uri.fromFile(file);
    sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri));
} catch (Exception e) {
    e.printStackTrace();
}
复制代码

HEIF 图片格式的诸多优势使它具有很好的市场前景,这次谷歌 Android P 版本对此格式的支持也充分说明了这一点,建议各位广大开发者赶紧适配。

关注“安卓绿色联盟”公众号回复关键词“申请”,了解华为终端开放实验室免费云测申请流程。

package com.example.tigongzhe; import android.R.integer; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.provider.SyncStateContract.Helpers; import android.text.Selection; import android.util.Log; public class provider extends ContentProvider { private MyOpenHelper myOpenHelper; private SQLiteDatabase sqLiteDatabase; private static final UriMatcher URI_MATCHER=new UriMatcher(UriMatcher.NO_MATCH); private final String TAG="provider"; private static final String authority="com.example.tigongzhe.provider"; static { URI_MATCHER.addURI(authority, "contacter", 1); URI_MATCHER.addURI(authority, "contacter/#", 2); } private static final String _id="id"; private static final String name="name"; private static final String num="num"; @Override public boolean onCreate() { // TODO Auto-generated method stub myOpenHelper=new MyOpenHelper(getContext(), DB_Name, null, version_1); return true; } @Override public String getType(Uri uri) { // TODO Auto-generated method stub int flag=URI_MATCHER.match(uri); switch (flag) { case 2: return "vnd.android.cursor.item/contacter"; case 1: return "vnd.android.dir.item/contacter"; default: throw new IllegalArgumentException("异常参数"); } } @Override public Uri insert(Uri uri, ContentValues values) { // TODO Auto-generated method stub sqLiteDatabase=myOpenHelper.getWritableDatabase(); int flag=URI_MATCHER.match(uri); switch (flag) { case 1: sqLiteDatabase.insert(Table_Name, name, values); break; case 2: long id=sqLiteDatabase.insert(Table_Name, name, values); ContentUris.withAppendedId(uri, id); default: break; } return uri; } @Override public Cursor query(Uri uri, String[] arg1, String arg2, String[] arg3, String arg4) { // TODO Auto-generated method stub Cursor cursor; sqLiteDatabase=myOpenHelper.getReadableDatabase(); int flag=URI_MATCHER.match(uri); switch (flag) { case 1: cursor=sqLiteDatabase.query(Table_Name, arg1, arg2, arg3, null, null,arg4); break; case 2: long id=ContentUris.parseId(uri); arg2=(arg2==null||"".equals(arg2.trim()))? _id+"="+id:arg2+"and"+_id+"="+id; cursor=sqLiteDatabase.query(Table_Name, arg1, arg2, arg3, null, null,arg4); default: throw new IllegalArgumentException("参数错误"); } return cursor; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO Auto-generated method stub int num=0; sqLiteDatabase=myOpenHelper.getWritableDatabase(); int flag=URI_MATCHER.match(uri); switch (flag) { case 1: num=sqLiteDatabase.update(Table_Name, values,selection, selectionArgs); break; case 2: long id=ContentUris.parseId(uri); selection=(selection==null||"".equals(selection.trim()))? _id+"="+id:selection+"and"+_id+"="+id; num=sqLiteDatabase.update(Table_Name, values,selection, selectionArgs); default: break; } return num; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // TODO Auto-generated method stub int num=0; sqLiteDatabase=myOpenHelper.getWritableDatabase(); int flag=URI_MATCHER.match(uri); switch (flag) { case 1: num=sqLiteDatabase.delete(Table_Name, selection, selectionArgs); break; case 2: long id=ContentUris.parseId(uri); selection=(selection==null||"".equals(selection.trim()))?_id+"="+id:selection+"and"+_id+"="+id; num=sqLiteDatabase.delete(Table_Name, selection, selectionArgs); default: throw new IllegalArgumentException("异常参数"); } return num; } private final String DB_Name = "mydb.db"; private final String Table_Name="contacter"; private final int version_1=1; private class MyOpenHelper extends SQLiteOpenHelper { public MyOpenHelper(Context context, String name, CursorFactory factory, int version) { super(context, name, factory, version); } /** * @description 当数据表无连接时创建新的表 */ @Override public void onCreate(SQLiteDatabase db) { String sql = " create table if not exists " + Table_Name + "(id INTEGER PRIMARY KEY AUTOINCREMENT," + "name varchar(64),num varchar(64))"; db.execSQL(sql); } /** * @description 当版本更新时触发的方法 */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { String sql = " drop table if exists " + Table_Name; db.execSQL(sql); onCreate(db); } } } +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.tigongzhe" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="18" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="com.example.tigongzhe.MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <provider android:name=".provider" android:authorities="com.example.tigongzhe.provider" android:multiprocess="true" android:exported="true" android:permission="com.example.tigongzhe.permission" ></provider> </application> <permission android:name="com.example.tigongzhe.permission" android:protectionLevel="normal"></permission> </manifest> ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++package com.example.tigongzhe; import android.R.integer; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; import android.net.Uri; import android.provider.SyncStateContract.Helpers; import android.text.Selection; import android.util.Log; public class provider extends ContentProvider { private MyOpenHelper myOpenHelper; private SQLiteDatabase sqLiteDatabase; private static final UriMatcher URI_MATCHER=new UriMatcher(UriMatcher.NO_MATCH); private final String TAG="provider"; private static final String authority="com.example.tigongzhe.provider"; static { URI_MATCHER.addURI(authority, "contacter", 1); URI_MATCHER.addURI(authority, "contacter/#", 2); } private static final String _id="id"; private static final String name="name"; private static final String num="num"; @Override public boolean onCreate() { // TODO Auto-generated method stub myOpenHelper=new MyOpenHelper(getContext(), DB_Name, null, version_1); return true; } @Override public String getType(Uri uri) { // TODO Auto-generated method stub int flag=URI_MATCHER.match(uri); switch (flag) { case 2: return "vnd.android.cursor.item/contacter"; case 1: return "vnd.android.dir.item/contacter"; default: throw new IllegalArgumentException("异常参数"); } } @Override public Uri insert(Uri uri, ContentValues values) { // TODO Auto-generated method stub sqLiteDatabase=myOpenHelper.getWritableDatabase(); int flag=URI_MATCHER.match(uri); switch (flag) { case 1: sqLiteDatabase.insert(Table_Name, name, values); break; case 2: long id=sqLiteDatabase.insert(Table_Name, name, values); ContentUris.withAppendedId(uri, id); default: break; } return uri; } @Override public Cursor query(Uri uri, String[] arg1, String arg2, String[] arg3, String arg4) { // TODO Auto-generated method stub Cursor cursor; sqLiteDatabase=myOpenHelper.getReadableDatabase(); int flag=URI_MATCHER.match(uri); switch (flag) { case 1: cursor=sqLiteDatabase.query(Table_Name, arg1, arg2, arg3, null, null,arg4); break; case 2: long id=ContentUris.parseId(uri); arg2=(arg2==null||"".equals(arg2.trim()))? _id+"="+id:arg2+"and"+_id+"="+id; cursor=sqLiteDatabase.query(Table_Name, arg1, arg2, arg3, null, null,arg4); default: throw new IllegalArgumentException("参数错误"); } return cursor; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // TODO Auto-generated method stub int num=0; sqLiteDatabase=myOpenHelper.getWritableDatabase(); int flag=URI_MATCHER.match(uri); switch (flag) { case 1: num=sqLiteDatabase.update(Table_Name, values,selection, selectionArgs); break; case 2: long id=ContentUris.parseId(uri); selection=(selection==null||"".equals(selection.trim()))? _id+"="+id:selection+"and"+_id+"="+id; num=sqLiteDatabase.update(Table_Name, values,selection, selectionArgs); default: break; } return num; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // TODO Auto-generated method stub int num=0; sqLiteDatabase=myOpenHelper.getWritableDatabase(); int flag=URI_MATCHER.match(uri); switch (flag) { case 1: num=sqLiteDatabase.delete(Table_Name, selection, selectionArgs); break; case 2: long id=ContentUris.parseId(uri); selection=(selection==null||"".equals(selection.trim()))?_id+"="+id:selection+"and"+_id+"="+id; num=sqLiteDatabase.delete(Table_Name, selection, selectionArgs); default: throw new IllegalArgumentException("异常参数"); } return num; } private final String DB_Name = "mydb.db"; private final String Table_Name="contacter"; private final int version_1=1; private class MyOpenHelper extends SQLiteOpenHelper { public MyOpenHelper(Context context, String name, CursorFactory factory, int version) { super(context, name, factory, version); } /** * @description 当数据表无连接时创建新的表 */ @Override public void onCreate(SQLiteDatabase db) { String sql = " create table if not exists " + Table_Name + "(id INTEGER PRIMARY KEY AUTOINCREMENT," + "name varchar(64),num varchar(64))"; db.execSQL(sql); } /** * @description 当版本更新时触发的方法 */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { String sql = " drop table if exists " + Table_Name; db.execSQL(sql); onCreate(db); } } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值