Pull to refresh

Daydream: Интерактивная заставка для Android 4.2

Reading time 9 min
Views 4.9K
В новой версии Android появилась возможность создавать интерактивные заставки. Когда устройство находится в режиме ожидания или заряжается, оно может отображать фотографии из галереи, данные из интернета (новости, фотографии и так далее) или просто анимацию. Для активации заставки нужно в настройках, в категории Дисплей выбрать заставку и указать, когда ее отображать, к примеру во время зарядки устройства.

Архитектура Daydream



Каждая реализация заставки является подклассом android.service.dreams.DreamService. Когда вы создаете DreamService, вы получаете доступ к API похожему на API жизненного цикла Activity. Основные методы DreamService для переопределения в подклассе (не забудьте вызвать в реализации суперкласса):
  • onAttachedToWindow() — используется для начальной установки, также как вызов setContentView()
  • onDreamingStarted() — вызывается, когда создается и отображается окно Daydream, теперь можно запускать анимацию.
  • onDreamingStopped() — останавливает анимацию.
  • onDetachedFromWindow() – удаляет все, что было создано в onAttachedToWindow().

Важные методы DreamService, которые вы можете вызывать:
  • setContentView() – устанавливает сцену Daydream, ведет себя точно так же как setContentView() в активити. Включает использование MATCH_PARENT для отображения ширины и высоты.
  • setInteractive(boolean) – по умолчанию, Daydream закроется, когда пользователь коснется экрана. Если нужно, чтобы пользователь взаимодействовал с заставкой, используем setInteractive(true).
  • setFullscreen(boolean) – удобный метод, скрывающий статусбар.
  • setScreenBright(boolean) – по умолчанию, заставка устанавливает полную яркость дисплея. setScreenBrith(false) – установит яркость на минимум.

Для создания заставки, необходимо создать service в файле манифеста:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app">
    <uses-sdk android:targetSdkVersion="17" android:minSdkVersion="17" />

    <application>
        <service
            android:name=".TestDaydream"
            android:exported="true"
            android:label="@string/my_daydream_name">
            <intent-filter>
                <action android:name="android.service.dreams.DreamService" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <meta-data
                android:name="android.service.dream"
                android:resource="@xml/dream_info" />
        </service>
    </application>
</manifest>

Тэг <meta-data> необязательный. Он позволяет указать на ресурс XML в котором, описаны параметры, характерные для Daydream. Пользователь имеет к ним доступ, для этого он должен нажать на значок настроек рядом с названием заставки.

<!-- res/xml/dream_info.xml -->
<?xml version="1.0" encoding="utf-8"?>
<dream xmlns:android="http://schemas.android.com/apk/res/android"
    android:settingsActivity="com.example.app/.ExampleDreamSettingsActivity" />

Следующий код, создает классическую заставку с прыгающим логотипом. Анимация реализована с помощью TimeAnimator, что дает эффект гладкой анимации.


public class TestDreamer extends DreamService {
    @Override
    public void onDreamingStarted() {
        super.onDreamingStarted();

        // Our content view will take care of animating its children.
        final Bouncer bouncer = new Bouncer(this);
        bouncer.setLayoutParams(new 
            ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        bouncer.setSpeed(200); // pixels/sec

        // Add some views that will be bounced around.
        // Here I'm using ImageViews but they could be any kind of 
        // View or ViewGroup, constructed in Java or inflated from 
        // resources.
        for (int i=0; i<5; i++) {
            final FrameLayout.LayoutParams lp 
                = new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT);
            final ImageView image = new ImageView(this);
            image.setImageResource(R.drawable.android);
            image.setBackgroundColor(0xFF004000);
            bouncer.addView(image, lp);
        }

        setContentView(bouncer);
    }
}

public class Bouncer extends FrameLayout implements TimeAnimator.TimeListener {
    private float mMaxSpeed;
    private final TimeAnimator mAnimator;
    private int mWidth, mHeight;

    public Bouncer(Context context) {
        this(context, null);
    }

    public Bouncer(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Bouncer(Context context, AttributeSet attrs, int flags) {
        super(context, attrs, flags);
        mAnimator = new TimeAnimator();
        mAnimator.setTimeListener(this);
    }

    /**
     * Start the bouncing as soon as we’re on screen.
     */
    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        mAnimator.start();
    }

    /**
     * Stop animations when the view hierarchy is torn down.
     */
    @Override
    public void onDetachedFromWindow() {
        mAnimator.cancel();
        super.onDetachedFromWindow();
    }

    /**
     * Whenever a view is added, place it randomly.
     */
    @Override
    public void addView(View v, ViewGroup.LayoutParams lp) {
        super.addView(v, lp);
        setupView(v);
    }

    /**
     * Reposition all children when the container size changes.
     */
    @Override
    protected void onSizeChanged (int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        for (int i=0; i<getChildCount(); i++) {
            setupView(getChildAt(i));
        }
    }

    /**
     * Bouncing view setup: random placement, random velocity.
     */
    private void setupView(View v) {
        final PointF p = new PointF();
        final float a = (float) (Math.random()*360);
        p.x = mMaxSpeed * (float)(Math.cos(a));
        p.y = mMaxSpeed * (float)(Math.sin(a));
        v.setTag(p);
        v.setX((float) (Math.random() * (mWidth - v.getWidth())));
        v.setY((float) (Math.random() * (mHeight - v.getHeight())));
    }

    /**
     * Every TimeAnimator frame, nudge each bouncing view along.
     */
    public void onTimeUpdate(TimeAnimator animation, long elapsed, long dt_ms) {
        final float dt = dt_ms / 1000f; // seconds 
        for (int i=0; i<getChildCount(); i++) {
            final View view = getChildAt(i);
            final PointF v = (PointF) view.getTag();

            // step view for velocity * time 
            view.setX(view.getX() + v.x * dt);
            view.setY(view.getY() + v.y * dt);

            // handle reflections
            final float l = view.getX();
            final float t = view.getY();
            final float r = l + view.getWidth();
            final float b = t + view.getHeight();
            boolean flipX = false, flipY = false;
            if (r > mWidth) {
                view.setX(view.getX() - 2 * (r - mWidth));
                flipX = true;
            } else if (l < 0) {
                view.setX(-l);
                flipX = true;
            }
            if (b > mHeight) {
                view.setY(view.getY() - 2 * (b - mHeight));
                flipY = true;
            } else if (t < 0) {
                view.setY(-t);
                flipY = true;
            }
            if (flipX) v.x *= -1;
            if (flipY) v.y *= -1;
        }
    }

    public void setSpeed(float s) {
        mMaxSpeed = s;
    }
}

Попробуем создать что-нибудь своими руками.


Создадим новый проект с минимальной версией SDK – 17. Отредактируем AndroidManifest.xml, указав ему, что мы хотим сделать заставку.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.habrdreamer"
    >
    <!-- uses-permission android:name="android.permission.WRITE_SETTINGS" -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-sdk android:minSdkVersion="17" android:maxSdkVersion="17" android:targetSdkVersion="17"/>
    <application android:label="HabrDreamer">
        <service
            android:name="ScreensaverHabr"
            android:exported="true"
            android:label="ScreensaverHabr">
            <intent-filter>
                <action android:name="android.service.dreams.DreamService" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </service>

        <activity
            android:name="SetURL"
            android:label="Dream URL"
            android:theme="@android:style/Theme.Translucent.NoTitleBar">

            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>
        </activity>

        <activity
            android:name="SetURLInteractive"
            android:label="Dream URL (Interactive)"
            android:theme="@android:style/Theme.Translucent.NoTitleBar">

            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>
        </activity>
        </application>
</manifest>

Главный класс нашей заставки будет выглядеть следующим образом:
package com.example.habrdreamer;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.graphics.drawable.Drawable;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.Handler;
import android.provider.Settings;
import android.service.dreams.DreamService;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.widget.TextView;

public class ScreensaverHabr extends DreamService {
    private class LinkLauncher extends WebViewClient {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            startActivity(intent);
            finish();
            return true;
        }
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();

        setContentView(R.layout.screensaver_habr);

        setFullscreen(true);

        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        final String url = prefs.getString("url", "http://habrahabr.ru");

        final boolean interactive = prefs.getBoolean("interactive", false);
        Log.v("WebViewDream", String.format("loading %s in %s mode",
                url, interactive ? "interactive" : "noninteractive"));
        setInteractive(interactive);

        WebView webview = (WebView) findViewById(R.id.webview);
        webview.setWebViewClient(new LinkLauncher());

        WebSettings webSettings = webview.getSettings();
        webSettings.setJavaScriptEnabled(true);
        
        webview.loadUrl(url);
    }
}

Так же нам нужно добавить два класса. Первый будет устанавливать URL адрес страницы, которая отобразится в нашей заставке. Второй, делать то же самое что первый плюс делать нашу заставку интерактивной.
Итак, первый класс – SetURL.java:
package com.example.habrdreamer;

import android.util.Log;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.widget.Toast;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;

public class SetURL extends Activity {
    @Override
    public void onCreate(Bundle stuff) {
        super.onCreate(stuff);

        final Intent intent = getIntent();

        final String action = intent.getAction();
        String url = intent.getStringExtra(Intent.EXTRA_TEXT);

        if (url == null) {
            finish();
        } else if (Intent.ACTION_SEND.equals(action)) {
            set(url);
            finish();
        }
    }

    protected void set(String url) {
        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        final Editor editor = prefs.edit();
        editor.putString("url", url);
        editor.putBoolean("interactive", false);
        editor.commit();

        Toast.makeText(this, "WebView dream URL set to: " + url, Toast.LENGTH_SHORT).show();
    }
}

И второй – SetURLInteractive.java:
package com.example.habrdreamer;


import android.util.Log;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.widget.Toast;
import android.os.Bundle;
import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;

public class SetURLInteractive extends Activity {
    @Override
    public void onCreate(Bundle stuff) {
        super.onCreate(stuff);

        final Intent intent = getIntent();

        final String action = intent.getAction();
        String url = intent.getStringExtra(Intent.EXTRA_TEXT);

        if (url == null) {
            finish();
        } else if (Intent.ACTION_SEND.equals(action)) {
            set(url);
            finish();
        }
    }
    protected void set(String url) {
        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        final Editor editor = prefs.edit();
        editor.putString("url", url);
        editor.putBoolean("interactive", true);
        editor.commit();

        Toast.makeText(this, "WebView dream URL set to: " + url, Toast.LENGTH_SHORT).show();
    }
}

Файл стиля, в данном случае, не содержит ничего кроме WebView:

<?xml version="1.0" encoding="utf-8"?>
<WebView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/webview"
    android:layout_width="match_parent"
    android:layout_height="match_parent" 
    >
</WebView>

Последним штрихом, будет создание html файла default.html в папке assets. В нем напишем, приветственное сообщение и небольшие инструкции.
<html>
  <head>
    <style>
    body {
      background-color: black;
      color: gray;
      font-size: 16pt; 
      font-family: "Roboto-Light", Roboto, sans-serif;
      text-align: center;
    }
    #text {
      margin-left: auto;
      margin-right: auto;
      margin-top: 96pt;
    } 
    </style>
  </head>
  <body>
    <div id="text">
      Используйте опцию вашего браузера “Отправить страницу (Share)”, чтобы выбрать страницу для отображения в заставке.
    </div>
  </body>
</html>

Вот в принципе и все, можно компилировать и тестировать.

Полезные ссылки:



В следующей статье, планирую поведать о создании заставок при помощи libGDX.
Tags:
Hubs:
+5
Comments 0
Comments Leave a comment

Articles