Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Androidの通知領域でRemoteViewsの支配から卒業してアニメーションするには

More than 3 years have passed since last update.

このブログのコピーです。

概要

iOS10ではリッチ通知が実装され、動画も通知領域に表示(ただし音声のみ)できるようになったのに、Androidができないだなんて不公平だ!と思って、人類解放戦線を戦うことを決意しました。
人類を支配しているのはRemoteViews。支配からの開放なるか…!
…とか言いながら実は既に先人の知恵が。主にApp Widgets界の話なのだけど、RemoteViewsと戦うことには変わりなし。
Animating Android home-screen widget

環境

  • macOS Sierra 10.12.3
  • Android Studio 2.2(2.3にするとGradleがタヒぬので…
  • Android 6.0

構成

描画部分はAnimating Android home-screen widgetのとおりRemoteViews#setImageViewBitmapにBitmapを指定して、フレーム毎描画するという力技。
描画リソースはアニメーションGifとしてそのパーサを以下から拝借。
AndroidでGIFアニメーションが動いたんだもんね(バグフィックス)
redraw部分はWidgetsのようにイベントがとんでこないのでループするためだけのServiceを実装しました。
リモートPushするのがめんどかったので、ローカルプッシュで実装しました。
コードは以下の通りです。

コード

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.xxxxx.animationgifnotificationsample">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
...
        </activity>
        <service android:name=".ImageProcService"/>
    </application>

</manifest>
MainActivity.java
package com.xxxx.animationgifnotificationsample;

...

public class MainActivity extends AppCompatActivity implements View.OnClickListener, IRedrawable {

    public static final String IMAGE_PROC_ACTION = "com.xxxx.animationgifnotificationsample.IMAGE_PROC_ACTION";
    public static final String IMAGE_FINISH_ACTION = "com.xxxx.animationgifnotificationsample.IMAGE_FINISH_ACTION";
    private static final int TIME_PERIOD = 30000;
    private static final int TIME_DELAY = 100;
    private static final int NOTIFICATION_ID = 0;

    private Context mContext;
    private BroadcastReceiver mReceiver;
    private IntentFilter mFilter;
    private NotificationCompat.Builder mBuilder;
    private GifDecoder mDecoder;
    private RemoteViews mRemoteViews;
    private NotificationManagerCompat mManager;
    private Bitmap mCanvas;
    private int mFrameCount;

....
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button_notification:
                this.sendNotification();
                break;
        }
    }
    private void sendNotification() {
        try {
            this.mDecoder = new GifDecoder();
            this.mDecoder.read(this.mContext.getResources().openRawResource(R.raw.nyancat));
        } catch (Throwable e) {
            // TODO
            Log.d("xxxx", e.toString());
            return;
        }
        this.mBuilder = new NotificationCompat.Builder(mContext);
        this.mBuilder.setSmallIcon(R.mipmap.ic_launcher);
        this.redrawImage();
        this.setRedrawSchedule();
    }

    /**
     * IRedrawable
     */
    synchronized public void redrawImage() {
        this.mCanvas = this.mDecoder.getFrame(this.mFrameCount);

        this.mRemoteViews = new RemoteViews(this.mContext.getPackageName(), R.layout.activity_notification);
        this.mRemoteViews.setImageViewBitmap(R.id.gifView, this.mCanvas);
        Notification notification = this.mBuilder.build();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            notification.bigContentView = this.mRemoteViews;
        }

        this.mManager = NotificationManagerCompat.from(mContext.getApplicationContext());

        this.mManager.notify(NOTIFICATION_ID, notification);
        this.mFrameCount = ++this.mFrameCount % this.mDecoder.getFrameCount();

    }
    private void setRedrawSchedule() {
        Intent intent = new Intent(this, ImageProcService.class);
        this.mReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                final IRedrawable redrawable = (IRedrawable) mContext;
                switch(intent.getAction()) {
                    case MainActivity.IMAGE_PROC_ACTION:
                        redrawable.redrawImage();
                        break;
                    case MainActivity.IMAGE_FINISH_ACTION:
                        redrawable.finish();

                }
            }
        };
        this.mFilter = new IntentFilter();
        this.mFilter.addAction(IMAGE_PROC_ACTION);
        this.mFilter.addAction(IMAGE_FINISH_ACTION);
        this.registerReceiver(this.mReceiver, this.mFilter);
        intent.putExtra("loop_period", TIME_PERIOD);
        intent.putExtra("loop_interval", TIME_DELAY);
        this.startService(intent);
    }

    /**
     * IRedrawable
     */
    public void finish() {
        this.unregisterReceiver();
    }
    synchronized private void unregisterReceiver() {
        if(this.mReceiver != null) {
            this.unregisterReceiver(this.mReceiver);
            this.mReceiver = null;
        }

    }
}
IRedrawable.java
package com.xxxx.animationgifnotificationsample;

public interface IRedrawable {
    void redrawImage();
    void finish();
}
ImageProcService.java
package com.xxxx.animationgifnotificationsample;

...

public class ImageProcService extends IntentService {
    static final private int DEFAULT_LOOP_PERIOD = 10000;
    static final private int DEFAULT_LOOP_INTERVAL = 100;
    public ImageProcService(String name) {
        super(name);
    }
    public ImageProcService() {
        super("ImageProcService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        final long now = System.currentTimeMillis();
        long endtime = now + DEFAULT_LOOP_PERIOD;
        long interval = DEFAULT_LOOP_INTERVAL;
        final Bundle extras = intent.getExtras();
        final Integer _loop_period_buf = (Integer)extras.get("loop_period");
        if(_loop_period_buf != null) {
            endtime = now + _loop_period_buf;
        }
        final Integer _loop_interval_buf = (Integer)extras.get("loop_interval");
        if(_loop_interval_buf != null) {
            interval = _loop_interval_buf;
        }

        while(System.currentTimeMillis() < endtime) {
            try {
                Thread.sleep(interval);

                final Intent broadcastintent = new Intent();
                broadcastintent.setAction(MainActivity.IMAGE_PROC_ACTION);
                this.getBaseContext().sendBroadcast(broadcastintent);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

結果

できたー。

ハマったこと・課題など

  • Android Engineerには常識だとはおもうのだけど、RemoteViewsに自分で作ったViewクラス派生のGifAnimationView的なのをRemoteViewsに何回も何回もCouldn't Expand RemoteViews 言われたのがきっかけ
  • ViewFlipperでもできるらしいけどやってない
  • ちなみにリモートPushでやっても反映の際にローカルNotifyしないとだめだから非現実的

github

参考

bhind13
最近Rustしかしとらん
http://bhind.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away