「筋トレタイマー」…名前の響きはいいのだけど
Androidアプリは基本的にアプリ名で何のソフトか分からないと
手にとってもらえないことを学んだので
アプリ名は「ボイスタイマー」にしようと思う。
半分くらいできた。あとちょっと。
デザインとか細かいことは一旦出してからにしよう。
Androidスマホにして、せっかくプログラマブルな端末を手にしたのだから
日常の不便をプログラムで解決してしまいたい。
まずは下調べ。Androidの音声入力、認識、発声ってどんな感じなのかなと。
良いサイトがあった。
Androidで音声入力した内容を認識し,そのまま音声合成。「おうむ返し」アプリのソースコード - 主に言語とシステム開発に関して
ソースコードをコピペして起動…
くりま「Ten minutes before 8 o'clock!
アプリ「ten minutes before 8 o clock
お~。すごい。流暢にオウム返ししてくれる。
こんなんがつくれちゃうのかぁー。
日本語入力だとどうなんだろ。
でも、なんとなく行けそうな予感。
前回の続き。
WPFを使って実験用アプリを作ってみた。
試してみるとズームアウトがどうもじれったい。
実際現実世界もこんなかんじかもと思いつつも使いづらいのはよろしくない。
そこで、ズームアウト時の拡大率にはズームイン時の拡大率の逆数使うようにしてみた。
それが上のチェックボックス。
いいような気もするが、そんなことするならなんでモデリングしたんだっけか…。
以下ソースコード。
MainWindow.xaml
<Window x:Class="ImageZooming.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525" Loaded="window_Loaded"> <DockPanel Height="Auto" Width="Auto" LastChildFill="True"> <CheckBox DockPanel.Dock="Top" Name="checkBox" Content="縮小率に拡大率の逆数を使う" Canvas.Left="109" Canvas.Top="59" Checked="checkBox_Checked" Unchecked="checkBox_Unchecked"/> <ScrollViewer DockPanel.Dock="Bottom" x:Name="scrollViewer" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" PreviewMouseWheel="scrollViewer_PreviewMouseWheel"> <Canvas x:Name="canvas"> <Image x:Name="image" Stretch="Fill" Width="{Binding Source.PixelWidth, RelativeSource={RelativeSource Self}}" Height="{Binding Source.PixelHeight, RelativeSource={RelativeSource Self}}"/> </Canvas> </ScrollViewer> </DockPanel> </Window>
MainWindow.xaml.cs
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace ImageZooming { /// <summary> /// MainWindow.xaml の相互作用ロジックs /// </summary> public partial class MainWindow : Window { const double MAGNIFICATION_LEVEL_1 = 1.1; const double VIEW_ANGLE = Math.PI / 4; private TransformGroup _canvasTransformGroup = new TransformGroup(); private int _zoomLevel = 0; private double _canvasOriginalWidth; private double _canvasOriginalHeight; public MainWindow() { InitializeComponent(); } private void window_Loaded(object sender, RoutedEventArgs e) { string currentDirectory = System.IO.Directory.GetCurrentDirectory(); string filePath = System.IO.Path.Combine(currentDirectory, "image.jpg"); BitmapImage bi = new BitmapImage(); bi.BeginInit(); bi.UriSource = new Uri(filePath); bi.EndInit(); image.Source = bi; // キャンバスのサイズをロードしたコントロールに合わせる canvas.Width = image.Width; canvas.Height = image.Height; _canvasOriginalWidth = image.Width; _canvasOriginalHeight = image.Height; } void scrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e) { if ((Keyboard.Modifiers & ModifierKeys.Control) != ModifierKeys.None) { changeCanvasScale(e.Delta); e.Handled = true; } } private void changeCanvasScale(int delta) { int originalZoomLevel = _zoomLevel; int zoomType = 0; if (checkBox.IsChecked.Value) { zoomType = 1; } double magnification = GetMagnification(zoomType, _zoomLevel); // 拡大操作時に中央に表示していたものがそのまま中央に表示されるようにする // そのためにまず中央に表示されている点の元の画像サイズの画像上での位置を求める double centerX = (scrollViewer.HorizontalOffset + scrollViewer.ViewportWidth / 2) / magnification; double centerY = (scrollViewer.VerticalOffset + scrollViewer.ViewportHeight / 2) / magnification; if (delta > 0) { _zoomLevel += 1; } else { _zoomLevel -= 1; } // 拡大率が異常になったら元の拡大率に戻す magnification = GetMagnification(zoomType, _zoomLevel); if (magnification <= 0 || double.IsInfinity(magnification) || double.IsNegativeInfinity(magnification)) { _zoomLevel = originalZoomLevel; return; } _canvasTransformGroup.Children.Clear(); _canvasTransformGroup.Children.Add(new ScaleTransform(magnification, magnification)); canvas.RenderTransform = _canvasTransformGroup; canvas.Width = _canvasOriginalWidth * magnification; canvas.Height = _canvasOriginalHeight * magnification; // 中央に表示されていたものがそのまま中央に表示されるようにスクロールする scrollViewer.ScrollToHorizontalOffset(centerX * magnification - scrollViewer.ViewportWidth / 2); scrollViewer.ScrollToVerticalOffset(centerY * magnification - scrollViewer.ViewportHeight / 2); } /// <summary> /// 拡大レベルに応じた拡大率を得る /// </summary> /// <param name="zoomType">拡大縮小の方式 0:通常 0以外:縮小時の拡大率を拡大時の逆数で代用する</param> /// <param name="zoomLevel">拡大レベル 0だと等倍 正だと拡大 負だと縮小</param> /// <returns>拡大率</returns> private double GetMagnification(int zoomType, int zoomLevel) { double x = 0.1 / (2.0 * MAGNIFICATION_LEVEL_1 * Math.Tan(VIEW_ANGLE / 2)); if (zoomType == 0) { return 1 / (1 - (x * zoomLevel) * Math.Tan(VIEW_ANGLE / 2) * 2); } else { double d = 1 / (1 - (x * Math.Abs(zoomLevel)) * Math.Tan(VIEW_ANGLE / 2) * 2); if (zoomLevel >= 0) { return d; } else { return 1 / d; } } } private void checkBox_Checked(object sender, RoutedEventArgs e) { _zoomLevel = 0; } private void checkBox_Unchecked(object sender, RoutedEventArgs e) { _zoomLevel = 0; } } }
前回の続き。
Windowsアプリでよく見る Ctrl + マウスホイール での拡大表示の
拡大率を現実世界をモデリングして求めてみようという話。
ある人が視野角θで対象物を見ていて、それが高さhで見えているとする。
そこから人がxだけ前進すると対象物の見える範囲がh'になるとする。
hだけ見えていたのがh'しか見えなくなるのだから
拡大率 = h / h'
だ。
ある距離x進んだ時の拡大率が知りたいのだから
h'をxとhで表せれば良さそう。
図の赤い三角形に着目すると
赤い三角形右辺の長さ = (h - h')
であることが分かる。
同時に
赤い三角形右辺の長さ = x * tan(θ/2) * 2
でもあるので
(h - h') = x * tan(θ/2) * 2
式を変形すると
h' = h - x * tan(θ/2) * 2
よって
拡大率 = h / (h - x * tan(θ/2) * 2)
110%(=1.1)の拡大率になるxはというと
1.1 = h / (h - x * tan(θ/2) * 2) 1.1 * (h - x * tan(θ/2) * 2) = h 1.1 * h - 1.1 * x * tan(θ/2) * 2 = h -1.1 * x * tan(θ/2) * 2 = -0.1 * h x = h / (22 * tan(θ/2))
ここで
θ = 45°
とすると
tan(θ/2) ≒ 0.4142135623730950488016887242097 x ≒ h * 0.10973698010786795676371312382771
例えばhが100の時は1.1倍になるxは約11。
で、約11ずつ進んでいくと拡大率はどうなるのかグラフにしてみる。
なんかそれっぽいけどどうなんだろ。
次は実際にアプリにして感じを見てみる。
Ctrl + マウスのホイール で拡大縮小というのは
いつの間にやら Word や Excel や IE や Chrome と
多くのアプリで採用される操作方法になっている。
それで自作のアプリにも実装してみたのだが
どうも拡大縮小の加減が心地良くない。
どのような実装にしたかというと
マウスホイールが1ノッチ動くたびに
単純に拡大率を15%足したり引いたりするようにしたのだ。
つまり 100% → 115% → 130% … といった感じ。
これだとはじめの方はいいのだが、ある程度拡大したところから
もっと拡大したいな、となったときに、ホイールをコロコロやっても
なかなか大きくならないな―…と感じてしまう。
そこで Chrome を見てみると
100% → 110% → 125% → 150% → 175% → 200% → 250% …
とだんだん拡大率の増え方が大きくなっている。
この拡大率のテーブルはどう作っているんだ?
これが正解なのか? 自分のよりは心地よいが…。
困ったときは現実世界をモデリングだ。
人間、物に近づけばそれが大きく見えるし、物から遠ざかればそれが小さく見える。
ホイールのコロコロを対象物に向かっての前進や後退と捉えては…?
ある距離近づくと、どれだけ大きく見えるようになる?
真面目に計算して実装してみたいと思う。
直感的で使いやすいものになることを信じて。
つづく。
上司「ある処理を8つの言語で書いて欲しいんだよね。
自分「はぁ。多分大丈夫だと思いますが…。
せっかくなのでちゃんと書きたいと思い
再入門ばっかりしてますが…。
例によって書籍を探しました。
PHPというとWebアプリですが
この本一冊でPHPをはじめてWebアプリが作れるところまでたどり着けます。
実際本の中で簡易ツイッターを作ります。
セキュリティに関する具体的な対策方法等も盛り込まれていて実践的。
何らかのプログラミング言語を知っている人向けに感じますが
自分にはむしろありがたかったです。
それから、何かとIDEが欲しくなる病の自分
PHP用IDEはPhpStormが素晴らしいそうです。
有償ですがオープンソース開発者、学生や先生なんかは無償のよう。
その他で無償のものというとNetBeansあたりかなぁ…。
前にRubyでつくったのだけどAndroid版ImageSpiderの為にJava版も必要になり作成。
変なところあったらご指摘ください。
package jp.ne.sakura.kurima.imagespider.util; import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.InputStream; public class ImageFileExtentionGetter { /** * 指定の画像ファイルの内容から適切な拡張子の文字列を推測して返す * 不明な場合や例外発生時は空文字列を返す * ".bmp" ".gif" ".jpg" ".png" ".psd" ".tif" に対応 * WBMP, JPEG 2000, PICT 等はヘッダでの判定が複雑そうなのと一般的ではないことから割愛 * * @param filePath 画像ファイルのファイルパス * @return {@code ".bmp"} 等ファイルの内容から推測した拡張子の文字列 */ public static String getImageFileExtention(String filePath) { FileInputStream fis = null; InputStream is = null; byte[] buf = new byte[8]; int len = 0; try { fis = new FileInputStream(filePath); is = new BufferedInputStream(fis); len = is.read(buf); if (len == -1) { return ""; } } catch (Exception e) { return ""; } finally { try { if (is != null) { is.close(); } } catch (Exception e) { } try { if (fis != null) { fis.close(); } } catch (Exception e) { } } final byte[] BMP_SIG = new byte[] { 0x42, 0x4D }; final byte[] GIF_SIG_1 = new byte[] { 0x47, 0x49, 0x46, 0x38, 0x37, 0x61 }; final byte[] GIF_SIG_2 = new byte[] { 0x47, 0x49, 0x46, 0x38, 0x39, 0x61 }; final byte[] JPG_SIG = new byte[] { (byte)0xFF, (byte)0xD8, (byte)0xFF }; final byte[] PNG_SIG = new byte[] { (byte)0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; final byte[] PSD_SIG = new byte[] { 0x38, 0x42, 0x50, 0x53 }; final byte[] TIF_SIG_1 = new byte[] { 0x49, 0x49, 0x2A, 0x00 }; final byte[] TIF_SIG_2 = new byte[] { 0x4D, 0x4D, 0x00, 0x2A }; if (len >= BMP_SIG.length && compareByteArray(buf, BMP_SIG, BMP_SIG.length) == 0) { return ".bmp"; } else if (len >= GIF_SIG_1.length && compareByteArray(buf, GIF_SIG_1, GIF_SIG_1.length) == 0) { return ".gif"; } else if (len >= GIF_SIG_2.length && compareByteArray(buf, GIF_SIG_2, GIF_SIG_2.length) == 0) { return ".gif"; } else if (len >= JPG_SIG.length && compareByteArray(buf, JPG_SIG, JPG_SIG.length) == 0) { return ".jpg"; } else if (len >= PNG_SIG.length && compareByteArray(buf, PNG_SIG, PNG_SIG.length) == 0) { return ".png"; } else if (len >= PSD_SIG.length && compareByteArray(buf, PSD_SIG, PSD_SIG.length) == 0) { return ".psd"; } else if (len >= TIF_SIG_1.length && compareByteArray(buf, TIF_SIG_1, TIF_SIG_1.length) == 0) { return ".tif"; } else if (len >= TIF_SIG_2.length && compareByteArray(buf, TIF_SIG_2, TIF_SIG_2.length) == 0) { return ".tif"; } return ""; } /** * byte配列を指定長さ分比較して結果を返す * * @param a1 比較対象の配列(1つ目) * @param a2 比較対象の配列(2つ目) * @param compareLength 比較する長さ * @return 指定長さの内容が等しければ{@code 0}を等しくなければ{@code -1} * いずれかの配列の長さが比較用の指定長さに満たない場合は{@code -1} */ private static int compareByteArray(byte[] a1, byte[] a2, int compareLength) { if (a1.length < compareLength || a2.length < compareLength) { return -1; } for (int i = 0; i < compareLength; i++) { if (a1[i] != a2[i]) { return -1; } } return 0; } }