RoundRectShapeの引数について調査した

角丸画像をコードから生成したくて、ShapeDrawableを使うことになった。 角丸にするにはRoundRectShapeをShapeDrawableに食わせるんだけど、その引数がドキュメントだけだと分からなかったので検証した。

RoundRectShape(float outerRadii, RectF inset, float innerRadii) RoundRectShape constructor. Specifies an outer (round)rect and an optional inner (round)rect. outerRadii float: An array of 8 radius values, for the outer roundrect. The first two floats are for the top-left corner (remaining pairs correspond clockwise). For no rounded corners on the outer rectangle, pass null.

時計回りで指定すればいいんだよってことは分かるけど、8つ渡すのはよく分からなかった。

RoundRectShape | Android Developers

結論からいうと以下のようになっている

float[]{
  topLeft-x, topLeft-y,
  topRight-x, topRight-y,
  bottomRight-x, bottomRight-y,
  bottomLeft-x, bottomLeft-y
}

4つの角 + xとyで8つある理由ですね。 検証に使用したコードを載せておきます。

f:id:tomorrowkey:20160512132934p:plain:w200

import android.content.Context;
import android.databinding.DataBindingUtil;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import jp.tomorrowkey.android.roundrectshapeapp.databinding.ActivityMainBinding;
import jp.tomorrowkey.android.roundrectshapeapp.databinding.ListItemBinding;

public class MainActivity extends AppCompatActivity {

    ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.recyclerView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
        binding.recyclerView.setAdapter(new MyRecyclerViewAdapter(this));
    }

    private static class MyRecyclerViewAdapter extends RecyclerView.Adapter<ViewHolder> {

        private static float[][] PATTERNS = new float[][]{
                {50, 50, 0, 0, 0, 0, 0, 0},
                {0, 0, 50, 50, 0, 0, 0, 0},
                {0, 0, 0, 0, 50, 50, 0, 0},
                {0, 0, 0, 0, 0, 0, 50, 50},
                {50, 50, 50, 50, 0, 0, 0, 0},
                {25, 50, 25, 50, 0, 0, 0, 0},
                {25, 50, 25, 50, 25, 50, 25, 50},
        };

        private static final int SOLID_COLOR = Color.rgb(0x00, 0x80, 0x80);

        private static final int RECT_SIZE = 100;

        LayoutInflater inflater;

        public MyRecyclerViewAdapter(Context context) {
            inflater = LayoutInflater.from(context);
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return new ViewHolder(inflater.inflate(R.layout.list_item, parent, false));
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            ListItemBinding binding = DataBindingUtil.bind(holder.itemView);

            float[] outerR = PATTERNS[position];
            binding.textView.setText("{" + Util.join(outerR, " ,") + "}");

            ShapeDrawable drawable = new ShapeDrawable(new RoundRectShape(outerR, null, null));
            drawable.getPaint().setColor(SOLID_COLOR);
            drawable.getPaint().setStyle(Paint.Style.FILL);
            drawable.setIntrinsicHeight(RECT_SIZE);
            drawable.setIntrinsicWidth(RECT_SIZE);
            binding.imageView.setImageDrawable(drawable);
        }

        @Override
        public int getItemCount() {
            return PATTERNS.length;
        }
    }

    private static class ViewHolder extends RecyclerView.ViewHolder {
        public ViewHolder(View itemView) {
            super(itemView);
        }
    }

}

ObjectAnimatorでTextViewの内容を書き換える

ObjectAnimatorおもろいなー setNumber(int)を持つNumberTextViewというクラスを作り

public class NumberTextView extends AppCompatTextView {

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

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

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

    public void setNumber(int number) {
        setText(String.valueOf(number));
    }
}

ObjectAnimatorでエイッとすると

ObjectAnimator objectAnimator = ObjectAnimator.ofInt(binding.numberTextView, "number", 0, 10000);
objectAnimator.setDuration(10 * 1000);
objectAnimator.setInterpolator(new LinearInterpolator());
objectAnimator.start();

数値がテキストとして表示される

f:id:tomorrowkey:20160301093402g:plain

単にリフレクション使って値を設定しているだけなのかなー。

ProgressBarの値をアニメーションで変更する

f:id:tomorrowkey:20160229190212g:plain ↑こういうことやりたいんだけど、ProgressBar#setProgress(:int)だとアニメーションしてくれない。

そうだ、ObjectAnimatorを使ってアニメーションを実現しよう。

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:tools="http://schemas.android.com/tools"
  tools:context=".activity.MainActivity"
  >
  <LinearLayout
    android:orientation="vertical"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ProgressBar
      style="?android:attr/progressBarStyleHorizontal"
      android:id="@+id/progress_bar"
      android:layout_margin="16dp"
      android:layout_width="match_parent"
      android:layout_height="wrap_content"/>

    <Button
      android:id="@+id/update_progress_button"
      android:text="Update progress"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"/>
  </LinearLayout>
</layout>
public class MainActivity extends AppCompatActivity {

  ActivityMainBinding binding;

  ObjectAnimator objectAnimator;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    binding.updateProgressButton.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        int progress = (int) (Math.random() * 100);
        setProgressWithAnimation(progress);
      }
    });
  }

  private void setProgressWithAnimation(int progress) {
    int[] values = {binding.progressBar.getProgress(), progress};
    if (objectAnimator == null) {
      objectAnimator = ObjectAnimator.ofInt(binding.progressBar, "progress", values);
    } else {
      objectAnimator.cancel();
      objectAnimator.setIntValues(values);
    }
    objectAnimator.setDuration(1000);
    objectAnimator.setInterpolator(new DecelerateInterpolator(2.0f));
    objectAnimator.start();
  }

}

Activityに書いちゃうとアレなんでカスタムViewでやるとスッキリしそうでよさそう。 アニメーションしている最中はそのときのProgressの値になってしまうので、getProgress()を使いたいケースではこれは使えない。

Androidエミュレータにテキストをペーストすることができない時代がありました

Androidエミュレータは母艦とクリップボードの共有がされないので、パスワードなどの複雑なテキストを毎回手打ちしなくてはならなくて面倒でした。 そこで、簡単にコピペできるような関数を作りました。

function paste_in_android() {
  e=$(pbpaste | sed -e "s/ /\\\\ /g" | sed -e "s/'/\\\\'/g" | sed -e 's/"/\\"/g')
  adb shell input text "$e"
}

クリップボードに入っているテキストをadb経由で入力します。

input textを使っているためマルチバイト文字を渡すことはできなく、さらにIMEの状態によって入力のされ方が変わります。 adbを経由するので、エミュレータだけではなく実機でも使うことができます。面倒なURLの入力にも使えるんじゃないでしょうか。 うまくうごかなかったら、エスケープすべき文字があるんだと思います。sedエスケープ処理を追加してください。

私はターミナルで入力するのすら面倒なのでAlfred workflowにしておきました。(リポジトリのgifアニを見ればどういう風に使うのか一発で分かると思います。) github.com

べんり〜


という時代もありました。 現代のAndroidエミュレータではいい感じにしてくれるので、EditTextなどを長押しすればペーストすることができます。

コマンドラインから特定のクラスのみテストする

次のコマンドで実現できる

./gradlew connectedAndroidTest -Pcom.android.tools.instrumentationTestRunnerArgs=class=com.example.Util

com.android.tools.instrumentationTestRunnerArgsにはいろんなオプションが指定でき、どんなオプションがあるかは次のURLから見ることができる

AndroidJUnitRunner | Android Developers

複数の引数を指定する場合はカンマで区切る

./gradlew connectedAndroidTest -Pcom.android.tools.instrumentationTestRunnerArgs=class=com.example.Util,size=medium

キーが重複するハッシュをマージする

2つのハッシュをマージするときにキーが重複するとレシーバを優先されてしまう

$ pry
[1] pry(main)> h1 = {a: [1, 2, 3], b: [1, 2, 3]}
=> {:a=>[1, 2, 3], :b=>[1, 2, 3]}
[2] pry(main)> h2 = {a: [10], c: [10,11]}
=> {:a=>[10], :c=>[10, 11]}
[3] pry(main)> h1.merge(h2)
=> {:a=>[10], :b=>[1, 2, 3], :c=>[10, 11]}

Hash#mergeはブロックを受け取ることができて、キーが重複した時の挙動を指定することができる

$ pry
[1] pry(main)> h1 = {a: [1, 2, 3], b: [1, 2, 3]}
=> {:a=>[1, 2, 3], :b=>[1, 2, 3]}
[2] pry(main)> h2 = {a: [10], c: [10,11]}
=> {:a=>[10], :c=>[10, 11]}
[3] pry(main)> h1.merge(h2) { |key, h1v, h2v| h1v + h2v }
=> {:a=>[1, 2, 3, 10], :b=>[1, 2, 3], :c=>[10, 11]}

class Hash (Ruby 2.2.0)

Array#shuffleで同じ結果を得る

shuffleの引数に同じRandomオブジェクトを渡せば何度シャッフルしても同じ結果になる

$ pry
[1] pry(main)> Array(1...10).shuffle(random: Random.new(473))
=> [6, 4, 9, 5, 8, 3, 2, 1, 7]
[2] pry(main)> Array(1...10).shuffle(random: Random.new(473))
=> [6, 4, 9, 5, 8, 3, 2, 1, 7]
[3] pry(main)> Array(1...10).shuffle(random: Random.new(473))
=> [6, 4, 9, 5, 8, 3, 2, 1, 7]

class Array (Ruby 2.2.0)