中文网站: https://www.kotlincn.net/docs/reference/

在线测试: https://try.kotlinlang.org

或使用 IDEA 开发工具:
下载: https://www.jetbrains.com/idea/download/#section=windows
入门文档:https://www.kotlincn.net/docs/tutorials/getting-started.html

概述

  • Kotlin 用于服务器端

    语言什么都很厉害,完全可以代替 Java。有服务端的开发框架,如 Spring。可以部署在支持 JavaWeb 的任何主机。

  • Kotlin 用于 Android

    兼容 JDK6、性能与 Java 类似、与 Java 100% 互操作性、额外占用小、增量编译与 Java 类似。

  • Kotlin 用于 JavaScript

    支持 ECMAScript 5.1 标准,代码会转成 JavaScript。可以与 JQuery、ReactJS 一起使用。

  • 新特性

    1. 协程(??)

      如在 UI 线程中异步启动一个网络线程,当网络线程完成后回来更新 UI。代码如下:

      // 在 UI 上下文中启动新的协程
      launch(UI) {
          // 等待异步叠加完成
          val image = asyncOverlay().await()
          // 然后在 UI 中显示
          showImage(image)
      }
      

      yieldyieldAll 函数使用协程来支持惰性生成序列?

    2. 类型别名

      类型别名允许你为现有类型定义备用名称。

      typealias OscarWinners = Map<String, String>
      
    3. 已绑定的可调用引用(?)

      现在可以使用 :: 操作符来获取指向特定对象实例的方法或属性的成员引用

      val numberRegex = "\\d+".toRegex()
      val numbers = listOf("abc", "123", "456").filter(numberRegex::matches)
      
    4. 密封类和数据类(???)

      数据类现在可以扩展其他类。 这可以用来友好且清晰地定义一个表达式类的层次结构:

      sealed class Expr
      
      data class Const(val number: Double) : Expr()
      data class Sum(val e1: Expr, val e2: Expr) : Expr()
      object NotANumber : Expr()
      
      fun eval(expr: Expr): Double = when (expr) {
          is Const -> expr.number
          is Sum -> eval(expr.e1) + eval(expr.e2)
          NotANumber -> Double.NaN
      }
      val e = eval(Sum(Const(1.0), Const(2.0)))
      
    5. lambda 表达式中的解构

      可以使用解构声明语法来解开传递给 lambda 表达式的参数。

      val map = mapOf(1 to "one", 2 to "two")
      // 之前
      println(map.mapValues { entry ->
                             val (key, value) = entry
                             "$key -> $value!"
                            })
      // 现在
      println(map.mapValues { (key, value) -> "$key -> $value!" })
      
    6. 下划线用于未使用的参数

      对于具有多个参数的 lambda 表达式,可以使用 _ 字符替换不使用的参数的名称

      map.forEach { _, value -> println("$value!") }
      // 这也适用于解构声明
      val (_, status) = getResult()
      
    7. 数字字面值中的下划线

      正如在 Java 8 中一样,Kotlin 现在允许在数字字面值中使用下划线来分隔数字分组

      val oneMillion = 1_000_000
      val hexBytes = 0xFF_EC_DE_5E
      val bytes = 0b11010010_01101001_10010100_10010010
      
    8. 对于属性的更短语法

      对于没有自定义访问器、或者将 getter 定义为表达式主体的属性,现在可以省略属性的类型

      data class Person(val name: String, val age: Int) {
          val isAdult get() = age >= 20 // 属性类型推断为 “Boolean”
      }
      
    9. 内联属性访问器

      如果属性没有幕后字段,现在可以使用 inline 修饰符来标记该属性访问器。 这些访问器的编译方式与内联函数相同。

      public val <T> List<T>.lastIndex: Int
      inline get() = this.size - 1
      
    10. 局部委托属性(?)

      现在可以对局部变量使用委托属性语法。 一个可能的用途是定义一个延迟求值的局部变量

      val answer by lazy {
          println("Calculating the answer...")
          42
      }
      if (needAnswer()) {                     // 返回随机值
          println("The answer is $answer.")   // 此时计算出答案
      }
      else {
          println("Sometimes no answer is the answer...")
      }
      
    11. 委托属性绑定的拦截(???)

      对于委托属性,现在可以使用 provideDelegate 操作符拦截委托到属性之间的绑定 。provideDelegate 方法在创建 MyUI 实例期间将会为每个属性调用,并且可以立即执行必要的验证。

      class ResourceLoader<T>(id: ResourceID<T>) {
          operator fun provideDelegate(thisRef: MyUI, property: KProperty<*>): ReadOnlyProperty<MyUI, T> {
              checkProperty(thisRef, property.name)
              …… // 属性创建
          }
      
          private fun checkProperty(thisRef: MyUI, name: String) { …… }
      }
      
      fun <T> bindResource(id: ResourceID<T>): ResourceLoader<T> { …… }
      
      class MyUI {
          val image by bindResource(ResourceID.image_id)
          val text by bindResource(ResourceID.text_id)
      }
      
    12. 泛型枚举值访问(??)

      现在可以用泛型的方式来对枚举类的值进行枚举

      enum class RGB { RED, GREEN, BLUE }
      
      inline fun <reified T : Enum<T>> printAllValues() {
          print(enumValues<T>().joinToString { it.name })
      }
      
    13. 对于 DSL 中隐式接收者的作用域控制(???) ->>> gradle 表达式?

      @DslMarker 注解允许限制来自 DSL 上下文中的外部作用域的接收者的使用。

      table {
          tr {
              td { +"Text" }
          }
      }
      
    14. rem 操作符

      mod 操作符现已弃用,而使用 rem 取代

    15. 标准库

      • 字符串到数字的转换

      • onEach()

      • also()、takeIf() 和 takeUnless()

      • groupingBy()

      • Map.toMap() 和 Map.toMutableMap()

      • Map.minus(key)

      • minOf() 和 maxOf()

      • 类似数组的列表实例化函数

      • Map.getValue()

      • 抽象集合

      • 数组处理函数

啥也别说,先上效果图。

分析效果图,主要涉及两部分:
1,围绕圆圈绘制的文字
2,动画(背景的呼吸动效,文字的旋转动效)

一、围绕圆圈绘制的文字

在 Canvas 中有一个绘制方法,

canvas.drawTextOnPath(String text, Path path, float hOffset,
        float vOffset, Paint paint)

另外要考虑的因素是如何旋转文字:

  • hOffset 属性可以水平方向移动文字
  • vOffset 属性垂直方向移动文字

让文字环绕圆绘制,定义一个 Path,并添加 Circle 形路径

Path path = new Path();
path.addCircle(float x, float y, float radius, Direction dir);

最后一个属性 Direction,有两种类型:CWCCW,效果如下:

  • 设置为 Path.Direction.CW 时,文字沿顺时针绘制;
  • 设置为 Path.Direction.CCW 时,文字沿逆时针绘制。

默认文字是从 0 角度开始绘制,那么 如何让文字的起始角度偏移 ?有两种方式:

1,设置 hOffset偏移的距离 = angle / 360f * 圆形周长
但是有一个问题:当偏移角度 + 文字长度 > 360° 时,文字显示不全,最后放弃了这种做法。

2,设置 PathMatrix 属性,通过旋转来控制偏移

matrix.setRotate(float degrees, float px, float py);
  1. 如果不设置 px、py,则使用默认 (0,0)作为中心点旋转

  2. 设置时有 presetpost 三种形式。原因是矩阵乘法不满足乘法交换律,因此左乘还是右乘最终的效果都不一样。我们可以把Matrix变换想象成一个队列,队列里面包含了若干个变换操作,队列中每个操作按照先后顺序操作变换目标完成变换,pre 相当于 向队首增加 一个操作,post 相当于 向队尾增加 一个操作,set 相当于清空当前队列 重新设置

这里已经把围绕圆圈绘制文字的部分说完了。接下来来分析下动画的部分。

二、动画

  1. 使用系统的 ValueAnimator 来执行动画刷新操作
  2. 在动画更新时,达到动画效果
    • 修改文字的开始绘制角度
    • 修改背景圆的半径
  3. 研究效果图一个动画周期的波形
    1. 文字旋转波形,研究发现大概是这样的

      文字旋转波形

      从 0 到一半周期为线性递增到最大值,后一半再从最大值线性递减到 0。反正实现起来很容易,略过。

    2. 背景的动画,分为内圆和外圆

      通过对 GIF 图片的帧分析,发现内圆和外圆的波形并不一致,最终波形研究如下。

      背景的动画波形

      如上图所示,红线 代表 内圆 半径变化的波形,蓝线 代表 外圆 半径变化的波形。

      对于这样的波形变化,可以用控制波形的插值器(Interpolator),不过 Android 中自带的几种插值器波形与我们的并不相符。根据如上图的波形变化情况,我们自定义一个可定制的插值器。

      根据波形的变化,我们定义如下几种类型:

      1. 从 A 点递减到 B 点(结合效果图观察,这里定义线性变化就可以了),用 Decline 表示
      2. 从 B 点保持到 C 点,因为此时并不需要显示,我们用 Lose 表示
      3. 从 C 点递增到 D 点,用 Rise 表示
      4. 从 D 点递减到 E 点,用 Decline
      5. 从 E 点不显示到 F 点,用 Lose
      6. 从 F 点递增到 G 点,用 Rise
      7. 从 G 点保持到 H 点,用 Keep 表示

      如此,可以定义一个 Wave 对象,包含如上几种变化的过程,通过例如下面的方式设置波形的变化规则,最后调用 float getInterpolation(float input) 的方式来获取当前值。模拟代码如下:

      new Wave(A).declineTo(B).loseTo(C).riseTo(D).decline(E).loseTo(F).riseTo(G).keepTo(H);
      

      通过如上方式自定义,让背景圆按照自定义的波形收缩。

三、代码如下

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;

import com.ionesmile.test.common.utils.WaveInterpolator;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by ionesmile on 05/09/2017.
 */

public class SpeakView extends View {

    private static final String TAG = SpeakView.class.getSimpleName();
    private SpeakModel speakModel;
    private SpeakDraw speakDraw;
    private SpeakCalc speakCalc;
    private ViewAnimation viewAnimation;

    public SpeakView(Context context) {
        super(context);
        initBase(context);
    }

    public SpeakView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initBase(context);
    }

    public SpeakView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initBase(context);
    }

    private void initBase(Context context) {
        speakModel = new SpeakModel(context);
        speakDraw = new SpeakDraw(speakModel);
        speakCalc = new SpeakCalc(speakDraw, speakModel);
        ViewAnimation.SimpleAnimationListener animationListener = new ViewAnimation.SimpleAnimationListener() {
            @Override
            public void clearAnimation() {
                SpeakView.this.clearAnimation();
            }

            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                float value = (float) valueAnimator.getAnimatedValue();

                // set text start draw angle
                float textAngle = Math.abs(0.5f - value) * 2;
                textAngle = speakCalc.getTextInterpolator(textAngle) * 60;
                for (TextCircle textCircle : speakModel.textCircleList) {
                    textCircle.drawTextCurrentAngle = textCircle.drawTextStartAngle + textAngle;
                }

                // set background circle anim radius
                for (BackgroundCircle backgroundCircle : speakModel.backgroundCircleList) {
                    float realValue = backgroundCircle.interpolator.getInterpolation(value);
                    if (realValue < 0){
                        backgroundCircle.currentRadius = 0;
                    } else {
                        backgroundCircle.currentRadius = (backgroundCircle.maxRadius - backgroundCircle.minRadius) * realValue + backgroundCircle.minRadius;
                    }
                }

                invalidate();
            }
        };
        viewAnimation = new ViewAnimation(animationListener);
    }

    @Override
    protected void onDraw(Canvas canvas) {

        drawBackgroundCircle(canvas);

        drawTextCircle(canvas);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        speakCalc.onSizeChange(w, h);
    }


    private void drawBackgroundCircle(Canvas canvas) {
        for (BackgroundCircle backgroundCircle : speakDraw.speakModel.backgroundCircleList) {
            // draw background circle
            if (backgroundCircle.currentRadius > 0)
                canvas.drawCircle(speakCalc.centerX, speakCalc.centerY, backgroundCircle.currentRadius, speakDraw.getBackgroundPaint(backgroundCircle));
        }
    }

    private void drawTextCircle(Canvas canvas) {
        boolean isLockWise = true;
        for (TextCircle textCircle : speakDraw.speakModel.textCircleList) {

            // draw circle ring
            if (textCircle.needDrawCircle) {
                canvas.drawCircle(speakCalc.centerX, speakCalc.centerY, textCircle.circle.radius, speakDraw.getCirclePaint(textCircle.circle));
            }

            if (textCircle.textList == null || textCircle.textList.isEmpty()){
                continue;
            }

            float hOffset = 0;

            // draw circle text
            for (Text text : textCircle.textList) {

                Paint textPaint = speakDraw.getTextPaint(text);

                canvas.drawTextOnPath(text.text, speakDraw.getPath(textCircle, isLockWise), hOffset, textCircle.offsetVertical, textPaint);

                hOffset += (textPaint.measureText(text.text) + speakDraw.getTextSpace());
            }

            isLockWise = !isLockWise;
        }
    }

    public void startAnim(){
        viewAnimation.startViewAnim(0, 1, 3600);
    }

    public void stopAnim(){
        viewAnimation.stopAnim();
    }

    class SpeakCalc {

        private final SpeakDraw speakDraw;
        private final SpeakModel speakModel;

        private Interpolator textInterpolator;

        int centerX, centerY;

        SpeakCalc(SpeakDraw speakDraw, SpeakModel speakModel){
            this.speakDraw = speakDraw;
            this.speakModel = speakModel;

            textInterpolator = new LinearInterpolator();
        }

        void onSizeChange(int viewWidth, int viewHeight) {
            centerX = viewWidth / 2;
            centerY = viewHeight / 2;
        }

        float getTextInterpolator(float value){
            return textInterpolator.getInterpolation(value);
        }
    }

    class SpeakDraw {

        private final SpeakModel speakModel;
        Paint circlePaint;
        Paint textPaint;
        Paint backgroundPaint;

        Path path = new Path();
        Matrix matrix = new Matrix();
        private int textSpace;

        SpeakDraw(SpeakModel speakModel){
            this.speakModel = speakModel;

            circlePaint = new Paint();
            circlePaint.setStyle(Paint.Style.STROKE);

            textPaint = new Paint();
            textSpace = dp2px(30, getContext());

            backgroundPaint = new Paint();
            backgroundPaint.setStyle(Paint.Style.FILL);
        }

        public Paint getCirclePaint(Circle circle){
            circlePaint.setColor(circle.lineColor);
            circlePaint.setStrokeWidth(circle.lineWidth);
            return circlePaint;
        }

        public Paint getTextPaint(Text text){
            textPaint.setTextSize(dp2px(TextUtil.getTextSize(text.ratio), getContext()));
            textPaint.setColor(TextUtil.getTextColor(text.ratio));
            return textPaint;
        }

        public Paint getBackgroundPaint(BackgroundCircle backgroundCircle){
            backgroundPaint.setColor(backgroundCircle.color);
            return backgroundPaint;
        }

        public Path getPath(TextCircle textCircle, boolean isLockWise){
            path.reset();
            path.addCircle(speakCalc.centerX, speakCalc.centerY, textCircle.circle.radius, textCircle.direction);
            matrix.reset();
            matrix.setRotate(textCircle.drawTextCurrentAngle * (isLockWise ? 1 : -1), speakCalc.centerX, speakCalc.centerY);
            path.transform(matrix);
            return path;
        }

        public float getTextSpace() {
            return textSpace;
        }
    }

    class SpeakModel {
        List<TextCircle> textCircleList;
        List<BackgroundCircle> backgroundCircleList;

        SpeakModel(Context context){
            textCircleList = new ArrayList<>(5);
            textCircleList.add(buildTextCircle1(context));
            textCircleList.add(buildTextCircle2(context));
            textCircleList.add(buildTextCircle3(context));
            textCircleList.add(buildTextCircle4(context));
            textCircleList.add(buildTextCircle5(context));

            backgroundCircleList = new ArrayList<>(2);
            backgroundCircleList.add(buildBackgroundCircle1(context));
            backgroundCircleList.add(buildBackgroundCircle2(context));
        }

        private BackgroundCircle buildBackgroundCircle1(Context context) {
            BackgroundCircle backgroundCircle = new BackgroundCircle();
            backgroundCircle.maxRadius = dp2px(160, context);
            backgroundCircle.minRadius = dp2px(20, context);
            backgroundCircle.color = backgroundCircle.color & 0x33FFFFFF;
            backgroundCircle.interpolator = new WaveInterpolator(1)
                    .declineTo(9).loseTo(13).riseTo(21).declineTo(29)
                    .loseTo(32).riseTo(42).keepTo(50);
            return backgroundCircle;
        }

        private BackgroundCircle buildBackgroundCircle2(Context context) {
            BackgroundCircle backgroundCircle = new BackgroundCircle();
            backgroundCircle.maxRadius = dp2px(120, context);
            backgroundCircle.minRadius = dp2px(10, context);
            backgroundCircle.color = backgroundCircle.color & 0x88FFFFFF;
            backgroundCircle.interpolator = new WaveInterpolator(1)
                    .declineTo(6).loseTo(16).riseTo(21).declineTo(26)
                    .loseTo(34).riseTo(42).keepTo(50);
            return backgroundCircle;
        }

        private TextCircle buildTextCircle1(Context context) {
            TextCircle textCircle = new TextCircle();

            textCircle.circle = new Circle();
            textCircle.circle.radius = dp2px(60, context);
            textCircle.circle.lineWidth = dp2px(textCircle.circle.lineWidth, context);
            textCircle.circle.lineColor = textCircle.circle.lineColor & 0xFFFFFFFF;

            textCircle.setDrawTextStartAngle(-100);
            textCircle.offsetVertical = -dp2px(1, context);

            textCircle.textList = new ArrayList<>();
            textCircle.textList.add(new Text("亮一点", 1f));
            textCircle.textList.add(new Text("灯光律动", 2f));
            textCircle.textList.add(new Text("开灯", 1f));
            textCircle.textList.add(new Text("夜灯", 1f));

            return textCircle;
        }

        private TextCircle buildTextCircle2(Context context) {
            TextCircle textCircle = new TextCircle();

            textCircle.circle = new Circle();
            textCircle.circle.radius = dp2px(100, context);
            textCircle.circle.lineWidth = dp2px(textCircle.circle.lineWidth, context);
            textCircle.circle.lineColor = textCircle.circle.lineColor & 0x99FFFFFF;

            textCircle.setDrawTextStartAngle(60);
            textCircle.offsetVertical = -dp2px(1, context);

            textCircle.textList = new ArrayList<>();
            textCircle.textList.add(new Text("灯光律动", 1f));
            textCircle.textList.add(new Text("暗一点", 1.2f));
            textCircle.textList.add(new Text("关灯", 2.4f));
            textCircle.textList.add(new Text("夜灯", 1.2f));

            return textCircle;
        }

        private TextCircle buildTextCircle3(Context context) {
            TextCircle textCircle = new TextCircle();

            textCircle.circle = new Circle();
            textCircle.circle.radius = dp2px(140, context);
            textCircle.circle.lineWidth = dp2px(textCircle.circle.lineWidth, context);
            textCircle.circle.lineColor = textCircle.circle.lineColor & 0x33FFFFFF;

            textCircle.setDrawTextStartAngle(-135);
            textCircle.offsetVertical = -dp2px(1, context);

            textCircle.textList = new ArrayList<>();
            textCircle.textList.add(new Text("亮一点", 1.2f));
            textCircle.textList.add(new Text("灯光律动", 1.2f));
            textCircle.textList.add(new Text("开灯", 2.5f));
            textCircle.textList.add(new Text("夜灯", 1.2f));

            return textCircle;
        }

        private TextCircle buildTextCircle4(Context context) {
            TextCircle textCircle = new TextCircle();

            textCircle.circle = new Circle();
            textCircle.circle.radius = dp2px(140, context);
            textCircle.circle.lineWidth = dp2px(textCircle.circle.lineWidth, context);
            textCircle.circle.lineColor = textCircle.circle.lineColor & 0x33FFFFFF;

            textCircle.setDrawTextStartAngle(150);
            textCircle.direction = Path.Direction.CCW;
            textCircle.offsetVertical = dp2px(textCircle.circle.lineWidth, context);
            textCircle.needDrawCircle = false;

            textCircle.textList = new ArrayList<>();
            textCircle.textList.add(new Text("开灯", 2.5f));
            textCircle.textList.add(new Text("亮一点", 2f));
            textCircle.textList.add(new Text("灯光律动", 2f));

            return textCircle;
        }

        private TextCircle buildTextCircle5(Context context) {
            TextCircle textCircle = new TextCircle();

            textCircle.circle = new Circle();
            textCircle.circle.radius = dp2px(180, context);
            textCircle.circle.lineWidth = dp2px(textCircle.circle.lineWidth, context);
            textCircle.circle.lineColor = textCircle.circle.lineColor & 0x11FFFFFF;

            textCircle.textList = new ArrayList<>();

            return textCircle;
        }
    }

    class BackgroundCircle {
        int maxRadius = 100;
        int minRadius = 10;
        float currentRadius = 0;
        int color = 0xFF57FFFF;
        Interpolator interpolator;
    }

    class TextCircle {
        Circle circle;
        List<Text> textList;
        int drawTextStartAngle;
        float drawTextCurrentAngle;
        Path.Direction direction = Path.Direction.CW;
        int offsetVertical = 0;
        boolean needDrawCircle = true;

        public void setDrawTextStartAngle(int drawTextStartAngle) {
            this.drawTextStartAngle = drawTextStartAngle;
            this.drawTextCurrentAngle = drawTextStartAngle;
        }
    }

    class Text {
        String text;
        float ratio = 1;

        public Text(String text, float ratio) {
            this.text = text;
            this.ratio = ratio;
        }
    }

    class Circle {
        int radius;
        int lineWidth = 2;
        int lineColor = 0xFF04A9E0;
    }

    static class TextUtil {

        static final int TEXT_SIZE = 12;
        static final int TEXT_COLOR = 0xFF0097D9;

        public static int getTextSize(float ratio){
            return (int) (TEXT_SIZE * ratio);
        }

        public static int getTextColor(float ratio){
            return TEXT_COLOR;
        }
    }

    public static int dp2px(float value, Context context) {
        final float scale = context.getResources().getDisplayMetrics().densityDpi;
        return (int) (value * (scale / 160) + 0.5f);
    }
}

动画帮助类:

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;

/**
 * Created by ionesmile on 05/09/2017.
 */

public class ViewAnimation {

    private AnimationListener animationListener;

    public ValueAnimator valueAnimator;

    public ViewAnimation(AnimationListener animationListener) {
        this.animationListener = animationListener;
    }

    public void startAnim() {
        stopAnim();
        startViewAnim(0f, 1f, 1000);
    }

    public void startAnim(int time) {
        stopAnim();
        startViewAnim(0f, 1f, time);
    }


    public void stopAnim() {
        if (valueAnimator != null) {
            animationListener.clearAnimation();

            valueAnimator.setRepeatCount(0);
            valueAnimator.cancel();
            valueAnimator.end();
            if (animationListener.onStopAnim() == 0) {
                valueAnimator.setRepeatCount(0);
                valueAnimator.cancel();
                valueAnimator.end();
            }
        }
    }

    public ValueAnimator startViewAnim(float startF, final float endF, long time) {
        valueAnimator = ValueAnimator.ofFloat(startF, endF);
        valueAnimator.setDuration(time);
        valueAnimator.setInterpolator(animationListener.getInterpolator());

        valueAnimator.setRepeatCount(animationListener.setAnimRepeatCount());

        if (ValueAnimator.RESTART == animationListener.setAnimRepeatMode()) {
            valueAnimator.setRepeatMode(ValueAnimator.RESTART);

        } else if (ValueAnimator.REVERSE == animationListener.setAnimRepeatMode()) {
            valueAnimator.setRepeatMode(ValueAnimator.REVERSE);

        }

        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                animationListener.onAnimationUpdate(valueAnimator);
            }
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }

            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                super.onAnimationRepeat(animation);
                animationListener.onAnimationRepeat(animation);
            }
        });
        if (!valueAnimator.isRunning()) {
            animationListener.animIsRunning();
            valueAnimator.start();

        }

        return valueAnimator;
    }


    public interface AnimationListener {

        void clearAnimation();

        void onAnimationUpdate(ValueAnimator valueAnimator);

        void onAnimationRepeat(Animator animation);

        int onStopAnim();

        int setAnimRepeatMode();

        int setAnimRepeatCount();

        void animIsRunning();

        Interpolator getInterpolator();
    }

    public abstract static class SimpleAnimationListener implements AnimationListener {

        @Override
        public void onAnimationRepeat(Animator animation) {

        }

        @Override
        public int onStopAnim() {
            return 0;
        }

        @Override
        public int setAnimRepeatMode() {
            return ValueAnimator.RESTART;
        }

        @Override
        public int setAnimRepeatCount() {
            return ValueAnimator.INFINITE;
        }

        @Override
        public void animIsRunning() {

        }

        @Override
        public Interpolator getInterpolator() {
            return new LinearInterpolator();
        }
    }
}

自定义的波形插值器:

import android.view.animation.Interpolator;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by ionesmile on 06/09/2017.
 */

public class WaveInterpolator implements Interpolator {

    private List<Pulse> pulseList = new ArrayList<>();

    public WaveInterpolator(float startValue) {
        pulseList.add(new Pulse(Action.KEEP, startValue, 0));
    }

    public WaveInterpolator declineTo(float progress) {
        pulseList.add(new Pulse(Action.DECLINE, 0, progress));
        return this;
    }

    public WaveInterpolator riseTo(float progress) {
        pulseList.add(new Pulse(Action.RISE, 1, progress));
        return this;
    }

    public WaveInterpolator keepTo(float progress) {
        pulseList.add(new Pulse(Action.KEEP, pulseList.get(pulseList.size() - 1).value, progress));
        return this;
    }

    public WaveInterpolator loseTo(float progress) {
        pulseList.add(new Pulse(Action.LOSE, -1, progress));
        return this;
    }

    /**
     * 将输入的进度装换成对应的值
     *
     * @param input 0 ~ 1.0f
     * @return
     */
    public float getInterpolation(float input) {
        float durationProgress = pulseList.get(pulseList.size() - 1).progress - pulseList.get(0).progress;
        float progress = durationProgress * input;
        int pulseIndex = getPulseIndex(progress);
        Pulse pulse = pulseList.get(pulseIndex);
        switch (pulse.action) {
            case KEEP:
                return pulse.value;
            case LOSE:
                return -1;
            case DECLINE:
                return getDeclineValue(pulseList.get(pulseIndex - 1), pulse, progress);
            case RISE:
                return getRiseValue(pulseList.get(pulseIndex - 1), pulse, progress);
        }
        return input;
    }

    private float getDeclineValue(Pulse lastPulse, Pulse pulse, float progress) {
        progress = progress - lastPulse.progress;
        return 1 - progress / (pulse.progress - lastPulse.progress);
    }

    private float getRiseValue(Pulse lastPulse, Pulse pulse, float progress) {
        progress = progress - lastPulse.progress;
        return progress / (pulse.progress - lastPulse.progress);
    }

    private int getPulseIndex(float progress) {
        int length = pulseList.size();
        int index = 1;
        while (index < length) {
            float itemProgress = pulseList.get(index).progress;
            if (progress <= itemProgress) {
                return index;
            }
            index++;
        }
        return 0;
    }

    enum Action {
        DECLINE, RISE, KEEP, LOSE
    }

    class Pulse {
        Action action;
        float value;
        float progress;

        public Pulse(Action action, float value, float progress) {
            this.action = action;
            this.value = value;
            this.progress = progress;
        }
    }
}

Google 推出 Kotlin 作为 Android 的官方语言已经有一段时间,最近用工作上一些闲暇时间做了个项目,切身体验下。

一、需求描述

一直以来对各个网站的密码管理都比较头疼,因为担心“撞库”,所有网站密码都不相同。注册网站时都会随便写一个密码,却没有一个好的密码管理工具,下次登录时基本都需要找回密码,结果又忘记如此反复。对于普通的网站重新找回一次并不算复杂,但是对于像 QQ、微信、支付宝 这样有比较高安全验证的网站找回起来并不容易,处理起来很繁琐特别头疼(承认我记忆力不好,突然想到自己好几张银行卡密码也忘记了 ( ̄▽ ̄)~~~ ,不过也没存款。。。。。。)

之前找过管理密码的软件,但不是太放心。软件又没开源,也不确定有没有后门或漏洞,自己动手要踏实得多。

我的《密码本》正是基于这一需求产生的,不但让自己的密码相对有一个保障,同时练练手学习新的技术。最后该项目作为开源项目,希望也能帮助你解决同样的烦恼。

二、项目截图

GitHub: https://github.com/iOnesmile/PasswordNotebook

安装包: 百度云下载

三、待完善

  • 提升加密文件安全度,研究其它算法并检验安全性
  • 应用内安全验证,如数据存储、锁屏、页面超时、导出权限等
  • 优化交互体验,简化操作流程,和指纹解锁等验证机制
  • 其它平台开发(iOS、Windows、MacOS),信息同步
  • 语言国际化
  • 其它……

如果有什么好的想法和建议,或在使用中遇到什么问题,欢迎反馈,我们一起完善吧!!!

四、使用 Kotlin 的坑或技术总结

  1. 在设置监听时,提示错误 Expected a value of type Boolean
    原因:该监听有一个返回值,类型是 Boolean
    例如:

    textView.onLongClick {
        // TODO
        return@onLongClick true
    }
    
  2. EditText 设置值时提示 Type mismatch. Required: Editable! Found: String
    原因:要给 EditText 设置 String 类型的值时,需要使用 setText() 方法
    例如:

    editText.setText("XXX")
    
  3. 函数式编程
    • map
      映射函数也是一个高阶函数,将一个集合经过一个传入的变换函数映射成另外一种集合

    • filter
      筛选函数将用户给定的布尔逻辑作用于集合,返回由原集合中符合条件的元素组合的一个子集

    • reduce
      归纳函数将一个数据集合的所有元素通过传入的操作函数实现数据集合的积累叠加效果

五、使用技术/库