フリープログラマー日記

iOS,アンドロイド開発を気ままにしながら生きてるおじさんのブログです。

第24回 設計変更。クラスって難しい。

前回、Runner クラスが便利そうだと書きましたが、進めているうちに、Runner クラスを統括していく、Team クラスがあるといいなと思いつき(私は常に思いつきでプログラムする・・・)、Team クラスを書いてみました。

Androidの記録方法、SharedPreferencesあたりを思い出すのに少しかかりましたが、ここに全て保存しようと思います。容量はそんなに大きくないです。

あと、Team クラスをView に渡そうと考えてましたが、これって逆ですよね。

Team クラスのアイテムとして、エントリービューや記録などがあるとスッキリするので、 Team クラスにViewを返すメソッドを実装することにしました。.
最後の方にある EntryView を返すメソッドがそれです。

これで、EntryView の”未登録”部分をタップした時、入力モードに移るようにすればなかなかいい感じなのではないかと思います。

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.widget.ScrollView;
import java.util.ArrayList;
public class Team {

    
    private Context myContext;
    private int lastId;      // 登録メンバー数

    private ArrayList<Runner> runners;

    // コンストラクタ
    Team(Context context) {

        myContext = context;
        lastId = loadLastId();
        runners = new ArrayList<>();

        // count(=登録数)が 0 の時は、名前の読み込みはしない
        if (lastId>0) {
            for (int i=1 ; i<=lastId ; i++ ) {
                runners.add(new Runner(i,myContext));
            }
        }
        // 未登録枠を一つ作る
        runners.add(new Runner(lastId+1,myContext));
    }

    private int loadLastId() {

        // ID番号は1からの連番で最終番号をプリファレンスに保存しておく
        // この関数は最終番号を読み出す

        // アプリ標準の Preferences を取得する
        SharedPreferences sp =
                PreferenceManager.getDefaultSharedPreferences(myContext);

        return sp.getInt("LastID",0);
    }

    private void saveLastId(int presentId) {

        // ID番号は1からの連番で最終番号をプリファレンスに保存しておく
        // この関数は最終番号を書き出す
        SharedPreferences sp =
                PreferenceManager.getDefaultSharedPreferences(myContext);

        // Preferences に書き込むための Editor クラスを取得する
        SharedPreferences.Editor editor = sp.edit();
        editor.putInt("LastID",presentId);
        editor.apply();
    }

    // 選手の追加・削除
    void addRunner(String name) {

        // 氏名未登録のrunnerオブジェクトに氏名を入れて保存する
        runners.get(runners.size()).saveName(name);
        lastId++;
        saveLastId(lastId);

        // 未登録枠を一つ作る
        runners.add(new Runner(lastId+1,myContext));
    }

    public EntryView entryMenuView(int width, int height) {

        ScrollView scrollView = new ScrollView(myContext);
        RunnerList listView = new RunnerList(myContext,width*8/10,width*13/100);
        // 選手名と番号を入れるメソッド
        for (int i=0; i<runners.size(); i++) {
            listView.setRunnerView(i+1,runners.get(i).getName());
        }

        scrollView.addView(listView);

        return new EntryView(myContext, width,height,scrollView);
    }
}


EntryView はこのようになっています。

import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.util.TypedValue;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TextView;


public class EntryView extends RelativeLayout {


    public EntryView(Context context,int width, int height, ScrollView runnerEntryList) {

        // ランナーを指定する画面。
        super(context);
        setBackgroundColor(Color.argb(255,100,240,255));
        // このビューのサイズを受け取っておく
        // エントリー用のパーツを配置する
        // Entry(走者名の登録) TextView
        TextView title = new TextView(getContext());
        title.setText("Entry(走者名の登録)");
        final float size = width * 0.07f;
        title.setTextSize(TypedValue.COMPLEX_UNIT_PX,size);
        title.setTextColor(Color.BLACK);
        title.setTypeface(Typeface.DEFAULT_BOLD);
        LayoutParams titleParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT);
        titleParams.addRule(ALIGN_PARENT_TOP);
        titleParams.addRule(CENTER_HORIZONTAL);

        this.addView(title,titleParams);

        // UIButton 2個、「メニューに戻る」「奏者選択終了」

        // ボタンビューの位置決めをしたくないので、1個のViewに乗せてしまいます。

        // ビューのサイズは下から15%
        EntryButtons buttons = new EntryButtons(getContext(),width,width*15/100);
        RelativeLayout.LayoutParams buttonsParams = new LayoutParams(
                width,width*15/100
        );
        buttonsParams.addRule(RelativeLayout.CENTER_IN_PARENT);
        buttonsParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);

        addView(buttons,buttonsParams);

        // スクロールビューの設置
        RelativeLayout.LayoutParams SCViewParams = new LayoutParams(
                width*80/100,height-width*40/100
        );

        SCViewParams.addRule(CENTER_IN_PARENT);
        SCViewParams.addRule(CENTER_IN_PARENT);

        addView(runnerEntryList,SCViewParams);
    }
}

第23回 クラスの設計は難しいです!

スクロールビューまで来て、はたと困りました。

なぜなら、思いつくままにプログラムを書く癖があるので、後になってここはどうしようかと考えてしまうから。

で、困った原因はというと、Runnerクラスってこのアプリの中心的存在なのに、いちいち消したり、ファイルから呼び出したりするもの?って疑問に思えたからなんです。
最初にこのクラスをListにしておいて、エントリーするときや、計測するときはこのリストを渡してしまえばいいんじゃないかって思ったんですよね。

だいたい、メニューを行き来する間に消えてしまうクラスにデータを保持させておくのは効率が悪いですよね?

なので、スタティックなRunnerクラスを束ねたRunnerListを作るのがいいんじゃないかと思い、作ってみようかと思います。

まずいことに、スクロールビューに並べているオブジェクトに Runner と名前をつけているので、まず、これを RunnerEntryView とRenameしておきます。

Refacter - Rename どちらにもありますね。まあ、あるのが普通なんでしょうけど。


さて、こんなクラスのイメージですが・・・

import UIKit

class Runner {
    
    let myNumber: Int
    private var myName: String
    var entry: Bool = false
    var lap: Int = 0
    private var time: [Int]     // タイムは100倍して切り捨て
    
    
    init(number: Int) {
        
        myNumber = number
        myName = ""
        time = []
        
    }
    
    // 名前セッター・ゲッター
    func setName(name: String) {
        myName = name
    }
    
    func getName() -> String {
        return myName
    }
    
    
    // ラップデータの処理
    func setLapTime(rapTime: Float) {
        // 100倍して整数化
        let time100 = Int(rapTime*100)
        time.append(time100)
        lap += 1    // ラップ数をカウント
    }
    
    // ラップボタン
    func LapButton(frame: CGRect) -> UIButton {
        
        let lapButton: UIButton = UIButton(frame: frame)
        lapButton.setTitle(myName, for: .normal)
        
        return lapButton
    }
    
    // エントリー用のビュー
    func EntryView(frame: CGRect) -> RunnerEntryView {

        let entryView: RunnerEntryView = RunnerEntryView(frame: frame, number: myNumber)
        
        return entryView
        
    }

    
}

最後にある関数はスクロールビューに乗っけたものを、このクラスのメンバーとして返すように作り直そうと思っている物です。こうしておくと、名前も入れやすいし、便利なのではないかと。

もう一つのラップボタンの処理は、タイムを記録するのもこの Runner クラスなのかな?と思いながら、とりあえず UIButton を返す関数として作っています。
このボタンの挙動はこのクラスに書くべきなんでしょう。きっと。

第22回 Androidでスクロールビュー

ちょっと、イベントがあり、留守にしてました。
早速ですが、続きで、スクロールビューの設置。

Android版で注意するべきことが一つあり、スクロールビューにのせることができるのは一つのオブジェクトになっているということです。なので、中に入れるレイアウトを、リラティブレイアウト を利用することにしました。個人的な趣味により、リニアレイアウトは使いません。

では、まず、一人分の Runnerクラスから。

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;

public class Runner extends View {

    int myWidth;
    int myHeight;

    final float lineWidth;
    final int myNumber;

    String name = "未登録";        //氏名表示
    Boolean entry = false;         // エントリーしたか?


    public Runner(Context context, int number, int width, int height) {
        super(context);
        myNumber = number;
        myWidth = width;
        myHeight = height;

        lineWidth = width * 0.01f;

        setBackgroundColor(Color.argb(255,255,255,180));

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        final float numberWidth = myWidth * 0.22f;

        // アンドロイドではcanvasを使用する,canvasは呼び出し元にあるので引数で受け取る

        // android では Paintクラスを使って色やスタイルを決めていく
        Paint paint = new Paint();

        // 両サイド
        paint.setStrokeWidth(lineWidth);
        paint.setColor(Color.BLACK);
        canvas.drawLine(lineWidth/2, 0, lineWidth/2, myHeight,paint);
        canvas.drawLine(myWidth-lineWidth/2, 0, myWidth-lineWidth/2, myHeight,paint);

        // 上下サイド
        paint.reset();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(lineWidth*0.3f);
        canvas.drawLine(0,lineWidth*0.15f,myWidth,lineWidth*0.15f,paint);
        canvas.drawLine(0,myHeight-lineWidth*0.15f,myWidth,myHeight-lineWidth*0.15f,paint);

        // 見た目が悪いので、最初の分だけ上の線を太く。
        if (myNumber == 1) {
            paint.reset();
            paint.setStrokeWidth(lineWidth*0.8f);
            paint.setColor(Color.BLACK);
            canvas.drawLine(0,lineWidth*0.4f,myWidth,lineWidth*0.4f,paint);
        }

        // 中間の仕切り線
        paint.reset();
        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(lineWidth*0.6f);
        canvas.drawLine(numberWidth,0.0f,numberWidth,myHeight,paint);

        // 番号の描画
        paint.reset();
        if (name=="未登録") {
            paint.setColor(Color.LTGRAY);
        } else {
            paint.setColor(Color.BLACK);
        }
        paint.setTextSize(myHeight*0.5f);
        paint.setTextAlign(Paint.Align.CENTER);
        final String number = String.valueOf(myNumber);
        canvas.drawText(number,numberWidth*0.5f,myHeight*0.7f,paint);

        paint.setTextSize(myHeight*0.6f);
        paint.setTextAlign(Paint.Align.CENTER);
        canvas.drawText(name,numberWidth*0.5f+myWidth*0.5f,myHeight*0.7f,paint);

    }
}

途中で、entryというBooleanの変数が入っていますが、これはまだ使っていません。いずれ使うことになるだろうなと思いながら書いています。

だいたい、オブジェクト指向プログラムというのは、オブジェクトにその属性となる変数(プロパティ)や関数(メソッド)を入れるものだから、このRunnerオブジェクトというのは、名前、タイム、周回数、タイム、エントリーの状況、ボタンの位置など様々なプロパティを持つことが予想されます。ただ、心配な状況として、エントリーが終わると廃棄されるのか、そもそも、Viewオブジェクトでいいのか?みたいなちょっと心配な状況があります。

この辺りが、教科書にない自分で考えなければならないところなんですよね。

さて、続いて、スクロールビューのクラスですが、インナークラスとしてリラティブレイアウト を入れました。おかしいかもしれませんが、スクロールビューのコードが少ないのでいい感じです。

import android.content.Context;
import android.widget.RelativeLayout;
import android.widget.ScrollView;

public class RunnerList extends ScrollView {

    /**
     * このクラスはスクロールビューの中におくものとして作成
     * 内部クラスとして一つのリラティブレイアウト を作成し
     * スクロールビューにのせる
     */

    // 一人分のサイズ
    private int aWidth;
    private int aHeight;

    // このクラスには選手名のリストが必要

    // 暫定的に50人いるとして、ビューを作成
    private int member = 50;


    // コンストラクタ
    public RunnerList(Context context,int width, int height) {
        super(context);

        aWidth = width;
        aHeight = height;

        RunnerLayout runnerLayout = new RunnerLayout(getContext());

        addView(runnerLayout);
    }


    class RunnerLayout extends RelativeLayout {


        // コンストラクタ
        public RunnerLayout(Context context) {
            super(context);

            for (int i=1; i<=member; i++) {

                final Runner runner = new Runner(getContext(),i,aWidth,aHeight);
                runner.setId(i);        // ビューにidセット

                // Androidの位置決めはレイアウトパラメータで指定する
                RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
                        aWidth,aHeight);

                if (i == 1) {
                    // 1番目のビューは親ビューを基準にする
                    params.addRule(RelativeLayout.CENTER_IN_PARENT);
                    params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
                } else {
                    // 2番目以降は前のビューを基準にする
                    params.addRule(RelativeLayout.CENTER_IN_PARENT);
                    params.addRule(RelativeLayout.BELOW,i - 1);
                }

                addView(runner,params);

            }
        }
    }
}

Runnerビューを縦にずらずらっと並べるのですが、idをセットし、一つ前のビューの真下に順番に置いています。多分これがいいやり方だと思うのですが。
なお、1番目だけは、親ビューを基準にしています。

そして、スクロールビューでは Addview するだけで終わっています。

このクラスの呼び出しは、前に作ったEntryViewにおきます。(ボタン配置の後)

        addView(buttons,buttonsParams);

        // スクロールビュー設置
        // 一人分のサイズ
        final int aWidth = myWidth*80/100;
        final int aHeight = myWidth*13/100;

        final RunnerList runnerList = new RunnerList(getContext(),aWidth,aHeight);

        RelativeLayout.LayoutParams runnerListParams = new RelativeLayout.LayoutParams(
                myWidth*80/100, myHeight-myWidth*40/100
        );
        runnerListParams.addRule(CENTER_IN_PARENT);
        runnerListParams.addRule(CENTER_IN_PARENT);

        addView(runnerList,runnerListParams);
    }
}

もう一つ悩みが・・・
int と float が混ざりすぎてて厄介ですよね?
ちょっと課題です。

結果としてはほぼ同じ状態となりました。

f:id:momonga117:20180522111553p:plain

第21回 swiftでスクロールビュー

 スクロールビューって、利用しない手はないのに、なかなかいいサンプルにお目にかかりません。ぜひぜひ参考にしてください。結構、教科書的に書けたように思います。

プログラムは3つの部分に分かれます。
A:スクロールビューを呼び出すプログラム
B:スクロールビュー本体
C:スクロールビューに配置するオブジェクト

f:id:momonga117:20180518101018p:plain

xcodeの階層表示ですが、クリーム色の小さな長方形がCです。

色々付け加えてますが、まず、CのオブジェクトをRunnerクラスとして定義しました。

import UIKit

class Runner: UIView {
    
    let thiswidth: CGFloat
    let thisheight: CGFloat
    
    let lineWidth: CGFloat
    let myNumber: Int
    
    var name: String = "未登録"        //氏名表示
    var entry: Bool = false         // エントリーしたか?
    
    
    init(frame: CGRect, number: Int) {
        // 自前のイニシャライザ
        thiswidth = frame.width
        thisheight = frame.height
        
        lineWidth = frame.width*0.01
        myNumber = number
        super.init(frame: frame)
        // 背景色をセット、エントリー時には色を変える 色は未調整
        if entry {
            backgroundColor = UIColor(red: 255/255, green: 255/255, blue: 240/255, alpha: 255/255)
        } else {
            backgroundColor = UIColor(red: 255/255, green: 255/255, blue: 180/255, alpha: 255/255)
        }
        
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        // いつものビューへの描画

        // 背景色をセット、エントリー時には色を変える 色は未調整
        if entry {
            backgroundColor = UIColor(red: 255/255, green: 255/255, blue: 240/255, alpha: 255/255)
        } else {
            backgroundColor = UIColor(red: 255/255, green: 255/255, blue: 180/255, alpha: 255/255)
        }

        // 番号枠,氏名枠の幅計算
        let numberWidth = thiswidth * 0.22
        let nameWidth = thiswidth - numberWidth - lineWidth * 3
        
        // コンテキストの取得
        let context = UIGraphicsGetCurrentContext()
    
        // 両サイド
        context?.setLineWidth(lineWidth)
        context?.move(to: CGPoint(x: lineWidth/2, y: 0))
        context?.addLine(to: CGPoint(x: lineWidth/2, y: thisheight))
        context?.move(to: CGPoint(x: thiswidth-lineWidth/2, y: 0))
        context?.addLine(to: CGPoint(x: thiswidth-lineWidth/2, y: thisheight))
        context?.strokePath()
        
        // 上下サイド
        context?.setLineWidth(lineWidth*0.3)
        context?.move(to: CGPoint(x: 0, y: lineWidth*0.15))
        context?.addLine(to: CGPoint(x: thiswidth, y: lineWidth*0.15))
        context?.move(to: CGPoint(x: 0, y: thisheight - lineWidth*0.15))
        context?.addLine(to: CGPoint(x: thiswidth, y: thisheight - lineWidth*0.15))
        context?.strokePath()
        
        // 見た目が悪いので、最初の分だけ上の線を太く。
        if myNumber == 1 {
            context?.setLineWidth(lineWidth*0.8)
            context?.move(to: CGPoint(x: 0, y: lineWidth*0.4))
            context?.addLine(to: CGPoint(x: thiswidth, y: lineWidth*0.4))
        }
        
        // 中間の仕切り線
        context?.setLineWidth(lineWidth*0.6)
        context?.move(to: CGPoint(x: numberWidth, y: 0))
        context?.addLine(to: CGPoint(x: numberWidth, y: thisheight))
        context?.strokePath()
        
        
        // 番号の描画
        let textcolor = name=="未登録" ? UIColor.lightGray : UIColor.black
        let numCenter: CGPoint = CGPoint(x:numberWidth*0.5, y: thisheight*0.5)
        centerString(string: String(myNumber), charSize: thisheight*0.5, center: numCenter, color: textcolor)
        
        // 氏名の描画
        let nameCenter: CGPoint = CGPoint(x: numberWidth + lineWidth + nameWidth * 0.5, y: thisheight*0.5)
        centerString(string: name, charSize: thisheight*0.6, center: nameCenter, color: textcolor)
        
        
        
    }
    
    func centerString(string: String, charSize: CGFloat, center: CGPoint, color: UIColor) {
        // 文字列、文字サイズ、中央を示す座標、
        let stringAttributes: [NSAttributedStringKey : Any] = [
            NSAttributedStringKey.foregroundColor : color,
            NSAttributedStringKey.font : UIFont.systemFont(ofSize: charSize)
        ]
        
        let nsstr = string as NSString
        
        // サイズを取得
        let textSize: CGSize = nsstr.size(withAttributes: stringAttributes)
        let textTopLeft = CGPoint(x: center.x - textSize.width/2, y: center.y - textSize.height/2)
        
        // 文字列の描画
        nsstr.draw(in: CGRect(origin: textTopLeft, size: textSize), withAttributes: stringAttributes)
    }
}

体裁にこだわったため、少し長くなりました。
でも、これができたら、あとは楽です。

スクロールビューは継承して作っています。

import UIKit

class RunnerList: UIScrollView {
    
    private var aWidth: CGFloat
    private var aHeight: CGFloat
    
    
    // このクラスには選手名のリストが必要
    
    // 暫定的に50人いるとして、ビューを作成
    
    var member: Int = 50
    
    override init(frame: CGRect) {
        // 一人分のサイズをもらっておく
        aWidth = 0.0
        aHeight = 0.0
        
        
        super.init(frame: frame)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    init(frame: CGRect, unitWidth: CGFloat, unitHeight: CGFloat) {
        // 自前のイニシャライザ
        // 一人分のサイズをもらっておく
        aWidth = unitWidth
        aHeight = unitHeight
        super.init(frame: frame)
        
        // ここで走者リストの確認(後で実装)
        // メンバー数だけRunner(UIView)を作成
        // 初期状態では50番まで氏名がなくとも枠を作成する。
        
        self.contentSize = CGSize(width: unitWidth, height: unitHeight*50)
        
        
        for i: Int in 1...50 {
            
            let myRect = CGRect(x: 0.0, y: unitHeight * CGFloat(i-1), width: unitWidth, height: unitHeight)
            let unit: Runner = Runner(frame: myRect, number: i)
            addSubview(unit)
        }
    }
}

50個のRunnerオブジェクトを乗せているのがわかると思います。でも、大事なのは、self.contentSizeのところで、ここで、スクロールするかどうか決まります。指定するのを忘れないでください。

なお、一人分のサイズは呼び出し側よりもらっています。

呼び出し側はこんな感じです。

        let listFrame: CGRect = CGRect(x: myWidth * 0.1, y: myWidth * 0.2, width: myWidth * 0.8, height: myHeight - myWidth * 0.4)
        
        let list: RunnerList = RunnerList(frame: listFrame, unitWidth: myWidth * 0.8, unitHeight: myWidth * 0.13)
        
        addSubview(list)

単純ですが、frameの数値を変えたり、unitWidth,unitheightの数値を変えると、見栄えがすごく変わってしまいます。


完成した画面がこちら。

f:id:momonga117:20180518103409p:plain

スクロールしているところはお見せできませんが、実機でやってみてもツルンとスクロールしてくれて、気持ちいいですよ。

ただ、ここまでは、そんなに大変じゃないんですが、スクロールビューをタッチして、選択したり、名前登録に入ったり、まだまだ作り込まないといけません。そして、こういうところが、結構情報不足だったりするんですよね。

多分、このアプリで一番大変なのはこの辺りではないかと思っています。

ご参考になれば、幸いです。

第20回 setMarginがどこかに消えた!

ボタンを貼り付けました。
と言っても、見た目だけです。
なぜかというと、以前はあったはずのsetMarginが出てこない。Viewを生成し、setMerginを使おうとしても出てこない・・・謎。
今回、左ボタンは、左側のマージンを指定、右ボタンは、右側のマージン指定と考えていたのですが、この方法でボタンを配置するのは諦めました。

この謎は未だ解決していません。今までマージンを使ってやったことがないので、本当のところは良くわかりません。XMLで指定したら楽なのかもしれませんが。

そこで、2つの解決策を考えてみました。
ひとつ目は以前よく使ってた方法で、見えないViewを基準に配置し、その両側に、ボタンを配置する方法。
もう一つは、ひとつのビューを設置し、座標を計算して、ボタンに見えるように配置する方法。
できてしまえば、ボタンを貼り付けても、ビューにボタンを描いても中身は変わらないのですが。

f:id:momonga117:20180517192242p:plain

なんだかんだと考えて、2番目の方法を採用してみました。理由の一つは、プログラム上、一つのクラスにまとめることができたので、処理が書きやすくなったかなと思っています。

f:id:momonga117:20180517185206p:plain

出来上がったのがこちら。
体裁はだいたい同じになりました。
プログラムですが、呼び出す側がこちら。

        // ビューのサイズは下から横幅の15%
        EntryButtons buttons = new EntryButtons(getContext(),myWidth,myWidth*15/100);
        RelativeLayout.LayoutParams buttonsParams = new LayoutParams(
                myWidth,myWidth*15/100
        );
        buttonsParams.addRule(RelativeLayout.CENTER_IN_PARENT);
        buttonsParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);

        addView(buttons,buttonsParams);

そして呼び出される側として新しいクラスを導入しました。

public class EntryButtons extends View {

    // エントリー画面の下部の2個のボタンを作る
    private String button1Text = "メニューに戻る";
    private String button2Text = "走者選択終了";

    // このビューのサイズ
    private final int myWidth;
    private final int myHeight;


    public EntryButtons(Context context, int width, int height) {
        super(context);
        myWidth = width;
        myHeight = height;
    }

    public void firstButton() {
        button1Text = "メニューに戻る";
        button2Text = "走者選択終了";
        invalidate();
    }

    public void secondButton() {
        button1Text = "走者選択へ";
        button2Text = "決定する";
        invalidate();
    }

    @Override
    public void draw(Canvas canvas) {

        super.draw(canvas);
        Paint paint = new Paint();

        // グリーンの長方形
        paint.setColor(Color.argb(255,50,150,50));

        // 長方形のサイズを計算
        final Rect button1Rect = new Rect(
                myWidth*7/100, 0,myWidth*47/100, myHeight*2/3
        );
        final Rect button2Rect = new Rect(
                myWidth*53/100, 0,myWidth*93/100, myHeight*2/3
        );

        // 長方形を描く
        canvas.drawRect(button1Rect,paint);
        canvas.drawRect(button2Rect,paint);

        // 文字入れ
        paint.reset();
        paint.setColor(Color.WHITE);
        paint.setTextSize(myHeight*0.3f);
        paint.setTextAlign(Paint.Align.CENTER);

        canvas.drawText(button1Text,myWidth*0.27f,myHeight*0.43f,paint);
        canvas.drawText(button2Text,myWidth*0.73f,myHeight*0.43f,paint);
    }
}

途中にある2つのメソッド(firstButtonとsecondButton)は画面の切り替え時に、文字だけ変えてしまおうという手抜きですが、表示すべき文字を置き換えて、invalidate();を発行すると、drawで書き換えてくれる仕組みです。

自分で言うのもなんだけど、なんか、日曜大工的な感じがして「楽しんでるならいいんじゃない?」って思ってしまいます。

次はいよいよ、スクロールビューの設置に入ります。

第19回 swift で UIButton!

swift で ボタンを作ってみます。

        // UIButton 2個、「メニューに戻る」「奏者選択終了」
        // ボタンのサイズは共通
        let buttonSize: CGSize = CGSize(width: myWidth*0.4, height: myWidth*0.1)
        let cancelButtonPoint: CGPoint =
            CGPoint(x: myWidth*0.07, y: myHeight-myWidth*0.15)
        let okButtonPoint: CGPoint =
            CGPoint(x: myWidth*0.53, y: myHeight-myWidth*0.15)
        let buttonBackColor = UIColor(red: 50/255, green: 150/255, blue: 50/255, alpha: 255/255)
        
        
        
        let cancelButton =
            UIButton(frame: CGRect(origin: cancelButtonPoint, size: buttonSize))
        cancelButton.setTitle("メニューに戻る", for: UIControlState.normal)
        cancelButton.backgroundColor = buttonBackColor
        
        let okButton =
            UIButton(frame: CGRect(origin: okButtonPoint, size: buttonSize))
        okButton.setTitle("走者選択終了", for: .normal)
        okButton.backgroundColor = buttonBackColor
        
        addSubview(cancelButton)
        addSubview(okButton)

ボタンのサイズは横幅の40%に決め撃ちです。
左右に7%、中央に6%の余白を開けています。
文字が溢れないようにこのサイズにしましたが、大きな文字表示に設定していますと、はみ出す可能性があるかもしれません。悩むな〜。

これによって、iPAD,iPhone8,iphone5Sでシュミレートしたら、次のようになりました。

f:id:momonga117:20180517142606p:plain

ボタン内の余白が調整されているのがわかると思います。

が、しかし。しかしですよ。ねえ。(って誰に行ってるんだか・・・)
これって本当にUIButtonクラスを使う必要があるのでしょうか?

UIButtonクラスって本当に便利なんですか?
教科書には決まって最初に書かれています。
文字を入れたりとか、なんとなく他と違うし、結構お面倒だし、色々な便利な機能を用意しているのはわかるのですが、そのほとんどを使ってないし。
おまけに、addTargetがいまいちよくわからないし。(素人なもので。)

もひとつおまけなんですが、仕様がよく変わりますよね?

CGRectZEROがCGRect.zeroになってたり、CGRectMakeがなくなってたり。(ちょっと前に買った)解説本が悩みのタネになったりしてませんか?

時間に余裕のない素人が、最新情報を追っかけるのはとても辛いんですよね。

ということで、画面はあとスクロールビューの設置へと向かいます。

スクロールビューだけは攻略しないと自前ではなんともならない気がします。

第18回 AndroidのTextViewでサイズ指定の肝!

iOSでUILabelを使ったことは、AndroidではTextViewでやるといいようです。
氏名の入力で使うことになる予定ですが、テキストの入力は、iOSではUITextView、Androidでは、EditTextというクラスを使うようです。

この辺りはなるべくシンプルにいきたいです。

で、前回のプログラムのAndroidバージョンです。

    public EntryView(Context context,int width,int height) {
        super(context);
        setBackgroundColor(Color.argb(255,100,240,255));
        // このビューのサイズを受け取っておく
        myWidth = width;
        myHeight = height;

        runningSelect();

    }

    private void runningSelect() {

        // ランナーを指定する画面。
        // エントリー用のパーツを配置する

        // Entry(走者名の登録) TextView
        TextView title = new TextView(getContext());
        title.setText("Entry(走者名の登録)");
        final float size = myWidth * 0.07f;
        title.setTextSize(TypedValue.COMPLEX_UNIT_PX,size);
        title.setTextColor(Color.BLACK);

        LayoutParams titleParams = new LayoutParams(
                LayoutParams.WRAP_CONTENT,
                LayoutParams.WRAP_CONTENT);
        titleParams.addRule(ALIGN_PARENT_TOP);
        titleParams.addRule(CENTER_HORIZONTAL);

        this.addView(title,titleParams);
    }

文字のサイズ指定ですが、
title.setTextSize(TypedValue.COMPLEX_UNIT_PX,size);
という指定方法なんですね。
ピクセル単位で、このサイズで描いてね〜って感じでしょうか。

あと、swiftではsiztToFitで済ませたものが、AndroidではWRAP_CONTENT、縦と横に指定しています。
位置は、親ビューのTop,Centerで指定しています。

で、出来上がったのがこちらです。

f:id:momonga117:20180516103826p:plain

サイズ的には問題ないですが、swiftでWeight指定したために、文字の太さが変わってしまいました。むむ、ちょっと気になる。

また、調べておきます。

第17回 iPhoneとiPadで似た表示を作る。(UILabel使用)

ソースコードをすべてアップしてませんが、前回までで、メインメニューと4つの画面の行き来ができるようになっています。

私のブログを読んでくださっている方の中には、プログラム精通者も入れば、初心者の方もいると思いますし、若い方も、ご年配の方もいることと思います。

ちらっと私のプログラム遍歴を披露しておきますね。

基本はBASICという世代です。シャープ、富士通NEC、日立・・・国産のパソコンが華やかなりし頃、色々なBASICを触りましたね。

スピードが不満で、アセンブラに手を出したり。アセンブラは劇的に早かったです。というか、BASICが遅すぎ。

CやC++に手を出すも、大きな作品は作っていません。

その後、仕事でエクセルマクロに手を染めますが、これが意外と、オブジェクト指向だったんだなと、今にして分かります。

そんなわけで、私のプログラミングスタイルが少しは理解していただけるかもしれません。

さて、今回は、エントリー画面なんですが、以前作ったアプリはIOS版でここにピッカービューを使っていました。ピッカービューの見た目が面白いという理由だけで使ったのですが、これがAndroidへの移植を阻んでしまった大きな原因でした。今回リニューアルをする上で、iOS,Androidの両方でリリースすることが前提であること、および、ここの使い勝手を大幅に向上させたいという理由から、スクロールビューを使用することにします。

イメージをスケッチしてみました。

f:id:momonga117:20180515094118p:plain

左側が、走者選択画面です。
登録した氏名がスクロールビューに一覧表示され、タップすることで、選択される。
選択が終了したら、決定ボタンを押し、右の画面に。

サンプル画面はボタン配置を示す図が表示されるエリアで、何列に表示するか決めてもらうところです。

今回の画面は、スクロールビューを含め、UI部品を多用してみたいと思います。
この程度なら、ストーリーボード使った方がという、ご意見は無視させていただきます。
デザイン上の問題から、最初のスケッチと変わる可能性は十分ありますが、そういうことは日常茶飯事なので、あしからず・・・

で、左側の第1画面から作成しますが、ラベル2個、ボタン4個をまずおいてみます。空いた場所にスクロールビューを可能な限り縦に長く入れてみようかと思います。

とりあえず、swiftで。

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor(red: 100/255, green: 240/255, blue: 255/255, alpha: 255/255)
        // このビューのサイズを取っておく
        myWidth = frame.width
        myHeight = frame.height
        runnerSelect()
        
  
    }
    
    func runnerSelect() {
        
        // ランナーを指定する画面。
        // エントリー用のパーツを配置する
        
        // Entry(走者名の登録) UILabel
        let title = UILabel(frame: CGRect.zero)     // この時点でサイズは決めない
        title.text = "Entry(走者名の登録)"
        title.font = UIFont.systemFont(ofSize: myWidth*0.07, weight: .semibold)
        title.sizeToFit()
        addSubview(title)
        title.center = CGPoint(x: myWidth*0.5, y: myHeight*0.05)
        
    }

今回の肝は、フォントのサイズ指定のところ。
title.font = UIFont.systemFont(ofSize: myWidth*0.07, weight: .semibold)
というところですね。
画面幅を基準にフォントサイズを指定すると、iPhoneでもiPadでも似たような表示になるよってところです。

iPadに対しては、ビューの横幅を少し削り、本当の横幅より小さい目のサイズをmyWidthに渡せば、もう少し、よくなるかもしれません。

左がiPhone、右がiPad.

文字の感覚的な大きさはほぼ同じではないでしょうか?

f:id:momonga117:20180515225855p:plain

第16回 AndroidではRunnableで画面遷移

前回は、swiftでクロージャを使って画面遷移をしましたが、javaでは、Runnableを使うと同じようなことができます。

前回の繰り返しですが、

1.呼び出される側に、ランナブル変数を置く。
2.呼び出し側から、このランナブルに「どこに制御が移るか」を教える。
3.呼び出される側のイベント発生時に、このランナブルを使って、呼び出し側に制御を移す。

となります。

まず、1番ですね。
セッターも一緒に書いておきます。

public class MainMenu extends View {

    // メニューへ飛ぶためのランナブル
    private Runnable entryViewRunnable;
    private Runnable settigViewRunnnable;
    private Runnable runningViewRunnnable;
    private Runnable recordViewRunnnable;


    // コンストラクタ
    public MainMenu(Context context) {
        super(context);
    }

    // 分岐先を渡すためのランナブルをセット(4種)
    public void setEntryViewRunnable(Runnable runnable) {
        entryViewRunnable = runnable;
    }

    public void setSettigViewRunnnable(Runnable runnable) {
        settigViewRunnnable = runnable;
    }

    public void setRunningViewRunnnable(Runnable runnable) {
        runningViewRunnnable = runnable;
    }

    public void setRecordViewRunnnable(Runnable runnable) {
        recordViewRunnnable = runnable;
    }

swift版とあまり違わないですね。

次にランナブルをセットする呼び出し側。

    public void showMainmenu() {

        // 以前のビューは消しておく
        this.removeAllViews();
        // 端末のサイズを取得
        final int width = this.getWidth();
        final int height = this.getMeasuredHeight();

        // MainMenu のインスタンスを作成。
        MainMenu mainMenu = new MainMenu(getContext());

        // Runnableを4個セット
        mainMenu.setEntryViewRunnable(new Runnable() {
            @Override
            public void run() {
                showEntryMenu();
            }
        });

        mainMenu.setSettigViewRunnnable(new Runnable() {
            @Override
            public void run() {
                showSetting();
            }
        });
        mainMenu.setRunningViewRunnnable(new Runnable() {
            @Override
            public void run() {
                showRunningView();
            }
        });
        mainMenu.setRecordViewRunnnable(new Runnable() {
            @Override
            public void run() {
                showRecordView();
            }
        });

AndroidStudioで作業してると、結構自然に補完してくれるので、楽です。
ちょっと名前のちぐはぐなところもあります。あまり、関係ないですが。


最後に、タッチの判定でジャンプするところ。

 void judge(float x,float y) {

        float width = getWidth();
        float height = getHeight();

        // 4種の判定
        // 4コマンド(エントリー・設定・ラップ計測・記録)
        final float y1 = height/2 - width*0.4f;
        final float y2 = height/2 - width*0.3f;
        final float y3 = height/2 + width*0.3f;
        final float y4 = height/2 + width*0.4f;
        final float x1 = width * 0.05f;
        final float x2 = width * 0.3f;
        final float x3 = width * 0.7f;
        final float x4 = width * 0.95f;
        // 判定
        if (y > y1 && y < y2) {
            if (x > x1 && x < x2) {
                // エントリーメニューの呼び出し
                entryViewRunnable.run();
            } else if (x > x3 && x < x4) {
                // 設定メニューの呼び出し
                settigViewRunnnable.run();
            }
        } else if (y > y3 && y < y4) {
            if (x > x1 && x < x2) {
                // ラップ計測画面の呼び出し
                runningViewRunnnable.run();
            } else if (x > x3 && x < x4) {
                // 記録呼び出し画面へ
                recordViewRunnnable.run();
            }
        }

ランナブルの後ろに .run() とつければいいのですね。

これで、大きな画面移動は完了です。
サブメニューから、元に戻るのも同じ要領ですね。


swiftでビューコントローラー間を行ったり来たりするのも良いですし、javaでActivity間を移動するのもたくさん紹介されていますので、このような異端?とも言える方法があると知ってもらえたらいいのでは、と思っています。

第15回 swift、ハンドラーとかクロージャとか

よくわからないんです。
初心者にとっては絶壁に登るような感覚ですよね?

なので、初心者目線で、説明して見ます。

クロージャというのは、「次にここから実行してね。」と伝える手段という見方でいいと思います。
あるエリアがタップされたら、次は(同じクラスの)ここだよってボタンに設定するのは、イベントハンドラーで、あちこちに例題があるし、割と簡単なんです。

でも、2つのクラスにまたがっているともう大変です。
呼び出したクラスのビューでタップされて、呼び出した側のある場所から実行したいとなると途端に難しくなる。

手順としては、次のようになります。

1.呼び出される側に、ハンドラーを置く。
2.呼び出し側から、このハンドラーに「どこに制御が移るか」を教える。この実態がクロージャ
3.呼び出される側のイベント発生時に、このハンドラーを使って、呼び出し側に制御を移す。

えーっ、これだけ〜?
そうなんです。これだけなんです。
初心者目線でしょ?簡単な気がしませんか?

じゃ、swift で書いていきますね。

まず、1の手順。
クラス内の変数にハンドラーを置くのでした。
ハンドラーを置くと同時に、ハンドラーのセッターも書いておきます。セッターはハンドラー1つにつき1つずつ作ります。
ハンドラーはプロパティーでセットしないで(多分できない?)セッターを使うのがよろしいようで。

私はあえて、private設定です。

import UIKit

class MainMenu: UIView {
        
    // メニューへ飛ぶためのハンドラー
    private var entryViewHandler: (() -> Void)?
    private var settingViewHandler: (() -> Void)?
    private var runningViewHandler: (() -> Void)?
    private var recordViewHandler: (() -> Void)?
    
    // 分岐先を渡すためのハンドラーのセッター(4種)
    func setEntryViewHandler(handler: @escaping () -> Void) {
        self.entryViewHandler = handler
    }
    
    func setsettingViewHandler(handler: @escaping () -> Void) {
        self.settingViewHandler = handler
    }
    
    func setRunningViewHandler(handler: @escaping () -> Void) {
        self.runningViewHandler = handler
    }
    
    func setRecordViewHandler(handler: @escaping () -> Void) {
        self.recordViewHandler = handler
    }

私のアプリでは4つの飛び先があるので、ハンドラーは4つです。
必要に応じて作ればいいですね。

続いて、2番のハンドラーのセット。

こちらは呼び出し側にあります。

    func showMainMenu() {
        
        // 以前のビューは消しておく
        for myview in self.view.subviews {
            myview.removeFromSuperview()
        }
        // MainMenu のインスタンスを作成。
        let mainMenu:MainMenu = MainMenu(frame: CGRect.zero)
       
        // ハンドラーのセット
        mainMenu.setEntryViewHandler(handler: showEntry)
        mainMenu.setsettingViewHandler (handler: showSetting)
        mainMenu.setRunningViewHandler (handler: showRunning)
        mainMenu.setRecordViewHandler (handler: showRecord)
        
        // ビューのサイズを設定
        mainMenu.frame = CGRect(x: 0, y: 0, width: width, height: height)
        
        mainMenu.center = CGPoint(x: width/2, y: height/2)
        self.view.addSubview(mainMenu)

    }

メインメニューのインスタンスを作った後に入れてます。
ハンドラーをセットするのも4個ですね。


最後にこのハンドラーの実行部分です。
結構ハマりやすいので気をつけてください。

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        // タッチアップの位置で判定する
        let touches = event?.allTouches
        let touch = touches?.first?.location(in: self)
        // 4コマンド(エントリー・設定・ラップ計測・記録)
        let y1 = viewHeight/2 - viewWidth*0.4
        let y2 = viewHeight/2 - viewWidth*0.3
        let y3 = viewHeight/2 + viewWidth*0.3
        let y4 = viewHeight/2 + viewWidth*0.4
        let x1 = viewWidth * 0.05
        let x2 = viewWidth * 0.3
        let x3 = viewWidth * 0.7
        let x4 = viewWidth * 0.95
        // 判定
        if touch!.y > y1 && touch!.y < y2 {
            if touch!.x > x1 && touch!.x < x2 {
                // エントリーメニューの呼び出し
                if entryViewHandler != nil {
                    entryViewHandler!()
                }
            } else if touch!.x > x3 && touch!.x < x4 {
                // 設定メニューの呼び出し
                if settingViewHandler != nil {
                    settingViewHandler!()
                }
            }
        } else if touch!.y > y3 && touch!.y < y4 {
            if touch!.x > x1 && touch!.x < x2 {
                // ラップ計測画面の呼び出し
                if runningViewHandler != nil {
                    runningViewHandler!()
                }
            } else if touch!.x > x3 && touch!.x < x4 {
                // 記録呼び出し画面へ
                if recordViewHandler != nil {
                    recordViewHandler!()
                }
            }
        }
    }

4箇所のタッチアップを判定してます。
ここで、if文でnilでないことを確認しないと、わけのわからないエラーが発生します。

Expression resolves to an unused l-value などというエラーに付き合いたくないなら、この構文をお勧めします。

もし、私のプログラムを実行させようと考えているなら、
EntryVIew.swiftなどの飛び先になるクラスを作ってお試しください。