分类目录归档:Android

Android中常用的设计模式

本文参考来自:《Java设计模式:23种设计模式》

设计原则

在我们日常编程中,常见的面向对象设计原则有以下几大原则,能够帮助在面向对象过程设计出合理的结构。减少对象之间的耦合,方便业务扩展。

  • 开放封闭原则
  • 单一职责原则
  • 里氏替换原则
  • 依赖倒置原则
  • 接口隔离原则
  • 合成复用原则
  • 迪米特法则

开放封闭原则

开闭原则指的是: 软件实体应当对扩展开放,对修改关闭。这里的软件实体可以是:项目中划分出的模块、类与接口、方法。
开放封闭的含义是: 当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。
举个例子: 我们购买商品付款的时候有多种支付渠道如支付宝、微信。付款的流程是一致的,但付款方式是根据不同,我们可以定一个支付接口play();接口内部维护付款流程,外部实现支付接口来实现不同的付款方式,以后提供新的付款方式也不需要修改源代码了,所以他就满足了开放封闭原则。

单一职责原则

单一职责原则:这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。
该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。

Android Studio Markdown 开启实时预览功能

Markdown 插件中明确说明了不支持Android Studio,引用原文:

  • Live HTML preview (except Android Studio, see issue and workaround).

原因是Android Studio 不支持 JCEF,但好在有地方可以设置,参考以下步骤:

  1. Find action (ctrl + shift + A / command + shift + A)
  2. Search for ChooseBoot Java Runtime for the IDE
  3. Select the latest version in the "New:" dropdown – e.g. 11.0.12+7-b1504.27 JetBrains Runtime with JCEF
  4. OK
  5. Restart

Java 等分数组

将一个数组分割成相等的数组。
如将1234567等分以3个为一组等分,结果为[123]、[456]、[7]

实现算法如下

 /**
     * 等分数组
     * @param source 来源
     * @param size 切分数量
     * @param <T> 类型
     * @return 等分后的结果
     */
    public static <T> List<List<T>> divide(List<T> source, int size) {
        if (size <= 0) {
            throw new IllegalArgumentException("等分大小必须大于0");
        }
        if (source == null || source.size() <= 0) {
            return null;
        }
        List<List<T>> result = new ArrayList<>();
        int cursor = 0;
        int total = source.size();
        while (cursor < total) {
            int index = Math.min(cursor + size, total);
            List<T> item = source.subList(cursor, index);
            result.add(item);
            cursor = index;
        }
        return result;
    }

如何修改jar的class文件

在项目中遇到需求想要从已经打包好的jar修改其中的类来扩展功能怎么办?大概分为以下步骤:

  • 反编译jar从class文件拿到Java源码
  • 修改源码后生成class文件
  • 替换class文件
  • 重新打包

1、反编译

执行以下命令解压jar文件

jar -xvf X.jar

反编译jar得到源码

使用工具 jadx-gui,下载地址:https://github.com/skylot/jadx

file

找到要修改的类复制Java源码。

注意:如果不想下载工具可以自己在AndroidStudio里面直接复制源码

2、编译项目

在AndroidStudio 新建项目,修改源码后进行编译。执行assemble编译任务后在/build/intermediates/javac/release/classes 找到生成好的class文件

3、替换class文件重新打包

把class 文件替换回去后,执行以下命名进行打包

cd 到刚才解压的目录里面
jar -cvf YOUR.jar ./

至此已完成新的jar文件。

详情参考文章

Kotlin 编译出现 Internal error: unexpected lint return value -1 的解决方法

在一次Java和Kotlin混合编译时这样的错误:

Execution failed for task ':kotlin:extractDebugAnnotations'.
> A failure occurred while executing com.android.build.gradle.internal.lint.AndroidLintWorkAction
   > Internal error: unexpected lint return value -1

原因排查:去除所有Java的类,一个个类进行编译排查后发现只要引用了import androidx.annotation.xxx; 的注解时都编译失败。

原因分析:kotlin是100%兼容Java的,可能由于编译插件版本导致。

解决方案:

将原来的kotlin gradle插件版本降级。

修改根目录build.gradle 文件:


// 编译失败的配置
plugins {
    // ...省略其他
    id 'org.jetbrains.kotlin.android' version '1.7.0' apply false
}

// 修改后的配置
plugins {
    // ...省略其他
    id 'org.jetbrains.kotlin.android' version '1.6.21' apply false
}

Kotlin 常用配置

Kotlin Parcelable 自动生成配置

code>@Parcelize</code注解框架

plugins {
    id 'kotlin-parcelize'
}
import android.os.Parcelable
import kotlinx.parcelize.Parcelize

@Parcelize
data class VideoBean(var url: String?) : Parcelable

ViewBinding

buildFeatures {
    viewBinding = true
}

MVVM 依赖包


// kotlin coroutines + livedata + view model
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'

Android中使用动态代理巧妙的管理SharedPreferences配置项

在Android应用程序中不少地方会使用SharedPreferences来保存配置文件,这样你就会出现不少下面的写法:


// 程序配置文件
public class MyAppConfig {
    SharedPreferences mSharedPreferences;

    public MyAppConfig(Context context) {
        mSharedPreferences = context.getSharedPreferences("AppConfig", Context.MODE_PRIVATE);
    }

    /**
     * 设置配置1
     */
    public void setConfig1(String value) {
        mSharedPreferences.edit().putString("config1", value).apply();
    }

    /**
     * 获取配置1
     */
    public String getConfig1() {
        return mSharedPreferences.getString("config1", "");
    }

    // ... 省略其他更多的配置项
}

这样一来我们就需要写很多跟SharedPreferences打交道的代码,其实我们关注的只有SET方法GET方法两个方法罢了。那有没有办法,我只需要定义好配置的接口就直接获取到值呢?这就是本篇文章要讨论的啦。其实我们理想效果应该是这样的:


// 定义一个配置接口
public interface ITestConfig {
    // 定义获取配置的Get、Set方法
    void setName(String name);
    String getName();
}

// 实际操作中的调用
private void runTestMethod() {
    // 能实现这样的效果就完美了
    ITestConfig config = XXX.create(context, ITestConfig.class);
    Log.i("rae", "结果:" + config.getName());
}

探索

不妨思考一下,这种需求好像我们在哪个地方见过呢?没错,就是我们经常用到的Retrofit框架中就有用到,我能对public <T> T create(final Class<T> service){}这个方法应该很熟悉了,我们好奇它为什么传入一个接口它就能构造出接口的实例呢?让我们走进源码看看:

 public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }

    // 关键的地方来了:Proxy,动态代理!
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();
          private final Object[] emptyArgs = new Object[0];

          @Override public @Nullable Object invoke(Object proxy, Method method,
              @Nullable Object[] args) throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
          }
        });
  }

就是这个神奇的动态代理。具体什么是动态代理这里就不做解析了,有兴趣的同学可以看看这里介绍的Java动态代理

动手前操作

知道大概的原理之后,我们就开始动手操作了。理一理咱们的思路,定义一个动态代理实现以下功能:

  • 处理我们定义的Get、Set方法,利用SharedPreferences保存配置项
  • 还可以处理一个clear()方法清除配置
  • 还可以处理一个remove(String key) 方法移除配置项
  • 还要处理保存对象类型(利用json字符串来实现)

动态代理的类


/**
 * 应用程序配置注解
 * Created by rae on 2020-02-20.
 * Copyright (c) https://github.com/raedev All rights reserved.
 */
@Documented
@Retention(RUNTIME)
public @interface Config {

    /**
     * 程序配置名称
     */
    String value();

}

/**
 * 应用程序代理类
 * Created by rae on 2020-02-20.
 * Copyright (c) https://github.com/raedev All rights reserved.
 */
public final class AppConfigHandler {

    private AppConfigHandler() {
    }

    /**
     * 创建程序配置代理类
     *
     * @param cls 类的Class
     */
    @SuppressWarnings("unchecked")
    public static <T> T create(Context context, Class<T> cls) {
        Config config = cls.getAnnotation(Config.class);
        if (config == null) {
            throw new RuntimeException("请在配置类标注@Config()");
        }
        if (!cls.isInterface()) {
            throw new RuntimeException("配置类必须是接口");
        }
        String configName = config.value();
        if (TextUtils.isEmpty(configName)) {
            configName = cls.getName();
        }
        SharedPreferences preferences = context.getSharedPreferences(configName, Context.MODE_PRIVATE);
        // 创建动态代理
        return (T) Proxy.newProxyInstance(cls.getClassLoader(), new Class<?>[]{cls}, new ConfigProxy(preferences));
    }

    private static class ConfigProxy implements InvocationHandler {

        private final SharedPreferences mPreference;
        private final Gson mGson = new Gson();

        private ConfigProxy(SharedPreferences preference) {
            this.mPreference = preference;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) {
            String methodName = method.getName().toUpperCase();
            // 清除配置文件
            if (methodName.equalsIgnoreCase("clear")) {
                mPreference.edit().clear().apply();
            }
            // 移除配置项处理
            else if (methodName.equalsIgnoreCase("remove") && args != null) {
                String key = args[0].toString().toUpperCase();
                mPreference.edit().remove(key).apply();
            }
            // Get方法处理
            else if (methodName.startsWith("SET")) {
                setValue(methodName.replace("SET", ""), method, args);
            }
            // Set方法处理
            else if (methodName.startsWith("GET")) {
                return getValue(methodName.replace("GET", ""), method, args);
            }
            // Is方法处理,比如:isLogin()、isVip(),这类的布尔值
            else if (methodName.startsWith("IS")) {
                boolean value = mPreference.getBoolean(methodName.replace("IS", ""), false);
                return value;
            }
            return null;
        }

        /**
         * 设置配置值
         */
        private void setValue(String name, Method method, Object[] args) {
            if (args.length != 1) throw new IllegalArgumentException("set方法的方法参数只允许一个");
            Class<?>[] parameterTypes = method.getParameterTypes();
            Class<?> parameterType = parameterTypes[0];
            Object arg = args[0];
            SharedPreferences.Editor editor = mPreference.edit();
            if (parameterType == String.class) {
                editor.putString(name, (String) arg);
            } else if (parameterType == int.class) {
                editor.putInt(name, (int) arg);
            } else if (parameterType == boolean.class) {
                editor.putBoolean(name, (boolean) arg);
            } else if (parameterType == float.class) {
                editor.putFloat(name, (float) arg);
            } else if (parameterType == long.class) {
                editor.putLong(name, (long) arg);
            } else {
                // 其他值默认使用Json字符串
                String json = mGson.toJson(arg);
                editor.putString(name, json);
            }
            editor.apply();
        }

        /**
         * 获取配置值
         */
        private Object getValue(String name, Method method, Object[] args) {
            Class<?> type = method.getReturnType();
            Object defaultValue = args == null ? null : args[0];
            if (type == String.class) {
                return mPreference.getString(name, (String) defaultValue);
            } else if (type == int.class) {
                return mPreference.getInt(name, defaultValue == null ? 0 : (int) defaultValue);
            } else if (type == boolean.class) {
                return mPreference.getBoolean(name, defaultValue != null && (boolean) defaultValue);
            } else if (type == float.class) {
                return mPreference.getFloat(name, defaultValue == null ? 0 : (float) defaultValue);
            } else if (type == long.class) {
                return mPreference.getLong(name, defaultValue == null ? 0 : (long) defaultValue);
            } else {
                // 其他值默认使用Json字符串
                String json = mPreference.getString(name, null);
                return mGson.fromJson(json, type);
            }
        }
    }
}

实践操作

最终我们定义一个接口,就可以轻松实现读取配置文件啦!

/**
 * 程序配置
 * Created by rae on 2020/2/22.
 * Copyright (c) https://github.com/raedev All rights reserved.
 */
@Config("YourAppConfig")
public interface IAppConfig {

    void setUserName(String name);

    String getUserName(String defaultValue);

    void setVip(boolean isVip);

    boolean isVip();

    void setVersion(int version);

    int getVersion();

    void clear();

    void remove(String key);
}

方法调用

private void runTestMethod() {
    IAppConfig config = AppConfigHandler.create(getApplicationContext(), IAppConfig.class);
    config.clear();
    config.setUserName("RAE");
    config.remove("UserName");
    Log.i("Rae", "username is " + config.getUserName("DefaultValue"));
    config.setVip(true);
    Log.i("Rae", "is vip: " + config.isVip());
    config.setVersion(10);
    Log.i("Rae", "version is  " + config.getVersion());
}

输出结果

I/Rae: username is DefaultValue
I/Rae: is vip: true
I/Rae: version is  10

Android-View篇之启动页倒计时动画的实现

Hello,小伙伴们大家好,今天来实现一个很简单的倒计时动画,仿酷狗音乐的启动页倒计时效果,也是大多数APP在用的一个动画,来看看效果图:

倒计时动画

实现思路

看看是不是很简单,画个圈圈动起来,整体的思路就是用一个平滑的帧动画来画圆弧就行了。

这篇文章学到什么?

  • 了解属性动画ValueAnimator的用法
  • 了解动画属性插值Interpolator,让动画过度得更自然
  • 如何画圆弧

开始准备

新建一个类继承TextView,因为中间还有跳过的文本,所以选择用TextView来画个动起来的背景图。

/**
 * 倒计时文本
 * Created by ChenRui on 2017/10/31 0031 23:01.
 */
public class CountDownTextView extends RaeTextView {
    // 倒计时动画时间
    private int duration = 5000;
    // 动画扫过的角度
    private int mSweepAngle = 360;
    // 属性动画
    private ValueAnimator animator;
    // 矩形用来保存位置大小信息
    private final RectF mRect = new RectF();
    // 圆弧的画笔
    private Paint mBackgroundPaint;

    public CountDownTextView(Context context) {
        super(context);
    }

    public CountDownTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CountDownTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    void init() {
        super.init();
        // 设置画笔平滑
        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        // 设置画笔颜色
        mBackgroundPaint.setColor(Color.WHITE);
        // 设置画笔边框宽度
        mBackgroundPaint.setStrokeWidth(dp2px(2));
        // 设置画笔样式为边框类型
        mBackgroundPaint.setStyle(Paint.Style.STROKE);
    }

开始动画

原理: 利用圆的360度角来做属性动画,让它平滑的分配做每帧动画的角度值,然后调用invalidate() 来重绘自己本身,从而进入到本身的onDraw()方法来画图。

  /**
     * 开始倒计时
     */
    public void start() {
        // 在动画中
        if (mSweepAngle != 360) return;
        //  初始化属性动画
        animator = ValueAnimator.ofInt(mSweepAngle).setDuration(duration);
        // 设置插值
        animator.setInterpolator(new LinearInterpolator());
        // 设置动画监听
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 获取属性动画返回的动画值
                mSweepAngle = (int) animation.getAnimatedValue();
                // 重绘自己
                invalidate();
            }
        });
        // 开始动画
        animator.start();
    }

画圆弧

画圆弧比较简单, 从效果图来看,有的同学可能刚开始以为要画两个圆,一个背景的内圆和一个白色边框的大圆,其实这里可以利用画笔设置画笔样式paint.setStyle()和宽度大小paint.setStrokeWidth()的特性来实现。代码很简单,开始的角度选择-90,从头顶开始画。这样实现的是一个顺时针的倒计时效果。如果你想实现酷狗的逆时针效果,就控制mSweepAngle 的值用mSweepAngle = 360 - mSweepAngle 开始就可以了。

 @Override
    protected void onDraw(Canvas canvas) {
        int padding = dp2px(4);
        mRect.top = padding;
        mRect.left = padding;
        mRect.right = getWidth() - padding;
        mRect.bottom = getHeight() - padding;

        // 画倒计时线内圆
        canvas.drawArc(mRect, //弧线所使用的矩形区域大小
                -90,  //开始角度
                mSweepAngle, //扫过的角度
                false, //是否使用中心
                mBackgroundPaint); // 设置画笔

        super.onDraw(canvas);
    }

什么是插值动画?

为了让动画过度的更加自然或者添加一些动画效果,比如匀速运动、加速运动、减速运动、弹跳运动等等,这些的动画的效果就是靠插值来实现的。在Android中系统内置了一些插值,这里做下搬运工记录一下。推荐一个能在线运行Interpolator的效果以及数学公式定义的网站 http://inloop.github.io/interpolator/ 更加直观的展示下面介绍的动画效果。

插值 说明
LinearInterpolator 以常量速率改变
BounceInterpolator 动画结束的时候弹起
CycleInterpolator 动画循环播放特定的次数,速率改变沿着正弦曲线
DecelerateInterpolator 在动画开始的地方快然后慢
OvershootInterpolator 向前甩一定值后再回到原来位置
AccelerateInterpolator 在动画开始的地方速率改变比较慢,然后开始加速
AnticipateInterpolator 开始的时候向后然后向前甩
AccelerateDecelerateInterpolator 在动画开始与介绍的地方速率改变比较慢,在中间的时候加速
AnticipateOvershootInterpolator 开始的时候向后然后向前甩一定值后返回最后的值

项目使用

这里要定义文本的宽高,因为没有画底部的黑色圆背景,还要设置一下背景图。

  <com.rae.cnblogs.widget.CountDownTextView
            android:id="@+id/tv_skip"
            style="@style/Widget.AppCompat.Button.Borderless"
            android:layout_width="40dp"
            android:layout_height="40dp"
            android:background="@drawable/bg_count_down"
            android:text="跳过"
            android:textColor="#ffffff"
            android:textSize="12sp" />

背景图

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true">
        <shape android:shape="oval">
            <solid android:color="#302d2d2d" />
        </shape>
    </item>
    <item>
        <shape android:shape="oval">
            <solid android:color="#7F2d2d2d" />
        </shape>
    </item>
</selector>

到这里结束啦,希望对你有帮助,本篇文章的源码都在开源的博客园Android客户端这里。