Android で動画のスクリーンショットを撮る

プログラムからスクリーンショットを撮影し保存したいことがあったんですが、動画があると普通の撮り方だと真っ黒になってしまい駄目だったので少し調べました。

動画の再生には ExoPlayer を使って再生しています。

github.com

今回はこの ExoPlayer が内部で表示するために使っている SurfaceView と TextureView のスクリーンショットの撮り方の説明です。

普通の View の場合

(普通の View とは、という疑問もありますが、今回はこれでだいたいの画面でこの方法が使えるため普通と言います)

普通の画面の場合は、 window や DataBinding から root view を取得して bitmap として保存するだけです。

fun takeScreenshotForNormalView(context: Context, view: View) {
    val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888).apply {
        view.draw(Canvas(this))
    }
    saveScreenshot(context, bitmap)
}

private fun saveScreenshot(context: Context, bitmap: Bitmap) {
    val values = ContentValues().apply {
        val name = "${System.currentTimeMillis()}.jpeg"
        put(MediaStore.Images.Media.DISPLAY_NAME, name)
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/Screenshots/")
        put(MediaStore.Images.Media.IS_PENDING, 1)
    }

    val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    context.contentResolver.insert(collection, values)?.let { imageUri ->
        context.contentResolver.openOutputStream(imageUri).use { outputStream ->
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
        }

        values.clear()
        values.put(MediaStore.Images.Media.IS_PENDING, 0)
        context.contentResolver.update(imageUri, values, null, null)

        Toast.makeText(context, "保存しました", Toast.LENGTH_SHORT).show()
    }
}

動画の場合

動画の場合 ExoPlayer を使っていると、動画自体を描画しているのは SurfaceView か TextureView になります。

この SurfaceView と TextureView を使っている場合、普通の方法でスクリーンショットを撮ると、真っ黒い画像が保存されてしまい駄目なので別の方法が必要になります。

SurfaceView

SurfaceView の場合は PixelCopy を使って bitmap を生成してそれを保存します。

fun takeScreenshotForSurfaceView(context: Context, view: SurfaceView) {
    val bitmap = Bitmap.createBitmap(
        view.width,
        view.height,
        Bitmap.Config.ARGB_8888
    )
    PixelCopy.request(
        view,
        bitmap,
        { result ->
            if (result == PixelCopy.SUCCESS) {
                saveScreenshot(context, bitmap)
            } else {
                Toast.makeText(context, "失敗しました", Toast.LENGTH_SHORT).show()
            }
        },
        Handler()
    )
}

TextureView

TexutureView の場合は、 getBitmap() というメソッドが定義されているので、それを使って bitmap を取得して保存するだけです。

fun takeScreenshotForTextureView(context: Context, view: TextureView) {
    saveScreenshot(context, view.bitmap)
}

動画を撮る場合の注意点

上記のように SurfaceView と TextureView を撮影するには PlayerView#getVideoSurfaceView() メソッドを使って動画を描画している View を取得する場合があります。
しかし、これをそのままスクリーンショットとして使うと、動画を描画している部分しか残せないため、画面全体としてスクリーンショットを撮りたい場合は不十分なので、別で画面全体の bitmap を生成し合成する、といった工夫が必要です。

サンプルコード

github.com

Android で動画のスクリーンショットを撮る

プログラムからスクリーンショットを撮影し保存したいことがあったんですが、動画があると普通の撮り方だと真っ黒になってしまい駄目だったので少し調べました。

動画の再生には ExoPlayer を使って再生しています。

github.com

今回はこの ExoPlayer が内部で表示するために使っている SurfaceView と TextureView のスクリーンショットの撮り方の説明です。

普通の View の場合

(普通の View とは、という疑問もありますが、今回はこれでだいたいの画面でこの方法が使えるため普通と言います)

普通の画面の場合は、 window や DataBinding から root view を取得して bitmap として保存するだけです。

fun takeScreenshotForNormalView(context: Context, view: View) {
    val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888).apply {
        view.draw(Canvas(this))
    }
    saveScreenshot(context, bitmap)
}

private fun saveScreenshot(context: Context, bitmap: Bitmap) {
    val values = ContentValues().apply {
        val name = "${System.currentTimeMillis()}.jpeg"
        put(MediaStore.Images.Media.DISPLAY_NAME, name)
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/Screenshots/")
        put(MediaStore.Images.Media.IS_PENDING, 1)
    }

    val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
    context.contentResolver.insert(collection, values)?.let { imageUri ->
        context.contentResolver.openOutputStream(imageUri).use { outputStream ->
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
        }

        values.clear()
        values.put(MediaStore.Images.Media.IS_PENDING, 0)
        context.contentResolver.update(imageUri, values, null, null)

        Toast.makeText(context, "保存しました", Toast.LENGTH_SHORT).show()
    }
}

動画の場合

動画の場合 ExoPlayer を使っていると、動画自体を描画しているのは SurfaceView か TextureView になります。

この SurfaceView と TextureView を使っている場合、普通の方法でスクリーンショットを撮ると、真っ黒い画像が保存されてしまい駄目なので別の方法が必要になります。

SurfaceView

SurfaceView の場合は PixelCopy を使って bitmap を生成してそれを保存します。

fun takeScreenshotForSurfaceView(context: Context, view: SurfaceView) {
    val bitmap = Bitmap.createBitmap(
        view.width,
        view.height,
        Bitmap.Config.ARGB_8888
    )
    PixelCopy.request(
        view,
        bitmap,
        { result ->
            if (result == PixelCopy.SUCCESS) {
                saveScreenshot(context, bitmap)
            } else {
                Toast.makeText(context, "失敗しました", Toast.LENGTH_SHORT).show()
            }
        },
        Handler()
    )
}

TextureView

TexutureView の場合は、 getBitmap() というメソッドが定義されているので、それを使って bitmap を取得して保存するだけです。

fun takeScreenshotForTextureView(context: Context, view: TextureView) {
    saveScreenshot(context, view.bitmap)
}

動画を撮る場合の注意点

上記のように SurfaceView と TextureView を撮影するには PlayerView#getVideoSurfaceView() メソッドを使って動画を描画している View を取得する場合があります。
しかし、これをそのままスクリーンショットとして使うと、動画を描画している部分しか残せないため、画面全体としてスクリーンショットを撮りたい場合は不十分なので、別で画面全体の bitmap を生成し合成する、といった工夫が必要です。

サンプルコード

github.com

Room + LiveData で observe し直さずにクエリを変える

Room + LiveData を使い、特定のクエリで DB に Observer を登録しておくと insert した時などに新しいデータが自動で Observer に流れてくるので便利です。

@Entity(tableName = "articles")
data class Article(
    @PrimaryKey @ColumnInfo(name = "id") val id: Int = 0,
    @ColumnInfo(name = "title") val title: String,
    @ColumnInfo(name = "description") val description: String,
    @ColumnInfo(name = "starts_at") val startsAt: String,
    @ColumnInfo(name = "ends_at") val endsAt: String
)
@Dao
abstract class ArticleDao {

    @Query(
    """
        SELECT
            *
        FROM
            articles
        WHERE
            starts_at <= :date
            AND :date <= ends_at
    """
    )
    abstract fun selectAvailableArticles(date: String): LiveData<List<Article>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    abstract suspend fun insertArticles(articles: List<Article>)
}
class MainViewModel(private val dao: ArticleDao) : ViewModel() {

    fun fetchArticles(): LiveData<List<Article>> {
        // DateTimeUtils.now().formatString() は現在時刻を String で返す
        return articleRepository.selectAvailableArticles(ZonedDateTimeUtils.now().formatString())
    }

    fun insertArticles(articles: List<Article>) {
        dao.insertArticles(articles)
    }
}
class MainActivity : AppCompatActivity() {

    // 省略

    override fun onCreate(savedInstanceState: Bundle?) {
        // 省略
        viewModel.fetchArticles().observe(this, Observer { articles ->
            // データの更新があったら新しい articles が渡される
        })
        
        // insert すると上の Observer にデータが来る
        viewModel.insertArticles(listOf(Article(/* 省略 */)))
    }
}

今回コンテンツに表示期間があるアプリを想定して Articlestarts_atends_at を作りました。
また、 ArticleDao を見るとクエリする時に渡す時刻によってクエリ結果が違うことがわかります。

viewModel.artciles.observe() を呼び出すとメソッドを呼び出した時刻でクエリが実行され、その時点で表示できる Article 一覧が取得できます。
しかし、この実装だと画面更新が必要になった場合 Observer を一度 remove して再度 observe する必要があります。

class MainActivity: AppCompatActivity() {

    private var articles: LiveData<List<Article>>? = null
    
    override fun onCreate(savedInstanceState: Bundle?) {
        articles = viewModel.fetchArticles()
        articles?.observe(this, Observer { })
    }
    
    fun updateArticles() {
        articles?.removeObservers(this)
        articles = viewModel.fetchArticles()
        articles?.observe(this, Observer { })
    }
}

この実装でも想定する挙動は実装できるので問題ないのは事実です。
しかし、グローバル変数が増えますし一度削除して observe し直すので面倒だし美しくないです。

Transformations.switchMap を使う

こういった observe し直さずにクエリパラメータを変えて新しいデータを取得したい場合、パラメータを LiveData にし Transformations.switchMap を使ってクエリを実行しなおするようにすると解決できます。

class MainViewModel(private val dao: ArticleDao) : ViewModel() {

    private val queryTime = MutableLiveData<ZonedDateTime>(ZonedDateTimeUtils.now())

    val availableArticles: LiveData<List<Article>> = Transformations.switchMap(queryTime) {
        articleRepository.fetchAvailableArticles(it)
    }

}
class MainActivity: AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        viewModel.availableArticles.observe(this, Observer { articles ->
    }
    
    fun update() {
        viewModel.queryTime.value = ZonedDateTimeUtils.now()
    }
}

こうすることで、クエリパラメータを変更するだけで新しいデータを取得できるようになりました。

サンプルコード

0.5秒ごとにクエリを実行し直して RecylerView に表示するサンプルを作りました。
詳しい実装は github を見てください。

github.com

参考資料

stackoverflow.com

Android DataBinding の使い方

codelab android databinding

Android DevSummit 2019 の codelabs が google codelabs に追加されていたので、 DataBinding を改めて勉強しました。

codelabs はこちら

そもそも DataBinding を使う理由としては、 codelabs には以下のように書かれています。

  • findViewById() を使うのはよくない
    • 処理速度が遅い
    • コンパイル時に例外検査がないので安全ではない
      • layout xml に存在しない id を渡してしまうと実行時にクラッシュする
    • findViewById() だと onCreate 時に初期値が決まるが、これより DataBinding でデフォルト値を設定して自動的に反映させるほうが良い
  • Button の android:onClick attribute に直接 onClick 時に実行する method 名を書くのは良くない
    • コンパイル時に例外検査がないので実行時にクラッシュしてしまう
    • また、 rename 時に見逃しやすい

そのため、 DataBinding を使ってない場合は、現在動いているコードは積極的に書き換えなくても良い気がしますが、新たに追加する画面などでは DataBinding を使っていく感じにしたほうが良さそうです。

DataBinding 使用方法

build.gradle

build.gradle に以下のコードを追加して dDataBinding を有効化します

android {
...
    dataBinding {
       enabled true
    }
}

layout xml

layout xml はルートタグが <layout> タグであれば DataBinding が使用できます。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
    ...
    
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

AndroidStudio では、一番上の親要素にカーソルを合わせると以下の画像のようにダイアログが出てくるので Convert to data binding layout を選択すると自動的に DataBinding が使用できる状態に変換してくれます。

f:id:Nshiba:20191117143420p:plain
Convert to data binding layout

Activity で使うには onCreate 内の setContentView() を以下のように置き換えます

class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
    }
}

これで DataBinding を使用する準備はできました。

レイアウト変数/レイアウト式

DataBinding を使用すると xml 上で変数と式を使用することができるようになります。

  • 変数は data タグ内で宣言します
  • data タグ内では import 文を書くことが出来て、 import するとレイアウト式で import したクラスのメソッドが使用することができます。
  • レイアウト式を使うには android:text=@{text} という形で書きます。
  • レイアウト式ではラムダ式も使えるため、 android:onClick="@{() -> viewModel.onClick()}" という形で書くことが出来ます。
    • このようにラムダ式で書くとコンパイル時の例外検査が走るので、実行前にメソッドがあるかどうかのチェックができるようになります
<layout>

    <data>

        <import type="android.view.View" />

        <variable
            name="viewModel"
            type="net.nshiba.databindingsample.MainViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{viewModel.name + String.valueOf(viewModel.age)}"
            android:visibility="@{viewModel.age &lt; 13 ? View.GONE : View.VISIBLE}"
            android:onClick="@{() -> viewModel.onClick()}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

あとは Activity 側でデータを設定してあげるだけです。

class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    private val viewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

        ここで viewModel を設定
        binding.viewModel = viewModel
    }
}

またここでは、 ViewModel を使用しています。
ViewModel を使用するとデータを View に反映する前段階の処理と状態の保持を一箇所にまとめることができます。
ちなみにここで使っている MainViewModel の中身は以下の感じです。

class MainViewModel : ViewModel() {

    val name: String = "nshiba"

    val age: Int = 15

    fun onClick() {
        // do nothing
    }
}

注意点

DataBinding はとても便利です。 しかし、 layout xml に複雑なロジックを書いてしまうと、可読性とメンテナンス性が下がってしまうためバランスをとって書きましょう

LiveData

LiveData を使うとデータの変更を監視して、変更があったら自動的にUIに反映させることが可能です。

使い方は簡単で、以下のように MutableLiveData を使って値を宣言し layout に設定するだけです。

class MainViewModel(): ViewModel() {

    ...

    private val likes = MutableLiveData(0)
    
    fun onClick() {
        likes.value = (likes.value ?: 0) + 1
    }
}
        ...
    
        <TextView
            android:id="@+id/likes"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="@{String.valueOf(viewModel.likes)}"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toBottomOf="@id/button" />
        
        ...

また LiveData は LifeCycle を知っている必要があるので Activity/Fragment で DataBinding に対して LifeCycleOwner を設定してあげる必要があります

   override fun onCreate(savedInstanceState: Bundle?) {
        ...
        
        binding.lifecycleOwner = this
    }

こうすると、最初に作った Button のクリックリスナーに viewMode.onClick() が設定されているため、ボタンを押すと like が増えていくようになりました。

他にも LiveData には Transformations.map() というメソッドを使うと、データに変更があったらそのデータを加工して違うデータにすることが可能です。

class MainViewModel(): ViewModel() {
    ...
       
    val popularity = Transformations.map(likes) {
        when {
            it > 9 -> "START"
            it > 4 -> "POPULAR"
            else -> "NORMAL"
        }
    }
    
    ...
}

BindingAdapter

BindingAdapter を使うと簡単にカスタム attributes を作ることができます。
まず、 BindingAdapter を使うには build.gradle に apply plugin: 'kotlin-kapt' を追記してください。

次に BindingExt.kt ファイルを作り @BindingAdapter annotation がついたメソッドを作ります。

@BindingAdapter("app:visibleFromLike")
fun View.setVisibleFromLike(likes: Int) {
    visibility = if (likes < 4) {
        View.GONE
    } else {
        View.VISIBLE
    }
}

@BindingAdapter に指定した引数を layout xml で使用することができます

<TextView
    android:id="@+id/popularity"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:text="@{viewModel.popularity}"
    app:visibleFromLike="@{viewModel.likes}"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@id/likes" />

これで lives < 4 のときに現れる View を作ることができました。

また、 BindingAdapter は複数の attributes を設定することができます

@BindingAdapter(value = ["app:visibleFromLike", "app:visibleFromLikeMax"], requireAll = false)
fun View.setVisibleFromLike(likes: Int?, max: Int?) {
    ...
}

value に設定する項目を配列にして、メソッドの引数を配列のサイズと同じにします。
ここで requireAll = true を指定すると、すべての attributes が View に設定されていなければコンパイルエラーが発生するようになります。

まとめ

DataBinding を使うと Activity/Fragment 側で UI 変更のコードがなくなりすっきりしていいですね。
Android アプリ開発において DataBinding を使うかどうかは結構好みが分かれると思いますが、がっつり使うとかなり便利になるので個人的にはおすすめです。

ただ、 findViewById() の置き換えとして使うだけだとビルド時間も伸びるので、その場合は ViewBinding を使うほうが良いと思います。

DataBinding についてもっと詳しくやりたい方は、とりあえず実際に codelabs をやってみると良いと思います。
https://codelabs.developers.google.com/codelabs/android-databinding/#0

今回作成したサンプルプロジェクトはこちらです。
https://github.com/nshiba/DataBindingSample

小説家になろうのAndroidアプリ「なろうTime」を作った

小説家になろう(以下、なろう)*1のランキング閲覧と小説リーダのAndroidアプリ、なろうTimeを作った。

github.com play.google.com

まぁ作ったと言ってもリリースしたのが2016年5月なので結構前に作ってた。
ちょこちょこアップデートしてたんだが、忙しくなってり他にやりたいことあったりと自分の中の優先度が低くなっていて最近はアップデートしていなかった。
それで、最近落ち着いてきたのでアップデートしようと思ったが、そういえば作ったことをブログに書いていなかったので振り返りもかねてブログを書く。

作った経緯

なぜなのかは自分でもきちんとわかってないんだけど、なろうの小説を読むのが好きで結構読んでる。
ただ、自分はわりと完結済みの小説しかよまない。
また、あまり少ない文字数小説とかもよまない。
なのでその辺りのフィルターを簡単に切り替えて見れるようにしたかった。
というのが最大の理由。

あとは詳細検索があまり見やすくなかったのでその辺も改善したかった。

機能紹介

ランキング閲覧 & フィルター

なろうでは、日間、週間、月間、四半期、累計の計5種類のランキングがあって、それぞれ300件*2が最初の画面でみれるようになってる。
右下にFABを表示してあって、それをクリックするとランキングに対してフィルターをかけることができる。
このフィルターは完結済み、ジャンル、最大/最小文字数でフィルターをかけることができる。
これは経緯で語ったやりたいことをフィルターとして実装した。
ただアプリ使ってて思ったのが、フィルターはランキング結果に対してかけるため、フィルターかけた後は数が減ってしまう。
でも、詳細検索でリクエスト投げれば普通にフィルターかけた後の結果で300件とってこれるのでそうすればいいじゃんって思ってきたので実装しようと思ってる。

あと、公式のWebページだと最初ジャンル別に日間、週間…と表示されてるんだけど、1番最初に見たいのは総合ランキングじゃない?と思って総合ランキングをデフォルトで表示している。
f:id:Nshiba:20170620014639j:plain f:id:Nshiba:20170620014644j:plain

詳細検索

詳細検索をやりやすくしたかった。
でも使っててあんまり改善できてないな…って感じてしまってるのでいい感じにしていきたい。
f:id:Nshiba:20170620014811j:plain f:id:Nshiba:20170620014821j:plain

小説ダウンロード

小説なのでオフラインで読みたいことがまぁまぁある。地下鉄とか。
ただ、今ダウンロードの進捗をダイアログで表示しているのでダイアログ閉じてしまうとダウンロードが中断されてしまう。
これはほんとに良くない仕様なのでNotificationとかに変更したい。
なぜこんな仕様にしたんだろう…
f:id:Nshiba:20170620014833j:plain

しおり

小説なのでしおり機能あったほうがいいよね、ということで実装した。
公式ではユーザに関するAPIはないのとでローカルDBに保存してる。
あとたとえAPIあったとしても、オフラインで読む場合はローカルにないと読み込めないので。

小説リーダ

文字色と背景色が変えられる。
これはレビューで変えたい、という要望があった。
あと、暗い所で読んだりとか、人によっては読みやすい背景色とかがありそう。

また、自動しおり付与機能もある。
ブラウザで読んでいて結構しおり挟む前にタブ消しちゃった、とかあるので。
自分でしおりを気にしなくていいのは結構快適になる。
f:id:Nshiba:20170620014721j:plain f:id:Nshiba:20170620014724j:plain

技術的なお話

振り返ってみて、作ったときの俺はなんでこんな実装したんだろう、とか結構あるので、これから改善したいことも含めて書く。

設計

MVP
作った当初はまだぜんぜん設計とかわかってなくて最初はArchitecture考えずに実装してた。 でもいじってるうちにどんどん辛くなり、とりあえずActivity/Fragmentにこれ以上実装書きたくなくて、分離するためにMVPにリファクタした。
個人的には、個人アプリ + 中規模くらいのアプリだったらそこまでたくさん層はわけずにMVPでいいじゃないかな、と思ってる。
ただ、自分自身、今までたくさん開発経験があるわけではなく、これからって感じで、いろいろ試していきたいので、設計部分は今後も思考錯誤していきたい。

DB

Realm
たしか特に検討するわけでもなく、とりあえずRealmを触ってみたかったのでRealmにした。
でも実際は、ローカルDBの機能しかつかってないし、そこまで速さが求められる使い方でもないので、個人的に使いやすいと思ってるOrmaに乗り換えたいなぁなんて最近は思ってる。
特にRealmのマイグレーション辛い…(今年に入ってマイグレーションしてないけど改善されたのかな)
複数プラットフォームに対応する、なんていうことがあったらRealmの出番かもしれない。

非同期通信

Okhttp + RxJava
定番のやつ。
ただ、RxJavaは非同期通信の部分でしか使えてなくて、今後Kotlinに以降しようかな、と考えているのでそこでお役目御免かなぁと思ってる。
これなんでRetrofit使わなかったんだろ。

雑感

こんな感じですがランキング部分はそこそこ使いやすいかと思うのでぜひ使ってみてほしいです。
使ってみた感想や欲しい機能なんかがあったらレビューもらえるとすごい嬉しい。

また、いろいろ振り返ってみたら、リファクタしたい所やいれたい機能なんかが浮かんできた。
他にも普段つかってて、なんか使いにくい機能あったり、バグが残っていたりともどかしい部分があるのでやっていくぞ。
あと、Kotlinにするぞ。

*1:http://syosetu.com/

*2:ランキングAPIで返ってくる件数

2016年の振り返りと2017年の目標

正直振り返りと言っても何かやった記憶がほとんどない。完。

というのはさすがにあれなのでいくつか挙げてみる。

2016年振り返り

小説家になろう

唐突だが私は小説家になろうというサービスが好きだ。 なぜ好きなのか、というとまぁいわゆる厨二といわれるファンタジーやSF世界が好きだからである。 まぁそれで、アニメとか完結したラノベ、漫画でいろいろ想像したりするわけだが、それだけだと世界は限定される。 しかし小説家になろうでは、たくさんの人がたくさんの小説でその人だけの世界を展開しているわけである。 そういった新しい世界を探すのがとてもおもしろいのである。

そこで、自分のなんか関わりたいな、と思って作ったのが以下の奴。

github.com

github.com

しょうじきコードの品質はあまりよくない。半年前の俺はなんでこんなコードを書いていたんだ...ってな感じになってる。 また、これ書いてるうちになんか幾つか案が出てきたので2017年やっていきたい。

研究室のMTG

自分が所属している研究室は、自分で4期生でありまだまだできたばかりと思っている。
その為、研究室としての研究の進めかただったり、全体の進捗管理MTGのやり方など、ぜんぜん整っていない。
自分の研究だけやってれば?と思う気もしなくはないが、人間自分のやる気というのは周りの環境にとても左右されると思っている。
その為、研究室全体で研究をやる。という環境(空気と言ったほうがいいかも)が整っていたほうがやる気も出るし、進捗もでるだろう。
また、やっぱり自分が所属している組織なので今後もできるだけ続いて欲しいし強くなっていって欲しい。

という思いがあって10月くらいから、「毎週5人がそれまでの進捗発表をやる」というのをはじめた。
個人的にこれが結構良かったと思っていて、誰が何やってるか把握できるし、今の所属人数だとだいたい1ヶ月で1度程度の頻度で発表が回ってくるため、 かならず1ヶ月になんかしらの進捗は出さなければいけないのでやらない人もやる。
また、定期的に自分の研究をまとめることによって、今自分は何をやっているか、今突き当たっている問題は何か、今後は何をやらなければいけないか、が明確になる。
という、今までなんでやってこなかったって感じになってるので今後も続けていきたい。

じゃあなんで10月からなの?というのは2015年にもちゃんとMTGやっていったほうがいいよね、と思っていろいろやったんだが大失敗していろいろな人に迷惑をかけた。また、大失敗して自分のやる気がでなかった。ごめんなさい。

今年、協力していただいた人、ほんとにありがとう。 2017年も年明けからやっていきたいと思っているのでよろしくお願いします。

100万円貯金

3年くらい継続して目標にしてるけど無理だった。来年の俺よろしく。

2017年の目標

エンジニアとしての目標

アウトプットを増やす

細かい目標は結構あるが、総括するとこれに限る。 個人的には年を追うごとにインプットは増えてると思う。
じゃあそれを外に出してきたか?と言われると怪しいだろう。かろうじて小説家になろう関係で作ったものぐらいな気がする。
あとはQiitaとかに少し記事を書いた。

しかしそれだけだ。
なのでアウトプットをしていきたい。
細かいことを言うと

  • 技術系同人誌を買いて売る
  • 勉強会やカンファレンスで発表する
  • もっと細かいものでも作ったものを形にしてGithubに上げる
  • OSSにコミットする
  • 技術系の記事(ブログとかQiita)を書く

などを考えている。 あと、ついでにGithubで50スターくらい欲しい。頑張る。

時間を操る

もっと時間が欲しい、と思ったことありませんか?自分はあります。
なので2017年は時間を操れるようになりたいと思います。
一日30時間位欲しい

100万円貯金

2017年の俺頑張って。

まとめ

2017年は今まで貯めてきた経験値を使って進化する年にするぞ。

CPSLab VR Tourを作った

www.adventar.org

4日目です。 3日目は 岩井研残留日誌 - あつおの日常~あつおと過ごした365日~ でした。

自分の研究は複数の360度画像・動画を扱うのですが、その過程で所属している研究室のVR Tourなるものを作りました。

CPSLabVRTour
f:id:Nshiba:20161204232536p:plain
※ 画像が結構重いため初回ロード時は時間がかかります。

今回これの作成には three.js - Javascript 3D library を使用しました。

白いアイコンをクリックするとそこの情報が、矢印っぽいアイコンをクリックするとその方向の別の視点の画像に遷移します。
CPSLab入りたいなーとか、今年入ってきたB3生でどこに何があるのかまだわかってなかったときに参考にしてもらえると嬉しいです。

やっていること

やっていることは簡単でthree.jsにあるSphereGeometryを使ってMeshを作成しその上に各オブジェクトを配置しています。
オブジェクトの配置は、3D空間上にある球体の面に配置するためx, y, z座標が必要になります。
しかし現状、オブジェクトを配置するためのUIは作っていません。
そのため、手動で配置する場所の座標を打ち込んでいくしかないのですが、球面上に配置するオブジェクトのx, y, z座標なんて人間がすることじゃありません。 というかかなり厳しいと思います。まぁ他にももろもろ事情はあるんですが。

では、何でやってるかって言うと、仰角と方位角を用いて位置を指定し、配置する際にx, y, z座標に変換しています。
それならばどの方向でどれくらいの高さなのかを指定してあげるだけなので人間でもできますね。(仰角と方位角がわからない場合はググってください)

ちなみに仰角を phi , 方位角を theta , 球の半径を r にした場合、以下の式でx, y, z座標に変換できます。

  theta = theta * Math.PI / 180;
  phi = phi * Math.PI / 180;

  x = -r * Math.cos(phi) * Math.cos(theta);
  y = r * Math.sin(phi);
  z = r * Math.cos(phi) * Math.sin(theta);

まぁ3Dの分野では割と一般的に手法ではあると思います。三角関数は結構使われてると思うし。

最後に

もともと公開していたものですし、複数人の人には話して問題ないだろって感じだったんですが、プライベート空間でもあるので公開されるのが嫌な人がいましたらすぐに公開を停止します。その時は私まで連絡ください。