跳至主要内容

[Skill]Android版本兼容器

Android版本兼容器

随着Android版本一代代发布,碎片化的问题越来越严重,不过好在趋势上市面上的版本已经开始比较集中了。但我们终究还是要面对版本兼容问题。我们不能因为要用高版本方法而提高最低版本限制,高版本里炫酷的效果及高效的方法只会导致你的最低版本显示越来越高,而官方的解决方案(Support-v4)无疑是给了我们新的启示。

示例

首先我们抽取一个官方的版本兼容器的一部分看看:
/**
 * Helper for accessing features in {@link TextView} introduced after API level
 * 4 in a backwards compatible fashion.
 */
public final class TextViewCompat {

    // Hide constructor
    private TextViewCompat() {}

    interface TextViewCompatImpl {
        ...
        int getMaxLines(TextView textView);
        int getMinLines(TextView textView);
        void setTextAppearance(@NonNull TextView textView, @StyleRes int resId);
        Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView);
    }

    static class BaseTextViewCompatImpl implements TextViewCompatImpl {
        ...
        @Override
        public int getMaxLines(TextView textView) {
            return TextViewCompatGingerbread.getMaxLines(textView);
        }

        @Override
        public int getMinLines(TextView textView) {
            return TextViewCompatGingerbread.getMinLines(textView);
        }

        @Override
        public void setTextAppearance(TextView textView, @StyleRes int resId) {
            TextViewCompatGingerbread.setTextAppearance(textView, resId);
        }

        @Override
        public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
            return TextViewCompatGingerbread.getCompoundDrawablesRelative(textView);
        }
    }

    static class JbTextViewCompatImpl extends BaseTextViewCompatImpl {
        @Override
        public int getMaxLines(TextView textView) {
            return TextViewCompatJb.getMaxLines(textView);
        }

        @Override
        public int getMinLines(TextView textView) {
            return TextViewCompatJb.getMinLines(textView);
        }
    }

    static class JbMr1TextViewCompatImpl extends JbTextViewCompatImpl {
        ...
        @Override
        public Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
            return TextViewCompatJbMr1.getCompoundDrawablesRelative(textView);
        }
    }

    static class JbMr2TextViewCompatImpl extends JbMr1TextViewCompatImpl {
        ...
        @Override
        public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView,
                @DrawableRes int start, @DrawableRes int top, @DrawableRes int end,
                @DrawableRes int bottom) {
            TextViewCompatJbMr2.setCompoundDrawablesRelativeWithIntrinsicBounds(textView,
                    start, top, end, bottom);
        }
    }

    static class Api23TextViewCompatImpl extends JbMr2TextViewCompatImpl {
        @Override
        public void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
            TextViewCompatApi23.setTextAppearance(textView, resId);
        }
    }

    static final TextViewCompatImpl IMPL;

    static {
        final int version = Build.VERSION.SDK_INT;
        if (version >= 23) {
            IMPL = new Api23TextViewCompatImpl();
        } else if (version >= 18) {
            IMPL = new JbMr2TextViewCompatImpl();
        } else if (version >= 17) {
            IMPL = new JbMr1TextViewCompatImpl();
        } else if (version >= 16) {
            IMPL = new JbTextViewCompatImpl();
        } else {
            IMPL = new BaseTextViewCompatImpl();
        }
    }

    ...
    public static int getMaxLines(@NonNull TextView textView) {
        return IMPL.getMaxLines(textView);
    }

    public static int getMinLines(@NonNull TextView textView) {
        return IMPL.getMinLines(textView);
    }

    public static void setTextAppearance(@NonNull TextView textView, @StyleRes int resId) {
        IMPL.setTextAppearance(textView, resId);
    }

    public static Drawable[] getCompoundDrawablesRelative(@NonNull TextView textView) {
        return textView.getCompoundDrawables();
    }
}

分析

首先private私有化构造函数,避免开发者去new一个出来;然后定义接口,其包含配套的需要做版本兼容的方法;接着实现基础的类来实现这些接口,再就是根据版本断层来重写这一基础类,重写这些类以达到高版本使用高级方法,在就是通过静态代码块来根据不同系统版本实例化一个兼容接口,最后用静态方法来调用这一接口达到版本兼容的效果。 官方的兼容器习惯于使用另外的一个类的静态方法来实现具体功能,好处是版本兼容器只管版本兼容不管具体实现,使代码更清晰,但我们可以直接简化到该重写方法内部实现即可。有些低版本上无法实现高版本方法的效果,但有些可以以曲线救国的方式来达到。所以,使用版本控制器还是要注意低版本上是不做任何处理还是实现了同样的效果。 官方支持库内提供了很多版本兼容器:ViewCompat、ViewGroupCompat、ViewConfigurationCompat、ViewParentCompat等等,基本使用Compat结尾命名。

使用

载入支持库后,我们可以直接使用这些版本兼容器,但是官方也并非面面俱到得提供了所有方法的版本兼容器。但我们可以套用这种模式实现自己的版本兼容器。 下面列出一个WebView的版本兼容器:
public class WebViewCompat {
    private WebViewCompat() {
    }

    /**
     * A callback interface used to provide values asynchronously.
     */
    public interface ValueCallbackCompat {
        /**
         * Invoked when the value is available.
         *
         * @param value The value.
         */
        void onReceiveValue(String value);
    }

    private interface WebViewCompatImpl {
        boolean isSupportEvaluateJavascript();

        void evaluateJavascript(WebView webView, String script, ValueCallbackCompat callback);
    }

    private static class BaseWebViewCompatImpl implements WebViewCompatImpl {
        @Override
        public boolean isSupportEvaluateJavascript() {
            return false;
        }

        @Override
        public void evaluateJavascript(WebView webView, String script, ValueCallbackCompat callback) {
            // do nothing
        }
    }

    @TargetApi(19)
    private static class KitKatWebViewCompatImpl extends BaseWebViewCompatImpl implements ValueCallback<String> {

        private WeakReference<ValueCallbackCompat> callbackCompatWeakReference;

        @Override
        public boolean isSupportEvaluateJavascript() {
            return true;
        }

        @Override
        public void evaluateJavascript(WebView webView, String script, ValueCallbackCompat callback) {
            callbackCompatWeakReference = new WeakReference<>(callback);
            webView.evaluateJavascript(script, this);
        }

        @Override
        public void onReceiveValue(String value) {
            try {
                callbackCompatWeakReference.get().onReceiveValue(value);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static final WebViewCompatImpl IMPL;

    static {
        final int version = android.os.Build.VERSION.SDK_INT;
        if (version >= 19) {
            IMPL = new KitKatWebViewCompatImpl();
        } else {
            IMPL = new BaseWebViewCompatImpl();
        }
    }

    public static boolean isSupportEvaluateJavascript() {
        return IMPL.isSupportEvaluateJavascript();
    }

    public static void evaluateJavascript(WebView webView, String script, ValueCallbackCompat callback) {
        IMPL.evaluateJavascript(webView, script, callback);
    }
}
值得注意的是我这里定义了另一个回调接口,因为我将版本限制放置在4,而ValueCallback是在API 7才引入,所以在最低版本限制在7以上的,也就可以直接使用ValueCallback,不需要重新定义另一个接口了。 @TargetApi(int)用于避免编译器报错 一旦用到android.os.Build.VERSION.SDK_INT,那么版本限制就一定要放在4了,从4开始才有这个参数。 最后再给出一个我用该方式构建的一个控件ShapeImageView,高低版本呈现的效果一致,但是使用的是不同的方式。

评论

此博客中的热门博文

[Widget]ShapeImageView-图形裁剪ImageView

ShapeImageView 图形裁剪ImageView,API 21 及以上 使用 setOutlineProvider 方式实现,支持动态图;以下使用 BitmapShader 方式实现。 支持固定高宽缩放比缩放,支持前景Drawable,支持ImageView的所有ScaleType,且API 21具备更高性能。 预览 要求 minSdkVersion 4 链接 Github Bintray 使用 基本布局 < am .widget.shapeimageview.ShapeImageView android : id = " @+id/siv_image_c " android : layout_width = " match_parent " android : layout_height = " wrap_content " android : layout_margin = " 5dp " android : layout_weight = " 1 " android : clickable = " true " android : scaleType = " centerCrop " android : src = " @drawable/bg_welcome " app : sivBorderColor = " @color/colorAccent " app : sivBorderWidth = " 2dp " app : sivForeground = " @drawable/bg_common_press_dark " app : sivHeightScale = " 1 " app : sivScaleTarget = " height " app : sivShape = &q

[Widget]StateFrameLayout-状态帧布局

StateFrameLayout icon 一般网络交互的状态提示及处理大多数情况下考虑使用Dialog,在一切状态处理理想状态下时,使用Dialog进行交互是可行的。但稍微一不注意,使用Dialog则会出现一系列隐藏的Bug。为节省用户时间怎加体验感觉,数据的载入可以在onCreate时候就进行,甚至可以在Activity构造函数里面启动网络请求,因为Activity还没有建立窗口(onAttachedToWindow),而Dialog必须附着在Activity的Window上,显然这时候不能弹出Dialog;网络交互并非即时,也就是在交互过程中用户可能进行任何操作,多数情况下,应用并不允许用户中断网络交互,而将Dialog设置为不可取消的话,用户体验是很差的,因为你同时阻止了用户退出当前Activity的操作,若用户仅仅是误点了进来,那么必须等待交互结束才能退出,而如果不讲Dialog设置为不可取消的话,那么用户进行了取消操作,但实际是并没有取消,这又会让用户很困惑,如果交互是更新当前页面的数据,当用户取消以后就可以进行旧数据操作,但其实这时候数据已过时,操作是不应该的;当网络交互已完成时,若交互结果需要告知用户时,此时又得注意Activity的状态,也许Activity已经关闭了Window(用户进行了返回操作,Activity在销毁;或者用户点按了Home键,设备内存不够,Activity在进行保存并关闭Window)。操控好Window,则使用Dialog并无任何问题,但是这就会怎加代码复杂度。其实我们的目的就是告知用户在进行网络请求,阻止用户对未载入或旧页面进行操作,网络交互结束后有必要时告知用户;使用StateFrameLayout则能轻松达到效果。 状态帧布局,通常用于网络请求的四种状态,普通、载入、错误、空白。支持Drawable或者View来展示,也可以混搭。 预览 screenshots 要求 minSdkVersion 4 链接 Github Bintray 使用 基本布局 < am .widget.stateframelayout.StateFrameLayout xmlns : app = " http://schemas.androi

[Skill]URLConnection从HTTP重定向到HTTPS

URLConnection从HTTP重定向到HTTPS   也不知什么原因,公司项目的服务端一直在吸引着大波攻击,于是服务端的同学打算把所有HTTP的请求都换为HTTPS,他们决定兼容旧版本于是就将之前的所有HTTP请求全部重定向到另一个HTTPS请求。 项目请求框架搭建初期,考虑到应用也不会使用太复杂的请求模式,于是就简单使用URLConnection完成服务端交互。服务端一修改,全部请求都失败了。虽然URLConnection有是否遵循重定向开关(setInstanceFollowRedirects),其默认就是开启的,即便你再强制其打开,也是没有用,问题依旧。找了大量资料,其实问题的关键点不是重定向而是从HTTP重定向到HTTPS,关键点就在URLConnection的两个子类上。 HttpURLConnection与HttpsURLConnection   HttpURLConnection为URLConnection的子类,而HttpsURLConnection为HttpURLConnection的子类,在HttpURLConnection基础上对HTTPS进行支持。 URLConnection通常使用URL的openConnection()方法获得,而URL是根据其是否为Https开头来打开一个HttpURLConnection还是HttpsURLConnection。 而当URLConnection进行connect()时,遇到了重定向,如果打开了遵循重定向,那么其会获取重定向的地址,然后尝试连接这个地址。值得注意的是,这时候并不是使用新的链接地址重新openConnection()一个URLConnection,而是直接尝试连接这个重定向的地址,否则也就不存在以上的Bug了。 于是理论上分析,HTTP重定向到HTTP是不存在问题的,HTTPS重定向到HTTPS也是不存在问题的,而HTTP与HTTPS之间的重定向,那么就很可能会有问题了。HTTP重定向到HTTPS,URLConnection会将重定向的HTTPS以HTTP方式继续提交,那么服务端肯定是认为你是错误的提交方式;同理,HTTPS重定向HTTP也一样。 问题解决 使用URLConnection抓取到重定向,就使用重定向的地址重新人为openConnection()一个新