分类目录归档: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-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客户端这里。

Android-View篇之自定义验证码输入框

首先,我们来看看实现的是怎么样的效果:

验证码输入框效果图

如果我们拿到这样的UI,想到的布局应该是用4个EditText包在横向的LinearLayout里面,但今天要讲的View,所以我们决定用一个自定义的EditText 画出来。

学到什么?

  • 基本理解画布概念
  • 画布的状态、平移
  • 布局测量
  • 画图片

功能需求

  • 高亮当前输入框
  • 输入满4个数字自动调用方法

思路

完全重画一个EditText,就包含了测量布局重新绘制这两个关键步骤。好了,到这里理一下整体的思路:

  • 根据验证码个数以及边框大小来计算输入框显示的宽度
  • 覆盖原来的EditText画布,重新绘制方框
  • 根据输入的索引来确定高亮的方框
  • 重写onTextChanged 但满足验证码个数的时候调用自动完成方法

开始动手

准备开始了,果断继承一个AppCompatEditText 来初始化基本参数先:

  • 验证码个数
  • 输入方框的大小
  • 边框的大小及间距

/**
 * 验证码输入框,重写EditText的绘制方法实现。
 * @author RAE
 */
public class CodeEditText extends AppCompatEditText {

   //  验证码文本颜色
    private int mTextColor;
    // 输入的最大长度
    private int mMaxLength = 4;
    // 边框宽度
    private int mStrokeWidth;
    // 边框高度
    private int mStrokeHeight;
    // 边框之间的距离
    private int mStrokePadding = 20;
    // 用矩形来保存方框的位置、大小信息
    private final Rect mRect = new Rect();
    // 方框的背景
    private Drawable mStrokeDrawable;

   /**
     * 构造方法
     *
     */
    public CodeEditText(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CodeEditText);
        int indexCount = typedArray.getIndexCount();
        for (int i = 0; i < indexCount; i++) {
            int index = typedArray.getIndex(i);
            if (index == R.styleable.CodeEditText_strokeHeight) {
                this.mStrokeHeight = (int) typedArray.getDimension(index, 60);
            } else if (index == R.styleable.CodeEditText_strokeWidth) {
                this.mStrokeWidth = (int) typedArray.getDimension(index, 60);

            } else if (index == R.styleable.CodeEditText_strokePadding) {
                this.mStrokePadding = (int) typedArray.getDimension(index, 20);

            } else if (index == R.styleable.CodeEditText_strokeBackground) {
                this.mStrokeDrawable = typedArray.getDrawable(index);

            } else if (index == R.styleable.CodeEditText_strokeLength) {
                this.mMaxLength = typedArray.getInteger(index, 4);
            }
        }
        typedArray.recycle();

        if (mStrokeDrawable == null) {
            throw new NullPointerException("stroke drawable not allowed to be null!");
        }

        setMaxLength(mMaxLength);
        setLongClickable(false);
        // 去掉背景颜色
        setBackgroundColor(Color.TRANSPARENT);
        // 不显示光标
        setCursorVisible(false);
    }

    @Override
    public boolean onTextContextMenuItem(int id) {
        return false;
    }

   /**
     * 设置最大长度
     */
    private void setMaxLength(int maxLength) {
        if (maxLength >= 0) {
            setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
        } else {
            setFilters(new InputFilter[0]);
        }
    }
}

开始测量布局

初始化完了就要开始测量布局了,计算公式为:

输入框宽度 = 边框宽度 数量 + 边框间距 (数量-1)


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 当前输入框的宽高信息
        int width = getMeasuredWidth();
        int height = getMeasuredHeight();
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        // 判断高度是否小于推荐高度
        if (height < mStrokeHeight) {
            height = mStrokeHeight;
        }

        // 输入框宽度 = 边框宽度 * 数量 + 边框间距 *(数量-1)
        int recommendWidth = mStrokeWidth * mMaxLength + mStrokePadding * (mMaxLength - 1);
        // 判断宽度是否小于推荐宽度
        if (width < recommendWidth) {
            width = recommendWidth;
        }

        widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, widthMode);
        heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, heightMode);
        // 设置测量布局
        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }

画家登场

来到最重要的步骤了,重画输入框!来一步步看代码注释:

    @Override
    protected void onDraw(Canvas canvas) {
        // 在画支持设置文本颜色,把系统化的文本透明掉,相当于覆盖
        mTextColor = getCurrentTextColor();
        setTextColor(Color.TRANSPARENT);
        //  系统画的方法
        super.onDraw(canvas);
        // 重新设置文本颜色
        setTextColor(mTextColor);
        // 重绘背景颜色
        drawStrokeBackground(canvas);
        // 重绘文本
        drawText(canvas);
    }

绘制背景方框


    /**
     * 绘制方框
     */
    private void drawStrokeBackground(Canvas canvas) {
        // 下面绘制方框背景颜色
        // 确定反馈位置
        mRect.left = 0;
        mRect.top = 0;
        mRect.right = mStrokeWidth;
        mRect.bottom = mStrokeHeight;
        int count = canvas.getSaveCount(); //  当前画布保存的状态
        canvas.save(); // 保存画布
        for (int i = 0; i < mMaxLength; i++) {
            mStrokeDrawable.setBounds(mRect); // 设置位置
            mStrokeDrawable.setState(new int[]{android.R.attr.state_enabled}); // 设置图像状态
            mStrokeDrawable.draw(canvas); //  画到画布上
            //  确定下一个方框的位置
            float dx = mRect.right + mStrokePadding; // X坐标位置
            // 保存画布
            canvas.save();
            // [注意细节] 移动画布到下一个位置
            canvas.translate(dx, 0);
        }
        // [注意细节] 把画布还原到画反馈之前的状态,这样就还原到最初位置了
        canvas.restoreToCount(count);
        // 画布归位
        canvas.translate(0, 0);

        // 下面绘制高亮状态的边框
        // 当前高亮的索引
        int activatedIndex = Math.max(0, getEditableText().length());
        mRect.left = mStrokeWidth * activatedIndex + mStrokePadding * activatedIndex;
        mRect.right = mRect.left + mStrokeWidth;
        mStrokeDrawable.setState(new int[]{android.R.attr.state_focused});
        mStrokeDrawable.setBounds(mRect);
        mStrokeDrawable.draw(canvas);

    }

一般画布的移动canvas.translate(x,y)会结合canvas.save();来使用。
1、调用canvas.save();保存当前画布的状态,用PS来解析就是按下ctrl +s键,然后帮你新建一个新的图层。你之后画的内容不会影响到之前画的内容,要回到之前的状态就调用canvas.restoreToCount(count)来还原。
2、把画布的位置移到下一个位置canvas.translate(x,y),下图所示,你会发现方框在画布中的位置没有发生变化而是画布距离发生了变化。这就是画布平移的效果了。

画布平移

画验证码文字

    /**
     * 重绘文本
     */
    private void drawText(Canvas canvas) {
        int count = canvas.getSaveCount();
        canvas.translate(0, 0);
        int length = getEditableText().length();
        for (int i = 0; i < length; i++) {
            String text = String.valueOf(getEditableText().charAt(i));
            TextPaint textPaint = getPaint();
            textPaint.setColor(mTextColor);
            // 获取文本大小
            textPaint.getTextBounds(text, 0, 1, mRect);
            // 计算(x,y) 坐标
            int x = mStrokeWidth / 2 + (mStrokeWidth + mStrokePadding) * i - (mRect.centerX());
            int y = canvas.getHeight() / 2 + mRect.height() / 2;
            canvas.drawText(text, x, y, textPaint);
        }
        canvas.restoreToCount(count);
    }

监听文本变化回调自动完成方法

    @Override
    protected void onTextChanged(CharSequence text, int start,
                                 int lengthBefore, int lengthAfter) {
        super.onTextChanged(text, start, lengthBefore, lengthAfter);

        // 当前文本长度
        int textLength = getEditableText().length();

        if (textLength == mMaxLength) {
            hideSoftInput();
            if (mOnInputFinishListener != null) {
                mOnInputFinishListener.onTextFinish(getEditableText().toString(), mMaxLength);
            }
        }

    }

查看完整的源码

到这里你能大概理解画布的概念了,本文完。