中文网站: 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
      归纳函数将一个数据集合的所有元素通过传入的操作函数实现数据集合的积累叠加效果

五、使用技术/库

一、打包 aar

1、单个模块打包

  1. 打开 Gradle 工具窗口,找到 Android Library 模块. 在 build 任务中双击 assemble.

  2. 执行成功后,在 mylibrary/build/outputs/aar 目录下找到 aar 包.

默认 DebugReleaseAAR 包都会打出来,当然你也可以选择只打 Debug 的包,双击 assembleDebug 任务就可以了. 只打 Release 的包同理.

2、多个模块打包

当要打包的模块又依赖了其它几个模块时,常常需要把它们打包成一个 aar。多模块打包使用 fat-aar,打包关键步骤如下:

  1. 将下载好的 fat-aar.gradle 文件添加到对应的模块目录中,并在 build.gradle 中引入 apply from: 'fat-aar.gradle'。或直接引用 apply from: 'https://raw.githubusercontent.com/adwiv/android-fat-aar/master/fat-aar.gradle'

  2. 添加要打包的工程,使用 embedded 关键字。示例代码如下:

    apply from: 'fat-aar.gradle'
    dependencies {
        ...
       embedded project(':DynamicPageLibrary')
       embedded project(':VideoPlayerLib')
       embedded project(':AudioPlayLibrary')
       embedded project(':BaseCloudMusicResource')
    }
    
  3. 步骤同上《单个模块打包》一致。

二、引入 aar

方法一、通过 libs 引入到 app 中

  1. aar 文件放在 libs 目录下

  2. appbuild.gradle 中添加如下内容

    repositories {
        flatDir {
            dirs 'libs' 
        }
    }
    
  3. 之后通过如下方式引入
    dependencies {
        compile(name:'test', ext:'aar')
    }
    
  4. Rebuild project

  5. 如果发现引入后无法使用,重启 Android studio

方法二、把 aar 作为一个库工程的方式引入

当项目中库工程较多且依赖关系比较复杂时,最好采用这一种方式。如:某一个库工程也要引入这个 aar 时。

菜单栏 -> File -> New -> New Module

-> Import .Jar/.AAR Package

-> Next

-> 选择 File name 的文件 -> Subproject name 命名工程

-> Finish

创建完成后是一个工程,工程中包括 aar 文件和 build.gradle 文件。build.gradle 文件内容如下:

configurations.create("default")
artifacts.add("default", file('musiclibrary_20170622.aar'))

三,遇到的问题

  1. Non-constant Fields in Case Labels

    原因:在 Android Library 中不能使用 switch case

    解决:改成用 else if,如下图:

  2. java.lang.IllegalArgumentException: No view found for id 0x7f0d013d () for fragment TestFragment

    描述:在项目中引用了库里的 fragment,在运行后抛出了找不到 view 的异常。但是在 Demo 项目中运行是没有问题的。

    原因:库里 fragmentlayoutID 与项目中另外一个 FragmentlayoutID 名字相同,导致项目中的布局会覆盖库中的布局。

    解决:修改为不同的名称。在库中要注意资源名称可能与项目同名的问题,比如在库中的资源文件都添加前缀或后缀,或较长不容易重复的名字。同名的资源文件只会存在一个,根据库的嵌套关系,外层会覆盖内层的资源文件。

一、类与方法

1,类

  • 类的声明
    class Bar(var b: Int): Foo() {
        var c = 1
        init {
            println("class initializer")
        }
    
        constructor(): this(1) {
            println("secondary constructor")
        }
    }
    

    Bar类在这里继承了Foo类,Bar类有两个构造函数,直接在Bar类头的是primary constructor,另外一个构造函数使用constructor关键字定义,注意必须要先调用primary constructor,另外,init标明的是class initializer,每个构造函数都会首先调用class initializer里面的代码,再调用构造函数

  • 创建类的实例,不需要 new

    var bar = Bar()
    
  • 继承

    内定义默认是 final 的,要想能被继承,基类头必须有 open 注解

  • Inner class

    class Outer {
        class Inner {      
        }
    }
    

    与 Java 不同,Kotlin 中所有的内部类默认就是静态的,这样可以减少很多内存泄露的问题。如果需要在内部类中引用外部类对象,可以在Inner类的声明前加上inner关键字,然后在Inner类中使用标记的this:this@Outer来指向外部类对象

  • Singleton

    object Single {
        var c = 1
    
        fun foo() = println("foo")
    }
    

    单利对象用 object 关键字表示,可以直接使用 Single.foo() 来调用了

2,接口

interface Interface {
    fun foo() {
        println(1)
    }
    fun bar()
}

可以带有默认的实现方法,并且不允许通过属性来维护状态。

3,函数

  • 函数声明
    fun foo(va: Int): Int {
        return 1
    }
    
  • 也可以使用单行声明
    fun foo(va: Int): Int = 1
    
  • 重载

    与类的派生一样,允许重载的方法要有open注解,而在派生类中重载时要使用override注解

    override fun foo(va: Int): Int {
        return 2
    }
    

4,修饰符

5,成员变量的 Get 与 Set

注:
1,类和方法默认定义都是 final,以此来提高效率。类想要被继承用 open 关键字
2,类 和 成员变量 默认是 public 修饰

二、语法

1,语法糖,对类的扩充

在不修改类的原始定义的情况下实现对类的扩展,如下面的代码为Person类增加了一个名为isTeenager的扩展:

fun Person.isTeenager(): Boolean {
    return age in 13..19
}

2,排除空指针

  • 定义一个为空的变量是需要加上 ? 符号
    var text: String? = null
    
  • 操作一个可能为空的对象时,同样要加上 ? 符号
    var length = text?.length
    
  • 如果将该变量传递给函数,在参数后面需要加 !! 符号
    if (text != null) {
        customPrint(text!!)
    }
    
  • 如何去掉 !! 符号呢,当代码充满该符号时显然很不优雅,这时可以使用 let 函数
    text?.let { customPrint(it) }
    
  • 如果遇到多个参数的情况,你可以选择嵌套多个 let,但这样可读性并不好。比如:
    if (mUserName != null && mPhotoUrl != null) {
       uploadPhoto(mUserName!!, mPhotoUrl!!)
    }
    

    这时你可以构建一个全局函数:

    fun <T1, T2> ifNotNull(value1: T1?, value2: T2?, bothNotNull: (T1, T2) -> (Unit)) {
       if (value1 != null && value2 != null) {
           bothNotNull(value1, value2)
       }
    }
    

    调用方式

    ifNotNull(mUserName, mPhotoUrl, {name, url ->
            uploadPhoto(name, url)
    })
    

3,高阶函数和Lambda表达式

  • 例如给一个变量赋 lambda 表达式 {x,y->x+y}
    val sumLambda: (Int, Int) -> Int = {x,y -> x+y}
    
  • 定义一个可以传表达式的高阶函数

    kotlin
    fun doubleTheResult(x:Int, y:Int, f:(Int, Int)->Int): Int {
    return f(x,y) * 2
    }
    kotlin

  • 调用方法如下

    val result1 = doubleTheResult(3, 4, sumLambda)
    或
    val result2 = doubleTheResult(3, 4, {x,y -> x+y})
    

4,范围表达式

  • 范围创建只需要 .. 操作符,为升序,例如:
    // 该范围包含数值1,2,3,4,5
    val r1 = 1..5
    
  • 如果要表示降序,用 downTo 函数
    // 该范围包含数值5,4,3,2,1
    val r2 = 5 downTo 1
    
  • 如果步长不是1,则需要使用step函数
    // 该范围包含数值5,3,1
    val r3 = 5 downTo 1 step 2
    // 同理,升序的序列
    val r4 = 1..10 step 2
    

5,条件结构

  • if 表达式(类似于 Java 的 ?: 运算符)
    var age = 20
    val isEligibleToVote = if(age > 18) "Yes" else "No"
    
  • when表达式(类似于 Java 的 switch,但功能更强大)
    val age = 17
    
    val typeOfPerson = when(age){
        0 -> "New born"
        in 1..12 -> "Child"
        in 13..19 -> "Teenager"
        else -> "Adult"
    }
    

6,循环结构

使用 for..in 遍历数组、集合及其它提供了迭代器的数据结构,语法同Java几乎完全相同,只是用 in 操作符取代了 : 操作符

val names = arrayOf("Jake", "Jill", "Ashley", "Bill")

for (name in names) {
    println(name)
}

while 和 do..while 循环的语法与Java完全相同。

7,字符串模板

可以在字符串中嵌入变量表达式,例如:

val name = "Bob"
println("My name is ${name}") //打印"My name is Bob"

val a = 10
val b = 20
println("The sum is ${a+b}") //打印"The sum is 30"

三、XML 布局 + kotlin-android-extensions

1, 通常在 xml 中查找控件的写法

val name = find<TextView>(R.id.tv_name)
// 等同于 findViewById()
val name = findViewById(R.id.tv_name) as TextView
name.text="张三"

2,如果使用扩展后,可以直接调用并赋值

tv_name.text="张三"

环境配置

1,项目下面的 build.gradle 加入如下代码:

buildscript {

    ext.kotlin_version ="1.0.4"

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

2,app 下面的 build.gradle 加入如下代码:

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    sourceSets{
        main.java.srcDirs+='src/main/kotlin'
    }
}

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    compile 'org.jetbrains.anko:anko-sdk25:0.10.0-beta-1'// sdk15, sdk19, sdk21, sdk23 are also available
    compile 'org.jetbrains.anko:anko-appcompat-v7:0.10.0-beta-1'
}

3,Gradle Sync

4,菜单栏 —> Code —> Convert Java File to Kotlin File

Anko Layout

一、优点

1,运行速度快。XML布局是在运行时解析的,也就是说XML需要从资源文件中获取,然后 XmlPullParser 需要解析所有的元素并一个一个的创建它们。还要解析元素的属性,然后设置,这个过程非常繁重。

2,类型安全,不再需要那么多的 findById() 之后的类型转换。

3,null 安全,Kotlin 里,如果一个变量用?表示为可空,并且使用?之后再调用的时候,即使变量为空也不会引发异常。

4,代码复用,可以通过继承AnkoComponent的方式实现代码复用。XML布局是每一个Activity,每一个View各自专属一个,代码复用比较少。

二、缺点

1,Anko DSL 布局不能预览。虽然有一个叫 Anko Preview Plugin 的预览插件,但是每次修改后都需要 make 下才能预览,关键是在新版本 Android Studio2.2 以上都不支持。

  • 笔者在 Android studio2.3 上安装该插件,导致重启后无法进入项目界面。
  • 幸好在启动页面的左下角有一个 Config 选项,点击其中的 Plugin,卸载 Anko Preview 插件才可以正常启动。

2,定义 id 比较繁琐,需要定义一个变量,或者在 values 资源文件下定义 ids。不用 id 行不行呢?你去问问 RelativeLayout 答应不答应吧。

3,如果定义在 xml 的话,可以直接通过 id 使用对应的 View(XML 布局 + kotlin-android-extensions 的方式),但是在 Anko DSL 布局的话,只能通过定义变量的方式来实现。

4,动态替换外部资源以达到换肤的效果,那么 XML 显然比 Kotlin 代码要来得容易:前者可以编译成一个只有资源的 apk 供应用加载,后者的话就得搞一下动态类加载了。

三、引用方式

// 继承 AnkoComponent 创建布局
class LoginLayout<T> : AnkoComponent<T> {

    override fun createView(ui: AnkoContext<T>): View {
        return with(ui){
            ...
        }
    }
}

// Activity 中引用
override fun onCreate(savedInstanceState: Bundle?) {
    LoginLayout<MainActivity>().setContentView(this)
}

// Fragment 中引用
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    var view = LoginLayout<LoginFragment>().createView(AnkoContext.create(context, LoginFragment()))
    return view
}

四、常用语法

1,定义 TextView

textView("Hello") {
    textSize = 16f
    textColor = Color.RED
    backgroundResource = R.drawable.shape_et_bg
    gravity = Gravity.CENTER
}.lparams(matchParent, wrapContent){
    margin = dip(12)
    padding = dip(2)
}

2,提取样式

// 给 EditText 扩展样式方法
fun EditText.commonStyle(){
    textSize = 16f
    backgroundResource = R.drawable.shape_et_bg
}

// 直接在布局的后面添加上
.style {
    view ->
    when (view) {
        is Button -> {
            view.gravity = Gravity.CENTER
        }
        is TextView -> {
            view.gravity = Gravity.LEFT
            view.textSize = 20f
            view.textColor = Color.DKGRAY
        }
    }
}

3,设置点击事件

var etInput = editText {
    hint = "请输入文字"
    commonStyle()
}

button("点我"){
    // 在按钮属性内部设置点击事件
    onClick {
        toast("输入的内容:${etInput.text}")
    }
}

// 通过变量 + . 的方式设置
etInput.onClick { 

}

4,布局方式

val ID_USERNAME = 1

// 垂直布局,== LinearLayout + orientation="vertical"
verticalLayout {  }
// 相对布局,需要使用到 ID
relativeLayout {
    textView("姓名") {
        id = ID_USERNAME
    }
    textView("描述") {

    }.lparams {
        below(ID_USERNAME)
        alignParentLeft()
    }
}
// 线性布局
linearLayout {
    orientation = LinearLayout.HORIZONTAL
}
frameLayout { }
tableLayout { }

5,ui: AnkoContext

// 包含的变量
val ctx: Context
val owner: T
val view: View

// 例如,可以通过 owner 直接调用外部 Activity 的方法
override fun createView(ui: AnkoContext<T>): View {
    if (ui.owner is Activity) {
        (owner as Activity).onBackPressed()
    }
}

参考链接

Kotlin Primer·第二章·基本语法 https://kymjs.com/code/2017/02/04/01/

http://blog.csdn.net/io_field/article/details/53365834

只需五分钟,开始使用Kotlin开发Android
https://barryhappy.github.io/2016/10/20/start-kotlin-in-5-mins/

登陆注册 Demo
http://blog.csdn.net/xiehuimx/article/details/72354371

Kotlin 系统入门到进阶 视频教程 https://github.com/enbandari/Kotlin-Tutorials

官方文档:https://www.kancloud.cn/pholance/kotlin/125094