am fa90e0ba: am 646064ed: Merge "Add some test for wearable extenders in support lib." into klp-modular-dev
* commit 'fa90e0ba568e4834e481ce895c002d9d51f44fb9':
Add some test for wearable extenders in support lib.
diff --git a/build.gradle b/build.gradle
index d1da9fc..8012388 100644
--- a/build.gradle
+++ b/build.gradle
@@ -9,7 +9,7 @@
}
}
-ext.supportVersion = '20.0.0'
+ext.supportVersion = '21.0.0-rc1'
ext.extraVersion = 6
ext.supportRepoOut = ''
ext.buildToolsVersion = '19.0.3'
@@ -51,16 +51,20 @@
createArchive.dependsOn createRepository
// prepare repository with older versions
-task prepareRepo(type: Copy) {
+task unzipRepo(type: Copy) {
from "$rootDir/../../prebuilts/maven_repo/android"
into project.ext.supportRepoOut
}
-prepareRepo.doFirst {
+unzipRepo.doFirst {
project.ext.supportRepoOut.deleteDir()
project.ext.supportRepoOut.mkdirs()
}
+// anchor for prepare repo. This is post unzip + sourceProp.
+task(prepareRepo) << {
+}
+
import com.google.common.io.Files
import com.google.common.base.Charsets
@@ -94,6 +98,21 @@
}
createArchive.dependsOn createXml
+task(createSourceProp) << {
+ def sourceProp =
+"Extra.VendorDisplay=Android\n\
+Extra.Path=m2repository\n\
+Archive.Arch=ANY\n\
+Extra.NameDisplay=Android Support Repository\n\
+Archive.Os=ANY\n\
+Pkg.Revision=${project.ext.extraVersion}.0.0\n\
+Extra.VendorId=android"
+
+ Files.write(sourceProp, new File(project.ext.supportRepoOut, 'source.properties'), Charsets.UTF_8)
+}
+createSourceProp.dependsOn unzipRepo
+prepareRepo.dependsOn createSourceProp
+
import com.google.common.hash.HashCode
import com.google.common.hash.HashFunction
diff --git a/media/protocols/Android.mk b/media/protocols/Android.mk
new file mode 100644
index 0000000..f4f0202
--- /dev/null
+++ b/media/protocols/Android.mk
@@ -0,0 +1,22 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-media-protocols
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+LOCAL_JAVA_LIBRARIES := android-support-annotations
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/media/protocols/README.txt b/media/protocols/README.txt
new file mode 100644
index 0000000..8a22f6d
--- /dev/null
+++ b/media/protocols/README.txt
@@ -0,0 +1,12 @@
+Library Project including media protocol declarations.
+
+This library contains declarations for protocols used by
+applications to communicate with services provided by media
+routes. For example, the MediaPlayerProtocol defines a protocol
+by which an application may enqueue content to play on a
+remote media device.
+
+There is technically no source, but the src folder is necessary
+to ensure that the build system works. The content is actually
+located in libs/android-support-media-protocols.jar.
+
diff --git a/media/protocols/build.gradle b/media/protocols/build.gradle
new file mode 100644
index 0000000..254770c
--- /dev/null
+++ b/media/protocols/build.gradle
@@ -0,0 +1,34 @@
+apply plugin: 'java'
+
+archivesBaseName = 'support-media-protocols'
+
+sourceSets {
+ main.java.srcDir 'src'
+}
+
+jar {
+ from sourceSets.main.output
+}
+
+// configuration for the javadoc to include all source sets.
+javadoc {
+ source sourceSets.main.allJava
+}
+
+// custom tasks for creating source/javadoc jars
+task sourcesJar(type: Jar, dependsOn:classes) {
+ classifier = 'sources'
+ from sourceSets.main.allSource
+}
+
+task javadocJar(type: Jar, dependsOn:javadoc) {
+ classifier 'javadoc'
+ from javadoc.destinationDir
+}
+
+// add javadoc/source jar tasks as artifacts
+artifacts {
+ archives jar
+ archives sourcesJar
+ archives javadocJar
+}
diff --git a/media/protocols/src/android/support/media/protocols/MediaPlayerProtocol.java b/media/protocols/src/android/support/media/protocols/MediaPlayerProtocol.java
new file mode 100644
index 0000000..3dc72dc
--- /dev/null
+++ b/media/protocols/src/android/support/media/protocols/MediaPlayerProtocol.java
@@ -0,0 +1,2179 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.protocols;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Parcelable;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.view.accessibility.CaptioningManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Media route protocol for managing a queue of media to be played remotely
+ * by a media device.
+ */
+public class MediaPlayerProtocol extends MediaRouteProtocol {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = { RESUME_STATE_UNCHANGED, RESUME_STATE_PLAY,
+ RESUME_STATE_PAUSE })
+ public @interface ResumeState { }
+
+ /**
+ * A resume state indicating that the player state should be left unchanged.
+ */
+ public static final int RESUME_STATE_UNCHANGED = 0;
+
+ /**
+ * A resume state indicating that the player should be playing,
+ * regardless of its current state.
+ */
+ public static final int RESUME_STATE_PLAY = 1;
+
+ /**
+ * A resume state indicating that the player should be paused,
+ * regardless of its current state.
+ */
+ public static final int RESUME_STATE_PAUSE = 2;
+
+ /**
+ * Creates the protocol client object for an application to use to send
+ * messages to a media route.
+ * <p>
+ * This constructor is called automatically if you use
+ * {@link android.media.routing.MediaRouter.ConnectionInfo#getProtocolObject getProtocolObject}
+ * to obtain a protocol object from a media route connection.
+ * </p>
+ *
+ * @param binder The remote binder supplied by the media route service. May be
+ * obtained using {@link android.media.routing.MediaRouter.ConnectionInfo#getProtocolBinder}
+ * on a route connection.
+ */
+ public MediaPlayerProtocol(@NonNull IBinder binder) {
+ super(binder);
+ }
+
+ /**
+ * Loads and optionally starts playback of a new media item.
+ * The media item starts playback at playPosition.
+ *
+ * @param mediaInfo An object describing the media item to load.
+ * @param autoplay Whether playback should start immediately.
+ * @param playPosition The initial playback position, in milliseconds from the
+ * beginning of the stream.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void load(@NonNull MediaInfo mediaInfo, boolean autoplay,
+ long playPosition, @Nullable Bundle extras) {
+ if (mediaInfo == null) {
+ throw new IllegalArgumentException("mediaInfo must not be null");
+ }
+ Bundle args = new Bundle();
+ args.putBundle("mediaInfo", mediaInfo.toBundle());
+ args.putBoolean("autoplay", autoplay);
+ args.putLong("playPosition", playPosition);
+ args.putBundle("extras", extras);
+ sendRequest("load", args);
+ }
+
+ /**
+ * Begins or resumes playback of the current media item.
+ *
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void play(@Nullable Bundle extras) {
+ Bundle args = new Bundle();
+ args.putBundle("extras", extras);
+ sendRequest("play", args);
+ }
+
+ /**
+ * Pauses playback of the current media item.
+ *
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void pause(@Nullable Bundle extras) {
+ Bundle args = new Bundle();
+ args.putBundle("extras", extras);
+ sendRequest("pause", args);
+ }
+
+ /**
+ * Requests updated media status information from the receiver.
+ *
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void requestStatus(@Nullable Bundle extras) {
+ Bundle args = new Bundle();
+ args.putBundle("extras", extras);
+ sendRequest("requestStatus", args);
+ }
+
+ /**
+ * Seeks to a new position within the current media item.
+ *
+ * @param position The new position, in milliseconds from the beginning of the stream.
+ * @param resumeState The action to take after the seek operation has finished.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void seek(long position, @ResumeState int resumeState, @Nullable Bundle extras) {
+ Bundle args = new Bundle();
+ args.putLong("position", position);
+ args.putInt("resumeState", resumeState);
+ args.putBundle("extras", extras);
+ sendRequest("seek", args);
+ }
+
+ /**
+ * Sets the active media tracks.
+ *
+ * @param trackIds The media track IDs.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void setActiveMediaTracks(@NonNull long[] trackIds, @Nullable Bundle extras) {
+ if (trackIds == null) {
+ throw new IllegalArgumentException("trackIds must not be null");
+ }
+ Bundle args = new Bundle();
+ args.putLongArray("trackIds", trackIds);
+ args.putBundle("extras", extras);
+ sendRequest("setActiveMediaTracks", args);
+ }
+
+ /**
+ * Toggles the stream muting.
+ *
+ * @param muteState Whether the stream should be muted or unmuted.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void setStreamMute(boolean muteState, @Nullable Bundle extras) {
+ Bundle args = new Bundle();
+ args.putBoolean("muteState", muteState);
+ args.putBundle("extras", extras);
+ sendRequest("setStreamMute", args);
+ }
+
+ /**
+ * Sets the stream volume.
+ * If volume is outside of the range [0.0, 1.0], then the value will be clipped.
+ *
+ * @param volume The new volume, in the range [0.0 - 1.0].
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void setStreamVolume(int volume, @Nullable Bundle extras) {
+ Bundle args = new Bundle();
+ args.putInt("volume", volume);
+ args.putBundle("extras", extras);
+ sendRequest("setStreamVolume", args);
+ }
+
+ /**
+ * Sets the text track style.
+ *
+ * @param trackStyle The track style.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void setTextTrackStyle(@NonNull TextTrackStyle trackStyle, @Nullable Bundle extras) {
+ if (trackStyle == null) {
+ throw new IllegalArgumentException("trackStyle must not be null");
+ }
+ Bundle args = new Bundle();
+ args.putBundle("trackStyle", trackStyle.toBundle());
+ args.putBundle("extras", extras);
+ sendRequest("setTextTrackStyle", args);
+ }
+
+ /**
+ * Stops playback of the current media item.
+ *
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void stop(@Nullable Bundle extras) {
+ Bundle args = new Bundle();
+ args.putBundle("extras", extras);
+ sendRequest("stop", args);
+ }
+
+ /**
+ * Media player callbacks.
+ */
+ public static abstract class Callback extends MediaRouteProtocol.Callback {
+ /**
+ * Called when updated player status information is received.
+ *
+ * @param status The updated status, or null if none.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onStatusUpdated(@Nullable MediaStatus status,
+ @Nullable Bundle extras) { }
+
+ @Override
+ public void onEvent(String event, Bundle args) {
+ switch (event) {
+ case "statusUpdated":
+ onStatusUpdated(MediaStatus.fromBundle(args.getBundle("status")),
+ args.getBundle("extras"));
+ return;
+ }
+ super.onEvent(event, args);
+ }
+ }
+
+ /**
+ * Media player stubs.
+ */
+ public static abstract class Stub extends MediaRouteProtocol.Stub {
+ /**
+ * Creates an implementation of a media route protocol.
+ *
+ * @param handler The handler on which to receive requests, or null to use
+ * the current looper thread.
+ */
+ public Stub(@Nullable Handler handler) {
+ super(handler);
+ }
+
+ /**
+ * Loads and optionally starts playback of a new media item.
+ * The media item starts playback at playPosition.
+ *
+ * @param mediaInfo An object describing the media item to load.
+ * @param autoplay Whether playback should start immediately.
+ * @param playPosition The initial playback position, in milliseconds from the
+ * beginning of the stream.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onLoad(@NonNull MediaInfo mediaInfo, boolean autoplay,
+ long playPosition, @Nullable Bundle extras) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Begins or resumes playback of the current media item.
+ *
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onPlay(@Nullable Bundle extras) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Pauses playback of the current media item.
+ *
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onPause(@Nullable Bundle extras) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Requests updated media status information from the receiver.
+ *
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onRequestStatus(@Nullable Bundle extras) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Seeks to a new position within the current media item.
+ *
+ * @param position The new position, in milliseconds from the beginning of the stream.
+ * @param resumeState The action to take after the seek operation has finished.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onSeek(long position, @ResumeState int resumeState, @Nullable Bundle extras) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets the active media tracks.
+ *
+ * @param trackIds The media track IDs.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onSetActiveMediaTracks(long[] trackIds, @Nullable Bundle extras) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Toggles the stream muting.
+ *
+ * @param muteState Whether the stream should be muted or unmuted.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onSetStreamMute(boolean muteState, @Nullable Bundle extras) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets the stream volume.
+ * If volume is outside of the range [0.0, 1.0], then the value will be clipped.
+ *
+ * @param volume The new volume, in the range [0.0 - 1.0].
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onSetStreamVolume(int volume, @Nullable Bundle extras) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets the text track style.
+ *
+ * @param trackStyle The track style.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onSetTextTrackStyle(@NonNull TextTrackStyle trackStyle,
+ @Nullable Bundle extras) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Stops playback of the current media item.
+ *
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void onStop(@Nullable Bundle extras) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sends a status updated event.
+ *
+ * @param status The updated media status, or null if none.
+ * @param extras Custom application-specific data to pass along with the request.
+ */
+ public void sendStatusUpdatedEvent(@Nullable MediaStatus status,
+ @Nullable Bundle extras) {
+ Bundle args = new Bundle();
+ args.putBundle("status", status.toBundle());
+ args.putBundle("extras", extras);
+ sendEvent("statusUpdated", args);
+ }
+
+ @Override
+ public void onRequest(String request, Bundle args)
+ throws UnsupportedOperationException {
+ switch (request) {
+ case "load":
+ onLoad(MediaInfo.fromBundle(args.getBundle("mediaInfo")),
+ args.getBoolean("autoplay"), args.getLong("playPosition"),
+ args.getBundle("extras"));
+ return;
+ case "play":
+ onPlay(args.getBundle("extras"));
+ return;
+ case "pause":
+ onPause(args.getBundle("extras"));
+ return;
+ case "requestStatus":
+ onRequestStatus(args.getBundle("extras"));
+ return;
+ case "seek":
+ onSeek(args.getLong("position"), args.getInt("resumeState"),
+ args.getBundle("extras"));
+ return;
+ case "setActiveMediaTracks":
+ onSetActiveMediaTracks(args.getLongArray("trackIds"), args.getBundle("extras"));
+ return;
+ case "setStreamMute":
+ onSetStreamMute(args.getBoolean("muteState"), args.getBundle("extras"));
+ return;
+ case "setStreamVolume":
+ onSetStreamVolume(args.getInt("volume"), args.getBundle("extras"));
+ return;
+ case "setTextTrackStyle":
+ onSetTextTrackStyle(TextTrackStyle.fromBundle(args.getBundle("trackStyle")),
+ args.getBundle("extras"));
+ return;
+ case "stop":
+ onStop(args.getBundle("extras"));
+ return;
+ }
+ super.onRequest(request, args);
+ }
+ }
+
+ /**
+ * A class that aggregates information about a media item.
+ */
+ public static final class MediaInfo {
+ /** A stream type of "none". */
+ public static final int STREAM_TYPE_NONE = 0;
+
+ /** A buffered stream type. */
+ public static final int STREAM_TYPE_BUFFERED = 1;
+
+ /** A live stream type. */
+ public static final int STREAM_TYPE_LIVE = 2;
+
+ /** An invalid (unknown) stream type. */
+ public static final int STREAM_TYPE_INVALID = -1;
+
+ private static final int STREAM_TYPE_MAX = STREAM_TYPE_LIVE;
+
+ private static final String KEY_CONTENT_ID = "contentId";
+ private static final String KEY_CONTENT_TYPE = "contentType";
+ private static final String KEY_EXTRAS = "extras";
+ private static final String KEY_DURATION = "duration";
+ private static final String KEY_METADATA = "metadata";
+ private static final String KEY_STREAM_TYPE = "streamType";
+ private static final String KEY_TEXT_TRACK_STYLE = "textTrackStyle";
+ private static final String KEY_TRACKS = "tracks";
+
+ private final String mContentId;
+ private final int mStreamType;
+ private final String mContentType;
+ private MediaMetadata mMediaMetadata;
+ private long mStreamDuration;
+ private final ArrayList<MediaTrack> mMediaTracks = new ArrayList<MediaTrack>();
+ private TextTrackStyle mTextTrackStyle;
+ private Bundle mExtras;
+
+ /**
+ * Constructs a new MediaInfo with the given content ID.
+ *
+ * @throws IllegalArgumentException If the content ID or content type
+ * is {@code null} or empty, or if the stream type is invalid.
+ */
+ public MediaInfo(@NonNull String contentId, int streamType,
+ @NonNull String contentType) {
+ if (TextUtils.isEmpty(contentId)) {
+ throw new IllegalArgumentException("content ID cannot be null or empty");
+ }
+ if ((streamType < STREAM_TYPE_INVALID) || (streamType > STREAM_TYPE_MAX)) {
+ throw new IllegalArgumentException("invalid stream type");
+ }
+ if (TextUtils.isEmpty(contentType)) {
+ throw new IllegalArgumentException("content type cannot be null or empty");
+ }
+ mContentId = contentId;
+ mStreamType = streamType;
+ mContentType = contentType;
+ }
+
+ /**
+ * Returns the content ID.
+ */
+ public @NonNull String getContentId() {
+ return mContentId;
+ }
+
+ /**
+ * Returns the stream type.
+ */
+ public int getStreamType() {
+ return mStreamType;
+ }
+
+ /**
+ * Returns the content (MIME) type.
+ */
+ public @NonNull String getContentType() {
+ return mContentType;
+ }
+
+ /**
+ * Sets the media item metadata.
+ */
+ public void setMetadata(@Nullable MediaMetadata metadata) {
+ mMediaMetadata = metadata;
+ }
+
+ /**
+ * Returns the media item metadata.
+ */
+ public @Nullable MediaMetadata getMetadata() {
+ return mMediaMetadata;
+ }
+
+ /**
+ * Sets the stream duration, in milliseconds.
+ *
+ * @throws IllegalArgumentException If the duration is negative.
+ */
+ public void setStreamDuration(long streamDuration) {
+ if (streamDuration < 0) {
+ throw new IllegalArgumentException("Stream duration cannot be negative");
+ }
+ mStreamDuration = streamDuration;
+ }
+
+ /**
+ * Returns the stream duration, in milliseconds.
+ */
+ public long getStreamDuration() {
+ return mStreamDuration;
+ }
+
+ /**
+ * Sets the media tracks.
+ */
+ public void setMediaTracks(@NonNull List<MediaTrack> mediaTracks) {
+ mMediaTracks.clear();
+ mMediaTracks.addAll(mediaTracks);
+ }
+
+ /**
+ * Returns the list of media tracks, or {@code null} if none have been specified.
+ */
+ public @NonNull List<MediaTrack> getMediaTracks() {
+ return mMediaTracks;
+ }
+
+ /**
+ * Sets the text track style.
+ */
+ public void setTextTrackStyle(@Nullable TextTrackStyle textTrackStyle) {
+ mTextTrackStyle = textTrackStyle;
+ }
+
+ /**
+ * Returns the text track style, or {@code null} if none has been specified.
+ */
+ public @Nullable TextTrackStyle getTextTrackStyle() {
+ return mTextTrackStyle;
+ }
+
+ /**
+ * Sets the custom application-specific data.
+ */
+ public void setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ }
+
+ /**
+ * Returns the extras, if any.
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Creates a bundle representation of the object.
+ */
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_CONTENT_ID, mContentId);
+ bundle.putInt(KEY_STREAM_TYPE, mStreamType);
+ bundle.putString(KEY_CONTENT_TYPE, mContentType);
+ if (mMediaMetadata != null) {
+ bundle.putBundle(KEY_METADATA, mMediaMetadata.toBundle());
+ }
+ bundle.putLong(KEY_DURATION, mStreamDuration);
+ if (mTextTrackStyle != null) {
+ bundle.putBundle(KEY_TEXT_TRACK_STYLE, mTextTrackStyle.toBundle());
+ }
+ if (mExtras != null) {
+ bundle.putBundle(KEY_EXTRAS, mExtras);
+ }
+ if (!mMediaTracks.isEmpty()) {
+ Parcelable[] trackBundles = new Parcelable[mMediaTracks.size()];
+ for (int i = 0; i < trackBundles.length; i++) {
+ trackBundles[i] = mMediaTracks.get(i).toBundle();
+ }
+ bundle.putParcelableArray(KEY_TRACKS, trackBundles);
+ }
+ return bundle;
+ }
+
+ /**
+ * Constructs a new {@link MediaInfo} object from a bundle.
+ */
+ public static @Nullable MediaInfo fromBundle(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+
+ String contentId = bundle.getString(KEY_CONTENT_ID);
+ int streamType = bundle.getInt(KEY_STREAM_TYPE, STREAM_TYPE_INVALID);
+ String contentType = bundle.getString(KEY_CONTENT_TYPE);
+
+ MediaInfo info = new MediaInfo(contentId, streamType, contentType);
+ info.setMetadata(MediaMetadata.fromBundle(bundle.getBundle(KEY_METADATA)));
+ info.setStreamDuration(bundle.getLong(KEY_DURATION));
+ info.setTextTrackStyle(TextTrackStyle.fromBundle(
+ bundle.getBundle(KEY_TEXT_TRACK_STYLE)));
+ info.setExtras(bundle.getBundle("extras"));
+
+ Parcelable[] trackBundles = bundle.getParcelableArray(KEY_TRACKS);
+ if (trackBundles != null) {
+ for (int i = 0; i < trackBundles.length; ++i) {
+ info.mMediaTracks.add(MediaTrack.fromBundle((Bundle)trackBundles[i]));
+ }
+ }
+
+ return info;
+ }
+ }
+
+ /**
+ * Container class for media metadata. Metadata has a media type, an optional
+ * list of images, and a collection of metadata fields. Keys for common
+ * metadata fields are predefined as constants, but the application is free to
+ * define and use additional fields of its own.
+ * <p>
+ * The values of the predefined fields have predefined types. For example, a track number is
+ * an <code>int</code> and a creation date is a <code>Calendar</code>. Attempting to
+ * store a value of an incorrect type in a field will result in a
+ * {@link IllegalArgumentException}.
+ */
+ public static final class MediaMetadata {
+ /** A media type representing generic media content. */
+ public static final int MEDIA_TYPE_GENERIC = 0;
+ /** A media type representing a movie. */
+ public static final int MEDIA_TYPE_MOVIE = 1;
+ /** A media type representing an TV show. */
+ public static final int MEDIA_TYPE_TV_SHOW = 2;
+ /** A media type representing a music track. */
+ public static final int MEDIA_TYPE_MUSIC_TRACK = 3;
+ /** A media type representing a photo. */
+ public static final int MEDIA_TYPE_PHOTO = 4;
+ /** The smallest media type value that can be assigned for application-defined media types. */
+ public static final int MEDIA_TYPE_USER = 100;
+
+ // Field types.
+ private static final int TYPE_NONE = 0;
+ private static final int TYPE_STRING = 1;
+ private static final int TYPE_INT = 2;
+ private static final int TYPE_DOUBLE = 3;
+ private static final int TYPE_DATE = 4;
+
+ // Field type names. Used when constructing exceptions.
+ private static final String[] sTypeNames = { null, "String", "int", "double", "Calendar" };
+
+ private final int mMediaType;
+ private final Bundle mFields;
+ private final ArrayList<WebImage> mImages;
+
+ private static final String BUNDLE_KEY_MEDIA_TYPE = "mediaType";
+ private static final String BUNDLE_KEY_FIELDS = "fields";
+ private static final String BUNDLE_KEY_IMAGES = "images";
+
+ /**
+ * String key: Creation date.
+ * <p>
+ * The value is the date and/or time at which the media was created.
+ * For example, this could be the date and time at which a photograph was taken or a piece of
+ * music was recorded.
+ */
+ public static final String KEY_CREATION_DATE =
+ "android.support.media.protocols.metadata.CREATION_DATE";
+
+ /**
+ * String key: Release date.
+ * <p>
+ * The value is the date and/or time at which the media was released.
+ * For example, this could be the date that a movie or music album was released.
+ */
+ public static final String KEY_RELEASE_DATE =
+ "android.support.media.protocols.metadata.RELEASE_DATE";
+
+ /**
+ * String key: Broadcast date.
+ * <p>
+ * The value is the date and/or time at which the media was first broadcast.
+ * For example, this could be the date that a TV show episode was first aired.
+ */
+ public static final String KEY_BROADCAST_DATE =
+ "android.support.media.protocols.metadata.BROADCAST_DATE";
+
+ /**
+ * String key: Title.
+ * <p>
+ * The title of the media. For example, this could be the title of a song, movie, or TV show
+ * episode. This value is suitable for display purposes.
+ */
+ public static final String KEY_TITLE =
+ "android.support.media.protocols.metadata.TITLE";
+
+ /**
+ * String key: Subtitle.
+ * <p>
+ * The subtitle of the media. This value is suitable for display purposes.
+ */
+ public static final String KEY_SUBTITLE =
+ "android.support.media.protocols.metadata.SUBTITLE";
+
+ /**
+ * String key: Artist.
+ * <p>
+ * The name of the artist who created the media. For example, this could be the name of a
+ * musician, performer, or photographer. This value is suitable for display purposes.
+ */
+ public static final String KEY_ARTIST =
+ "android.support.media.protocols.metadata.ARTIST";
+
+ /**
+ * String key: Album artist.
+ * <p>
+ * The name of the artist who produced an album. For example, in compilation albums such as DJ
+ * mixes, the album artist is not necessarily the same as the artist(s) of the individual songs
+ * on the album. This value is suitable for display purposes.
+ */
+ public static final String KEY_ALBUM_ARTIST =
+ "android.support.media.protocols.metadata.ALBUM_ARTIST";
+
+ /**
+ * String key: Album title.
+ * <p>
+ * The title of the album that a music track belongs to. This value is suitable for display
+ * purposes.
+ */
+ public static final String KEY_ALBUM_TITLE =
+ "android.support.media.protocols.metadata.ALBUM_TITLE";
+
+ /**
+ * String key: Composer.
+ * <p>
+ * The name of the composer of a music track. This value is suitable for display purposes.
+ */
+ public static final String KEY_COMPOSER =
+ "android.support.media.protocols.metadata.COMPOSER";
+
+ /**
+ * Integer key: Disc number.
+ * <p>
+ * The disc number (counting from 1) that a music track belongs to in a multi-disc album.
+ */
+ public static final String KEY_DISC_NUMBER =
+ "android.support.media.protocols.metadata.DISC_NUMBER";
+
+ /**
+ * Integer key: Track number.
+ * <p>
+ * The track number of a music track on an album disc. Typically track numbers are counted
+ * starting from 1, however this value may be 0 if it is a "hidden track" at the beginning of
+ * an album.
+ */
+ public static final String KEY_TRACK_NUMBER =
+ "android.support.media.protocols.metadata.TRACK_NUMBER";
+
+ /**
+ * Integer key: Season number.
+ * <p>
+ * The season number that a TV show episode belongs to. Typically season numbers are counted
+ * starting from 1, however this value may be 0 if it is a "pilot" episode that predates the
+ * official start of a TV series.
+ */
+ public static final String KEY_SEASON_NUMBER =
+ "android.support.media.protocols.metadata.SEASON_NUMBER";
+
+ /**
+ * Integer key: Episode number.
+ * <p>
+ * The number of an episode in a given season of a TV show. Typically episode numbers are
+ * counted starting from 1, however this value may be 0 if it is a "pilot" episode that is not
+ * considered to be an official episode of the first season.
+ */
+ public static final String KEY_EPISODE_NUMBER =
+ "android.support.media.protocols.metadata.EPISODE_NUMBER";
+
+ /**
+ * String key: Series title.
+ * <p>
+ * The name of a series. For example, this could be the name of a TV show or series of related
+ * music albums. This value is suitable for display purposes.
+ */
+ public static final String KEY_SERIES_TITLE =
+ "android.support.media.protocols.metadata.SERIES_TITLE";
+
+ /**
+ * String key: Studio.
+ * <p>
+ * The name of a recording studio that produced a piece of media. For example, this could be
+ * the name of a movie studio or music label. This value is suitable for display purposes.
+ */
+ public static final String KEY_STUDIO =
+ "android.support.media.protocols.metadata.STUDIO";
+
+ /**
+ * Integer key: Width.
+ *
+ * The width of a piece of media, in pixels. This would typically be used for providing the
+ * dimensions of a photograph.
+ */
+ public static final String KEY_WIDTH =
+ "android.support.media.protocols.metadata.WIDTH";
+
+ /**
+ * Integer key: Height.
+ *
+ * The height of a piece of media, in pixels. This would typically be used for providing the
+ * dimensions of a photograph.
+ */
+ public static final String KEY_HEIGHT =
+ "android.support.media.protocols.metadata.HEIGHT";
+
+ /**
+ * String key: Location name.
+ * <p>
+ * The name of a location where a piece of media was created. For example, this could be the
+ * location of a photograph or the principal filming location of a movie. This value is
+ * suitable for display purposes.
+ */
+ public static final String KEY_LOCATION_NAME =
+ "android.support.media.protocols.metadata.LOCATION_NAME";
+
+ /**
+ * Double key: Location latitude.
+ * <p>
+ * The latitude component of the geographical location where a piece of media was created.
+ * For example, this could be the location of a photograph or the principal filming location of
+ * a movie.
+ */
+ public static final String KEY_LOCATION_LATITUDE =
+ "android.support.media.protocols.metadata.LOCATION_LATITUDE";
+
+ /**
+ * Double key: Location longitude.
+ * <p>
+ * The longitude component of the geographical location where a piece of media was created.
+ * For example, this could be the location of a photograph or the principal filming location of
+ * a movie.
+ */
+ public static final String KEY_LOCATION_LONGITUDE =
+ "android.support.media.protocols.metadata.LOCATION_LONGITUDE";
+
+ /**
+ * Constructs a new, empty, MediaMetadata with a media type of {@link #MEDIA_TYPE_GENERIC}.
+ */
+ public MediaMetadata() {
+ this(MEDIA_TYPE_GENERIC);
+ }
+
+ /**
+ * Constructs a new, empty, MediaMetadata with the given media type.
+ *
+ * @param mediaType The media type; one of the {@code MEDIA_TYPE_*} constants, or a value
+ * greater than or equal to {@link #MEDIA_TYPE_USER} for custom media types.
+ */
+ public MediaMetadata(int mediaType) {
+ this(mediaType, null);
+ }
+
+ private MediaMetadata(int mediaType, Bundle fields) {
+ mMediaType = mediaType;
+ mFields = fields != null ? fields : new Bundle();
+ mImages = new ArrayList<WebImage>();
+ }
+
+ /**
+ * Gets the media type.
+ */
+ public int getMediaType() {
+ return mMediaType;
+ }
+
+ /**
+ * Clears this object. The media type is left unchanged.
+ */
+ public void clear() {
+ mFields.clear();
+ mImages.clear();
+ }
+
+ /**
+ * Tests if the object contains a field with the given key.
+ */
+ public boolean containsKey(@NonNull String key) {
+ return mFields.containsKey(key);
+ }
+
+ /**
+ * Returns a set of keys for all fields that are present in the object.
+ */
+ public @NonNull Set<String> keySet() {
+ return mFields.keySet();
+ }
+
+ /**
+ * Stores a value in a String field.
+ *
+ * @param key The key for the field.
+ * @param value The new value for the field.
+ * @throws IllegalArgumentException If the key is {@code null} or empty or refers to a
+ * predefined field which is not a {@code String} field.
+ */
+ public void putString(@NonNull String key, String value) {
+ throwIfWrongType(key, TYPE_STRING);
+ mFields.putString(key, value);
+ }
+
+ /**
+ * Reads the value of a String field.
+ *
+ * @return The value of the field, or {@code null} if the field has not been set.
+ * @throws IllegalArgumentException If the key is {@code null} or empty or refers to a
+ * predefined field which is not a {@code String} field.
+ */
+ public @Nullable String getString(@NonNull String key) {
+ throwIfWrongType(key, TYPE_STRING);
+ return mFields.getString(key);
+ }
+
+ /**
+ * Stores a value in an int field.
+ *
+ * @param key The key for the field.
+ * @param value The new value for the field.
+ * @throws IllegalArgumentException If the key is {@code null} or empty or refers to a
+ * predefined field which is not an {@code int} field.
+ */
+ public void putInt(@NonNull String key, int value) {
+ throwIfWrongType(key, TYPE_INT);
+ mFields.putInt(key, value);
+ }
+
+ /**
+ * Reads the value of an {@code int} field.
+ *
+ * @return The value of the field, or {@code null} if the field has not been set.
+ * @throws IllegalArgumentException If the key is {@code null} or empty or refers to a
+ * predefined field which is not an {@code int} field.
+ */
+ public int getInt(@NonNull String key) {
+ throwIfWrongType(key, TYPE_INT);
+ return mFields.getInt(key);
+ }
+
+ /**
+ * Stores a value in a {@code double} field.
+ *
+ * @param key The key for the field.
+ * @param value The new value for the field.
+ * @throws IllegalArgumentException If the key is {@code null} or empty or refers to a
+ * predefined field which is not a {@code double} field.
+ */
+ public void putDouble(@NonNull String key, double value) {
+ throwIfWrongType(key, TYPE_DOUBLE);
+ mFields.putDouble(key, value);
+ }
+
+ /**
+ * Reads the value of a {@code double} field.
+ *
+ * @return The value of the field, or {@code null} if the field has not been set.
+ * @throws IllegalArgumentException If the key is {@code null} or empty or refers to a
+ * predefined field which is not a {@code double} field.
+ */
+ public double getDouble(@NonNull String key) {
+ throwIfWrongType(key, TYPE_DOUBLE);
+ return mFields.getDouble(key);
+ }
+
+ /**
+ * Stores a value in a date field.
+ *
+ * @param key The key for the field.
+ * @param value The new value for the field.
+ * @throws IllegalArgumentException If the key is {@code null} or empty or refers to a
+ * predefined field which is not a date field.
+ */
+ public void putDate(@NonNull String key, @Nullable Calendar value) {
+ throwIfWrongType(key, TYPE_DATE);
+ if (value != null) {
+ mFields.putLong(key, value.getTimeInMillis());
+ } else {
+ mFields.remove(key);
+ }
+ }
+
+ /**
+ * Reads the value of a date field.
+ *
+ * @param key The field name.
+ * @return The date, as a {@link Calendar}, or {@code null} if this field has not been set.
+ * @throws IllegalArgumentException If the key is {@code null} or empty or the specified field's
+ * predefined type is not a date.
+ */
+ public @Nullable Calendar getDate(String key) {
+ throwIfWrongType(key, TYPE_DATE);
+ if (mFields.containsKey(key)) {
+ Calendar date = Calendar.getInstance();
+ date.setTimeInMillis(mFields.getLong(key));
+ return date;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the list of images. If there are no images, returns an empty list.
+ */
+ public List<WebImage> getImages() {
+ return mImages;
+ }
+
+ /**
+ * Checks if the metadata includes any images.
+ */
+ public boolean hasImages() {
+ return (mImages != null) && !mImages.isEmpty();
+ }
+
+ /**
+ * Clears the list of images.
+ */
+ public void clearImages() {
+ mImages.clear();
+ }
+
+ /**
+ * Adds an image to the list of images.
+ */
+ public void addImage(WebImage image) {
+ mImages.add(image);
+ }
+
+ /*
+ * Verifies that a key is not empty, and if the key is a predefined key, verifies that it has
+ * the specified type.
+ */
+ private void throwIfWrongType(String key, int type) {
+ if (TextUtils.isEmpty(key)) {
+ throw new IllegalArgumentException("null and empty keys are not allowed");
+ }
+ int actualType = getFieldType(key);
+ if ((actualType != type) && (actualType != TYPE_NONE))
+ throw new IllegalArgumentException("Value for " + key + " must be a "
+ + sTypeNames[type]);
+ }
+
+ /**
+ * Creates a bundle representation of the object.
+ */
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(BUNDLE_KEY_MEDIA_TYPE, mMediaType);
+ bundle.putBundle(BUNDLE_KEY_FIELDS, mFields);
+
+ if (mImages.isEmpty()) {
+ Parcelable[] imageBundles = new Parcelable[mImages.size()];
+ for (int i = 0; i < imageBundles.length; i++) {
+ imageBundles[i] = mImages.get(i).toBundle();
+ }
+ bundle.putParcelableArray(BUNDLE_KEY_IMAGES, imageBundles);
+ }
+
+ return bundle;
+ }
+
+ /**
+ * Constructs a new {@link MediaMetadata} object from a bundle.
+ */
+ public static MediaMetadata fromBundle(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+
+ int mediaType = bundle.getInt(BUNDLE_KEY_MEDIA_TYPE);
+ Bundle fields = bundle.getBundle(BUNDLE_KEY_FIELDS);
+ MediaMetadata metadata = new MediaMetadata(mediaType, fields);
+
+ Parcelable[] imageBundles = bundle.getParcelableArray(BUNDLE_KEY_IMAGES);
+ if (imageBundles != null) {
+ for (Parcelable imageBundle : imageBundles) {
+ metadata.addImage(WebImage.fromBundle((Bundle)imageBundle));
+ }
+ }
+
+ return metadata;
+ }
+
+ private static int getFieldType(String key) {
+ switch (key) {
+ case KEY_CREATION_DATE: return TYPE_DATE;
+ case KEY_RELEASE_DATE: return TYPE_DATE;
+ case KEY_BROADCAST_DATE: return TYPE_DATE;
+ case KEY_TITLE: return TYPE_STRING;
+ case KEY_SUBTITLE: return TYPE_STRING;
+ case KEY_ARTIST: return TYPE_STRING;
+ case KEY_ALBUM_ARTIST: return TYPE_STRING;
+ case KEY_ALBUM_TITLE: return TYPE_STRING;
+ case KEY_COMPOSER: return TYPE_STRING;
+ case KEY_DISC_NUMBER: return TYPE_INT;
+ case KEY_TRACK_NUMBER: return TYPE_INT;
+ case KEY_SEASON_NUMBER: return TYPE_INT;
+ case KEY_EPISODE_NUMBER: return TYPE_INT;
+ case KEY_SERIES_TITLE: return TYPE_STRING;
+ case KEY_STUDIO: return TYPE_STRING;
+ case KEY_WIDTH: return TYPE_INT;
+ case KEY_HEIGHT: return TYPE_INT;
+ case KEY_LOCATION_NAME: return TYPE_STRING;
+ case KEY_LOCATION_LATITUDE: return TYPE_DOUBLE;
+ case KEY_LOCATION_LONGITUDE: return TYPE_DOUBLE;
+ default: return TYPE_NONE;
+ }
+ }
+ }
+
+ /**
+ * A class that holds status information about some media.
+ */
+ public static final class MediaStatus {
+ private static final String KEY_ACTIVE_TRACK_IDS = "activeTrackIds";
+ private static final String KEY_CURRENT_TIME = "currentTime";
+ private static final String KEY_EXTRAS = "extras";
+ private static final String KEY_IDLE_REASON = "idleReason";
+ private static final String KEY_MEDIA = "media";
+ private static final String KEY_MEDIA_SESSION_ID = "mediaSessionId";
+ private static final String KEY_MUTED = "muted";
+ private static final String KEY_PLAYBACK_RATE = "playbackRate";
+ private static final String KEY_PLAYER_STATE = "playerState";
+ private static final String KEY_SUPPORTED_MEDIA_COMMANDS = "supportedMediaCommands";
+ private static final String KEY_VOLUME = "volume";
+
+ /** A flag (bitmask) indicating that a media item can be paused. */
+ public static final long COMMAND_PAUSE = 1 << 0;
+
+ /** A flag (bitmask) indicating that a media item supports seeking. */
+ public static final long COMMAND_SEEK = 1 << 1;
+
+ /** A flag (bitmask) indicating that a media item's audio volume can be changed. */
+ public static final long COMMAND_SET_VOLUME = 1 << 2;
+
+ /** A flag (bitmask) indicating that a media item's audio can be muted. */
+ public static final long COMMAND_TOGGLE_MUTE = 1 << 3;
+
+ /** A flag (bitmask) indicating that a media item supports skipping forward. */
+ public static final long COMMAND_SKIP_FORWARD = 1 << 4;
+
+ /** A flag (bitmask) indicating that a media item supports skipping backward. */
+ public static final long COMMAND_SKIP_BACKWARD = 1 << 5;
+
+ /** Constant indicating unknown player state. */
+ public static final int PLAYER_STATE_UNKNOWN = 0;
+
+ /** Constant indicating that the media player is idle. */
+ public static final int PLAYER_STATE_IDLE = 1;
+
+ /** Constant indicating that the media player is playing. */
+ public static final int PLAYER_STATE_PLAYING = 2;
+
+ /** Constant indicating that the media player is paused. */
+ public static final int PLAYER_STATE_PAUSED = 3;
+
+ /** Constant indicating that the media player is buffering. */
+ public static final int PLAYER_STATE_BUFFERING = 4;
+
+ /** Constant indicating that the player currently has no idle reason. */
+ public static final int IDLE_REASON_NONE = 0;
+
+ /** Constant indicating that the player is idle because playback has finished. */
+ public static final int IDLE_REASON_FINISHED = 1;
+
+ /**
+ * Constant indicating that the player is idle because playback has been canceled in
+ * response to a STOP command.
+ */
+ public static final int IDLE_REASON_CANCELED = 2;
+
+ /**
+ * Constant indicating that the player is idle because playback has been interrupted by
+ * a LOAD command.
+ */
+ public static final int IDLE_REASON_INTERRUPTED = 3;
+
+ /** Constant indicating that the player is idle because a playback error has occurred. */
+ public static final int IDLE_REASON_ERROR = 4;
+
+ private final long mMediaSessionId;
+ private final MediaInfo mMediaInfo;
+ private double mPlaybackRate;
+ private int mPlayerState;
+ private int mIdleReason;
+ private long mStreamPosition;
+ private long mSupportedMediaCommands;
+ private double mVolume;
+ private boolean mMuteState;
+ private long mActiveTrackIds[];
+ private Bundle mExtras;
+
+ /**
+ * Constructs a new {@link MediaStatus} object with the given properties.
+ */
+ public MediaStatus(long mediaSessionId, @NonNull MediaInfo mediaInfo) {
+ if (mediaInfo == null) {
+ throw new IllegalArgumentException("mediaInfo must not be null");
+ }
+
+ mMediaSessionId = mediaSessionId;
+ mMediaInfo = mediaInfo;
+ mPlayerState = PLAYER_STATE_UNKNOWN;
+ mIdleReason = IDLE_REASON_NONE;
+ }
+
+ /**
+ * Returns the media session ID for this item.
+ */
+ public long getMediaSessionId() {
+ return mMediaSessionId;
+ }
+
+ /**
+ * Returns the {@link MediaInfo} for this item.
+ */
+ public @NonNull MediaInfo getMediaInfo() {
+ return mMediaInfo;
+ }
+
+ /**
+ * Gets the current media player state.
+ */
+ public int getPlayerState() {
+ return mPlayerState;
+ }
+
+ /**
+ * Sets the current media player state.
+ */
+ public void setPlayerState(int playerState) {
+ mPlayerState = playerState;
+ }
+
+ /**
+ * Gets the player state idle reason. This value is only meaningful if the player state is
+ * in fact {@link #PLAYER_STATE_IDLE}.
+ */
+ public int getIdleReason() {
+ return mIdleReason;
+ }
+
+ /**
+ * Sets the player state idle reason. This value is only meaningful if the player state is
+ * in fact {@link #PLAYER_STATE_IDLE}.
+ */
+ public void setIdleReason(int idleReason) {
+ mIdleReason = idleReason;
+ }
+
+ /**
+ * Gets the current stream playback rate. This will be negative if the stream is seeking
+ * backwards, 0 if the stream is paused, 1 if the stream is playing normally, and some other
+ * positive value if the stream is seeking forwards.
+ */
+ public double getPlaybackRate() {
+ return mPlaybackRate;
+ }
+
+ /**
+ * Sets the current stream playback rate. This will be negative if the stream is seeking
+ * backwards, 0 if the stream is paused, 1 if the stream is playing normally, and some other
+ * positive value if the stream is seeking forwards.
+ */
+ public void setPlaybackRate(double playbackRate) {
+ mPlaybackRate = playbackRate;
+ }
+
+ /**
+ * Returns the current stream position, in milliseconds.
+ */
+ public long getStreamPosition() {
+ return mStreamPosition;
+ }
+
+ /**
+ * Sets the current stream position, in milliseconds.
+ */
+ public void setStreamPosition(long streamPosition) {
+ mStreamPosition = streamPosition;
+ }
+
+ /**
+ * Tests if the stream supports a given control command.
+ *
+ * @param mediaCommand The media command.
+ * @return {@code true} if the command is supported, {@code false} otherwise.
+ */
+ public boolean isMediaCommandSupported(long mediaCommand) {
+ return (mSupportedMediaCommands & mediaCommand) != 0;
+ }
+
+ /**
+ * Sets whether the stream supports a given control command.
+ */
+ public void setSupportedMediaCommands(long supportedMediaCommands) {
+ mSupportedMediaCommands = supportedMediaCommands;
+ }
+
+ /**
+ * Returns the stream's volume.
+ */
+ public double getStreamVolume() {
+ return mVolume;
+ }
+
+ /**
+ * Sets the stream's volume.
+ */
+ public void setStreamVolume(double volume) {
+ mVolume = volume;
+ }
+
+ /**
+ * Returns the stream's mute state.
+ */
+ public boolean isMute() {
+ return mMuteState;
+ }
+
+ /**
+ * Sets the stream's mute state.
+ */
+ public void setMute(boolean muteState) {
+ mMuteState = muteState;
+ }
+
+ /**
+ * Returns the list of active track IDs, if any, otherwise {@code null}.
+ */
+ public @Nullable long[] getActiveTrackIds() {
+ return mActiveTrackIds;
+ }
+
+ /**
+ * Sets the list of active track IDs, if any, otherwise {@code null}.
+ */
+ public void setActiveTrackIds(@Nullable long[] trackIds) {
+ mActiveTrackIds = trackIds;
+ }
+
+ /**
+ * Returns any extras that are is associated with the media item.
+ */
+ public @Nullable Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Sets any extras that are associated with the media item.
+ */
+ public void setExtras(@Nullable Bundle extras) {
+ mExtras = extras;
+ }
+
+ /**
+ * Creates a bundle representation of the object.
+ */
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putLong(KEY_MEDIA_SESSION_ID, mMediaSessionId);
+ bundle.putBundle(KEY_MEDIA, mMediaInfo.toBundle());
+ bundle.putLongArray(KEY_ACTIVE_TRACK_IDS, mActiveTrackIds);
+ bundle.putLong(KEY_CURRENT_TIME, mStreamPosition);
+ bundle.putInt(KEY_IDLE_REASON, mIdleReason);
+ bundle.putBoolean(KEY_MUTED, mMuteState);
+ bundle.putDouble(KEY_PLAYBACK_RATE, mPlaybackRate);
+ bundle.putInt(KEY_PLAYER_STATE, mPlayerState);
+ bundle.putLong(KEY_SUPPORTED_MEDIA_COMMANDS, mSupportedMediaCommands);
+ bundle.putDouble(KEY_VOLUME, mVolume);
+ return bundle;
+ }
+
+ /**
+ * Constructs a new {@link MediaStatus} object from a bundle.
+ */
+ public static MediaStatus fromBundle(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+
+ long mediaSessionId = bundle.getLong(KEY_MEDIA_SESSION_ID);
+ MediaInfo mediaInfo = MediaInfo.fromBundle(bundle.getBundle(KEY_MEDIA));
+ MediaStatus status = new MediaStatus(mediaSessionId, mediaInfo);
+
+ status.setActiveTrackIds(bundle.getLongArray(KEY_ACTIVE_TRACK_IDS));
+ status.setStreamPosition(bundle.getLong(KEY_CURRENT_TIME));
+ status.setIdleReason(bundle.getInt(KEY_IDLE_REASON));
+ status.setMute(bundle.getBoolean(KEY_MUTED));
+ status.setPlaybackRate(bundle.getDouble(KEY_PLAYBACK_RATE));
+ status.setPlayerState(bundle.getInt(KEY_PLAYER_STATE));
+ status.setSupportedMediaCommands(bundle.getLong(KEY_SUPPORTED_MEDIA_COMMANDS));
+ status.setStreamVolume(bundle.getDouble(KEY_VOLUME));
+ status.setExtras(bundle.getBundle(KEY_EXTRAS));
+ return status;
+ }
+ }
+
+ /**
+ * A class that represents a media track, such as a language track or closed caption text track
+ * in a video.
+ */
+ public static final class MediaTrack {
+ private static final String KEY_TRACK_ID = "trackId";
+ private static final String KEY_TYPE = "type";
+ private static final String KEY_TRACK_CONTENT_ID = "trackContentId";
+ private static final String KEY_TRACK_CONTENT_TYPE = "trackContentType";
+ private static final String KEY_NAME = "name";
+ private static final String KEY_LANGUAGE = "language";
+ private static final String KEY_SUBTYPE = "subtype";
+ private static final String KEY_EXTRAS = "extras";
+
+ /** A media track type indicating an unknown track type. */
+ public static final int TYPE_UNKNOWN = 0;
+ /** A media track type indicating a text track. */
+ public static final int TYPE_TEXT = 1;
+ /** A media track type indicating an audio track. */
+ public static final int TYPE_AUDIO = 2;
+ /** A media track type indicating a video track. */
+ public static final int TYPE_VIDEO = 3;
+
+ /** A media track subtype indicating an unknown subtype. */
+ public static final int SUBTYPE_UNKNOWN = -1;
+ /** A media track subtype indicating no subtype. */
+ public static final int SUBTYPE_NONE = 0;
+ /** A media track subtype indicating subtitles. */
+ public static final int SUBTYPE_SUBTITLES = 1;
+ /** A media track subtype indicating closed captions. */
+ public static final int SUBTYPE_CAPTIONS = 2;
+ /** A media track subtype indicating descriptions. */
+ public static final int SUBTYPE_DESCRIPTIONS = 3;
+ /** A media track subtype indicating chapters. */
+ public static final int SUBTYPE_CHAPTERS = 4;
+ /** A media track subtype indicating metadata. */
+ public static final int SUBTYPE_METADATA = 5;
+
+ private long mId;
+ private int mType;
+ private String mContentId;
+ private String mContentType;
+ private String mName;
+ private String mLanguage;
+ private int mSubtype;
+ private Bundle mExtras;
+
+ /**
+ * Constructs a new track with the given track ID and type.
+ *
+ * @throws IllegalArgumentException If the track type is invalid.
+ */
+ public MediaTrack(long id, int type) {
+ clear();
+ mId = id;
+ if ((type <= TYPE_UNKNOWN) || (type > TYPE_VIDEO)) {
+ throw new IllegalArgumentException("invalid type " + type);
+ }
+ mType = type;
+ }
+
+ /**
+ * Returns the unique ID of the media track.
+ */
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the type of the track; one of the {@code TYPE_} constants defined above.
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Returns the content ID of the media track.
+ */
+ public String getContentId() {
+ return mContentId;
+ }
+
+ /**
+ * Sets the content ID for the media track.
+ */
+ public void setContentId(String contentId) {
+ mContentId = contentId;
+ }
+
+ /**
+ * Returns the content type (MIME type) of the media track, or {@code null} if none was
+ * specified.
+ */
+ public String getContentType() {
+ return mContentType;
+ }
+
+ /**
+ * Sets the content type (MIME type) of the media track.
+ */
+ public void setContentType(String contentType) {
+ mContentType = contentType;
+ }
+
+ /**
+ * Returns the name of the media track, or {@code null} if none was specified.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Sets the track name.
+ */
+ public void setName(String name) {
+ mName = name;
+ }
+
+ /**
+ * Returns the language of this media track, or {@code null} if none was specified.
+ */
+ public String getLanguage() {
+ return mLanguage;
+ }
+
+ /**
+ * Sets the track language.
+ */
+ public void setLanguage(String language) {
+ mLanguage = language;
+ }
+
+ /**
+ * Returns the subtype of this media track; one of the {@code SUBTYPE_}
+ * constants defined above.
+ */
+ public int getSubtype() {
+ return mSubtype;
+ }
+
+ /**
+ * Sets the track subtype.
+ *
+ * @throws IllegalArgumentException If the subtype is invalid.
+ */
+ public void setSubtype(int subtype) {
+ if ((subtype <= SUBTYPE_UNKNOWN) || (subtype > SUBTYPE_METADATA)) {
+ throw new IllegalArgumentException("invalid subtype " + subtype);
+ }
+ if ((subtype != SUBTYPE_NONE) && (mType != TYPE_TEXT)) {
+ throw new IllegalArgumentException("subtypes are only valid for text tracks");
+ }
+
+ mSubtype = subtype;
+ }
+
+ /**
+ * Returns the extras object for this media track, or {@code null} if none was
+ * specified.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Sets the track's extras object.
+ */
+ public void setExtras(Bundle extras) {
+ mExtras = extras;
+ }
+
+ private void clear() {
+ mId = 0;
+ mType = TYPE_UNKNOWN;
+ mContentId = null;
+ mName = null;
+ mLanguage = null;
+ mSubtype = SUBTYPE_UNKNOWN;
+ mExtras = null;
+ }
+
+ /**
+ * Creates a bundle representation of the object.
+ */
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putLong(KEY_TRACK_ID, mId);
+ bundle.putInt(KEY_TYPE, mType);
+ bundle.putString(KEY_TRACK_CONTENT_ID, mContentId);
+ bundle.putString(KEY_TRACK_CONTENT_TYPE, mContentType);
+ bundle.putString(KEY_NAME, mName);
+ bundle.putString(KEY_LANGUAGE, mLanguage);
+ bundle.putInt(KEY_SUBTYPE, mSubtype);
+ bundle.putBundle(KEY_EXTRAS, mExtras);
+ return bundle;
+ }
+
+ /**
+ * Constructs a new {@link MediaTrack} object from a bundle.
+ */
+ public static MediaTrack fromBundle(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+
+ long trackId = bundle.getLong(KEY_TRACK_ID);
+ int type = bundle.getInt(KEY_TYPE);
+ MediaTrack track = new MediaTrack(trackId, type);
+
+ track.setContentId(bundle.getString(KEY_TRACK_CONTENT_ID));
+ track.setContentType(bundle.getString(KEY_TRACK_CONTENT_TYPE));
+ track.setName(bundle.getString(KEY_NAME));
+ track.setLanguage(bundle.getString(KEY_LANGUAGE));
+ track.setSubtype(bundle.getInt(KEY_SUBTYPE));
+ track.setExtras(bundle.getBundle(KEY_EXTRAS));
+ return track;
+ }
+ }
+
+ /**
+ * A class that specifies how a text track's text will be displayed on-screen. The text is
+ * displayed inside a rectangular "window". The appearance of both the text and the window are
+ * configurable.
+ * <p>
+ * With the exception of the font scale, which has a predefined default value, any attribute that
+ * is not explicitly set will remain "unspecified", and the player will select an appropriate
+ * value.
+ */
+ public static final class TextTrackStyle {
+ /** The default font scale. */
+ public static final float DEFAULT_FONT_SCALE = 1.0f;
+
+ /** A color value that indicates an unspecified (unset) color. */
+ public static final int COLOR_UNSPECIFIED = 0;
+
+ /** An edge type indicating an unspecified edge type. */
+ public static final int EDGE_TYPE_UNSPECIFIED = -1;
+ /** An edge type indicating no edge. */
+ public static final int EDGE_TYPE_NONE = 0;
+ /** An edge type indicating an outline edge. */
+ public static final int EDGE_TYPE_OUTLINE = 1;
+ /** An edge type indicating a drop shadow edge. */
+ public static final int EDGE_TYPE_DROP_SHADOW = 2;
+ /** An edge type indicating a raised edge. */
+ public static final int EDGE_TYPE_RAISED = 3;
+ /** An edge type indicating a depressed edge. */
+ public static final int EDGE_TYPE_DEPRESSED = 4;
+
+ /** A window type indicating an unspecified window type. */
+ public static final int WINDOW_TYPE_UNSPECIFIED = -1;
+ /** A window type indicating no window type. */
+ public static final int WINDOW_TYPE_NONE = 0;
+ /** A window type indicating a normal window. */
+ public static final int WINDOW_TYPE_NORMAL = 1;
+ /** A window type indicating a window with rounded corners. */
+ public static final int WINDOW_TYPE_ROUNDED = 2;
+
+ /** A font family indicating an unspecified font family. */
+ public static final int FONT_FAMILY_UNSPECIFIED = -1;
+ /** A font family indicating Sans Serif. */
+ public static final int FONT_FAMILY_SANS_SERIF = 0;
+ /** A font family indicating Monospaced Sans Serif. */
+ public static final int FONT_FAMILY_MONOSPACED_SANS_SERIF = 1;
+ /** A font family indicating Serif. */
+ public static final int FONT_FAMILY_SERIF = 2;
+ /** A font family indicating Monospaced Serif. */
+ public static final int FONT_FAMILY_MONOSPACED_SERIF = 3;
+ /** A font family indicating Casual. */
+ public static final int FONT_FAMILY_CASUAL = 4;
+ /** A font family indicating Cursive. */
+ public static final int FONT_FAMILY_CURSIVE = 5;
+ /** A font family indicating Small Capitals. */
+ public static final int FONT_FAMILY_SMALL_CAPITALS = 6;
+
+ /** A font style indicating an unspecified style. */
+ public static final int FONT_STYLE_UNSPECIFIED = -1;
+ /** A font style indicating a normal style. */
+ public static final int FONT_STYLE_NORMAL = 0;
+ /** A font style indicating a bold style. */
+ public static final int FONT_STYLE_BOLD = 1;
+ /** A font style indicating an italic style. */
+ public static final int FONT_STYLE_ITALIC = 2;
+ /** A font style indicating a bold and italic style. */
+ public static final int FONT_STYLE_BOLD_ITALIC = 3;
+
+ private static final String KEY_FONT_SCALE = "fontScale";
+ private static final String KEY_FOREGROUND_COLOR = "foregroundColor";
+ private static final String KEY_BACKGROUND_COLOR = "backgroundColor";
+ private static final String KEY_EDGE_TYPE = "edgeType";
+ private static final String KEY_EDGE_COLOR = "edgeColor";
+ private static final String KEY_WINDOW_TYPE = "windowType";
+ private static final String KEY_WINDOW_COLOR = "windowColor";
+ private static final String KEY_WINDOW_CORNER_RADIUS = "windowRoundedCornerRadius";
+ private static final String KEY_FONT_FAMILY = "fontFamily";
+ private static final String KEY_FONT_GENERIC_FAMILY = "fontGenericFamily";
+ private static final String KEY_FONT_STYLE = "fontStyle";
+ private static final String KEY_EXTRAS = "extras";
+
+ private float mFontScale;
+ private int mForegroundColor;
+ private int mBackgroundColor;
+ private int mEdgeType;
+ private int mEdgeColor;
+ private int mWindowType;
+ private int mWindowColor;
+ private int mWindowCornerRadius;
+ private String mFontFamily;
+ private int mFontGenericFamily;
+ private int mFontStyle;
+ private Bundle mExtras;
+
+ /**
+ * Constructs a new TextTrackStyle.
+ */
+ public TextTrackStyle() {
+ clear();
+ }
+
+ /**
+ * Sets the font scale factor. The default is {@link #DEFAULT_FONT_SCALE}.
+ */
+ public void setFontScale(float fontScale) {
+ mFontScale = fontScale;
+ }
+
+ /**
+ * Gets the font scale factor.
+ */
+ public float getFontScale() {
+ return mFontScale;
+ }
+
+ /**
+ * Sets the text's foreground color.
+ *
+ * @param foregroundColor The color, as an ARGB value.
+ */
+ public void setForegroundColor(int foregroundColor) {
+ mForegroundColor = foregroundColor;
+ }
+
+ /**
+ * Gets the text's foreground color.
+ */
+ public int getForegroundColor() {
+ return mForegroundColor;
+ }
+
+ /**
+ * Sets the text's background color.
+ *
+ * @param backgroundColor The color, as an ARGB value.
+ */
+ public void setBackgroundColor(int backgroundColor) {
+ mBackgroundColor = backgroundColor;
+ }
+
+ /**
+ * Gets the text's background color.
+ */
+ public int getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ /**
+ * Sets the caption window's edge type.
+ *
+ * @param edgeType The edge type; one of the {@code EDGE_TYPE_} constants defined above.
+ */
+ public void setEdgeType(int edgeType) {
+ if ((edgeType < EDGE_TYPE_NONE) || (edgeType > EDGE_TYPE_DEPRESSED)) {
+ throw new IllegalArgumentException("invalid edgeType");
+ }
+ mEdgeType = edgeType;
+ }
+
+ /**
+ * Gets the caption window's edge type.
+ */
+ public int getEdgeType() {
+ return mEdgeType;
+ }
+
+ /**
+ * Sets the window's edge color.
+ *
+ * @param edgeColor The color, as an ARGB value.
+ */
+ public void setEdgeColor(int edgeColor) {
+ mEdgeColor = edgeColor;
+ }
+
+ /**
+ * Gets the window's edge color.
+ */
+ public int getEdgeColor() {
+ return mEdgeColor;
+ }
+
+ /**
+ * Sets the window type.
+ *
+ * @param windowType The window type; one of the {@code WINDOW_TYPE_} constants defined above.
+ */
+ public void setWindowType(int windowType) {
+ if ((windowType < WINDOW_TYPE_NONE) || (windowType > WINDOW_TYPE_ROUNDED)) {
+ throw new IllegalArgumentException("invalid windowType");
+ }
+ mWindowType = windowType;
+ }
+
+ /**
+ * Gets the caption window type.
+ */
+ public int getWindowType() {
+ return mWindowType;
+ }
+
+ /**
+ * Sets the window's color.
+ *
+ * @param windowColor The color, as an ARGB value.
+ */
+ public void setWindowColor(int windowColor) {
+ mWindowColor = windowColor;
+ }
+
+ /**
+ * Gets the window's color.
+ */
+ public int getWindowColor() {
+ return mWindowColor;
+ }
+
+ /**
+ * If the window type is {@link #WINDOW_TYPE_ROUNDED}, sets the radius for the window's
+ * corners.
+ *
+ * @param windowCornerRadius The radius, in pixels. Must be a positive value.
+ */
+ public void setWindowCornerRadius(int windowCornerRadius) {
+ if (windowCornerRadius < 0) {
+ throw new IllegalArgumentException("invalid windowCornerRadius");
+ }
+ mWindowCornerRadius = windowCornerRadius;
+ }
+
+ /**
+ * Gets the window corner radius.
+ */
+ public int getWindowCornerRadius() {
+ return mWindowCornerRadius;
+ }
+
+ /**
+ * Sets the text's font family.
+ *
+ * @param fontFamily The text font family.
+ */
+ public void setFontFamily(String fontFamily) {
+ mFontFamily = fontFamily;
+ }
+
+ /**
+ * Gets the text's font family.
+ */
+ public String getFontFamily() {
+ return mFontFamily;
+ }
+
+ /**
+ * Sets the text's generic font family. This will be used if the font family specified with
+ * {@link #setFontFamily} (if any) is unavailable.
+ *
+ * @param fontGenericFamily The generic family; one of the {@code FONT_FAMILY_} constants
+ * defined above.
+ */
+ public void setFontGenericFamily(int fontGenericFamily) {
+ if ((fontGenericFamily < FONT_FAMILY_SANS_SERIF)
+ || (fontGenericFamily > FONT_FAMILY_SMALL_CAPITALS)) {
+ throw new IllegalArgumentException("invalid fontGenericFamily");
+ }
+ mFontGenericFamily = fontGenericFamily;
+ }
+
+ /**
+ * Gets the text's generic font family.
+ */
+ public int getFontGenericFamily() {
+ return mFontGenericFamily;
+ }
+
+ /**
+ * Sets the text font style.
+ *
+ * @param fontStyle The font style; one of the {@code FONT_STYLE_} constants defined above.
+ */
+ public void setFontStyle(int fontStyle) {
+ if ((fontStyle < FONT_STYLE_NORMAL) || (fontStyle > FONT_STYLE_BOLD_ITALIC)) {
+ throw new IllegalArgumentException("invalid fontStyle");
+ }
+ mFontStyle = fontStyle;
+ }
+
+ /**
+ * Gets the text font style.
+ */
+ public int getFontStyle() {
+ return mFontStyle;
+ }
+
+ /**
+ * Sets the extras object.
+ */
+ public void setExtras(Bundle extras) {
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the extras object.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ private void clear() {
+ mFontScale = DEFAULT_FONT_SCALE;
+ mForegroundColor = COLOR_UNSPECIFIED;
+ mBackgroundColor = COLOR_UNSPECIFIED;
+ mEdgeType = EDGE_TYPE_UNSPECIFIED;
+ mEdgeColor = COLOR_UNSPECIFIED;
+ mWindowType = WINDOW_TYPE_UNSPECIFIED;
+ mWindowColor = COLOR_UNSPECIFIED;
+ mWindowCornerRadius = 0;
+ mFontFamily = null;
+ mFontGenericFamily = FONT_FAMILY_UNSPECIFIED;
+ mFontStyle = FONT_STYLE_UNSPECIFIED;
+ mExtras = null;
+ }
+
+ /**
+ * Constructs a new TextTrackStyle based on the systems' current closed caption style settings.
+ * On platform levels below 19, this returns an object with "unspecified" values for all
+ * fields.
+ *
+ * @param context The calling context.
+ * @return The new TextTrackStyle.
+ */
+ public static TextTrackStyle fromSystemSettings(Context context) {
+ TextTrackStyle style = new TextTrackStyle();
+ if (Build.VERSION.SDK_INT >= 19) {
+ Impl19.loadSystemSettings(style, context);
+ }
+ return style;
+ }
+
+ /**
+ * Creates a bundle representation of the object.
+ */
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putFloat(KEY_FONT_SCALE, mFontScale);
+ bundle.putInt(KEY_FOREGROUND_COLOR, mForegroundColor);
+ bundle.putInt(KEY_BACKGROUND_COLOR, mBackgroundColor);
+ bundle.putInt(KEY_EDGE_TYPE, mEdgeType);
+ bundle.putInt(KEY_EDGE_COLOR, mEdgeColor);
+ bundle.putInt(KEY_WINDOW_TYPE, mWindowType);
+ bundle.putInt(KEY_WINDOW_COLOR, mWindowColor);
+ bundle.putInt(KEY_WINDOW_CORNER_RADIUS, mWindowCornerRadius);
+ bundle.putString(KEY_FONT_FAMILY, mFontFamily);
+ bundle.putInt(KEY_FONT_GENERIC_FAMILY, mFontGenericFamily);
+ bundle.putInt(KEY_FONT_STYLE, mFontStyle);
+ bundle.putBundle(KEY_EXTRAS, mExtras);
+ return bundle;
+ }
+
+ /**
+ * Constructs a new {@link MediaTrack} object from a bundle.
+ */
+ public static TextTrackStyle fromBundle(Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+
+ TextTrackStyle style = new TextTrackStyle();
+ style.setFontScale(bundle.getFloat(KEY_FONT_SCALE));
+ style.setForegroundColor(bundle.getInt(KEY_FOREGROUND_COLOR));
+ style.setBackgroundColor(bundle.getInt(KEY_BACKGROUND_COLOR));
+ style.setEdgeType(bundle.getInt(KEY_EDGE_TYPE));
+ style.setEdgeColor(bundle.getInt(KEY_EDGE_COLOR));
+ style.setWindowType(bundle.getInt(KEY_WINDOW_TYPE));
+ style.setWindowColor(bundle.getInt(KEY_WINDOW_COLOR));
+ style.setWindowCornerRadius(bundle.getInt(KEY_WINDOW_CORNER_RADIUS));
+ style.setFontFamily(bundle.getString(KEY_FONT_FAMILY));
+ style.setFontGenericFamily(bundle.getInt(KEY_FONT_GENERIC_FAMILY));
+ style.setFontStyle(bundle.getInt(KEY_FONT_STYLE));
+ style.setExtras(bundle.getBundle(KEY_EXTRAS));
+ return style;
+ }
+
+ // Compatibility for new platform features introduced in KitKat.
+ private static final class Impl19 {
+ public static void loadSystemSettings(TextTrackStyle style, Context context) {
+ CaptioningManager captioningManager =
+ (CaptioningManager)context.getSystemService(Context.CAPTIONING_SERVICE);
+ style.setFontScale(captioningManager.getFontScale());
+
+ CaptioningManager.CaptionStyle userStyle = captioningManager.getUserStyle();
+ style.setBackgroundColor(userStyle.backgroundColor);
+ style.setForegroundColor(userStyle.foregroundColor);
+
+ switch (userStyle.edgeType) {
+ case CaptioningManager.CaptionStyle.EDGE_TYPE_OUTLINE:
+ style.setEdgeType(EDGE_TYPE_OUTLINE);
+ break;
+
+ case CaptioningManager.CaptionStyle.EDGE_TYPE_DROP_SHADOW:
+ style.setEdgeType(EDGE_TYPE_DROP_SHADOW);
+ break;
+
+ case CaptioningManager.CaptionStyle.EDGE_TYPE_NONE: // Fall through
+ default:
+ style.setEdgeType(EDGE_TYPE_NONE);
+ }
+
+ style.setEdgeColor(userStyle.edgeColor);
+
+ Typeface typeface = userStyle.getTypeface();
+ if (typeface != null) {
+ if (Typeface.MONOSPACE.equals(typeface)) {
+ style.setFontGenericFamily(FONT_FAMILY_MONOSPACED_SANS_SERIF);
+ } else if (Typeface.SANS_SERIF.equals(typeface)) {
+ style.setFontGenericFamily(FONT_FAMILY_SANS_SERIF);
+ } else if (Typeface.SERIF.equals(typeface)) {
+ style.setFontGenericFamily(FONT_FAMILY_SERIF);
+ } else {
+ // Otherwise, assume sans-serif.
+ style.setFontGenericFamily(FONT_FAMILY_SANS_SERIF);
+ }
+
+ boolean bold = typeface.isBold();
+ boolean italic = typeface.isItalic();
+
+ if (bold && italic) {
+ style.setFontStyle(FONT_STYLE_BOLD_ITALIC);
+ } else if (bold) {
+ style.setFontStyle(FONT_STYLE_BOLD);
+ } else if (italic) {
+ style.setFontStyle(FONT_STYLE_ITALIC);
+ } else {
+ style.setFontStyle(FONT_STYLE_NORMAL);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * A class that represents an image that is located on a web server.
+ */
+ public static final class WebImage {
+ private final Uri mUrl;
+ private final int mWidth;
+ private final int mHeight;
+
+ private static final String KEY_URL = "url";
+ private static final String KEY_HEIGHT = "height";
+ private static final String KEY_WIDTH = "width";
+
+ /**
+ * Constructs a new {@link WebImage} with the given URL.
+ *
+ * @param url The URL of the image.
+ * @throws IllegalArgumentException If the URL is null or empty.
+ */
+ public WebImage(@NonNull Uri url) throws IllegalArgumentException {
+ this(url, 0, 0);
+ }
+
+ /**
+ * Constructs a new {@link WebImage} with the given URL and dimensions.
+ *
+ * @param url The URL of the image.
+ * @param width The width of the image, in pixels.
+ * @param height The height of the image, in pixels.
+ * @throws IllegalArgumentException If the URL is null or empty,
+ * or the dimensions are invalid.
+ */
+ public WebImage(@NonNull Uri url, int width, int height) throws IllegalArgumentException {
+ if (url == null) {
+ throw new IllegalArgumentException("url cannot be null");
+ }
+
+ if ((width < 0) || (height < 0)) {
+ throw new IllegalArgumentException("width and height must not be negative");
+ }
+
+ mUrl = url;
+ mWidth = width;
+ mHeight = height;
+ }
+
+ /**
+ * Gets the image URL.
+ */
+ public @NonNull Uri getUrl() {
+ return mUrl;
+ }
+
+ /**
+ * Gets the image width, in pixels.
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Gets the image height, in pixels.
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * Returns a string representation of this object.
+ */
+ @Override
+ public @NonNull String toString() {
+ return String.format("Image %dx%d %s", mWidth, mHeight, mUrl.toString());
+ }
+
+ /**
+ * Creates a bundle representation of this object.
+ */
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_URL, mUrl.toString());
+ bundle.putInt(KEY_WIDTH, mWidth);
+ bundle.putInt(KEY_HEIGHT, mHeight);
+ return bundle;
+ }
+
+ /**
+ * Creates a {@link WebImage} from a bundle.
+ */
+ public static @Nullable WebImage fromBundle(@Nullable Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ return new WebImage(Uri.parse(bundle.getString(KEY_URL)),
+ bundle.getInt(KEY_WIDTH), bundle.getInt(KEY_HEIGHT));
+ }
+ }
+}
diff --git a/media/protocols/src/android/support/media/protocols/MediaRouteProtocol.java b/media/protocols/src/android/support/media/protocols/MediaRouteProtocol.java
new file mode 100644
index 0000000..cb49732
--- /dev/null
+++ b/media/protocols/src/android/support/media/protocols/MediaRouteProtocol.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.media.protocols;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IInterface;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.Closeable;
+import java.lang.ref.WeakReference;
+
+/**
+ * Base class for media route protocols.
+ * <p>
+ * A media route protocol expresses an interface contract between an application and
+ * a media route that it would like to communicate with and control. By using
+ * a protocol to send messages to a media route, an application can
+ * ask the media route to perform functions such as creating a playlist of music
+ * to be played on behalf of the application.
+ * </p><p>
+ * Subclasses should extend this class to offer specialized protocols.
+ * </p><p>
+ * Instances of this class are thread-safe but event will only be received
+ * on the handler that was specified when the callback was registered.
+ * </p>
+ *
+ * <h3>Overview</h3>
+ * <p>
+ * A media route protocol is essentially just a binder-based messaging interface.
+ * Messages sent from the application to the media route service are called "requests"
+ * whereas messages sent from the media route service back to the application are
+ * called "events" or "errors" depending on their purpose.
+ * </p><p>
+ * All communication through a protocol is asynchronous and is dispatched to a
+ * a {@link android.os.Looper} of the application or the media route service's choice
+ * (separate for each end). Arguments are transferred through key/value pairs in
+ * {@link Bundle bundles}.
+ * </p><p>
+ * The overall interface is somewhat simpler than directly using AIDL and Binder which
+ * requires additional care to extend and maintain binary compatibility and to
+ * perform thread synchronization on either end of the communication channel.
+ * Media route protocols also support bidirectional asynchronous communication
+ * requests, events, and errors between the application and the media route service.
+ * </p>
+ *
+ * <h3>Using Protocols</h3>
+ * <p>
+ * To use a protocol, an application must do the following.
+ * </p><ul>
+ * <li>Create a {@link android.media.routing.MediaRouter media router}.
+ * <li>Add a {@link android.media.routing.MediaRouteSelector media route selector}
+ * that specifies the protocol as required or optional.
+ * <li>Show a media route button in the application's action bar to enable the
+ * user to choose a destination to connect to.
+ * <li>Once the connection has been established, obtain the protocol's
+ * binder from the {@link android.media.routing.MediaRouter.ConnectionInfo route connection}
+ * information and {@link MediaRouteProtocol#MediaRouteProtocol(IBinder) create} the protocol
+ * object. There is also a convenience method called
+ * {@link android.media.routing.MediaRouter.ConnectionInfo#getProtocolObject getProtocolObject}
+ * to do this all in one step.
+ * <li>Set a {@link Callback} on the protocol object to receive events.
+ * <li>At this point, the application can begin sending requests to the media route
+ * and receiving events in response via the protocol object.
+ * </ul>
+ *
+ * <h3>Providing Protocols</h3>
+ * <p>
+ * The provide a protocol, a media route service must do the following.
+ * </p><ul>
+ * <li>Upon receiving a
+ * {@link android.media.routing.MediaRouter.DiscoveryRequest discovery request}
+ * from an application that contains a
+ * {@link android.media.routing.MediaRouteSelector media route selector}
+ * which asks to find routes that support known protocols during discovery, the media
+ * route service should indicate that it supports this protocol by adding it to the list
+ * of supported protocols in the
+ * {@link android.media.routing.MediaRouter.RouteInfo route information} for those
+ * routes that support them.
+ * <li>Upon receiving a
+ * {@link android.media.routing.MediaRouter.ConnectionRequest connection request}
+ * from an application that requests to connect to a route for which the application
+ * previously requested support of known protocols, the media route service should
+ * {@link MediaRouteProtocol.Stub#MediaRouteProtocol.Stub(Handler) create} a subclass of the stub
+ * object that implements the protocol then add it to the list of protocol binders
+ * in the {@link android.media.routing.MediaRouter.ConnectionInfo route connection}
+ * object it returns to the application.
+ * <li>Once the route is connected, the media route service should handle incoming
+ * protocol requests from the client and respond accordingly.
+ * <li>Once the route is disconnected, the media route service should cease to
+ * handle incoming protocol requests from the client and should clean up its state
+ * accordingly.
+ * </ul>
+ *
+ * <h3>Creating Custom Protocols</h3>
+ * <p>
+ * Although the framework provides standard media route protocols to encourage
+ * interoperability, it may be useful to create and publish custom protocols to
+ * access extended functionality only supported by certain routes.
+ * </p><p>
+ * To create a custom protocol, create a subclass of the {@link MediaRouteProtocol}
+ * class to declare the new request methods and marshal their arguments. Also create
+ * a subclass of the {@link MediaRouteProtocol.Callback} class to decode any new kinds
+ * of events and subclass the {@link MediaRouteProtocol.Stub} class to decode
+ * incoming requests.
+ * </p><p>
+ * It may help to refer to the source code of the <code>android.support.media.protocol.jar</code>
+ * library for details.
+ * </p><p>
+ * Here is a simple example:
+ * </p><pre>
+ * public abstract class CustomProtocol extends MediaRouteProtocol {
+ * public CustomProtocol(IBinder binder) {
+ * super(binder);
+ * }
+ *
+ * // declare custom request
+ * public void tuneRadio(int station) {
+ * Bundle args = new Bundle();
+ * args.putInt("station", station);
+ * sendRequest("tuneRadio", args);
+ * }
+ *
+ * public static abstract class Callback extends MediaRouteProtocol.Callback {
+ * // declare custom event
+ * public void onStationTuned(int station, boolean hifi) { }
+ *
+ * @Override
+ * public void onEvent(String event, Bundle args) {
+ * switch (event) {
+ * case "radioTuned":
+ * onRadioTuned(args.getInt("station"), args.getBoolean("hifi"));
+ * return;
+ * }
+ * super.onEvent(event, args);
+ * }
+ * }
+ *
+ * public static abstract class Stub extends MediaRouteProtocol.Stub {
+ * // declare custom request stub
+ * public abstract void onTuneRadio(int station);
+ *
+ * @Override
+ * public void onRequest(String request, Bundle args) {
+ * switch (request) {
+ * case "tuneRadio":
+ * onTuneRadio(args.getInt("station"));
+ * return;
+ * }
+ * super.onRequest(request, args);
+ * }
+ * }
+ * }
+ * </pre>
+ */
+public abstract class MediaRouteProtocol {
+ private static final String TAG = "MediaRouteProtocol";
+
+ private static final int REQUEST_MSG_SUBSCRIBE = 1;
+ private static final int REQUEST_MSG_COMMAND = 2;
+
+ private static final int REPLY_MSG_ERROR = 1;
+ private static final int REPLY_MSG_EVENT = 2;
+
+ private final Object mLock = new Object();
+ private final Messenger mRequestMessenger;
+ private Messenger mReplyMessenger;
+ private volatile Callback mCallback;
+ private Looper mCallbackLooper;
+ private Handler mCallbackHandler;
+
+ /**
+ * Error code: Some other unknown error occurred.
+ */
+ public static final String ERROR_UNKNOWN =
+ "android.support.errors.UNKNOWN";
+
+ /**
+ * Error code: The media route has been disconnected.
+ */
+ public static final String ERROR_DISCONNECTED =
+ "android.support.errors.DISCONNECTED";
+
+ /**
+ * Error code: The application issued an unsupported request.
+ */
+ public static final String ERROR_UNSUPPORTED_OPERATION =
+ "android.support.errors.UNSUPPORTED_OPERATION";
+
+ /**
+ * Creates the protocol client object for an application to use to send
+ * messages to a media route.
+ * <p>
+ * This constructor is called automatically if you use
+ * {@link android.media.routing.MediaRouter.ConnectionInfo#getProtocolObject getProtocolObject}
+ * to obtain a protocol object from a media route connection.
+ * </p>
+ *
+ * @param binder The remote binder supplied by the media route service. May be
+ * obtained using {@link android.media.routing.MediaRouter.ConnectionInfo#getProtocolBinder}
+ * on a route connection.
+ */
+ public MediaRouteProtocol(@NonNull IBinder binder) {
+ if (binder == null) {
+ throw new IllegalArgumentException("binder must not be null");
+ }
+
+ mRequestMessenger = new Messenger(binder);
+ }
+
+ /**
+ * Sets the callback interface and handler on which to receive events and errors.
+ *
+ * @param callback The callback interface, or null if none.
+ * @param handler The handler on which to receive events and errors, or null to use
+ * the current looper thread.
+ */
+ public void setCallback(@Nullable Callback callback, @Nullable Handler handler) {
+ synchronized (mLock) {
+ Looper looper = callback != null ?
+ (handler != null ? handler.getLooper() : Looper.myLooper()) : null;
+ if (mCallback != callback || mCallbackLooper != looper) {
+ mCallback = callback;
+ if (mCallback != null) {
+ mCallbackLooper = looper;
+ mCallbackHandler = handler != null ? handler : new Handler();
+ mReplyMessenger = new Messenger(new ReplyHandler(this, looper));
+ } else {
+ mCallbackLooper = null;
+ mCallbackHandler = null;
+ mReplyMessenger = null;
+ }
+
+ Message msg = Message.obtain();
+ msg.what = REQUEST_MSG_SUBSCRIBE;
+ msg.replyTo = mReplyMessenger;
+ sendSafelyLocked(msg);
+ }
+ }
+ }
+
+ /**
+ * Sends an asynchronous request to the media route service.
+ * <p>
+ * If an error occurs, it will be reported to the callback's {@link Callback#onError}
+ * method.
+ * </p>
+ *
+ * @param request The request name.
+ * @param args The request arguments, or null if none.
+ */
+ public void sendRequest(@NonNull String request, @Nullable Bundle args) {
+ if (TextUtils.isEmpty(request)) {
+ throw new IllegalArgumentException("request must not be null or empty");
+ }
+
+ synchronized (mLock) {
+ Message msg = Message.obtain();
+ msg.what = REQUEST_MSG_COMMAND;
+ msg.obj = request;
+ msg.setData(args);
+ sendSafelyLocked(msg);
+ }
+ }
+
+ private void sendSafelyLocked(Message msg) {
+ if (mRequestMessenger != null) {
+ try {
+ mRequestMessenger.send(msg);
+ } catch (RemoteException ex) {
+ postErrorLocked(ERROR_DISCONNECTED, null);
+ }
+ } else {
+ postErrorLocked(ERROR_DISCONNECTED, null);
+ }
+ }
+
+ private void postErrorLocked(final String error, final Bundle args) {
+ final Callback callback = mCallback;
+ if (callback != null) {
+ mCallbackHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (callback != null) {
+ callback.onError(error, args);
+ }
+ }
+ });
+ }
+ }
+
+ private void handleReply(Message msg) {
+ Callback callback = mCallback; // note: racy
+ if (callback != null) {
+ // ignore unrecognized messages in case of future protocol extension
+ if (msg.what == REPLY_MSG_ERROR && msg.obj instanceof String) {
+ mCallback.onError((String)msg.obj, msg.peekData());
+ } else if (msg.what == REPLY_MSG_EVENT && msg.obj instanceof String) {
+ mCallback.onEvent((String)msg.obj, msg.peekData());
+ }
+ }
+ }
+
+ /*
+ * Only use this handler to handle replies coming back from the media route service
+ * because the service can send any message it wants to it.
+ * Validate arguments carefully.
+ */
+ private static final class ReplyHandler extends Handler {
+ // break hard reference cycles through binder
+ private final WeakReference<MediaRouteProtocol> mProtocolRef;
+
+ public ReplyHandler(MediaRouteProtocol protocol, Looper looper) {
+ super(looper);
+ mProtocolRef = new WeakReference<MediaRouteProtocol>(protocol);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ MediaRouteProtocol protocol = mProtocolRef.get();
+ if (protocol != null) {
+ protocol.handleReply(msg);
+ }
+ }
+ }
+
+ /**
+ * Base class for application callbacks from the media route service.
+ * <p>
+ * Subclasses should extend this class to offer events for specialized protocols.
+ * </p>
+ */
+ public static abstract class Callback {
+ /**
+ * Called when an event is received from the media route service.
+ *
+ * @param event The event name.
+ * @param args The event arguments, or null if none.
+ */
+ public void onEvent(@NonNull String event, @Nullable Bundle args) { }
+
+ /**
+ * Called when an error occurs in the media route service.
+ *
+ * @param error The error name.
+ * @param args The error arguments, or null if none.
+ */
+ public void onError(@NonNull String error, @Nullable Bundle args) { }
+ }
+
+ /**
+ * Base class for a media route protocol stub implemented by a media route service.
+ * <p>
+ * Subclasses should extend this class to offer implementation for specialized
+ * protocols.
+ * </p><p>
+ * Instances of this class are thread-safe but requests will only be received
+ * on the handler that was specified in the constructor.
+ * </p>
+ */
+ public static abstract class Stub implements IInterface, Closeable {
+ private final Object mLock = new Object();
+
+ private final Messenger mRequestMessenger;
+ private Messenger mReplyMessenger;
+ private volatile boolean mClosed;
+
+ /**
+ * Creates an implementation of a media route protocol.
+ *
+ * @param handler The handler on which to receive requests, or null to use
+ * the current looper thread.
+ */
+ public Stub(@Nullable Handler handler) {
+ mRequestMessenger = new Messenger(new RequestHandler(this,
+ handler != null ? handler.getLooper() : Looper.myLooper()));
+ }
+
+ /**
+ * Gets the underlying binder object for the stub.
+ */
+ @Override
+ public @NonNull IBinder asBinder() {
+ return mRequestMessenger.getBinder();
+ }
+
+ /**
+ * Closes the stub and prevents it from receiving any additional
+ * messages from the application.
+ */
+ @Override
+ public void close() {
+ synchronized (mLock) {
+ mClosed = true;
+ mReplyMessenger = null;
+ }
+ }
+
+ /**
+ * Called when the application sends a request to the media route service
+ * through this protocol.
+ * <p>
+ * The default implementation throws {@link UnsupportedOperationException}
+ * which is reported back to the application's error callback as
+ * {@link #ERROR_UNSUPPORTED_OPERATION}.
+ * </p>
+ *
+ * @param request The request name.
+ * @param args The request arguments, or null if none.
+ */
+ public void onRequest(@NonNull String request, @Nullable Bundle args)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Called when the application attaches a callback to receive events and errors.
+ */
+ public void onClientAttached() {
+ }
+
+ /**
+ * Called when the application removes its callback and can no longer receive
+ * events and errors.
+ */
+ public void onClientDetached() {
+ }
+
+ /**
+ * Sends an error to the application.
+ *
+ * @param error The error name.
+ * @param args The error arguments, or null if none.
+ * @return True if the message was sent, or false if the client is not
+ * attached, cannot be reached, or if the stub has been closed.
+ */
+ public boolean sendError(@NonNull String error, @Nullable Bundle args) {
+ if (TextUtils.isEmpty(error)) {
+ throw new IllegalArgumentException("error must not be null or empty");
+ }
+
+ synchronized (mLock) {
+ Message msg = Message.obtain();
+ msg.what = REPLY_MSG_ERROR;
+ msg.obj = error;
+ msg.setData(args);
+ return replySafelyLocked(msg);
+ }
+ }
+
+ /**
+ * Sends an event to the application.
+ *
+ * @param event The event name.
+ * @param args The event arguments, or null if none.
+ * @return True if the message was sent, or false if the client is not
+ * attached, cannot be reached, or if the stub has been closed.
+ */
+ public boolean sendEvent(@NonNull String event, @Nullable Bundle args) {
+ if (TextUtils.isEmpty(event)) {
+ throw new IllegalArgumentException("event must not be null or empty");
+ }
+
+ synchronized (mLock) {
+ Message msg = Message.obtain();
+ msg.what = REPLY_MSG_EVENT;
+ msg.obj = event;
+ msg.setData(args);
+ return replySafelyLocked(msg);
+ }
+ }
+
+ private boolean replySafelyLocked(Message msg) {
+ if (mClosed) {
+ Log.w(TAG, "Could not send reply message because the stub has been closed: "
+ + msg + ", in: " + getClass().getName());
+ return false;
+ }
+ if (mReplyMessenger == null) {
+ Log.w(TAG, "Could not send reply message because the client has not yet "
+ + "attached a callback: " + msg + ", in: " + getClass().getName());
+ return false;
+ }
+
+ try {
+ mReplyMessenger.send(msg);
+ return true;
+ } catch (RemoteException ex) {
+ Log.w(TAG, "Could not send reply message because the client died: "
+ + msg + ", in: " + getClass().getName());
+ return false;
+ }
+ }
+
+ private void handleRequest(Message msg) {
+ if (mClosed) { // note: racy
+ Log.w(TAG, "Dropping request because the media route service has "
+ + "closed its end of the protocol: " + msg + ", in: " + getClass());
+ return;
+ }
+
+ // ignore unrecognized messages in case of future protocol extension
+ if (msg.what == REQUEST_MSG_COMMAND && msg.obj instanceof String) {
+ String command = (String)msg.obj;
+ try {
+ onRequest(command, msg.peekData());
+ } catch (UnsupportedOperationException ex) {
+ Log.w(TAG, "Client sent unsupported command request: "
+ + msg + ", in: " + getClass());
+ sendError(ERROR_UNSUPPORTED_OPERATION, null);
+ } catch (RuntimeException ex) {
+ Log.e(TAG, "Stub threw runtime exception while processing command "
+ + "request: " + msg + ", in: " + getClass());
+ sendError(ERROR_UNKNOWN, null);
+ }
+ } else if (msg.what == REQUEST_MSG_SUBSCRIBE) {
+ synchronized (mLock) {
+ if (mClosed) {
+ return; // fix possible race if close() is called on another thread
+ }
+ mReplyMessenger = msg.replyTo;
+ }
+ if (msg.replyTo != null) {
+ onClientAttached();
+ } else {
+ onClientDetached();
+ }
+ }
+ }
+
+ /*
+ * Use this handler only to handle requests coming from the application
+ * because the application can send any message it wants to it.
+ */
+ private static final class RequestHandler extends Handler {
+ // break hard reference cycles through binder
+ private final WeakReference<Stub> mStubRef;
+
+ public RequestHandler(Stub stub, Looper looper) {
+ super(looper);
+ mStubRef = new WeakReference<Stub>(stub);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ Stub stub = mStubRef.get();
+ if (stub != null) {
+ stub.handleRequest(msg);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index ae8c0d1..856c8d9 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -13,5 +13,17 @@
include ':support-mediarouter-v7'
project(':support-mediarouter-v7').projectDir = new File(rootDir, 'v7/mediarouter')
+include ':support-palette-v7'
+project(':support-palette-v7').projectDir = new File(rootDir, 'v7/palette')
+
+include ':support-recyclerview-v7'
+project(':support-recyclerview-v7').projectDir = new File(rootDir, 'v7/recyclerview')
+
+include ':support-cardview-v7'
+project(':support-cardview-v7').projectDir = new File(rootDir, 'v7/cardview')
+
include ':support-v13'
project(':support-v13').projectDir = new File(rootDir, 'v13')
+
+include ':support-leanback-v17'
+project(':support-leanback-v17').projectDir = new File(rootDir, 'v17/leanback')
diff --git a/tests/Android.mk b/tests/Android.mk
index b14ca93..7bd3b00 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -5,7 +5,7 @@
LOCAL_SRC_FILES := $(call all-java-files-under, java)
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4
+LOCAL_STATIC_JAVA_LIBRARIES := mockito-target android-support-v4
LOCAL_JAVA_LIBRARIES := android.test.runner
LOCAL_PACKAGE_NAME := AndroidSupportTests
diff --git a/v14/appcompat/.classpath b/v14/appcompat/.classpath
new file mode 100644
index 0000000..a4763d1
--- /dev/null
+++ b/v14/appcompat/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
diff --git a/v14/appcompat/.gitignore b/v14/appcompat/.gitignore
new file mode 100644
index 0000000..c3c25e0
--- /dev/null
+++ b/v14/appcompat/.gitignore
@@ -0,0 +1,4 @@
+.settings
+bin
+libs
+gen
diff --git a/v14/appcompat/.project b/v14/appcompat/.project
new file mode 100644
index 0000000..224f486
--- /dev/null
+++ b/v14/appcompat/.project
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>android-support-v14-appcompat</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v14/appcompat/AndroidManifest.xml
similarity index 68%
copy from v17/leanback/res/drawable/lb_transition_action_bg.xml
copy to v14/appcompat/AndroidManifest.xml
index 36688bc..c83f8d9 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v14/appcompat/AndroidManifest.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2014 The Android Open Source Project
+<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,8 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.v7.appcompat">
+ <uses-sdk android:minSdkVersion="14"/>
+ <application />
+</manifest>
diff --git a/v14/appcompat/build.gradle b/v14/appcompat/build.gradle
new file mode 100644
index 0000000..d3a554f
--- /dev/null
+++ b/v14/appcompat/build.gradle
@@ -0,0 +1,31 @@
+apply plugin: 'android-library'
+
+archivesBaseName = 'appcompat-v14'
+
+dependencies {
+ compile project(':support-v4')
+}
+
+android {
+ compileSdkVersion 'current'
+ buildToolsVersion '19.0.3'
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ main.java.srcDir 'src'
+ main.res.srcDir 'res'
+ main.assets.srcDir 'assets'
+ main.resources.srcDir 'src'
+
+ // this moves src/instrumentTest to tests so all folders follow:
+ // tests/java, tests/res, tests/assets, ...
+ // This is a *reset* so it replaces the default paths
+ androidTest.setRoot('tests')
+ androidTest.java.srcDir 'tests/src'
+ }
+
+ lintOptions {
+ // TODO: fix errors and reenable.
+ abortOnError false
+ }
+}
diff --git a/v14/appcompat/project.properties b/v14/appcompat/project.properties
new file mode 100644
index 0000000..91d2b02
--- /dev/null
+++ b/v14/appcompat/project.properties
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-19
+android.library=true
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v14/appcompat/res/values-land/dimens.xml
similarity index 75%
copy from v17/leanback/res/drawable/lb_transition_action_bg.xml
copy to v14/appcompat/res/values-land/dimens.xml
index 36688bc..8d8e2e5 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v14/appcompat/res/values-land/dimens.xml
@@ -14,8 +14,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
+<resources>
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+ <!-- Default height of an action bar. -->
+ <dimen name="action_bar_default_height_quantum">48dp</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/v14/appcompat/res/values-v19/themes.xml b/v14/appcompat/res/values-v19/themes.xml
new file mode 100644
index 0000000..5fa3fa1
--- /dev/null
+++ b/v14/appcompat/res/values-v19/themes.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!--
+ TODO: Update comment
+
+ These theme declarations contain any version-independent specification. Items
+ that need to vary based on platform version should be defined in the corresponding
+ "Theme.Base" theme.
+ -->
+
+ <!-- Variant of the appcompat (dark) theme that has no title bar and translucent
+ system decor (if available) -->
+ <style name="Theme.AppCompat.NoActionBar.TranslucentDecor">
+ <item name="android:windowTranslucentStatus">true</item>
+ <item name="android:windowTranslucentNavigation">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ </style>
+
+ <!-- Variant of the appcompat (light) theme that has no title bar and translucent
+ system decor (if available) -->
+ <style name="Theme.AppCompat.Light.NoActionBar.TranslucentDecor">
+ <item name="android:windowTranslucentStatus">true</item>
+ <item name="android:windowTranslucentNavigation">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ </style>
+
+</resources>
\ No newline at end of file
diff --git a/v14/appcompat/res/values-v21/styles_base.xml b/v14/appcompat/res/values-v21/styles_base.xml
new file mode 100644
index 0000000..8bf68eb
--- /dev/null
+++ b/v14/appcompat/res/values-v21/styles_base.xml
@@ -0,0 +1,126 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!--
+ Themes in the "Base.Theme.*" family vary based on the current platform
+ version to provide the correct basis on each device. You probably don't
+ want to use them directly in your apps.
+
+ Themes in the "Theme.AppCompat" family are meant to be extended or used
+ directly by apps.
+ -->
+ <eat-comment />
+
+ <style name="Base.Widget.AppCompat.ActionBar" parent="android:Widget.Quantum.ActionBar" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar" parent="android:Widget.Quantum.Light.ActionBar" />
+
+ <style name="Base.Widget.AppCompat.ActionBar.Solid" parent="android:Widget.Quantum.ActionBar.Solid" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.Solid" parent="android:Widget.Quantum.Light.ActionBar.Solid" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.Solid.Inverse" parent="android:Widget.Quantum.Light.ActionBar.Solid" />
+
+ <style name="Base.Widget.AppCompat.ActionBar.TabBar" parent="android:Widget.Quantum.ActionBar.TabBar" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabBar" parent="android:Widget.Quantum.Light.ActionBar.TabBar" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabBar.Inverse" parent="android:Widget.Quantum.Light.ActionBar.TabBar" />
+
+ <style name="Base.Widget.AppCompat.ActionBar.TabView" parent="android:Widget.Quantum.ActionBar.TabView" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabView" parent="android:Widget.Quantum.Light.ActionBar.TabView" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabView.Inverse" parent="android:Widget.Quantum.Light.ActionBar.TabView" />
+
+ <style name="Base.Widget.AppCompat.ActionBar.TabText" parent="android:Widget.Quantum.ActionBar.TabText" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabText" parent="android:Widget.Quantum.Light.ActionBar.TabText" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabText.Inverse" parent="android:Widget.Quantum.Light.ActionBar.TabText" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionMode.Inverse" parent="android:Widget.Quantum.Light.ActionMode" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionBar.Menu" parent="android:TextAppearance.Quantum.Widget.ActionBar.Menu" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionBar.Title" parent="android:TextAppearance.Quantum.Widget.ActionBar.Title" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionBar.Subtitle" parent="android:TextAppearance.Quantum.Widget.ActionBar.Subtitle" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse" parent="android:TextAppearance.Quantum.Widget.ActionBar.Title.Inverse" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse" parent="android:TextAppearance.Quantum.Widget.ActionBar.Subtitle.Inverse" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionMode.Title" parent="android:TextAppearance.Quantum.Widget.ActionMode.Title" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionMode.Subtitle" parent="android:TextAppearance.Quantum.Widget.ActionMode.Subtitle" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionMode.Title.Inverse" parent="android:TextAppearance.Quantum.Widget.ActionMode.Title.Inverse" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionMode.Subtitle.Inverse" parent="android:TextAppearance.Quantum.Widget.ActionMode.Subtitle.Inverse" />
+
+ <!-- Action Button Styles -->
+
+ <style name="Base.Widget.AppCompat.ActionButton" parent="android:Widget.Quantum.ActionButton" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionButton" parent="android:Widget.Quantum.Light.ActionButton" />
+
+ <style name="Base.Widget.AppCompat.ActionButton.CloseMode" parent="android:Widget.Quantum.ActionButton.CloseMode" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionButton.CloseMode" parent="android:Widget.Quantum.Light.ActionButton.CloseMode" />
+
+ <style name="Base.Widget.AppCompat.ActionButton.Overflow" parent="android:Widget.Quantum.ActionButton.Overflow" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionButton.Overflow" parent="android:Widget.Quantum.Light.ActionButton.Overflow" />
+
+ <!-- Spinner Widgets -->
+
+ <style name="Base.Widget.AppCompat.ListView.DropDown" parent="android:Widget.Quantum.ListView.DropDown" />
+
+ <style name="Base.Widget.AppCompat.Light.ListView.DropDown" parent="android:Widget.Quantum.ListView.DropDown" />
+
+ <style name="Base.Widget.AppCompat.DropDownItem.Spinner" parent="android:Widget.Quantum.DropDownItem.Spinner" />
+
+ <style name="Base.Widget.AppCompat.Light.DropDownItem.Spinner" parent="android:Widget.Quantum.Light.DropDownItem.Spinner" />
+
+ <style name="Base.Widget.AppCompat.Spinner" parent="android:Widget.Quantum.Spinner" />
+
+ <style name="Base.Widget.AppCompat.Light.Spinner" parent="android:Widget.Quantum.Light.Spinner" />
+
+ <style name="Base.Widget.AppCompat.ListView.Menu" parent="android:Widget.ListView.Menu" />
+
+ <!-- Popup Menu -->
+
+ <style name="Base.Widget.AppCompat.ListPopupWindow" parent="android:Widget.Quantum.ListPopupWindow" />
+
+ <style name="Base.Widget.AppCompat.Light.ListPopupWindow" parent="android:Widget.Quantum.Light.ListPopupWindow" />
+
+ <style name="Base.Widget.AppCompat.PopupMenu" parent="android:Widget.Quantum.PopupMenu" />
+
+ <style name="Base.Widget.AppCompat.Light.PopupMenu" parent="android:Widget.Quantum.Light.PopupMenu" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.PopupMenu.Large" parent="android:TextAppearance.Quantum.Widget.PopupMenu.Large" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.PopupMenu.Small" parent="android:TextAppearance.Quantum.Widget.PopupMenu.Small" />
+
+ <style name="Base.TextAppearance.AppCompat.Light.Widget.PopupMenu.Large" parent="android:TextAppearance.Quantum.Widget.PopupMenu.Large" />
+
+ <style name="Base.TextAppearance.AppCompat.Light.Widget.PopupMenu.Small" parent="android:TextAppearance.Quantum.Widget.PopupMenu.Small" />
+
+</resources>
diff --git a/v14/appcompat/res/values-v21/styles_base_text.xml b/v14/appcompat/res/values-v21/styles_base_text.xml
new file mode 100644
index 0000000..4fd3612
--- /dev/null
+++ b/v14/appcompat/res/values-v21/styles_base_text.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <style name="Base.TextAppearance.AppCompat" parent="android:TextAppearance.Quantum" />
+
+ <style name="Base.TextAppearance.AppCompat.Display4" parent="android:TextAppearance.Quantum.Display4" />
+
+ <style name="Base.TextAppearance.AppCompat.Display3" parent="android:TextAppearance.Quantum.Display3" />
+
+ <style name="Base.TextAppearance.AppCompat.Display2" parent="android:TextAppearance.Quantum.Display2" />
+
+ <style name="Base.TextAppearance.AppCompat.Display1" parent="android:TextAppearance.Quantum.Display1" />
+
+ <style name="Base.TextAppearance.AppCompat.Headline" parent="android:TextAppearance.Quantum.Headline" />
+
+ <style name="Base.TextAppearance.AppCompat.Title" parent="android:TextAppearance.Quantum.Title" />
+
+ <style name="Base.TextAppearance.AppCompat.Subhead" parent="android:TextAppearance.Quantum.Subhead" />
+
+ <style name="Base.TextAppearance.AppCompat.Body2" parent="android:TextAppearance.Quantum.Body2" />
+
+ <style name="Base.TextAppearance.AppCompat.Body1" parent="android:TextAppearance.Quantum.Body1" />
+
+ <style name="Base.TextAppearance.AppCompat.Caption" parent="android:TextAppearance.Quantum.Caption" />
+
+ <style name="Base.TextAppearance.AppCompat.Menu" parent="android:TextAppearance.Quantum.Menu" />
+
+ <!-- Now deprecated styles -->
+
+ <style name="Base.TextAppearance.AppCompat.Inverse" parent="android:TextAppearance.Quantum.Inverse" />
+
+ <style name="Base.TextAppearance.AppCompat.Large" parent="android:TextAppearance.Quantum.Large" />
+
+ <style name="Base.TextAppearance.AppCompat.Large.Inverse" parent="android:TextAppearance.Quantum.Large.Inverse" />
+
+ <style name="Base.TextAppearance.AppCompat.Medium" parent="android:TextAppearance.Quantum.Medium" />
+
+ <style name="Base.TextAppearance.AppCompat.Medium.Inverse" parent="android:TextAppearance.Quantum.Medium.Inverse" />
+
+ <style name="Base.TextAppearance.AppCompat.Small" parent="android:TextAppearance.Quantum.Small" />
+
+ <style name="Base.TextAppearance.AppCompat.Small.Inverse" parent="android:TextAppearance.Quantum.Small.Inverse" />
+
+</resources>
\ No newline at end of file
diff --git a/v14/appcompat/res/values-v21/themes_base.xml b/v14/appcompat/res/values-v21/themes_base.xml
new file mode 100644
index 0000000..acf6ed4
--- /dev/null
+++ b/v14/appcompat/res/values-v21/themes_base.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!--
+ Themes in the "Base.Theme.*" family vary based on the current platform
+ version to provide the correct basis on each device. You probably don't
+ want to use them directly in your apps.
+
+ Themes in the "Theme.AppCompat" family are meant to be extended or used
+ directly by apps.
+ -->
+ <eat-comment />
+
+ <style name="Base.Theme.AppCompat" parent="android:Theme.Quantum">
+ <!-- Copy our color palette attribute values to the framework's -->
+ <item name="android:colorPrimary">?attr/colorPrimary</item>
+ <item name="android:colorPrimaryDark">?attr/colorPrimaryDark</item>
+ <item name="android:colorPrimaryLight">?attr/colorPrimaryLight</item>
+ <item name="android:colorAccent">?attr/colorAccent</item>
+ <item name="android:colorControlNormal">?attr/colorControlNormal</item>
+ <item name="android:colorControlActivated">?attr/colorControlActivated</item>
+ <item name="android:colorButtonNormal">?attr/colorButtonNormal</item>
+ </style>
+
+ <style name="Base.Theme.AppCompat.Light" parent="android:Theme.Quantum.Light">
+ <!-- Copy our color palette attribute values to the framework's -->
+ <item name="android:colorPrimary">?attr/colorPrimary</item>
+ <item name="android:colorPrimaryDark">?attr/colorPrimaryDark</item>
+ <item name="android:colorPrimaryLight">?attr/colorPrimaryLight</item>
+ <item name="android:colorAccent">?attr/colorAccent</item>
+ <item name="android:colorControlNormal">?attr/colorControlNormal</item>
+ <item name="android:colorControlActivated">?attr/colorControlActivated</item>
+ <item name="android:colorButtonNormal">?attr/colorButtonNormal</item>
+ </style>
+
+ <style name="Base.Theme.AppCompat.Light.DarkActionBar" parent="android:Theme.Quantum.Light.DarkActionBar">
+ <!-- Copy our color palette attribute values to the framework's -->
+ <item name="android:colorPrimary">?attr/colorPrimary</item>
+ <item name="android:colorPrimaryDark">?attr/colorPrimaryDark</item>
+ <item name="android:colorPrimaryLight">?attr/colorPrimaryLight</item>
+ <item name="android:colorAccent">?attr/colorAccent</item>
+ <item name="android:colorControlNormal">?attr/colorControlNormal</item>
+ <item name="android:colorControlActivated">?attr/colorControlActivated</item>
+ <item name="android:colorButtonNormal">?attr/colorButtonNormal</item>
+ </style>
+
+</resources>
diff --git a/v14/appcompat/res/values/attrs.xml b/v14/appcompat/res/values/attrs.xml
new file mode 100644
index 0000000..03c4b70
--- /dev/null
+++ b/v14/appcompat/res/values/attrs.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+
+ <!-- ============= -->
+ <!-- Color palette -->
+ <!-- ============= -->
+ <eat-comment />
+
+ <!-- The primary branding color for the app. By default, this is the color applied to the
+ action bar background and framework controls (via colorControlActivated). -->
+ <attr name="colorPrimary" format="color" />
+
+ <!-- Dark variant of the primary branding color. By default, this is the color applied to
+ the status bar (via statusBarColor) and navigation bar (via navigationBarColor). -->
+ <attr name="colorPrimaryDark" format="color" />
+
+ <!-- Light variant of the primary branding color. TODO: Not used? -->
+ <attr name="colorPrimaryLight" format="color" />
+
+ <!-- Bright complement to the primary branding color. TODO: Not used? -->
+ <attr name="colorAccent" format="color" />
+
+ <!-- The color applied to framework controls in their normal state. -->
+ <attr name="colorControlNormal" format="color" />
+
+ <!-- The color applied to framework controls in their activated (ex. checked) state. -->
+ <attr name="colorControlActivated" format="color" />
+
+ <!-- The color applied to framework control highlights (ex. ripples, selection). -->
+ <attr name="colorControlHighlight" format="color" />
+
+ <!-- The color applied to framework buttons in their normal state. -->
+ <attr name="colorButtonNormal" format="color" />
+
+</resources>
diff --git a/v14/appcompat/res/values/colors.xml b/v14/appcompat/res/values/colors.xml
new file mode 100644
index 0000000..47c4f2d
--- /dev/null
+++ b/v14/appcompat/res/values/colors.xml
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Colors specific to Quantum themes. -->
+<resources>
+ <color name="background_quantum_dark">#ff303030</color>
+ <color name="background_quantum_light">@android:color/white</color>
+
+ <color name="bright_foreground_quantum_dark">@android:color/white</color>
+ <color name="bright_foreground_quantum_light">@android:color/black</color>
+ <!-- TODO: This is 50% alpha black -->
+ <color name="bright_foreground_disabled_quantum_dark">#80000000</color>
+ <!-- TODO: This is 50% alpha white -->
+ <color name="bright_foreground_disabled_quantum_light">#80ffffff</color>
+ <color name="bright_foreground_inverse_quantum_dark">@color/bright_foreground_quantum_light
+ </color>
+ <color name="bright_foreground_inverse_quantum_light">@color/bright_foreground_quantum_dark
+ </color>
+
+ <color name="dim_foreground_quantum_dark">#ffbebebe</color>
+ <color name="dim_foreground_quantum_light">#ff323232</color>
+ <color name="dim_foreground_disabled_quantum_dark">#80bebebe</color>
+ <color name="dim_foreground_disabled_quantum_light">#80323232</color>
+
+ <color name="hint_foreground_quantum_dark">@color/bright_foreground_disabled_quantum_dark
+ </color>
+ <color name="hint_foreground_quantum_light">@color/bright_foreground_disabled_quantum_light
+ </color>
+ <!-- TODO: This is 40% alpha teal_A200 -->
+ <color name="highlighted_text_quantum_dark">#660097a7</color>
+ <!-- TODO: This is 40% alpha teal_A200 -->
+ <color name="highlighted_text_quantum_light">#660097a7</color>
+
+ <!-- Primary & accent colors -->
+ <eat-comment />
+
+ <color name="quantum_red_100">#fff4c7c3</color>
+ <color name="quantum_red_300">#ffe67c73</color>
+ <color name="quantum_red_500">#ffdb4437</color>
+ <color name="quantum_red_700">#ffc53929</color>
+ <color name="quantum_red_A200">#ffff5252</color>
+ <color name="quantum_red_A400">#ffff1744</color>
+
+ <color name="quantum_blue_100">#ffc6dafc</color>
+ <color name="quantum_blue_300">#ff7baaf7</color>
+ <color name="quantum_blue_500">#ff4285f4</color>
+ <color name="quantum_blue_700">#ff3367d6</color>
+ <color name="quantum_blue_A200">#ff448aff</color>
+ <color name="quantum_blue_A400">#ff2979ff</color>
+
+ <color name="quantum_teal_100">#ffb2ebf2</color>
+ <color name="quantum_teal_300">#ff4dd0e1</color>
+ <color name="quantum_teal_500">#ff00bcd4</color>
+ <color name="quantum_teal_700">#ff0097a7</color>
+ <color name="quantum_teal_A200">#ff18ffff</color>
+ <color name="quantum_teal_A400">#ff00e5ff</color>
+
+ <color name="quantum_green_100">#ffb7e1cd</color>
+ <color name="quantum_green_300">#ff57bb8a</color>
+ <color name="quantum_green_500">#ff0f9d58</color>
+ <color name="quantum_green_700">#ff0b8043</color>
+ <color name="quantum_green_A200">#ff69f0ae</color>
+ <color name="quantum_green_A400">#ff00e676</color>
+
+ <color name="quantum_lime_100">#fff0f4c3</color>
+ <color name="quantum_lime_300">#ffdce775</color>
+ <color name="quantum_lime_500">#ffcddc39</color>
+ <color name="quantum_lime_700">#ffafb42b</color>
+ <color name="quantum_lime_A200">#ffeeff41</color>
+ <color name="quantum_lime_A400">#ffc6ff00</color>
+
+ <color name="quantum_yellow_100">#fffce8b2</color>
+ <color name="quantum_yellow_300">#fff7cb4d</color>
+ <color name="quantum_yellow_500">#fff4b400</color>
+ <color name="quantum_yellow_700">#fff09300</color>
+ <color name="quantum_yellow_A200">#ffffcd40</color>
+ <color name="quantum_yellow_A400">#ffffbc00</color>
+
+ <color name="quantum_orange_100">#ffffe0b2</color>
+ <color name="quantum_orange_300">#ffffb74d</color>
+ <color name="quantum_orange_500">#ffff9800</color>
+ <color name="quantum_orange_700">#fff57c00</color>
+ <color name="quantum_orange_A200">#ffffab40</color>
+ <color name="quantum_orange_A400">#ffff9100</color>
+
+ <color name="quantum_deep_orange_100">#fff4c7c3</color>
+ <color name="quantum_deep_orange_300">#ffe67c73</color>
+ <color name="quantum_deep_orange_500">#ffff5722</color>
+ <color name="quantum_deep_orange_700">#ffc53929</color>
+ <color name="quantum_deep_orange_A200">#ffff5252</color>
+ <color name="quantum_deep_orange_A400">#ffff1744</color>
+
+ <!-- Neutral colors -->
+ <eat-comment />
+
+ <color name="quantum_grey_50">#fffafafa</color>
+ <color name="quantum_grey_100">#fff5f5f5</color>
+ <color name="quantum_grey_300">#ffeeeeee</color>
+ <color name="quantum_grey_500">#ffa3a3a3</color>
+ <color name="quantum_grey_700">#ff717171</color>
+
+ <color name="quantum_blue_grey_50">#ffeceff1</color>
+ <color name="quantum_blue_grey_100">#ffcfd8dc</color>
+ <color name="quantum_blue_grey_300">#ff90a4ae</color>
+ <color name="quantum_blue_grey_500">#ff607d8b</color>
+ <color name="quantum_blue_grey_700">#ff455a64</color>
+
+ <color name="quantum_brown_100">#ffd7ccc8</color>
+ <color name="quantum_brown_300">#ffa1887f</color>
+ <color name="quantum_brown_500">#ff795548</color>
+ <color name="quantum_brown_700">#ff5d4037</color>
+
+ <!-- Text & foreground colors -->
+ <eat-comment />
+
+ <color name="primary_text_default_quantum_light">#de000000</color>
+ <color name="secondary_text_quantum_light">#8a000000</color>
+ <color name="tertiary_text_quantum_light">#4d000000</color>
+
+ <color name="primary_text_default_quantum_dark">#deffffff</color>
+ <color name="secondary_text_quantum_dark">#8affffff</color>
+ <color name="tertiary_text_quantum_dark">#4dffffff</color>
+</resources>
\ No newline at end of file
diff --git a/v14/appcompat/res/values/dimens.xml b/v14/appcompat/res/values/dimens.xml
new file mode 100644
index 0000000..56e09ac
--- /dev/null
+++ b/v14/appcompat/res/values/dimens.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+
+ <!-- Default height of an action bar. -->
+ <dimen name="action_bar_default_height_quantum">56dp</dimen>
+
+ <dimen name="text_size_display_4_quantum">112sp</dimen>
+ <dimen name="text_size_display_3_quantum">56sp</dimen>
+ <dimen name="text_size_display_2_quantum">45sp</dimen>
+ <dimen name="text_size_display_1_quantum">34sp</dimen>
+ <dimen name="text_size_headline_quantum">24sp</dimen>
+ <dimen name="text_size_title_quantum">20sp</dimen>
+ <dimen name="text_size_subhead_quantum">16sp</dimen>
+ <dimen name="text_size_body_2_quantum">14sp</dimen>
+ <dimen name="text_size_body_1_quantum">14sp</dimen>
+ <dimen name="text_size_caption_quantum">12sp</dimen>
+ <dimen name="text_size_menu_quantum">14sp</dimen>
+ <dimen name="text_size_button_quantum">14sp</dimen>
+
+ <!-- Text size for action bar titles -->
+ <dimen name="action_bar_title_text_size_quantum">20sp</dimen>
+ <!-- Text size for action bar subtitles -->
+ <dimen name="action_bar_subtitle_text_size_quantum">16sp</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/v14/appcompat/res/values/styles.xml b/v14/appcompat/res/values/styles.xml
new file mode 100644
index 0000000..955d291
--- /dev/null
+++ b/v14/appcompat/res/values/styles.xml
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+
+ <!--
+ Styles in here can be extended for customisation in your application. Each utilises
+ one of the Base styles. If Quantum themes are available on the current platform version
+ they will be used instead of the compat styles.
+ -->
+ <eat-comment />
+
+ <style name="Widget.AppCompat.ActionBar" parent="Base.Widget.AppCompat.ActionBar" />
+
+ <style name="Widget.AppCompat.Light.ActionBar" parent="Base.Widget.AppCompat.Light.ActionBar" />
+
+ <style name="Widget.AppCompat.ActionBar.Solid" parent="Base.Widget.AppCompat.ActionBar.Solid" />
+
+ <style name="Widget.AppCompat.Light.ActionBar.Solid" parent="Base.Widget.AppCompat.Light.ActionBar.Solid" />
+
+ <style name="Widget.AppCompat.Light.ActionBar.Solid.Inverse" parent="Base.Widget.AppCompat.Light.ActionBar.Solid.Inverse" />
+
+ <style name="TextAppearance.AppCompat.Widget.ActionBar.Title" parent="Base.TextAppearance.AppCompat.Widget.ActionBar.Title" />
+
+ <style name="TextAppearance.AppCompat.Widget.ActionBar.Subtitle" parent="Base.TextAppearance.AppCompat.Widget.ActionBar.Subtitle" />
+
+ <style name="TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse" parent="Base.TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse" />
+
+ <style name="TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse" parent="Base.TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse" />
+
+ <style name="Widget.AppCompat.ProgressBar.Horizontal" parent="Base.Widget.AppCompat.ProgressBar.Horizontal" />
+
+ <style name="Widget.AppCompat.ProgressBar" parent="Base.Widget.AppCompat.ProgressBar" />
+
+ <style name="Widget.AppCompat.ActionButton" parent="Base.Widget.AppCompat.ActionButton" />
+
+ <style name="Widget.AppCompat.Light.ActionButton" parent="Base.Widget.AppCompat.Light.ActionButton" />
+
+ <style name="Widget.AppCompat.ActionButton.CloseMode" parent="Base.Widget.AppCompat.ActionButton.CloseMode" />
+
+ <style name="Widget.AppCompat.Light.ActionButton.CloseMode" parent="Base.Widget.AppCompat.Light.ActionButton.CloseMode" />
+
+ <style name="Widget.AppCompat.ActionButton.Overflow" parent="Base.Widget.AppCompat.ActionButton.Overflow" />
+
+ <style name="Widget.AppCompat.Light.ActionButton.Overflow" parent="Base.Widget.AppCompat.Light.ActionButton.Overflow" />
+
+ <style name="Widget.AppCompat.ActionBar.TabBar" parent="Base.Widget.AppCompat.ActionBar.TabBar" />
+
+ <style name="Widget.AppCompat.Light.ActionBar.TabBar" parent="Base.Widget.AppCompat.Light.ActionBar.TabBar" />
+
+ <style name="Widget.AppCompat.Light.ActionBar.TabBar.Inverse" parent="Base.Widget.AppCompat.Light.ActionBar.TabBar.Inverse" />
+
+ <style name="Widget.AppCompat.ActionBar.TabView" parent="Base.Widget.AppCompat.ActionBar.TabView" />
+
+ <style name="Widget.AppCompat.Light.ActionBar.TabView" parent="Base.Widget.AppCompat.Light.ActionBar.TabView" />
+
+ <style name="Widget.AppCompat.Light.ActionBar.TabView.Inverse" parent="Base.Widget.AppCompat.Light.ActionBar.TabView.Inverse" />
+
+ <style name="Widget.AppCompat.ActionBar.TabText" parent="Base.Widget.AppCompat.ActionBar.TabText" />
+
+ <style name="Widget.AppCompat.Light.ActionBar.TabText" parent="Base.Widget.AppCompat.Light.ActionBar.TabText" />
+
+ <style name="Widget.AppCompat.Light.ActionBar.TabText.Inverse" parent="Base.Widget.AppCompat.Light.ActionBar.TabText.Inverse" />
+
+ <style name="TextAppearance.AppCompat.Widget.ActionBar.Menu" parent="Base.TextAppearance.AppCompat.Widget.ActionBar.Menu" />
+
+ <style name="Widget.AppCompat.ActionMode" parent="Base.Widget.AppCompat.ActionMode" />
+
+ <style name="Widget.AppCompat.Light.ActionMode" parent="Base.Widget.AppCompat.Light.ActionMode" />
+
+ <style name="Widget.AppCompat.Light.ActionMode.Inverse" parent="Base.Widget.AppCompat.Light.ActionMode.Inverse" />
+
+ <style name="TextAppearance.AppCompat.Widget.ActionMode.Title" parent="Base.TextAppearance.AppCompat.Widget.ActionMode.Title" />
+
+ <style name="TextAppearance.AppCompat.Widget.ActionMode.Subtitle" parent="Base.TextAppearance.AppCompat.Widget.ActionMode.Subtitle" />
+
+ <style name="TextAppearance.AppCompat.Widget.ActionMode.Title.Inverse" parent="Base.TextAppearance.AppCompat.Widget.ActionMode.Title.Inverse" />
+
+ <style name="TextAppearance.AppCompat.Widget.ActionMode.Subtitle.Inverse" parent="Base.TextAppearance.AppCompat.Widget.ActionMode.Subtitle.Inverse" />
+
+ <style name="Widget.AppCompat.Spinner.DropDown.ActionBar" parent="Base.Widget.AppCompat.Spinner" />
+
+ <style name="Widget.AppCompat.Light.Spinner.DropDown.ActionBar" parent="Base.Widget.AppCompat.Light.Spinner" />
+
+ <style name="Widget.AppCompat.DropDownItem.Spinner" parent="Base.Widget.AppCompat.DropDownItem.Spinner" />
+
+ <style name="Widget.AppCompat.Light.DropDownItem.Spinner" parent="Base.Widget.AppCompat.Light.DropDownItem.Spinner" />
+
+ <style name="Widget.AppCompat.ListView.DropDown" parent="Base.Widget.AppCompat.ListView.DropDown" />
+
+ <style name="Widget.AppCompat.Light.ListView.DropDown" parent="Base.Widget.AppCompat.Light.ListView.DropDown" />
+
+ <style name="Widget.AppCompat.ListPopupWindow" parent="Base.Widget.AppCompat.ListPopupWindow" />
+
+ <style name="Widget.AppCompat.Light.ListPopupWindow" parent="Base.Widget.AppCompat.Light.ListPopupWindow" />
+
+ <style name="Widget.AppCompat.PopupMenu" parent="Base.Widget.AppCompat.PopupMenu" />
+
+ <style name="Widget.AppCompat.Light.PopupMenu" parent="Base.Widget.AppCompat.Light.PopupMenu" />
+
+ <style name="Widget.AppCompat.ListView.Menu" parent="Base.Widget.AppCompat.ListView.Menu" />
+
+ <style name="TextAppearance.AppCompat.Widget.PopupMenu.Large" parent="Base.TextAppearance.AppCompat.Widget.PopupMenu.Large" />
+
+ <style name="TextAppearance.AppCompat.Widget.PopupMenu.Small" parent="Base.TextAppearance.AppCompat.Widget.PopupMenu.Small" />
+
+ <style name="TextAppearance.AppCompat.Light.Widget.PopupMenu.Large" parent="Base.TextAppearance.AppCompat.Light.Widget.PopupMenu.Large" />
+
+ <style name="TextAppearance.AppCompat.Light.Widget.PopupMenu.Small" parent="Base.TextAppearance.AppCompat.Light.Widget.PopupMenu.Small" />
+
+ <eat-comment />
+ <!-- Text styles -->
+
+ <style name="TextAppearance.AppCompat" parent="Base.TextAppearance.AppCompat" />
+
+ <style name="TextAppearance.AppCompat.Display4" parent="Base.TextAppearance.AppCompat.Display4" />
+
+ <style name="TextAppearance.AppCompat.Display3" parent="Base.TextAppearance.AppCompat.Display3" />
+
+ <style name="TextAppearance.AppCompat.Display2" parent="Base.TextAppearance.AppCompat.Display2" />
+
+ <style name="TextAppearance.AppCompat.Display1" parent="Base.TextAppearance.AppCompat.Display1" />
+
+ <style name="TextAppearance.AppCompat.Headline" parent="Base.TextAppearance.AppCompat.Headline" />
+
+ <style name="TextAppearance.AppCompat.Title" parent="Base.TextAppearance.AppCompat.Title" />
+
+ <style name="TextAppearance.AppCompat.Subhead" parent="Base.TextAppearance.AppCompat.Subhead" />
+
+ <style name="TextAppearance.AppCompat.Body2" parent="Base.TextAppearance.AppCompat.Body2" />
+
+ <style name="TextAppearance.AppCompat.Body1" parent="Base.TextAppearance.AppCompat.Body1" />
+
+ <style name="TextAppearance.AppCompat.Caption" parent="Base.TextAppearance.AppCompat.Caption" />
+
+ <style name="TextAppearance.AppCompat.Menu" parent="Base.TextAppearance.AppCompat.Menu" />
+
+ <style name="TextAppearance.AppCompat.Inverse" parent="Base.TextAppearance.AppCompat.Inverse" />
+
+ <style name="TextAppearance.AppCompat.Large" parent="Base.TextAppearance.AppCompat.Large" />
+
+ <style name="TextAppearance.AppCompat.Large.Inverse" parent="Base.TextAppearance.AppCompat.Large.Inverse" />
+
+ <style name="TextAppearance.AppCompat.Medium" parent="Base.TextAppearance.AppCompat.Medium" />
+
+ <style name="TextAppearance.AppCompat.Medium.Inverse" parent="Base.TextAppearance.AppCompat.Medium.Inverse" />
+
+ <style name="TextAppearance.AppCompat.Small" parent="Base.TextAppearance.AppCompat.Small" />
+
+ <style name="TextAppearance.AppCompat.Small.Inverse" parent="Base.TextAppearance.AppCompat.Small.Inverse" />
+
+</resources>
\ No newline at end of file
diff --git a/v14/appcompat/res/values/styles_base.xml b/v14/appcompat/res/values/styles_base.xml
new file mode 100644
index 0000000..7e42de7
--- /dev/null
+++ b/v14/appcompat/res/values/styles_base.xml
@@ -0,0 +1,213 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!--
+ Like in themes_base.xml, the namespace "Base.*.AppCompat" is used to
+ define base styles for the platform version. The "Widget.AppCompat"
+ variants are for direct use or use as parent styles by the app.
+ -->
+ <eat-comment />
+
+ <style name="Base.Widget.AppCompat.ActionBar" parent="android:Widget.Holo.ActionBar">
+ <item name="android:background">@null</item>
+ <item name="android:backgroundStacked">@null</item>
+ <item name="android:backgroundSplit">@null</item>
+
+ <item name="android:divider">?android:attr/dividerVertical</item>
+ <item name="android:progressBarPadding">32dip</item>
+ <item name="android:itemPadding">8dip</item>
+
+ <item name="android:titleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Title</item>
+ <item name="android:subtitleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle</item>
+ </style>
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar" parent="android:Widget.Holo.Light.ActionBar">
+ <item name="android:background">@null</item>
+ <item name="android:backgroundStacked">@null</item>
+ <item name="android:backgroundSplit">@null</item>
+
+ <item name="android:divider">?android:attr/dividerVertical</item>
+ <item name="android:progressBarPadding">32dip</item>
+ <item name="android:itemPadding">8dip</item>
+
+ <item name="android:titleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Title</item>
+ <item name="android:subtitleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle</item>
+ </style>
+
+ <style name="Base.Widget.AppCompat.ActionBar.Solid" parent="android:Widget.Holo.ActionBar.Solid">
+ <item name="android:background">?attr/colorPrimary</item>
+ <item name="android:backgroundStacked">?attr/colorPrimary</item>
+ <item name="android:backgroundSplit">?attr/colorPrimary</item>
+
+ <item name="android:divider">?android:attr/dividerVertical</item>
+ <item name="android:progressBarPadding">32dip</item>
+ <item name="android:itemPadding">8dip</item>
+
+ <item name="android:titleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Title</item>
+ <item name="android:subtitleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle</item>
+ </style>
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.Solid" parent="android:Widget.Holo.Light.ActionBar.Solid">
+ <item name="android:background">?attr/colorPrimary</item>
+ <item name="android:backgroundStacked">?attr/colorPrimary</item>
+ <item name="android:backgroundSplit">?attr/colorPrimary</item>
+
+ <item name="android:divider">?android:attr/dividerVertical</item>
+ <item name="android:progressBarPadding">32dip</item>
+ <item name="android:itemPadding">8dip</item>
+
+ <item name="android:titleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Title</item>
+ <item name="android:subtitleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle</item>
+ </style>
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.Solid.Inverse" parent="android:Widget.Holo.Light.ActionBar.Solid.Inverse">
+ <item name="android:background">?attr/colorPrimary</item>
+ <item name="android:backgroundStacked">?attr/colorPrimary</item>
+ <item name="android:backgroundSplit">?attr/colorPrimary</item>
+
+ <item name="android:divider">?android:attr/dividerVertical</item>
+ <item name="android:progressBarPadding">32dip</item>
+ <item name="android:itemPadding">8dip</item>
+
+ <item name="android:titleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse</item>
+ <item name="android:subtitleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse</item>
+ </style>
+
+ <style name="Base.Widget.AppCompat.ActionMode" parent="android:Widget.Holo.ActionMode">
+ <item name="android:titleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionMode.Title</item>
+ <item name="android:subtitleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionMode.Subtitle</item>
+ </style>
+
+ <style name="Base.Widget.AppCompat.Light.ActionMode" parent="android:Widget.Holo.Light.ActionMode">
+ <item name="android:titleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionMode.Title</item>
+ <item name="android:subtitleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionMode.Subtitle</item>
+ </style>
+
+ <style name="Base.Widget.AppCompat.Light.ActionMode.Inverse" parent="android:Widget.Holo.Light.ActionMode.Inverse">
+ <item name="android:titleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionMode.Title.Inverse</item>
+ <item name="android:subtitleTextStyle">@style/TextAppearance.AppCompat.Widget.ActionMode.Subtitle.Inverse</item>
+ </style>
+
+ <style name="Base.Widget.AppCompat.ActionBar.TabBar" parent="android:Widget.Holo.ActionBar.TabBar" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabBar" parent="android:Widget.Holo.Light.ActionBar.TabBar" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabBar.Inverse" parent="android:Widget.Holo.Light.ActionBar.TabBar.Inverse" />
+
+ <style name="Base.Widget.AppCompat.ActionBar.TabView" parent="android:Widget.Holo.ActionBar.TabView" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabView" parent="android:Widget.Holo.Light.ActionBar.TabView" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabView.Inverse" parent="android:Widget.Holo.Light.ActionBar.TabView.Inverse" />
+
+ <style name="Base.Widget.AppCompat.ActionBar.TabText" parent="android:Widget.Holo.ActionBar.TabText" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabText" parent="android:Widget.Holo.Light.ActionBar.TabText" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionBar.TabText.Inverse" parent="android:Widget.Holo.Light.ActionBar.TabText.Inverse" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionBar.Menu" parent="android:TextAppearance.Holo.Widget.ActionBar.Menu" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionBar.Title" parent="Base.TextAppearance.AppCompat.Medium">
+ <item name="android:textSize">@dimen/action_bar_title_text_size_quantum</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionBar.Subtitle" parent="Base.TextAppearance.AppCompat.Small">
+ <item name="android:textSize">@dimen/action_bar_title_text_size_quantum</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionBar.Title.Inverse" parent="Base.TextAppearance.AppCompat.Medium.Inverse">
+ <item name="android:textSize">@dimen/action_bar_subtitle_text_size_quantum</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionBar.Subtitle.Inverse" parent="Base.TextAppearance.AppCompat.Small.Inverse">
+ <item name="android:textSize">@dimen/action_bar_subtitle_text_size_quantum</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionMode.Title" parent="Base.TextAppearance.AppCompat.Medium">
+ <item name="android:textSize">@dimen/action_bar_title_text_size_quantum</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionMode.Subtitle" parent="Base.TextAppearance.AppCompat.Small">
+ <item name="android:textSize">@dimen/action_bar_title_text_size_quantum</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionMode.Title.Inverse" parent="Base.TextAppearance.AppCompat.Medium.Inverse">
+ <item name="android:textSize">@dimen/action_bar_subtitle_text_size_quantum</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Widget.ActionMode.Subtitle.Inverse" parent="Base.TextAppearance.AppCompat.Small.Inverse">
+ <item name="android:textSize">@dimen/action_bar_subtitle_text_size_quantum</item>
+ </style>
+
+ <!-- Progress Bar -->
+
+ <style name="Base.Widget.AppCompat.ProgressBar.Horizontal" parent="android:Widget.Holo.ProgressBar.Horizontal" />
+
+ <style name="Base.Widget.AppCompat.ProgressBar" parent="android:Widget.Holo.ProgressBar" />
+
+ <!-- Action Button Styles -->
+
+ <style name="Base.Widget.AppCompat.ActionButton" parent="android:Widget.Holo.ActionButton" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionButton" parent="android:Widget.Holo.Light.ActionButton" />
+
+ <style name="Base.Widget.AppCompat.ActionButton.CloseMode" parent="android:Widget.Holo.ActionButton.CloseMode" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionButton.CloseMode" parent="android:Widget.Holo.Light.ActionButton.CloseMode" />
+
+ <style name="Base.Widget.AppCompat.ActionButton.Overflow" parent="android:Widget.Holo.ActionButton.Overflow" />
+
+ <style name="Base.Widget.AppCompat.Light.ActionButton.Overflow" parent="android:Widget.Holo.Light.ActionButton.Overflow" />
+
+ <!-- Spinner Widgets -->
+
+ <style name="Base.Widget.AppCompat.ListView.DropDown" parent="android:Widget.Holo.ListView.DropDown" />
+
+ <style name="Base.Widget.AppCompat.Light.ListView.DropDown" parent="android:Widget.Holo.ListView.DropDown" />
+
+ <style name="Base.Widget.AppCompat.DropDownItem.Spinner" parent="android:Widget.Holo.DropDownItem.Spinner" />
+
+ <style name="Base.Widget.AppCompat.Light.DropDownItem.Spinner" parent="android:Widget.Holo.Light.DropDownItem.Spinner" />
+
+ <style name="Base.Widget.AppCompat.Spinner" parent="android:Widget.Holo.Spinner" />
+
+ <style name="Base.Widget.AppCompat.Light.Spinner" parent="android:Widget.Holo.Light.Spinner" />
+
+ <style name="Base.Widget.AppCompat.ListView.Menu" parent="android:Widget.ListView.Menu" />
+
+ <!-- Popup Menu -->
+
+ <style name="Base.Widget.AppCompat.ListPopupWindow" parent="android:Widget.Holo.ListPopupWindow" />
+
+ <style name="Base.Widget.AppCompat.Light.ListPopupWindow" parent="android:Widget.Holo.Light.ListPopupWindow" />
+
+ <style name="Base.Widget.AppCompat.PopupMenu" parent="android:Widget.Holo.PopupMenu" />
+
+ <style name="Base.Widget.AppCompat.Light.PopupMenu" parent="android:Widget.Holo.Light.PopupMenu" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.PopupMenu.Large" parent="android:TextAppearance.Holo.Widget.PopupMenu.Large" />
+
+ <style name="Base.TextAppearance.AppCompat.Widget.PopupMenu.Small" parent="android:TextAppearance.Holo.Widget.PopupMenu.Small" />
+
+ <style name="Base.TextAppearance.AppCompat.Light.Widget.PopupMenu.Large" parent="android:TextAppearance.Holo.Widget.PopupMenu.Large" />
+
+ <style name="Base.TextAppearance.AppCompat.Light.Widget.PopupMenu.Small" parent="android:TextAppearance.Holo.Widget.PopupMenu.Small" />
+
+</resources>
\ No newline at end of file
diff --git a/v14/appcompat/res/values/styles_base_text.xml b/v14/appcompat/res/values/styles_base_text.xml
new file mode 100644
index 0000000..0a156bc
--- /dev/null
+++ b/v14/appcompat/res/values/styles_base_text.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <style name="Base.TextAppearance.AppCompat" parent="android:TextAppearance.Holo">
+ <item name="android:textColor">?android:textColorPrimary</item>
+ <item name="android:textColorHint">?android:textColorHint</item>
+ <item name="android:textColorHighlight">?android:textColorHighlight</item>
+ <item name="android:textColorLink">?android:textColorLink</item>
+ <item name="android:textSize">@dimen/text_size_body_1_quantum</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Display4">
+ <item name="android:textSize">@dimen/text_size_display_4_quantum</item>
+ <item name="android:textColor">?android:textColorSecondary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Display3">
+ <item name="android:textSize">@dimen/text_size_display_3_quantum</item>
+ <item name="android:textColor">?android:textColorSecondary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Display2">
+ <item name="android:textSize">@dimen/text_size_display_2_quantum</item>
+ <item name="android:textColor">?android:textColorSecondary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Display1">
+ <item name="android:textSize">@dimen/text_size_display_1_quantum</item>
+ <item name="android:textColor">?android:textColorSecondary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Headline">
+ <item name="android:textSize">@dimen/text_size_headline_quantum</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Title">
+ <item name="android:textSize">@dimen/text_size_title_quantum</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Subhead">
+ <item name="android:textSize">@dimen/text_size_subhead_quantum</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Body2">
+ <item name="android:textSize">@dimen/text_size_body_2_quantum</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Body1">
+ <item name="android:textSize">@dimen/text_size_body_1_quantum</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Caption">
+ <item name="android:textSize">@dimen/text_size_caption_quantum</item>
+ <item name="android:textColor">?android:textColorSecondary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Menu">
+ <item name="android:textSize">@dimen/text_size_menu_quantum</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Button">
+ <item name="android:textSize">@dimen/text_size_button_quantum</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textColor">?android:textColorPrimary</item>
+ </style>
+
+ <!-- Deprecated text styles -->
+
+ <style name="Base.TextAppearance.AppCompat.Inverse">
+ <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
+ <item name="android:textColorHint">?android:attr/textColorHintInverse</item>
+ <item name="android:textColorHighlight">?android:attr/textColorHighlightInverse</item>
+ <item name="android:textColorLink">?android:attr/textColorLinkInverse</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Large" parent="TextAppearance.AppCompat.Headline" />
+
+ <style name="Base.TextAppearance.AppCompat.Large.Inverse">
+ <item name="android:textColor">?android:attr/textColorPrimaryInverse</item>
+ <item name="android:textColorHint">?android:attr/textColorHintInverse</item>
+ <item name="android:textColorHighlight">?android:attr/textColorHighlightInverse</item>
+ <item name="android:textColorLink">?android:attr/textColorLinkInverse</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Medium" parent="TextAppearance.AppCompat.Body1" />
+
+ <style name="Base.TextAppearance.AppCompat.Medium.Inverse">
+ <item name="android:textColor">?android:attr/textColorSecondaryInverse</item>
+ <item name="android:textColorHint">?android:attr/textColorHintInverse</item>
+ <item name="android:textColorHighlight">?android:attr/textColorHighlightInverse</item>
+ <item name="android:textColorLink">?android:attr/textColorLinkInverse</item>
+ </style>
+
+ <style name="Base.TextAppearance.AppCompat.Small" parent="TextAppearance.AppCompat.Caption" />
+
+ <style name="Base.TextAppearance.AppCompat.Small.Inverse">
+ <item name="android:textColor">?android:attr/textColorTertiaryInverse</item>
+ <item name="android:textColorHint">?android:attr/textColorHintInverse</item>
+ <item name="android:textColorHighlight">?android:attr/textColorHighlightInverse</item>
+ <item name="android:textColorLink">?android:attr/textColorLinkInverse</item>
+ </style>
+
+</resources>
\ No newline at end of file
diff --git a/v14/appcompat/res/values/themes.xml b/v14/appcompat/res/values/themes.xml
new file mode 100644
index 0000000..72df717
--- /dev/null
+++ b/v14/appcompat/res/values/themes.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <!--
+ These theme declarations contain any version-independent specification. Items
+ that need to vary based on platform version should be defined in the corresponding
+ "Theme.Base" theme.
+ -->
+
+ <!-- Platform-independent theme providing an action bar in a dark-themed activity. -->
+ <style name="Theme.AppCompat" parent="Base.Theme.AppCompat" />
+
+ <!-- Variant of the appcompat (dark) theme with no action bar. -->
+ <style name="Theme.AppCompat.NoActionBar">
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+
+ <!-- Variant of the appcompat (dark) theme that has no title bar and fills
+ the entire screen. This theme sets {@link android.R.attr#windowFullscreen} to true. -->
+ <style name="Theme.AppCompat.NoActionBar.Fullscreen">
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ </style>
+
+ <!-- Variant of the appcompat (dark) theme that has no title bar and translucent
+ system decor (if available) -->
+ <style name="Theme.AppCompat.NoActionBar.TranslucentDecor" />
+
+ <!-- Platform-independent theme providing an action bar in a light-themed activity. -->
+ <style name="Theme.AppCompat.Light" parent="Base.Theme.AppCompat.Light" />
+
+ <!-- Platform-independent theme providing an action bar in a dark-themed activity. -->
+ <style name="Theme.AppCompat.Light.DarkActionBar" parent="Base.Theme.AppCompat.Light.DarkActionBar" />
+
+ <!-- Variant of the appcompat (light) theme with no action bar. -->
+ <style name="Theme.AppCompat.Light.NoActionBar">
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+
+ <!-- Variant of the appcompat (light) theme that has no title bar and fills
+ the entire screen. This theme sets {@link android.R.attr#windowFullscreen} to true. -->
+ <style name="Theme.AppCompat.Light.NoActionBar.Fullscreen">
+ <item name="android:windowFullscreen">true</item>
+ <item name="android:windowContentOverlay">@null</item>
+ </style>
+
+ <!-- Variant of the quantum (light) theme that has no title bar and translucent
+ system decor (if available) -->
+ <style name="Theme.AppCompat.Light.NoActionBar.TranslucentDecor" />
+
+</resources>
\ No newline at end of file
diff --git a/v14/appcompat/res/values/themes_base.xml b/v14/appcompat/res/values/themes_base.xml
new file mode 100644
index 0000000..f0f87c9
--- /dev/null
+++ b/v14/appcompat/res/values/themes_base.xml
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!--
+ Themes in the "Base.Theme.*" family vary based on the current platform
+ version to provide the correct basis on each device. You probably don't
+ want to use them directly in your apps.
+
+ Themes in the "Theme.AppCompat" family are meant to be extended or used
+ directly by apps.
+ -->
+ <eat-comment />
+
+ <!-- Base platform-dependent theme providing an action bar in a dark-themed activity. -->
+ <style name="Base.Theme.AppCompat" parent="android:Theme.Holo">
+ <!-- Color palette -->
+ <item name="colorPrimaryDark">@color/quantum_blue_700</item>
+ <item name="colorPrimary">@color/quantum_blue_500</item>
+ <item name="colorPrimaryLight">@color/quantum_blue_100</item>
+ <item name="colorAccent">@color/quantum_teal_A200</item>
+
+ <item name="colorControlNormal">?android:attr/textColorSecondary</item>
+ <item name="colorControlActivated">?attr/colorPrimary</item>
+ <item name="colorControlHighlight">#30ffffff</item>
+
+ <item name="android:actionBarSize">@dimen/action_bar_default_height_quantum</item>
+ <item name="android:actionBarStyle">@style/Widget.AppCompat.ActionBar.Solid</item>
+ <item name="android:actionModeBackground">?attr/colorPrimaryDark</item>
+ <item name="android:actionModeSplitBackground">?attr/colorPrimaryDark</item>
+ <item name="android:actionModeStyle">@style/Widget.AppCompat.ActionMode</item>
+
+ <!-- Text styles -->
+ <item name="android:textAppearance">@style/TextAppearance.AppCompat</item>
+ <item name="android:textAppearanceInverse">@style/TextAppearance.AppCompat.Inverse</item>
+
+ <item name="android:textAppearanceLarge">@style/TextAppearance.AppCompat.Large</item>
+ <item name="android:textAppearanceLargeInverse">
+ @style/TextAppearance.AppCompat.Large.Inverse
+ </item>
+ <item name="android:textAppearanceMedium">@style/TextAppearance.AppCompat.Medium</item>
+ <item name="android:textAppearanceMediumInverse">
+ @style/TextAppearance.AppCompat.Medium.Inverse
+ </item>
+ <item name="android:textAppearanceSmall">@style/TextAppearance.AppCompat.Small</item>
+ <item name="android:textAppearanceSmallInverse">
+ @style/TextAppearance.AppCompat.Small.Inverse
+ </item>
+
+ <item name="android:textAppearanceListItem">@style/TextAppearance.AppCompat.Subhead</item>
+ <item name="android:textAppearanceListItemSmall">@style/TextAppearance.AppCompat.Subhead
+ </item>
+ <!-- TODO <item name="android:textAppearanceListItemSecondary">@style/TextAppearance.AppCompat.Body1</item> -->
+ <item name="android:listPreferredItemPaddingLeft">16dip</item>
+ <item name="android:listPreferredItemPaddingRight">16dip</item>
+ </style>
+
+ <!-- Base platform-dependent theme providing an action bar in a light-themed activity. -->
+ <style name="Base.Theme.AppCompat.Light" parent="android:Theme.Holo.Light">
+ <!-- Color palette -->
+ <item name="colorPrimaryDark">@color/quantum_blue_700</item>
+ <item name="colorPrimary">@color/quantum_blue_500</item>
+ <item name="colorPrimaryLight">@color/quantum_blue_100</item>
+ <item name="colorAccent">@color/quantum_teal_A200</item>
+
+ <item name="colorControlNormal">?android:attr/textColorSecondary</item>
+ <item name="colorControlActivated">?attr/colorPrimary</item>
+ <item name="colorControlHighlight">#30000000</item>
+
+ <item name="android:actionBarSize">@dimen/action_bar_default_height_quantum</item>
+ <item name="android:actionBarStyle">@style/Widget.AppCompat.Light.ActionBar.Solid</item>
+ <item name="android:actionModeBackground">?attr/colorPrimaryDark</item>
+ <item name="android:actionModeSplitBackground">?attr/colorPrimaryDark</item>
+ <item name="android:actionModeStyle">@style/Widget.AppCompat.Light.ActionMode</item>
+
+ <!-- Text styles -->
+ <item name="android:textAppearance">@style/TextAppearance.AppCompat</item>
+ <item name="android:textAppearanceInverse">@style/TextAppearance.AppCompat.Inverse</item>
+
+ <item name="android:textAppearanceLarge">@style/TextAppearance.AppCompat.Large</item>
+ <item name="android:textAppearanceLargeInverse">
+ @style/TextAppearance.AppCompat.Large.Inverse
+ </item>
+ <item name="android:textAppearanceMedium">@style/TextAppearance.AppCompat.Medium</item>
+ <item name="android:textAppearanceMediumInverse">
+ @style/TextAppearance.AppCompat.Medium.Inverse
+ </item>
+ <item name="android:textAppearanceSmall">@style/TextAppearance.AppCompat.Small</item>
+ <item name="android:textAppearanceSmallInverse">
+ @style/TextAppearance.AppCompat.Small.Inverse
+ </item>
+
+ <item name="android:textAppearanceListItem">@style/TextAppearance.AppCompat.Subhead</item>
+ <item name="android:textAppearanceListItemSmall">@style/TextAppearance.AppCompat.Subhead
+ </item>
+ <!-- TODO <item name="android:textAppearanceListItemSecondary">@style/TextAppearance.AppCompat.Body1</item> -->
+ <item name="android:listPreferredItemPaddingLeft">16dip</item>
+ <item name="android:listPreferredItemPaddingRight">16dip</item>
+ </style>
+
+ <!-- Base platform-dependent theme providing a dark action bar in a light-themed activity. -->
+ <style name="Base.Theme.AppCompat.Light.DarkActionBar" parent="android:Theme.Holo.Light.DarkActionBar">
+ <!-- Color palette -->
+ <item name="colorPrimaryDark">@color/quantum_blue_700</item>
+ <item name="colorPrimary">@color/quantum_blue_500</item>
+ <item name="colorPrimaryLight">@color/quantum_blue_100</item>
+ <item name="colorAccent">@color/quantum_teal_A200</item>
+
+ <item name="colorControlNormal">?android:attr/textColorSecondary</item>
+ <item name="colorControlActivated">?attr/colorPrimary</item>
+ <item name="colorControlHighlight">#30000000</item>
+
+ <item name="android:actionBarSize">@dimen/action_bar_default_height_quantum</item>
+ <item name="android:actionBarStyle">@style/Widget.AppCompat.Light.ActionBar.Solid.Inverse
+ </item>
+ <item name="android:actionModeBackground">?attr/colorPrimaryDark</item>
+ <item name="android:actionModeSplitBackground">?attr/colorPrimaryDark</item>
+ <item name="android:actionModeStyle">@style/Widget.AppCompat.Light.ActionMode.Inverse</item>
+
+ <!-- Text styles -->
+ <item name="android:textAppearance">@style/TextAppearance.AppCompat</item>
+ <item name="android:textAppearanceInverse">@style/TextAppearance.AppCompat.Inverse</item>
+
+ <item name="android:textAppearanceLarge">@style/TextAppearance.AppCompat.Large</item>
+ <item name="android:textAppearanceLargeInverse">
+ @style/TextAppearance.AppCompat.Large.Inverse
+ </item>
+ <item name="android:textAppearanceMedium">@style/TextAppearance.AppCompat.Medium</item>
+ <item name="android:textAppearanceMediumInverse">
+ @style/TextAppearance.AppCompat.Medium.Inverse
+ </item>
+ <item name="android:textAppearanceSmall">@style/TextAppearance.AppCompat.Small</item>
+ <item name="android:textAppearanceSmallInverse">
+ @style/TextAppearance.AppCompat.Small.Inverse
+ </item>
+
+ <item name="android:textAppearanceListItem">@style/TextAppearance.AppCompat.Subhead</item>
+ <item name="android:textAppearanceListItemSmall">@style/TextAppearance.AppCompat.Subhead
+ </item>
+ <!-- TODO <item name="android:textAppearanceListItemSecondary">@style/TextAppearance.AppCompat.Body1</item> -->
+ <item name="android:listPreferredItemPaddingLeft">16dip</item>
+ <item name="android:listPreferredItemPaddingRight">16dip</item>
+
+ </style>
+
+</resources>
\ No newline at end of file
diff --git a/v17/leanback/.classpath b/v17/leanback/.classpath
index a4763d1..7bc01d9 100644
--- a/v17/leanback/.classpath
+++ b/v17/leanback/.classpath
@@ -3,6 +3,7 @@
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
- <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>
diff --git a/v17/leanback/Android.mk b/v17/leanback/Android.mk
index 01a8ae3..f64ca8e 100644
--- a/v17/leanback/Android.mk
+++ b/v17/leanback/Android.mk
@@ -30,12 +30,32 @@
# -----------------------------------------------------------------------
+# Base sub-library contains classes both needed by api-level specific libraries
+# (e.g. KitKat) and final static library.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v17-leanback-common
+LOCAL_SDK_VERSION := 17
+LOCAL_SRC_FILES := $(call all-java-files-under, common)
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# -----------------------------------------------------------------------
+
+# A helper sub-library that makes direct use of API 21.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v17-leanback-api21
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, api21)
+LOCAL_JAVA_LIBRARIES := android-support-v17-leanback-res android-support-v17-leanback-common
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# -----------------------------------------------------------------------
+
# A helper sub-library that makes direct use of KitKat APIs.
include $(CLEAR_VARS)
LOCAL_MODULE := android-support-v17-leanback-kitkat
LOCAL_SDK_VERSION := 19
LOCAL_SRC_FILES := $(call all-java-files-under, kitkat)
-LOCAL_JAVA_LIBRARIES := android-support-v17-leanback-res
+LOCAL_JAVA_LIBRARIES := android-support-v17-leanback-res android-support-v17-leanback-common
include $(BUILD_STATIC_JAVA_LIBRARY)
# -----------------------------------------------------------------------
@@ -43,9 +63,9 @@
# A helper sub-library that makes direct use of JBMR2 APIs.
include $(CLEAR_VARS)
LOCAL_MODULE := android-support-v17-leanback-jbmr2
-LOCAL_SDK_VERSION := 19
+LOCAL_SDK_VERSION := 18
LOCAL_SRC_FILES := $(call all-java-files-under, jbmr2)
-LOCAL_JAVA_LIBRARIES := android-support-v17-leanback-res
+LOCAL_JAVA_LIBRARIES := android-support-v17-leanback-res android-support-v17-leanback-common
include $(BUILD_STATIC_JAVA_LIBRARY)
# -----------------------------------------------------------------------
@@ -58,7 +78,8 @@
LOCAL_MODULE := android-support-v17-leanback
LOCAL_SDK_VERSION := 17
LOCAL_SRC_FILES := $(call all-java-files-under, src)
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-v17-leanback-kitkat android-support-v17-leanback-jbmr2
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v17-leanback-kitkat android-support-v17-leanback-jbmr2 \
+ android-support-v17-leanback-api21 android-support-v17-leanback-common
LOCAL_JAVA_LIBRARIES := \
android-support-v4 \
android-support-v7-recyclerview \
@@ -119,6 +140,7 @@
LOCAL_SRC_FILES := $(leanback.docs.src_files)
LOCAL_JAVA_LIBRARIES := $(leanback.docs.java_libraries)
+LOCAL_SDK_VERSION := current
LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR := build/tools/droiddoc/templates-sdk
LOCAL_UNINSTALLABLE_MODULE := true
diff --git a/v17/leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java b/v17/leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java
new file mode 100644
index 0000000..c1b5850
--- /dev/null
+++ b/v17/leanback/api21/android/support/v17/leanback/widget/ShadowHelperApi21.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+import android.support.v17.leanback.R;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.view.ViewGroup;
+import android.view.View;
+
+class ShadowHelperApi21 {
+
+ static int sNormalZ = Integer.MIN_VALUE;
+ static int sFocusedZ;
+
+ private static void initializeResources(Resources res) {
+ if (sNormalZ == Integer.MIN_VALUE) {
+ sNormalZ = (int) res.getDimension(R.dimen.lb_material_shadow_normal_z);
+ sFocusedZ = (int) res.getDimension(R.dimen.lb_material_shadow_focused_z);
+ }
+ }
+
+ /* add shadows and return a implementation detail object */
+ public static Object addShadow(ViewGroup shadowContainer) {
+ initializeResources(shadowContainer.getResources());
+ shadowContainer.setBackground(new ColorDrawable(Color.TRANSPARENT));
+ shadowContainer.setZ(sNormalZ);
+ return shadowContainer;
+ }
+
+ /* set shadow focus level 0 for unfocused 1 for fully focused */
+ public static void setShadowFocusLevel(Object impl, float level) {
+ ViewGroup shadowContainer = (ViewGroup) impl;
+ shadowContainer.setZ(sNormalZ + level * (sFocusedZ - sNormalZ));
+ }
+
+ public static void setZ(View view, float level) {
+ initializeResources(view.getResources());
+ view.setZ(sNormalZ + level * (sFocusedZ - sNormalZ));
+ }
+}
diff --git a/v17/leanback/build.gradle b/v17/leanback/build.gradle
index 89d5f3b..f1e04a1 100644
--- a/v17/leanback/build.gradle
+++ b/v17/leanback/build.gradle
@@ -1,6 +1,6 @@
apply plugin: 'android-library'
-archivesBaseName = 'support-leanback-v17'
+archivesBaseName = 'leanback-v17'
dependencies {
compile project(':support-v4')
@@ -8,21 +8,96 @@
}
android {
+ // WARNING: should be 17
compileSdkVersion 'current'
+
buildToolsVersion "19.0.1"
+ defaultConfig {
+ minSdkVersion 17
+ // TODO: get target from branch
+ //targetSdkVersion 19
+ }
+
sourceSets {
main.manifest.srcFile 'AndroidManifest.xml'
- main.java.srcDirs = [
- 'src',
- 'kitkat',
- 'jbmr2'
- ]
- main.res.srcDir 'res'
+ main.java.srcDirs = ['common', 'jbmr2', 'kitkat', 'api21', 'src']
+ main.aidl.srcDirs = ['common', 'jbmr2', 'kitkat', 'api21', 'src']
+ main.res.srcDirs = ['res']
+
+ androidTest.setRoot('tests')
+ androidTest.java.srcDir 'tests/java'
}
lintOptions {
// TODO: fix errors and reenable.
abortOnError false
}
-}
\ No newline at end of file
+}
+
+android.libraryVariants.all { variant ->
+ def name = variant.buildType.name
+
+ if (name.equals(com.android.builder.BuilderConstants.DEBUG)) {
+ return; // Skip debug builds.
+ }
+ def suffix = name.capitalize()
+
+ def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
+ dependsOn variant.javaCompile
+ from variant.javaCompile.destinationDir
+ from 'LICENSE.txt'
+ }
+ def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
+ source android.sourceSets.main.allJava
+ classpath = files(variant.javaCompile.classpath.files) + files(
+ "${android.plugin.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
+ }
+
+ def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
+ classifier = 'javadoc'
+ from 'build/docs/javadoc'
+ }
+
+ def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
+ classifier = 'sources'
+ from android.sourceSets.main.allSource
+ }
+
+ artifacts.add('archives', javadocJarTask);
+ artifacts.add('archives', sourcesJarTask);
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ repository(url: uri(rootProject.ext.supportRepoOut)) {
+ }
+
+ pom.project {
+ name 'Android Support Leanback v17'
+ description "Android Support Leanback v17"
+ url 'http://developer.android.com/tools/extras/support-library.html'
+ inceptionYear '2011'
+
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+
+ scm {
+ url "http://source.android.com"
+ connection "scm:git:https://android.googlesource.com/platform/frameworks/support"
+ }
+ developers {
+ developer {
+ name 'The Android Open Source Project'
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/v17/leanback/common/android/support/v17/leanback/app/SlideCallback.java b/v17/leanback/common/android/support/v17/leanback/app/SlideCallback.java
new file mode 100644
index 0000000..0693541
--- /dev/null
+++ b/v17/leanback/common/android/support/v17/leanback/app/SlideCallback.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.view.View;
+
+/**
+ * Used by Slide to determine the slide edge and distance when it is about to
+ * create animator.
+ */
+interface SlideCallback {
+
+ /**
+ * Called when Slide is about to create animator for an appearing/disappearing view.
+ * Callback returns true to ask Slide to create animator, edge is returned
+ * in edge[0], distance in pixels is returned in distance[0]. Slide will not
+ * create animator if callback returns false.
+ */
+ public boolean getSlide(View view, boolean appear, int[] edge, float[] distance);
+
+}
diff --git a/v17/leanback/common/android/support/v17/leanback/app/TransitionListener.java b/v17/leanback/common/android/support/v17/leanback/app/TransitionListener.java
new file mode 100644
index 0000000..bcda031
--- /dev/null
+++ b/v17/leanback/common/android/support/v17/leanback/app/TransitionListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+/**
+ * Listeners for transition start and stop.
+ */
+class TransitionListener {
+
+ public void onTransitionStart(Object transition) {
+ }
+
+ public void onTransitionEnd(Object transition) {
+ }
+
+}
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/app/Slide.java b/v17/leanback/kitkat/android/support/v17/leanback/app/Slide.java
new file mode 100644
index 0000000..d9c156d
--- /dev/null
+++ b/v17/leanback/kitkat/android/support/v17/leanback/app/Slide.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.graphics.Rect;
+import android.util.Log;
+import android.util.Property;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.transition.Visibility;
+import android.transition.Transition;
+import android.transition.TransitionValues;
+
+/**
+ * Slide distance toward/from a edge. The direction and distance are determined by
+ * {@link SlideCallback}.
+ */
+class Slide extends Visibility {
+ private static final String TAG = "Slide";
+
+ /**
+ * Move Views in or out of the left edge of the scene.
+ * @see #setSlideEdge(int)
+ */
+ public static final int LEFT = 0;
+
+ /**
+ * Move Views in or out of the top edge of the scene.
+ * @see #setSlideEdge(int)
+ */
+ public static final int TOP = 1;
+
+ /**
+ * Move Views in or out of the right edge of the scene.
+ * @see #setSlideEdge(int)
+ */
+ public static final int RIGHT = 2;
+
+ /**
+ * Move Views in or out of the bottom edge of the scene. This is the
+ * default slide direction.
+ * @see #setSlideEdge(int)
+ */
+ public static final int BOTTOM = 3;
+
+ private static final TimeInterpolator sDecelerate = new DecelerateInterpolator();
+ private static final TimeInterpolator sAccelerate = new AccelerateInterpolator();
+
+ private int[] mTempLoc = new int[2];
+ SlideCallback mCallback;
+ private int[] mTempEdge = new int[1];
+ private float[] mTempDistance = new float[1];
+
+ private interface CalculateSlide {
+ /** Returns the translation value for view when it out of the scene */
+ float getGone(float slide, View view);
+
+ /** Returns the translation value for view when it is in the scene */
+ float getHere(View view);
+
+ /** Returns the property to animate translation */
+ Property<View, Float> getProperty();
+ }
+
+ private static abstract class CalculateSlideHorizontal implements CalculateSlide {
+ @Override
+ public float getHere(View view) {
+ return view.getTranslationX();
+ }
+
+ @Override
+ public Property<View, Float> getProperty() {
+ return View.TRANSLATION_X;
+ }
+ }
+
+ private static abstract class CalculateSlideVertical implements CalculateSlide {
+ @Override
+ public float getHere(View view) {
+ return view.getTranslationY();
+ }
+
+ @Override
+ public Property<View, Float> getProperty() {
+ return View.TRANSLATION_Y;
+ }
+ }
+
+ private static final CalculateSlide sCalculateLeft = new CalculateSlideHorizontal() {
+ @Override
+ public float getGone(float distance, View view) {
+ return view.getTranslationX() - distance;
+ }
+ };
+
+ private static final CalculateSlide sCalculateTop = new CalculateSlideVertical() {
+ @Override
+ public float getGone(float distance, View view) {
+ return view.getTranslationY() - distance;
+ }
+ };
+
+ private static final CalculateSlide sCalculateRight = new CalculateSlideHorizontal() {
+ @Override
+ public float getGone(float distance, View view) {
+ return view.getTranslationX() + distance;
+ }
+ };
+
+ private static final CalculateSlide sCalculateBottom = new CalculateSlideVertical() {
+ @Override
+ public float getGone(float distance, View view) {
+ return view.getTranslationY() + distance;
+ }
+ };
+
+ public Slide() {
+ }
+
+ public void setCallback(SlideCallback callback) {
+ mCallback = callback;
+ }
+
+ private CalculateSlide getSlideEdge(int slideEdge) {
+ switch (slideEdge) {
+ case LEFT:
+ return sCalculateLeft;
+ case TOP:
+ return sCalculateTop;
+ case RIGHT:
+ return sCalculateRight;
+ case BOTTOM:
+ return sCalculateBottom;
+ default:
+ throw new IllegalArgumentException("Invalid slide direction");
+ }
+ }
+
+ private Animator createAnimation(final View view, Property<View, Float> property,
+ float start, float end, float terminalValue, TimeInterpolator interpolator) {
+ view.setTranslationY(start);
+ if (start == end) {
+ return null;
+ }
+ final ObjectAnimator anim = ObjectAnimator.ofFloat(view, property, start, end);
+
+ SlideAnimatorListener listener = new SlideAnimatorListener(view, terminalValue, end);
+ anim.addListener(listener);
+ anim.addPauseListener(listener);
+ anim.setInterpolator(interpolator);
+ return anim;
+ }
+
+ @Override
+ public Animator onAppear(ViewGroup sceneRoot,
+ TransitionValues startValues, int startVisibility,
+ TransitionValues endValues, int endVisibility) {
+ View view = (endValues != null) ? endValues.view : null;
+ if (view == null) {
+ return null;
+ }
+ if (mCallback == null || !mCallback.getSlide(view, true, mTempEdge, mTempDistance)) {
+ return null;
+ }
+ final CalculateSlide slideCalculator = getSlideEdge(mTempEdge[0]);
+ float end = slideCalculator.getHere(view);
+ float start = slideCalculator.getGone(mTempDistance[0], view);
+ return createAnimation(view, slideCalculator.getProperty(), start, end, end, sDecelerate);
+ }
+
+ @Override
+ public Animator onDisappear(ViewGroup sceneRoot,
+ TransitionValues startValues, int startVisibility,
+ TransitionValues endValues, int endVisibility) {
+ View view = (startValues != null) ? startValues.view : null;
+ if (view == null) {
+ return null;
+ }
+ if (mCallback == null || !mCallback.getSlide(view, false, mTempEdge, mTempDistance)) {
+ return null;
+ }
+ final CalculateSlide slideCalculator = getSlideEdge(mTempEdge[0]);
+ float start = slideCalculator.getHere(view);
+ float end = slideCalculator.getGone(mTempDistance[0], view);
+
+ return createAnimation(view, slideCalculator.getProperty(), start, end, start,
+ sAccelerate);
+ }
+
+ private static class SlideAnimatorListener extends AnimatorListenerAdapter {
+ private boolean mCanceled = false;
+ private float mPausedY;
+ private final View mView;
+ private final float mEndY;
+ private final float mTerminalY;
+
+ public SlideAnimatorListener(View view, float terminalY, float endY) {
+ mView = view;
+ mTerminalY = terminalY;
+ mEndY = endY;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ mView.setTranslationY(mTerminalY);
+ mCanceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ if (!mCanceled) {
+ mView.setTranslationY(mTerminalY);
+ }
+ }
+
+ @Override
+ public void onAnimationPause(Animator animator) {
+ mPausedY = mView.getTranslationY();
+ mView.setTranslationY(mEndY);
+ }
+
+ @Override
+ public void onAnimationResume(Animator animator) {
+ mView.setTranslationY(mPausedY);
+ }
+ }
+}
\ No newline at end of file
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/app/TransitionHelperKitkat.java b/v17/leanback/kitkat/android/support/v17/leanback/app/TransitionHelperKitkat.java
index 4ad20b0..07a3995 100644
--- a/v17/leanback/kitkat/android/support/v17/leanback/app/TransitionHelperKitkat.java
+++ b/v17/leanback/kitkat/android/support/v17/leanback/app/TransitionHelperKitkat.java
@@ -14,6 +14,7 @@
package android.support.v17.leanback.app;
import android.animation.Animator;
+import android.animation.TimeInterpolator;
import android.content.Context;
import android.transition.AutoTransition;
import android.transition.ChangeBounds;
@@ -33,10 +34,7 @@
class TransitionHelperKitkat {
- private final Context mContext;
-
- TransitionHelperKitkat(Context context) {
- mContext = context;
+ TransitionHelperKitkat() {
}
Object createScene(ViewGroup sceneRoot, Runnable enterAction) {
@@ -60,6 +58,12 @@
return new AutoTransition();
}
+ Object createSlide(SlideCallback callback) {
+ Slide slide = new Slide();
+ slide.setCallback(callback);
+ return slide;
+ }
+
Object createFadeTransition(int fadingMode) {
Fade fade = new Fade(fadingMode);
return fade;
@@ -143,6 +147,14 @@
((CustomChangeBounds) changeBounds).setDefaultStartDelay(startDelay);
}
+ void setStartDelay(Object transition, long startDelay) {
+ ((Transition)transition).setStartDelay(startDelay);
+ }
+
+ void setDuration(Object transition, long duration) {
+ ((Transition)transition).setDuration(duration);
+ }
+
void exclude(Object transition, int targetId, boolean exclude) {
((Transition) transition).excludeTarget(targetId, exclude);
}
@@ -167,13 +179,13 @@
((Transition) transition).addTarget(targetView);
}
- public void setTransitionCompleteListener(Object transition, Runnable listener) {
+ public void setTransitionListener(Object transition, final TransitionListener listener) {
Transition t = (Transition) transition;
- final Runnable completeListener = listener;
t.addListener(new Transition.TransitionListener() {
@Override
public void onTransitionStart(Transition transition) {
+ listener.onTransitionStart(transition);
}
@Override
@@ -186,7 +198,7 @@
@Override
public void onTransitionEnd(Transition transition) {
- completeListener.run();
+ listener.onTransitionEnd(transition);
}
@Override
@@ -198,4 +210,8 @@
void runTransition(Object scene, Object transition) {
TransitionManager.go((Scene) scene, (Transition) transition);
}
+
+ void setInterpolator(Object transition, Object timeInterpolator) {
+ ((Transition) transition).setInterpolator((TimeInterpolator) timeInterpolator);
+ }
}
diff --git a/v17/leanback/res/drawable-hdpi/ic_action_search.png b/v17/leanback/res/drawable-hdpi/ic_action_search.png
deleted file mode 100644
index a70393b..0000000
--- a/v17/leanback/res/drawable-hdpi/ic_action_search.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/ic_circle_g_bg.png b/v17/leanback/res/drawable-hdpi/ic_circle_g_bg.png
deleted file mode 100644
index 6777e19..0000000
--- a/v17/leanback/res/drawable-hdpi/ic_circle_g_bg.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/ic_circle_shadow.9.png b/v17/leanback/res/drawable-hdpi/ic_circle_shadow.9.png
deleted file mode 100644
index f556eac..0000000
--- a/v17/leanback/res/drawable-hdpi/ic_circle_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_bg_protection.png b/v17/leanback/res/drawable-hdpi/lb_bg_protection.png
new file mode 100644
index 0000000..34fb6a2
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_bg_protection.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png b/v17/leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png
new file mode 100644
index 0000000..e737a66
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_in_app_search.png b/v17/leanback/res/drawable-hdpi/lb_ic_in_app_search.png
new file mode 100644
index 0000000..542dd87
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_sad_cloud.png b/v17/leanback/res/drawable-hdpi/lb_ic_sad_cloud.png
new file mode 100644
index 0000000..620aba6
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_search_mic.png b/v17/leanback/res/drawable-hdpi/lb_ic_search_mic.png
new file mode 100644
index 0000000..5db5420
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_ic_search_mic_out.png b/v17/leanback/res/drawable-hdpi/lb_ic_search_mic_out.png
new file mode 100644
index 0000000..78e9b1e
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png b/v17/leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png
new file mode 100644
index 0000000..d5c5dc1
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png b/v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png
new file mode 100644
index 0000000..562ae9d
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png b/v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png
new file mode 100644
index 0000000..db44754
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/ic_action_search.png b/v17/leanback/res/drawable-mdpi/ic_action_search.png
deleted file mode 100644
index dea3962..0000000
--- a/v17/leanback/res/drawable-mdpi/ic_action_search.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/ic_circle_g_bg.png b/v17/leanback/res/drawable-mdpi/ic_circle_g_bg.png
deleted file mode 100644
index 641f096..0000000
--- a/v17/leanback/res/drawable-mdpi/ic_circle_g_bg.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/ic_circle_shadow.9.png b/v17/leanback/res/drawable-mdpi/ic_circle_shadow.9.png
deleted file mode 100644
index f45b76c..0000000
--- a/v17/leanback/res/drawable-mdpi/ic_circle_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_bg_protection.png b/v17/leanback/res/drawable-mdpi/lb_bg_protection.png
new file mode 100644
index 0000000..369e36a
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_bg_protection.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png b/v17/leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png
new file mode 100644
index 0000000..aa6b44f
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_in_app_search.png b/v17/leanback/res/drawable-mdpi/lb_ic_in_app_search.png
new file mode 100644
index 0000000..ffdc8ec
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_sad_cloud.png b/v17/leanback/res/drawable-mdpi/lb_ic_sad_cloud.png
new file mode 100644
index 0000000..5634efc
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_search_mic.png b/v17/leanback/res/drawable-mdpi/lb_ic_search_mic.png
new file mode 100644
index 0000000..7cc6d61
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_ic_search_mic_out.png b/v17/leanback/res/drawable-mdpi/lb_ic_search_mic_out.png
new file mode 100644
index 0000000..9e36b2e
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png b/v17/leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png
new file mode 100644
index 0000000..be062cc
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png b/v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png
new file mode 100644
index 0000000..b63a57f
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png b/v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png
new file mode 100644
index 0000000..d91acee
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/ic_action_search.png b/v17/leanback/res/drawable-xhdpi/ic_action_search.png
deleted file mode 100644
index 19658e4..0000000
--- a/v17/leanback/res/drawable-xhdpi/ic_action_search.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/ic_circle_g_bg.png b/v17/leanback/res/drawable-xhdpi/ic_circle_g_bg.png
deleted file mode 100644
index acb7c79..0000000
--- a/v17/leanback/res/drawable-xhdpi/ic_circle_g_bg.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/ic_circle_shadow.9.png b/v17/leanback/res/drawable-xhdpi/ic_circle_shadow.9.png
deleted file mode 100644
index 60930f4..0000000
--- a/v17/leanback/res/drawable-xhdpi/ic_circle_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_bg_protection.png b/v17/leanback/res/drawable-xhdpi/lb_bg_protection.png
new file mode 100644
index 0000000..5e9f150
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_bg_protection.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png b/v17/leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png
new file mode 100644
index 0000000..9796171
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_in_app_search.png b/v17/leanback/res/drawable-xhdpi/lb_ic_in_app_search.png
new file mode 100644
index 0000000..1dd0d0f
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png b/v17/leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png
new file mode 100644
index 0000000..e301816
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_search_mic.png b/v17/leanback/res/drawable-xhdpi/lb_ic_search_mic.png
new file mode 100644
index 0000000..c987c25
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png b/v17/leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png
new file mode 100644
index 0000000..5904a4f
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png b/v17/leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png
new file mode 100644
index 0000000..8cc5438
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png b/v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png
new file mode 100644
index 0000000..f913c0f
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png b/v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png
new file mode 100644
index 0000000..791ffd7
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/ic_action_search.png b/v17/leanback/res/drawable-xxhdpi/ic_action_search.png
deleted file mode 100644
index a108638..0000000
--- a/v17/leanback/res/drawable-xxhdpi/ic_action_search.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/ic_circle_g_bg.png b/v17/leanback/res/drawable-xxhdpi/ic_circle_g_bg.png
deleted file mode 100644
index 2bd8ab7..0000000
--- a/v17/leanback/res/drawable-xxhdpi/ic_circle_g_bg.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/ic_circle_shadow.9.png b/v17/leanback/res/drawable-xxhdpi/ic_circle_shadow.9.png
deleted file mode 100644
index 619ce81..0000000
--- a/v17/leanback/res/drawable-xxhdpi/ic_circle_shadow.9.png
+++ /dev/null
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png b/v17/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png
index 48f82a4..d2227b9 100644
--- a/v17/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png
+++ b/v17/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png b/v17/leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png
new file mode 100644
index 0000000..6539869
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_ic_actions_right_arrow.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png b/v17/leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png
new file mode 100644
index 0000000..f47827f
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_ic_in_app_search.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png b/v17/leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png
new file mode 100644
index 0000000..8e7e111
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_ic_sad_cloud.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic.png b/v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic.png
new file mode 100644
index 0000000..c8a0790
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png b/v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png
new file mode 100644
index 0000000..cb97131
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_ic_search_mic_out.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png b/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png
new file mode 100644
index 0000000..b9e372e
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_bg.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png b/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png
new file mode 100644
index 0000000..65f3a9e
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png b/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png
new file mode 100644
index 0000000..9bc3f6f
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_in_app_search_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v17/leanback/res/drawable/lb_search_orb.xml
similarity index 76%
rename from v17/leanback/res/drawable/lb_transition_action_bg.xml
rename to v17/leanback/res/drawable/lb_search_orb.xml
index 36688bc..438099c 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v17/leanback/res/drawable/lb_search_orb.xml
@@ -15,7 +15,6 @@
limitations under the License.
-->
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+</shape>
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v17/leanback/res/drawable/lb_speech_orb.xml
similarity index 76%
copy from v17/leanback/res/drawable/lb_transition_action_bg.xml
copy to v17/leanback/res/drawable/lb_speech_orb.xml
index 36688bc..3b66f12 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v17/leanback/res/drawable/lb_speech_orb.xml
@@ -15,7 +15,6 @@
limitations under the License.
-->
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+</shape>
diff --git a/v17/leanback/res/layout/lb_action_1_line.xml b/v17/leanback/res/layout/lb_action_1_line.xml
index 2374cf3..52d89e0 100644
--- a/v17/leanback/res/layout/lb_action_1_line.xml
+++ b/v17/leanback/res/layout/lb_action_1_line.xml
@@ -15,17 +15,10 @@
limitations under the License.
-->
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/lb_action_text"
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/lb_action_button"
android:layout_width="wrap_content"
android:layout_height="@dimen/lb_action_1_line_height"
- android:background="@drawable/lb_transition_action_bg"
- android:focusable="true"
- android:focusableInTouchMode="true"
- android:gravity="center_vertical"
+ style="?attr/detailsActionButtonStyle"
android:lines="1"
- android:paddingLeft="@dimen/lb_action_1_line_padding_left"
- android:paddingRight="@dimen/lb_action_padding_right"
- android:textAllCaps="true"
- android:textColor="@color/lb_action_text_color"
- android:textSize="@dimen/lb_action_text_size" />
+ />
diff --git a/v17/leanback/res/layout/lb_action_2_lines.xml b/v17/leanback/res/layout/lb_action_2_lines.xml
index 15cff81..697074a 100644
--- a/v17/leanback/res/layout/lb_action_2_lines.xml
+++ b/v17/leanback/res/layout/lb_action_2_lines.xml
@@ -15,32 +15,10 @@
limitations under the License.
-->
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/lb_action_button"
android:layout_width="wrap_content"
android:layout_height="@dimen/lb_action_2_lines_height"
- android:background="@drawable/lb_transition_action_bg"
- android:focusable="true"
- android:focusableInTouchMode="true" >
-
- <ImageView
- android:id="@+id/lb_action_icon"
- android:layout_width="@dimen/lb_action_icon_width"
- android:layout_height="wrap_content"
- android:layout_alignParentLeft="true"
- android:layout_centerVertical="true"
- android:layout_marginLeft="@dimen/lb_action_icon_margin"
- android:layout_marginRight="@dimen/lb_action_icon_margin" />
-
- <TextView
- android:id="@+id/lb_action_text"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_centerVertical="true"
- android:layout_toRightOf="@id/lb_action_icon"
- android:gravity="left|center_vertical"
- android:lines="2"
- android:textAllCaps="true"
- android:textColor="@color/lb_action_text_color"
- android:textSize="@dimen/lb_action_text_size" />
-
-</RelativeLayout>
\ No newline at end of file
+ style="?attr/detailsActionButtonStyle"
+ android:lines="2"
+ />
diff --git a/v17/leanback/res/layout/lb_browse_fragment.xml b/v17/leanback/res/layout/lb_browse_fragment.xml
index 0315daa..076a2f1 100644
--- a/v17/leanback/res/layout/lb_browse_fragment.xml
+++ b/v17/leanback/res/layout/lb_browse_fragment.xml
@@ -24,18 +24,19 @@
<android.support.v17.leanback.app.BrowseFrameLayout
android:focusable="true"
android:focusableInTouchMode="true"
+ android:descendantFocusability="afterDescendants"
android:id="@+id/browse_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
- <include layout="@layout/lb_browse_title" />
<android.support.v17.leanback.app.BrowseRowsFrameLayout
android:id="@+id/browse_container_dock"
- android:layout_width="wrap_content"
+ android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/browse_headers_dock"
android:layout_width="wrap_content"
android:layout_height="match_parent" />
+ <include layout="@layout/lb_browse_title" />
</android.support.v17.leanback.app.BrowseFrameLayout>
</FrameLayout>
diff --git a/v17/leanback/res/layout/lb_browse_title.xml b/v17/leanback/res/layout/lb_browse_title.xml
index 77bd35c..75068d8 100644
--- a/v17/leanback/res/layout/lb_browse_title.xml
+++ b/v17/leanback/res/layout/lb_browse_title.xml
@@ -14,43 +14,13 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<android.support.v17.leanback.widget.TitleView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/browse_title_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="?attr/browsePaddingTop"
- android:paddingLeft="?attr/browsePaddingLeft">
+ android:paddingRight="?attr/browsePaddingRight"
+ android:paddingLeft="?attr/browsePaddingLeft"
+ android:paddingBottom="?attr/browsePaddingTop"
+ style="?attr/browseTitleViewStyle" />
- <RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
-
- <ImageView
- android:id="@+id/browse_badge"
- android:layout_width="@dimen/lb_browse_title_icon_width"
- android:layout_height="@dimen/lb_browse_title_icon_height"
- android:layout_marginRight="@dimen/lb_browse_title_icon_margin_right"
- android:layout_alignParentLeft="true"
- android:layout_centerVertical="true"
- android:src="@null"
- android:visibility="gone"
- style="?attr/browseTitleIconStyle"/>
-
- <TextView
- android:id="@+id/browse_title"
- android:layout_width="wrap_content"
- android:layout_height="@dimen/lb_browse_title_height"
- android:layout_toRightOf="@id/browse_badge"
- android:layout_centerVertical="true"
- style="?attr/browseTitleTextStyle"/>
-
- <android.support.v17.leanback.widget.SearchOrbView
- android:id="@+id/browse_orb"
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:layout_alignParentTop="true"
- android:layout_alignParentRight="true"
- android:layout_marginRight="48dip"
- android:layout_marginBottom="4dip"/>
- </RelativeLayout>
-</LinearLayout>
\ No newline at end of file
diff --git a/v17/leanback/res/layout/lb_details_description.xml b/v17/leanback/res/layout/lb_details_description.xml
index 1cd26cd..798504f 100644
--- a/v17/leanback/res/layout/lb_details_description.xml
+++ b/v17/leanback/res/layout/lb_details_description.xml
@@ -22,6 +22,7 @@
android:layout_height="wrap_content"
>
+ <!-- Top margins set programatically -->
<TextView
android:id="@+id/lb_details_description_title"
android:layout_width="wrap_content"
diff --git a/v17/leanback/res/layout/lb_details_overview.xml b/v17/leanback/res/layout/lb_details_overview.xml
index ea8220e..ae8cddd 100644
--- a/v17/leanback/res/layout/lb_details_overview.xml
+++ b/v17/leanback/res/layout/lb_details_overview.xml
@@ -15,45 +15,63 @@
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:lb="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:orientation="vertical" >
+ android:paddingLeft="@dimen/lb_details_overview_margin_left"
+ android:paddingRight="@dimen/lb_details_overview_margin_right"
+ android:paddingBottom="@dimen/lb_details_overview_margin_bottom"
+ >
+ <!-- Background is applied to this inner layout -->
<LinearLayout
+ android:id="@+id/details_overview"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal" >
+ android:layout_height="@dimen/lb_details_overview_height_large"
+ android:orientation="horizontal"
+ >
<ImageView
android:id="@+id/details_overview_image"
- android:layout_width="@dimen/lb_details_overview_image_width"
- android:layout_height="wrap_content"
- android:layout_marginLeft="@dimen/lb_details_overview_image_margin_left"
- android:gravity="top|left" />
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:adjustViewBounds="true"
+ android:scaleType="fitStart"
+ />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginBottom="@dimen/lb_details_overview_description_margin_bottom"
+ android:layout_marginTop="@dimen/lb_details_overview_description_margin_top"
+ android:orientation="vertical" >
<FrameLayout
android:id="@+id/details_overview_description"
android:layout_width="wrap_content"
- android:layout_height="wrap_content"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:gravity="top"
android:layout_marginLeft="@dimen/lb_details_overview_description_margin_left"
android:layout_marginRight="@dimen/lb_details_overview_description_margin_right"
- android:gravity="top" />
+ />
+
+ <android.support.v17.leanback.widget.HorizontalGridView
+ android:id="@+id/details_overview_actions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:paddingLeft="@dimen/lb_details_overview_description_margin_left"
+ android:paddingRight="@dimen/lb_details_overview_description_margin_right"
+ lb:horizontalMargin="@dimen/lb_details_overview_action_items_margin"
+ lb:rowHeight="@dimen/lb_details_overview_actions_height" />
+
+ </LinearLayout>
</LinearLayout>
- <android.support.v17.leanback.widget.HorizontalGridView
- android:id="@+id/details_overview_actions"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/lb_details_overiew_actions_margin_top"
- android:clipChildren="false"
- android:clipToPadding="false"
- android:focusable="true"
- android:focusableInTouchMode="true"
- android:paddingLeft="@dimen/lb_details_overview_actions_padding_left"
- android:paddingRight="@dimen/lb_details_overview_actions_padding_right"
- lb:horizontalMargin="@dimen/lb_details_overview_action_items_margin"
- lb:rowHeight="@dimen/lb_details_overview_actions_height" />
-
-</LinearLayout>
\ No newline at end of file
+</FrameLayout>
diff --git a/v17/leanback/res/layout/lb_error_fragment.xml b/v17/leanback/res/layout/lb_error_fragment.xml
new file mode 100644
index 0000000..2815560
--- /dev/null
+++ b/v17/leanback/res/layout/lb_error_fragment.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/error_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:descendantFocusability="afterDescendants"
+ android:nextFocusLeft="@id/error_frame"
+ android:nextFocusRight="@id/error_frame"
+ android:nextFocusUp="@id/error_frame"
+ android:nextFocusDown="@id/error_frame">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/lb_error_image_max_height"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:maxWidth="@dimen/lb_error_message_max_width"
+ android:visibility="gone"
+ style="?attr/errorMessageStyle"/>
+ <Button
+ android:id="@+id/button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"
+ android:nextFocusLeft="@id/button"
+ android:nextFocusRight="@id/button"
+ android:nextFocusUp="@id/button"
+ android:nextFocusDown="@id/button"
+ style="?android:attr/buttonStyle"/>
+ </LinearLayout>
+
+ <include layout="@layout/lb_browse_title" />
+
+</FrameLayout>
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v17/leanback/res/layout/lb_header.xml
similarity index 71%
copy from v17/leanback/res/drawable/lb_transition_action_bg.xml
copy to v17/leanback/res/layout/lb_header.xml
index 36688bc..7437cf3 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v17/leanback/res/layout/lb_header.xml
@@ -15,7 +15,10 @@
limitations under the License.
-->
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+<android.support.v17.leanback.widget.RowHeaderView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/row_header"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?headerStyle"
+ />
diff --git a/v17/leanback/res/layout/lb_headers_fragment.xml b/v17/leanback/res/layout/lb_headers_fragment.xml
index c72cd06..dbfbb8d 100644
--- a/v17/leanback/res/layout/lb_headers_fragment.xml
+++ b/v17/leanback/res/layout/lb_headers_fragment.xml
@@ -18,6 +18,6 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:lb="http://schemas.android.com/apk/res-auto"
android:id="@+id/browse_headers"
- android:layout_width="match_parent"
+ android:layout_width="@dimen/lb_browse_headers_width"
android:layout_height="match_parent"
style="?attr/headersVerticalGridStyle"/>
diff --git a/v17/leanback/res/layout/lb_image_card_view.xml b/v17/leanback/res/layout/lb_image_card_view.xml
index 0a14ff7..15f1164 100644
--- a/v17/leanback/res/layout/lb_image_card_view.xml
+++ b/v17/leanback/res/layout/lb_image_card_view.xml
@@ -25,6 +25,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
+ android:adjustViewBounds="true"
android:contentDescription="@null" />
<FrameLayout
android:layout_width="match_parent"
@@ -70,6 +71,7 @@
android:layout_alignParentRight="true"
android:scaleType="fitCenter"
android:background="@color/lb_basic_card_info_bg_color"
+ android:visibility="gone"
android:contentDescription="@null" />
<ImageView
android:id="@+id/fade_mask"
@@ -79,6 +81,7 @@
android:layout_alignParentBottom="true"
android:layout_toStartOf="@id/extra_badge"
android:scaleType="fitCenter"
+ android:visibility="gone"
android:contentDescription="@null" />
</RelativeLayout>
</FrameLayout>
diff --git a/v17/leanback/res/layout/lb_list_row.xml b/v17/leanback/res/layout/lb_list_row.xml
index a432518..80d7bef 100644
--- a/v17/leanback/res/layout/lb_list_row.xml
+++ b/v17/leanback/res/layout/lb_list_row.xml
@@ -19,5 +19,5 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/row_content"
android:layout_width="match_parent"
- android:layout_height="@dimen/lb_browse_row_list_height"
+ android:layout_height="wrap_content"
style="?attr/rowHorizontalGridStyle" />
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v17/leanback/res/layout/lb_row_header.xml
similarity index 71%
copy from v17/leanback/res/drawable/lb_transition_action_bg.xml
copy to v17/leanback/res/layout/lb_row_header.xml
index 36688bc..69fac46 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v17/leanback/res/layout/lb_row_header.xml
@@ -15,7 +15,10 @@
limitations under the License.
-->
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+<android.support.v17.leanback.widget.RowHeaderView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/row_header"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?rowHeaderStyle"
+ />
diff --git a/v17/leanback/res/layout/lb_rows_fragment.xml b/v17/leanback/res/layout/lb_rows_fragment.xml
index 5b147c5..c4ffdc3 100644
--- a/v17/leanback/res/layout/lb_rows_fragment.xml
+++ b/v17/leanback/res/layout/lb_rows_fragment.xml
@@ -16,8 +16,8 @@
-->
<android.support.v17.leanback.widget.VerticalGridView
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:lb="http://schemas.android.com/apk/res-auto"
android:id="@+id/container_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="?attr/rowsVerticalGridStyle" />
+
diff --git a/v17/leanback/res/layout/lb_search_bar.xml b/v17/leanback/res/layout/lb_search_bar.xml
index 8e1a1b7..959d230 100644
--- a/v17/leanback/res/layout/lb_search_bar.xml
+++ b/v17/leanback/res/layout/lb_search_bar.xml
@@ -14,52 +14,62 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<android.support.v17.leanback.widget.SearchBar
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/lb_search_bar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content" >
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <android.support.v17.leanback.widget.SpeechOrbView
+ android:id="@+id/lb_search_bar_speech_orb"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/lb_search_bar_speech_orb_margin_left"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ >
+ <requestFocus/>
+ </android.support.v17.leanback.widget.SpeechOrbView>
<RelativeLayout
- android:id="@+id/lb_search_bar_layout"
- android:layout_width="match_parent"
- android:layout_height="@dimen/lb_search_bar_height"
- android:paddingLeft="@dimen/lb_search_bar_padding_left"
- android:clipChildren="false"
- android:layout_alignParentTop="true"
- android:layout_gravity="top"
- android:background="@android:color/transparent" >
+ android:id="@+id/lb_search_bar_items"
+ android:layout_width="@dimen/lb_search_bar_items_width"
+ android:layout_height="@dimen/lb_search_bar_items_height"
+ android:layout_toRightOf="@+id/lb_search_bar_speech_orb"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/lb_search_bar_items_margin_left"
+ android:orientation="horizontal"
+ android:background="@drawable/lb_in_app_search_bg"
+ >
+ <ImageView
+ android:id="@+id/lb_search_bar_badge"
+ android:layout_width="@dimen/lb_search_bar_icon_width"
+ android:layout_height="@dimen/lb_search_bar_icon_height"
+ android:layout_gravity="center_vertical|left"
+ android:layout_centerVertical="true"
+ android:layout_marginLeft="@dimen/lb_search_bar_icon_margin_left"
+ android:src="@null"
+ android:visibility="gone"
+ style="?attr/browseTitleIconStyle"/>
- <FrameLayout
- android:id="@+id/lb_search_bar_items"
- android:layout_width="@dimen/lb_search_bar_items_width"
- android:layout_height="wrap_content"
- android:layout_gravity="top"
- android:layout_marginLeft="@dimen/lb_search_browse_row_padding_left"
- android:layout_marginTop="@dimen/lb_search_bar_items_layout_margin_top"
- android:layout_alignParentLeft="true"
- android:layout_alignParentTop="true"
- android:orientation="horizontal"
- android:background="@android:color/transparent"
- android:layout_weight="1">
- <android.support.v17.leanback.widget.SearchEditText
- android:id="@+id/lb_search_text_editor"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:cursorVisible="true"
- android:editable="true"
- android:background="@null"
- android:fontFamily="sans-serif"
- android:focusable="true"
- android:imeOptions="normal|flagNoExtractUi|actionSearch"
- android:inputType="text|textAutoComplete"
- android:singleLine="true"
- android:textColor="@color/lb_search_bar_text_color"
- android:textColorHint="@color/lb_search_bar_hint_color"
- android:textCursorDrawable="@null"
- android:hint="@string/lb_search_bar_hint"
- android:textSize="@dimen/lb_search_bar_text_size"/>
- </FrameLayout>
+ <android.support.v17.leanback.widget.SearchEditText
+ android:id="@+id/lb_search_text_editor"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical|right"
+ android:layout_marginLeft="@dimen/lb_search_bar_edit_text_margin_left"
+ android:layout_centerVertical="true"
+ android:cursorVisible="true"
+ android:layout_toRightOf="@+id/lb_search_bar_badge"
+ android:editable="true"
+ android:background="@null"
+ android:fontFamily="sans-serif"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:imeOptions="normal|flagNoExtractUi|actionSearch"
+ android:inputType="text|textAutoComplete"
+ android:singleLine="true"
+ android:textColor="@color/lb_search_bar_text_color"
+ android:textColorHint="@color/lb_search_bar_hint_color"
+ android:textCursorDrawable="@null"
+ android:hint="@string/lb_search_bar_hint"
+ android:textSize="@dimen/lb_search_bar_text_size"/>
</RelativeLayout>
-</android.support.v17.leanback.widget.SearchBar>
+</merge>
diff --git a/v17/leanback/res/layout/lb_search_fragment.xml b/v17/leanback/res/layout/lb_search_fragment.xml
index 985ab21..57a46b4 100644
--- a/v17/leanback/res/layout/lb_search_fragment.xml
+++ b/v17/leanback/res/layout/lb_search_fragment.xml
@@ -18,10 +18,18 @@
android:id="@+id/lb_search_frame"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:paddingTop="@dimen/lb_search_bar_padding_top">
+ android:paddingTop="@dimen/lb_search_bar_padding_top"
+ android:clipToPadding="false"
+ android:clipChildren="false"
+ >
<FrameLayout
android:id="@+id/lb_results_frame"
android:layout_width="wrap_content"
android:layout_height="match_parent"/>
- <include layout="@layout/lb_search_bar" />
+ <android.support.v17.leanback.widget.SearchBar
+ android:id="@+id/lb_search_bar"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ />
</FrameLayout>
diff --git a/v17/leanback/res/layout/lb_search_orb.xml b/v17/leanback/res/layout/lb_search_orb.xml
index c6bf690..0eff71e 100644
--- a/v17/leanback/res/layout/lb_search_orb.xml
+++ b/v17/leanback/res/layout/lb_search_orb.xml
@@ -14,45 +14,21 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_height="match_parent"
- android:layout_width="wrap_content"
- android:orientation="horizontal"
- android:focusable="true">
- <TextView
- android:layout_height="wrap_content"
- android:layout_width="wrap_content"
- android:id="@+id/title"
- android:text="@string/orb_search_label"
- android:layout_gravity="center_vertical"
- android:textAppearance="@style/TextAppearance.Leanback.SearchLabel" />
+<merge xmlns:android="http://schemas.android.com/apk/res/android" >
- <FrameLayout
+ <View
android:id="@+id/search_orb"
android:layout_width="@dimen/lb_search_orb_size"
android:layout_height="@dimen/lb_search_orb_size"
- android:layout_gravity="top|start"
- android:clipChildren="false"
- android:layout_marginBottom="@dimen/lb_search_orb_margin_bottom"
- android:layout_marginLeft="@dimen/lb_search_orb_margin_left"
- android:layout_marginRight="@dimen/lb_search_orb_margin_right"
- android:layout_marginTop="@dimen/lb_search_orb_margin_top" >
+ android:background="@drawable/lb_search_orb" />
- <ImageView
- android:id="@+id/orb"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:src="@drawable/ic_circle_g_bg"
- android:background="@drawable/ic_circle_shadow"
- android:contentDescription="@null" />
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:src="@drawable/lb_ic_in_app_search"
+ android:contentDescription="@string/orb_search_action" />
- <ImageView
- android:id="@+id/icon"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:src="@drawable/ic_action_search"
- android:contentDescription="@string/orb_search_action" />
- </FrameLayout>
-</LinearLayout>
\ No newline at end of file
+</merge>
diff --git a/v17/leanback/res/layout/lb_speech_orb.xml b/v17/leanback/res/layout/lb_speech_orb.xml
new file mode 100644
index 0000000..ff6129b
--- /dev/null
+++ b/v17/leanback/res/layout/lb_speech_orb.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <View
+ android:id="@+id/lb_speech_orb"
+ android:layout_width="@dimen/lb_search_bar_speech_orb_size"
+ android:layout_height="@dimen/lb_search_bar_speech_orb_size"
+ android:background="@drawable/lb_speech_orb" />
+
+ <ImageView
+ android:id="@+id/lb_speech_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:src="@drawable/lb_ic_search_mic_out"
+ android:contentDescription="@string/orb_search_action" />
+
+</merge>
diff --git a/v17/leanback/res/layout/lb_title_view.xml b/v17/leanback/res/layout/lb_title_view.xml
new file mode 100644
index 0000000..590a683
--- /dev/null
+++ b/v17/leanback/res/layout/lb_title_view.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <ImageView
+ android:id="@+id/browse_badge"
+ android:layout_width="@dimen/lb_browse_title_text_width"
+ android:layout_height="@dimen/lb_browse_title_height"
+ android:layout_gravity="center_vertical|right"
+ android:src="@null"
+ android:visibility="gone"
+ style="?attr/browseTitleIconStyle"/>
+
+ <TextView
+ android:id="@+id/browse_title"
+ android:layout_width="@dimen/lb_browse_title_text_width"
+ android:layout_height="@dimen/lb_browse_title_height"
+ android:layout_gravity="center_vertical|right"
+ style="?attr/browseTitleTextStyle"/>
+
+ <android.support.v17.leanback.widget.SearchOrbView
+ android:id="@+id/browse_orb"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical|left"
+ android:visibility="invisible" />
+
+</merge>
diff --git a/v17/leanback/res/layout/lb_vertical_grid.xml b/v17/leanback/res/layout/lb_vertical_grid.xml
index 5dfe0c9..7154e48 100644
--- a/v17/leanback/res/layout/lb_vertical_grid.xml
+++ b/v17/leanback/res/layout/lb_vertical_grid.xml
@@ -18,6 +18,7 @@
<android.support.v17.leanback.widget.VerticalGridView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/browse_grid"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="match_parent"
+ android:layout_gravity="center"
style="?attr/itemsVerticalGridStyle" />
diff --git a/v17/leanback/res/layout/lb_vertical_grid_fragment.xml b/v17/leanback/res/layout/lb_vertical_grid_fragment.xml
index 902c483..4cd3a22 100644
--- a/v17/leanback/res/layout/lb_vertical_grid_fragment.xml
+++ b/v17/leanback/res/layout/lb_vertical_grid_fragment.xml
@@ -14,17 +14,24 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/browse_dummy"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
+ android:layout_height="match_parent" >
- <include layout="@layout/lb_browse_title" />
-
- <FrameLayout
- android:id="@+id/browse_grid_dock"
+ <android.support.v17.leanback.app.BrowseFrameLayout
+ android:id="@+id/browse_frame"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
android:layout_width="match_parent"
- android:layout_height="match_parent" />
+ android:layout_height="match_parent" >
-</LinearLayout>
\ No newline at end of file
+ <include layout="@layout/lb_browse_title" />
+
+ <FrameLayout
+ android:id="@+id/browse_grid_dock"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </android.support.v17.leanback.app.BrowseFrameLayout>
+</FrameLayout>
diff --git a/v17/leanback/res/raw/lb_voice_failure.ogg b/v17/leanback/res/raw/lb_voice_failure.ogg
new file mode 100644
index 0000000..d10b3d4
--- /dev/null
+++ b/v17/leanback/res/raw/lb_voice_failure.ogg
Binary files differ
diff --git a/v17/leanback/res/raw/lb_voice_no_input.ogg b/v17/leanback/res/raw/lb_voice_no_input.ogg
new file mode 100644
index 0000000..503eeab
--- /dev/null
+++ b/v17/leanback/res/raw/lb_voice_no_input.ogg
Binary files differ
diff --git a/v17/leanback/res/raw/lb_voice_open.ogg b/v17/leanback/res/raw/lb_voice_open.ogg
new file mode 100644
index 0000000..bf73679
--- /dev/null
+++ b/v17/leanback/res/raw/lb_voice_open.ogg
Binary files differ
diff --git a/v17/leanback/res/raw/lb_voice_success.ogg b/v17/leanback/res/raw/lb_voice_success.ogg
new file mode 100644
index 0000000..06afa23
--- /dev/null
+++ b/v17/leanback/res/raw/lb_voice_success.ogg
Binary files differ
diff --git a/v17/leanback/res/values-af/strings.xml b/v17/leanback/res/values-af/strings.xml
new file mode 100644
index 0000000..fa93257
--- /dev/null
+++ b/v17/leanback/res/values-af/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Soekhandeling"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Soek"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Praat om te soek"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Deursoek <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Praat om <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> te deursoek"</string>
+</resources>
diff --git a/v17/leanback/res/values-am/strings.xml b/v17/leanback/res/values-am/strings.xml
new file mode 100644
index 0000000..4b0f26f
--- /dev/null
+++ b/v17/leanback/res/values-am/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"እርምጃ ይፈልጉ"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"ይፈልጉ"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"ለመፈለግ ይናገሩ"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ፈልግ"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>ን ለመፈለግ ይናገሩ"</string>
+</resources>
diff --git a/v17/leanback/res/values-ar/strings.xml b/v17/leanback/res/values-ar/strings.xml
new file mode 100644
index 0000000..6540a3e
--- /dev/null
+++ b/v17/leanback/res/values-ar/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"إجراء البحث"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"بحث"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"التحدث للبحث"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"بحث في <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"تحدّث للبحث في <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-bg/strings.xml b/v17/leanback/res/values-bg/strings.xml
new file mode 100644
index 0000000..9cd9b5c
--- /dev/null
+++ b/v17/leanback/res/values-bg/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Действие за търсене"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Търсете"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Говорете, за да търсите"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Търсете в/ъв <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Говорете, за да търсите в/ъв <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-bn-rBD/strings.xml b/v17/leanback/res/values-bn-rBD/strings.xml
new file mode 100644
index 0000000..400553a
--- /dev/null
+++ b/v17/leanback/res/values-bn-rBD/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"অনুসন্ধান অ্যাকশন"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"অনুসন্ধান"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"অনুসন্ধান করতে বলুন"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> অনুসন্ধান করুন"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> অনুসন্ধান করতে বলুন"</string>
+</resources>
diff --git a/v17/leanback/res/values-bn-rWB/strings.xml b/v17/leanback/res/values-bn-rWB/strings.xml
new file mode 100644
index 0000000..400553a
--- /dev/null
+++ b/v17/leanback/res/values-bn-rWB/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"অনুসন্ধান অ্যাকশন"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"অনুসন্ধান"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"অনুসন্ধান করতে বলুন"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> অনুসন্ধান করুন"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> অনুসন্ধান করতে বলুন"</string>
+</resources>
diff --git a/v17/leanback/res/values-ca/strings.xml b/v17/leanback/res/values-ca/strings.xml
new file mode 100644
index 0000000..9087b56
--- /dev/null
+++ b/v17/leanback/res/values-ca/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Acció de cerca"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Cerca."</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Parla per fer una cerca."</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Cerca a <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>."</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Parla per cercar a <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>."</string>
+</resources>
diff --git a/v17/leanback/res/values-cs/strings.xml b/v17/leanback/res/values-cs/strings.xml
new file mode 100644
index 0000000..7c5b18c
--- /dev/null
+++ b/v17/leanback/res/values-cs/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Vyhledávání akce"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Vyhledávání"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Vyhledávejte hlasem"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Hledat <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Vyhledávejte v kontextu <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> hlasem"</string>
+</resources>
diff --git a/v17/leanback/res/values-da/strings.xml b/v17/leanback/res/values-da/strings.xml
new file mode 100644
index 0000000..51a195a
--- /dev/null
+++ b/v17/leanback/res/values-da/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Søgehandling"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Søg"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Tal for at søge"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Søg efter <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Tal for at søge efter <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-de/strings.xml b/v17/leanback/res/values-de/strings.xml
new file mode 100644
index 0000000..1b353a0
--- /dev/null
+++ b/v17/leanback/res/values-de/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Suchvorgang"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Suchen"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Zum Suchen sprechen"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"In <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> suchen"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Zum Suchen in <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> sprechen"</string>
+</resources>
diff --git a/v17/leanback/res/values-el/strings.xml b/v17/leanback/res/values-el/strings.xml
new file mode 100644
index 0000000..fc8c159
--- /dev/null
+++ b/v17/leanback/res/values-el/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Ενέργεια αναζήτησης"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Αναζήτηση"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Μιλήστε για να κάνετε αναζήτηση"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Αναζήτηση <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Μιλήστε για αναζήτηση <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-en-rGB/strings.xml b/v17/leanback/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..666fa5c
--- /dev/null
+++ b/v17/leanback/res/values-en-rGB/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Search Action"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Search"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Speak to search"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Search <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Speak to search <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-en-rIN/strings.xml b/v17/leanback/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..666fa5c
--- /dev/null
+++ b/v17/leanback/res/values-en-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Search Action"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Search"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Speak to search"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Search <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Speak to search <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-es-rUS/strings.xml b/v17/leanback/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..896535a
--- /dev/null
+++ b/v17/leanback/res/values-es-rUS/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Acción de búsqueda"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Búsqueda"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Habla para buscar"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Buscar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Habla para buscar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>."</string>
+</resources>
diff --git a/v17/leanback/res/values-es/strings.xml b/v17/leanback/res/values-es/strings.xml
new file mode 100644
index 0000000..9990ae6
--- /dev/null
+++ b/v17/leanback/res/values-es/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Buscar..."</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Buscar"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Buscar por voz"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Buscar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Buscar por voz <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-et-rEE/strings.xml b/v17/leanback/res/values-et-rEE/strings.xml
new file mode 100644
index 0000000..a097b5d
--- /dev/null
+++ b/v17/leanback/res/values-et-rEE/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Otsimistoiming"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Otsing"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Öelge otsimiseks"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Otsige teenusest <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Kõnelge teenusest <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> otsimiseks"</string>
+</resources>
diff --git a/v17/leanback/res/values-eu-rES/strings.xml b/v17/leanback/res/values-eu-rES/strings.xml
new file mode 100644
index 0000000..2690a84
--- /dev/null
+++ b/v17/leanback/res/values-eu-rES/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Bilaketa"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Bilatu"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Esan bilatu nahi duzuna"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Bilatu <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Esan bilatu nahi duzuna, bilaketa hemen egiteko: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-eu-rPV/strings.xml b/v17/leanback/res/values-eu-rPV/strings.xml
new file mode 100644
index 0000000..2690a84
--- /dev/null
+++ b/v17/leanback/res/values-eu-rPV/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Bilaketa"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Bilatu"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Esan bilatu nahi duzuna"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Bilatu <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Esan bilatu nahi duzuna, bilaketa hemen egiteko: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-fa/strings.xml b/v17/leanback/res/values-fa/strings.xml
new file mode 100644
index 0000000..fa4bb0c
--- /dev/null
+++ b/v17/leanback/res/values-fa/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"عملکرد جستجو"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"جستجو"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"برای جستجو صحبت کنید"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"جستجوی <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"جستجو با گفتن <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-fi/strings.xml b/v17/leanback/res/values-fi/strings.xml
new file mode 100644
index 0000000..3193006
--- /dev/null
+++ b/v17/leanback/res/values-fi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Hakutoiminto"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Haku"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Tee haku puhumalla"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Haku: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Puhehaku: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-fr-rCA/strings.xml b/v17/leanback/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..20595c3
--- /dev/null
+++ b/v17/leanback/res/values-fr-rCA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Action de recherche"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Rechercher"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Énoncez votre recherche"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Rechercher dans <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Énoncez votre recherche dans <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-fr/strings.xml b/v17/leanback/res/values-fr/strings.xml
new file mode 100644
index 0000000..9a8d2c7
--- /dev/null
+++ b/v17/leanback/res/values-fr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Commande de recherche"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Rechercher"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Énoncer la recherche"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Rechercher \"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>\""</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Énoncer la recherche \"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>\""</string>
+</resources>
diff --git a/v17/leanback/res/values-gl-rES/strings.xml b/v17/leanback/res/values-gl-rES/strings.xml
new file mode 100644
index 0000000..b85dcd1
--- /dev/null
+++ b/v17/leanback/res/values-gl-rES/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Acción de busca"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Buscar"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Fala para efectuar a busca"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Busca <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Fala para buscar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-gl-rGA/strings.xml b/v17/leanback/res/values-gl-rGA/strings.xml
new file mode 100644
index 0000000..b85dcd1
--- /dev/null
+++ b/v17/leanback/res/values-gl-rGA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Acción de busca"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Buscar"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Fala para efectuar a busca"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Busca <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Fala para buscar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-hi/strings.xml b/v17/leanback/res/values-hi/strings.xml
new file mode 100644
index 0000000..0d3e72d
--- /dev/null
+++ b/v17/leanback/res/values-hi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"खोज कार्रवाई"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"खोजें"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"खोजने के लिए बोलें"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> खोजें"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> खोजने के लिए बोलें"</string>
+</resources>
diff --git a/v17/leanback/res/values-hr/strings.xml b/v17/leanback/res/values-hr/strings.xml
new file mode 100644
index 0000000..89d8053
--- /dev/null
+++ b/v17/leanback/res/values-hr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Radnja pretraživanja"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Pretražite"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Izgovorite upit za pretraživanje"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Tražite <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Izgovorite upit za pretraživanje <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-hu/strings.xml b/v17/leanback/res/values-hu/strings.xml
new file mode 100644
index 0000000..e6eaacb
--- /dev/null
+++ b/v17/leanback/res/values-hu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Keresési művelet"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Keresés"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Beszéljen a keresés indításához"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Keresés itt: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Mondjon valamit, hogy itt keressen: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-hy-rAM/strings.xml b/v17/leanback/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000..de8561f
--- /dev/null
+++ b/v17/leanback/res/values-hy-rAM/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Որոնման հրամանը"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Որոնում"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Խոսեք՝ որոնելու համար"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Որոնեք <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Խոսեք՝ <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> որոնելու համար"</string>
+</resources>
diff --git a/v17/leanback/res/values-in/strings.xml b/v17/leanback/res/values-in/strings.xml
new file mode 100644
index 0000000..9be0a01
--- /dev/null
+++ b/v17/leanback/res/values-in/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Tindakan Penelusuran"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Telusuri"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Ucapkan untuk menelusuri"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Telusuri <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Ucapkan untuk menelusuri <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-is-rIS/strings.xml b/v17/leanback/res/values-is-rIS/strings.xml
new file mode 100644
index 0000000..29ccdc1
--- /dev/null
+++ b/v17/leanback/res/values-is-rIS/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Leitaraðgerð"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Leitaðu"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Talaðu til að leita"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Leita í <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Talaðu til að leita í <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-it/strings.xml b/v17/leanback/res/values-it/strings.xml
new file mode 100644
index 0000000..fa11e9d
--- /dev/null
+++ b/v17/leanback/res/values-it/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Azione di ricerca"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Ricerca"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Parla per cercare"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Cerca in <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Parla per cercare in <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-iw/strings.xml b/v17/leanback/res/values-iw/strings.xml
new file mode 100644
index 0000000..dacb052
--- /dev/null
+++ b/v17/leanback/res/values-iw/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"פעולת חיפוש"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"חפש"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"דבר כדי לחפש"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"חפש את <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"דבר כדי לחפש את <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-ja/strings.xml b/v17/leanback/res/values-ja/strings.xml
new file mode 100644
index 0000000..62b08b5
--- /dev/null
+++ b/v17/leanback/res/values-ja/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"検索操作"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"検索"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"音声検索"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>を検索"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>を音声検索"</string>
+</resources>
diff --git a/v17/leanback/res/values-ka-rGE/strings.xml b/v17/leanback/res/values-ka-rGE/strings.xml
new file mode 100644
index 0000000..ab9492c
--- /dev/null
+++ b/v17/leanback/res/values-ka-rGE/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"ძიების მოქმედება"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"ძიება"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"თქვით საძიებო ფრაზა"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>-ის ძიება"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"თქვით <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>-ის საძიებლად"</string>
+</resources>
diff --git a/v17/leanback/res/values-kk-rKZ/strings.xml b/v17/leanback/res/values-kk-rKZ/strings.xml
new file mode 100644
index 0000000..160a8e7
--- /dev/null
+++ b/v17/leanback/res/values-kk-rKZ/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Іздеу әрекеті"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Іздеу"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Іздеу үшін сөйлеу"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> іздеу"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> іздеу үшін сөйлеңіз"</string>
+</resources>
diff --git a/v17/leanback/res/values-km-rKH/strings.xml b/v17/leanback/res/values-km-rKH/strings.xml
new file mode 100644
index 0000000..92245b7
--- /dev/null
+++ b/v17/leanback/res/values-km-rKH/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"ស្វែងរកសកម្មភាព"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"ស្វែងរក"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"និយាយដើម្បីស្វែងរក"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"ស្វែងរក <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"និយាយដើម្បីស្វែងរក <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-kn-rIN/strings.xml b/v17/leanback/res/values-kn-rIN/strings.xml
new file mode 100644
index 0000000..6ca527f
--- /dev/null
+++ b/v17/leanback/res/values-kn-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"ಹುಡುಕಾಟ ಕ್ರಿಯೆ"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"ಹುಡುಕಿ"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"ಹುಡುಕಲು ಮಾತನಾಡಿ"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ಹುಡುಕಿ"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ಮಾತನಾಡಿ ಹುಡುಕಾಟ ನಡೆಸಿ"</string>
+</resources>
diff --git a/v17/leanback/res/values-kn-rKA/strings.xml b/v17/leanback/res/values-kn-rKA/strings.xml
new file mode 100644
index 0000000..6ca527f
--- /dev/null
+++ b/v17/leanback/res/values-kn-rKA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"ಹುಡುಕಾಟ ಕ್ರಿಯೆ"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"ಹುಡುಕಿ"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"ಹುಡುಕಲು ಮಾತನಾಡಿ"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ಹುಡುಕಿ"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ಮಾತನಾಡಿ ಹುಡುಕಾಟ ನಡೆಸಿ"</string>
+</resources>
diff --git a/v17/leanback/res/values-ko/strings.xml b/v17/leanback/res/values-ko/strings.xml
new file mode 100644
index 0000000..af0130d
--- /dev/null
+++ b/v17/leanback/res/values-ko/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"검색 작업"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"검색"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"음성 검색"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> 검색"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> 음성 검색"</string>
+</resources>
diff --git a/v17/leanback/res/values-ky-rKG/strings.xml b/v17/leanback/res/values-ky-rKG/strings.xml
new file mode 100644
index 0000000..64a2e72
--- /dev/null
+++ b/v17/leanback/res/values-ky-rKG/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Издөө аракети"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Издөө"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Издөө үчүн сүйлөңүз"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> издөө"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> издөө үчүн сүйлөңүз"</string>
+</resources>
diff --git a/v17/leanback/res/values-lo-rLA/strings.xml b/v17/leanback/res/values-lo-rLA/strings.xml
new file mode 100644
index 0000000..107f989
--- /dev/null
+++ b/v17/leanback/res/values-lo-rLA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"ຊອກຫາຄຳສັ່ງ"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"ຊອກຫາ"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"ເວົ້າເພື່ອຊອກຫາ"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"ຊອກຫາ <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"ເວົ້າເພື່ອຊອກຫາ <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-lt/strings.xml b/v17/leanback/res/values-lt/strings.xml
new file mode 100644
index 0000000..f765d04
--- /dev/null
+++ b/v17/leanback/res/values-lt/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Paieškos veiksmas"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Paieška"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Pasakykite, kad ieškotumėte"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Ieškoti „<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>“"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Kalbėkite, kad ieškotumėte „<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>“"</string>
+</resources>
diff --git a/v17/leanback/res/values-lv/strings.xml b/v17/leanback/res/values-lv/strings.xml
new file mode 100644
index 0000000..677ae6b
--- /dev/null
+++ b/v17/leanback/res/values-lv/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Meklēšanas darbība"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Meklēt"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Runāt, lai meklētu"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Meklējiet <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Runājiet, lai meklētu <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-mk-rMK/strings.xml b/v17/leanback/res/values-mk-rMK/strings.xml
new file mode 100644
index 0000000..8ab8384
--- /dev/null
+++ b/v17/leanback/res/values-mk-rMK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Акција на пребарување"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Пребарување"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Зборувајте за да пребарувате"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Пребарувај <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Кажете за да се пребарува <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-ml-rIN/strings.xml b/v17/leanback/res/values-ml-rIN/strings.xml
new file mode 100644
index 0000000..e31770d
--- /dev/null
+++ b/v17/leanback/res/values-ml-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"തിരയൽ പ്രവർത്തനം"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"തിരയുക"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"ശബ്ദം ഉപയോഗിച്ച് തിരയുക"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> തിരയുക"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> തിരയുന്നതിന് സംസാരിക്കുക"</string>
+</resources>
diff --git a/v17/leanback/res/values-ml-rKL/strings.xml b/v17/leanback/res/values-ml-rKL/strings.xml
new file mode 100644
index 0000000..e31770d
--- /dev/null
+++ b/v17/leanback/res/values-ml-rKL/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"തിരയൽ പ്രവർത്തനം"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"തിരയുക"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"ശബ്ദം ഉപയോഗിച്ച് തിരയുക"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> തിരയുക"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> തിരയുന്നതിന് സംസാരിക്കുക"</string>
+</resources>
diff --git a/v17/leanback/res/values-mn-rMN/strings.xml b/v17/leanback/res/values-mn-rMN/strings.xml
new file mode 100644
index 0000000..a58dfe9
--- /dev/null
+++ b/v17/leanback/res/values-mn-rMN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Хайлтын үйлдэл"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Хайлт"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Ярьж хайх"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> Хайх"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> хайхын тулд ярина уу"</string>
+</resources>
diff --git a/v17/leanback/res/values-mr-rIN/strings.xml b/v17/leanback/res/values-mr-rIN/strings.xml
new file mode 100644
index 0000000..296b21c
--- /dev/null
+++ b/v17/leanback/res/values-mr-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"शोध क्रिया"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"शोधा"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"शोधण्यासाठी बोला"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> शोधा"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> शोधण्यासाठी बोला"</string>
+</resources>
diff --git a/v17/leanback/res/values-mr-rMH/strings.xml b/v17/leanback/res/values-mr-rMH/strings.xml
new file mode 100644
index 0000000..296b21c
--- /dev/null
+++ b/v17/leanback/res/values-mr-rMH/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"शोध क्रिया"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"शोधा"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"शोधण्यासाठी बोला"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> शोधा"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> शोधण्यासाठी बोला"</string>
+</resources>
diff --git a/v17/leanback/res/values-ms-rMY/strings.xml b/v17/leanback/res/values-ms-rMY/strings.xml
new file mode 100644
index 0000000..d915409
--- /dev/null
+++ b/v17/leanback/res/values-ms-rMY/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Tindakan Carian"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Carian"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Tutur untuk membuat carian"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Cari <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Sebut untuk mencari <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-my-rMM/strings.xml b/v17/leanback/res/values-my-rMM/strings.xml
new file mode 100644
index 0000000..76e70f2
--- /dev/null
+++ b/v17/leanback/res/values-my-rMM/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"ရှာဖွေရန် လုပ်ဆောင်ချက်"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"ရှာဖွေရန်"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"ရှာဖွေရန် ပြောပါ"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>ကို ရှာရန်"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ကို ရှာရန် ပြောပါ"</string>
+</resources>
diff --git a/v17/leanback/res/values-nb/strings.xml b/v17/leanback/res/values-nb/strings.xml
new file mode 100644
index 0000000..c1fe682
--- /dev/null
+++ b/v17/leanback/res/values-nb/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Søkehandling"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Søk"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Snakk for å søke"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Søk i <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Snakk for å søke i <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-ne-rNP/strings.xml b/v17/leanback/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000..81ce461
--- /dev/null
+++ b/v17/leanback/res/values-ne-rNP/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"कार्य खोजी गर्नुहोस्"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"खोजी गर्नुहोस्"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"खोजी गर्न बोल्नुहोस्"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> खोज्नुहोस्"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> खोजी गर्न बोल्नुहोस्"</string>
+</resources>
diff --git a/v17/leanback/res/values-nl/strings.xml b/v17/leanback/res/values-nl/strings.xml
new file mode 100644
index 0000000..5c0ba2e
--- /dev/null
+++ b/v17/leanback/res/values-nl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Actie zoeken"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Zoeken"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Spreek om te zoeken"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> zoeken"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Spreek om <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> te zoeken"</string>
+</resources>
diff --git a/v17/leanback/res/values-pl/strings.xml b/v17/leanback/res/values-pl/strings.xml
new file mode 100644
index 0000000..9942bc7
--- /dev/null
+++ b/v17/leanback/res/values-pl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Wyszukaj czynność"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Szukaj"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Powiedz, aby wyszukać"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Szukaj <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Powiedz, by wyszukać <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-pt-rPT/strings.xml b/v17/leanback/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..0f15262
--- /dev/null
+++ b/v17/leanback/res/values-pt-rPT/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Ação de pesquisa"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Pesquisar"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Fale para pesquisar"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Pesquisar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Fale para pesquisar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-pt/strings.xml b/v17/leanback/res/values-pt/strings.xml
new file mode 100644
index 0000000..0f15262
--- /dev/null
+++ b/v17/leanback/res/values-pt/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Ação de pesquisa"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Pesquisar"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Fale para pesquisar"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Pesquisar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Fale para pesquisar <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-ro/strings.xml b/v17/leanback/res/values-ro/strings.xml
new file mode 100644
index 0000000..fd354372
--- /dev/null
+++ b/v17/leanback/res/values-ro/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Acțiunea de căutare"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Căutați"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Rostiți pentru a căuta"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Căutați <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Vorbiți pentru a căuta <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-ru/strings.xml b/v17/leanback/res/values-ru/strings.xml
new file mode 100644
index 0000000..a06ebbc
--- /dev/null
+++ b/v17/leanback/res/values-ru/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Поиск"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Поиск"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Произнесите запрос"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Поиск здесь: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Произнесите запрос для поиска здесь: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-si-rLK/strings.xml b/v17/leanback/res/values-si-rLK/strings.xml
new file mode 100644
index 0000000..77742b5
--- /dev/null
+++ b/v17/leanback/res/values-si-rLK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"සෙවීමේ ක්රියාව"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"සොයන්න"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"සෙවීමට කථා කරන්න"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> සොයන්න"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> සොයන්න කථා කරන්න"</string>
+</resources>
diff --git a/v17/leanback/res/values-sk/strings.xml b/v17/leanback/res/values-sk/strings.xml
new file mode 100644
index 0000000..8af51d8
--- /dev/null
+++ b/v17/leanback/res/values-sk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Akcia vyhľadávania"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Hľadať"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Hovorením spustíte vyhľadávanie"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Vyhľadať výraz <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Vyslovením výrazu <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> spustíte jeho vyhľad."</string>
+</resources>
diff --git a/v17/leanback/res/values-sl/strings.xml b/v17/leanback/res/values-sl/strings.xml
new file mode 100644
index 0000000..d367572
--- /dev/null
+++ b/v17/leanback/res/values-sl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Dejanje iskanja"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Iskanje"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Izgovorite, če želite iskati"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Iskanje: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Govorite, če želite iskati: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-sr/strings.xml b/v17/leanback/res/values-sr/strings.xml
new file mode 100644
index 0000000..771067c
--- /dev/null
+++ b/v17/leanback/res/values-sr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Радња претраге"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Претражите"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Говорите да бисте претраживали"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Претражите <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Изговорите да бисте претражили <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-sv/strings.xml b/v17/leanback/res/values-sv/strings.xml
new file mode 100644
index 0000000..8b64837
--- /dev/null
+++ b/v17/leanback/res/values-sv/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Sökåtgärd"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Sök"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Säg det du söker efter"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Sök i <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Tala för att söka i <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-sw/strings.xml b/v17/leanback/res/values-sw/strings.xml
new file mode 100644
index 0000000..23d2641
--- /dev/null
+++ b/v17/leanback/res/values-sw/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Kitendo cha Kutafuta"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Utafutaji"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Tamka ili utafute"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Tafuta <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Tamka ili utafute <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-ta-rIN/strings.xml b/v17/leanback/res/values-ta-rIN/strings.xml
new file mode 100644
index 0000000..9533839
--- /dev/null
+++ b/v17/leanback/res/values-ta-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"செயலைத் தேடுக"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"தேடு"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"தேட, பேசவும்"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ஐத் தேடுக"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ஐத் தேட, பேசவும்"</string>
+</resources>
diff --git a/v17/leanback/res/values-ta-rTN/strings.xml b/v17/leanback/res/values-ta-rTN/strings.xml
new file mode 100644
index 0000000..9533839
--- /dev/null
+++ b/v17/leanback/res/values-ta-rTN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"செயலைத் தேடுக"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"தேடு"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"தேட, பேசவும்"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ஐத் தேடுக"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> ஐத் தேட, பேசவும்"</string>
+</resources>
diff --git a/v17/leanback/res/values-te-rAP/strings.xml b/v17/leanback/res/values-te-rAP/strings.xml
new file mode 100644
index 0000000..2715f97
--- /dev/null
+++ b/v17/leanback/res/values-te-rAP/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"శోధన చర్య"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"శోధించండి"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"శోధించడానికి చదివి వినిపించండి"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>ని శోధించండి"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>ని శోధించడానికి చదివి వినిపించండి"</string>
+</resources>
diff --git a/v17/leanback/res/values-te-rIN/strings.xml b/v17/leanback/res/values-te-rIN/strings.xml
new file mode 100644
index 0000000..2715f97
--- /dev/null
+++ b/v17/leanback/res/values-te-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"శోధన చర్య"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"శోధించండి"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"శోధించడానికి చదివి వినిపించండి"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>ని శోధించండి"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>ని శోధించడానికి చదివి వినిపించండి"</string>
+</resources>
diff --git a/v17/leanback/res/values-th/strings.xml b/v17/leanback/res/values-th/strings.xml
new file mode 100644
index 0000000..09922aa
--- /dev/null
+++ b/v17/leanback/res/values-th/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"การดำเนินการค้นหา"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"ค้นหา"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"พูดเพื่อค้นหา"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"ค้นหา <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"พูดเพื่อค้นหา <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-tl/strings.xml b/v17/leanback/res/values-tl/strings.xml
new file mode 100644
index 0000000..43eaad2
--- /dev/null
+++ b/v17/leanback/res/values-tl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Pagkilos sa Paghahanap"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Maghanap"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Magsalita upang maghanap"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Hanapin ang <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Magsalita upang hanapin ang <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-tr/strings.xml b/v17/leanback/res/values-tr/strings.xml
new file mode 100644
index 0000000..cfa5167
--- /dev/null
+++ b/v17/leanback/res/values-tr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Arama İşlemi"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Ara"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Arama yapmak için konuşun"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Ara: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Aramak için konuşun: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-uk/strings.xml b/v17/leanback/res/values-uk/strings.xml
new file mode 100644
index 0000000..c67bc8b
--- /dev/null
+++ b/v17/leanback/res/values-uk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Команда пошуку"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Пошук"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Продиктуйте пошуковий запит"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Шукати: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Продиктуйте, щоб шукати: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-ur-rPK/strings.xml b/v17/leanback/res/values-ur-rPK/strings.xml
new file mode 100644
index 0000000..bcc9fde
--- /dev/null
+++ b/v17/leanback/res/values-ur-rPK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"تلاش کی کارروائی"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"تلاش کریں"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"تلاش کرنے کیلئے بولیں"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> تلاش کریں"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g> تلاش کرنے کیلئے بولیں"</string>
+</resources>
diff --git a/v17/leanback/res/values-uz-rUZ/strings.xml b/v17/leanback/res/values-uz-rUZ/strings.xml
new file mode 100644
index 0000000..95ae803
--- /dev/null
+++ b/v17/leanback/res/values-uz-rUZ/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Qidiruv amali"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Qidirish"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Qidirish uchun gapiring"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Qidirish: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Qidirish uchun ayting: <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v17/leanback/res/values-v21/themes.xml
similarity index 75%
copy from v17/leanback/res/drawable/lb_transition_action_bg.xml
copy to v17/leanback/res/values-v21/themes.xml
index 36688bc..9538d2b 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v17/leanback/res/values-v21/themes.xml
@@ -15,7 +15,7 @@
limitations under the License.
-->
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+<resources>
+ <style name="Theme.LeanbackBase" parent="android:Theme.Material.NoActionBar">
+ </style>
+</resources>
diff --git a/v17/leanback/res/values-vi/strings.xml b/v17/leanback/res/values-vi/strings.xml
new file mode 100644
index 0000000..2da6873
--- /dev/null
+++ b/v17/leanback/res/values-vi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Tác vụ tìm kiếm"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Tìm kiếm"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Nói để tìm kiếm"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Tìm kiếm <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Nói để tìm kiếm <xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values-zh-rCN/strings.xml b/v17/leanback/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..7f91918
--- /dev/null
+++ b/v17/leanback/res/values-zh-rCN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"搜索操作"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"搜索"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"说话即可开始搜索"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"搜索<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"说话即可在<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>中搜索"</string>
+</resources>
diff --git a/v17/leanback/res/values-zh-rHK/strings.xml b/v17/leanback/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..6e32bf5
--- /dev/null
+++ b/v17/leanback/res/values-zh-rHK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"搜尋動作"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"搜尋"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"使用語音搜尋"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"搜尋「<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>」"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"使用語音搜尋「<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>」"</string>
+</resources>
diff --git a/v17/leanback/res/values-zh-rTW/strings.xml b/v17/leanback/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..6e32bf5
--- /dev/null
+++ b/v17/leanback/res/values-zh-rTW/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"搜尋動作"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"搜尋"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"使用語音搜尋"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"搜尋「<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>」"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"使用語音搜尋「<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>」"</string>
+</resources>
diff --git a/v17/leanback/res/values-zu/strings.xml b/v17/leanback/res/values-zu/strings.xml
new file mode 100644
index 0000000..4168b19
--- /dev/null
+++ b/v17/leanback/res/values-zu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2014 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="orb_search_action" msgid="5651268540267663887">"Isenzo sokusesha"</string>
+ <string name="lb_search_bar_hint" msgid="8325490927970116252">"Sesha"</string>
+ <string name="lb_search_bar_hint_speech" msgid="5511270823320183816">"Khuluma ukuze useshe"</string>
+ <string name="lb_search_bar_hint_with_title" msgid="1627103380996590035">"Sesha i-<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+ <string name="lb_search_bar_hint_with_title_speech" msgid="2712734639766312034">"Khuluma ukuze useshe i-<xliff:g id="SEARCH_CONTEXT">%1$s</xliff:g>"</string>
+</resources>
diff --git a/v17/leanback/res/values/attrs.xml b/v17/leanback/res/values/attrs.xml
index 78456c6..ecacc55 100644
--- a/v17/leanback/res/values/attrs.xml
+++ b/v17/leanback/res/values/attrs.xml
@@ -26,18 +26,24 @@
<attr name="horizontalMargin" format="dimension" />
<!-- Defining margin between two items vertically -->
<attr name="verticalMargin" format="dimension" />
+ <!-- Defining gravity of child view -->
+ <attr name="android:gravity" />
</declare-styleable>
<declare-styleable name="lbHorizontalGridView">
<!-- Defining height of each row of HorizontalGridView -->
- <attr name="rowHeight" format="dimension" />
+ <attr name="rowHeight" format="dimension" >
+ <enum name="wrap_content" value="-2" />
+ </attr>
<!-- Defining number of rows -->
<attr name="numberOfRows" format="integer" />
</declare-styleable>
<declare-styleable name="lbVerticalGridView">
<!-- Defining width of each column of VerticalGridView -->
- <attr name="columnWidth" format="dimension" />
+ <attr name="columnWidth" format="dimension" >
+ <enum name="wrap_content" value="-2" />
+ </attr>
<!-- Defining number of columns -->
<attr name="numberOfColumns" format="integer" />
</declare-styleable>
@@ -109,6 +115,17 @@
</attr>
</declare-styleable>
+ <declare-styleable name="lbSearchOrbView">
+ <!-- Defining icon of the search affordance -->
+ <attr name="searchOrbIcon" format="reference"/>
+ <!-- Defining icon tint color of the search affordance -->
+ <attr name="searchOrbIconColor" format="reference|color"/>
+ <!-- Defining color of the search affordance -->
+ <attr name="searchOrbColor" format="reference|color" />
+ <!-- Defining pulse color of the search affordance -->
+ <attr name="searchOrbBrightColor" format="reference|color" />
+ </declare-styleable>
+
<declare-styleable name="LeanbackTheme">
<!-- left padding of BrowseFragment, RowsFragment, DetailsFragment -->
@@ -132,8 +149,13 @@
<!-- BrowseFragment Title icon style -->
<attr name="browseTitleIconStyle" format="reference" />
+ <!-- BrowseFragment TitleView style -->
+ <attr name="browseTitleViewStyle" format="reference" />
+
<!-- vertical grid style inside HeadersFragment -->
<attr name="headersVerticalGridStyle" format="reference" />
+ <!-- header style inside HeadersFragment -->
+ <attr name="headerStyle" format="reference" />
<!-- vertical grid style inside RowsFragment -->
<attr name="rowsVerticalGridStyle" format="reference" />
@@ -156,9 +178,24 @@
<attr name="detailsDescriptionTitleStyle" format="reference" />
<attr name="detailsDescriptionSubtitleStyle" format="reference" />
<attr name="detailsDescriptionBodyStyle" format="reference" />
+ <attr name="detailsActionButtonStyle" format="reference" />
<!-- style for a vertical grid of items -->
<attr name="itemsVerticalGridStyle" format="reference" />
+ <!-- for messages in the error fragment -->
+ <attr name="errorMessageStyle" format="reference" />
+
+ <!-- Default colors -->
+ <attr name="defaultBrandColor" format="reference|color" />
+ <attr name="defaultSearchColor" format="reference|color" />
+ <!-- Default color that search orb pulses to. If not set, this color is determined programatically based on the defaultSearchColor -->
+ <attr name="defaultSearchBrightColor" format="reference|color" />
+
+ <!-- Style for searchOrb -->
+ <attr name="searchOrbViewStyle" format="reference"/>
+ <attr name="defaultSearchIcon" format="reference" />
</declare-styleable>
+
+
</resources>
diff --git a/v17/leanback/res/values/colors.xml b/v17/leanback/res/values/colors.xml
index 0eeb825..5fc7e0a 100644
--- a/v17/leanback/res/values/colors.xml
+++ b/v17/leanback/res/values/colors.xml
@@ -19,24 +19,37 @@
<color name="lb_grey">#888888</color>
<color name="lb_browse_title_color">#EEEEEE</color>
- <color name="lb_browse_header_color">#EEEEEE</color>
+ <color name="lb_browse_header_color">#FFFFFF</color>
<color name="lb_list_item_unselected_text_color">#FFF1F1F1</color>
- <color name="lb_background_protection">#A0333333</color>
+ <color name="lb_background_protection">#99000000</color>
<color name="lb_view_dim_mask_color">#000000</color>
<item name="lb_view_dimmed_level" type="dimen">60%</item>
+ <color name="lb_details_overview_bg_color">#1B1B1B</color>
<color name="lb_details_description_color">#EEEEEE</color>
+ <color name="lb_error_background_color_opaque">#262626</color>
+ <color name="lb_error_background_color_translucent">#E6000000</color>
+ <color name="lb_error_message_color_on_opaque">#80EEEEEE</color>
+ <color name="lb_error_message_color_on_translucent">#80555555</color>
+
<color name="lb_action_text_color">#EEEEEE</color>
<color name="lb_action_bg_color">#3D3D3D</color>
<color name="lb_search_bar_text_color">#FFEEEEEE</color>
- <color name="lb_search_bar_hint_color">#33EEEEEE</color>
+ <color name="lb_search_bar_text_speech_color">#FF444444</color>
+ <color name="lb_search_bar_hint_color">#66222222</color>
- <color name="lb_basic_card_bg_color">#FF1B1B1B</color>
- <color name="lb_basic_card_info_bg_color">#FF1B1B1B</color>
+ <color name="lb_speech_orb_not_recording">#33EEEEEE</color>
+ <color name="lb_speech_orb_recording">#33EE0000</color>
+
+ <color name="lb_basic_card_bg_color">#FF3B3B3B</color>
+ <color name="lb_basic_card_info_bg_color">#FF3B3B3B</color>
<color name="lb_basic_card_title_text_color">#FFEEEEEE</color>
- <color name="lb_basic_card_content_text_color">#FFEEEEEE</color>
+ <color name="lb_basic_card_content_text_color">#FF888888</color>
+
+ <color name="lb_default_brand_color">#FF455A64</color>
+ <color name="lb_default_search_color">#FFFFAA3F</color>
</resources>
diff --git a/v17/leanback/res/values/dimens.xml b/v17/leanback/res/values/dimens.xml
index 61792fa..30bc8e8 100644
--- a/v17/leanback/res/values/dimens.xml
+++ b/v17/leanback/res/values/dimens.xml
@@ -15,103 +15,151 @@
limitations under the License.
-->
<resources>
+ <dimen name="lb_list_row_height">224dp</dimen>
+
<dimen name="lb_browse_padding_left">56dp</dimen>
<dimen name="lb_browse_padding_top">27dp</dimen>
<dimen name="lb_browse_padding_right">56dp</dimen>
<dimen name="lb_browse_padding_bottom">48dp</dimen>
<dimen name="lb_browse_rows_margin_start">238dp</dimen>
- <dimen name="lb_browse_rows_margin_top">120dp</dimen>
+ <dimen name="lb_browse_rows_margin_top">167dp</dimen>
<dimen name="lb_browse_rows_fading_edge">16dp</dimen>
+ <dimen name="lb_vertical_grid_padding_bottom">87dp</dimen>
<dimen name="lb_browse_title_height">60dp</dimen>
<dimen name="lb_browse_title_icon_height">52dp</dimen>
<dimen name="lb_browse_title_icon_width">52dp</dimen>
- <dimen name="lb_browse_title_icon_margin_right">24dp</dimen>
- <dimen name="lb_browse_title_text_size">28sp</dimen>
+ <dimen name="lb_browse_title_icon_margin_right">52dp</dimen>
+ <dimen name="lb_browse_title_text_size">44sp</dimen>
+ <dimen name="lb_browse_title_text_width">584dp</dimen>
- <integer name="lb_browse_headers_transition_delay">250</integer>
+ <dimen name="lb_browse_headers_width">270dp</dimen>
+ <integer name="lb_browse_headers_transition_delay">150</integer>
+ <integer name="lb_browse_headers_transition_duration">250</integer>
<integer name="lb_browse_rows_anim_duration">250</integer>
- <dimen name="lb_browse_headers_vertical_margin">12dp</dimen>
- <dimen name="lb_browse_header_height">48dp</dimen>
- <dimen name="lb_browse_header_half_height">24dp</dimen>
- <dimen name="lb_browse_header_text_size">22sp</dimen>
- <item name="lb_browse_header_select_duration" format="integer" type="dimen">150</item>
- <item name="lb_browse_header_unselect_alpha" format="float" type="dimen">0.5</item>
- <item name="lb_browse_header_select_scale" format="float" type="dimen">1.1</item>
+ <!-- Derived from the ux spec of 48dp baseline to baseline -->
+ <dimen name="lb_browse_headers_vertical_margin">21dp</dimen>
+ <dimen name="lb_browse_header_text_size">20sp</dimen>
+ <dimen name="lb_browse_header_padding_right">24dp</dimen>
+ <dimen name="lb_browse_header_height">24dp</dimen>
- <dimen name="lb_browse_row_list_height">224dp</dimen>
- <dimen name="lb_browse_row_title_height">24dp</dimen>
- <dimen name="lb_browse_row_hovercard_max_width">420dp</dimen>
+ <item name="lb_browse_header_select_duration" format="integer" type="dimen">150</item>
+ <item name="lb_browse_header_unselect_alpha" type="fraction">50%</item>
+ <item name="lb_browse_header_select_scale" format="float" type="dimen">1.2</item>
+
+ <dimen name="lb_browse_row_hovercard_max_width">400dp</dimen>
<dimen name="lb_browse_row_hovercard_title_font_size">18sp</dimen>
- <dimen name="lb_browse_row_hovercard_description_font_size">12sp</dimen>
- <dimen name="lb_browse_row_header_text_size">18sp</dimen>
- <dimen name="lb_browse_item_margin">12dp</dimen>
- <dimen name="lb_browse_item_margin_vertical">12dp</dimen>
- <dimen name="lb_browse_item_margin_horizontal">12dp</dimen>
+ <dimen name="lb_browse_row_hovercard_description_font_size">14sp</dimen>
+ <dimen name="lb_browse_item_horizontal_margin">8dp</dimen>
+ <dimen name="lb_browse_item_vertical_margin">8dp</dimen>
+ <dimen name="lb_browse_selected_row_top_padding">20dp</dimen>
+ <dimen name="lb_browse_expanded_selected_row_top_padding">16dp</dimen>
+ <dimen name="lb_browse_expanded_row_no_hovercard_bottom_padding">28dp</dimen>
<item name="lb_focus_zoom_factor_small" type="fraction">106%</item>
<item name="lb_focus_zoom_factor_medium" type="fraction">110%</item>
<item name="lb_focus_zoom_factor_large" type="fraction">114%</item>
- <dimen name="lb_details_overview_image_width">132dp</dimen>
- <dimen name="lb_details_overview_image_margin_left">132dp</dimen>
- <dimen name="lb_details_overview_description_intertext_spacing">16dp</dimen>
- <dimen name="lb_details_overview_description_margin_left">30dp</dimen>
- <dimen name="lb_details_overview_description_margin_right">132dp</dimen>
- <dimen name="lb_details_overiew_actions_margin_top">19dp</dimen>
- <dimen name="lb_details_overview_action_items_margin">32dp</dimen>
+ <dimen name="lb_details_overview_height_large">274dp</dimen>
+ <dimen name="lb_details_overview_height_small">159dp</dimen>
+ <dimen name="lb_details_overview_margin_left">132dp</dimen>
+ <dimen name="lb_details_overview_margin_right">132dp</dimen>
+ <dimen name="lb_details_overview_margin_bottom">40dp</dimen>
+
+ <dimen name="lb_details_overview_description_margin_top">24dp</dimen>
+ <dimen name="lb_details_overview_description_margin_left">24dp</dimen>
+ <dimen name="lb_details_overview_description_margin_right">24dp</dimen>
+ <dimen name="lb_details_overview_description_margin_bottom">12dp</dimen>
+ <dimen name="lb_details_overview_image_margin_horizontal">24dp</dimen>
+ <dimen name="lb_details_overview_image_margin_vertical">24dp</dimen>
+ <dimen name="lb_details_overview_action_items_margin">0dp</dimen>
<item name="lb_details_overview_action_select_duration" format="integer" type="dimen">150</item>
<dimen name="lb_details_overview_actions_padding_left">294dp</dimen>
<dimen name="lb_details_overview_actions_padding_right">132dp</dimen>
<dimen name="lb_details_overview_actions_height">56dp</dimen>
- <dimen name="lb_details_rows_align_top">120dp</dimen>
+ <dimen name="lb_details_overview_actions_fade_size">16dp</dimen>
+ <dimen name="lb_details_rows_align_top">167dp</dimen>
<dimen name="lb_details_description_title_text_size">34sp</dimen>
- <dimen name="lb_details_description_title_leading_space">42sp</dimen>
- <dimen name="lb_details_description_subtitle_text_size">14sp</dimen>
+ <dimen name="lb_details_description_subtitle_text_size">16sp</dimen>
<dimen name="lb_details_description_body_text_size">14sp</dimen>
+ <dimen name="lb_details_description_title_line_spacing">40dp</dimen>
+ <dimen name="lb_details_description_body_line_spacing">20dp</dimen>
+ <dimen name="lb_details_description_title_baseline">26dp</dimen>
+ <dimen name="lb_details_description_under_title_baseline_margin">32dp</dimen>
+ <dimen name="lb_details_description_under_subtitle_baseline_margin">32dp</dimen>
+
<integer name="lb_details_description_title_max_lines">2</integer>
<integer name="lb_details_description_subtitle_max_lines">1</integer>
<integer name="lb_details_description_body_max_lines">5</integer>
+ <integer name="lb_details_description_body_min_lines">3</integer>
<dimen name="lb_action_1_line_height">36dp</dimen>
- <dimen name="lb_action_1_line_padding_left">32dp</dimen>
- <dimen name="lb_action_2_lines_height">54dp</dimen>
- <dimen name="lb_action_padding_right">32dp</dimen>
+ <dimen name="lb_action_2_lines_height">56dp</dimen>
+ <dimen name="lb_action_padding_horizontal">34dp</dimen>
+ <dimen name="lb_action_with_icon_padding_left">14dp</dimen>
+ <dimen name="lb_action_with_icon_padding_right">20dp</dimen>
<dimen name="lb_action_icon_margin">12dp</dimen>
- <dimen name="lb_action_icon_width">30dp</dimen>
<dimen name="lb_action_text_size">16sp</dimen>
- <dimen name="lb_action_text_spacing">2sp</dimen>
+
+ <dimen name="lb_error_image_max_height">120dp</dimen>
+ <integer name="lb_error_message_max_lines">1</integer>
+ <dimen name="lb_error_message_max_width">600dp</dimen>
+ <dimen name="lb_error_message_text_size">16sp</dimen>
+ <dimen name="lb_error_under_image_baseline_margin">36dp</dimen>
+ <dimen name="lb_error_under_message_baseline_margin">24dp</dimen>
<!-- Search bar -->
<dimen name="lb_search_bar_height">60dp</dimen>
<dimen name="lb_search_bar_padding_left">56dp</dimen>
<dimen name="lb_search_bar_padding_top">27dp</dimen>
- <dimen name="lb_search_bar_text_size">28sp</dimen>
+ <dimen name="lb_search_bar_text_size">22sp</dimen>
+ <dimen name="lb_search_bar_unfocused_text_size">18sp</dimen>
<dimen name="lb_search_bar_items_layout_margin_top">27dp</dimen>
- <dimen name="lb_search_bar_items_width">660dp</dimen>
+ <dimen name="lb_search_bar_items_width">600dp</dimen>
+ <dimen name="lb_search_bar_items_height">56dp</dimen>
+ <dimen name="lb_search_bar_items_margin_left">56dp</dimen>
+ <dimen name="lb_search_bar_icon_height">32dp</dimen>
+ <dimen name="lb_search_bar_icon_width">32dp</dimen>
+ <dimen name="lb_search_bar_icon_margin_left">16dp</dimen>
+ <dimen name="lb_search_bar_edit_text_margin_left">24dp</dimen>
+ <dimen name="lb_search_bar_hint_margin_left">52dp</dimen>
+
<!-- Search Fragment -->
<dimen name="lb_search_browse_rows_align_top">120dp</dimen>
<dimen name="lb_search_browse_row_padding_left">56dp</dimen>
<dimen name="lb_search_orb_size">52dp</dimen>
+ <item name="lb_search_orb_focused_zoom" type="fraction">120%</item>
+ <item name="lb_search_orb_pulse_duration_ms" type="integer">1000</item>
+ <item name="lb_search_orb_scale_down_duration_ms" type="integer">100</item>
<dimen name="lb_search_orb_margin_top">4dp</dimen>
<dimen name="lb_search_orb_margin_bottom">4dp</dimen>
<dimen name="lb_search_orb_margin_left">4dp</dimen>
<dimen name="lb_search_orb_margin_right">4dp</dimen>
+ <dimen name="lb_search_bar_speech_orb_size">52dp</dimen>
+ <item name="lb_search_bar_speech_orb_focused_zoom" type="fraction">120%</item>
+ <item name="lb_search_bar_speech_orb_max_level_zoom" type="fraction">144%</item>
+ <dimen name="lb_search_bar_speech_orb_margin_left">56dp</dimen>
+
<!-- BasicCardView -->
<dimen name="lb_basic_card_main_width">140dp</dimen>
<dimen name="lb_basic_card_main_height">188dp</dimen>
<dimen name="lb_basic_card_info_height">52dp</dimen>
<dimen name="lb_basic_card_info_padding">6dp</dimen>
<dimen name="lb_basic_card_info_text_margin">2dp</dimen>
- <dimen name="lb_basic_card_title_text_size">14sp</dimen>
- <dimen name="lb_basic_card_content_text_size">10sp</dimen>
+ <dimen name="lb_basic_card_title_text_size">16sp</dimen>
+ <dimen name="lb_basic_card_content_text_size">12sp</dimen>
<dimen name="lb_basic_card_info_badge_size">16dp</dimen>
+
+ <!-- z based shadow -->
+ <dimen name="lb_material_shadow_normal_z">2dp</dimen>
+ <dimen name="lb_material_shadow_focused_z">8dp</dimen>
+
</resources>
diff --git a/v17/leanback/res/values/ids.xml b/v17/leanback/res/values/ids.xml
index 4010b2c..05a6304 100644
--- a/v17/leanback/res/values/ids.xml
+++ b/v17/leanback/res/values/ids.xml
@@ -16,4 +16,5 @@
-->
<resources>
<item type="id" name="lb_focus_animator" />
+ <item type="id" name="lb_header_transition_position" />
</resources>
\ No newline at end of file
diff --git a/v17/leanback/res/values/integers.xml b/v17/leanback/res/values/integers.xml
index 4fbe83f..ff2cf89 100644
--- a/v17/leanback/res/values/integers.xml
+++ b/v17/leanback/res/values/integers.xml
@@ -17,4 +17,6 @@
<integer name="lb_card_selected_animation_delay">400</integer>
<integer name="lb_card_selected_animation_duration">150</integer>
<integer name="lb_card_activated_animation_duration">150</integer>
+ <integer name="lb_search_bar_text_mode_background_alpha">51</integer>
+ <integer name="lb_search_bar_speech_mode_background_alpha">179</integer>
</resources>
diff --git a/v17/leanback/res/values/strings.xml b/v17/leanback/res/values/strings.xml
index 333c06a..bad8000 100644
--- a/v17/leanback/res/values/strings.xml
+++ b/v17/leanback/res/values/strings.xml
@@ -14,8 +14,15 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<resources>
- <string name="orb_search_label">Search</string>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Image description for the call to search action visible when browsing content [CHAR LIMIT=40] -->
<string name="orb_search_action">Search Action</string>
+ <!-- Hint showing in the empty search bar [CHAR LIMIT=40] -->
<string name="lb_search_bar_hint">Search</string>
-</resources>
\ No newline at end of file
+ <!-- Hint showing in the empty search bar when using the voice input [CHAR LIMIT=40] -->
+ <string name="lb_search_bar_hint_speech">Speak to search</string>
+ <!-- Hint showing in the empty search bar using a provided context (usually the application name) [CHAR LIMIT=40] -->
+ <string name="lb_search_bar_hint_with_title">Search <xliff:g id="search context">%1$s</xliff:g></string>
+ <!-- Hint showing in the empty search bar using a provided context (usually the application name) while in voice input mode [CHAR LIMIT=40] -->
+ <string name="lb_search_bar_hint_with_title_speech">Speak to search <xliff:g id="search context">%1$s</xliff:g></string>
+</resources>
diff --git a/v17/leanback/res/values/styles.xml b/v17/leanback/res/values/styles.xml
index 955e9fe..1d43795 100644
--- a/v17/leanback/res/values/styles.xml
+++ b/v17/leanback/res/values/styles.xml
@@ -22,24 +22,18 @@
</style>
<style name="TextAppearance.Leanback.Title" parent="TextAppearance.Leanback">
+ <item name="android:fontFamily">sans-serif-light</item>
<item name="android:textSize">@dimen/lb_browse_title_text_size</item>
<item name="android:textColor">@color/lb_browse_title_color</item>
</style>
- <style name="TextAppearance.Leanback.Row.Header" parent="TextAppearance.Leanback">
- <item name="android:textAllCaps">true</item>
- <item name="android:textSize">@dimen/lb_browse_row_header_text_size</item>
- <item name="android:textColor">@color/lb_browse_header_color</item>
- </style>
-
<style name="TextAppearance.Leanback.Header" parent="TextAppearance.Leanback">
- <item name="android:textAllCaps">true</item>
<item name="android:textSize">@dimen/lb_browse_header_text_size</item>
+ <item name="android:textColor">@color/lb_browse_header_color</item>
</style>
- <style name="TextAppearance.Leanback.SearchLabel" parent="TextAppearance.Leanback">
- <item name="android:textSize">@dimen/lb_browse_header_text_size</item>
- <item name="android:textColor">@color/lb_list_item_unselected_text_color</item>
+ <style name="TextAppearance.Leanback.Row.Header" parent="TextAppearance.Leanback.Header">
+ <item name="android:fontFamily">sans-serif</item>
</style>
<style name="TextAppearance.Leanback.SearchTextEdit" parent="TextAppearance.Leanback">
@@ -49,13 +43,13 @@
<style name="TextAppearance.Leanback.DetailsDescriptionTitle">
<item name="android:textSize">@dimen/lb_details_description_title_text_size</item>
<item name="android:textColor">@color/lb_details_description_color</item>
- <item name="android:fontFamily">sans-serif-condensed</item>
+ <item name="android:fontFamily">sans-serif</item>
</style>
<style name="TextAppearance.Leanback.DetailsDescriptionSubtitle">
<item name="android:textSize">@dimen/lb_details_description_subtitle_text_size</item>
<item name="android:textColor">@color/lb_details_description_color</item>
- <item name="android:fontFamily">sans-serif-condensed</item>
+ <item name="android:fontFamily">sans-serif</item>
</style>
<style name="TextAppearance.Leanback.DetailsDescriptionBody">
@@ -64,6 +58,18 @@
<item name="android:fontFamily">sans-serif</item>
</style>
+ <style name="TextAppearance.Leanback.DetailsActionButton">
+ <item name="android:textSize">@dimen/lb_action_text_size</item>
+ <item name="android:textColor">@color/lb_action_text_color</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
+
+ <style name="TextAppearance.Leanback.ErrorMessage">
+ <item name="android:textSize">@dimen/lb_error_message_text_size</item>
+ <item name="android:textColor">@color/lb_error_message_color_on_opaque</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+
<style name="Widget.Leanback" parent="android:Widget.Holo" />
<style name="Widget.Leanback.BaseCardViewStyle" />
@@ -74,19 +80,23 @@
<item name="android:background">@color/lb_basic_card_bg_color</item>
</style>
+ <style name="Widget.Leanback.TitleView" >
+ </style>
+
<style name="Widget.Leanback.Title" />
<style name="Widget.Leanback.Title.Text">
- <item name="android:gravity">center_vertical</item>
<item name="android:singleLine">true</item>
+ <item name="android:gravity">right</item>
+ <item name="android:ellipsize">end</item>
<item name="android:textAppearance">@style/TextAppearance.Leanback.Title</item>
</style>
<style name="Widget.Leanback.Title.Icon">
- <item name="android:scaleType">centerInside</item>
+ <item name="android:scaleType">fitEnd</item>
</style>
- <!-- HeadersFragment (fast lane) -->
+ <!-- HeadersFragment -->
<style name="Widget.Leanback.Headers" />
<!-- RowsFragment -->
@@ -110,11 +120,12 @@
</style>
<style name="Widget.Leanback.Header" >
- <item name="android:gravity">center_vertical</item>
+ <item name="android:minHeight">@dimen/lb_browse_header_height</item>
<item name="android:textAppearance">@style/TextAppearance.Leanback.Header</item>
<item name="android:singleLine">true</item>
- <item name="android:focusable">true</item>
- <item name="android:focusableInTouchMode">true</item>
+ <item name="android:ellipsize">marquee</item>
+ <item name="android:marqueeRepeatLimit">0</item>
+ <item name="android:paddingRight">@dimen/lb_browse_header_padding_right</item>
</style>
<style name="Widget.Leanback.Rows.VerticalGridView" >
@@ -132,11 +143,12 @@
<item name="android:focusableInTouchMode">true</item>
<item name="android:paddingLeft">?attr/browsePaddingLeft</item>
<item name="android:paddingRight">?attr/browsePaddingRight</item>
- <item name="android:paddingBottom">@dimen/lb_browse_item_margin_vertical</item>
- <item name="android:paddingTop">@dimen/lb_browse_item_margin_vertical</item>
- <item name="horizontalMargin">@dimen/lb_browse_item_margin</item>
- <item name="verticalMargin">@dimen/lb_browse_item_margin</item>
+ <item name="android:paddingBottom">@dimen/lb_browse_item_vertical_margin</item>
+ <item name="android:paddingTop">@dimen/lb_browse_item_vertical_margin</item>
+ <item name="horizontalMargin">@dimen/lb_browse_item_horizontal_margin</item>
+ <item name="verticalMargin">@dimen/lb_browse_item_vertical_margin</item>
<item name="focusOutFront">true</item>
+ <item name="rowHeight">wrap_content</item>
</style>
<style name="Widget.Leanback.GridItems.VerticalGridView">
@@ -145,15 +157,16 @@
<item name="android:focusableInTouchMode">true</item>
<item name="android:paddingLeft">?attr/browsePaddingLeft</item>
<item name="android:paddingRight">?attr/browsePaddingRight</item>
- <item name="android:paddingBottom">@dimen/lb_browse_item_margin_vertical</item>
- <item name="android:paddingTop">@dimen/lb_browse_item_margin_vertical</item>
- <item name="horizontalMargin">@dimen/lb_browse_item_margin</item>
- <item name="verticalMargin">@dimen/lb_browse_item_margin</item>
+ <item name="android:paddingBottom">@dimen/lb_vertical_grid_padding_bottom</item>
+ <item name="android:paddingTop">?attr/browseRowsMarginTop</item>
+ <item name="android:gravity">center_horizontal</item>
+ <item name="horizontalMargin">@dimen/lb_browse_item_horizontal_margin</item>
+ <item name="verticalMargin">@dimen/lb_browse_item_vertical_margin</item>
+ <item name="columnWidth">wrap_content</item>
<item name="focusOutFront">true</item>
</style>
- <style name="Widget.Leanback.Row.Header">
- <item name="android:minHeight">@dimen/lb_browse_row_title_height</item>
+ <style name="Widget.Leanback.Row.Header" parent="Widget.Leanback.Header">
<item name="android:textAppearance">@style/TextAppearance.Leanback.Row.Header</item>
</style>
@@ -176,21 +189,49 @@
<item name="android:textAppearance">@style/TextAppearance.Leanback.Row.HoverCardDescription</item>
<item name="android:maxWidth">@dimen/lb_browse_row_hovercard_max_width</item>
<item name="android:ellipsize">end</item>
- <item name="android:maxLines">2</item>
+ <item name="android:maxLines">4</item>
</style>
<style name="Widget.Leanback.DetailsDescriptionTitleStyle">
<item name="android:textAppearance">@style/TextAppearance.Leanback.DetailsDescriptionTitle</item>
<item name="android:maxLines">@integer/lb_details_description_title_max_lines</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:ellipsize">end</item>
</style>
<style name="Widget.Leanback.DetailsDescriptionSubtitleStyle">
<item name="android:textAppearance">@style/TextAppearance.Leanback.DetailsDescriptionSubtitle</item>
<item name="android:maxLines">@integer/lb_details_description_subtitle_max_lines</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:ellipsize">end</item>
</style>
<style name="Widget.Leanback.DetailsDescriptionBodyStyle">
<item name="android:textAppearance">@style/TextAppearance.Leanback.DetailsDescriptionBody</item>
- <item name="android:maxLines">@integer/lb_details_description_body_max_lines</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+
+ <style name="Widget.Leanback.DetailsActionButtonStyle" parent="android:Widget.Holo.Button.Borderless">
+ <item name="android:textAppearance">@style/TextAppearance.Leanback.DetailsActionButton</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:drawablePadding">@dimen/lb_action_icon_margin</item>
+ <item name="android:focusable">true</item>
+ <item name="android:focusableInTouchMode">true</item>
+ <item name="android:paddingLeft">@dimen/lb_action_padding_horizontal</item>
+ <item name="android:paddingRight">@dimen/lb_action_padding_horizontal</item>
+ </style>
+
+ <style name="Widget.Leanback.ErrorMessageStyle">
+ <item name="android:textAppearance">@style/TextAppearance.Leanback.ErrorMessage</item>
+ <item name="android:includeFontPadding">false</item>
+ <item name="android:maxLines">@integer/lb_error_message_max_lines</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+
+ <style name="Widget.Leanback.SearchOrbViewStyle">
+ <item name="searchOrbIcon">?attr/defaultSearchIcon</item>
+ <item name="searchOrbColor">?attr/defaultSearchColor</item>
+ <item name="searchOrbBrightColor">?attr/defaultSearchBrightColor</item>
</style>
</resources>
diff --git a/v17/leanback/res/values/themes.xml b/v17/leanback/res/values/themes.xml
index b683b28..fee4967 100644
--- a/v17/leanback/res/values/themes.xml
+++ b/v17/leanback/res/values/themes.xml
@@ -17,7 +17,11 @@
<resources>
- <style name="Theme.Leanback" parent="android:Theme.Holo.NoActionBar">
+ <!-- LeanbackBase may be overridden for specific api levels -->
+ <style name="Theme.LeanbackBase" parent="android:Theme.Holo.NoActionBar">
+ </style>
+
+ <style name="Theme.Leanback" parent="Theme.LeanbackBase">
<item name="android:windowOverscan">true</item>
@@ -33,19 +37,35 @@
<item name="browseRowsFadingEdgeLength">@dimen/lb_browse_rows_fading_edge</item>
<item name="headersVerticalGridStyle">@style/Widget.Leanback.Headers.VerticalGridView</item>
+ <item name="headerStyle">@style/Widget.Leanback.Header</item>
+
<item name="rowsVerticalGridStyle">@style/Widget.Leanback.Rows.VerticalGridView</item>
<item name="rowHorizontalGridStyle">@style/Widget.Leanback.Row.HorizontalGridView</item>
<item name="itemsVerticalGridStyle">@style/Widget.Leanback.GridItems.VerticalGridView</item>
<item name="browseTitleTextStyle">@style/Widget.Leanback.Title.Text</item>
<item name="browseTitleIconStyle">@style/Widget.Leanback.Title.Icon</item>
+ <item name="browseTitleViewStyle">@style/Widget.Leanback.TitleView</item>
+
<item name="rowHeaderStyle">@style/Widget.Leanback.Row.Header</item>
<item name="rowHoverCardTitleStyle">@style/Widget.Leanback.Row.HoverCardTitle</item>
<item name="rowHoverCardDescriptionStyle">@style/Widget.Leanback.Row.HoverCardDescription</item>
+ <item name="searchOrbViewStyle">@style/Widget.Leanback.SearchOrbViewStyle</item>
+
<item name="detailsDescriptionTitleStyle">@style/Widget.Leanback.DetailsDescriptionTitleStyle</item>
<item name="detailsDescriptionSubtitleStyle">@style/Widget.Leanback.DetailsDescriptionSubtitleStyle</item>
<item name="detailsDescriptionBodyStyle">@style/Widget.Leanback.DetailsDescriptionBodyStyle</item>
+ <item name="detailsActionButtonStyle">@style/Widget.Leanback.DetailsActionButtonStyle</item>
+
+ <item name="errorMessageStyle">@style/Widget.Leanback.ErrorMessageStyle</item>
+
+ <!-- TODO should be null, and set programatically to avoid dependence on leanback theme -->
+ <item name="defaultBrandColor">@color/lb_default_brand_color</item>
+ <item name="defaultSearchColor">@null</item>
+ <item name="defaultSearchBrightColor">@null</item>
+ <item name="defaultSearchIcon">@null</item>
+
</style>
</resources>
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java b/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
index 8b56775..9e4ac26 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
@@ -14,25 +14,29 @@
package android.support.v17.leanback.app;
import android.support.v17.leanback.R;
-import android.animation.ObjectAnimator;
+import android.animation.Animator;
+import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
+import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.ColorFilter;
import android.graphics.Matrix;
-import android.graphics.drawable.BitmapDrawable;
+import android.graphics.Paint;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Handler;
import android.util.Log;
-import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
+import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
/**
@@ -75,8 +79,7 @@
private static final int FULL_ALPHA = 255;
private static final int DIM_ALPHA_ON_SOLID = (int) (0.8f * FULL_ALPHA);
private static final int CHANGE_BG_DELAY_MS = 500;
- private static final int FADE_DURATION_QUICK = 200;
- private static final int FADE_DURATION_SLOW = 1000;
+ private static final int FADE_DURATION = 500;
/**
* Using a separate window for backgrounds can improve graphics performance by
@@ -85,15 +88,6 @@
*/
private static final boolean USE_SEPARATE_WINDOW = false;
- /**
- * If true, bitmaps will be scaled to the exact display size.
- * Small bitmaps will be scaled up, using more memory but improving display quality.
- * Large bitmaps will be scaled down to use less memory.
- * Introduces an allocation overhead.
- * TODO: support a leanback configuration option.
- */
- private static final boolean SCALE_BITMAPS_TO_FIT = true;
-
private static final String WINDOW_NAME = "BackgroundManager";
private static final String FRAGMENT_TAG = BackgroundManager.class.getCanonicalName();
@@ -111,12 +105,88 @@
private int mBackgroundColor;
private boolean mAttached;
- private class DrawableWrapper {
+ private static class BitmapDrawable extends Drawable {
+
+ static class ConstantState extends Drawable.ConstantState {
+ Bitmap mBitmap;
+ Matrix mMatrix;
+ Paint mPaint;
+
+ @Override
+ public Drawable newDrawable() {
+ return new BitmapDrawable(null, mBitmap, mMatrix);
+ }
+
+ @Override
+ public int getChangingConfigurations() {
+ return 0;
+ }
+ }
+
+ private ConstantState mState = new ConstantState();
+
+ BitmapDrawable(Resources resources, Bitmap bitmap) {
+ this(resources, bitmap, null);
+ }
+
+ BitmapDrawable(Resources resources, Bitmap bitmap, Matrix matrix) {
+ mState.mBitmap = bitmap;
+ mState.mMatrix = matrix != null ? matrix : new Matrix();
+ mState.mPaint = new Paint();
+ mState.mPaint.setFilterBitmap(true);
+ }
+
+ Bitmap getBitmap() {
+ return mState.mBitmap;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mState.mBitmap == null) {
+ return;
+ }
+ canvas.drawBitmap(mState.mBitmap, mState.mMatrix, mState.mPaint);
+ }
+
+ @Override
+ public int getOpacity() {
+ return android.graphics.PixelFormat.OPAQUE;
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ if (mState.mPaint.getAlpha() != alpha) {
+ mState.mPaint.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ // Abstract in Drawable, not implemented
+ }
+
+ @Override
+ public ConstantState getConstantState() {
+ return mState;
+ }
+ }
+
+ private static class DrawableWrapper {
protected int mAlpha;
protected Drawable mDrawable;
- protected ObjectAnimator mAnimator;
+ protected ValueAnimator mAnimator;
protected boolean mAnimationPending;
+ private final Interpolator mInterpolator = new LinearInterpolator();
+ private final ValueAnimator.AnimatorUpdateListener mAnimationUpdateListener =
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setAlpha((Integer) animation.getAnimatedValue());
+ }
+ };
+
public DrawableWrapper(Drawable drawable) {
mDrawable = drawable;
setAlpha(FULL_ALPHA);
@@ -145,8 +215,9 @@
if (mAnimator != null && mAnimator.isStarted()) {
mAnimator.cancel();
}
- mAnimator = ObjectAnimator.ofInt(this, "alpha", alpha);
- mAnimator.setInterpolator(new LinearInterpolator());
+ mAnimator = ValueAnimator.ofInt(getAlpha(), alpha);
+ mAnimator.addUpdateListener(mAnimationUpdateListener);
+ mAnimator.setInterpolator(mInterpolator);
mAnimator.setDuration(durationMs);
mAnimator.setStartDelay(delayMs);
mAnimationPending = true;
@@ -158,6 +229,12 @@
return mAnimator != null && mAnimator.isStarted();
}
public void startAnimation() {
+ startAnimation(null);
+ }
+ public void startAnimation(Animator.AnimatorListener listener) {
+ if (listener != null) {
+ mAnimator.addListener(listener);
+ }
mAnimator.start();
mAnimationPending = false;
}
@@ -254,13 +331,7 @@
return new BackgroundManager(activity);
}
- /**
- * Construct a BackgroundManager instance. The Initial background is set
- * from the continuity service.
- * @deprecated Use getInstance(Activity).
- */
- @Deprecated
- public BackgroundManager(Activity activity) {
+ private BackgroundManager(Activity activity) {
mContext = activity;
mService = BackgroundContinuityService.getInstance();
mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels;
@@ -302,10 +373,11 @@
return;
}
if (mLayerDrawable == null) {
- if (DEBUG) Log.v(TAG, "onActivityResume: released state, syncing with service");
+ if (DEBUG) Log.v(TAG, "onActivityResume " + this +
+ " released state, syncing with service");
syncWithService();
} else {
- if (DEBUG) Log.v(TAG, "onActivityResume: updating service color "
+ if (DEBUG) Log.v(TAG, "onActivityResume " + this + " updating service color "
+ mBackgroundColor + " drawable " + mBackgroundDrawable);
mService.setColor(mBackgroundColor);
mService.setDrawable(mBackgroundDrawable);
@@ -319,12 +391,9 @@
if (DEBUG) Log.v(TAG, "syncWithService color " + Integer.toHexString(color)
+ " drawable " + drawable);
- if (drawable != null) {
- drawable = drawable.getConstantState().newDrawable(mContext.getResources()).mutate();
- }
-
mBackgroundColor = color;
- mBackgroundDrawable = drawable;
+ mBackgroundDrawable = drawable == null ? null :
+ drawable.getConstantState().newDrawable().mutate();
updateImmediate();
}
@@ -335,7 +404,7 @@
}
mLayerDrawable = (LayerDrawable) mContext.getResources().getDrawable(
- R.drawable.lb_background);
+ R.drawable.lb_background).mutate();
mBgView.setBackground(mLayerDrawable);
mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable());
@@ -394,7 +463,7 @@
* @hide
*/
void detach() {
- if (DEBUG) Log.v(TAG, "detach");
+ if (DEBUG) Log.v(TAG, "detach " + this);
release();
if (mWindowManager != null && mBgView != null) {
@@ -422,7 +491,7 @@
* inherits the current state from the continuity service.
*/
public void release() {
- if (DEBUG) Log.v(TAG, "release");
+ if (DEBUG) Log.v(TAG, "release " + this);
if (mLayerDrawable != null) {
mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable());
@@ -501,11 +570,19 @@
}
if (mChangeRunnable != null) {
+ if (sameDrawable(drawable, mChangeRunnable.mDrawable)) {
+ if (DEBUG) Log.v(TAG, "setting same drawable");
+ return;
+ }
mChangeRunnable.cancel();
}
mChangeRunnable = new ChangeBackgroundRunnable(drawable);
- mHandler.postDelayed(mChangeRunnable, CHANGE_BG_DELAY_MS);
+ if (mImageInWrapper != null && mImageInWrapper.isAnimationStarted()) {
+ if (DEBUG) Log.v(TAG, "animation in progress");
+ } else {
+ mHandler.postDelayed(mChangeRunnable, CHANGE_BG_DELAY_MS);
+ }
}
/**
@@ -531,52 +608,32 @@
return;
}
- if (mBackgroundDrawable instanceof BitmapDrawable &&
- ((BitmapDrawable) mBackgroundDrawable).getBitmap() == bitmap) {
- if (DEBUG) {
- Log.v(TAG, "same bitmap detected");
- }
- mService.setDrawable(mBackgroundDrawable);
- return;
- }
+ Matrix matrix = null;
- if (SCALE_BITMAPS_TO_FIT &&
- (bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) {
- // Scale proportionately to fit width and height.
-
- Matrix matrix = new Matrix();
-
+ if ((bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) {
int dwidth = bitmap.getWidth();
int dheight = bitmap.getHeight();
float scale;
- int dx;
- if (DEBUG) {
- Log.v(TAG, "original image size " + dwidth + "x" + dheight);
- }
-
+ // Scale proportionately to fit width and height.
if (dwidth * mHeightPx > mWidthPx * dheight) {
scale = (float) mHeightPx / (float) dheight;
} else {
scale = (float) mWidthPx / (float) dwidth;
}
- matrix.setScale(scale, scale);
-
- if (DEBUG) {
- Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight());
- }
int subX = Math.min((int) (mWidthPx / scale), dwidth);
- int subY = Math.min((int) (mHeightPx / scale), dheight);
- dx = Math.max(0, (dwidth - subX) / 2);
+ int dx = Math.max(0, (dwidth - subX) / 2);
- bitmap = Bitmap.createBitmap(bitmap, dx, 0, subX, subY, matrix, true);
- if (DEBUG) {
- Log.v(TAG, "new image size " + bitmap.getWidth() + "x" + bitmap.getHeight());
- }
+ matrix = new Matrix();
+ matrix.setScale(scale, scale);
+ matrix.preTranslate(-dx, 0);
+
+ if (DEBUG) Log.v(TAG, "original image size " + bitmap.getWidth() + "x" + bitmap.getHeight() +
+ " scale " + scale + " dx " + dx);
}
- BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
+ BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap, matrix);
setDrawableInternal(bitmapDrawable);
}
@@ -603,18 +660,37 @@
mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable);
if (DEBUG) Log.v(TAG, "mImageInWrapper animation starting");
mImageInWrapper.setAlpha(0);
- mImageInWrapper.fadeIn(FADE_DURATION_SLOW, 0);
- mImageInWrapper.startAnimation();
+ mImageInWrapper.fadeIn(FADE_DURATION, 0);
+ mImageInWrapper.startAnimation(mImageInListener);
dimAlpha = FULL_ALPHA;
}
if (mDimWrapper != null && dimAlpha != 0) {
if (DEBUG) Log.v(TAG, "dimwrapper animation starting to " + dimAlpha);
- mDimWrapper.fade(FADE_DURATION_SLOW, 0, dimAlpha);
+ mDimWrapper.fade(FADE_DURATION, 0, dimAlpha);
mDimWrapper.startAnimation();
}
}
+ private final Animator.AnimatorListener mImageInListener = new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mChangeRunnable != null) {
+ if (DEBUG) Log.v(TAG, "animation ended, found change runnable");
+ mChangeRunnable.run();
+ }
+ }
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+ };
+
/**
* Returns the current background color.
*/
@@ -629,6 +705,21 @@
return mBackgroundDrawable;
}
+ private boolean sameDrawable(Drawable first, Drawable second) {
+ if (first == null || second == null) {
+ return false;
+ }
+ if (first == second) {
+ return true;
+ }
+ if (first instanceof BitmapDrawable && second instanceof BitmapDrawable) {
+ if (((BitmapDrawable) first).getBitmap().sameAs(((BitmapDrawable) second).getBitmap())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Task which changes the background.
*/
@@ -652,22 +743,10 @@
}
private void runTask() {
- boolean newBackground = false;
lazyInit();
- if (mDrawable != mBackgroundDrawable) {
- newBackground = true;
- if (mDrawable instanceof BitmapDrawable &&
- mBackgroundDrawable instanceof BitmapDrawable) {
- if (((BitmapDrawable) mDrawable).getBitmap() ==
- ((BitmapDrawable) mBackgroundDrawable).getBitmap()) {
- if (DEBUG) Log.v(TAG, "same underlying bitmap detected");
- newBackground = false;
- }
- }
- }
-
- if (!newBackground) {
+ if (sameDrawable(mDrawable, mBackgroundDrawable)) {
+ if (DEBUG) Log.v(TAG, "same bitmap detected");
return;
}
@@ -676,7 +755,7 @@
if (mImageInWrapper != null) {
mImageOutWrapper = new DrawableWrapper(mImageInWrapper.getDrawable());
mImageOutWrapper.setAlpha(mImageInWrapper.getAlpha());
- mImageOutWrapper.fadeOut(FADE_DURATION_QUICK);
+ mImageOutWrapper.fadeOut(FADE_DURATION);
// Order is important! Setting a drawable "removes" the
// previous one from the view
@@ -691,6 +770,8 @@
mService.setDrawable(mBackgroundDrawable);
applyBackgroundChanges();
+
+ mChangeRunnable = null;
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
index 2be3e54..2336690 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
@@ -35,6 +35,8 @@
private PresenterSelector mPresenterSelector;
private ItemBridgeAdapter mBridgeAdapter;
private int mSelectedPosition = -1;
+ protected int mReparentHeaderId;
+ protected boolean mInTransition;
abstract protected int getLayoutResourceId();
@@ -144,4 +146,45 @@
return null;
}
}
+
+ void setReparentHeaderId(int reparentId) {
+ mReparentHeaderId = reparentId;
+ }
+
+ void onTransitionStart() {
+ mInTransition = true;
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setAnimateChildLayout(false);
+ mVerticalGridView.setPruneChild(false);
+ mVerticalGridView.setFocusSearchDisabled(true);
+ }
+ }
+
+ void onTransitionEnd() {
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setAnimateChildLayout(true);
+ mVerticalGridView.setPruneChild(true);
+ mVerticalGridView.setFocusSearchDisabled(false);
+ }
+ mInTransition = false;
+ }
+
+ void setItemAlignment() {
+ if (mVerticalGridView != null) {
+ // align the top edge of item
+ mVerticalGridView.setItemAlignmentOffset(0);
+ mVerticalGridView.setItemAlignmentOffsetPercent(
+ VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ }
+ }
+
+ void setWindowAlignmentFromTop(int alignedTop) {
+ if (mVerticalGridView != null) {
+ // align to a fixed position from top
+ mVerticalGridView.setWindowAlignmentOffset(alignedTop);
+ mVerticalGridView.setWindowAlignmentOffsetPercent(
+ VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ mVerticalGridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
+ }
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
index 0df07f4..949a35f 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
@@ -15,7 +15,13 @@
import android.support.v17.leanback.R;
import android.support.v17.leanback.widget.HorizontalGridView;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.TitleView;
import android.support.v17.leanback.widget.VerticalGridView;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.ObjectAdapter;
@@ -23,8 +29,10 @@
import android.support.v17.leanback.widget.OnItemClickedListener;
import android.support.v17.leanback.widget.SearchOrbView;
import android.util.Log;
-import android.util.SparseIntArray;
+import android.app.Activity;
import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentManager.BackStackEntry;
import android.content.res.TypedArray;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -32,63 +40,151 @@
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
-import android.widget.ImageView;
-import android.widget.TextView;
+import android.graphics.Color;
+import android.graphics.Rect;
import android.graphics.drawable.Drawable;
-import java.util.ArrayList;
-
import static android.support.v7.widget.RecyclerView.NO_POSITION;
/**
- * Wrapper fragment for leanback browse screens. Composed of a
+ * A fragment for creating Leanback browse screens. It is composed of a
* RowsFragment and a HeadersFragment.
- *
+ * <p>
+ * A BrowseFragment renders the elements of its {@link ObjectAdapter} as a set
+ * of rows in a vertical list. The elements in this adapter must be subclasses
+ * of {@link Row}.
+ * <p>
+ * The HeadersFragment can be set to be either shown or hidden by default, or
+ * may be disabled entirely. See {@link #setHeadersState} for details.
+ * <p>
+ * By default the BrowseFragment includes support for returning to the headers
+ * when the user presses Back. For Activities that customize {@link
+ * Activity#onBackPressed()}, you must disable this default Back key support by
+ * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and
+ * use {@link BrowseFragment.BrowseTransitionListener} and
+ * {@link #startHeadersTransition(boolean)}.
*/
public class BrowseFragment extends Fragment {
+
+ final class BackStackListener implements FragmentManager.OnBackStackChangedListener {
+ int mLastEntryCount;
+ int mIndexOfHeadersBackStack;
+
+ BackStackListener() {
+ mLastEntryCount = getFragmentManager().getBackStackEntryCount();
+ mIndexOfHeadersBackStack = -1;
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ if (getFragmentManager() == null) {
+ Log.w(TAG, "getFragmentManager() is null, stack:", new Exception());
+ return;
+ }
+ int count = getFragmentManager().getBackStackEntryCount();
+ // if backstack is growing and last pushed entry is "headers" backstack,
+ // remember the index of the entry.
+ if (count > mLastEntryCount) {
+ BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
+ if (mWithHeadersBackStackName.equals(entry.getName())) {
+ mIndexOfHeadersBackStack = count - 1;
+ }
+ } else if (count < mLastEntryCount) {
+ // if popped "headers" backstack, initiate the show header transition if needed
+ if (mIndexOfHeadersBackStack >= count) {
+ if (!mShowingHeaders) {
+ startHeadersTransitionInternal(true);
+ }
+ }
+ }
+ mLastEntryCount = count;
+ }
+ }
+
+ /**
+ * Listener for transitions between browse headers and rows.
+ */
+ public static class BrowseTransitionListener {
+ /**
+ * Callback when headers transition starts.
+ *
+ * @param withHeaders True if the transition will result in headers
+ * being shown, false otherwise.
+ */
+ public void onHeadersTransitionStart(boolean withHeaders) {
+ }
+ /**
+ * Callback when headers transition stops.
+ *
+ * @param withHeaders True if the transition will result in headers
+ * being shown, false otherwise.
+ */
+ public void onHeadersTransitionStop(boolean withHeaders) {
+ }
+ }
+
private static final String TAG = "BrowseFragment";
+
+ private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_";
+
private static boolean DEBUG = false;
- /** The fastlane navigation panel is enabled and shown by default. */
+ /** The headers fragment is enabled and shown by default. */
public static final int HEADERS_ENABLED = 1;
- /** The fastlane navigation panel is enabled and hidden by default. */
+ /** The headers fragment is enabled and hidden by default. */
public static final int HEADERS_HIDDEN = 2;
- /** The fastlane navigation panel is disabled and will never be shown. */
+ /** The headers fragment is disabled and will never be shown. */
public static final int HEADERS_DISABLED = 3;
+ private static final float SLIDE_DISTANCE_FACTOR = 2;
+
private RowsFragment mRowsFragment;
private HeadersFragment mHeadersFragment;
private ObjectAdapter mAdapter;
- private Params mParams;
+ private String mTitle;
+ private Drawable mBadgeDrawable;
+ private int mHeadersState = HEADERS_ENABLED;
+ private int mBrandColor = Color.TRANSPARENT;
+ private boolean mBrandColorSet;
+
private BrowseFrameLayout mBrowseFrame;
- private ImageView mBadgeView;
- private TextView mTitleView;
- private ViewGroup mBrowseTitle;
- private SearchOrbView mSearchOrbView;
+ private TitleView mTitleView;
private boolean mShowingTitle = true;
+ private boolean mHeadersBackStackEnabled = true;
+ private String mWithHeadersBackStackName;
private boolean mShowingHeaders = true;
private boolean mCanShowHeaders = true;
private int mContainerListMarginLeft;
private int mContainerListAlignTop;
- private TransitionHelper mTransitionHelper;
+ private SearchOrbView.Colors mSearchAffordanceColors;
+ private boolean mSearchAffordanceColorSet;
private OnItemSelectedListener mExternalOnItemSelectedListener;
private OnClickListener mExternalOnSearchClickedListener;
private OnItemClickedListener mOnItemClickedListener;
+ private OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
private int mSelectedPosition = -1;
+ private PresenterSelector mHeaderPresenterSelector;
+
// transition related:
- private static int sReparentHeaderId = View.generateViewId();
+ private static TransitionHelper sTransitionHelper = TransitionHelper.getInstance();
+ private int mReparentHeaderId = View.generateViewId();
private Object mSceneWithTitle;
private Object mSceneWithoutTitle;
private Object mSceneWithHeaders;
private Object mSceneWithoutHeaders;
- private Object mTitleTransition;
+ private Object mTitleUpTransition;
+ private Object mTitleDownTransition;
private Object mHeadersTransition;
private int mHeadersTransitionStartDelay;
+ private int mHeadersTransitionDuration;
+ private BackStackListener mBackStackChangedListener;
+ private BrowseTransitionListener mBrowseTransitionListener;
private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
@@ -96,95 +192,58 @@
BrowseFragment.class.getCanonicalName() + ".headersState";
/**
- * @param args Bundle to use for the arguments, if null a new Bundle will be created.
+ * Create arguments for a browse fragment.
+ *
+ * @param args The Bundle to place arguments into, or null if the method
+ * should return a new Bundle.
+ * @param title The title of the BrowseFragment.
+ * @param headersState The initial state of the headers of the
+ * BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link
+ * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}.
+ * @return A Bundle with the given arguments for creating a BrowseFragment.
*/
- public static Bundle createArgs(Bundle args, String title, String badgeUri) {
- return createArgs(args, title, badgeUri, HEADERS_ENABLED);
- }
-
- public static Bundle createArgs(Bundle args, String title, String badgeUri, int headersState) {
+ public static Bundle createArgs(Bundle args, String title, int headersState) {
if (args == null) {
args = new Bundle();
}
args.putString(ARG_TITLE, title);
- args.putString(ARG_BADGE_URI, badgeUri);
args.putInt(ARG_HEADERS_STATE, headersState);
return args;
}
- public static class Params {
- private String mTitle;
- private Drawable mBadgeDrawable;
- private int mHeadersState;
-
- /**
- * Sets the badge image.
- */
- public void setBadgeImage(Drawable drawable) {
- mBadgeDrawable = drawable;
- }
-
- /**
- * Returns the badge image.
- */
- public Drawable getBadgeImage() {
- return mBadgeDrawable;
- }
-
- /**
- * Sets a title for the browse fragment.
- */
- public void setTitle(String title) {
- mTitle = title;
- }
-
- /**
- * Returns the title for the browse fragment.
- */
- public String getTitle() {
- return mTitle;
- }
-
- /**
- * Sets the state for the headers column in the browse fragment.
- */
- public void setHeadersState(int headersState) {
- if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
- Log.e(TAG, "Invalid headers state: " + headersState
- + ", default to enabled and shown.");
- mHeadersState = HEADERS_ENABLED;
- } else {
- mHeadersState = headersState;
- }
- }
-
- /**
- * Returns the state for the headers column in the browse fragment.
- */
- public int getHeadersState() {
- return mHeadersState;
- }
- }
-
/**
- * Set browse parameters.
+ * Sets the brand color for the browse fragment. The brand color is used as
+ * the primary color for UI elements in the browse fragment. For example,
+ * the background color of the headers fragment uses the brand color.
+ *
+ * @param color The color to use as the brand color of the fragment.
*/
- public void setBrowseParams(Params params) {
- mParams = params;
- setBadgeDrawable(mParams.mBadgeDrawable);
- setTitle(mParams.mTitle);
- setHeadersState(mParams.mHeadersState);
+ public void setBrandColor(int color) {
+ mBrandColor = color;
+ mBrandColorSet = true;
+
+ if (mHeadersFragment != null) {
+ mHeadersFragment.setBackgroundColor(mBrandColor);
+ }
}
/**
- * Returns browse parameters.
+ * Returns the brand color for the browse fragment.
+ * The default is transparent.
*/
- public Params getBrowseParams() {
- return mParams;
+ public int getBrandColor() {
+ return mBrandColor;
}
/**
- * Sets the list of rows for the fragment.
+ * Sets the adapter containing the rows for the fragment.
+ *
+ * <p>The items referenced by the adapter must be be derived from
+ * {@link Row}. These rows will be used by the rows fragment and the headers
+ * fragment (if not disabled) to render the browse rows.
+ *
+ * @param adapter An ObjectAdapter for the browse rows. All items must
+ * derive from {@link Row}.
*/
public void setAdapter(ObjectAdapter adapter) {
mAdapter = adapter;
@@ -195,24 +254,48 @@
}
/**
- * Returns the list of rows.
+ * Returns the adapter containing the rows for the fragment.
*/
public ObjectAdapter getAdapter() {
return mAdapter;
}
/**
- * Sets an item selection listener.
+ * Sets an item selection listener. This listener will be called when an
+ * item or row is selected by a user.
+ *
+ * @param listener The listener to call when an item or row is selected.
+ * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)}
*/
public void setOnItemSelectedListener(OnItemSelectedListener listener) {
mExternalOnItemSelectedListener = listener;
}
/**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mExternalOnItemViewSelectedListener = listener;
+ }
+
+ /**
+ * Returns an item selection listener.
+ */
+ public OnItemViewSelectedListener getOnItemViewSelectedListener() {
+ return mExternalOnItemViewSelectedListener;
+ }
+
+ /**
* Sets an item clicked listener on the fragment.
- * OnItemClickedListener will override {@link View.OnClickListener} that
- * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
- * So in general, developer should choose one of the listeners but not both.
+ *
+ * <p>OnItemClickedListener will override {@link View.OnClickListener} that
+ * an item presenter may set during
+ * {@link Presenter#onCreateViewHolder(ViewGroup)}. So in general, you
+ * should choose to use an {@link OnItemClickedListener} or a
+ * {@link View.OnClickListener} on your item views, but not both.
+ *
+ * @param listener The listener to call when an item is clicked.
+ * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)}
*/
public void setOnItemClickedListener(OnItemClickedListener listener) {
mOnItemClickedListener = listener;
@@ -222,34 +305,164 @@
}
/**
- * Returns the item Clicked listener.
+ * Returns the item clicked listener.
+ * @deprecated Use {@link #getOnItemViewClickedListener()}
*/
public OnItemClickedListener getOnItemClickedListener() {
return mOnItemClickedListener;
}
/**
- * Sets a click listener for the search affordance.
- *
- * The presence of a listener will change the visibility of the search affordance in the
- * title area. When set to non-null the title area will contain a call to search action.
- *
- * The listener onClick method will be invoked when the user click on the search action.
- *
- * @param listener The listener.
+ * Sets an item clicked listener on the fragment.
+ * OnItemViewClickedListener will override {@link View.OnClickListener} that
+ * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
+ * So in general, developer should choose one of the listeners but not both.
*/
- public void setOnSearchClickedListener(View.OnClickListener listener) {
- mExternalOnSearchClickedListener = listener;
- if (mSearchOrbView != null) {
- mSearchOrbView.setOnOrbClickedListener(listener);
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ if (mRowsFragment != null) {
+ mRowsFragment.setOnItemViewClickedListener(listener);
}
}
- private void onHeadersTransitionStart(boolean withHeaders) {
- mRowsFragment.getVerticalGridView().setAnimateChildLayout(false);
- mRowsFragment.getVerticalGridView().setFocusSearchDisabled(true);
- mHeadersFragment.getVerticalGridView().setFocusSearchDisabled(true);
- createHeadersTransition(withHeaders);
+ /**
+ * Returns the item Clicked listener.
+ */
+ public OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
+
+ /**
+ * Sets a click listener for the search affordance.
+ *
+ * <p>The presence of a listener will change the visibility of the search
+ * affordance in the fragment title. When set to non-null, the title will
+ * contain an element that a user may click to begin a search.
+ *
+ * <p>The listener's {@link View.OnClickListener#onClick onClick} method
+ * will be invoked when the user clicks on the search element.
+ *
+ * @param listener The listener to call when the search element is clicked.
+ */
+ public void setOnSearchClickedListener(View.OnClickListener listener) {
+ mExternalOnSearchClickedListener = listener;
+ if (mTitleView != null) {
+ mTitleView.setOnSearchClickedListener(listener);
+ }
+ }
+
+ /**
+ * Sets the {@link SearchOrbView.Colors} used to draw the search affordance.
+ */
+ public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
+ mSearchAffordanceColors = colors;
+ mSearchAffordanceColorSet = true;
+ if (mTitleView != null) {
+ mTitleView.setSearchAffordanceColors(mSearchAffordanceColors);
+ }
+ }
+
+ /**
+ * Returns the {@link SearchOrbView.Colors} used to draw the search affordance.
+ */
+ public SearchOrbView.Colors getSearchAffordanceColors() {
+ if (mSearchAffordanceColorSet) {
+ return mSearchAffordanceColors;
+ }
+ if (mTitleView == null) {
+ throw new IllegalStateException("Fragment views not yet created");
+ }
+ return mTitleView.getSearchAffordanceColors();
+ }
+
+ /**
+ * Sets the color used to draw the search affordance.
+ * A default brighter color will be set by the framework.
+ *
+ * @param color The color to use for the search affordance.
+ */
+ public void setSearchAffordanceColor(int color) {
+ setSearchAffordanceColors(new SearchOrbView.Colors(color));
+ }
+
+ /**
+ * Returns the color used to draw the search affordance.
+ */
+ public int getSearchAffordanceColor() {
+ return getSearchAffordanceColors().color;
+ }
+
+ /**
+ * Start a headers transition.
+ *
+ * <p>This method will begin a transition to either show or hide the
+ * headers, depending on the value of withHeaders. If headers are disabled
+ * for this browse fragment, this method will throw an exception.
+ *
+ * @param withHeaders True if the headers should transition to being shown,
+ * false if the transition should result in headers being hidden.
+ */
+ public void startHeadersTransition(boolean withHeaders) {
+ if (!mCanShowHeaders) {
+ throw new IllegalStateException("Cannot start headers transition");
+ }
+ if (isInHeadersTransition() || mShowingHeaders == withHeaders) {
+ return;
+ }
+ startHeadersTransitionInternal(withHeaders);
+ }
+
+ /**
+ * Returns true if the headers transition is currently running.
+ */
+ public boolean isInHeadersTransition() {
+ return mHeadersTransition != null;
+ }
+
+ /**
+ * Returns true if headers are shown.
+ */
+ public boolean isShowingHeaders() {
+ return mShowingHeaders;
+ }
+
+ /**
+ * Set a listener for browse fragment transitions.
+ *
+ * @param listener The listener to call when a browse headers transition
+ * begins or ends.
+ */
+ public void setBrowseTransitionListener(BrowseTransitionListener listener) {
+ mBrowseTransitionListener = listener;
+ }
+
+ private void startHeadersTransitionInternal(boolean withHeaders) {
+ if (getFragmentManager().isDestroyed()) {
+ return;
+ }
+ mShowingHeaders = withHeaders;
+ mRowsFragment.onTransitionStart();
+ mHeadersFragment.onTransitionStart();
+ createHeadersTransition();
+ if (mBrowseTransitionListener != null) {
+ mBrowseTransitionListener.onHeadersTransitionStart(withHeaders);
+ }
+ sTransitionHelper.runTransition(withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders,
+ mHeadersTransition);
+ if (mHeadersBackStackEnabled) {
+ if (!withHeaders) {
+ getFragmentManager().beginTransaction()
+ .addToBackStack(mWithHeadersBackStackName).commit();
+ } else {
+ int count = getFragmentManager().getBackStackEntryCount();
+ if (count > 0) {
+ BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1);
+ if (mWithHeadersBackStackName.equals(entry.getName())) {
+ getFragmentManager().popBackStack();
+ }
+ }
+ }
+ }
}
private boolean isVerticalScrolling() {
@@ -264,11 +477,12 @@
new BrowseFrameLayout.OnFocusSearchListener() {
@Override
public View onFocusSearch(View focused, int direction) {
- // If fastlane is disabled, just return null.
+ // If headers fragment is disabled, just return null.
if (!mCanShowHeaders) return null;
- // if fast lane is running transition, focus stays
- if (mHeadersTransition != null) return focused;
+ final View searchOrbView = mTitleView.getSearchAffordanceView();
+ // if headers is running transition, focus stays
+ if (isInHeadersTransition()) return focused;
if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
if (direction == View.FOCUS_LEFT) {
if (isVerticalScrolling() || mShowingHeaders) {
@@ -280,13 +494,13 @@
return focused;
}
return mRowsFragment.getVerticalGridView();
- } else if (focused == mSearchOrbView && direction == View.FOCUS_DOWN) {
+ } else if (focused == searchOrbView && direction == View.FOCUS_DOWN) {
return mShowingHeaders ? mHeadersFragment.getVerticalGridView() :
mRowsFragment.getVerticalGridView();
- } else if (focused != mSearchOrbView && mSearchOrbView.getVisibility() == View.VISIBLE
+ } else if (focused != searchOrbView && searchOrbView.getVisibility() == View.VISIBLE
&& direction == View.FOCUS_UP) {
- return mSearchOrbView;
+ return searchOrbView;
} else {
return null;
@@ -296,19 +510,29 @@
private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
new BrowseFrameLayout.OnChildFocusListener() {
+
+ @Override
+ public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ // Make sure not changing focus when requestFocus() is called.
+ if (mCanShowHeaders && mShowingHeaders) {
+ if (mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ }
+ if (mRowsFragment.getView().requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ return mTitleView.requestFocus(direction, previouslyFocusedRect);
+ };
+
@Override
public void onRequestChildFocus(View child, View focused) {
int childId = child.getId();
- if (mHeadersTransition != null) return;
+ if (!mCanShowHeaders || isInHeadersTransition()) return;
if (childId == R.id.browse_container_dock && mShowingHeaders) {
- mShowingHeaders = false;
- onHeadersTransitionStart(false);
- mTransitionHelper.runTransition(mSceneWithoutHeaders, mHeadersTransition);
+ startHeadersTransitionInternal(false);
} else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
- mShowingHeaders = true;
- //mHeadersFragment.getView().setAlpha(1f);
- onHeadersTransitionStart(true);
- mTransitionHelper.runTransition(mSceneWithHeaders, mHeadersTransition);
+ startHeadersTransitionInternal(true);
}
}
};
@@ -322,8 +546,32 @@
mContainerListAlignTop = (int) ta.getDimension(
R.styleable.LeanbackTheme_browseRowsMarginTop, 0);
ta.recycle();
+
mHeadersTransitionStartDelay = getResources()
.getInteger(R.integer.lb_browse_headers_transition_delay);
+ mHeadersTransitionDuration = getResources()
+ .getInteger(R.integer.lb_browse_headers_transition_duration);
+
+ readArguments(getArguments());
+
+ if (mCanShowHeaders && mHeadersBackStackEnabled) {
+ mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this;
+ mBackStackChangedListener = new BackStackListener();
+ getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener);
+ if (!mShowingHeaders) {
+ getFragmentManager().beginTransaction()
+ .addToBackStack(mWithHeadersBackStackName).commit();
+ }
+ }
+
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mBackStackChangedListener != null) {
+ getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener);
+ }
+ super.onDestroy();
}
@Override
@@ -341,13 +589,21 @@
mRowsFragment = (RowsFragment) getChildFragmentManager()
.findFragmentById(R.id.browse_container_dock);
}
+
+ mHeadersFragment.setHeadersGone(!mCanShowHeaders);
+
mRowsFragment.setAdapter(mAdapter);
+ if (mHeaderPresenterSelector != null) {
+ mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
+ }
mHeadersFragment.setAdapter(mAdapter);
mRowsFragment.setOnItemSelectedListener(mRowSelectedListener);
+ mRowsFragment.setOnItemViewSelectedListener(mRowViewSelectedListener);
mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener);
mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
mRowsFragment.setOnItemClickedListener(mOnItemClickedListener);
+ mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
@@ -355,139 +611,129 @@
mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener);
- mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group);
- mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge);
- mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title);
- mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb);
+ mTitleView = (TitleView) root.findViewById(R.id.browse_title_group);
+ mTitleView.setTitle(mTitle);
+ mTitleView.setBadgeDrawable(mBadgeDrawable);
+ if (mSearchAffordanceColorSet) {
+ mTitleView.setSearchAffordanceColors(mSearchAffordanceColors);
+ }
if (mExternalOnSearchClickedListener != null) {
- mSearchOrbView.setOnOrbClickedListener(mExternalOnSearchClickedListener);
+ mTitleView.setOnSearchClickedListener(mExternalOnSearchClickedListener);
}
- readArguments(getArguments());
- if (mParams != null) {
- setBadgeDrawable(mParams.mBadgeDrawable);
- setTitle(mParams.mTitle);
- setHeadersState(mParams.mHeadersState);
+ if (mBrandColorSet) {
+ mHeadersFragment.setBackgroundColor(mBrandColor);
}
- mTransitionHelper = new TransitionHelper(getActivity());
- mSceneWithTitle = mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ mSceneWithTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
@Override
public void run() {
- showTitle(true);
+ TitleTransitionHelper.showTitle(mTitleView, true);
}
});
- mSceneWithoutTitle = mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ mSceneWithoutTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
@Override
public void run() {
- showTitle(false);
+ TitleTransitionHelper.showTitle(mTitleView, false);
}
});
- mSceneWithHeaders = mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ mSceneWithHeaders = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
@Override
public void run() {
showHeaders(true);
}
});
- mSceneWithoutHeaders = mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ mSceneWithoutHeaders = sTransitionHelper.createScene(mBrowseFrame, new Runnable() {
@Override
public void run() {
showHeaders(false);
}
});
- mTitleTransition = mTransitionHelper.createAutoTransition();
- mTransitionHelper.excludeChildren(mTitleTransition, R.id.browse_headers, true);
- mTransitionHelper.excludeChildren(mTitleTransition, R.id.container_list, true);
+ mTitleUpTransition = TitleTransitionHelper.createTransitionTitleUp(sTransitionHelper);
+ mTitleDownTransition = TitleTransitionHelper.createTransitionTitleDown(sTransitionHelper);
+
+ sTransitionHelper.excludeChildren(mTitleUpTransition, R.id.browse_headers, true);
+ sTransitionHelper.excludeChildren(mTitleDownTransition, R.id.browse_headers, true);
+ sTransitionHelper.excludeChildren(mTitleUpTransition, R.id.container_list, true);
+ sTransitionHelper.excludeChildren(mTitleDownTransition, R.id.container_list, true);
return root;
}
- private void createHeadersTransition(boolean withHeaders) {
- ArrayList<View> fastHeaders = new ArrayList<View>();
- ArrayList<Integer> fastHeaderPositions = new ArrayList<Integer>();
- ArrayList<View> headers = new ArrayList<View>();
- ArrayList<Integer> headerPositions = new ArrayList<Integer>();
+ private void createHeadersTransition() {
+ mHeadersTransition = sTransitionHelper.createTransitionSet(false);
+ sTransitionHelper.excludeChildren(mHeadersTransition, R.id.browse_title_group, true);
+ Object changeBounds = sTransitionHelper.createChangeBounds(false);
+ Object fadeIn = sTransitionHelper.createFadeTransition(TransitionHelper.FADE_IN);
+ Object fadeOut = sTransitionHelper.createFadeTransition(TransitionHelper.FADE_OUT);
- mHeadersFragment.getHeaderViews(fastHeaders, fastHeaderPositions);
- mRowsFragment.getHeaderViews(headers, headerPositions);
-
- mHeadersTransition = mTransitionHelper.createTransitionSet(true);
- mTransitionHelper.excludeChildren(mHeadersTransition, R.id.browse_title_group, true);
- Object changeBounds = mTransitionHelper.createChangeBounds(true);
- Object fadeIn = mTransitionHelper.createFadeTransition(TransitionHelper.FADE_IN);
- Object fadeOut = mTransitionHelper.createFadeTransition(TransitionHelper.FADE_OUT);
- if (!withHeaders) {
- mTransitionHelper.setChangeBoundsDefaultStartDelay(changeBounds,
- mHeadersTransitionStartDelay);
+ sTransitionHelper.setDuration(fadeOut, mHeadersTransitionDuration);
+ sTransitionHelper.addTransition(mHeadersTransition, fadeOut);
+ if (mShowingHeaders) {
+ sTransitionHelper.setStartDelay(changeBounds, mHeadersTransitionStartDelay);
}
+ sTransitionHelper.setDuration(changeBounds, mHeadersTransitionDuration);
+ sTransitionHelper.addTransition(mHeadersTransition, changeBounds);
+ sTransitionHelper.setDuration(fadeIn, mHeadersTransitionDuration);
+ sTransitionHelper.setStartDelay(fadeIn, mHeadersTransitionStartDelay);
+ sTransitionHelper.addTransition(mHeadersTransition, fadeIn);
- for (int i = 0; i < headerPositions.size(); i++) {
- Integer position = headerPositions.get(i);
- if (position == mSelectedPosition) {
- headers.get(i).setId(sReparentHeaderId);
- mTransitionHelper.setChangeBoundsStartDelay(changeBounds, sReparentHeaderId,
- withHeaders ? mHeadersTransitionStartDelay : 0);
- mTransitionHelper.exclude(fadeIn, headers.get(i), true);
- mTransitionHelper.exclude(fadeOut, headers.get(i), true);
- } else {
- headers.get(i).setId(View.NO_ID);
- }
- }
- for (int i = 0; i < fastHeaderPositions.size(); i++) {
- Integer position = fastHeaderPositions.get(i);
- if (position == mSelectedPosition) {
- fastHeaders.get(i).setId(sReparentHeaderId);
- mTransitionHelper.setChangeBoundsStartDelay(changeBounds, sReparentHeaderId,
- withHeaders ? mHeadersTransitionStartDelay : 0);
- mTransitionHelper.exclude(fadeIn, fastHeaders.get(i), true);
- mTransitionHelper.exclude(fadeOut, fastHeaders.get(i), true);
- } else {
- fastHeaders.get(i).setId(View.NO_ID);
- }
- }
-
- mTransitionHelper.addTransition(mHeadersTransition, fadeOut);
- mTransitionHelper.addTransition(mHeadersTransition, changeBounds);
- mTransitionHelper.addTransition(mHeadersTransition, fadeIn);
-
- mTransitionHelper.setTransitionCompleteListener(mHeadersTransition, new Runnable() {
+ sTransitionHelper.setTransitionListener(mHeadersTransition, new TransitionListener() {
@Override
- public void run() {
+ public void onTransitionStart(Object transition) {
+ }
+ @Override
+ public void onTransitionEnd(Object transition) {
mHeadersTransition = null;
- // TODO: deal fragment destroy view properly
- VerticalGridView rowsGridView = mRowsFragment.getVerticalGridView();
- if (rowsGridView != null) {
- rowsGridView.setAnimateChildLayout(true);
- rowsGridView.setFocusSearchDisabled(false);
- if (!mShowingHeaders && !rowsGridView.hasFocus()) {
+ mRowsFragment.onTransitionEnd();
+ mHeadersFragment.onTransitionEnd();
+ if (mShowingHeaders) {
+ VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
+ if (headerGridView != null && !headerGridView.hasFocus()) {
+ headerGridView.requestFocus();
+ }
+ } else {
+ VerticalGridView rowsGridView = mRowsFragment.getVerticalGridView();
+ if (rowsGridView != null && !rowsGridView.hasFocus()) {
rowsGridView.requestFocus();
}
}
- VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
- if (headerGridView != null) {
- headerGridView.setFocusSearchDisabled(false);
- headerGridView.invalidate();
- if (mShowingHeaders && !headerGridView.hasFocus()) {
- headerGridView.requestFocus();
- }
+ if (mBrowseTransitionListener != null) {
+ mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders);
}
}
});
}
- private void showTitle(boolean show) {
- mBrowseTitle.setVisibility(show ? View.VISIBLE : View.GONE);
+ /**
+ * Sets the {@link PresenterSelector} used to render the row headers.
+ *
+ * @param headerPresenterSelector The PresenterSelector that will determine
+ * the Presenter for each row header.
+ */
+ public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) {
+ mHeaderPresenterSelector = headerPresenterSelector;
+ if (mHeadersFragment != null) {
+ mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector);
+ }
}
private void showHeaders(boolean show) {
if (DEBUG) Log.v(TAG, "showHeaders " + show);
- mHeadersFragment.setHeadersVisiblity(show);
-
- View containerList = mRowsFragment.getView();
+ mHeadersFragment.setHeadersEnabled(show);
MarginLayoutParams lp;
+ View containerList;
+
+ containerList = mRowsFragment.getView();
lp = (MarginLayoutParams) containerList.getLayoutParams();
lp.leftMargin = show ? mContainerListMarginLeft : 0;
containerList.setLayoutParams(lp);
+
+ containerList = mHeadersFragment.getView();
+ lp = (MarginLayoutParams) containerList.getLayoutParams();
+ lp.leftMargin = show ? 0 : -mContainerListMarginLeft;
+ containerList.setLayoutParams(lp);
+
mRowsFragment.setExpand(!show);
}
@@ -495,24 +741,31 @@
new HeadersFragment.OnHeaderClickedListener() {
@Override
public void onHeaderClicked() {
- if (!mCanShowHeaders || !mShowingHeaders) return;
-
- if (mHeadersTransition != null) {
+ if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) {
return;
}
- mShowingHeaders = false;
- onHeadersTransitionStart(false);
- mTransitionHelper.runTransition(mSceneWithoutHeaders, mHeadersTransition);
+ startHeadersTransitionInternal(false);
mRowsFragment.getVerticalGridView().requestFocus();
}
};
- private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
+ private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() {
@Override
- public void onItemSelected(Object item, Row row) {
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
if (DEBUG) Log.v(TAG, "row selected position " + position);
onRowSelected(position);
+ if (mExternalOnItemViewSelectedListener != null) {
+ mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
+ rowViewHolder, row);
+ }
+ }
+ };
+
+ private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(Object item, Row row) {
if (mExternalOnItemSelectedListener != null) {
mExternalOnItemSelectedListener.onItemSelected(item, row);
}
@@ -533,13 +786,13 @@
mSetSelectionRunnable.mPosition = position;
mBrowseFrame.getHandler().post(mSetSelectionRunnable);
- if (position == 0) {
+ if (getAdapter() == null || getAdapter().size() == 0 || position == 0) {
if (!mShowingTitle) {
- mTransitionHelper.runTransition(mSceneWithTitle, mTitleTransition);
+ sTransitionHelper.runTransition(mSceneWithTitle, mTitleDownTransition);
mShowingTitle = true;
}
} else if (mShowingTitle) {
- mTransitionHelper.runTransition(mSceneWithoutTitle, mTitleTransition);
+ sTransitionHelper.runTransition(mSceneWithoutTitle, mTitleUpTransition);
mShowingTitle = false;
}
}
@@ -563,39 +816,45 @@
mSelectedPosition = position;
}
- private void setVerticalVerticalGridViewLayout(VerticalGridView listview, int extraOffset) {
- // align the top edge of item to a fixed position
- listview.setItemAlignmentOffset(0);
- listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
- listview.setWindowAlignmentOffset(mContainerListAlignTop + extraOffset);
- listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
- listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
- }
-
- /**
- * Setup dimensions that are only meaningful when the child Fragments are inside
- * BrowseFragment.
- */
- private void setupChildFragmentsLayout() {
- VerticalGridView headerList = mHeadersFragment.getVerticalGridView();
- VerticalGridView containerList = mRowsFragment.getVerticalGridView();
-
- // Both fragments list view has the same alignment
- setVerticalVerticalGridViewLayout(headerList, 16);
- setVerticalVerticalGridViewLayout(containerList, 0);
- }
-
@Override
public void onStart() {
super.onStart();
- setupChildFragmentsLayout();
+ mHeadersFragment.setWindowAlignmentFromTop(mContainerListAlignTop);
+ mHeadersFragment.setItemAlignment();
+ mRowsFragment.setWindowAlignmentFromTop(mContainerListAlignTop);
+ mRowsFragment.setItemAlignment();
+
if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
mHeadersFragment.getView().requestFocus();
} else if ((!mCanShowHeaders || !mShowingHeaders)
&& mRowsFragment.getView() != null) {
mRowsFragment.getView().requestFocus();
}
- showHeaders(mCanShowHeaders && mShowingHeaders);
+ if (mCanShowHeaders) {
+ showHeaders(mShowingHeaders);
+ }
+ }
+
+ /**
+ * Enable/disable headers transition on back key support. This is enabled by
+ * default. The BrowseFragment will add a back stack entry when headers are
+ * showing. Running a headers transition when the back key is pressed only
+ * works when the headers state is {@link #HEADERS_ENABLED} or
+ * {@link #HEADERS_HIDDEN}.
+ * <p>
+ * NOTE: If an Activity has its own onBackPressed() handling, you must
+ * disable this feature. You may use {@link #startHeadersTransition(boolean)}
+ * and {@link BrowseTransitionListener} in your own back stack handling.
+ */
+ public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) {
+ mHeadersBackStackEnabled = headersBackStackEnabled;
+ }
+
+ /**
+ * Returns true if headers transition on back key support is enabled.
+ */
+ public final boolean isHeadersTransitionOnBackEnabled() {
+ return mHeadersBackStackEnabled;
}
private void readArguments(Bundle args) {
@@ -605,56 +864,94 @@
if (args.containsKey(ARG_TITLE)) {
setTitle(args.getString(ARG_TITLE));
}
-
- if (args.containsKey(ARG_BADGE_URI)) {
- setBadgeUri(args.getString(ARG_BADGE_URI));
- }
-
if (args.containsKey(ARG_HEADERS_STATE)) {
setHeadersState(args.getInt(ARG_HEADERS_STATE));
}
}
- private void setBadgeUri(String badgeUri) {
- // TODO - need a drawable downloader
- }
-
- private void setBadgeDrawable(Drawable drawable) {
- if (mBadgeView == null) {
- return;
- }
- mBadgeView.setImageDrawable(drawable);
- if (drawable != null) {
- mBadgeView.setVisibility(View.VISIBLE);
- } else {
- mBadgeView.setVisibility(View.GONE);
+ /**
+ * Sets the drawable displayed in the browse fragment title.
+ *
+ * @param drawable The Drawable to display in the browse fragment title.
+ */
+ public void setBadgeDrawable(Drawable drawable) {
+ if (mBadgeDrawable != drawable) {
+ mBadgeDrawable = drawable;
+ if (mTitleView != null) {
+ mTitleView.setBadgeDrawable(drawable);
+ }
}
}
- private void setTitle(String title) {
+ /**
+ * Returns the badge drawable used in the fragment title.
+ */
+ public Drawable getBadgeDrawable() {
+ return mBadgeDrawable;
+ }
+
+ /**
+ * Sets a title for the browse fragment.
+ *
+ * @param title The title of the browse fragment.
+ */
+ public void setTitle(String title) {
+ mTitle = title;
if (mTitleView != null) {
- mTitleView.setText(title);
+ mTitleView.setTitle(title);
}
}
- private void setHeadersState(int headersState) {
- if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
- switch (headersState) {
- case HEADERS_ENABLED:
- mCanShowHeaders = true;
- mShowingHeaders = true;
- break;
- case HEADERS_HIDDEN:
- mCanShowHeaders = true;
- mShowingHeaders = false;
- break;
- case HEADERS_DISABLED:
- mCanShowHeaders = false;
- mShowingHeaders = false;
- break;
- default:
- Log.w(TAG, "Unknown headers state: " + headersState);
- break;
+ /**
+ * Returns the title for the browse fragment.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Sets the state for the headers column in the browse fragment. Must be one
+ * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or
+ * {@link #HEADERS_DISABLED}.
+ *
+ * @param headersState The state of the headers for the browse fragment.
+ */
+ public void setHeadersState(int headersState) {
+ if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) {
+ throw new IllegalArgumentException("Invalid headers state: " + headersState);
}
+ if (DEBUG) Log.v(TAG, "setHeadersState " + headersState);
+
+ if (headersState != mHeadersState) {
+ mHeadersState = headersState;
+ switch (headersState) {
+ case HEADERS_ENABLED:
+ mCanShowHeaders = true;
+ mShowingHeaders = true;
+ break;
+ case HEADERS_HIDDEN:
+ mCanShowHeaders = true;
+ mShowingHeaders = false;
+ break;
+ case HEADERS_DISABLED:
+ mCanShowHeaders = false;
+ mShowingHeaders = false;
+ break;
+ default:
+ Log.w(TAG, "Unknown headers state: " + headersState);
+ break;
+ }
+ if (mHeadersFragment != null) {
+ mHeadersFragment.setHeadersGone(!mCanShowHeaders);
+ }
+ }
+ }
+
+ /**
+ * Returns the state of the headers column in the browse fragment.
+ */
+ public int getHeadersState() {
+ return mHeadersState;
}
}
+
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFrameLayout.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseFrameLayout.java
index 4f04b05..9b87305 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/BrowseFrameLayout.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseFrameLayout.java
@@ -14,6 +14,7 @@
package android.support.v17.leanback.app;
import android.content.Context;
+import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
@@ -30,6 +31,8 @@
}
public interface OnChildFocusListener {
+ public boolean onRequestFocusInDescendants(int direction,
+ Rect previouslyFocusedRect);
public void onRequestChildFocus(View child, View focused);
}
@@ -57,6 +60,16 @@
}
@Override
+ protected boolean onRequestFocusInDescendants(int direction,
+ Rect previouslyFocusedRect) {
+ if (mOnChildFocusListener != null) {
+ return mOnChildFocusListener.onRequestFocusInDescendants(direction,
+ previouslyFocusedRect);
+ }
+ return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
+ }
+
+ @Override
public View focusSearch(View focused, int direction) {
if (mListener != null) {
View view = mListener.onFocusSearch(focused, direction);
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
index 9f426d3..c06cf1c 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -15,12 +15,14 @@
import android.support.v17.leanback.R;
import android.support.v17.leanback.widget.ObjectAdapter;
-import android.support.v17.leanback.widget.OnChildSelectedListener;
import android.support.v17.leanback.widget.OnItemClickedListener;
import android.support.v17.leanback.widget.OnItemSelectedListener;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
import android.support.v17.leanback.widget.VerticalGridView;
-import android.util.Log;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -40,6 +42,8 @@
private int mContainerListAlignTop;
private OnItemSelectedListener mExternalOnItemSelectedListener;
private OnItemClickedListener mOnItemClickedListener;
+ private OnItemViewSelectedListener mExternalOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
private int mSelectedPosition = -1;
/**
@@ -61,6 +65,7 @@
/**
* Sets an item selection listener.
+ * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)}
*/
public void setOnItemSelectedListener(OnItemSelectedListener listener) {
mExternalOnItemSelectedListener = listener;
@@ -68,6 +73,7 @@
/**
* Sets an item Clicked listener.
+ * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)}
*/
public void setOnItemClickedListener(OnItemClickedListener listener) {
mOnItemClickedListener = listener;
@@ -77,12 +83,37 @@
}
/**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mExternalOnItemViewSelectedListener = listener;
+ }
+
+ /**
+ * Sets an item Clicked listener.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ if (mRowsFragment != null) {
+ mRowsFragment.setOnItemViewClickedListener(listener);
+ }
+ }
+
+ /**
* Returns the item Clicked listener.
+ * @deprecated Use {@link #getOnItemViewClickedListener()}
*/
public OnItemClickedListener getOnItemClickedListener() {
return mOnItemClickedListener;
}
+ /**
+ * Returns the item Clicked listener.
+ */
+ public OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
+
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -103,17 +134,23 @@
.replace(R.id.fragment_dock, mRowsFragment).commit();
}
mRowsFragment.setAdapter(mAdapter);
- mRowsFragment.setOnItemSelectedListener(mRowSelectedListener);
+ mRowsFragment.setOnItemViewSelectedListener(mRowSelectedListener);
mRowsFragment.setOnItemClickedListener(mOnItemClickedListener);
+ mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener);
return view;
}
- private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
+ private OnItemViewSelectedListener mRowSelectedListener = new OnItemViewSelectedListener() {
@Override
- public void onItemSelected(Object item, Row row) {
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
if (mExternalOnItemSelectedListener != null) {
mExternalOnItemSelectedListener.onItemSelected(item, row);
}
+ if (mExternalOnItemViewSelectedListener != null) {
+ mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
+ rowViewHolder, row);
+ }
}
};
diff --git a/v17/leanback/src/android/support/v17/leanback/app/ErrorFragment.java b/v17/leanback/src/android/support/v17/leanback/app/ErrorFragment.java
new file mode 100644
index 0000000..cc7e560
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/ErrorFragment.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.os.Bundle;
+import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.TitleView;
+import android.text.TextUtils;
+import android.util.Log;
+import android.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Paint.FontMetricsInt;
+import android.graphics.drawable.Drawable;
+
+/**
+ * A fragment for displaying an error indication.
+ */
+public class ErrorFragment extends Fragment {
+
+ private View mErrorFrame;
+ private String mTitle;
+ private Drawable mBadgeDrawable;
+ private TitleView mTitleView;
+ private ImageView mImageView;
+ private TextView mTextView;
+ private Button mButton;
+ private Drawable mDrawable;
+ private CharSequence mMessage;
+ private String mButtonText;
+ private View.OnClickListener mButtonClickListener;
+ private Drawable mBackgroundDrawable;
+ private boolean mIsBackgroundTranslucent = true;
+
+ /**
+ * Sets the drawable displayed in the browse fragment title.
+ *
+ * @param drawable The drawable to display in the browse fragment title.
+ */
+ public void setBadgeDrawable(Drawable drawable) {
+ mBadgeDrawable = drawable;
+ updateTitle();
+ }
+
+ /**
+ * Returns the badge drawable used in the fragment title.
+ */
+ public Drawable getBadgeDrawable() {
+ return mBadgeDrawable;
+ }
+
+ /**
+ * Sets a title for the browse fragment.
+ *
+ * @param title The title of the browse fragment.
+ */
+ public void setTitle(String title) {
+ mTitle = title;
+ updateTitle();
+ }
+
+ /**
+ * Returns the title for the browse fragment.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Sets the default background.
+ *
+ * @param translucent True to set a translucent background.
+ */
+ public void setDefaultBackground(boolean translucent) {
+ mBackgroundDrawable = null;
+ mIsBackgroundTranslucent = translucent;
+ updateBackground();
+ updateMessage();
+ }
+
+ /**
+ * Returns true if the background is translucent.
+ */
+ public boolean isBackgroundTranslucent() {
+ return mIsBackgroundTranslucent;
+ }
+
+ /**
+ * Sets a drawable for the fragment background.
+ *
+ * @param drawable The drawable used for the background.
+ */
+ public void setBackgroundDrawable(Drawable drawable) {
+ mBackgroundDrawable = drawable;
+ if (drawable != null) {
+ final int opacity = drawable.getOpacity();
+ mIsBackgroundTranslucent = (opacity == PixelFormat.TRANSLUCENT ||
+ opacity == PixelFormat.TRANSPARENT);
+ }
+ updateBackground();
+ updateMessage();
+ }
+
+ /**
+ * Returns the background drawable. May be null if a default is used.
+ */
+ public Drawable getBackgroundDrawable() {
+ return mBackgroundDrawable;
+ }
+
+ /**
+ * Sets the drawable to be used for the error image.
+ *
+ * @param drawable The drawable used for the error image.
+ */
+ public void setImageDrawable(Drawable drawable) {
+ mDrawable = drawable;
+ updateImageDrawable();
+ }
+
+ /**
+ * Returns the drawable used for the error image.
+ */
+ public Drawable getImageDrawable() {
+ return mDrawable;
+ }
+
+ /**
+ * Sets the error message.
+ *
+ * @param message The error message.
+ */
+ public void setMessage(CharSequence message) {
+ mMessage = message;
+ updateMessage();
+ }
+
+ /**
+ * Returns the error message.
+ */
+ public CharSequence getMessage() {
+ return mMessage;
+ }
+
+ /**
+ * Sets the button text.
+ *
+ * @param text The button text.
+ */
+ public void setButtonText(String text) {
+ mButtonText = text;
+ updateButton();
+ }
+
+ /**
+ * Returns the button text.
+ */
+ public String getButtonText() {
+ return mButtonText;
+ }
+
+ /**
+ * Set the button click listener.
+ *
+ * @param clickListener The click listener for the button.
+ */
+ public void setButtonClickListener(View.OnClickListener clickListener) {
+ mButtonClickListener = clickListener;
+ updateButton();
+ }
+
+ /**
+ * Returns the button click listener.
+ */
+ public View.OnClickListener getButtonClickListener() {
+ return mButtonClickListener;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.lb_error_fragment, container, false);
+
+ mErrorFrame = root.findViewById(R.id.error_frame);
+ updateBackground();
+
+ mImageView = (ImageView) root.findViewById(R.id.image);
+ updateImageDrawable();
+
+ mTextView = (TextView) root.findViewById(R.id.message);
+ updateMessage();
+
+ mButton = (Button) root.findViewById(R.id.button);
+ updateButton();
+
+ mTitleView = (TitleView) root.findViewById(R.id.browse_title_group);
+ updateTitle();
+
+ FontMetricsInt metrics = getFontMetricsInt(mTextView);
+ int underImageBaselineMargin = container.getResources().getDimensionPixelSize(
+ R.dimen.lb_error_under_image_baseline_margin);
+ setTopMargin(mTextView, underImageBaselineMargin + metrics.ascent);
+
+ int underMessageBaselineMargin = container.getResources().getDimensionPixelSize(
+ R.dimen.lb_error_under_message_baseline_margin);
+ setTopMargin(mButton, underMessageBaselineMargin - metrics.descent);
+
+ return root;
+ }
+
+ private void updateBackground() {
+ if (mErrorFrame != null) {
+ if (mBackgroundDrawable != null) {
+ mErrorFrame.setBackground(mBackgroundDrawable);
+ } else {
+ mErrorFrame.setBackgroundColor(mErrorFrame.getResources().getColor(
+ mIsBackgroundTranslucent ?
+ R.color.lb_error_background_color_translucent :
+ R.color.lb_error_background_color_opaque));
+ }
+ }
+ }
+
+ private void updateTitle() {
+ if (mTitleView != null) {
+ mTitleView.setTitle(mTitle);
+ mTitleView.setBadgeDrawable(mBadgeDrawable);
+ }
+ }
+
+ private void updateMessage() {
+ if (mTextView != null) {
+ mTextView.setText(mMessage);
+ mTextView.setTextColor(mTextView.getResources().getColor(mIsBackgroundTranslucent ?
+ R.color.lb_error_message_color_on_translucent :
+ R.color.lb_error_message_color_on_opaque));
+ mTextView.setVisibility(TextUtils.isEmpty(mMessage) ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ private void updateImageDrawable() {
+ if (mImageView != null) {
+ mImageView.setImageDrawable(mDrawable);
+ mImageView.setVisibility(mDrawable == null ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ private void updateButton() {
+ if (mButton != null) {
+ mButton.setText(mButtonText);
+ mButton.setOnClickListener(mButtonClickListener);
+ mButton.setVisibility(TextUtils.isEmpty(mButtonText) ? View.GONE : View.VISIBLE);
+ mButton.requestFocus();
+ }
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mErrorFrame.requestFocus();
+ }
+
+ private static FontMetricsInt getFontMetricsInt(TextView textView) {
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setTextSize(textView.getTextSize());
+ paint.setTypeface(textView.getTypeface());
+ return paint.getFontMetricsInt();
+ }
+
+ private static void setTopMargin(TextView textView, int topMargin) {
+ ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) textView.getLayoutParams();
+ lp.topMargin = topMargin;
+ textView.setLayoutParams(lp);
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java b/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
index 7cb5ce1..5d71b2e 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
@@ -18,18 +18,17 @@
import android.support.v17.leanback.R;
import android.support.v17.leanback.widget.FocusHighlightHelper;
import android.support.v17.leanback.widget.ItemBridgeAdapter;
-import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.PresenterSelector;
import android.support.v17.leanback.widget.OnItemSelectedListener;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.RowHeaderPresenter;
+import android.support.v17.leanback.widget.SinglePresenterSelector;
import android.support.v17.leanback.widget.VerticalGridView;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
+import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
-
-import java.util.List;
+import android.view.View.OnLayoutChangeListener;
+import android.widget.FrameLayout;
/**
* An internal fragment containing a list of row headers.
@@ -42,17 +41,16 @@
private OnItemSelectedListener mOnItemSelectedListener;
private OnHeaderClickedListener mOnHeaderClickedListener;
- private boolean mShow = true;
+ private boolean mHeadersEnabled = true;
+ private boolean mHeadersGone = false;
+ private int mBackgroundColor;
+ private boolean mBackgroundColorSet;
- private static final Presenter sHeaderPresenter = new RowHeaderPresenter();
+ private static final PresenterSelector sHeaderPresenter = new SinglePresenterSelector(
+ new RowHeaderPresenter(R.layout.lb_header));
public HeadersFragment() {
- setPresenterSelector(new PresenterSelector() {
- @Override
- public Presenter getPresenter(Object item) {
- return sHeaderPresenter;
- }
- });
+ setPresenterSelector(sHeaderPresenter);
}
public void setOnHeaderClickedListener(OnHeaderClickedListener listener) {
@@ -69,6 +67,8 @@
if (position >= 0) {
Row row = (Row) getAdapter().get(position);
mOnItemSelectedListener.onItemSelected(null, row);
+ } else {
+ mOnItemSelectedListener.onItemSelected(null, null);
}
}
}
@@ -88,12 +88,21 @@
});
headerView.setFocusable(true);
headerView.setFocusableInTouchMode(true);
+ if (mWrapper != null) {
+ viewHolder.itemView.addOnLayoutChangeListener(sLayoutChangeListener);
+ } else {
+ headerView.addOnLayoutChangeListener(sLayoutChangeListener);
+ }
}
+ };
+
+ private static OnLayoutChangeListener sLayoutChangeListener = new OnLayoutChangeListener() {
@Override
- public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
- View headerView = viewHolder.getViewHolder().view;
- headerView.setVisibility(mShow ? View.VISIBLE : View.INVISIBLE);
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ v.setPivotX(0);
+ v.setPivotY(v.getMeasuredHeight() / 2);
}
};
@@ -105,59 +114,80 @@
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- if (getBridgeAdapter() != null && getVerticalGridView() != null) {
- FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView());
- }
- }
-
- void getHeaderViews(List<View> headers, List<Integer> positions) {
final VerticalGridView listView = getVerticalGridView();
if (listView == null) {
return;
}
- final int count = listView.getChildCount();
- for (int i = 0; i < count; i++) {
- View child = listView.getChildAt(i);
- headers.add(child);
- positions.add(listView.getChildViewHolder(child).getPosition());
+ if (getBridgeAdapter() != null) {
+ FocusHighlightHelper.setupHeaderItemFocusHighlight(listView);
}
+ listView.setBackgroundColor(getBackgroundColor());
+ listView.setVisibility(mHeadersGone ? View.GONE : View.VISIBLE);
+ listView.setLayoutEnabled(mHeadersEnabled);
}
- void setHeadersVisiblity(boolean show) {
- mShow = show;
+ void setHeadersEnabled(boolean enabled) {
+ mHeadersEnabled = enabled;
final VerticalGridView listView = getVerticalGridView();
- if (listView == null) {
- return;
- }
- final int count = listView.getChildCount();
- final int visibility = mShow ? View.VISIBLE : View.INVISIBLE;
-
- // we should set visibility of selected view first so that it can
- // regain the focus from parent (which is FOCUS_AFTER_DESCENDANT)
- final int selectedPosition = listView.getSelectedPosition();
- if (selectedPosition >= 0) {
- RecyclerView.ViewHolder vh = listView.findViewHolderForPosition(selectedPosition);
- if (vh != null) {
- vh.itemView.setVisibility(visibility);
- }
- }
- for (int i = 0; i < count; i++) {
- View child = listView.getChildAt(i);
- if (listView.getChildPosition(child) != selectedPosition) {
- child.setVisibility(visibility);
- }
+ if (listView != null) {
+ listView.setLayoutEnabled(mHeadersEnabled);
}
}
+ void setHeadersGone(boolean gone) {
+ mHeadersGone = gone;
+ final VerticalGridView listView = getVerticalGridView();
+ if (listView != null) {
+ listView.setVisibility(mHeadersGone ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ // Wrapper needed because of conflict between RecyclerView's use of alpha
+ // for ADD animations, and RowHeaderPresnter's use of alpha for selected level.
+ private final ItemBridgeAdapter.Wrapper mWrapper = new ItemBridgeAdapter.Wrapper() {
+ @Override
+ public void wrap(View wrapper, View wrapped) {
+ ((FrameLayout) wrapper).addView(wrapped);
+ }
+
+ @Override
+ public View createWrapper(View root) {
+ return new FrameLayout(root.getContext());
+ }
+ };
@Override
protected void updateAdapter() {
super.updateAdapter();
ItemBridgeAdapter adapter = getBridgeAdapter();
if (adapter != null) {
adapter.setAdapterListener(mAdapterListener);
+ adapter.setWrapper(mWrapper);
}
if (adapter != null && getVerticalGridView() != null) {
FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView());
}
}
+
+ void setBackgroundColor(int color) {
+ mBackgroundColor = color;
+ mBackgroundColorSet = true;
+
+ if (getVerticalGridView() != null) {
+ getVerticalGridView().setBackgroundColor(mBackgroundColor);
+ }
+ }
+
+ int getBackgroundColor() {
+ if (getActivity() == null) {
+ throw new IllegalStateException("Activity must be attached");
+ }
+
+ if (mBackgroundColorSet) {
+ return mBackgroundColor;
+ }
+
+ TypedValue outValue = new TypedValue();
+ getActivity().getTheme().resolveAttribute(R.attr.defaultBrandColor, outValue, true);
+ return getResources().getColor(outValue.resourceId);
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
index c5c1d1e..d5275bc 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
@@ -13,6 +13,8 @@
*/
package android.support.v17.leanback.app;
+import java.util.ArrayList;
+
import android.animation.TimeAnimator;
import android.animation.TimeAnimator.TimeListener;
import android.graphics.Canvas;
@@ -20,11 +22,15 @@
import android.support.v17.leanback.R;
import android.support.v17.leanback.graphics.ColorOverlayDimmer;
import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
import android.support.v17.leanback.widget.RowPresenter.ViewHolder;
import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v17.leanback.widget.HorizontalGridView;
import android.support.v17.leanback.widget.OnItemSelectedListener;
import android.support.v17.leanback.widget.OnItemClickedListener;
import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.ListRowPresenter;
import android.support.v17.leanback.widget.Presenter;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
@@ -33,8 +39,6 @@
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
-import java.util.List;
-
/**
* An ordered set of rows of leanback widgets.
*/
@@ -50,6 +54,7 @@
final TimeAnimator mSelectAnimator = new TimeAnimator();
final ColorOverlayDimmer mColorDimmer;
+
int mSelectAnimatorDurationInUse;
Interpolator mSelectAnimatorInterpolatorInUse;
float mSelectLevelAnimStart;
@@ -69,6 +74,12 @@
@Override
public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
+ if (mSelectAnimator.isRunning()) {
+ updateSelect(totalTime, deltaTime);
+ }
+ }
+
+ void updateSelect(long totalTime, long deltaTime) {
float fraction;
if (totalTime >= mSelectAnimatorDurationInUse) {
fraction = 1;
@@ -87,7 +98,7 @@
}
void animateSelect(boolean select, boolean immediate) {
- endAnimation();
+ endSelectAnimation();
final float end = select ? 1 : 0;
if (immediate) {
mRowPresenter.setSelectLevel(mRowViewHolder, end);
@@ -103,7 +114,11 @@
}
}
- void endAnimation() {
+ void endAnimations() {
+ endSelectAnimation();
+ }
+
+ void endSelectAnimation() {
mSelectAnimator.end();
}
@@ -122,18 +137,25 @@
private boolean mViewsCreated;
private OnItemSelectedListener mOnItemSelectedListener;
+ private OnItemViewSelectedListener mOnItemViewSelectedListener;
private OnItemClickedListener mOnItemClickedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
- // Select animation and interpolator are not intended to exposed at this moment.
- // They might be synced with vertical scroll animation later.
+ // Select animation and interpolator are not intended to be
+ // exposed at this moment. They might be synced with vertical scroll
+ // animation later.
int mSelectAnimatorDuration;
Interpolator mSelectAnimatorInterpolator = new DecelerateInterpolator(2);
+ private RecyclerView.RecycledViewPool mRecycledViewPool;
+ private ArrayList<Presenter> mPresenterMapper;
+
/**
* Sets an item clicked listener on the fragment.
* OnItemClickedListener will override {@link View.OnClickListener} that
* item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
* So in general, developer should choose one of the listeners but not both.
+ * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)}
*/
public void setOnItemClickedListener(OnItemClickedListener listener) {
mOnItemClickedListener = listener;
@@ -145,19 +167,40 @@
/**
* Returns the item clicked listener.
+ * @deprecated Use {@link #getOnItemClickedListener()}
*/
public OnItemClickedListener getOnItemClickedListener() {
return mOnItemClickedListener;
}
/**
+ * Sets an item clicked listener on the fragment.
+ * OnItemViewClickedListener will override {@link View.OnClickListener} that
+ * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
+ * So in general, developer should choose one of the listeners but not both.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ if (mViewsCreated) {
+ throw new IllegalStateException(
+ "Item clicked listener must be set before views are created");
+ }
+ }
+
+ /**
+ * Returns the item clicked listener.
+ */
+ public OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
+
+ /**
* Set the visibility of titles/hovercard of browse rows.
*/
public void setExpand(boolean expand) {
mExpand = expand;
VerticalGridView listView = getVerticalGridView();
if (listView != null) {
- listView.setActivated(expand);
final int count = listView.getChildCount();
if (DEBUG) Log.v(TAG, "setExpand " + expand + " count " + count);
for (int i = 0; i < count; i++) {
@@ -170,6 +213,7 @@
/**
* Sets an item selection listener.
+ * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)}
*/
public void setOnItemSelectedListener(OnItemSelectedListener listener) {
mOnItemSelectedListener = listener;
@@ -185,6 +229,30 @@
}
}
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mOnItemViewSelectedListener = listener;
+ VerticalGridView listView = getVerticalGridView();
+ if (listView != null) {
+ final int count = listView.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View view = listView.getChildAt(i);
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder)
+ listView.getChildViewHolder(view);
+ setOnItemViewSelectedListener(vh, mOnItemViewSelectedListener);
+ }
+ }
+ }
+
+ /**
+ * Returns an item selection listener.
+ */
+ public OnItemViewSelectedListener getOnItemViewSelectedListener() {
+ return mOnItemViewSelectedListener;
+ }
+
@Override
protected void onRowSelected(ViewGroup parent, View view, int position, long id) {
VerticalGridView listView = getVerticalGridView();
@@ -222,8 +290,21 @@
public void onViewCreated(View view, Bundle savedInstanceState) {
if (DEBUG) Log.v(TAG, "onViewCreated");
super.onViewCreated(view, savedInstanceState);
+ // Align the top edge of child with id row_content.
+ // Need set this for directly using RowsFragment.
getVerticalGridView().setItemAlignmentViewId(R.id.row_content);
getVerticalGridView().addItemDecoration(mItemDecoration);
+
+ mRecycledViewPool = null;
+ mPresenterMapper = null;
+ }
+
+ @Override
+ void setItemAlignment() {
+ super.setItemAlignment();
+ if (getVerticalGridView() != null) {
+ getVerticalGridView().setItemAlignmentOffsetWithPadding(true);
+ }
}
private RecyclerView.ItemDecoration mItemDecoration = new RecyclerView.ItemDecoration() {
@@ -255,19 +336,25 @@
((RowPresenter) vh.getPresenter()).setOnItemSelectedListener(listener);
}
+ private static void setOnItemViewSelectedListener(ItemBridgeAdapter.ViewHolder vh,
+ OnItemViewSelectedListener listener) {
+ ((RowPresenter) vh.getPresenter()).setOnItemViewSelectedListener(listener);
+ }
+
private final ItemBridgeAdapter.AdapterListener mBridgeAdapterListener =
new ItemBridgeAdapter.AdapterListener() {
@Override
- public void onAddPresenter(Presenter presenter) {
+ public void onAddPresenter(Presenter presenter, int type) {
((RowPresenter) presenter).setOnItemClickedListener(mOnItemClickedListener);
+ ((RowPresenter) presenter).setOnItemViewClickedListener(mOnItemViewClickedListener);
}
@Override
public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
- Presenter rowPresenter = vh.getPresenter();
VerticalGridView listView = getVerticalGridView();
if (listView != null && ((RowPresenter) vh.getPresenter()).canDrawOutOfBounds()) {
listView.setClipChildren(false);
}
+ setupSharedViewPool(vh.getViewHolder());
mViewsCreated = true;
vh.setExtraObject(new RowViewHolderExtra(vh));
// selected state is initialized to false, then driven by grid view onChildSelected
@@ -285,14 +372,35 @@
// thing in onBind.
setRowViewExpanded(vh, mExpand);
setOnItemSelectedListener(vh, mOnItemSelectedListener);
+ setOnItemViewSelectedListener(vh, mOnItemViewSelectedListener);
}
@Override
public void onUnbind(ItemBridgeAdapter.ViewHolder vh) {
RowViewHolderExtra extra = (RowViewHolderExtra) vh.getExtraObject();
- extra.endAnimation();
+ extra.endAnimations();
}
};
+ private void setupSharedViewPool(Presenter.ViewHolder viewHolder) {
+ if (viewHolder instanceof ListRowPresenter.ViewHolder) {
+ HorizontalGridView view = ((ListRowPresenter.ViewHolder) viewHolder).getGridView();
+ // Recycled view pool is shared between all list rows
+ if (mRecycledViewPool == null) {
+ mRecycledViewPool = view.getRecycledViewPool();
+ } else {
+ view.setRecycledViewPool(mRecycledViewPool);
+ }
+
+ ItemBridgeAdapter bridgeAdapter =
+ ((ListRowPresenter.ViewHolder) viewHolder).getBridgeAdapter();
+ if (mPresenterMapper == null) {
+ mPresenterMapper = bridgeAdapter.getPresenterMapper();
+ } else {
+ bridgeAdapter.setPresenterMapper(mPresenterMapper);
+ }
+ }
+ }
+
@Override
protected void updateAdapter() {
super.updateAdapter();
@@ -305,22 +413,4 @@
}
}
- void getHeaderViews(List<View> headers, List<Integer> positions) {
- final VerticalGridView listView = getVerticalGridView();
- if (listView == null) {
- return;
- }
- final int count = listView.getChildCount();
- for (int i = 0; i < count; i++) {
- View child = listView.getChildAt(i);
- ItemBridgeAdapter.ViewHolder viewHolder = (ItemBridgeAdapter.ViewHolder)
- listView.getChildViewHolder(child);
- RowPresenter presenter = (RowPresenter) viewHolder.getPresenter();
- RowPresenter.ViewHolder rowViewHolder = presenter.getRowViewHolder(
- viewHolder.getViewHolder());
- headers.add(rowViewHolder.getHeaderViewHolder().view);
- positions.add(viewHolder.getPosition());
- }
- }
-
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java b/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
index 9720634..9d4f43b 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
@@ -14,14 +14,20 @@
package android.support.v17.leanback.app;
import android.app.Fragment;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
+import android.speech.SpeechRecognizer;
import android.support.v17.leanback.widget.ObjectAdapter;
import android.support.v17.leanback.widget.OnItemClickedListener;
import android.support.v17.leanback.widget.OnItemSelectedListener;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
import android.support.v17.leanback.widget.SearchBar;
import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v17.leanback.widget.Presenter.ViewHolder;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -29,16 +35,27 @@
import android.widget.FrameLayout;
import android.support.v17.leanback.R;
+import java.util.List;
+
/**
- * A fragment to handle searches
+ * A fragment to handle searches. An application will supply an implementation
+ * of the {@link SearchResultProvider} interface to handle the search and return
+ * an {@link ObjectAdapter} containing the results. The results are rendered
+ * into a {@link RowsFragment}, in the same way that they are in a {@link
+ * BrowseFragment}.
+ *
+ * <p>Note: Your application will need to request android.permission.RECORD_AUDIO.
*/
public class SearchFragment extends Fragment {
private static final String TAG = SearchFragment.class.getSimpleName();
private static final boolean DEBUG = false;
- private static final String ARG_QUERY = SearchFragment.class.getCanonicalName() + ".query";
+
+ private static final String ARG_PREFIX = SearchFragment.class.getCanonicalName();
+ private static final String ARG_QUERY = ARG_PREFIX + ".query";
+ private static final String ARG_TITLE = ARG_PREFIX + ".title";
/**
- * Search API exposed to application
+ * Search API to be provided by the application.
*/
public static interface SearchResultProvider {
/**
@@ -56,10 +73,13 @@
* <p>Method invoked when the search query is updated.</p>
*
* <p>This is called as soon as the query changes; it is up to the application to add a
- * delay before actually executing the queries if needed.</p>
+ * delay before actually executing the queries if needed.
+ *
+ * <p>This method might not always be called before onQueryTextSubmit gets called, in
+ * particular for voice input.
*
* @param newQuery The current search query.
- * @return whether the results changed or not.
+ * @return whether the results changed as a result of the new query.
*/
public boolean onQueryTextChange(String newQuery);
@@ -67,8 +87,8 @@
* Method invoked when the search query is submitted, either by dismissing the keyboard,
* pressing search or next on the keyboard or when voice has detected the end of the query.
*
- * @param query The query.
- * @return whether the results changed or not
+ * @param query The query entered.
+ * @return whether the results changed as a result of the query.
*/
public boolean onQueryTextSubmit(String query);
}
@@ -82,26 +102,39 @@
private OnItemSelectedListener mOnItemSelectedListener;
private OnItemClickedListener mOnItemClickedListener;
+ private OnItemViewSelectedListener mOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
private ObjectAdapter mResultAdapter;
+ private String mTitle;
+ private Drawable mBadgeDrawable;
+
+ private SpeechRecognizer mSpeechRecognizer;
+
/**
* @param args Bundle to use for the arguments, if null a new Bundle will be created.
*/
public static Bundle createArgs(Bundle args, String query) {
+ return createArgs(args, query, null);
+ }
+
+ public static Bundle createArgs(Bundle args, String query, String title) {
if (args == null) {
args = new Bundle();
}
args.putString(ARG_QUERY, query);
+ args.putString(ARG_TITLE, title);
return args;
}
/**
- * Create a search fragment with a given search query to start with
+ * Create a search fragment with a given search query.
*
- * You should only use this if you need to start the search fragment with a pre-filled query
+ * <p>You should only use this if you need to start the search fragment with a
+ * pre-filled query.
*
- * @param query the search query to start with
- * @return a new SearchFragment
+ * @param query The search query to begin with.
+ * @return A new SearchFragment.
*/
public static SearchFragment newInstance(String query) {
SearchFragment fragment = new SearchFragment();
@@ -145,15 +178,18 @@
@Override
public void onKeyboardDismiss(String query) {
+ if (DEBUG) Log.v(TAG, String.format("onKeyboardDismiss %s", query));
mRowsFragment.setSelectedPosition(0);
mRowsFragment.getVerticalGridView().requestFocus();
}
});
- Bundle args = getArguments();
- if (null != args) {
- String query = args.getString(ARG_QUERY, "");
- mSearchBar.setSearchQuery(query);
+ readArguments(getArguments());
+ if (null != mBadgeDrawable) {
+ setBadgeDrawable(mBadgeDrawable);
+ }
+ if (null != mTitle) {
+ setTitle(mTitle);
}
// Inject the RowsFragment in the results container
@@ -165,23 +201,35 @@
mRowsFragment = (RowsFragment) getChildFragmentManager()
.findFragmentById(R.id.browse_container_dock);
}
- mRowsFragment.setOnItemSelectedListener(new OnItemSelectedListener() {
+ mRowsFragment.setOnItemViewSelectedListener(new OnItemViewSelectedListener() {
@Override
- public void onItemSelected(Object item, Row row) {
+ public void onItemSelected(ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
if (DEBUG) Log.v(TAG, String.format("onItemSelected %d", position));
mSearchBar.setVisibility(0 >= position ? View.VISIBLE : View.GONE);
if (null != mOnItemSelectedListener) {
mOnItemSelectedListener.onItemSelected(item, row);
}
+ if (null != mOnItemViewSelectedListener) {
+ mOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
+ rowViewHolder, row);
+ }
}
});
- mRowsFragment.setOnItemClickedListener(new OnItemClickedListener() {
+ mRowsFragment.setOnItemViewClickedListener(new OnItemViewClickedListener() {
@Override
- public void onItemClicked(Object item, Row row) {
+ public void onItemClicked(ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
+ if (DEBUG) Log.v(TAG, String.format("onItemClicked %d", position));
if (null != mOnItemClickedListener) {
mOnItemClickedListener.onItemClicked(item, row);
}
+ if (null != mOnItemViewClickedListener) {
+ mOnItemViewClickedListener.onItemClicked(itemViewHolder, item,
+ rowViewHolder, row);
+ }
}
});
mRowsFragment.setExpand(true);
@@ -205,11 +253,28 @@
list.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
}
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (null == mSpeechRecognizer) {
+ mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(getActivity());
+ mSearchBar.setSpeechRecognizer(mSpeechRecognizer);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ if (null != mSpeechRecognizer) {
+ mSearchBar.setSpeechRecognizer(null);
+ mSpeechRecognizer.destroy();
+ mSpeechRecognizer = null;
+ }
+ super.onPause();
+ }
+
/**
- * Set the search provider, which is responsible for returning items given
- * a search term
- *
- * @param searchResultProvider the search provider
+ * Set the search provider that is responsible for returning results for the
+ * search query.
*/
public void setSearchResultProvider(SearchResultProvider searchResultProvider) {
mProvider = searchResultProvider;
@@ -217,20 +282,101 @@
}
/**
- * Sets an item selection listener.
- * @param listener the item selection listener
+ * Sets an item selection listener for the results.
+ *
+ * @param listener The item selection listener to be invoked when an item in
+ * the search results is selected.
+ * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)}
*/
public void setOnItemSelectedListener(OnItemSelectedListener listener) {
mOnItemSelectedListener = listener;
}
/**
- * Sets an item clicked listener.
+ * Sets an item clicked listener for the results.
+ *
+ * @param listener The item clicked listener to be invoked when an item in
+ * the search results is clicked.
+ * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)}
*/
public void setOnItemClickedListener(OnItemClickedListener listener) {
mOnItemClickedListener = listener;
}
+ /**
+ * Sets an item selection listener for the results.
+ *
+ * @param listener The item selection listener to be invoked when an item in
+ * the search results is selected.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mOnItemViewSelectedListener = listener;
+ }
+
+ /**
+ * Sets an item clicked listener for the results.
+ *
+ * @param listener The item clicked listener to be invoked when an item in
+ * the search results is clicked.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ }
+
+ /**
+ * Sets the title string to be be shown in an empty search bar. The title
+ * may be placed in a call-to-action, such as "Search <i>title</i>" or
+ * "Speak to search <i>title</i>".
+ */
+ public void setTitle(String title) {
+ mTitle = title;
+ if (null != mSearchBar) {
+ mSearchBar.setTitle(title);
+ }
+ }
+
+ /**
+ * Returns the title set in the search bar.
+ */
+ public String getTitle() {
+ if (null != mSearchBar) {
+ return mSearchBar.getTitle();
+ }
+ return null;
+ }
+
+ /**
+ * Sets the badge drawable that will be shown inside the search bar next to
+ * the title.
+ */
+ public void setBadgeDrawable(Drawable drawable) {
+ mBadgeDrawable = drawable;
+ if (null != mSearchBar) {
+ mSearchBar.setBadgeDrawable(drawable);
+ }
+ }
+
+ /**
+ * Returns the badge drawable in the search bar.
+ */
+ public Drawable getBadgeDrawable() {
+ if (null != mSearchBar) {
+ return mSearchBar.getBadgeDrawable();
+ }
+ return null;
+ }
+
+ /**
+ * Display the completions shown by the IME. An application may provide
+ * a list of query completions that the system will show in the IME.
+ *
+ * @param completions A list of completions to show in the IME. Setting to
+ * null or empty will clear the list.
+ */
+ public void displayCompletions(List<String> completions) {
+ mSearchBar.displayCompletions(completions);
+ }
+
private void retrieveResults(String searchQuery) {
if (DEBUG) Log.v(TAG, String.format("retrieveResults %s", searchQuery));
mProvider.onQueryTextChange(searchQuery);
@@ -258,4 +404,20 @@
}
}
+ private void readArguments(Bundle args) {
+ if (null == args) {
+ return;
+ }
+ if (args.containsKey(ARG_QUERY)) {
+ setSearchQuery(args.getString(ARG_QUERY));
+ }
+
+ if (args.containsKey(ARG_TITLE)) {
+ setTitle(args.getString(ARG_TITLE));
+ }
+ }
+
+ private void setSearchQuery(String query) {
+ mSearchBar.setSearchQuery(query);
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/TitleTransitionHelper.java b/v17/leanback/src/android/support/v17/leanback/app/TitleTransitionHelper.java
new file mode 100644
index 0000000..288c9eb
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/TitleTransitionHelper.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.app;
+
+import android.support.v17.leanback.widget.TitleView;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+class TitleTransitionHelper {
+
+ private static Interpolator createTransitionInterpolatorUp() {
+ return new DecelerateInterpolator(4);
+ }
+
+ private static Interpolator createTransitionInterpolatorDown() {
+ return new DecelerateInterpolator();
+ }
+
+ static public Object createTransitionTitleUp(TransitionHelper helper) {
+ Object transition = helper.createChangeBounds(false);
+ helper.setInterpolator(transition, createTransitionInterpolatorUp());
+ return transition;
+ }
+
+ static public Object createTransitionTitleDown(TransitionHelper helper) {
+ Object transition = helper.createChangeBounds(false);
+ helper.setInterpolator(transition, createTransitionInterpolatorDown());
+ return transition;
+ }
+
+ static public void showTitle(TitleView view, boolean show) {
+ MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
+ lp.topMargin = show ? 0 : -view.getHeight();
+ view.setLayoutParams(lp);
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/TransitionHelper.java b/v17/leanback/src/android/support/v17/leanback/app/TransitionHelper.java
index 78c2766..9a5a8fe 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/TransitionHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/TransitionHelper.java
@@ -13,9 +13,7 @@
*/
package android.support.v17.leanback.app;
-import android.content.Context;
import android.os.Build;
-import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
@@ -27,6 +25,12 @@
public static final int FADE_IN = 0x1;
public static final int FADE_OUT = 0x2;
+ public static final int SLIDE_LEFT = 0;
+ public static final int SLIDE_TOP = 1;
+ public static final int SLIDE_RIGHT = 2;
+ public static final int SLIDE_BOTTOM = 3;
+
+ private final static TransitionHelper sHelper = new TransitionHelper();
TransitionHelperVersionImpl mImpl;
/**
@@ -51,6 +55,8 @@
public Object createAutoTransition();
+ public Object createSlide(SlideCallback callback);
+
public Object createFadeTransition(int fadingMode);
public Object createChangeBounds(boolean reparent);
@@ -68,7 +74,7 @@
public void addTransition(Object transitionSet, Object transition);
- public void setTransitionCompleteListener(Object transition, Runnable listener);
+ public void setTransitionListener(Object transition, TransitionListener listener);
public void runTransition(Object scene, Object transition);
@@ -84,6 +90,12 @@
public void include(Object transition, View targetView);
+ public void setStartDelay(Object transition, long startDelay);
+
+ public void setDuration(Object transition, long duration);
+
+ public void setInterpolator(Object transition, Object timeInterpolator);
+
}
/**
@@ -92,7 +104,7 @@
private static final class TransitionHelperStubImpl implements TransitionHelperVersionImpl {
private static class TransitionStub {
- Runnable mCompleteListener;
+ TransitionListener mTransitionListener;
}
@Override
@@ -116,6 +128,11 @@
}
@Override
+ public Object createSlide(SlideCallback callback) {
+ return new TransitionStub();
+ }
+
+ @Override
public void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay) {
}
@@ -166,21 +183,36 @@
}
@Override
- public void setTransitionCompleteListener(Object transition, Runnable listener) {
- ((TransitionStub) transition).mCompleteListener = listener;
+ public void setStartDelay(Object transition, long startDelay) {
+ }
+
+ @Override
+ public void setDuration(Object transition, long duration) {
+ }
+
+ @Override
+ public void setTransitionListener(Object transition, TransitionListener listener) {
+ ((TransitionStub) transition).mTransitionListener = listener;
}
@Override
public void runTransition(Object scene, Object transition) {
+ TransitionStub transitionStub = (TransitionStub) transition;
+ if (transitionStub != null && transitionStub.mTransitionListener != null) {
+ transitionStub.mTransitionListener.onTransitionStart(transition);
+ }
Runnable r = ((Runnable) scene);
if (r != null) {
r.run();
}
- TransitionStub transitionStub = (TransitionStub) transition;
- if (transitionStub != null && transitionStub.mCompleteListener != null) {
- transitionStub.mCompleteListener.run();
+ if (transitionStub != null && transitionStub.mTransitionListener != null) {
+ transitionStub.mTransitionListener.onTransitionEnd(transition);
}
}
+
+ @Override
+ public void setInterpolator(Object transition, Object timeInterpolator) {
+ }
}
/**
@@ -189,8 +221,8 @@
private static final class TransitionHelperKitkatImpl implements TransitionHelperVersionImpl {
private final TransitionHelperKitkat mTransitionHelper;
- TransitionHelperKitkatImpl(Context context) {
- mTransitionHelper = new TransitionHelperKitkat(context);
+ TransitionHelperKitkatImpl() {
+ mTransitionHelper = new TransitionHelperKitkat();
}
@Override
@@ -214,6 +246,11 @@
}
@Override
+ public Object createSlide(SlideCallback callback) {
+ return mTransitionHelper.createSlide(callback);
+ }
+
+ @Override
public void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay) {
mTransitionHelper.setChangeBoundsStartDelay(changeBounds, view, startDelay);
}
@@ -275,25 +312,42 @@
}
@Override
- public void setTransitionCompleteListener(Object transition, Runnable listener) {
- mTransitionHelper.setTransitionCompleteListener(transition, listener);
+ public void setStartDelay(Object transition, long startDelay) {
+ mTransitionHelper.setStartDelay(transition, startDelay);
+ }
+
+ @Override
+ public void setDuration(Object transition, long duration) {
+ mTransitionHelper.setDuration(transition, duration);
+ }
+
+ @Override
+ public void setTransitionListener(Object transition, TransitionListener listener) {
+ mTransitionHelper.setTransitionListener(transition, listener);
}
@Override
public void runTransition(Object scene, Object transition) {
mTransitionHelper.runTransition(scene, transition);
}
+
+ @Override
+ public void setInterpolator(Object transition, Object timeInterpolator) {
+ mTransitionHelper.setInterpolator(transition, timeInterpolator);
+ }
}
/**
* Returns the TransitionHelper that can be used to perform Transition
* animations.
- *
- * @param context A context for accessing system resources.
*/
- public TransitionHelper(Context context) {
+ public static TransitionHelper getInstance() {
+ return sHelper;
+ }
+
+ private TransitionHelper() {
if (systemSupportsTransitions()) {
- mImpl = new TransitionHelperKitkatImpl(context);
+ mImpl = new TransitionHelperKitkatImpl();
} else {
mImpl = new TransitionHelperStubImpl();
}
@@ -327,6 +381,10 @@
return mImpl.createTransitionSet(sequential);
}
+ public Object createSlide(SlideCallback callback) {
+ return mImpl.createSlide(callback);
+ }
+
public void addTransition(Object transitionSet, Object transition) {
mImpl.addTransition(transitionSet, transition);
}
@@ -355,6 +413,14 @@
mImpl.include(transition, targetView);
}
+ public void setStartDelay(Object transition, long startDelay) {
+ mImpl.setStartDelay(transition, startDelay);
+ }
+
+ public void setDuration(Object transition, long duration) {
+ mImpl.setDuration(transition, duration);
+ }
+
public Object createAutoTransition() {
return mImpl.createAutoTransition();
}
@@ -363,11 +429,15 @@
return mImpl.createFadeTransition(fadeMode);
}
- public void setTransitionCompleteListener(Object transition, Runnable listener) {
- mImpl.setTransitionCompleteListener(transition, listener);
+ public void setTransitionListener(Object transition, TransitionListener listener) {
+ mImpl.setTransitionListener(transition, listener);
}
public void runTransition(Object scene, Object transition) {
mImpl.runTransition(scene, transition);
}
+
+ public void setInterpolator(Object transition, Object timeInterpolator) {
+ mImpl.setInterpolator(transition, timeInterpolator);
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
index ff4cddf..01467f0 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
@@ -14,108 +14,110 @@
package android.support.v17.leanback.app;
import android.support.v17.leanback.R;
+import android.support.v17.leanback.widget.OnItemViewClickedListener;
+import android.support.v17.leanback.widget.OnItemViewSelectedListener;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.RowPresenter;
+import android.support.v17.leanback.widget.TitleView;
import android.support.v17.leanback.widget.VerticalGridPresenter;
import android.support.v17.leanback.widget.ObjectAdapter;
import android.support.v17.leanback.widget.OnItemClickedListener;
import android.support.v17.leanback.widget.OnItemSelectedListener;
-import android.support.v17.leanback.widget.Presenter;
-import android.support.v17.leanback.widget.PresenterSelector;
-import android.support.v17.leanback.widget.RowPresenter;
import android.support.v17.leanback.widget.SearchOrbView;
-import android.support.v17.leanback.widget.VerticalGridView;
-import android.util.Log;
import android.app.Fragment;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
import android.widget.ImageView;
import android.widget.TextView;
/**
- * Leanback fragment for a vertical grid.
+ * A fragment for creating leanback vertical grids.
*
- * Renders a vertical grid of objects given a {@link VerticalGridPresenter} and
+ * <p>Renders a vertical grid of objects given a {@link VerticalGridPresenter} and
* an {@link ObjectAdapter}.
*/
public class VerticalGridFragment extends Fragment {
private static final String TAG = "VerticalGridFragment";
private static boolean DEBUG = false;
- private Params mParams;
+ private BrowseFrameLayout mBrowseFrame;
+ private String mTitle;
+ private Drawable mBadgeDrawable;
private ObjectAdapter mAdapter;
private VerticalGridPresenter mGridPresenter;
private VerticalGridPresenter.ViewHolder mGridViewHolder;
private OnItemSelectedListener mOnItemSelectedListener;
private OnItemClickedListener mOnItemClickedListener;
+ private OnItemViewSelectedListener mOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
private View.OnClickListener mExternalOnSearchClickedListener;
private int mSelectedPosition = -1;
- private ImageView mBadgeView;
- private TextView mTitleView;
- private ViewGroup mBrowseTitle;
- private SearchOrbView mSearchOrbView;
+ private TitleView mTitleView;
+ private SearchOrbView.Colors mSearchAffordanceColors;
+ private boolean mSearchAffordanceColorSet;
+ private boolean mShowingTitle = true;
- public static class Params {
- private String mTitle;
- private Drawable mBadgeDrawable;
+ // transition related
+ private static TransitionHelper sTransitionHelper = TransitionHelper.getInstance();
+ private Object mTitleUpTransition;
+ private Object mTitleDownTransition;
+ private Object mSceneWithTitle;
+ private Object mSceneWithoutTitle;
- /**
- * Sets the badge image.
- */
- public void setBadgeImage(Drawable drawable) {
+ /**
+ * Sets the badge drawable displayed in the title area.
+ */
+ public void setBadgeDrawable(Drawable drawable) {
+ if (drawable != mBadgeDrawable) {
mBadgeDrawable = drawable;
- }
-
- /**
- * Returns the badge image.
- */
- public Drawable getBadgeImage() {
- return mBadgeDrawable;
- }
-
- /**
- * Sets a title for the browse fragment.
- */
- public void setTitle(String title) {
- mTitle = title;
- }
-
- /**
- * Returns the title for the browse fragment.
- */
- public String getTitle() {
- return mTitle;
+ if (mTitleView != null) {
+ mTitleView.setBadgeDrawable(drawable);
+ }
}
}
/**
- * Set fragment parameters.
+ * Returns the badge drawable.
*/
- public void setParams(Params params) {
- mParams = params;
- setBadgeDrawable(mParams.mBadgeDrawable);
- setTitle(mParams.mTitle);
+ public Drawable getBadgeDrawable() {
+ return mBadgeDrawable;
}
/**
- * Returns fragment parameters.
+ * Sets a title for the fragment.
*/
- public Params getParams() {
- return mParams;
+ public void setTitle(String title) {
+ mTitle = title;
+ if (mTitleView != null) {
+ mTitleView.setTitle(mTitle);
+ }
}
/**
- * Set the grid presenter.
+ * Returns the title for the fragment.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Sets the grid presenter.
*/
public void setGridPresenter(VerticalGridPresenter gridPresenter) {
if (gridPresenter == null) {
throw new IllegalArgumentException("Grid presenter may not be null");
}
mGridPresenter = gridPresenter;
- if (mOnItemSelectedListener != null) {
- mGridPresenter.setOnItemSelectedListener(mOnItemSelectedListener);
+ mGridPresenter.setOnItemViewSelectedListener(mRowSelectedListener);
+ if (mOnItemViewClickedListener != null) {
+ mGridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener);
}
if (mOnItemClickedListener != null) {
mGridPresenter.setOnItemClickedListener(mOnItemClickedListener);
@@ -144,20 +146,58 @@
return mAdapter;
}
+ final private OnItemViewSelectedListener mRowSelectedListener =
+ new OnItemViewSelectedListener() {
+ @Override
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row) {
+ int position = mGridViewHolder.getGridView().getSelectedPosition();
+ if (DEBUG) Log.v(TAG, "row selected position " + position);
+ onRowSelected(position);
+ if (mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onItemSelected(item, row);
+ }
+ if (mOnItemViewSelectedListener != null) {
+ mOnItemViewSelectedListener.onItemSelected(itemViewHolder, item,
+ rowViewHolder, row);
+ }
+ }
+ };
+
/**
* Sets an item selection listener.
+ * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)}
*/
public void setOnItemSelectedListener(OnItemSelectedListener listener) {
mOnItemSelectedListener = listener;
- if (mGridPresenter != null) {
- mGridPresenter.setOnItemSelectedListener(mOnItemSelectedListener);
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mOnItemViewSelectedListener = listener;
+ }
+
+ private void onRowSelected(int position) {
+ if (position != mSelectedPosition) {
+ if (!mGridViewHolder.getGridView().hasPreviousViewInSameRow(position)) {
+ // if has no sibling in front of it, show title
+ if (!mShowingTitle) {
+ sTransitionHelper.runTransition(mSceneWithTitle, mTitleDownTransition);
+ mShowingTitle = true;
+ }
+ } else if (mShowingTitle) {
+ sTransitionHelper.runTransition(mSceneWithoutTitle, mTitleUpTransition);
+ mShowingTitle = false;
+ }
+ mSelectedPosition = position;
}
}
- // TODO: getitemselectedlistener?
-
/**
* Sets an item clicked listener.
+ * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)}
*/
public void setOnItemClickedListener(OnItemClickedListener listener) {
mOnItemClickedListener = listener;
@@ -168,63 +208,146 @@
/**
* Returns the item clicked listener.
+ * @deprecated Use {@link #getOnItemViewClickedListener()}
*/
public OnItemClickedListener getOnItemClickedListener() {
return mOnItemClickedListener;
}
/**
+ * Sets an item clicked listener.
+ */
+ public void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ if (mGridPresenter != null) {
+ mGridPresenter.setOnItemViewClickedListener(mOnItemViewClickedListener);
+ }
+ }
+
+ /**
+ * Returns the item clicked listener.
+ */
+ public OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
+
+ /**
* Sets a click listener for the search affordance.
*
- * The presence of a listener will change the visibility of the search affordance in the
- * title area. When set to non-null the title area will contain a call to search action.
+ * <p>The presence of a listener will change the visibility of the search
+ * affordance in the title area. When set to non-null, the title area will
+ * contain a call to search action.
*
- * The listener onClick method will be invoked when the user click on the search action.
+ * <p>The listener's onClick method will be invoked when the user clicks on
+ * the search action.
*
- * @param listener The listener.
+ * @param listener The listener to invoke when the search affordance is
+ * clicked, or null to hide the search affordance.
*/
public void setOnSearchClickedListener(View.OnClickListener listener) {
mExternalOnSearchClickedListener = listener;
- if (mSearchOrbView != null) {
- mSearchOrbView.setOnOrbClickedListener(listener);
- }
- }
-
- private void setBadgeDrawable(Drawable drawable) {
- if (mBadgeView == null) {
- return;
- }
- mBadgeView.setImageDrawable(drawable);
- if (drawable != null) {
- mBadgeView.setVisibility(View.VISIBLE);
- } else {
- mBadgeView.setVisibility(View.GONE);
- }
- }
-
- private void setTitle(String title) {
if (mTitleView != null) {
- mTitleView.setText(title);
+ mTitleView.setOnSearchClickedListener(listener);
}
}
+ /**
+ * Sets the {@link SearchOrbView.Colors} used to draw the search affordance.
+ */
+ public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
+ mSearchAffordanceColors = colors;
+ mSearchAffordanceColorSet = true;
+ if (mTitleView != null) {
+ mTitleView.setSearchAffordanceColors(mSearchAffordanceColors);
+ }
+ }
+
+ /**
+ * Returns the {@link SearchOrbView.Colors} used to draw the search affordance.
+ */
+ public SearchOrbView.Colors getSearchAffordanceColors() {
+ if (mSearchAffordanceColorSet) {
+ return mSearchAffordanceColors;
+ }
+ if (mTitleView == null) {
+ throw new IllegalStateException("Fragment views not yet created");
+ }
+ return mTitleView.getSearchAffordanceColors();
+ }
+
+ /**
+ * Sets the color used to draw the search affordance.
+ * A default brighter color will be set by the framework.
+ *
+ * @param color The color to use for the search affordance.
+ */
+ public void setSearchAffordanceColor(int color) {
+ setSearchAffordanceColors(new SearchOrbView.Colors(color));
+ }
+
+ /**
+ * Returns the color used to draw the search affordance.
+ */
+ public int getSearchAffordanceColor() {
+ return getSearchAffordanceColors().color;
+ }
+
+ private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
+ new BrowseFrameLayout.OnFocusSearchListener() {
+ @Override
+ public View onFocusSearch(View focused, int direction) {
+ if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
+
+ final View searchOrbView = mTitleView.getSearchAffordanceView();
+ if (focused == searchOrbView && (
+ direction == View.FOCUS_DOWN || direction == View.FOCUS_RIGHT)) {
+ return mGridViewHolder.view;
+
+ } else if (focused != searchOrbView && searchOrbView.getVisibility() == View.VISIBLE
+ && direction == View.FOCUS_UP) {
+ return searchOrbView;
+
+ } else {
+ return null;
+ }
+ }
+ };
+
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- View root = inflater.inflate(R.layout.lb_vertical_grid_fragment, container, false);
+ ViewGroup root = (ViewGroup) inflater.inflate(R.layout.lb_vertical_grid_fragment,
+ container, false);
- mBrowseTitle = (ViewGroup) root.findViewById(R.id.browse_title_group);
- mBadgeView = (ImageView) mBrowseTitle.findViewById(R.id.browse_badge);
- mTitleView = (TextView) mBrowseTitle.findViewById(R.id.browse_title);
- mSearchOrbView = (SearchOrbView) mBrowseTitle.findViewById(R.id.browse_orb);
+ mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
+ mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener);
+
+ mTitleView = (TitleView) root.findViewById(R.id.browse_title_group);
+ mTitleView.setBadgeDrawable(mBadgeDrawable);
+ mTitleView.setTitle(mTitle);
+ if (mSearchAffordanceColorSet) {
+ mTitleView.setSearchAffordanceColors(mSearchAffordanceColors);
+ }
if (mExternalOnSearchClickedListener != null) {
- mSearchOrbView.setOnOrbClickedListener(mExternalOnSearchClickedListener);
+ mTitleView.setOnSearchClickedListener(mExternalOnSearchClickedListener);
}
- if (mParams != null) {
- setBadgeDrawable(mParams.mBadgeDrawable);
- setTitle(mParams.mTitle);
- }
+ mSceneWithTitle = sTransitionHelper.createScene(root, new Runnable() {
+ @Override
+ public void run() {
+ TitleTransitionHelper.showTitle(mTitleView, true);
+ }
+ });
+ mSceneWithoutTitle = sTransitionHelper.createScene(root, new Runnable() {
+ @Override
+ public void run() {
+ TitleTransitionHelper.showTitle(mTitleView, false);
+ }
+ });
+ mTitleUpTransition = TitleTransitionHelper.createTransitionTitleUp(sTransitionHelper);
+ mTitleDownTransition = TitleTransitionHelper.createTransitionTitleDown(sTransitionHelper);
+ sTransitionHelper.excludeChildren(mTitleUpTransition, R.id.browse_grid_dock, true);
+ sTransitionHelper.excludeChildren(mTitleDownTransition, R.id.browse_grid_dock, true);
return root;
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
index c42f3e0..1d9ff8f 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
@@ -35,20 +35,55 @@
private final TextView mTitle;
private final TextView mSubtitle;
private final TextView mBody;
- private final int mUnderTitleSpacing;
- private final int mUnderSubtitleSpacing;
+ private final int mTitleMargin;
+ private final int mUnderTitleBaselineMargin;
+ private final int mUnderSubtitleBaselineMargin;
+ private final int mTitleLineSpacing;
+ private final int mBodyLineSpacing;
+ private final int mBodyMaxLines;
+ private final int mBodyMinLines;
+ private final FontMetricsInt mTitleFontMetricsInt;
+ private final FontMetricsInt mSubtitleFontMetricsInt;
+ private final FontMetricsInt mBodyFontMetricsInt;
public ViewHolder(View view) {
super(view);
mTitle = (TextView) view.findViewById(R.id.lb_details_description_title);
mSubtitle = (TextView) view.findViewById(R.id.lb_details_description_subtitle);
mBody = (TextView) view.findViewById(R.id.lb_details_description_body);
- int interTextSpacing = view.getContext().getResources().getDimensionPixelSize(
- R.dimen.lb_details_overview_description_intertext_spacing);
+
FontMetricsInt titleFontMetricsInt = getFontMetricsInt(mTitle);
- mUnderTitleSpacing = interTextSpacing - titleFontMetricsInt.descent;
- FontMetricsInt subtitleFontMetricsInt = getFontMetricsInt(mSubtitle);
- mUnderSubtitleSpacing = interTextSpacing - subtitleFontMetricsInt.descent;
+ final int titleAscent = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_title_baseline);
+ // Ascent is negative
+ mTitleMargin = titleAscent + titleFontMetricsInt.ascent;
+
+ mUnderTitleBaselineMargin = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_under_title_baseline_margin);
+ mUnderSubtitleBaselineMargin = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_under_subtitle_baseline_margin);
+
+ mTitleLineSpacing = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_title_line_spacing);
+ mBodyLineSpacing = view.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_description_body_line_spacing);
+
+ mBodyMaxLines = view.getResources().getInteger(
+ R.integer.lb_details_description_body_max_lines);
+ mBodyMinLines = view.getResources().getInteger(
+ R.integer.lb_details_description_body_min_lines);
+
+ mTitleFontMetricsInt = getFontMetricsInt(mTitle);
+ mSubtitleFontMetricsInt = getFontMetricsInt(mSubtitle);
+ mBodyFontMetricsInt = getFontMetricsInt(mBody);
+
+ mTitle.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ mBody.setMaxLines(mTitle.getLineCount() > 1 ? mBodyMinLines : mBodyMaxLines);
+ }
+ });
}
public TextView getTitle() {
@@ -90,7 +125,10 @@
hasTitle = false;
} else {
vh.mTitle.setVisibility(View.VISIBLE);
+ vh.mTitle.setLineSpacing(vh.mTitleLineSpacing - vh.mTitle.getLineHeight() +
+ vh.mTitle.getLineSpacingExtra(), vh.mTitle.getLineSpacingMultiplier());
}
+ setTopMargin(vh.mTitle, vh.mTitleMargin);
boolean hasSubtitle = true;
if (TextUtils.isEmpty(vh.mSubtitle.getText())) {
@@ -99,7 +137,8 @@
} else {
vh.mSubtitle.setVisibility(View.VISIBLE);
if (hasTitle) {
- setTopMargin(vh.mSubtitle, vh.mUnderTitleSpacing);
+ setTopMargin(vh.mSubtitle, vh.mUnderTitleBaselineMargin +
+ vh.mSubtitleFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent);
} else {
setTopMargin(vh.mSubtitle, 0);
}
@@ -109,10 +148,15 @@
vh.mBody.setVisibility(View.GONE);
} else {
vh.mBody.setVisibility(View.VISIBLE);
+ vh.mBody.setLineSpacing(vh.mBodyLineSpacing - vh.mBody.getLineHeight() +
+ vh.mBody.getLineSpacingExtra(), vh.mBody.getLineSpacingMultiplier());
+
if (hasSubtitle) {
- setTopMargin(vh.mBody, vh.mUnderSubtitleSpacing);
+ setTopMargin(vh.mBody, vh.mUnderSubtitleBaselineMargin +
+ vh.mBodyFontMetricsInt.ascent - vh.mSubtitleFontMetricsInt.descent);
} else if (hasTitle) {
- setTopMargin(vh.mBody, vh.mUnderTitleSpacing);
+ setTopMargin(vh.mBody, vh.mUnderTitleBaselineMargin +
+ vh.mBodyFontMetricsInt.ascent - vh.mTitleFontMetricsInt.descent);
} else {
setTopMargin(vh.mBody, 0);
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Action.java b/v17/leanback/src/android/support/v17/leanback/widget/Action.java
index 31d0fb5..deb36c4 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/Action.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/Action.java
@@ -29,18 +29,44 @@
private CharSequence mLabel1;
private CharSequence mLabel2;
+ /**
+ * Constructor for an Action.
+ *
+ * @param id The id of the Action.
+ */
public Action(long id) {
this(id, "");
}
+ /**
+ * Constructor for an Action.
+ *
+ * @param id The id of the Action.
+ * @param label The label to display for the Action.
+ */
public Action(long id, CharSequence label) {
this(id, label, null);
}
+ /**
+ * Constructor for an Action.
+ *
+ * @param id The id of the Action.
+ * @param label1 The label to display on the first line of the Action.
+ * @param label2 The label to display on the second line of the Action.
+ */
public Action(long id, CharSequence label1, CharSequence label2) {
this(id, label1, label2, null);
}
+ /**
+ * Constructor for an Action.
+ *
+ * @param id The id of the Action.
+ * @param label1 The label to display on the first line of the Action.
+ * @param label2 The label to display on the second line of the Action.
+ * @param icon The icon to display for the Action.
+ */
public Action(long id, CharSequence label1, CharSequence label2, Drawable icon) {
setId(id);
setLabel1(label1);
@@ -49,56 +75,56 @@
}
/**
- * Set id for this action.
+ * Set id for this Action.
*/
public final void setId(long id) {
mId = id;
}
/**
- * Returns the id for this action.
+ * Returns the id for this Action.
*/
public final long getId() {
return mId;
}
/**
- * Set the first line label for this action.
+ * Set the first line label for this Action.
*/
public final void setLabel1(CharSequence label) {
mLabel1 = label;
}
/**
- * Returns the first line label for this action.
+ * Returns the first line label for this Action.
*/
public final CharSequence getLabel1() {
return mLabel1;
}
/**
- * Set the second line label for this action.
+ * Set the second line label for this Action.
*/
public final void setLabel2(CharSequence label) {
mLabel2 = label;
}
/**
- * Returns the second line label for this action.
+ * Returns the second line label for this Action.
*/
public final CharSequence getLabel2() {
return mLabel2;
}
/**
- * Set the icon drawable for this action.
+ * Set the icon drawable for this Action.
*/
public final void setIcon(Drawable icon) {
mIcon = icon;
}
/**
- * Returns the icon drawable for this action.
+ * Returns the icon drawable for this Action.
*/
public final Drawable getIcon() {
return mIcon;
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java b/v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
index 7698872..bd87e14 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
@@ -18,8 +18,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
+import android.widget.Button;
class ActionPresenterSelector extends PresenterSelector {
@@ -47,13 +46,11 @@
static class ActionViewHolder extends Presenter.ViewHolder {
Action mAction;
- ImageView mIconView;
- TextView mLabel;
+ Button mButton;
public ActionViewHolder(View view) {
super(view);
- mIconView = (ImageView) view.findViewById(R.id.lb_action_icon);
- mLabel = (TextView) view.findViewById(R.id.lb_action_text);
+ mButton = (Button) view.findViewById(R.id.lb_action_button);
}
}
@@ -80,7 +77,7 @@
Action action = (Action) item;
ActionViewHolder vh = (ActionViewHolder) viewHolder;
vh.mAction = action;
- vh.mLabel.setText(action.getLabel1());
+ vh.mButton.setText(action.getLabel1());
}
@Override
@@ -113,32 +110,34 @@
ActionViewHolder vh = (ActionViewHolder) viewHolder;
vh.mAction = action;
- int horizontalPadding = vh.view.getContext().getResources()
- .getDimensionPixelSize(R.dimen.lb_action_1_line_padding_left);
if (action.getIcon() != null) {
- vh.view.setPadding(0, 0, horizontalPadding, 0);
- vh.mIconView.setVisibility(View.VISIBLE);
- // TODO: scale this?
- vh.mIconView.setImageDrawable(action.getIcon());
+ final int leftPadding = vh.view.getResources()
+ .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_left);
+ final int rightPadding = vh.view.getResources()
+ .getDimensionPixelSize(R.dimen.lb_action_with_icon_padding_right);
+ vh.view.setPadding(leftPadding, 0, rightPadding, 0);
} else {
- vh.view.setPadding(horizontalPadding, 0, horizontalPadding, 0);
- vh.mIconView.setVisibility(View.GONE);
+ final int padding = vh.view.getResources()
+ .getDimensionPixelSize(R.dimen.lb_action_padding_horizontal);
+ vh.view.setPadding(padding, 0, padding, 0);
}
+ vh.mButton.setCompoundDrawablesWithIntrinsicBounds(action.getIcon(), null, null, null);
+
CharSequence line1 = action.getLabel1();
CharSequence line2 = action.getLabel2();
if (TextUtils.isEmpty(line1)) {
- vh.mLabel.setText(line2);
+ vh.mButton.setText(line2);
} else if (TextUtils.isEmpty(line2)) {
- vh.mLabel.setText(line1);
+ vh.mButton.setText(line1);
} else {
- vh.mLabel.setText(line1 + "\n" + line2);
+ vh.mButton.setText(line1 + "\n" + line2);
}
}
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
ActionViewHolder vh = (ActionViewHolder) viewHolder;
- vh.mIconView.setVisibility(View.GONE);
+ vh.mButton.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
vh.view.setPadding(0, 0, 0, 0);
vh.mAction = null;
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
index 74bb038..6c4ee28 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
@@ -17,20 +17,29 @@
import java.util.Collection;
/**
- * Adapter implemented with an {@link ArrayList}.
+ * An ObjectAdapter implemented with an {@link ArrayList}.
*/
public class ArrayObjectAdapter extends ObjectAdapter {
private ArrayList<Object> mItems = new ArrayList<Object>();
+ /**
+ * Construct an adapter with the given {@link PresenterSelector}.
+ */
public ArrayObjectAdapter(PresenterSelector presenterSelector) {
super(presenterSelector);
}
+ /**
+ * Construct an adapter that uses the given {@link Presenter} for all items.
+ */
public ArrayObjectAdapter(Presenter presenter) {
super(presenter);
}
+ /**
+ * Construct an adapter.
+ */
public ArrayObjectAdapter() {
super();
}
@@ -46,19 +55,42 @@
}
/**
- * Adds an item to the end of the list.
+ * Returns the index for the first occurrence of item in the adapter, or -1 if
+ * not found.
*
- * @param item The item to add to the end of the list.
+ * @param item The item to find in the list.
+ * @return Index of the first occurrence of the item in the adapter, or -1
+ * if not found.
+ */
+ public int indexOf(Object item) {
+ return mItems.indexOf(item);
+ }
+
+ /**
+ * Notify that the content of a range of items changed. Note that this is
+ * not same as items being added or removed.
+ *
+ * @param positionStart The position of first item that has changed.
+ * @param itemCount The count of how many items have changed.
+ */
+ public void notifyArrayItemRangeChanged(int positionStart, int itemCount) {
+ notifyItemRangeChanged(positionStart, itemCount);
+ }
+
+ /**
+ * Adds an item to the end of the adapter.
+ *
+ * @param item The item to add to the end of the adapter.
*/
public void add(Object item) {
add(mItems.size(), item);
}
/**
- * Inserts an item into this list at the specified index.
+ * Inserts an item into this adapter at the specified index.
*
* @param index The index at which the item should be inserted.
- * @param item The item to insert into the list.
+ * @param item The item to insert into the adapter.
*/
public void add(int index, Object item) {
mItems.add(index, item);
@@ -66,7 +98,7 @@
}
/**
- * Adds the objects in the given collection to the list, starting at the
+ * Adds the objects in the given collection to the adapter, starting at the
* given index.
*
* @param index The index at which the items should be inserted.
@@ -79,10 +111,10 @@
}
/**
- * Removes the first occurrence of the given item from the list.
+ * Removes the first occurrence of the given item from the adapter.
*
- * @param item The item to remove from the list.
- * @return True if the item was found and thus removed from the list.
+ * @param item The item to remove from the adapter.
+ * @return True if the item was found and thus removed from the adapter.
*/
public boolean remove(Object item) {
int index = mItems.indexOf(item);
@@ -94,7 +126,7 @@
}
/**
- * Removes a range of items from the list. The range is specified by giving
+ * Removes a range of items from the adapter. The range is specified by giving
* the starting position and the number of elements to remove.
*
* @param position The index of the first item to remove.
@@ -112,7 +144,7 @@
}
/**
- * Removes all items from this list, leaving it empty.
+ * Removes all items from this adapter, leaving it empty.
*/
public void clear() {
int itemCount = mItems.size();
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java b/v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java
index 20360f7..500e21a 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java
@@ -32,22 +32,20 @@
import java.util.ArrayList;
/**
- * A card style layout that arranges its children in a vertical column according
- * to the card type set for the parent and the card view type property set for
- * these children. A BaseCardView can have from 1 to 3 card areas, depending
- * on the chosen card type. Children are assigned to these areas according to
- * the view type indicated by their layout parameters. The card type defines
- * when these card areas are visible or not, and how they animate inside the
- * parent card. These transitions are triggered when the card changes state. A
- * card has 2 sets of states: Activated(true/false), and Selected(true/false).
- * These states, combined with the card type chosen, determine what areas of the
- * card are visible depending on the current state. They card type also
- * determines the animations that are triggered when transitioning between
- * states. The card states are set by calling {@link #setActivated(boolean)
- * setActivated()} and {@link #setSelected(boolean) setSelected()}.
+ * A card style layout that responds to certain state changes. It arranges its
+ * children in a vertical column, with different regions becoming visible at
+ * different times.
+ *
* <p>
- * See {@link BaseCardView.LayoutParams BaseCardView.LayoutParams} for
- * layout attributes.
+ * A BaseCardView will draw its children based on its type, the region
+ * visibilities of the child types, and the state of the widget. A child may be
+ * marked as belonging to one of three regions: main, info, or extra. The main
+ * region is always visible, while the info and extra regions can be set to
+ * display based on the activated or selected state of the View. The card states
+ * are set by calling {@link #setActivated(boolean) setActivated} and
+ * {@link #setSelected(boolean) setSelected}.
+ * <p>
+ * See {@link BaseCardView.LayoutParams} for layout attributes.
* </p>
*/
public class BaseCardView extends ViewGroup {
@@ -64,34 +62,29 @@
public static final int CARD_TYPE_MAIN_ONLY = 0;
/**
- * A Card type with 2 layout areas: A main area, always visible, and an info
- * area, which is only visible when the card is set to its Active state. The
- * info area fades in over the main area, and does not cause the card height
- * to change.
+ * A Card type with 2 layout areas: A main area which is always visible, and
+ * an info area that fades in over the main area when it is visible.
+ * The card height will not change.
*
* @see #getCardType()
*/
public static final int CARD_TYPE_INFO_OVER = 1;
/**
- * A Card type with 2 layout areas: A main area, always visible, and an info
- * area, which is only visible when the card is set to its Active state. The
- * info area appears below the main area, causing the total card height to
- * change when the card switches between Active and Inactive states.
+ * A Card type with 2 layout areas: A main area which is always visible, and
+ * an info area that appears below the main area. When the info area is visible
+ * the total card height will change.
*
* @see #getCardType()
*/
public static final int CARD_TYPE_INFO_UNDER = 2;
/**
- * A Card type with 3 layout areas: A main area, always visible; an info
- * area, which is only visible when the card is set to its Active state; and
- * an extra area, which only becomes visible when the card is set to
- * Selected state. The info area appears below the main area, causing the
- * total card height to change when the card switches between Active and
- * Inactive states. The extra area only appears if the card stays in its
- * Selected state for a certain (small) amount of time. It animates in at
- * the bottom of the card, shifting up the info view. This does not affect
+ * A Card type with 3 layout areas: A main area which is always visible; an
+ * info area which will appear below the main area, and an extra area that
+ * only appears after a short delay. The info area appears below the main
+ * area, causing the total card height to change. The extra area animates in
+ * at the bottom of the card, shifting up the info view without affecting
* the card height.
*
* @see #getCardType()
@@ -339,6 +332,8 @@
state = View.combineMeasuredStates(state, mainView.getMeasuredState());
}
}
+ setPivotX(mMeasuredWidth / 2);
+ setPivotY(mainHeight / 2);
// The MAIN area determines the card width
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
index 854f5de..43f6d88 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
@@ -19,9 +19,8 @@
import android.support.v17.leanback.R;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
+import android.view.Gravity;
import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.Interpolator;
/**
* Base class for vertically and horizontally scrolling lists. The items come
@@ -104,6 +103,13 @@
protected final GridLayoutManager mLayoutManager;
+ /**
+ * Animate layout changes from a child resizing or adding/removing a child.
+ */
+ private boolean mAnimateChildLayout = true;
+
+ private RecyclerView.ItemAnimator mSavedItemAnimator;
+
public BaseGridView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLayoutManager = new GridLayoutManager(this);
@@ -122,6 +128,9 @@
a.getDimensionPixelSize(R.styleable.lbBaseGridView_verticalMargin, 0));
mLayoutManager.setHorizontalMargin(
a.getDimensionPixelSize(R.styleable.lbBaseGridView_horizontalMargin, 0));
+ if (a.hasValue(R.styleable.lbBaseGridView_android_gravity)) {
+ setGravity(a.getInt(R.styleable.lbBaseGridView_android_gravity, Gravity.NO_GRAVITY));
+ }
a.recycle();
}
@@ -248,6 +257,24 @@
}
/**
+ * Set to true if include padding in calculating item align offset.
+ *
+ * @param withPadding When it is true: we include left/top padding for positive
+ * item offset, include right/bottom padding for negative item offset.
+ */
+ public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
+ mLayoutManager.setItemAlignmentOffsetWithPadding(withPadding);
+ requestLayout();
+ }
+
+ /**
+ * Returns true if include padding in calculating item align offset.
+ */
+ public boolean isItemAlignmentOffsetWithPadding() {
+ return mLayoutManager.isItemAlignmentOffsetWithPadding();
+ }
+
+ /**
* Set offset percent for item alignment in addition to {@link
* #getItemAlignmentOffset()}.
*
@@ -361,7 +388,15 @@
* <p><i>Unstable API, might change later.</i>
*/
public void setAnimateChildLayout(boolean animateChildLayout) {
- mLayoutManager.setAnimateChildLayout(animateChildLayout);
+ if (mAnimateChildLayout != animateChildLayout) {
+ mAnimateChildLayout = animateChildLayout;
+ if (!mAnimateChildLayout) {
+ mSavedItemAnimator = getItemAnimator();
+ super.setItemAnimator(null);
+ } else {
+ super.setItemAnimator(mSavedItemAnimator);
+ }
+ }
}
/**
@@ -370,43 +405,7 @@
* <p><i>Unstable API, might change later.</i>
*/
public boolean isChildLayoutAnimated() {
- return mLayoutManager.isChildLayoutAnimated();
- }
-
- /**
- * Set an interpolator for the animation when a child changes size or when
- * adding or removing a child.
- * <p><i>Unstable API, might change later.</i>
- */
- public void setChildLayoutAnimationInterpolator(Interpolator interpolator) {
- mLayoutManager.setChildLayoutAnimationInterpolator(interpolator);
- }
-
- /**
- * Get the interpolator for the animation when a child changes size or when
- * adding or removing a child.
- * <p><i>Unstable API, might change later.</i>
- */
- public Interpolator getChildLayoutAnimationInterpolator() {
- return mLayoutManager.getChildLayoutAnimationInterpolator();
- }
-
- /**
- * Set the duration of the animation when a child changes size or when
- * adding or removing a child.
- * <p><i>Unstable API, might change later.</i>
- */
- public void setChildLayoutAnimationDuration(long duration) {
- mLayoutManager.setChildLayoutAnimationDuration(duration);
- }
-
- /**
- * Get the duration of the animation when a child changes size or when
- * adding or removing a child.
- * <p><i>Unstable API, might change later.</i>
- */
- public long getChildLayoutAnimationDuration() {
- return mLayoutManager.getChildLayoutAnimationDuration();
+ return mAnimateChildLayout;
}
/**
@@ -467,4 +466,42 @@
return mLayoutManager.isFocusSearchDisabled();
}
+ /**
+ * Enable or disable layout. All children will be removed when layout is
+ * disabled.
+ */
+ public void setLayoutEnabled(boolean layoutEnabled) {
+ mLayoutManager.setLayoutEnabled(layoutEnabled);
+ }
+
+ /**
+ * Enable or disable pruning child. Disable is useful during transition.
+ */
+ public void setPruneChild(boolean pruneChild) {
+ mLayoutManager.setPruneChild(pruneChild);
+ }
+
+ /**
+ * Returns true if the view at the given position has a same row sibling
+ * in front of it.
+ *
+ * @param position Position in adapter.
+ */
+ public boolean hasPreviousViewInSameRow(int position) {
+ return mLayoutManager.hasPreviousViewInSameRow(position);
+ }
+
+ /**
+ * Enable or disable the default "focus draw at last" order rule.
+ */
+ public void setFocusDrawingOrderEnabled(boolean enabled) {
+ super.setChildrenDrawingOrderEnabled(enabled);
+ }
+
+ /**
+ * Returns true if default "focus draw at last" order rule is enabled.
+ */
+ public boolean isFocusDrawingOrderEnabled() {
+ return super.isChildrenDrawingOrderEnabled();
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
index 1968822..8f3cf2c 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
@@ -18,7 +18,7 @@
import android.util.LruCache;
/**
- * Adapter implemented with a {@link Cursor}.
+ * An ObjectAdapter implemented with a {@link Cursor}.
*/
public class CursorObjectAdapter extends ObjectAdapter {
private static final int CACHE_SIZE = 100;
@@ -26,21 +26,31 @@
private CursorMapper mMapper;
private final LruCache<Integer, Object> mItemCache = new LruCache<Integer, Object>(CACHE_SIZE);
+ /**
+ * Construct an adapter with the given {@link PresenterSelector}.
+ */
public CursorObjectAdapter(PresenterSelector presenterSelector) {
super(presenterSelector);
}
+ /**
+ * Construct an adapter that uses the given {@link Presenter} for all items.
+ */
public CursorObjectAdapter(Presenter presenter) {
super(presenter);
}
+ /**
+ * Construct an adapter.
+ */
public CursorObjectAdapter() {
super();
}
/**
* Change the underlying cursor to a new cursor. If there is
- * an existing cursor it will be closed.
+ * an existing cursor it will be closed if it is different than the new
+ * cursor.
*
* @param cursor The new cursor to be used.
*/
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java b/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
index e5dbfaf..60fe6be 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
@@ -23,8 +23,8 @@
import java.util.List;
/**
- * The overview row for a details fragment. This row consists of an image, a
- * description view, and optionally a series of actions that can be taken for
+ * An overview row for a details fragment. This row consists of an image, a
+ * description view, and optionally a series of {@link Action}s that can be taken for
* the item.
*/
public class DetailsOverviewRow extends Row {
@@ -32,9 +32,10 @@
private Object mItem;
private Drawable mImageDrawable;
private ArrayList<Action> mActions = new ArrayList<Action>();
+ private boolean mImageScaleUpAllowed = true;
/**
- * Constructor.
+ * Constructor for a DetailsOverviewRow.
*
* @param item The main item for the details page.
*/
@@ -81,38 +82,53 @@
}
/**
- * Add an action to the overview.
+ * Allows or disallows scaling up of images.
+ * Images will always be scaled down if necessary.
+ */
+ public void setImageScaleUpAllowed(boolean allowed) {
+ mImageScaleUpAllowed = allowed;
+ }
+
+ /**
+ * Returns true if the image may be scaled up; false otherwise.
+ */
+ public boolean isImageScaleUpAllowed() {
+ return mImageScaleUpAllowed;
+ }
+
+ /**
+ * Add an Action to the overview.
*
- * @param action The action to add.
+ * @param action The Action to add.
*/
public final void addAction(Action action) {
mActions.add(action);
}
/**
- * Add an action to the overview at the specified position.
+ * Add an Action to the overview at the specified position.
*
- * @param pos The position to insert the action.
- * @param action The action to add.
+ * @param pos The position to insert the Action.
+ * @param action The Action to add.
*/
public final void addAction(int pos, Action action) {
mActions.add(pos, action);
}
/**
- * Remove the given action from the overview.
+ * Remove the given Action from the overview.
*
- * @param action The action to remove.
- * @return true if the overview contained the specified action.
+ * @param action The Action to remove.
+ * @return true if the overview contained the specified Action.
*/
public final boolean removeAction(Action action) {
return mActions.remove(action);
}
/**
- * Gets a read-only view of the list of actions of this details overview.
+ * Gets a read-only view of the list of Actions of this details overview.
*
- * @return An unmodifiable view of the list of actions.
+ * @return An unmodifiable view of the list of Actions.
*/
public final List<Action> getActions() {
return Collections.unmodifiableList(mActions);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
index 3504581..4269af8 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
@@ -13,7 +13,14 @@
*/
package android.support.v17.leanback.widget;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
import android.support.v17.leanback.R;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -23,9 +30,9 @@
import java.util.Collection;
/**
- * DetailsOverviewRowPresenter renders {@link DetailsOverviewRow} to display an
+ * A DetailsOverviewRowPresenter renders a {@link DetailsOverviewRow} to display an
* overview of an item. Typically this row will be the first row in a fragment
- * such as {@link android.support.v17.leanback.app.DetailsFragment
+ * such as the {@link android.support.v17.leanback.app.DetailsFragment
* DetailsFragment}.
*
* <p>The detailed description is rendered using a {@link Presenter}.
@@ -35,12 +42,109 @@
private static final String TAG = "DetailsOverviewRowPresenter";
private static final boolean DEBUG = false;
+ private static final int MORE_ACTIONS_FADE_MS = 100;
+
+ /**
+ * A ViewHolder for the DetailsOverviewRow.
+ */
public static class ViewHolder extends RowPresenter.ViewHolder {
final ImageView mImageView;
final FrameLayout mDetailsDescriptionFrame;
final HorizontalGridView mActionsRow;
Presenter.ViewHolder mDetailsDescriptionViewHolder;
+ int mNumItems;
+ boolean mShowMoreRight;
+ boolean mShowMoreLeft;
+ void bind(ItemBridgeAdapter bridgeAdapter) {
+ mNumItems = bridgeAdapter.getItemCount();
+ bridgeAdapter.setAdapterListener(mAdapterListener);
+
+ mShowMoreRight = false;
+ mShowMoreLeft = true;
+ showMoreLeft(false);
+ }
+
+ final View.OnLayoutChangeListener mLayoutChangeListener =
+ new View.OnLayoutChangeListener() {
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right,
+ int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ if (DEBUG) Log.v(TAG, "onLayoutChange " + v);
+ checkFirstAndLastPosition(false);
+ }
+ };
+
+ final ItemBridgeAdapter.AdapterListener mAdapterListener =
+ new ItemBridgeAdapter.AdapterListener() {
+
+ @Override
+ public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
+ // Remove first to ensure we don't add ourselves more than once.
+ viewHolder.itemView.removeOnLayoutChangeListener(mLayoutChangeListener);
+ viewHolder.itemView.addOnLayoutChangeListener(mLayoutChangeListener);
+ }
+ @Override
+ public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
+ viewHolder.itemView.removeOnLayoutChangeListener(mLayoutChangeListener);
+ checkFirstAndLastPosition(false);
+ }
+ };
+
+ final RecyclerView.OnScrollListener mScrollListener =
+ new RecyclerView.OnScrollListener() {
+
+ @Override
+ public void onScrollStateChanged(int newState) {
+ }
+ @Override
+ public void onScrolled(int dx, int dy) {
+ checkFirstAndLastPosition(true);
+ }
+ };
+
+ private int getViewCenter(View view) {
+ return (view.getRight() - view.getLeft()) / 2;
+ }
+
+ private void checkFirstAndLastPosition(boolean fromScroll) {
+ RecyclerView.ViewHolder viewHolder;
+
+ viewHolder = mActionsRow.findViewHolderForPosition(mNumItems - 1);
+ boolean showRight = (viewHolder == null ||
+ viewHolder.itemView.getRight() > mActionsRow.getWidth());
+
+ viewHolder = mActionsRow.findViewHolderForPosition(0);
+ boolean showLeft = (viewHolder == null || viewHolder.itemView.getLeft() < 0);
+
+ if (DEBUG) Log.v(TAG, "checkFirstAndLast fromScroll " + fromScroll +
+ " showRight " + showRight + " showLeft " + showLeft);
+
+ showMoreRight(showRight);
+ showMoreLeft(showLeft);
+ }
+
+ private void showMoreLeft(boolean show) {
+ if (show != mShowMoreLeft) {
+ mActionsRow.setFadingLeftEdge(show);
+ mShowMoreLeft = show;
+ }
+ }
+
+ private void showMoreRight(boolean show) {
+ if (show != mShowMoreRight) {
+ mActionsRow.setFadingRightEdge(show);
+ mShowMoreRight = show;
+ }
+ }
+
+ /**
+ * Constructor for the ViewHolder.
+ *
+ * @param rootView The root View that this view holder will be attached
+ * to.
+ */
public ViewHolder(View rootView) {
super(rootView);
mImageView = (ImageView) rootView.findViewById(R.id.details_overview_image);
@@ -48,69 +152,204 @@
(FrameLayout) rootView.findViewById(R.id.details_overview_description);
mActionsRow =
(HorizontalGridView) rootView.findViewById(R.id.details_overview_actions);
+ mActionsRow.setOnScrollListener(mScrollListener);
+
+ final int fadeLength = rootView.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_overview_actions_fade_size);
+ mActionsRow.setFadingRightEdgeLength(fadeLength);
+ mActionsRow.setFadingLeftEdgeLength(fadeLength);
}
}
private final Presenter mDetailsPresenter;
private final ActionPresenterSelector mActionPresenterSelector;
private final ItemBridgeAdapter mActionBridgeAdapter;
+ private int mBackgroundColor = Color.TRANSPARENT;
+ private boolean mBackgroundColorSet;
+ private boolean mIsStyleLarge = true;
/**
- * Constructor that uses the given {@link Presenter} to render the detailed
- * description for the row.
+ * Constructor for a DetailsOverviewRowPresenter.
+ *
+ * @param detailsPresenter The {@link Presenter} used to render the detailed
+ * description of the row.
*/
public DetailsOverviewRowPresenter(Presenter detailsPresenter) {
+ setHeaderPresenter(null);
setSelectEffectEnabled(false);
mDetailsPresenter = detailsPresenter;
mActionPresenterSelector = new ActionPresenterSelector();
mActionBridgeAdapter = new ItemBridgeAdapter();
- FocusHighlightHelper.setupActionItemFocusHighlight(mActionBridgeAdapter);
}
/**
- * Set the listener for action click events.
+ * Sets the listener for Action click events.
*/
public void setOnActionClickedListener(OnActionClickedListener listener) {
mActionPresenterSelector.setOnActionClickedListener(listener);
}
/**
- * Get the listener for action click events.
+ * Gets the listener for Action click events.
*/
public OnActionClickedListener getOnActionClickedListener() {
return mActionPresenterSelector.getOnActionClickedListener();
}
+ /**
+ * Sets the background color. If not set, a default from the theme will be used.
+ */
+ public void setBackgroundColor(int color) {
+ mBackgroundColor = color;
+ mBackgroundColorSet = true;
+ }
+
+ /**
+ * Returns the background color. If no background color was set, transparent
+ * is returned.
+ */
+ public int getBackgroundColor() {
+ return mBackgroundColor;
+ }
+
+ /**
+ * Sets the layout style to be large or small. This affects the height of
+ * the overview, including the text description. The default is large.
+ */
+ public void setStyleLarge(boolean large) {
+ mIsStyleLarge = large;
+ }
+
+ /**
+ * Returns true if the layout style is large.
+ */
+ public boolean isStyleLarge() {
+ return mIsStyleLarge;
+ }
+
+ /**
+ * Get image view associated with view holder.
+ */
+ public ImageView getImageView(RowPresenter.ViewHolder holder) {
+ return ((ViewHolder) holder).mImageView;
+ }
+
+ private int getDefaultBackgroundColor(Context context) {
+ TypedValue outValue = new TypedValue();
+ context.getTheme().resolveAttribute(R.attr.defaultBrandColor, outValue, true);
+ return context.getResources().getColor(outValue.resourceId);
+ }
+
@Override
protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext())
.inflate(R.layout.lb_details_overview, parent, false);
ViewHolder vh = new ViewHolder(v);
+
vh.mDetailsDescriptionViewHolder =
mDetailsPresenter.onCreateViewHolder(vh.mDetailsDescriptionFrame);
vh.mDetailsDescriptionFrame.addView(vh.mDetailsDescriptionViewHolder.view);
+ initDetailsOverview(vh);
+
return vh;
}
+ private int getCardHeight(Context context) {
+ int resId = mIsStyleLarge ? R.dimen.lb_details_overview_height_large :
+ R.dimen.lb_details_overview_height_small;
+ return context.getResources().getDimensionPixelSize(resId);
+ }
+
+ private void initDetailsOverview(ViewHolder vh) {
+ View overview = vh.view.findViewById(R.id.details_overview);
+ ViewGroup.LayoutParams lp = overview.getLayoutParams();
+ lp.height = getCardHeight(overview.getContext());
+ overview.setLayoutParams(lp);
+
+ overview.setBackgroundColor(mBackgroundColorSet ?
+ mBackgroundColor : getDefaultBackgroundColor(overview.getContext()));
+ ShadowHelper.getInstance().setZ(overview, 0f);
+ }
+
@Override
protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
super.onBindRowViewHolder(holder, item);
DetailsOverviewRow row = (DetailsOverviewRow) item;
ViewHolder vh = (ViewHolder) holder;
- if (row.getImageDrawable() != null) {
- vh.mImageView.setImageDrawable(row.getImageDrawable());
+
+ ViewGroup.MarginLayoutParams layoutParams =
+ (ViewGroup.MarginLayoutParams) vh.mImageView.getLayoutParams();
+ final int cardHeight = getCardHeight(vh.mImageView.getContext());
+ final int verticalMargin = vh.mImageView.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_overview_image_margin_vertical);
+ final int horizontalMargin = vh.mImageView.getResources().getDimensionPixelSize(
+ R.dimen.lb_details_overview_image_margin_horizontal);
+ boolean scaleImage = row.isImageScaleUpAllowed();
+ boolean useMargin = false;
+ boolean landscape = false;
+
+ // If large style and landscape image we always use margin.
+ if (row.getImageDrawable().getIntrinsicWidth() >
+ row.getImageDrawable().getIntrinsicHeight()) {
+ landscape = true;
+ if (mIsStyleLarge) {
+ useMargin = true;
+ }
}
- if (vh.mDetailsDescriptionViewHolder == null) {
+ // If long dimension bigger than the card height we scale down.
+ if ((landscape && row.getImageDrawable().getIntrinsicWidth() > cardHeight) ||
+ (!landscape && row.getImageDrawable().getIntrinsicHeight() > cardHeight)) {
+ scaleImage = true;
}
+ // If we're not scaling to fit the card height then we always use margin.
+ if (!scaleImage) {
+ useMargin = true;
+ }
+ // If using margin than may need to scale down.
+ if (useMargin && !scaleImage) {
+ if (landscape && row.getImageDrawable().getIntrinsicWidth() >
+ cardHeight - horizontalMargin) {
+ scaleImage = true;
+ } else if (!landscape && row.getImageDrawable().getIntrinsicHeight() >
+ cardHeight - 2 * verticalMargin) {
+ scaleImage = true;
+ }
+ }
+
+ if (useMargin) {
+ layoutParams.leftMargin = horizontalMargin;
+ layoutParams.topMargin = layoutParams.bottomMargin = verticalMargin;
+ } else {
+ layoutParams.leftMargin = layoutParams.topMargin = layoutParams.bottomMargin = 0;
+ }
+ if (scaleImage) {
+ vh.mImageView.setScaleType(ImageView.ScaleType.FIT_START);
+ vh.mImageView.setAdjustViewBounds(true);
+ vh.mImageView.setMaxWidth(cardHeight);
+ layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
+ layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
+ } else {
+ vh.mImageView.setScaleType(ImageView.ScaleType.CENTER);
+ vh.mImageView.setAdjustViewBounds(false);
+ layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ // Limit width to the card height
+ layoutParams.width = Math.min(cardHeight, row.getImageDrawable().getIntrinsicWidth());
+ }
+ vh.mImageView.setLayoutParams(layoutParams);
+ vh.mImageView.setImageDrawable(row.getImageDrawable());
+
mDetailsPresenter.onBindViewHolder(vh.mDetailsDescriptionViewHolder, row);
mActionBridgeAdapter.clear();
ArrayObjectAdapter aoa = new ArrayObjectAdapter(mActionPresenterSelector);
aoa.addAll(0, (Collection)row.getActions());
+
mActionBridgeAdapter.setAdapter(aoa);
vh.mActionsRow.setAdapter(mActionBridgeAdapter);
+
+ vh.bind(mActionBridgeAdapter);
}
@Override
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java b/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
index d027ede..4afb3e9 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
@@ -13,21 +13,13 @@
*/
package android.support.v17.leanback.widget;
-import android.graphics.drawable.TransitionDrawable;
import android.support.v17.leanback.R;
import android.view.View;
-import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.animation.TimeAnimator;
import android.content.res.Resources;
-import static android.support.v17.leanback.widget.FocusHighlight.ZOOM_FACTOR_NONE;
-import static android.support.v17.leanback.widget.FocusHighlight.ZOOM_FACTOR_SMALL;
-import static android.support.v17.leanback.widget.FocusHighlight.ZOOM_FACTOR_MEDIUM;
-import static android.support.v17.leanback.widget.FocusHighlight.ZOOM_FACTOR_LARGE;
-
-
/**
* Setup the behavior how to highlight when a item gains focus.
*/
@@ -147,9 +139,6 @@
}
}
- private static ActionItemFocusHighlight sActionItemFocusHighlight =
- new ActionItemFocusHighlight();
-
/**
* Setup the focus highlight behavior of a focused item in browse list row.
* @param adapter adapter of the list row.
@@ -169,14 +158,6 @@
}
}
- /**
- * Setup the focus highlight behavior of a focused item in an action list.
- * @param adapter adapter of the action list.
- */
- public static void setupActionItemFocusHighlight(ItemBridgeAdapter adapter) {
- adapter.setFocusHighlight(sActionItemFocusHighlight);
- }
-
static class HeaderItemFocusHighlight implements FocusHighlight {
private static boolean sInitialized;
private static float sSelectScale;
@@ -233,26 +214,4 @@
viewFocused(view, hasFocus);
}
}
-
- private static class ActionItemFocusHighlight implements FocusHighlight {
- private boolean mInitialized;
- private int mDuration;
-
- private void initializeDimensions(Resources res) {
- if (!mInitialized) {
- mDuration = Integer.parseInt(res.getString(R.dimen.lb_details_overview_action_select_duration));
- }
- }
-
- @Override
- public void onItemFocused(View view, boolean hasFocus) {
- initializeDimensions(view.getResources());
- TransitionDrawable td = (TransitionDrawable) view.getBackground();
- if (hasFocus) {
- td.startTransition(mDuration);
- } else {
- td.reverseTransition(mDuration);
- }
- }
- }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
index 8143767..83ac3e3 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -13,12 +13,14 @@
*/
package android.support.v17.leanback.widget;
-import android.animation.TimeAnimator;
import android.content.Context;
+import android.graphics.PointF;
import android.graphics.Rect;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
-import android.support.v7.widget.RecyclerView.Adapter;
import android.support.v7.widget.RecyclerView.Recycler;
+import android.support.v7.widget.RecyclerView.State;
import static android.support.v7.widget.RecyclerView.NO_ID;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
@@ -34,8 +36,6 @@
import android.view.View.MeasureSpec;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewGroup;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -46,10 +46,9 @@
/*
* LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}.
- * The class currently does three internal jobs:
+ * The class currently does two internal jobs:
* - Saves optical bounds insets.
* - Caches focus align view center.
- * - Manages child view layout animation.
*/
static class LayoutParams extends RecyclerView.LayoutParams {
@@ -66,15 +65,6 @@
private int mAlignX;
private int mAlignY;
- // For animations
- private TimeAnimator mAnimator;
- private long mDuration;
- private boolean mFirstAttached;
- // current virtual view position (scrollOffset + left/top) in the GridLayoutManager
- private int mViewX, mViewY;
- // animation start value of translation x and y
- private float mAnimationStartTranslationX, mAnimationStartTranslationY;
-
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
@@ -99,15 +89,6 @@
super(source);
}
- void onViewAttached() {
- endAnimate();
- mFirstAttached = true;
- }
-
- void onViewDetached() {
- endAnimate();
- }
-
int getAlignX() {
return mAlignX;
}
@@ -140,6 +121,22 @@
return view.getHeight() - mTopInset - mBottomInset;
}
+ int getOpticalLeftInset() {
+ return mLeftInset;
+ }
+
+ int getOpticalRightInset() {
+ return mRighInset;
+ }
+
+ int getOpticalTopInset() {
+ return mTopInset;
+ }
+
+ int getOpticalBottomInset() {
+ return mBottomInset;
+ }
+
void setAlignX(int alignX) {
mAlignX = alignX;
}
@@ -155,68 +152,6 @@
mBottomInset = bottomInset;
}
- private TimeAnimator.TimeListener mTimeListener = new TimeAnimator.TimeListener() {
- @Override
- public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
- if (mView == null) {
- return;
- }
- if (totalTime >= mDuration) {
- endAnimate();
- } else {
- float fraction = (float) (totalTime / (double)mDuration);
- float fractionToEnd = 1 - mAnimator
- .getInterpolator().getInterpolation(fraction);
- mView.setTranslationX(fractionToEnd * mAnimationStartTranslationX);
- mView.setTranslationY(fractionToEnd * mAnimationStartTranslationY);
- invalidateItemDecoration();
- }
- }
- };
-
- void startAnimate(GridLayoutManager layout, View view, long startDelay) {
- if (mAnimator == null) {
- mAnimator = new TimeAnimator();
- mAnimator.setTimeListener(mTimeListener);
- }
- if (mFirstAttached) {
- // first time record the initial location and return without animation
- // TODO do we need initial animation?
- mViewX = layout.getScrollOffsetX() + getOpticalLeft(view);
- mViewY = layout.getScrollOffsetY() + getOpticalTop(view);
- mFirstAttached = false;
- return;
- }
- mView = view;
- int newViewX = layout.getScrollOffsetX() + getOpticalLeft(mView);
- int newViewY = layout.getScrollOffsetY() + getOpticalTop(mView);
- if (newViewX != mViewX || newViewY != mViewY) {
- mAnimator.cancel();
- mAnimationStartTranslationX = mView.getTranslationX();
- mAnimationStartTranslationY = mView.getTranslationY();
- mAnimationStartTranslationX += mViewX - newViewX;
- mAnimationStartTranslationY += mViewY - newViewY;
- mDuration = layout.getChildLayoutAnimationDuration();
- mAnimator.setDuration(mDuration);
- mAnimator.setInterpolator(layout.getChildLayoutAnimationInterpolator());
- mAnimator.setStartDelay(startDelay);
- mAnimator.start();
- mViewX = newViewX;
- mViewY = newViewY;
- }
- }
-
- void endAnimate() {
- if (mAnimator != null) {
- mAnimator.end();
- }
- if (mView != null) {
- mView.setTranslationX(0);
- mView.setTranslationY(0);
- mView = null;
- }
- }
-
private void invalidateItemDecoration() {
ViewParent parent = mView.getParent();
if (parent instanceof RecyclerView) {
@@ -229,11 +164,6 @@
private static final String TAG = "GridLayoutManager";
private static final boolean DEBUG = false;
- private static final Interpolator sDefaultAnimationChildLayoutInterpolator
- = new DecelerateInterpolator();
-
- private static final long DEFAULT_CHILD_ANIMATION_DURATION_MS = 250;
-
private String getTag() {
return TAG + ":" + mBaseGridView.getId();
}
@@ -245,7 +175,7 @@
*/
private int mOrientation = HORIZONTAL;
- private RecyclerView.Adapter mAdapter;
+ private RecyclerView.State mState;
private RecyclerView.Recycler mRecycler;
private boolean mInLayout = false;
@@ -265,23 +195,43 @@
private boolean mForceFullLayout;
/**
+ * True if layout is enabled.
+ */
+ private boolean mLayoutEnabled = true;
+
+ /**
* The scroll offsets of the viewport relative to the entire view.
*/
private int mScrollOffsetPrimary;
private int mScrollOffsetSecondary;
/**
- * User-specified fixed size of each grid item in the secondary direction, can be
- * 0 to be determined by parent size and number of rows.
+ * User-specified row height/column width. Can be WRAP_CONTENT.
*/
- private int mItemLengthSecondaryRequested;
+ private int mRowSizeSecondaryRequested;
+
/**
* The fixed size of each grid item in the secondary direction. This corresponds to
* the row height, equal for all rows. Grid items may have variable length
* in the primary direction.
- *
*/
- private int mItemLengthSecondary;
+ private int mFixedRowSizeSecondary;
+
+ /**
+ * Tracks the secondary size of each row.
+ */
+ private int[] mRowSizeSecondary;
+
+ /**
+ * Flag controlling whether the current/next layout should
+ * be updating the secondary size of rows.
+ */
+ private boolean mRowSecondarySizeRefresh;
+
+ /**
+ * The maximum measured size of the view.
+ */
+ private int mMaxSizeSecondary;
/**
* Margin between items.
@@ -367,19 +317,20 @@
private boolean mFocusSearchDisabled;
/**
- * Animate layout changes from a child resizing or adding/removing a child.
+ * True if prune child, might be disabled during transition.
*/
- private boolean mAnimateChildLayout = true;
+ private boolean mPruneChild = true;
+
+ private int[] mTempDeltas = new int[2];
+
+ private boolean mUseDeltaInPreLayout;
+
+ private int mDeltaInPreLayout, mDeltaSecondaryInPreLayout;
/**
- * Interpolator used to animate layout of children.
+ * Temporaries used for measuring.
*/
- private Interpolator mAnimateLayoutChildInterpolator = sDefaultAnimationChildLayoutInterpolator;
-
- /**
- * Duration used to animate layout of children.
- */
- private long mAnimateLayoutChildDuration = DEFAULT_CHILD_ANIMATION_DURATION_MS;
+ private int[] mMeasuredDimension = new int[2];
public GridLayoutManager(BaseGridView baseGridView) {
mBaseGridView = baseGridView;
@@ -438,6 +389,15 @@
return mItemAlignment.mainAxis().getItemAlignmentOffset();
}
+ public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
+ mItemAlignment.mainAxis().setItemAlignmentOffsetWithPadding(withPadding);
+ updateChildAlignments();
+ }
+
+ public boolean isItemAlignmentOffsetWithPadding() {
+ return mItemAlignment.mainAxis().isItemAlignmentOffsetWithPadding();
+ }
+
public void setItemAlignmentOffsetPercent(float offsetPercent) {
mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent);
updateChildAlignments();
@@ -467,9 +427,15 @@
mForceFullLayout = true;
}
+ /**
+ * Set the row height. May be WRAP_CONTENT, or a size in pixels.
+ */
public void setRowHeight(int height) {
- if (height < 0) throw new IllegalArgumentException();
- mItemLengthSecondaryRequested = height;
+ if (height >= 0 || height == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ mRowSizeSecondaryRequested = height;
+ } else {
+ throw new IllegalArgumentException("Invalid row height: " + height);
+ }
}
public void setItemMargin(int margin) {
@@ -514,45 +480,35 @@
}
private int getPositionByView(View view) {
- return getPositionByIndex(mBaseGridView.indexOfChild(view));
+ if (view == null) {
+ return NO_POSITION;
+ }
+ LayoutParams params = (LayoutParams) view.getLayoutParams();
+ if (params == null || params.isItemRemoved()) {
+ // when item is removed, the position value can be any value.
+ return NO_POSITION;
+ }
+ return params.getViewPosition();
}
private int getPositionByIndex(int index) {
- if (index < 0) {
- return NO_POSITION;
- }
- return mFirstVisiblePos + index;
- }
-
- private View getViewByPosition(int position) {
- int index = getIndexByPosition(position);
- if (index < 0) {
- return null;
- }
- return getChildAt(index);
- }
-
- private int getIndexByPosition(int position) {
- if (mFirstVisiblePos < 0 ||
- position < mFirstVisiblePos || position > mLastVisiblePos) {
- return NO_POSITION;
- }
- return position - mFirstVisiblePos;
+ return getPositionByView(getChildAt(index));
}
private void dispatchChildSelected() {
if (mChildSelectedListener == null) {
return;
}
-
- View view = getViewByPosition(mFocusPosition);
-
if (mFocusPosition != NO_POSITION) {
- mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition,
- mAdapter.getItemId(mFocusPosition));
- } else {
- mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
+ View view = findViewByPosition(mFocusPosition);
+ if (view != null) {
+ RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
+ mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition,
+ vh == null? NO_ID: vh.getItemId());
+ return;
+ }
}
+ mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
}
@Override
@@ -594,11 +550,7 @@
}
protected View getViewForPosition(int position) {
- View v = mRecycler.getViewForPosition(position);
- if (v != null) {
- ((LayoutParams) v.getLayoutParams()).onViewAttached();
- }
- return v;
+ return mRecycler.getViewForPosition(position);
}
final int getOpticalLeft(View v) {
@@ -644,6 +596,25 @@
}
/**
+ * Save Recycler and State for convenience. Must be paired with leaveContext().
+ */
+ private void saveContext(Recycler recycler, State state) {
+ if (mRecycler != null || mState != null) {
+ Log.e(TAG, "Recycler information was not released, bug!");
+ }
+ mRecycler = recycler;
+ mState = state;
+ }
+
+ /**
+ * Discard saved Recycler and State.
+ */
+ private void leaveContext() {
+ mRecycler = null;
+ mState = null;
+ }
+
+ /**
* Re-initialize data structures for a data change or handling invisible
* selection. The method tries its best to preserve position information so
* that staggered grid looks same before and after re-initialize.
@@ -651,10 +622,9 @@
* focus on.
* @return Actual position that can be focused on.
*/
- private int init(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
- int focusPosition) {
+ private int init(int focusPosition) {
- final int newItemCount = adapter.getItemCount();
+ final int newItemCount = mState.getItemCount();
if (focusPosition == NO_POSITION && newItemCount > 0) {
// if focus position is never set before, initialize it to 0
@@ -705,7 +675,7 @@
}
// fill rows with minimal view positions of the subset
for (int i = firstIndex; i <= lastIndex; i++) {
- View v = getViewByPosition(i);
+ View v = findViewByPosition(i);
if (v == null) {
continue;
}
@@ -715,24 +685,34 @@
mRows[row].low = mRows[row].high = low;
}
}
- // fill other rows that does not include the subset using first item
int firstItemRowPosition = mRows[mGrid.getLocation(firstIndex).row].low;
if (firstItemRowPosition == Integer.MAX_VALUE) {
firstItemRowPosition = 0;
}
- for (int i = 0; i < mNumRows; i++) {
- if (mRows[i].low == Integer.MAX_VALUE) {
- mRows[i].low = mRows[i].high = firstItemRowPosition;
+ if (mState.didStructureChange()) {
+ // if there is structure change, the removed item might be in the
+ // subset, so it is meaningless to maintain the low locations.
+ for (int i = 0; i < mNumRows; i++) {
+ mRows[i].low = firstItemRowPosition;
+ mRows[i].high = firstItemRowPosition;
+ }
+ } else {
+ // fill other rows that does not include the subset using first item
+ for (int i = 0; i < mNumRows; i++) {
+ if (mRows[i].low == Integer.MAX_VALUE) {
+ mRows[i].low = mRows[i].high = firstItemRowPosition;
+ }
}
}
}
// Same adapter, we can reuse any attached views
- detachAndScrapAttachedViews(recycler);
+ detachAndScrapAttachedViews(mRecycler);
} else {
// otherwise recreate data structure
mRows = new StaggeredGrid.Row[mNumRows];
+
for (int i = 0; i < mNumRows; i++) {
mRows[i] = new StaggeredGrid.Row();
}
@@ -744,27 +724,158 @@
}
// Adapter may have changed so remove all attached views permanently
- removeAllViews();
+ removeAndRecycleAllViews(mRecycler);
mScrollOffsetPrimary = 0;
mScrollOffsetSecondary = 0;
mWindowAlignment.reset();
}
- mAdapter = adapter;
- mRecycler = recycler;
mGrid.setProvider(mGridProvider);
// mGrid share the same Row array information
mGrid.setRows(mRows);
mFirstVisiblePos = mLastVisiblePos = NO_POSITION;
initScrollController();
+ updateScrollSecondAxis();
return focusPosition;
}
+ private int getRowSizeSecondary(int rowIndex) {
+ if (mFixedRowSizeSecondary != 0) {
+ return mFixedRowSizeSecondary;
+ }
+ if (mRowSizeSecondary == null) {
+ return 0;
+ }
+ return mRowSizeSecondary[rowIndex];
+ }
+
+ private int getRowStartSecondary(int rowIndex) {
+ int start = 0;
+ for (int i = 0; i < rowIndex; i++) {
+ start += getRowSizeSecondary(i) + mMarginSecondary;
+ }
+ return start;
+ }
+
+ private int getSizeSecondary() {
+ return getRowStartSecondary(mNumRows - 1) + getRowSizeSecondary(mNumRows - 1);
+ }
+
+ private void measureScrapChild(int position, int widthSpec, int heightSpec,
+ int[] measuredDimension) {
+ View view = mRecycler.getViewForPosition(position);
+ if (view != null) {
+ LayoutParams p = (LayoutParams) view.getLayoutParams();
+ int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
+ getPaddingLeft() + getPaddingRight(), p.width);
+ int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
+ getPaddingTop() + getPaddingBottom(), p.height);
+ view.measure(childWidthSpec, childHeightSpec);
+ measuredDimension[0] = view.getMeasuredWidth();
+ measuredDimension[1] = view.getMeasuredHeight();
+ mRecycler.recycleView(view);
+ }
+ }
+
+ private boolean processRowSizeSecondary(boolean measure) {
+ if (mFixedRowSizeSecondary != 0) {
+ return false;
+ }
+
+ if (mGrid == null) {
+ if (mState.getItemCount() > 0) {
+ measureScrapChild(mFocusPosition == NO_POSITION ? 0 : mFocusPosition,
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ mMeasuredDimension);
+ if (DEBUG) Log.v(TAG, "measured scrap child: " + mMeasuredDimension[0] +
+ " " + mMeasuredDimension[1]);
+ } else {
+ mMeasuredDimension[0] = mMeasuredDimension[1] = 0;
+ }
+ }
+
+ List<Integer>[] rows = mGrid == null ? null :
+ mGrid.getItemPositionsInRows(mFirstVisiblePos, mLastVisiblePos);
+ boolean changed = false;
+
+ for (int rowIndex = 0; rowIndex < mNumRows; rowIndex++) {
+ int rowSize = 0;
+
+ final int rowItemCount = rows == null ? 1 : rows[rowIndex].size();
+ if (DEBUG) Log.v(getTag(), "processRowSizeSecondary row " + rowIndex +
+ " rowItemCount " + rowItemCount);
+
+ for (int i = 0; i < rowItemCount; i++) {
+ if (rows != null) {
+ final int position = rows[rowIndex].get(i);
+ final View view = findViewByPosition(position);
+ if (measure && view.isLayoutRequested()) {
+ measureChild(view);
+ }
+ mMeasuredDimension[0] = view.getMeasuredWidth();
+ mMeasuredDimension[1] = view.getMeasuredHeight();
+ }
+ final int secondarySize = mOrientation == HORIZONTAL ?
+ mMeasuredDimension[1] : mMeasuredDimension[0];
+ if (secondarySize > rowSize) {
+ rowSize = secondarySize;
+ }
+ }
+ if (DEBUG) Log.v(getTag(), "row " + rowIndex + " rowItemCount " + rowItemCount +
+ " rowSize " + rowSize);
+
+ if (mRowSizeSecondary[rowIndex] != rowSize) {
+ if (DEBUG) Log.v(getTag(), "row size secondary changed: " + mRowSizeSecondary[rowIndex] +
+ ", " + rowSize);
+
+ mRowSizeSecondary[rowIndex] = rowSize;
+ changed = true;
+ }
+ }
+
+ return changed;
+ }
+
+ /**
+ * Checks if we need to update row secondary sizes.
+ */
+ private void updateRowSecondarySizeRefresh() {
+ mRowSecondarySizeRefresh = processRowSizeSecondary(false);
+ if (mRowSecondarySizeRefresh) {
+ if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set");
+ forceRequestLayout();
+ }
+ }
+
+ private void forceRequestLayout() {
+ if (DEBUG) Log.v(getTag(), "forceRequestLayout");
+ // RecyclerView prevents us from requesting layout in many cases
+ // (during layout, during scroll, etc.)
+ // For secondary row size wrap_content support we currently need a
+ // second layout pass to update the measured size after having measured
+ // and added child views in layoutChildren.
+ // Force the second layout by posting a delayed runnable.
+ // TODO: investigate allowing a second layout pass,
+ // or move child add/measure logic to the measure phase.
+ ViewCompat.postOnAnimation(mBaseGridView, mRequestLayoutRunnable);
+ }
+
+ private final Runnable mRequestLayoutRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.v(getTag(), "request Layout from runnable");
+ requestLayout();
+ }
+ };
+
@Override
- public void onMeasure(int widthSpec, int heightSpec) {
+ public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
+ saveContext(recycler, state);
+
int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
int measuredSizeSecondary;
if (mOrientation == HORIZONTAL) {
@@ -778,52 +889,83 @@
modeSecondary = MeasureSpec.getMode(widthSpec);
paddingSecondary = getPaddingLeft() + getPaddingRight();
}
- switch (modeSecondary) {
- case MeasureSpec.UNSPECIFIED:
- if (mItemLengthSecondaryRequested == 0) {
- if (mOrientation == HORIZONTAL) {
- throw new IllegalStateException("Must specify rowHeight or view height");
- } else {
- throw new IllegalStateException("Must specify columnWidth or view width");
+ if (DEBUG) Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec) +
+ " heightSpec " + Integer.toHexString(heightSpec) +
+ " modeSecondary " + Integer.toHexString(modeSecondary) +
+ " sizeSecondary " + sizeSecondary + " " + this);
+
+ mMaxSizeSecondary = sizeSecondary;
+
+ if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) {
+ mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
+ mFixedRowSizeSecondary = 0;
+
+ if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) {
+ mRowSizeSecondary = new int[mNumRows];
+ }
+
+ // Measure all current children and update cached row heights
+ processRowSizeSecondary(true);
+
+ switch (modeSecondary) {
+ case MeasureSpec.UNSPECIFIED:
+ measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
+ break;
+ case MeasureSpec.AT_MOST:
+ measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
+ mMaxSizeSecondary);
+ break;
+ case MeasureSpec.EXACTLY:
+ measuredSizeSecondary = mMaxSizeSecondary;
+ break;
+ default:
+ throw new IllegalStateException("wrong spec");
+ }
+
+ } else {
+ switch (modeSecondary) {
+ case MeasureSpec.UNSPECIFIED:
+ if (mRowSizeSecondaryRequested == 0) {
+ if (mOrientation == HORIZONTAL) {
+ throw new IllegalStateException("Must specify rowHeight or view height");
+ } else {
+ throw new IllegalStateException("Must specify columnWidth or view width");
+ }
}
- }
- mItemLengthSecondary = mItemLengthSecondaryRequested;
- if (mNumRowsRequested == 0) {
- mNumRows = 1;
- } else {
- mNumRows = mNumRowsRequested;
- }
- measuredSizeSecondary = mItemLengthSecondary * mNumRows + mMarginSecondary
- * (mNumRows - 1) + paddingSecondary;
- break;
- case MeasureSpec.AT_MOST:
- case MeasureSpec.EXACTLY:
- if (mNumRowsRequested == 0 && mItemLengthSecondaryRequested == 0) {
- mNumRows = 1;
- mItemLengthSecondary = sizeSecondary - paddingSecondary;
- } else if (mNumRowsRequested == 0) {
- mItemLengthSecondary = mItemLengthSecondaryRequested;
- mNumRows = (sizeSecondary + mMarginSecondary)
- / (mItemLengthSecondaryRequested + mMarginSecondary);
- } else if (mItemLengthSecondaryRequested == 0) {
- mNumRows = mNumRowsRequested;
- mItemLengthSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary
- * (mNumRows - 1)) / mNumRows;
- } else {
- mNumRows = mNumRowsRequested;
- mItemLengthSecondary = mItemLengthSecondaryRequested;
- }
- measuredSizeSecondary = sizeSecondary;
- if (modeSecondary == MeasureSpec.AT_MOST) {
- int childrenSize = mItemLengthSecondary * mNumRows + mMarginSecondary
+ mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
+ mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
+ measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
* (mNumRows - 1) + paddingSecondary;
- if (childrenSize < measuredSizeSecondary) {
- measuredSizeSecondary = childrenSize;
+ break;
+ case MeasureSpec.AT_MOST:
+ case MeasureSpec.EXACTLY:
+ if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) {
+ mNumRows = 1;
+ mFixedRowSizeSecondary = sizeSecondary - paddingSecondary;
+ } else if (mNumRowsRequested == 0) {
+ mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
+ mNumRows = (sizeSecondary + mMarginSecondary)
+ / (mRowSizeSecondaryRequested + mMarginSecondary);
+ } else if (mRowSizeSecondaryRequested == 0) {
+ mNumRows = mNumRowsRequested;
+ mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary
+ * (mNumRows - 1)) / mNumRows;
+ } else {
+ mNumRows = mNumRowsRequested;
+ mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
}
+ measuredSizeSecondary = sizeSecondary;
+ if (modeSecondary == MeasureSpec.AT_MOST) {
+ int childrenSize = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
+ * (mNumRows - 1) + paddingSecondary;
+ if (childrenSize < measuredSizeSecondary) {
+ measuredSizeSecondary = childrenSize;
+ }
+ }
+ break;
+ default:
+ throw new IllegalStateException("wrong spec");
}
- break;
- default:
- throw new IllegalStateException("wrong spec");
}
if (mOrientation == HORIZONTAL) {
setMeasuredDimension(sizePrimary, measuredSizeSecondary);
@@ -833,35 +975,47 @@
if (DEBUG) {
Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary +
" measuredSizeSecondary " + measuredSizeSecondary +
- " mItemLengthSecondary " + mItemLengthSecondary +
+ " mFixedRowSizeSecondary " + mFixedRowSizeSecondary +
" mNumRows " + mNumRows);
}
+
+ leaveContext();
}
private void measureChild(View child) {
final ViewGroup.LayoutParams lp = child.getLayoutParams();
-
+ final int secondarySpec = (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) ?
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) :
+ MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY);
int widthSpec, heightSpec;
+
if (mOrientation == HORIZONTAL) {
widthSpec = ViewGroup.getChildMeasureSpec(
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, lp.width);
- heightSpec = ViewGroup.getChildMeasureSpec(MeasureSpec.makeMeasureSpec(
- mItemLengthSecondary, MeasureSpec.EXACTLY), 0, lp.height);
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ 0, lp.width);
+ heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.height);
} else {
heightSpec = ViewGroup.getChildMeasureSpec(
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, lp.height);
- widthSpec = ViewGroup.getChildMeasureSpec(MeasureSpec.makeMeasureSpec(
- mItemLengthSecondary, MeasureSpec.EXACTLY), 0, lp.width);
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ 0, lp.height);
+ widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.width);
}
child.measure(widthSpec, heightSpec);
+
+ if (DEBUG) Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec) +
+ " widthSpec " + Integer.toHexString(widthSpec) +
+ " heightSpec " + Integer.toHexString(heightSpec) +
+ " measuredWidth " + child.getMeasuredWidth() +
+ " measuredHeight " + child.getMeasuredHeight());
+ if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height);
}
private StaggeredGrid.Provider mGridProvider = new StaggeredGrid.Provider() {
@Override
public int getCount() {
- return mAdapter.getItemCount();
+ return mState.getItemCount();
}
@Override
@@ -877,14 +1031,16 @@
}
}
- if (append) {
- addView(v);
- } else {
- addView(v, 0);
+ // See recyclerView docs: we don't need re-add scraped view if it was removed.
+ if (!((RecyclerView.LayoutParams) v.getLayoutParams()).isItemRemoved()) {
+ if (append) {
+ addView(v);
+ } else {
+ addView(v, 0);
+ }
+ measureChild(v);
}
- measureChild(v);
-
int length = mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight();
int start, end;
if (append) {
@@ -923,22 +1079,27 @@
mFirstVisiblePos--;
}
}
- int startSecondary = rowIndex * (mItemLengthSecondary + mMarginSecondary);
- layoutChild(v, start - mScrollOffsetPrimary, end - mScrollOffsetPrimary,
- startSecondary - mScrollOffsetSecondary);
+ if (DEBUG) Log.v(getTag(), "start " + start + " end " + end);
+ int startSecondary = getRowStartSecondary(rowIndex) - mScrollOffsetSecondary;
+ layoutChild(rowIndex, v, start - mScrollOffsetPrimary, end - mScrollOffsetPrimary,
+ startSecondary);
if (DEBUG) {
Log.d(getTag(), "addView " + index + " " + v);
}
- updateScrollMin();
- updateScrollMax();
+ if (index == mFirstVisiblePos) {
+ updateScrollMin();
+ }
+ if (index == mLastVisiblePos) {
+ updateScrollMax();
+ }
}
};
- private void layoutChild(View v, int start, int end, int startSecondary) {
- int measuredSecondary = mOrientation == HORIZONTAL ? v.getMeasuredHeight()
- : v.getMeasuredWidth();
- if (measuredSecondary > mItemLengthSecondary) {
- measuredSecondary = mItemLengthSecondary;
+ private void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) {
+ int sizeSecondary = mOrientation == HORIZONTAL ? v.getMeasuredHeight()
+ : v.getMeasuredWidth();
+ if (mFixedRowSizeSecondary > 0) {
+ sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary);
}
final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int horizontalGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
@@ -947,22 +1108,22 @@
// do nothing
} else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM
|| mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT) {
- startSecondary += mItemLengthSecondary - measuredSecondary;
+ startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary;
} else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL
|| mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL) {
- startSecondary += (mItemLengthSecondary - measuredSecondary) / 2;
+ startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2;
}
int left, top, right, bottom;
if (mOrientation == HORIZONTAL) {
left = start;
top = startSecondary;
right = end;
- bottom = startSecondary + measuredSecondary;
+ bottom = startSecondary + sizeSecondary;
} else {
top = start;
left = startSecondary;
bottom = end;
- right = startSecondary + measuredSecondary;
+ right = startSecondary + sizeSecondary;
}
v.layout(left, top, right, bottom);
updateChildOpticalInsets(v, left, top, right, bottom);
@@ -1023,7 +1184,7 @@
// Append one column if possible and return true if reach end.
private boolean appendOneVisibleItem() {
while (true) {
- if (mLastVisiblePos != NO_POSITION && mLastVisiblePos < mAdapter.getItemCount() -1 &&
+ if (mLastVisiblePos != NO_POSITION && mLastVisiblePos < mState.getItemCount() -1 &&
mLastVisiblePos < mGrid.getLastIndex()) {
// append invisible view of saved location till last row
final int index = mLastVisiblePos + 1;
@@ -1032,9 +1193,9 @@
if (row == mNumRows - 1) {
return false;
}
- } else if ((mLastVisiblePos == NO_POSITION && mAdapter.getItemCount() > 0) ||
+ } else if ((mLastVisiblePos == NO_POSITION && mState.getItemCount() > 0) ||
(mLastVisiblePos != NO_POSITION &&
- mLastVisiblePos < mAdapter.getItemCount() - 1)) {
+ mLastVisiblePos < mState.getItemCount() - 1)) {
mGrid.appendItems(mScrollOffsetPrimary + mSizePrimary);
return false;
} else {
@@ -1082,20 +1243,22 @@
}
private void removeChildAt(int position) {
- View v = getViewByPosition(position);
+ View v = findViewByPosition(position);
if (v != null) {
if (DEBUG) {
Log.d(getTag(), "removeAndRecycleViewAt " + position);
}
- ((LayoutParams) v.getLayoutParams()).onViewDetached();
- removeAndRecycleViewAt(getIndexByPosition(position), mRecycler);
+ removeAndRecycleView(v, mRecycler);
}
}
private void removeInvisibleViewsAtEnd() {
+ if (!mPruneChild) {
+ return;
+ }
boolean update = false;
while(mLastVisiblePos > mFirstVisiblePos && mLastVisiblePos > mFocusPosition) {
- View view = getViewByPosition(mLastVisiblePos);
+ View view = findViewByPosition(mLastVisiblePos);
if (getViewMin(view) > mSizePrimary) {
removeChildAt(mLastVisiblePos);
mLastVisiblePos--;
@@ -1110,9 +1273,12 @@
}
private void removeInvisibleViewsAtFront() {
+ if (!mPruneChild) {
+ return;
+ }
boolean update = false;
while(mLastVisiblePos > mFirstVisiblePos && mFirstVisiblePos < mFocusPosition) {
- View view = getViewByPosition(mFirstVisiblePos);
+ View view = findViewByPosition(mFirstVisiblePos);
if (getViewMax(view) < 0) {
removeChildAt(mFirstVisiblePos);
mFirstVisiblePos++;
@@ -1135,7 +1301,7 @@
mRows[i].high = Integer.MIN_VALUE;
}
for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
- View view = getViewByPosition(i);
+ View view = findViewByPosition(i);
int row = mGrid.getLocation(i).row;
int low = getViewMin(view) + mScrollOffsetPrimary;
if (low < mRows[row].low) {
@@ -1148,35 +1314,6 @@
}
}
- /**
- * Relayout and re-positioning child for a possible new size and/or a new
- * start.
- *
- * @param view View to measure and layout.
- * @param start New start of the view or Integer.MIN_VALUE for not change.
- * @return New start of next view.
- */
- private int updateChildView(View view, int start, int startSecondary) {
- if (start == Integer.MIN_VALUE) {
- start = getViewMin(view);
- }
- int end;
- if (mOrientation == HORIZONTAL) {
- if (view.isLayoutRequested() || view.getMeasuredHeight() != mItemLengthSecondary) {
- measureChild(view);
- }
- end = start + view.getMeasuredWidth();
- } else {
- if (view.isLayoutRequested() || view.getMeasuredWidth() != mItemLengthSecondary) {
- measureChild(view);
- }
- end = start + view.getMeasuredHeight();
- }
-
- layoutChild(view, start, end, startSecondary);
- return end + mMarginPrimary;
- }
-
// Fast layout when there is no structure change, adapter change, etc.
protected void fastRelayout() {
initScrollController();
@@ -1186,35 +1323,73 @@
// relayout and repositioning views on each row
for (int i = 0; i < mNumRows; i++) {
List<Integer> row = rows[i];
- int start = Integer.MIN_VALUE;
- int startSecondary =
- i * (mItemLengthSecondary + mMarginSecondary) - mScrollOffsetSecondary;
+ final int startSecondary = getRowStartSecondary(i) - mScrollOffsetSecondary;
for (int j = 0, size = row.size(); j < size; j++) {
- int position = row.get(j);
- start = updateChildView(getViewByPosition(position), start, startSecondary);
+ final int position = row.get(j);
+ final View view = findViewByPosition(position);
+ int primaryDelta, start, end;
+
+ if (mOrientation == HORIZONTAL) {
+ final int primarySize = view.getMeasuredWidth();
+ if (view.isLayoutRequested()) {
+ measureChild(view);
+ }
+ start = getViewMin(view);
+ end = start + view.getMeasuredWidth();
+ primaryDelta = view.getMeasuredWidth() - primarySize;
+ if (primaryDelta != 0) {
+ for (int k = j + 1; k < size; k++) {
+ findViewByPosition(row.get(k)).offsetLeftAndRight(primaryDelta);
+ }
+ }
+ } else {
+ final int primarySize = view.getMeasuredHeight();
+ if (view.isLayoutRequested()) {
+ measureChild(view);
+ }
+ start = getViewMin(view);
+ end = start + view.getMeasuredHeight();
+ primaryDelta = view.getMeasuredHeight() - primarySize;
+ if (primaryDelta != 0) {
+ for (int k = j + 1; k < size; k++) {
+ findViewByPosition(row.get(k)).offsetTopAndBottom(primaryDelta);
+ }
+ }
+ }
+ layoutChild(i, view, start, end, startSecondary);
}
}
+ updateRowsMinMax();
appendVisibleItems();
prependVisibleItems();
updateRowsMinMax();
updateScrollMin();
updateScrollMax();
+ updateScrollSecondAxis();
if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
- View focusView = getViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition);
+ View focusView = findViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition);
scrollToView(focusView, false);
}
}
+ public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
+ if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ removeAndRecycleViewAt(i, recycler);
+ }
+ }
+
// Lays out items based on the current scroll position
- public void onLayoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
- boolean structureChanged, RecyclerView.State state) {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (DEBUG) {
Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
+ mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
- + " structureChanged " + structureChanged
+ + " inPreLayout " + state.isPreLayout()
+ + " didStructureChange " + state.didStructureChange()
+ " mForceFullLayout " + mForceFullLayout);
Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
}
@@ -1223,36 +1398,56 @@
// haven't done measure yet
return;
}
- final int itemCount = adapter.getItemCount();
+ final int itemCount = state.getItemCount();
if (itemCount < 0) {
return;
}
+ if (!mLayoutEnabled) {
+ discardLayoutInfo();
+ removeAndRecycleAllViews(recycler);
+ return;
+ }
mInLayout = true;
+ saveContext(recycler, state);
// Track the old focus view so we can adjust our system scroll position
// so that any scroll animations happening now will remain valid.
+ // We must use same delta in Pre Layout (if prelayout exists) and second layout.
+ // So we cache the deltas in PreLayout and use it in second layout.
int delta = 0, deltaSecondary = 0;
- if (mFocusPosition != NO_POSITION
- && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
- View focusView = getViewByPosition(mFocusPosition);
- if (focusView != null) {
- delta = mWindowAlignment.mainAxis().getSystemScrollPos(
- getViewCenter(focusView) + mScrollOffsetPrimary) - mScrollOffsetPrimary;
- deltaSecondary =
- mWindowAlignment.secondAxis().getSystemScrollPos(
- getViewCenterSecondary(focusView) + mScrollOffsetSecondary)
- - mScrollOffsetSecondary;
+ if (!state.isPreLayout() && mUseDeltaInPreLayout) {
+ delta = mDeltaInPreLayout;
+ deltaSecondary = mDeltaSecondaryInPreLayout;
+ } else {
+ if (mFocusPosition != NO_POSITION
+ && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
+ // FIXME: we should get the remaining scroll animation offset from RecyclerView
+ View focusView = findViewByPosition(mFocusPosition);
+ if (focusView != null) {
+ delta = mWindowAlignment.mainAxis().getSystemScrollPos(mScrollOffsetPrimary
+ + getViewCenter(focusView), false, false) - mScrollOffsetPrimary;
+ deltaSecondary = mWindowAlignment.secondAxis().getSystemScrollPos(
+ mScrollOffsetSecondary + getViewCenterSecondary(focusView),
+ false, false) - mScrollOffsetSecondary;
+ if (mUseDeltaInPreLayout = state.isPreLayout()) {
+ mDeltaInPreLayout = delta;
+ mDeltaSecondaryInPreLayout = deltaSecondary;
+ }
+ }
}
}
- boolean hasDoneFirstLayout = hasDoneFirstLayout();
- if (!structureChanged && !mForceFullLayout && hasDoneFirstLayout) {
+ final boolean hasDoneFirstLayout = hasDoneFirstLayout();
+ int savedFocusPos = mFocusPosition;
+ boolean fastRelayout = false;
+ if (!mState.didStructureChange() && !mForceFullLayout && hasDoneFirstLayout) {
+ fastRelayout = true;
fastRelayout();
} else {
boolean hadFocus = mBaseGridView.hasFocus();
- int newFocusPosition = init(adapter, recycler, mFocusPosition);
+ int newFocusPosition = init(mFocusPosition);
if (DEBUG) {
Log.v(getTag(), "mFocusPosition " + mFocusPosition + " newFocusPosition "
+ newFocusPosition);
@@ -1290,7 +1485,7 @@
do {
oldFirstVisible = mFirstVisiblePos;
oldLastVisible = mLastVisiblePos;
- View focusView = getViewByPosition(newFocusPosition);
+ View focusView = findViewByPosition(newFocusPosition);
// we need force to initialize the child view's position
scrollToView(focusView, false);
if (focusView != null && hadFocus) {
@@ -1320,12 +1515,20 @@
Log.d(getTag(), sw.toString());
}
- attemptAnimateLayoutChild();
+ if (mRowSecondarySizeRefresh) {
+ mRowSecondarySizeRefresh = false;
+ } else {
+ updateRowSecondarySizeRefresh();
+ }
- if (!hasDoneFirstLayout) {
- dispatchChildSelected();
+ if (!state.isPreLayout()) {
+ mUseDeltaInPreLayout = false;
+ if (!fastRelayout || mFocusPosition != savedFocusPos) {
+ dispatchChildSelected();
+ }
}
mInLayout = false;
+ leaveContext();
if (DEBUG) Log.v(getTag(), "layoutChildren end");
}
@@ -1340,7 +1543,6 @@
getChildAt(i).offsetLeftAndRight(increment);
}
}
- mScrollOffsetSecondary -= increment;
}
private void offsetChildrenPrimary(int increment) {
@@ -1354,79 +1556,196 @@
getChildAt(i).offsetLeftAndRight(increment);
}
}
- mScrollOffsetPrimary -= increment;
}
@Override
public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
- if (DEBUG) Log.v(TAG, "scrollHorizontallyBy " + dx);
-
- if (mOrientation == HORIZONTAL) {
- return scrollDirectionPrimary(dx);
- } else {
- return scrollDirectionSecondary(dx);
+ if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx);
+ if (!mLayoutEnabled || !hasDoneFirstLayout()) {
+ return 0;
}
+ saveContext(recycler, state);
+ int result;
+ if (mOrientation == HORIZONTAL) {
+ result = scrollDirectionPrimary(dx);
+ } else {
+ result = scrollDirectionSecondary(dx);
+ }
+ leaveContext();
+ return result;
}
@Override
public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) {
- if (DEBUG) Log.v(TAG, "scrollVerticallyBy " + dy);
- if (mOrientation == VERTICAL) {
- return scrollDirectionPrimary(dy);
- } else {
- return scrollDirectionSecondary(dy);
+ if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy);
+ if (!mLayoutEnabled || !hasDoneFirstLayout()) {
+ return 0;
}
+ saveContext(recycler, state);
+ int result;
+ if (mOrientation == VERTICAL) {
+ result = scrollDirectionPrimary(dy);
+ } else {
+ result = scrollDirectionSecondary(dy);
+ }
+ leaveContext();
+ return result;
}
// scroll in main direction may add/prune views
private int scrollDirectionPrimary(int da) {
+ if (da > 0) {
+ if (!mWindowAlignment.mainAxis().isMaxUnknown()) {
+ int maxScroll = mWindowAlignment.mainAxis().getMaxScroll();
+ if (mScrollOffsetPrimary + da > maxScroll) {
+ da = maxScroll - mScrollOffsetPrimary;
+ }
+ }
+ } else if (da < 0) {
+ if (!mWindowAlignment.mainAxis().isMinUnknown()) {
+ int minScroll = mWindowAlignment.mainAxis().getMinScroll();
+ if (mScrollOffsetPrimary + da < minScroll) {
+ da = minScroll - mScrollOffsetPrimary;
+ }
+ }
+ }
+ if (da == 0) {
+ return 0;
+ }
offsetChildrenPrimary(-da);
+ mScrollOffsetPrimary += da;
if (mInLayout) {
return da;
}
+
+ int childCount = getChildCount();
+ boolean updated;
+
if (da > 0) {
appendVisibleItems();
- removeInvisibleViewsAtFront();
} else if (da < 0) {
prependVisibleItems();
+ }
+ updated = getChildCount() > childCount;
+ childCount = getChildCount();
+
+ if (da > 0) {
+ removeInvisibleViewsAtFront();
+ } else if (da < 0) {
removeInvisibleViewsAtEnd();
}
- attemptAnimateLayoutChild();
+ updated |= getChildCount() < childCount;
+
+ if (updated) {
+ updateRowSecondarySizeRefresh();
+ }
+
mBaseGridView.invalidate();
return da;
}
// scroll in second direction will not add/prune views
private int scrollDirectionSecondary(int dy) {
+ if (dy == 0) {
+ return 0;
+ }
offsetChildrenSecondary(-dy);
+ mScrollOffsetSecondary += dy;
mBaseGridView.invalidate();
return dy;
}
private void updateScrollMax() {
- if (mLastVisiblePos >= 0 && mLastVisiblePos == mAdapter.getItemCount() - 1) {
- int maxEdge = Integer.MIN_VALUE;
- for (int i = 0; i < mRows.length; i++) {
- if (mRows[i].high > maxEdge) {
- maxEdge = mRows[i].high;
- }
+ if (mLastVisiblePos < 0) {
+ return;
+ }
+ final boolean lastAvailable = mLastVisiblePos == mState.getItemCount() - 1;
+ final boolean maxUnknown = mWindowAlignment.mainAxis().isMaxUnknown();
+ if (!lastAvailable && maxUnknown) {
+ return;
+ }
+ int maxEdge = Integer.MIN_VALUE;
+ int rowIndex = -1;
+ for (int i = 0; i < mRows.length; i++) {
+ if (mRows[i].high > maxEdge) {
+ maxEdge = mRows[i].high;
+ rowIndex = i;
}
+ }
+ int maxScroll = Integer.MAX_VALUE;
+ for (int i = mLastVisiblePos; i >= mFirstVisiblePos; i--) {
+ StaggeredGrid.Location location = mGrid.getLocation(i);
+ if (location != null && location.row == rowIndex) {
+ int savedMaxEdge = mWindowAlignment.mainAxis().getMaxEdge();
+ mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
+ maxScroll = getPrimarySystemScrollPosition(findViewByPosition(i));
+ mWindowAlignment.mainAxis().setMaxEdge(savedMaxEdge);
+ break;
+ }
+ }
+ if (lastAvailable) {
mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
- if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge);
+ mWindowAlignment.mainAxis().setMaxScroll(maxScroll);
+ if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge +
+ " scrollMax to " + maxScroll);
+ } else {
+ // the maxScroll for currently last visible item is larger,
+ // so we must invalidate the max scroll value.
+ if (maxScroll > mWindowAlignment.mainAxis().getMaxScroll()) {
+ mWindowAlignment.mainAxis().invalidateScrollMax();
+ if (DEBUG) Log.v(getTag(), "Invalidate scrollMax since it should be "
+ + "greater than " + maxScroll);
+ }
}
}
private void updateScrollMin() {
- if (mFirstVisiblePos == 0) {
- int minEdge = Integer.MAX_VALUE;
- for (int i = 0; i < mRows.length; i++) {
- if (mRows[i].low < minEdge) {
- minEdge = mRows[i].low;
- }
- }
- mWindowAlignment.mainAxis().setMinEdge(minEdge);
- if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge);
+ if (mFirstVisiblePos < 0) {
+ return;
}
+ final boolean firstAvailable = mFirstVisiblePos == 0;
+ final boolean minUnknown = mWindowAlignment.mainAxis().isMinUnknown();
+ if (!firstAvailable && minUnknown) {
+ return;
+ }
+ int minEdge = Integer.MAX_VALUE;
+ int rowIndex = -1;
+ for (int i = 0; i < mRows.length; i++) {
+ if (mRows[i].low < minEdge) {
+ minEdge = mRows[i].low;
+ rowIndex = i;
+ }
+ }
+ int minScroll = Integer.MIN_VALUE;
+ for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
+ StaggeredGrid.Location location = mGrid.getLocation(i);
+ if (location != null && location.row == rowIndex) {
+ int savedMinEdge = mWindowAlignment.mainAxis().getMinEdge();
+ mWindowAlignment.mainAxis().setMinEdge(minEdge);
+ minScroll = getPrimarySystemScrollPosition(findViewByPosition(i));
+ mWindowAlignment.mainAxis().setMinEdge(savedMinEdge);
+ break;
+ }
+ }
+ if (firstAvailable) {
+ mWindowAlignment.mainAxis().setMinEdge(minEdge);
+ mWindowAlignment.mainAxis().setMinScroll(minScroll);
+ if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge +
+ " scrollMin to " + minScroll);
+ } else {
+ // the minScroll for currently first visible item is smaller,
+ // so we must invalidate the min scroll value.
+ if (minScroll < mWindowAlignment.mainAxis().getMinScroll()) {
+ mWindowAlignment.mainAxis().invalidateScrollMin();
+ if (DEBUG) Log.v(getTag(), "Invalidate scrollMin, since it should be "
+ + "less than " + minScroll);
+ }
+ }
+ }
+
+ private void updateScrollSecondAxis() {
+ mWindowAlignment.secondAxis().setMinEdge(0);
+ mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary());
}
private void initScrollController() {
@@ -1435,18 +1754,10 @@
mWindowAlignment.vertical.setSize(getHeight());
mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
mSizePrimary = mWindowAlignment.mainAxis().getSize();
- mWindowAlignment.mainAxis().invalidateScrollMin();
- mWindowAlignment.mainAxis().invalidateScrollMax();
-
- // second axis min/max is determined at initialization, the mainAxis
- // min/max is determined later when we scroll to first or last item
- mWindowAlignment.secondAxis().setMinEdge(0);
- mWindowAlignment.secondAxis().setMaxEdge(mItemLengthSecondary * mNumRows + mMarginSecondary
- * (mNumRows - 1));
if (DEBUG) {
- Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary + " "
- + " mItemLengthSecondary " + mItemLengthSecondary + " " + mWindowAlignment);
+ Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
+ + " mWindowAlignment " + mWindowAlignment);
}
}
@@ -1466,24 +1777,60 @@
if (mFocusPosition == position) {
return;
}
- View view = getViewByPosition(position);
+ View view = findViewByPosition(position);
if (view != null) {
scrollToView(view, smooth);
} else {
- boolean right = position > mFocusPosition;
mFocusPosition = position;
+ if (!mLayoutEnabled) {
+ return;
+ }
if (smooth) {
if (!hasDoneFirstLayout()) {
Log.w(getTag(), "setSelectionSmooth should " +
"not be called before first layout pass");
return;
}
- if (right) {
- appendVisibleItems();
- } else {
- prependVisibleItems();
- }
- scrollToView(getViewByPosition(position), smooth);
+ LinearSmoothScroller linearSmoothScroller =
+ new LinearSmoothScroller(parent.getContext()) {
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ if (getChildCount() == 0) {
+ return null;
+ }
+ final int firstChildPos = getPosition(getChildAt(0));
+ final int direction = targetPosition < firstChildPos ? -1 : 1;
+ if (mOrientation == HORIZONTAL) {
+ return new PointF(direction, 0);
+ } else {
+ return new PointF(0, direction);
+ }
+ }
+ @Override
+ protected void onTargetFound(View targetView,
+ RecyclerView.State state, Action action) {
+ if (hasFocus()) {
+ targetView.requestFocus();
+ } else {
+ dispatchChildSelected();
+ }
+ if (getScrollPosition(targetView, mTempDeltas)) {
+ int dx, dy;
+ if (mOrientation == HORIZONTAL) {
+ dx = mTempDeltas[0];
+ dy = mTempDeltas[1];
+ } else {
+ dx = mTempDeltas[1];
+ dy = mTempDeltas[0];
+ }
+ final int distance = (int) Math.sqrt(dx * dx + dy * dy);
+ final int time = calculateTimeForDeceleration(distance);
+ action.update(dx, dy, time, mDecelerateInterpolator);
+ }
+ }
+ };
+ linearSmoothScroller.setTargetPosition(position);
+ startSmoothScroll(linearSmoothScroller);
} else {
mForceFullLayout = true;
parent.requestLayout();
@@ -1534,12 +1881,49 @@
}
public void getViewSelectedOffsets(View view, int[] offsets) {
- int scrollOffsetX = getScrollOffsetX();
- int scrollOffsetY = getScrollOffsetY();
- int viewCenterX = scrollOffsetX + getViewCenterX(view);
- int viewCenterY = scrollOffsetY + getViewCenterY(view);
- offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX;
- offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY;
+ if (mOrientation == HORIZONTAL) {
+ offsets[0] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary;
+ offsets[1] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary;
+ } else {
+ offsets[1] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary;
+ offsets[0] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary;
+ }
+ }
+
+ private int getPrimarySystemScrollPosition(View view) {
+ int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
+ int pos = getPositionByView(view);
+ StaggeredGrid.Location location = mGrid.getLocation(pos);
+ final int row = location.row;
+ boolean isFirst = mFirstVisiblePos == 0;
+ // TODO: change to use State object in onRequestChildFocus()
+ boolean isLast = mLastVisiblePos == (mState == null ?
+ getItemCount() : mState.getItemCount()) - 1;
+ if (isFirst || isLast) {
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ int position = getPositionByIndex(i);
+ StaggeredGrid.Location loc = mGrid.getLocation(position);
+ if (loc != null && loc.row == row) {
+ if (position < pos) {
+ isFirst = false;
+ } else if (position > pos) {
+ isLast = false;
+ }
+ }
+ }
+ }
+ return mWindowAlignment.mainAxis().getSystemScrollPos(viewCenterPrimary, isFirst, isLast);
+ }
+
+ private int getSecondarySystemScrollPosition(View view) {
+ int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
+ int pos = getPositionByView(view);
+ StaggeredGrid.Location location = mGrid.getLocation(pos);
+ final int row = location.row;
+ boolean isFirst = row == 0;
+ boolean isLast = row == mGrid.getNumRows() - 1;
+ return mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary,
+ isFirst, isLast);
}
/**
@@ -1547,9 +1931,11 @@
*/
private void scrollToView(View view, boolean smooth) {
int newFocusPosition = getPositionByView(view);
- if (mInLayout || newFocusPosition != mFocusPosition) {
+ if (newFocusPosition != mFocusPosition) {
mFocusPosition = newFocusPosition;
- dispatchChildSelected();
+ if (!mInLayout) {
+ dispatchChildSelected();
+ }
}
if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
mBaseGridView.invalidate();
@@ -1562,19 +1948,23 @@
// by setSelection())
view.requestFocus();
}
- switch (mFocusScrollStrategy) {
- case BaseGridView.FOCUS_SCROLL_ALIGNED:
- default:
- scrollToAlignedPosition(view, smooth);
- break;
- case BaseGridView.FOCUS_SCROLL_ITEM:
- case BaseGridView.FOCUS_SCROLL_PAGE:
- scrollItemOrPage(view, smooth);
- break;
+ if (getScrollPosition(view, mTempDeltas)) {
+ scrollGrid(mTempDeltas[0], mTempDeltas[1], smooth);
}
}
- private void scrollItemOrPage(View view, boolean smooth) {
+ private boolean getScrollPosition(View view, int[] deltas) {
+ switch (mFocusScrollStrategy) {
+ case BaseGridView.FOCUS_SCROLL_ALIGNED:
+ default:
+ return getAlignedPosition(view, deltas);
+ case BaseGridView.FOCUS_SCROLL_ITEM:
+ case BaseGridView.FOCUS_SCROLL_PAGE:
+ return getNoneAlignedPosition(view, deltas);
+ }
+ }
+
+ private boolean getNoneAlignedPosition(View view, int[] deltas) {
int pos = getPositionByView(view);
int viewMin = getViewMin(view);
int viewMax = getViewMax(view);
@@ -1594,10 +1984,10 @@
while (!prependOneVisibleItem()) {
List<Integer> positions =
mGrid.getItemPositionsInRows(mFirstVisiblePos, pos)[row];
- firstView = getViewByPosition(positions.get(0));
+ firstView = findViewByPosition(positions.get(0));
if (viewMax - getViewMin(firstView) > clientSize) {
if (positions.size() > 1) {
- firstView = getViewByPosition(positions.get(1));
+ firstView = findViewByPosition(positions.get(1));
}
break;
}
@@ -1611,7 +2001,7 @@
do {
List<Integer> positions =
mGrid.getItemPositionsInRows(pos, mLastVisiblePos)[row];
- lastView = getViewByPosition(positions.get(positions.size() - 1));
+ lastView = findViewByPosition(positions.get(positions.size() - 1));
if (getViewMax(lastView) - viewMin > clientSize) {
lastView = null;
break;
@@ -1640,38 +2030,31 @@
} else {
secondaryAlignedView = view;
}
- int viewCenterSecondary = mScrollOffsetSecondary +
- getViewCenterSecondary(secondaryAlignedView);
- mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
- scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
+ scrollSecondary = getSecondarySystemScrollPosition(secondaryAlignedView);
scrollSecondary -= mScrollOffsetSecondary;
- scrollGrid(scrollPrimary, scrollSecondary, smooth);
+ if (scrollPrimary != 0 || scrollSecondary != 0) {
+ deltas[0] = scrollPrimary;
+ deltas[1] = scrollSecondary;
+ return true;
+ }
+ return false;
}
- private void scrollToAlignedPosition(View view, boolean smooth) {
- int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
- int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
+ private boolean getAlignedPosition(View view, int[] deltas) {
+ int scrollPrimary = getPrimarySystemScrollPosition(view);
+ int scrollSecondary = getSecondarySystemScrollPosition(view);
if (DEBUG) {
- Log.v(getTag(), "scrollAligned smooth=" + smooth + " pos=" + mFocusPosition + " "
- + viewCenterPrimary +","+viewCenterSecondary + " " + mWindowAlignment);
+ Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary
+ +" " + mWindowAlignment);
}
-
- if (mInLayout || viewCenterPrimary != mWindowAlignment.mainAxis().getScrollCenter()
- || viewCenterSecondary != mWindowAlignment.secondAxis().getScrollCenter()) {
- mWindowAlignment.mainAxis().updateScrollCenter(viewCenterPrimary);
- mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
- int scrollPrimary = mWindowAlignment.mainAxis().getSystemScrollPos();
- int scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
- if (DEBUG) {
- Log.v(getTag(), "scrollAligned " + scrollPrimary + " " + scrollSecondary
- +" " + mWindowAlignment);
- }
-
- scrollPrimary -= mScrollOffsetPrimary;
- scrollSecondary -= mScrollOffsetSecondary;
-
- scrollGrid(scrollPrimary, scrollSecondary, smooth);
+ scrollPrimary -= mScrollOffsetPrimary;
+ scrollSecondary -= mScrollOffsetSecondary;
+ if (scrollPrimary != 0 || scrollSecondary != 0) {
+ deltas[0] = scrollPrimary;
+ deltas[1] = scrollSecondary;
+ return true;
}
+ return false;
}
private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
@@ -1696,50 +2079,17 @@
}
}
- public void setAnimateChildLayout(boolean animateChildLayout) {
- mAnimateChildLayout = animateChildLayout;
- for (int i = 0, c = getChildCount(); i < c; i++) {
- View v = getChildAt(i);
- LayoutParams p = (LayoutParams) v.getLayoutParams();
- if (!mAnimateChildLayout) {
- p.endAnimate();
- } else {
- // record initial location values
- p.mFirstAttached = true;
- p.startAnimate(this, v, 0);
+ public void setPruneChild(boolean pruneChild) {
+ if (mPruneChild != pruneChild) {
+ mPruneChild = pruneChild;
+ if (mPruneChild) {
+ requestLayout();
}
}
}
- private void attemptAnimateLayoutChild() {
- if (!mAnimateChildLayout) {
- return;
- }
- for (int i = 0, c = getChildCount(); i < c; i++) {
- // TODO: start delay can be staggered
- View v = getChildAt(i);
- ((LayoutParams) v.getLayoutParams()).startAnimate(this, v, 0);
- }
- }
-
- public boolean isChildLayoutAnimated() {
- return mAnimateChildLayout;
- }
-
- public void setChildLayoutAnimationInterpolator(Interpolator interpolator) {
- mAnimateLayoutChildInterpolator = interpolator;
- }
-
- public Interpolator getChildLayoutAnimationInterpolator() {
- return mAnimateLayoutChildInterpolator;
- }
-
- public void setChildLayoutAnimationDuration(long duration) {
- mAnimateLayoutChildDuration = duration;
- }
-
- public long getChildLayoutAnimationDuration() {
- return mAnimateLayoutChildDuration;
+ public boolean getPruneChild() {
+ return mPruneChild;
}
private int findImmediateChildIndex(View view) {
@@ -1769,6 +2119,26 @@
return null;
}
+ boolean hasPreviousViewInSameRow(int pos) {
+ if (mGrid == null || pos == NO_POSITION) {
+ return false;
+ }
+ if (mFirstVisiblePos > 0) {
+ return true;
+ }
+ final int focusedRow = mGrid.getLocation(pos).row;
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ int position = getPositionByIndex(i);
+ StaggeredGrid.Location loc = mGrid.getLocation(position);
+ if (loc != null && loc.row == focusedRow) {
+ if (position < pos) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
@Override
public boolean onAddFocusables(RecyclerView recyclerView,
ArrayList<View> views, int direction, int focusableMode) {
@@ -1792,7 +2162,7 @@
final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
// Add focusables of focused item.
if (focusedPos != NO_POSITION) {
- getViewByPosition(focusedPos).addFocusables(views, direction, focusableMode);
+ findViewByPosition(focusedPos).addFocusables(views, direction, focusableMode);
}
final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
mGrid.getLocation(focusedPos).row : NO_POSITION;
@@ -1861,6 +2231,7 @@
RecyclerView.State state) {
if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
+ saveContext(recycler, state);
View view = null;
int movement = getMovement(direction);
final FocusFinder ff = FocusFinder.getInstance();
@@ -1881,6 +2252,7 @@
view = mFocusOutEnd ? null : focused;
}
}
+ leaveContext();
if (DEBUG) Log.v(getTag(), "returning view " + view);
return view;
}
@@ -1901,7 +2273,7 @@
private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
int direction, Rect previouslyFocusedRect) {
- View view = getViewByPosition(mFocusPosition);
+ View view = findViewByPosition(mFocusPosition);
if (view != null) {
boolean result = view.requestFocus(direction, previouslyFocusedRect);
if (!result && DEBUG) {
@@ -1987,10 +2359,11 @@
}
int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
- int focusIndex = getIndexByPosition(mFocusPosition);
- if (focusIndex == NO_POSITION) {
+ View view = findViewByPosition(mFocusPosition);
+ if (view == null) {
return i;
}
+ int focusIndex = recyclerView.indexOfChild(view);
// supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
// drawing order is 0 1 2 3 9 8 7 6 5 4
if (i < focusIndex) {
@@ -2005,8 +2378,24 @@
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
RecyclerView.Adapter newAdapter) {
+ discardLayoutInfo();
+ mFocusPosition = NO_POSITION;
+ super.onAdapterChanged(oldAdapter, newAdapter);
+ }
+
+ private void discardLayoutInfo() {
mGrid = null;
mRows = null;
- super.onAdapterChanged(oldAdapter, newAdapter);
+ mRowSizeSecondary = null;
+ mFirstVisiblePos = -1;
+ mLastVisiblePos = -1;
+ mRowSecondarySizeRefresh = false;
+ }
+
+ public void setLayoutEnabled(boolean layoutEnabled) {
+ if (mLayoutEnabled != layoutEnabled) {
+ mLayoutEnabled = layoutEnabled;
+ requestLayout();
+ }
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
index 2024425..98acea6 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
@@ -27,6 +27,7 @@
import android.support.v17.leanback.R;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
+import android.util.TypedValue;
import android.view.View;
/**
@@ -66,7 +67,7 @@
protected void initAttributes(Context context, AttributeSet attrs) {
initBaseGridViewAttributes(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbHorizontalGridView);
- setRowHeight(a.getDimensionPixelSize(R.styleable.lbHorizontalGridView_rowHeight, 0));
+ setRowHeight(a);
setNumRows(a.getInt(R.styleable.lbHorizontalGridView_numberOfRows, 1));
a.recycle();
setWillNotDraw(false);
@@ -74,8 +75,19 @@
mTempPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
}
+ void setRowHeight(TypedArray array) {
+ TypedValue typedValue = array.peekValue(R.styleable.lbHorizontalGridView_rowHeight);
+ int size;
+ if (typedValue != null && typedValue.type == TypedValue.TYPE_DIMENSION) {
+ size = array.getDimensionPixelSize(R.styleable.lbHorizontalGridView_rowHeight, 0);
+ } else {
+ size = array.getInt(R.styleable.lbHorizontalGridView_rowHeight, 0);
+ }
+ setRowHeight(size);
+ }
+
/**
- * Set the number of rows.
+ * Set the number of rows. Defaults to one.
*/
public void setNumRows(int numRows) {
mLayoutManager.setNumRows(numRows);
@@ -84,6 +96,9 @@
/**
* Set the row height.
+ *
+ * @param height May be WRAP_CONTENT, or a size in pixels. If zero,
+ * row height will be fixed based on number of rows and view height.
*/
public void setRowHeight(int height) {
mLayoutManager.setRowHeight(height);
@@ -264,17 +279,18 @@
if (mTempBitmapHigh == null
|| mTempBitmapHigh.getWidth() != mHighFadeShaderLength
|| mTempBitmapHigh.getHeight() != getHeight()) {
- if (mTempBitmapLow != null
+ // TODO: fix logic for sharing mTempBitmapLow
+ if (false && mTempBitmapLow != null
&& mTempBitmapLow.getWidth() == mHighFadeShaderLength
&& mTempBitmapLow.getHeight() == getHeight()) {
// share same bitmap for low edge fading and high edge fading.
mTempBitmapHigh = mTempBitmapLow;
} else {
- mTempBitmapLow = Bitmap.createBitmap(mHighFadeShaderLength, getHeight(),
+ mTempBitmapHigh = Bitmap.createBitmap(mHighFadeShaderLength, getHeight(),
Bitmap.Config.ARGB_8888);
}
}
- return mTempBitmapLow;
+ return mTempBitmapHigh;
}
@Override
@@ -298,8 +314,8 @@
// draw not-fade content
int save = canvas.save();
- canvas.clipRect(lowEdge + mLowFadeShaderLength, 0,
- highEdge - mHighFadeShaderLength, getHeight());
+ canvas.clipRect(lowEdge + (mFadingLowEdge ? mLowFadeShaderLength : 0), 0,
+ highEdge - (mFadingHighEdge ? mHighFadeShaderLength : 0), getHeight());
super.draw(canvas);
canvas.restoreToCount(save);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java b/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
index 663a58b..3b411d7 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
@@ -22,8 +22,12 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
import android.widget.TextView;
+/**
+ * A card view with an {@link ImageView} as its main region.
+ */
public class ImageCardView extends BaseCardView {
private ImageView mImageView;
@@ -56,6 +60,22 @@
mBadgeFadeMask = (ImageView) v.findViewById(R.id.fade_mask);
}
+ public final ImageView getMainImageView() {
+ return mImageView;
+ }
+
+ public void setMainImageAdjustViewBounds(boolean adjustViewBounds) {
+ if (mImageView != null) {
+ mImageView.setAdjustViewBounds(adjustViewBounds);
+ }
+ }
+
+ public void setMainImageScaleType(ScaleType scaleType) {
+ if (mImageView != null) {
+ mImageView.setScaleType(scaleType);
+ }
+ }
+
public void setMainImage(Drawable drawable) {
if (mImageView == null) {
return;
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java b/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
index cefb431..3cc3ddf 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
@@ -36,6 +36,7 @@
private int mOffset = 0;
private float mOffsetPercent = 50;
private int mViewId = 0;
+ private boolean mOffsetWithPadding = false;
private Rect mRect = new Rect();
Axis(int orientation) {
@@ -50,6 +51,14 @@
return mOffset;
}
+ public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
+ mOffsetWithPadding = withPadding;
+ }
+
+ public boolean isItemAlignmentOffsetWithPadding() {
+ return mOffsetWithPadding;
+ }
+
public void setItemAlignmentOffsetPercent(float percent) {
if ( (percent < 0 || percent > 100) &&
percent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) {
@@ -70,8 +79,10 @@
return mViewId;
}
+ /**
+ * get alignment position relative to optical left/top of itemView.
+ */
public int getAlignmentPosition(View itemView) {
-
LayoutParams p = (LayoutParams) itemView.getLayoutParams();
View view = itemView;
if (mViewId != 0) {
@@ -84,30 +95,46 @@
if (mOrientation == HORIZONTAL) {
if (mOffset >= 0) {
alignPos = mOffset;
+ if (mOffsetWithPadding) {
+ alignPos += view.getPaddingLeft();
+ }
} else {
- alignPos = p.getOpticalWidth(itemView) + mOffset;
+ alignPos = view == itemView ? p.getOpticalWidth(view) : view.getWidth()
+ + mOffset;
+ if (mOffsetWithPadding) {
+ alignPos -= view.getPaddingRight();
+ }
}
if (mOffsetPercent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) {
- alignPos += (p.getOpticalWidth(itemView) * mOffsetPercent) / 100;
+ alignPos += ((view == itemView ? p.getOpticalWidth(view) : view.getWidth())
+ * mOffsetPercent) / 100f;
}
if (itemView != view) {
mRect.left = alignPos;
((ViewGroup) itemView).offsetDescendantRectToMyCoords(view, mRect);
- alignPos = mRect.left;
+ alignPos = mRect.left - p.getOpticalLeftInset();
}
} else {
if (mOffset >= 0) {
alignPos = mOffset;
+ if (mOffsetWithPadding) {
+ alignPos += view.getPaddingTop();
+ }
} else {
- alignPos = p.getOpticalHeight(itemView) + mOffset;
+ alignPos = view == itemView ? p.getOpticalHeight(view) : view.getHeight()
+ + mOffset;
+ if (mOffsetWithPadding) {
+ alignPos += view.getPaddingBottom();
+ }
}
if (mOffsetPercent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) {
- alignPos += (p.getOpticalHeight(itemView) * mOffsetPercent) / 100;
+ alignPos += ((view == itemView ? p.getOpticalHeight(view) : view.getHeight())
+ * mOffsetPercent) / 100f;
}
if (itemView != view) {
mRect.top = alignPos;
((ViewGroup) itemView).offsetDescendantRectToMyCoords(view, mRect);
- alignPos = mRect.top;
+ alignPos = mRect.top - p.getOpticalTopInset();
}
}
return alignPos;
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
index bc4a476..e970171 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
@@ -14,7 +14,6 @@
package android.support.v17.leanback.widget;
import android.support.v7.widget.RecyclerView;
-import android.support.v17.leanback.R;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -33,7 +32,7 @@
* Interface for listening to view holder operations.
*/
public static class AdapterListener {
- public void onAddPresenter(Presenter presenter) {
+ public void onAddPresenter(Presenter presenter, int type) {
}
public void onCreate(ViewHolder viewHolder) {
}
@@ -198,6 +197,14 @@
setAdapter(null);
}
+ public void setPresenterMapper(ArrayList<Presenter> presenters) {
+ mPresenters = presenters;
+ }
+
+ public ArrayList<Presenter> getPresenterMapper() {
+ return mPresenters;
+ }
+
@Override
public int getItemCount() {
return mAdapter.size();
@@ -213,8 +220,9 @@
if (type < 0) {
mPresenters.add(presenter);
type = mPresenters.indexOf(presenter);
+ if (DEBUG) Log.v(TAG, "getItemViewType added presenter " + presenter + " type " + type);
if (mAdapterListener != null) {
- mAdapterListener.onAddPresenter(presenter);
+ mAdapterListener.onAddPresenter(presenter, type);
}
}
return type;
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
index 0de39ab..6b03fd0 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
@@ -13,20 +13,14 @@
*/
package android.support.v17.leanback.widget;
-import java.util.ArrayList;
-
import android.content.Context;
import android.content.res.TypedArray;
-import android.graphics.Canvas;
import android.support.v17.leanback.R;
import android.support.v17.leanback.graphics.ColorOverlayDimmer;
-import android.support.v17.leanback.widget.Presenter.ViewHolder;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
+import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
-import android.widget.FrameLayout;
/**
* ListRowPresenter renders {@link ListRow} using a
@@ -59,12 +53,20 @@
final ItemBridgeAdapter mItemBridgeAdapter = new ItemBridgeAdapter();
final HorizontalHoverCardSwitcher mHoverCardViewSwitcher = new HorizontalHoverCardSwitcher();
final ColorOverlayDimmer mColorDimmer;
+ final int mPaddingTop;
+ final int mPaddingBottom;
+ final int mPaddingLeft;
+ final int mPaddingRight;
public ViewHolder(View rootView, HorizontalGridView gridView, ListRowPresenter p) {
super(rootView);
mGridView = gridView;
mListRowPresenter = p;
mColorDimmer = ColorOverlayDimmer.createDefault(rootView.getContext());
+ mPaddingTop = mGridView.getPaddingTop();
+ mPaddingBottom = mGridView.getPaddingBottom();
+ mPaddingLeft = mGridView.getPaddingLeft();
+ mPaddingRight = mGridView.getPaddingRight();
}
public final ListRowPresenter getListRowPresenter() {
@@ -74,13 +76,23 @@
public final HorizontalGridView getGridView() {
return mGridView;
}
+
+ public final ItemBridgeAdapter getBridgeAdapter() {
+ return mItemBridgeAdapter;
+ }
}
+ private int mRowHeight;
+ private int mExpandedRowHeight;
private PresenterSelector mHoverCardPresenterSelector;
private int mZoomFactor;
private boolean mShadowEnabled = true;
private int mBrowseRowsFadingEdgeLength = -1;
+ private static int sSelectedRowTopPadding;
+ private static int sExpandedSelectedRowTopPadding;
+ private static int sExpandedRowNoHovercardBottomPadding;
+
/**
* Constructs a ListRowPresenter with defaults.
* Uses {@link FocusHighlight#ZOOM_FACTOR_MEDIUM} for focus zooming.
@@ -103,6 +115,43 @@
}
/**
+ * Sets the row height for rows created by this Presenter. Rows
+ * created before calling this method will not be updated.
+ *
+ * @param rowHeight Row height in pixels, or WRAP_CONTENT, or 0
+ * to use the default height.
+ */
+ public void setRowHeight(int rowHeight) {
+ mRowHeight = rowHeight;
+ }
+
+ /**
+ * Returns the row height for list rows created by this Presenter.
+ */
+ public int getRowHeight() {
+ return mRowHeight;
+ }
+
+ /**
+ * Sets the expanded row height for rows created by this Presenter.
+ * If not set, expanded rows have the same height as unexpanded
+ * rows.
+ *
+ * @param rowHeight The row height in to use when the row is expanded,
+ * in pixels, or WRAP_CONTENT, or 0 to use the default.
+ */
+ public void setExpandedRowHeight(int rowHeight) {
+ mExpandedRowHeight = rowHeight;
+ }
+
+ /**
+ * Returns the expanded row height for rows created by this Presenter.
+ */
+ public int getExpandedRowHeight() {
+ return mExpandedRowHeight != 0 ? mExpandedRowHeight : mRowHeight;
+ }
+
+ /**
* Returns the zoom factor used for focus highlighting.
*/
public final int getZoomFactor() {
@@ -139,6 +188,7 @@
}
}
FocusHighlightHelper.setupBrowseItemFocusHighlight(rowViewHolder.mItemBridgeAdapter, mZoomFactor);
+ rowViewHolder.mGridView.setFocusDrawingOrderEnabled(!isUsingZOrder());
rowViewHolder.mGridView.setOnChildSelectedListener(
new OnChildSelectedListener() {
@Override
@@ -151,16 +201,21 @@
@Override
public void onCreate(final ItemBridgeAdapter.ViewHolder viewHolder) {
// Only when having an OnItemClickListner, we will attach the OnClickListener.
- if (getOnItemClickedListener() != null) {
+ if (getOnItemClickedListener() != null || getOnItemViewClickedListener() != null) {
viewHolder.mHolder.view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
- rowViewHolder.mGridView.getChildViewHolder(viewHolder.itemView);
+ rowViewHolder.mGridView
+ .getChildViewHolder(viewHolder.itemView);
if (getOnItemClickedListener() != null) {
getOnItemClickedListener().onItemClicked(ibh.mItem,
(ListRow) rowViewHolder.mRow);
}
+ if (getOnItemViewClickedListener() != null) {
+ getOnItemViewClickedListener().onItemClicked(viewHolder.mHolder,
+ ibh.mItem, rowViewHolder, (ListRow) rowViewHolder.mRow);
+ }
}
});
}
@@ -172,6 +227,12 @@
int dimmedColor = rowViewHolder.mColorDimmer.getPaint().getColor();
((ShadowOverlayContainer) viewHolder.itemView).setOverlayColor(dimmedColor);
}
+ viewHolder.itemView.setActivated(rowViewHolder.mExpanded);
+ }
+
+ @Override
+ public void onAddPresenter(Presenter presenter, int type) {
+ rowViewHolder.getGridView().getRecycledViewPool().setMaxRecycledViews(type, 24);
}
});
}
@@ -207,6 +268,10 @@
if (mHoverCardPresenterSelector != null) {
rowViewHolder.mHoverCardViewSwitcher.unselect();
}
+ if (getOnItemViewSelectedListener() != null) {
+ getOnItemViewSelectedListener().onItemSelected(null, null,
+ rowViewHolder, rowViewHolder.mRow);
+ }
if (getOnItemSelectedListener() != null) {
getOnItemSelectedListener().onItemSelected(null, rowViewHolder.mRow);
}
@@ -215,22 +280,75 @@
rowViewHolder.mHoverCardViewSwitcher.select(rowViewHolder.mGridView, view,
ibh.mItem);
}
+ if (getOnItemViewSelectedListener() != null) {
+ getOnItemViewSelectedListener().onItemSelected(ibh.mHolder, ibh.mItem,
+ rowViewHolder, rowViewHolder.mRow);
+ }
if (getOnItemSelectedListener() != null) {
getOnItemSelectedListener().onItemSelected(ibh.mItem, rowViewHolder.mRow);
}
}
}
+ private static void initStatics(Context context) {
+ if (sSelectedRowTopPadding == 0) {
+ sSelectedRowTopPadding = context.getResources().getDimensionPixelSize(
+ R.dimen.lb_browse_selected_row_top_padding);
+ sExpandedSelectedRowTopPadding = context.getResources().getDimensionPixelSize(
+ R.dimen.lb_browse_expanded_selected_row_top_padding);
+ sExpandedRowNoHovercardBottomPadding = context.getResources().getDimensionPixelSize(
+ R.dimen.lb_browse_expanded_row_no_hovercard_bottom_padding);
+ }
+ }
+
+ private int getSpaceUnderBaseline(ListRowPresenter.ViewHolder vh) {
+ RowHeaderPresenter.ViewHolder headerViewHolder = vh.getHeaderViewHolder();
+ if (headerViewHolder != null) {
+ if (getHeaderPresenter() != null) {
+ return getHeaderPresenter().getSpaceUnderBaseline(headerViewHolder);
+ }
+ return headerViewHolder.view.getPaddingBottom();
+ }
+ return 0;
+ }
+
+ private void setVerticalPadding(ListRowPresenter.ViewHolder vh) {
+ int paddingTop, paddingBottom;
+ if (vh.isExpanded()) {
+ int headerSpaceUnderBaseline = getSpaceUnderBaseline(vh);
+ if (DEBUG) Log.v(TAG, "headerSpaceUnderBaseline " + headerSpaceUnderBaseline);
+ paddingTop = (vh.isSelected() ? sExpandedSelectedRowTopPadding : vh.mPaddingTop) -
+ headerSpaceUnderBaseline;
+ paddingBottom = mHoverCardPresenterSelector == null ?
+ sExpandedRowNoHovercardBottomPadding : vh.mPaddingBottom;
+ } else if (vh.isSelected()) {
+ paddingTop = sSelectedRowTopPadding;
+ paddingBottom = sSelectedRowTopPadding - vh.mPaddingTop;
+ } else {
+ paddingTop = vh.mPaddingTop;
+ paddingBottom = 0;
+ }
+ vh.getGridView().setPadding(vh.mPaddingLeft, paddingTop, vh.mPaddingRight,
+ paddingBottom);
+ }
+
@Override
protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
+ initStatics(parent.getContext());
ListRowView rowView = new ListRowView(parent.getContext());
setupFadingEffect(rowView);
+ if (mRowHeight != 0) {
+ rowView.getGridView().setRowHeight(mRowHeight);
+ }
return new ViewHolder(rowView, rowView.getGridView(), this);
}
@Override
protected void onRowViewSelected(RowPresenter.ViewHolder holder, boolean selected) {
- updateFooterViewSwitcher((ViewHolder) holder);
+ super.onRowViewSelected(holder, selected);
+ ViewHolder vh = (ViewHolder) holder;
+ setVerticalPadding(vh);
+ updateFooterViewSwitcher(vh);
}
/*
@@ -248,7 +366,7 @@
selectChildView(vh, ibh == null ? null : ibh.itemView);
} else {
if (mHoverCardPresenterSelector != null) {
- vh.mHoverCardViewSwitcher.clear();
+ vh.mHoverCardViewSwitcher.unselect();
}
}
}
@@ -270,6 +388,11 @@
protected void onRowViewExpanded(RowPresenter.ViewHolder holder, boolean expanded) {
super.onRowViewExpanded(holder, expanded);
ViewHolder vh = (ViewHolder) holder;
+ if (getRowHeight() != getExpandedRowHeight()) {
+ int newHeight = expanded ? getExpandedRowHeight() : getRowHeight();
+ vh.getGridView().setRowHeight(newHeight);
+ }
+ setVerticalPadding(vh);
vh.getGridView().setFadingLeftEdge(!expanded);
updateFooterViewSwitcher(vh);
}
@@ -319,6 +442,15 @@
}
/**
+ * Returns true if SDK >= L, where Z shadow is enabled so that Z order is enabled
+ * on each child of horizontal list. If subclass returns false in isUsingDefaultShadow()
+ * and does not use Z-shadow on SDK >= L, it should override isUsingZOrder() return false.
+ */
+ public boolean isUsingZOrder() {
+ return ShadowHelper.getInstance().usesZShadow();
+ }
+
+ /**
* Enable or disable child shadow.
* This is not only for enable/disable default shadow implementation but also subclass must
* respect this flag.
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java b/v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java
index 41da46f..ab5729b 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java
@@ -15,16 +15,10 @@
import android.support.v17.leanback.R;
import android.content.Context;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.Rect;
import android.util.AttributeSet;
-import android.util.TypedValue;
import android.view.LayoutInflater;
-import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
-import android.widget.TextView;
/**
* ListRowView contains a horizontal grid view.
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
index 011b9c6..6a2f610 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
@@ -21,6 +21,7 @@
*/
public abstract class ObjectAdapter {
+ /** Indicates that an id has not been set. */
public static final int NO_ID = -1;
/**
@@ -31,7 +32,7 @@
public static abstract class DataObserver {
/**
* Called whenever the ObjectAdapter's data has changed in some manner
- * outside of the set of changes covered by the other range based change
+ * outside of the set of changes covered by the other range-based change
* notification methods.
*/
public void onChanged() {
@@ -148,7 +149,7 @@
}
/**
- * Returns the presenter selector;
+ * Returns the presenter selector for this ObjectAdapter.
*/
public final PresenterSelector getPresenterSelector() {
return mPresenterSelector;
@@ -169,7 +170,7 @@
}
/**
- * Unregister all DataObservers for this ObservableList.
+ * Unregister all DataObservers for this ObjectAdapter.
*/
public final void unregisterAllObservers() {
mObservable.unregisterAll();
@@ -193,8 +194,8 @@
/**
* Indicates whether the item ids are stable across changes to the
- * underlying data. When this is true, client of Adapter can use
- * {@link #getId(int)} to correlate objects across changes.
+ * underlying data. When this is true, clients of the ObjectAdapter can use
+ * {@link #getId(int)} to correlate Objects across changes.
*/
public final boolean hasStableIds() {
return mHasStableIds;
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java b/v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
index 531c1cf..5dd45e1 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
@@ -13,8 +13,6 @@
*/
package android.support.v17.leanback.widget;
-import android.view.View;
-
/**
* Interface for receiving notification when an action is clicked.
*/
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemClickedListener.java b/v17/leanback/src/android/support/v17/leanback/widget/OnItemClickedListener.java
index 9530f90..c6b33b5 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/OnItemClickedListener.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/OnItemClickedListener.java
@@ -17,11 +17,8 @@
/**
* Interface for receiving notification when a item is clicked.
- * <p>
- * Alternatively {@link Presenter} can attach its own {@link View.OnClickListener} in
- * {@link Presenter#onCreateViewHolder(android.view.ViewGroup)}; but developer should never
- * use these two listeners together.
- * </p>
+ *
+ * @deprecated Uses {@link OnItemViewClickedListener}
*/
public interface OnItemClickedListener {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.java b/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.java
index 946c69d..7d46e8f 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.java
@@ -15,13 +15,15 @@
/**
* Interface for receiving notification when a row or item becomes selected.
+ *
+ * @deprecated Use {@link OnItemViewSelectedListener}
*/
public interface OnItemSelectedListener {
/**
* Called when the a row or a new item becomes selected. The concept of current selection
* is different than focus. Row or item can be selected even they don't have focus.
* Having the concept of selection will allow developer to switch background to selected
- * item or selected row when user selects rows outside row UI (e.g. a fast lane next to
+ * item or selected row when user selects rows outside row UI (e.g. headers left of
* rows).
* <p>
* For a none {@link ListRow} case, parameter item is always null. Event is fired when
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java b/v17/leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java
new file mode 100644
index 0000000..c5dc25c
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/OnItemViewClickedListener.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+/**
+ * Interface for receiving notification when a item view holder is clicked.
+ */
+public interface OnItemViewClickedListener {
+
+ public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row);
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java b/v17/leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java
new file mode 100644
index 0000000..9ecfffb
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/OnItemViewSelectedListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+/**
+ * Interface for receiving notification when a row or item becomes selected.
+ */
+public interface OnItemViewSelectedListener {
+ /**
+ * Called when the a row or a new item becomes selected. The concept of current selection
+ * is different than focus. Row or item can be selected even they don't have focus.
+ * Having the concept of selection will allow developer to switch background to selected
+ * item or selected row when user selects rows outside row UI (e.g. headers left of
+ * rows).
+ * <p>
+ * For a none {@link ListRow} case, parameter item is always null. Event is fired when
+ * selection changes between rows, regardless if row view has focus or not.
+ * <p>
+ * For a {@link ListRow} case, parameter item can be null if the list row is empty.
+ * </p>
+ * <p>
+ * In the case of a grid, the row parameter is always null.
+ * </p>
+ * <li>
+ * Row has focus: event is fired when focus changes between child of the row.
+ * </li>
+ * <li>
+ * None of the row has focus: the event is fired with the current selected row and last
+ * focused item in the row.
+ * </li>
+ *
+ * @param itemViewHolder The view holder of item that is currently selected.
+ * @param item The item that is currently selected.
+ * @param rowViewHolder The view holder of row that is currently selected.
+ * @param row The row that is currently selected.
+ */
+ public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
+ RowPresenter.ViewHolder rowViewHolder, Row row);
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Row.java b/v17/leanback/src/android/support/v17/leanback/widget/Row.java
index 893d5c0..237fcf7 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/Row.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/Row.java
@@ -16,9 +16,9 @@
import static android.support.v17.leanback.widget.ObjectAdapter.NO_ID;
/**
- * A row in the RowContainerFragment. This is the basic class for all Rows.
- * Developer usually overrides {@link ListRow}, but may override this class
- * for non-list Row (e.g. a HtmlRow).
+ * A row in a RowsFragment. This is the base class for all rows.
+ * You will typically use a {@link ListRow}, but you may override this class
+ * for non-list Rows.
*/
public class Row {
@@ -30,20 +30,38 @@
private HeaderItem mHeaderItem;
private long mId = NO_ID;
+ /**
+ * Constructor for a Row.
+ *
+ * @param id The id of the row.
+ * @param headerItem The {@link HeaderItem} for this Row, or null if there
+ * is no header.
+ */
public Row(long id, HeaderItem headerItem) {
setId(id);
setHeaderItem(headerItem);
}
+ /**
+ * Constructor for a Row.
+ *
+ * @param headerItem The {@link HeaderItem} for this Row, or null if there
+ * is no header.
+ */
public Row(HeaderItem headerItem) {
setHeaderItem(headerItem);
}
+ /**
+ * Constructor for a Row.
+ */
public Row() {
}
/**
- * Get optional {@link HeaderItem} that represents metadata for the row.
+ * Get the {@link HeaderItem} that represents metadata for the row.
+ *
+ * @return The HeaderItem for this row, or null if unset.
*/
public final HeaderItem getHeaderItem() {
return mHeaderItem;
@@ -51,13 +69,18 @@
/**
* Set the {@link HeaderItem} that represents metadata for the row.
+ *
+ * @param headerItem The HeaderItem for this Row, or null if there is no
+ * header.
*/
public final void setHeaderItem(HeaderItem headerItem) {
mHeaderItem = headerItem;
}
/**
- * Set id for this row.
+ * Set the id for this row.
+ *
+ * @param id The id of the row.
*/
public final void setId(long id) {
mId = id;
@@ -65,9 +88,15 @@
}
/**
- * Returns a unique identifier for this row. If {@link #setId(long)}
- * is ever called, it will return this id; else returns {@link HeaderItem#getId()}
- * if header item is null; otherwise returns NO_ID.
+ * Returns a unique identifier for this row. This id can come from one of
+ * three places:
+ * <ul>
+ * <li>If {@link #setId(long)} is ever called on this row, it will return
+ * this id.
+ * <li>If {@link #setId(long)} has not been called but the header item is
+ * not null, the result of {@link HeaderItem#getId()} is returned.
+ * <li>Otherwise {@link ObjectAdapter#NO_ID NO_ID} is returned.
+ * </ul>
*/
public final long getId() {
if ( (mFlags & FLAG_ID_USE_MASK) == FLAG_ID_USE_HEADER) {
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java b/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
index 05dff40..4fbcb6f 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
@@ -62,4 +62,7 @@
addView(view);
}
+ public void showHeader(boolean show) {
+ mHeaderDock.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
index b31d5f0..886bf89 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
@@ -13,9 +13,12 @@
*/
package android.support.v17.leanback.widget;
-import android.support.v17.leanback.graphics.ColorOverlayDimmer;
+import android.graphics.Paint;
+import android.support.v17.leanback.R;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.TextView;
/**
* RowHeaderPresenter provides a default implementation for header using TextView.
@@ -24,10 +27,25 @@
*/
public class RowHeaderPresenter extends Presenter {
+ private final int mLayoutResourceId;
+ private final Paint mFontMeasurePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+
+ public RowHeaderPresenter() {
+ this(R.layout.lb_row_header);
+ }
+
+ /**
+ * @hide
+ */
+ public RowHeaderPresenter(int layoutResourceId) {
+ mLayoutResourceId = layoutResourceId;
+ }
+
public static class ViewHolder extends Presenter.ViewHolder {
float mSelectLevel;
int mOriginalTextColor;
- ColorOverlayDimmer mColorDimmer;
+ float mUnselectAlpha;
+
public ViewHolder(View view) {
super(view);
}
@@ -38,9 +56,13 @@
@Override
public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) {
- RowHeaderView headerView = new RowHeaderView(parent.getContext());
+ RowHeaderView headerView = (RowHeaderView) LayoutInflater.from(parent.getContext())
+ .inflate(mLayoutResourceId, parent, false);
+
ViewHolder viewHolder = new ViewHolder(headerView);
viewHolder.mOriginalTextColor = headerView.getCurrentTextColor();
+ viewHolder.mUnselectAlpha = parent.getResources().getFraction(
+ R.fraction.lb_browse_header_unselect_alpha, 1, 1);
return viewHolder;
}
@@ -59,7 +81,6 @@
@Override
public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
- ((RowHeaderView) viewHolder.view).setText(null);
}
public final void setSelectLevel(ViewHolder holder, float selectLevel) {
@@ -68,11 +89,29 @@
}
protected void onSelectLevelChanged(ViewHolder holder) {
- if (holder.mColorDimmer == null) {
- holder.mColorDimmer = ColorOverlayDimmer.createDefault(holder.view.getContext());
- }
- holder.mColorDimmer.setActiveLevel(holder.mSelectLevel);
- final RowHeaderView headerView = (RowHeaderView) holder.view;
- headerView.setTextColor(holder.mColorDimmer.applyToColor(holder.mOriginalTextColor));
+ holder.view.setAlpha(holder.mUnselectAlpha + holder.mSelectLevel *
+ (1f - holder.mUnselectAlpha));
}
-}
\ No newline at end of file
+
+ /**
+ * Returns the space (distance in pixels) below the baseline of the
+ * text view, if one exists; otherwise, returns 0.
+ */
+ public int getSpaceUnderBaseline(ViewHolder holder) {
+ int space = holder.view.getPaddingBottom();
+ if (holder.view instanceof TextView) {
+ space += (int) getFontDescent((TextView) holder.view, mFontMeasurePaint);
+ }
+ return space;
+ }
+
+ protected static float getFontDescent(TextView textView, Paint fontMeasurePaint) {
+ if (fontMeasurePaint.getTextSize() != textView.getTextSize()) {
+ fontMeasurePaint.setTextSize(textView.getTextSize());
+ }
+ if (fontMeasurePaint.getTypeface() != textView.getTypeface()) {
+ fontMeasurePaint.setTypeface(textView.getTypeface());
+ }
+ return fontMeasurePaint.descent();
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
index 787597f..14884d5 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
@@ -18,55 +18,55 @@
import android.view.ViewGroup;
/**
- * A presenter that renders {@link Row}.
+ * An abstract {@link Presenter} that renders a {@link Row}.
*
* <h3>Customize UI widgets</h3>
- * When subclass of RowPresenter adds UI widgets, it should subclass
+ * When a subclass of RowPresenter adds UI widgets, it should subclass
* {@link RowPresenter.ViewHolder} and override {@link #createRowViewHolder(ViewGroup)}
- * and {@link #initializeRowViewHolder(ViewHolder)}. Subclass must use layout id
- * "row_content" for the widget that will be aligned to title of {@link HeadersFragment}.
- * RowPresenter contains an optional and replaceable {@link RowHeaderPresenter} that
- * renders header. User can disable default rendering or replace with a new header presenter
+ * and {@link #initializeRowViewHolder(ViewHolder)}. The subclass must use layout id
+ * "row_content" for the widget that will be aligned to the title of any {@link HeadersFragment}
+ * that may exist in the parent fragment. RowPresenter contains an optional and
+ * replaceable {@link RowHeaderPresenter} that renders the header. You can disable
+ * the default rendering or replace the Presenter with a new header presenter
* by calling {@link #setHeaderPresenter(RowHeaderPresenter)}.
*
* <h3>UI events from fragments</h3>
- * In addition to {@link Presenter} which defines how to render and bind data to row view,
- * RowPresenter receives calls from upper level(typically a fragment) when:
+ * RowPresenter receives calls from its parent (typically a Fragment) when:
* <ul>
* <li>
- * Row is selected via {@link #setRowViewSelected(Presenter.ViewHolder, boolean)}. The event
+ * A Row is selected via {@link #setRowViewSelected(Presenter.ViewHolder, boolean)}. The event
* is triggered immediately when there is a row selection change before the selection
* animation is started.
- * Subclass of RowPresenter may override and add more works in
- * {@link #onRowViewSelected(ViewHolder, boolean)}.
+ * Subclasses of RowPresenter may override {@link #onRowViewSelected(ViewHolder, boolean)}.
* </li>
* <li>
- * Row is expanded to full width via {@link #setRowViewExpanded(Presenter.ViewHolder, boolean)}.
+ * A Row is expanded to full width via {@link #setRowViewExpanded(Presenter.ViewHolder, boolean)}.
* The event is triggered immediately before the expand animation is started.
- * Subclass of RowPresenter may override and add more works in
- * {@link #onRowViewExpanded(ViewHolder, boolean)}.
+ * Subclasses of RowPresenter may override {@link #onRowViewExpanded(ViewHolder, boolean)}.
* </li>
* </ul>
*
- * <h3>User events:</h3>
+ * <h3>User events</h3>
* RowPresenter provides {@link OnItemSelectedListener} and {@link OnItemClickedListener}.
- * If subclass wants to add its own {@link View.OnFocusChangeListener} or
+ * If a subclass wants to add its own {@link View.OnFocusChangeListener} or
* {@link View.OnClickListener}, it must do that in {@link #createRowViewHolder(ViewGroup)}
- * to be properly chained by framework. Adding view listeners after
- * {@link #createRowViewHolder(ViewGroup)} will interfere framework's listeners.
+ * to be properly chained by the library. Adding View listeners after
+ * {@link #createRowViewHolder(ViewGroup)} is undefined and may result in
+ * incorrect behavior by the library's listeners.
*
* <h3>Selection animation</h3>
* <p>
- * When user scrolls through rows, fragment will initiate animation and call
- * {@link #setSelectLevel(Presenter.ViewHolder, float)} with float value 0~1. By default, fragment
- * draws a dim overlay on top of row view for views not selected. Subclass may override
- * this default effect by having {@link #isUsingDefaultSelectEffect()} return false
- * and override {@link #onSelectLevelChanged(ViewHolder)} to apply its own selection effect.
+ * When a user scrolls through rows, a fragment will initiate animation and call
+ * {@link #setSelectLevel(Presenter.ViewHolder, float)} with float value between
+ * 0 and 1. By default, the RowPresenter draws a dim overlay on top of the row
+ * view for views that are not selected. Subclasses may override this default effect
+ * by having {@link #isUsingDefaultSelectEffect()} return false and overriding
+ * {@link #onSelectLevelChanged(ViewHolder)} to apply a different selection effect.
* </p>
* <p>
- * Call {@link #setSelectEffectEnabled(boolean)} to enable/disable select effect,
- * This is not only for enable/disable default dim implementation but also subclass must
- * respect this flag.
+ * Call {@link #setSelectEffectEnabled(boolean)} to enable/disable the select effect,
+ * This will not only enable/disable the default dim effect but also subclasses must
+ * respect this flag as well.
* </p>
*/
public abstract class RowPresenter extends Presenter {
@@ -88,6 +88,9 @@
}
}
+ /**
+ * A view holder for a {@link Row}.
+ */
public static class ViewHolder extends Presenter.ViewHolder {
ContainerViewHolder mContainerViewHolder;
RowHeaderPresenter.ViewHolder mHeaderViewHolder;
@@ -96,21 +99,51 @@
boolean mExpanded;
boolean mInitialzed;
float mSelectLevel = 0f; // initially unselected
+
+ /**
+ * Constructor for ViewHolder.
+ *
+ * @param view The View bound to the Row.
+ */
public ViewHolder(View view) {
super(view);
}
+
+ /**
+ * Returns the Row bound to the View in this ViewHolder.
+ */
public final Row getRow() {
return mRow;
}
+
+ /**
+ * Returns whether the Row is in its expanded state.
+ *
+ * @return true if the Row is expanded, false otherwise.
+ */
public final boolean isExpanded() {
return mExpanded;
}
+
+ /**
+ * Returns whether the Row is selected.
+ *
+ * @return true if the Row is selected, false otherwise.
+ */
public final boolean isSelected() {
return mSelected;
}
+
+ /**
+ * Returns the current selection level of the Row.
+ */
public final float getSelectLevel() {
return mSelectLevel;
}
+
+ /**
+ * Returns the view holder for the Row header for this Row.
+ */
public final RowHeaderPresenter.ViewHolder getHeaderViewHolder() {
return mHeaderViewHolder;
}
@@ -119,6 +152,8 @@
private RowHeaderPresenter mHeaderPresenter = new RowHeaderPresenter();
private OnItemSelectedListener mOnItemSelectedListener;
private OnItemClickedListener mOnItemClickedListener;
+ private OnItemViewSelectedListener mOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
boolean mSelectEffectEnabled = true;
@@ -145,37 +180,44 @@
}
/**
- * Called to create a ViewHolder object for row, subclass of {@link RowPresenter}
- * should override and return a different concrete ViewHolder object.
+ * Called to create a ViewHolder object for a Row. Subclasses will override
+ * this method to return a different concrete ViewHolder object.
+ *
+ * @param parent The parent View for the Row's view holder.
+ * @return A ViewHolder for the Row's View.
*/
protected abstract ViewHolder createRowViewHolder(ViewGroup parent);
/**
- * Called after a {@link RowPresenter.ViewHolder} is created,
- * subclass of {@link RowPresenter} may override this method and start with calling
+ * Called after a {@link RowPresenter.ViewHolder} is created for a Row.
+ * Subclasses may override this method and start by calling
* super.initializeRowViewHolder(ViewHolder).
+ *
+ * @param vh The ViewHolder to initialize for the Row.
*/
protected void initializeRowViewHolder(ViewHolder vh) {
vh.mInitialzed = true;
}
/**
- * Change the presenter used for rendering header. Can be null to disable header rendering.
- * The method must be called before creating any row view.
+ * Set the Presenter used for rendering the header. Can be null to disable
+ * header rendering. The method must be called before creating any Row Views.
*/
public final void setHeaderPresenter(RowHeaderPresenter headerPresenter) {
mHeaderPresenter = headerPresenter;
}
/**
- * Get optional presenter used for rendering header. May return null.
+ * Get the Presenter used for rendering the header, or null if none has been
+ * set.
*/
public final RowHeaderPresenter getHeaderPresenter() {
return mHeaderPresenter;
}
/**
- * Get wrapped {@link RowPresenter.ViewHolder}
+ * Get the {@link RowPresenter.ViewHolder} from the given Presenter
+ * ViewHolder.
*/
public final ViewHolder getRowViewHolder(Presenter.ViewHolder holder) {
if (holder instanceof ContainerViewHolder) {
@@ -186,7 +228,10 @@
}
/**
- * Change expanded state of row view.
+ * Set the expanded state of a Row view.
+ *
+ * @param holder The Row ViewHolder to set expanded state on.
+ * @param expanded True if the Row is expanded, false otherwise.
*/
public final void setRowViewExpanded(Presenter.ViewHolder holder, boolean expanded) {
ViewHolder rowViewHolder = getRowViewHolder(holder);
@@ -195,7 +240,10 @@
}
/**
- * Change select state of row view.
+ * Set the selected state of a Row view.
+ *
+ * @param holder The Row ViewHolder to set expanded state on.
+ * @param selected True if the Row is expanded, false otherwise.
*/
public final void setRowViewSelected(Presenter.ViewHolder holder, boolean selected) {
ViewHolder rowViewHolder = getRowViewHolder(holder);
@@ -204,37 +252,44 @@
}
/**
- * Subclass may override and respond to expanded state change of row in fragment.
- * Default implementation hide/show header view depending on expanded state.
- * Subclass may make visual changes to row view but not allowed to create
- * animation on the row view.
+ * Subclass may override this to respond to expanded state changes of a Row.
+ * The default implementation will hide/show the header view. Subclasses may
+ * make visual changes to the Row View but must not create animation on the
+ * Row view.
*/
protected void onRowViewExpanded(ViewHolder vh, boolean expanded) {
- if (mHeaderPresenter != null && vh.mHeaderViewHolder != null) {
- RowContainerView containerView = ((RowContainerView) vh.mContainerViewHolder.view);
- View headerView = vh.mHeaderViewHolder.view;
- if (expanded) {
- containerView.addHeaderView(headerView);
- } else {
- containerView.removeHeaderView(headerView);
- }
- }
+ updateHeaderViewVisibility(vh);
+ vh.view.setActivated(expanded);
}
/**
- * Subclass may override and respond to event Row is selected.
- * Subclass may make visual changes to row view but not allowed to create
- * animation on the row view.
+ * Subclass may override this to respond to selected state changes of a Row.
+ * Subclass may make visual changes to Row view but must not create
+ * animation on the Row view.
*/
protected void onRowViewSelected(ViewHolder vh, boolean selected) {
- if (selected && mOnItemSelectedListener != null) {
- mOnItemSelectedListener.onItemSelected(null, vh.getRow());
+ if (selected) {
+ if (mOnItemViewSelectedListener != null) {
+ mOnItemViewSelectedListener.onItemSelected(null, null, vh, vh.getRow());
+ }
+ if (mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onItemSelected(null, vh.getRow());
+ }
+ }
+ updateHeaderViewVisibility(vh);
+ }
+
+ private void updateHeaderViewVisibility(ViewHolder vh) {
+ if (mHeaderPresenter != null && vh.mHeaderViewHolder != null) {
+ RowContainerView containerView = ((RowContainerView) vh.mContainerViewHolder.view);
+ containerView.showHeader(vh.isExpanded());
}
}
/**
- * Set current select level from 0(unselected) to 1(selected).
- * Subclass should override {@link #onSelectLevelChanged(ViewHolder)}.
+ * Set the current select level to a value between 0 (unselected) and 1 (selected).
+ * Subclasses may override {@link #onSelectLevelChanged(ViewHolder)} to
+ * respond to changes in the selected level.
*/
public final void setSelectLevel(Presenter.ViewHolder vh, float level) {
ViewHolder rowViewHolder = getRowViewHolder(vh);
@@ -243,19 +298,20 @@
}
/**
- * Get current select level from 0(unselected) to 1(selected).
+ * Get the current select level. The value will be between 0 (unselected)
+ * and 1 (selected).
*/
public final float getSelectLevel(Presenter.ViewHolder vh) {
return getRowViewHolder(vh).mSelectLevel;
}
/**
- * Callback when select level is changed. Default implementation applies select level
- * to {@link RowHeaderPresenter#setSelectLevel(RowHeaderPresenter.ViewHolder, float)}
- * when {@link #getSelectEffectEnabled()} is true.
- * Subclass may override this function and implements its own select effect. When it
- * overrides, it should also override {@link #isUsingDefaultSelectEffect()} to disable
- * the default dimming effect applied by framework.
+ * Callback when select level is changed. The default implementation applies
+ * the select level to {@link RowHeaderPresenter#setSelectLevel(RowHeaderPresenter.ViewHolder, float)}
+ * when {@link #getSelectEffectEnabled()} is true. Subclasses may override
+ * this function and implement a different select effect. In this case, you
+ * should also override {@link #isUsingDefaultSelectEffect()} to disable
+ * the default dimming effect applied by the library.
*/
protected void onSelectLevelChanged(ViewHolder vh) {
if (getSelectEffectEnabled() && vh.mHeaderViewHolder != null) {
@@ -265,25 +321,26 @@
/**
* Enables or disables the row selection effect.
- * This is not only for enable/disable default dim implementation but also subclass must
- * respect this flag.
+ * This will not only affect the default dim effect, but subclasses must
+ * respect this flag as well.
*/
public final void setSelectEffectEnabled(boolean applyDimOnSelect) {
mSelectEffectEnabled = applyDimOnSelect;
}
/**
- * Returns true if row selection effect is enabled.
- * This is not only for enable/disable default dim implementation but also subclass must
- * respect this flag.
+ * Returns true if the row selection effect is enabled.
+ * This value not only determines whether the default dim implementation is
+ * used, but subclasses must also respect this flag.
*/
public final boolean getSelectEffectEnabled() {
return mSelectEffectEnabled;
}
/**
- * Return if using default dimming effect provided by framework (fragment). Subclass
- * may(most likely) return false and override {@link #onSelectLevelChanged(ViewHolder)}.
+ * Return whether this RowPresenter is using the default dimming effect
+ * provided by the library. Subclasses may(most likely) return false and
+ * override {@link #onSelectLevelChanged(ViewHolder)}.
*/
public boolean isUsingDefaultSelectEffect() {
return true;
@@ -298,7 +355,7 @@
}
/**
- * Return true if the Row view can draw outside bounds.
+ * Return true if the Row view can draw outside its bounds.
*/
public boolean canDrawOutOfBounds() {
return false;
@@ -351,36 +408,72 @@
}
/**
- * Set listener for item or row selection. RowPresenter fires row selection
- * event with null item, subclass of RowPresenter e.g. {@link ListRowPresenter} can
- * fire a selection event with selected item.
+ * Set the listener for item or row selection. A RowPresenter fires a row
+ * selection event with a null item. Subclasses (e.g. {@link ListRowPresenter})
+ * can fire a selection event with the selected item.
*/
public final void setOnItemSelectedListener(OnItemSelectedListener listener) {
mOnItemSelectedListener = listener;
}
/**
- * Get listener for item or row selection.
+ * Get the listener for item or row selection.
*/
public final OnItemSelectedListener getOnItemSelectedListener() {
return mOnItemSelectedListener;
}
/**
- * Set listener for item click event. RowPresenter does nothing but subclass of
- * RowPresenter may fire item click event if it does have a concept of item.
- * OnItemClickedListener will override {@link View.OnClickListener} that
- * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
- * So in general, developer should choose one of the listeners but not both.
+ * Set the listener for item click events. A RowPresenter does not use this
+ * listener, but a subclass may fire an item click event if it has the concept
+ * of an item. The {@link OnItemClickedListener} will override any
+ * {@link View.OnClickListener} that an item's Presenter sets during
+ * {@link Presenter#onCreateViewHolder(ViewGroup)}. So in general, you
+ * should choose to use an OnItemClickedListener or a {@link
+ * View.OnClickListener}, but not both.
*/
public final void setOnItemClickedListener(OnItemClickedListener listener) {
mOnItemClickedListener = listener;
}
/**
- * Set listener for item click event.
+ * Get the listener for item click events.
*/
public final OnItemClickedListener getOnItemClickedListener() {
return mOnItemClickedListener;
}
+
+ /**
+ * Set listener for item or row selection. RowPresenter fires row selection
+ * event with null item, subclass of RowPresenter e.g. {@link ListRowPresenter} can
+ * fire a selection event with selected item.
+ */
+ public final void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mOnItemViewSelectedListener = listener;
+ }
+
+ /**
+ * Get listener for item or row selection.
+ */
+ public final OnItemViewSelectedListener getOnItemViewSelectedListener() {
+ return mOnItemViewSelectedListener;
+ }
+
+ /**
+ * Set listener for item click event. RowPresenter does nothing but subclass of
+ * RowPresenter may fire item click event if it does have a concept of item.
+ * OnItemViewClickedListener will override {@link View.OnClickListener} that
+ * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
+ * So in general, developer should choose one of the listeners but not both.
+ */
+ public final void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ }
+
+ /**
+ * Set listener for item click event.
+ */
+ public final OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java b/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
index 029db3e..694f077 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
@@ -14,27 +14,56 @@
package android.support.v17.leanback.widget;
import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.media.SoundPool;
+import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
+import android.speech.RecognitionListener;
+import android.speech.RecognizerIntent;
+import android.speech.SpeechRecognizer;
import android.text.Editable;
+import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.SparseIntArray;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
+import android.widget.ImageView;
+import android.view.inputmethod.InputMethodManager;
import android.widget.RelativeLayout;
import android.support.v17.leanback.R;
import android.widget.TextView;
+import java.util.ArrayList;
+import java.util.List;
+
/**
- * SearchBar is a search widget.
+ * <p>SearchBar is a search widget.</p>
+ *
+ * <p>Note: Your application will need to request android.permission.RECORD_AUDIO</p>
*/
public class SearchBar extends RelativeLayout {
private static final String TAG = SearchBar.class.getSimpleName();
private static final boolean DEBUG = false;
+ private static final float FULL_LEFT_VOLUME = 1.0f;
+ private static final float FULL_RIGHT_VOLUME = 1.0f;
+ private static final int DEFAULT_PRIORITY = 1;
+ private static final int DO_NOT_LOOP = 0;
+ private static final float DEFAULT_RATE = 1.0f;
+
/**
* Listener for search query changes
*/
@@ -48,7 +77,10 @@
public void onSearchQueryChange(String query);
/**
- * Method invoked when the search query is submitted.
+ * <p>Method invoked when the search query is submitted.</p>
+ *
+ * <p>This method can be called without a preceeding onSearchQueryChange,
+ * in particular in the case of a voice input.</p>
*
* @param query The query being submitted.
*/
@@ -64,8 +96,27 @@
private SearchBarListener mSearchBarListener;
private SearchEditText mSearchTextEditor;
+ private SpeechOrbView mSpeechOrbView;
+ private ImageView mBadgeView;
private String mSearchQuery;
+ private String mTitle;
+ private Drawable mBadgeDrawable;
private final Handler mHandler = new Handler();
+ private final InputMethodManager mInputMethodManager;
+ private boolean mAutoStartRecognition = false;
+ private Drawable mBarBackground;
+
+ private int mTextColor;
+ private int mTextSpeechColor;
+ private int mBackgroundAlpha;
+ private int mBackgroundSpeechAlpha;
+ private int mBarHeight;
+ private SpeechRecognizer mSpeechRecognizer;
+ private boolean mListening;
+ private SoundPool mSoundPool;
+ private SparseIntArray mSoundMap = new SparseIntArray();
+ private boolean mRecognizing = false;
+ private final Context mContext;
public SearchBar(Context context) {
this(context, null);
@@ -77,21 +128,55 @@
public SearchBar(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ mContext = context;
+ enforceAudioRecordPermission();
+
+ Resources r = getResources();
+
+ LayoutInflater inflater = LayoutInflater.from(getContext());
+ inflater.inflate(R.layout.lb_search_bar, this, true);
+
+ mBarHeight = getResources().getDimensionPixelSize(R.dimen.lb_search_bar_height);
+ RelativeLayout.LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ mBarHeight);
+ params.addRule(ALIGN_PARENT_TOP, RelativeLayout.TRUE);
+ setLayoutParams(params);
+ setBackgroundColor(Color.TRANSPARENT);
+ setClipChildren(false);
+
mSearchQuery = "";
+ mInputMethodManager =
+ (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ mTextSpeechColor = r.getColor(R.color.lb_search_bar_text_speech_color);
+ mBackgroundSpeechAlpha = r.getInteger(R.integer.lb_search_bar_speech_mode_background_alpha);
+
+ mTextColor = r.getColor(R.color.lb_search_bar_text_color);
+ mBackgroundAlpha = r.getInteger(R.integer.lb_search_bar_text_mode_background_alpha);
+
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
+ RelativeLayout items = (RelativeLayout)findViewById(R.id.lb_search_bar_items);
+ mBarBackground = items.getBackground();
+
mSearchTextEditor = (SearchEditText)findViewById(R.id.lb_search_text_editor);
+ mBadgeView = (ImageView)findViewById(R.id.lb_search_bar_badge);
+ if (null != mBadgeDrawable) {
+ mBadgeView.setImageDrawable(mBadgeDrawable);
+ }
+
mSearchTextEditor.setOnFocusChangeListener(new OnFocusChangeListener() {
@Override
public void onFocusChange(View view, boolean hasFocus) {
- if (DEBUG) Log.v(TAG, "onFocusChange " + hasFocus);
+ if (DEBUG) Log.v(TAG, "EditText.onFocusChange " + hasFocus);
if (hasFocus) {
showNativeKeyboard();
}
+ updateUi();
}
});
mSearchTextEditor.addTextChangedListener(new TextWatcher() {
@@ -102,7 +187,9 @@
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
- setSearchQuery(charSequence.toString());
+ if (mSearchTextEditor.hasFocus()) {
+ setSearchQuery(charSequence.toString());
+ }
}
@Override
@@ -119,28 +206,102 @@
}
}
});
+
mSearchTextEditor.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int action, KeyEvent keyEvent) {
+ if (DEBUG) Log.v(TAG, "onEditorAction: " + action + " event: " + keyEvent);
+ boolean handled = true;
if (EditorInfo.IME_ACTION_SEARCH == action && null != mSearchBarListener) {
- mSearchBarListener.onSearchQuerySubmit(mSearchQuery);
- return true;
+ if (DEBUG) Log.v(TAG, "Action Pressed");
+ hideNativeKeyboard();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.v(TAG, "Delayed action handling (search)");
+ submitQuery();
+ }
+ }, 500);
+
+ } else if (EditorInfo.IME_ACTION_NONE == action && null != mSearchBarListener) {
+ if (DEBUG) Log.v(TAG, "Escaped North");
+ hideNativeKeyboard();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.v(TAG, "Delayed action handling (escape_north)");
+ mSearchBarListener.onKeyboardDismiss(mSearchQuery);
+ }
+ }, 500);
+ } else if (EditorInfo.IME_ACTION_GO == action) {
+ if (DEBUG) Log.v(TAG, "Voice Clicked");
+ hideNativeKeyboard();
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (DEBUG) Log.v(TAG, "Delayed action handling (voice_mode)");
+ mAutoStartRecognition = true;
+ mSpeechOrbView.requestFocus();
+ }
+ }, 500);
+ } else {
+ handled = false;
}
- return false;
+
+ return handled;
}
});
+
+ mSearchTextEditor.setPrivateImeOptions("EscapeNorth=1;VoiceDismiss=1;");
+
+ mSpeechOrbView = (SpeechOrbView)findViewById(R.id.lb_search_bar_speech_orb);
+ mSpeechOrbView.setOnOrbClickedListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ toggleRecognition();
+ }
+ });
+ mSpeechOrbView.setOnFocusChangeListener(new OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (DEBUG) Log.v(TAG, "SpeechOrb.onFocusChange " + hasFocus);
+ if (hasFocus) {
+ hideNativeKeyboard();
+ if (mAutoStartRecognition) {
+ startRecognition();
+ mAutoStartRecognition = false;
+ }
+ } else {
+ stopRecognition();
+ }
+ updateUi();
+ }
+ });
+
+ updateHint();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- mHandler.post(new Runnable() {
+ if (DEBUG) Log.v(TAG, "Loading soundPool");
+ mSoundPool = new SoundPool(2, AudioManager.STREAM_SYSTEM, 0);
+ loadSounds(mContext);
+
+ mHandler.postDelayed(new Runnable() {
@Override
public void run() {
- mSearchTextEditor.requestFocus();
- mSearchTextEditor.requestFocusFromTouch();
+ startRecognition();
}
- });
+ }, 300);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ if (DEBUG) Log.v(TAG, "Releasing SoundPool");
+ mSoundPool.release();
+
+ super.onDetachedFromWindow();
}
/**
@@ -166,14 +327,84 @@
}
/**
- * Set the hint text shown in the search bar.
- * @param hint The hint to use.
+ * Set the title text used in the hint shown in the search bar.
+ * @param title The hint to use.
*/
- public void setHint(String hint) {
- mSearchTextEditor.setHint(hint);
+ public void setTitle(String title) {
+ mTitle = title;
+ updateHint();
}
- protected void showNativeKeyboard() {
+ /**
+ * Returns the current title
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * Set the badge drawable showing inside the search bar.
+ * @param drawable The drawable to be used in the search bar.
+ */
+ public void setBadgeDrawable(Drawable drawable) {
+ mBadgeDrawable = drawable;
+ if (null != mBadgeView) {
+ mBadgeView.setImageDrawable(drawable);
+ if (null != drawable) {
+ mBadgeView.setVisibility(View.VISIBLE);
+ } else {
+ mBadgeView.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ /**
+ * Returns the badge drawable
+ */
+ public Drawable getBadgeDrawable() {
+ return mBadgeDrawable;
+ }
+
+ /**
+ * Update the completion list shown by the IME
+ *
+ * @param completions list of completions shown in the IME, can be null or empty to clear them
+ */
+ public void displayCompletions(List<String> completions) {
+ List<CompletionInfo> infos = new ArrayList<CompletionInfo>();
+ if (null != completions) {
+ for (String completion : completions) {
+ infos.add(new CompletionInfo(infos.size(), infos.size(), completion));
+ }
+ }
+
+ mInputMethodManager.displayCompletions(mSearchTextEditor,
+ infos.toArray(new CompletionInfo[] {}));
+ }
+
+ /**
+ * Set the speech recognizer to be used when doing voice search. The Activity/Fragment is in
+ * charge of creating and destroying the recognizer with its own lifecycle.
+ *
+ * @param recognizer a SpeechRecognizer
+ */
+ public void setSpeechRecognizer(SpeechRecognizer recognizer) {
+ if (null != mSpeechRecognizer) {
+ mSpeechRecognizer.setRecognitionListener(null);
+ if (mListening) {
+ mSpeechRecognizer.stopListening();
+ mListening = false;
+ }
+ }
+ mSpeechRecognizer = recognizer;
+ }
+
+ private void hideNativeKeyboard() {
+ mInputMethodManager.hideSoftInputFromWindow(mSearchTextEditor.getWindowToken(),
+ InputMethodManager.RESULT_UNCHANGED_SHOWN);
+ }
+
+ private void showNativeKeyboard() {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -188,4 +419,228 @@
});
}
+ /**
+ * This will update the hint for the search bar properly depending on state and provided title
+ */
+ private void updateHint() {
+ if (null == mSearchTextEditor) return;
+
+ String title = getResources().getString(R.string.lb_search_bar_hint);
+ if (!TextUtils.isEmpty(mTitle)) {
+ if (isVoiceMode()) {
+ title = getResources().getString(R.string.lb_search_bar_hint_with_title_speech, mTitle);
+ } else {
+ title = getResources().getString(R.string.lb_search_bar_hint_with_title, mTitle);
+ }
+ } else if (isVoiceMode()) {
+ title = getResources().getString(R.string.lb_search_bar_hint_speech);
+ }
+ mSearchTextEditor.setHint(title);
+ }
+
+ private void toggleRecognition() {
+ if (mRecognizing) {
+ stopRecognition();
+ } else {
+ startRecognition();
+ }
+ }
+
+ private void stopRecognition() {
+ if (null == mSpeechRecognizer) return;
+ if (!mRecognizing) return;
+ mRecognizing = false;
+
+ if (DEBUG) Log.v(TAG, "stopRecognition " + mListening);
+ mSpeechOrbView.showNotListening();
+
+ if (mListening) {
+ mSpeechRecognizer.cancel();
+ }
+ }
+
+ private void startRecognition() {
+ if (null == mSpeechRecognizer) return;
+ if (mRecognizing) return;
+ mRecognizing = true;
+
+ if (DEBUG) Log.v(TAG, "startRecognition " + mListening);
+
+ mSearchTextEditor.setText("");
+
+ Intent recognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+
+ recognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+ RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
+ recognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
+
+ mSpeechRecognizer.setRecognitionListener(new RecognitionListener() {
+ @Override
+ public void onReadyForSpeech(Bundle bundle) {
+ if (DEBUG) Log.v(TAG, "onReadyForSpeech");
+ }
+
+ @Override
+ public void onBeginningOfSpeech() {
+ if (DEBUG) Log.v(TAG, "onBeginningOfSpeech");
+ mListening = true;
+ }
+
+ @Override
+ public void onRmsChanged(float rmsdB) {
+ if (DEBUG) Log.v(TAG, "onRmsChanged " + rmsdB);
+ int level = rmsdB < 0 ? 0 : (int)(10 * rmsdB);
+ mSpeechOrbView.setSoundLevel(level);
+ }
+
+ @Override
+ public void onBufferReceived(byte[] bytes) {
+ if (DEBUG) Log.v(TAG, "onBufferReceived " + bytes.length);
+ }
+
+ @Override
+ public void onEndOfSpeech() {
+ if (DEBUG) Log.v(TAG, "onEndOfSpeech");
+ mListening = false;
+ }
+
+ @Override
+ public void onError(int error) {
+ if (DEBUG) Log.v(TAG, "onError " + error);
+ switch (error) {
+ case SpeechRecognizer.ERROR_NO_MATCH:
+ Log.d(TAG, "recognizer error no match");
+ break;
+ case SpeechRecognizer.ERROR_SERVER:
+ Log.d(TAG, "recognizer error server error");
+ break;
+ case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
+ Log.d(TAG, "recognizer error speech timeout");
+ break;
+ case SpeechRecognizer.ERROR_CLIENT:
+ Log.d(TAG, "recognizer error client error");
+ break;
+ default:
+ Log.d(TAG, "recognizer other error");
+ break;
+ }
+
+ mSpeechRecognizer.stopListening();
+ mListening = false;
+ mSpeechRecognizer.setRecognitionListener(null);
+ mSpeechOrbView.showNotListening();
+ playSearchFailure();
+ }
+
+ @Override
+ public void onResults(Bundle bundle) {
+ if (DEBUG) Log.v(TAG, "onResults");
+ final ArrayList<String> matches =
+ bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
+ if (matches != null) {
+ Log.v(TAG, "Got results" + matches);
+
+ mSearchQuery = matches.get(0);
+ mSearchTextEditor.setText(mSearchQuery);
+ submitQuery();
+
+ if (mListening) {
+ mSpeechRecognizer.stopListening();
+ }
+ }
+ mSpeechRecognizer.setRecognitionListener(null);
+ mSpeechOrbView.showNotListening();
+ playSearchSuccess();
+ }
+
+ @Override
+ public void onPartialResults(Bundle bundle) {
+
+ }
+
+ @Override
+ public void onEvent(int i, Bundle bundle) {
+
+ }
+ });
+
+ mSpeechOrbView.showListening();
+ playSearchOpen();
+ mSpeechRecognizer.startListening(recognizerIntent);
+ mListening = true;
+ }
+
+ private void updateUi() {
+ if (DEBUG) Log.v(TAG, String.format("Update UI %s %s",
+ isVoiceMode() ? "Voice" : "Text",
+ hasFocus() ? "Focused" : "Unfocused"));
+ if (isVoiceMode()) {
+ mBarBackground.setAlpha(mBackgroundSpeechAlpha);
+ mSearchTextEditor.setTextColor(mTextSpeechColor);
+ } else {
+ mBarBackground.setAlpha(mBackgroundAlpha);
+ mSearchTextEditor.setTextColor(mTextColor);
+ }
+
+ updateHint();
+ }
+
+ private boolean isVoiceMode() {
+ return mSpeechOrbView.isFocused();
+ }
+
+ private void submitQuery() {
+ if (!TextUtils.isEmpty(mSearchQuery) && null != mSearchBarListener) {
+ mSearchBarListener.onSearchQuerySubmit(mSearchQuery);
+ }
+ }
+
+ private void enforceAudioRecordPermission() {
+ String permission = "android.permission.RECORD_AUDIO";
+ int res = getContext().checkCallingOrSelfPermission(permission);
+ if (PackageManager.PERMISSION_GRANTED != res) {
+ throw new IllegalStateException("android.permission.RECORD_AUDIO required for search");
+ }
+ }
+
+ private void loadSounds(Context context) {
+ int[] sounds = {
+ R.raw.lb_voice_failure,
+ R.raw.lb_voice_open,
+ R.raw.lb_voice_no_input,
+ R.raw.lb_voice_success,
+ };
+ for (int sound : sounds) {
+ mSoundMap.put(sound, mSoundPool.load(context, sound, 1));
+ }
+ }
+
+ private void play(final int resId) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ int sound = mSoundMap.get(resId);
+ mSoundPool.play(sound, FULL_LEFT_VOLUME, FULL_RIGHT_VOLUME, DEFAULT_PRIORITY,
+ DO_NOT_LOOP, DEFAULT_RATE);
+ }
+ });
+ }
+
+ private void playSearchOpen() {
+ play(R.raw.lb_voice_open);
+ }
+
+ private void playSearchFailure() {
+ play(R.raw.lb_voice_failure);
+ }
+
+ private void playSearchNoInput() {
+ play(R.raw.lb_voice_no_input);
+ }
+
+ private void playSearchSuccess() {
+ play(R.raw.lb_voice_success);
+ }
+
+
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java b/v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java
index 41353c9..c421b6a 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java
@@ -50,7 +50,7 @@
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (DEBUG) Log.v(TAG, "Keyboard being dismissed");
mKeyboardDismissListener.onKeyboardDismiss();
- return true;
+ return false;
}
return super.onKeyPreIme(keyCode, event);
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java b/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
index 1fb1c93..8b866e9 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
@@ -16,44 +16,177 @@
package android.support.v17.leanback.widget;
-
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Color;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
import android.support.v17.leanback.R;
import android.util.AttributeSet;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
-import android.widget.LinearLayout;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
-public class SearchOrbView extends LinearLayout implements View.OnClickListener {
- private final static String TAG = SearchOrbView.class.getSimpleName();
- private final static boolean DEBUG = false;
-
+/**
+ * <p>A widget that draws a search affordance, represented by a round background and an icon.</p>
+ *
+ * Background color and icon can be customized
+ */
+public class SearchOrbView extends FrameLayout implements View.OnClickListener {
private OnClickListener mListener;
- private LinearLayout mSearchOrbView;
+ private View mSearchOrbView;
+ private ImageView mIcon;
+ private Drawable mIconDrawable;
+ private Colors mColors;
+ private final float mFocusedZoom;
+ private final int mPulseDurationMs;
+ private final int mScaleDownDurationMs;
+ private ValueAnimator mColorAnimator;
+
+ /**
+ * A set of colors used to display the search orb.
+ */
+ public static class Colors {
+ private static final float sBrightnessAlpha = 0.15f;
+
+ /**
+ * Constructs a color set using the given color for the search orb.
+ * Other colors are provided by the framework.
+ *
+ * @param color The main search orb color.
+ */
+ public Colors(int color) {
+ this(color, color);
+ }
+
+ /**
+ * Constructs a color set using the given colors for the search orb.
+ * Other colors are provided by the framework.
+ *
+ * @param color The main search orb color.
+ * @param brightColor A brighter version of the search orb used for animation.
+ */
+ public Colors(int color, int brightColor) {
+ this(color, brightColor, Color.TRANSPARENT);
+ }
+
+ /**
+ * Constructs a color set using the given colors.
+ *
+ * @param color The main search orb color.
+ * @param brightColor A brighter version of the search orb used for animation.
+ * @param iconColor A color used to tint the search orb icon.
+ */
+ public Colors(int color, int brightColor, int iconColor) {
+ this.color = color;
+ this.brightColor = brightColor == color ? getBrightColor(color) : brightColor;
+ this.iconColor = iconColor;
+ }
+
+ /**
+ * The main color of the search orb.
+ */
+ public int color;
+
+ /**
+ * A brighter version of the search orb used for animation.
+ */
+ public int brightColor;
+
+ /**
+ * A color used to tint the search orb icon.
+ */
+ public int iconColor;
+
+ /**
+ * Computes a default brighter version of the given color.
+ */
+ public static int getBrightColor(int color) {
+ final float brightnessValue = 0xff * sBrightnessAlpha;
+ int red = (int)(Color.red(color) * (1 - sBrightnessAlpha) + brightnessValue);
+ int green = (int)(Color.green(color) * (1 - sBrightnessAlpha) + brightnessValue);
+ int blue = (int)(Color.blue(color) * (1 - sBrightnessAlpha) + brightnessValue);
+ int alpha = (int)(Color.alpha(color) * (1 - sBrightnessAlpha) + brightnessValue);
+ return Color.argb(alpha, red, green, blue);
+ }
+ }
+
+ private final ArgbEvaluator mColorEvaluator = new ArgbEvaluator();
+
+ private final ValueAnimator.AnimatorUpdateListener mUpdateListener =
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ Integer color = (Integer) animator.getAnimatedValue();
+ setOrbViewColor(color.intValue());
+ }
+ };
+
+ private ValueAnimator mShadowFocusAnimator;
+
+ private final ValueAnimator.AnimatorUpdateListener mFocusUpdateListener =
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ ShadowHelper.getInstance().setZ(mSearchOrbView, animation.getAnimatedFraction());
+ }
+ };
public SearchOrbView(Context context) {
this(context, null);
}
public SearchOrbView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
+ this(context, attrs, R.attr.searchOrbViewStyle);
}
- public SearchOrbView(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
+ public SearchOrbView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ final Resources res = context.getResources();
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- mSearchOrbView = (LinearLayout) inflater.inflate(R.layout.lb_search_orb, this, true);
+ View root = inflater.inflate(R.layout.lb_search_orb, this, true);
+ mSearchOrbView = root.findViewById(R.id.search_orb);
+ mIcon = (ImageView)root.findViewById(R.id.icon);
- // By default we are not visible
- setVisibility(INVISIBLE);
+ mFocusedZoom = context.getResources().getFraction(
+ R.fraction.lb_search_orb_focused_zoom, 1, 1);
+ mPulseDurationMs = context.getResources().getInteger(
+ R.integer.lb_search_orb_pulse_duration_ms);
+ mScaleDownDurationMs = context.getResources().getInteger(
+ R.integer.lb_search_orb_scale_down_duration_ms);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbSearchOrbView,
+ defStyleAttr, 0);
+
+ Drawable img = a.getDrawable(R.styleable.lbSearchOrbView_searchOrbIcon);
+ if (img == null) {
+ img = res.getDrawable(R.drawable.lb_ic_in_app_search);
+ }
+ setOrbIcon(img);
+
+ int defColor = res.getColor(R.color.lb_default_search_color);
+ int color = a.getColor(R.styleable.lbSearchOrbView_searchOrbColor, defColor);
+ int brightColor = a.getColor(
+ R.styleable.lbSearchOrbView_searchOrbBrightColor, color);
+ int iconColor = a.getColor(R.styleable.lbSearchOrbView_searchOrbIconColor, Color.TRANSPARENT);
+ setOrbColors(new Colors(color, brightColor, iconColor));
+ a.recycle();
+
setFocusable(true);
- mSearchOrbView.setAlpha(0.5f);
-
+ setClipChildren(false);
setOnClickListener(this);
+
+ ShadowHelper.getInstance().setZ(mSearchOrbView, 0f);
+ // Icon has no background, but must be on top of the search orb view
+ ShadowHelper.getInstance().setZ(mIcon, 1f);
}
@Override
@@ -63,15 +196,44 @@
}
}
+ private void startShadowFocusAnimation(boolean gainFocus, int duration) {
+ if (mShadowFocusAnimator == null) {
+ mShadowFocusAnimator = ValueAnimator.ofFloat(0f, 1f);
+ mShadowFocusAnimator.addUpdateListener(mFocusUpdateListener);
+ }
+ if (gainFocus) {
+ mShadowFocusAnimator.start();
+ } else {
+ mShadowFocusAnimator.reverse();
+ }
+ mShadowFocusAnimator.setDuration(duration);
+ }
+
@Override
protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
- if (DEBUG) Log.v(TAG, "onFocusChanged " + gainFocus + " " + direction);
- if (gainFocus) {
- mSearchOrbView.setAlpha(1.0f);
- } else {
- mSearchOrbView.setAlpha(0.5f);
- }
+ final float zoom = gainFocus ? mFocusedZoom : 1f;
+ final int duration = gainFocus ? mPulseDurationMs : mScaleDownDurationMs;
+ mSearchOrbView.animate().scaleX(zoom).scaleY(zoom).setDuration(duration).start();
+ startShadowFocusAnimation(gainFocus, duration);
+ enableOrbColorAnimation(gainFocus);
+ }
+
+ /**
+ * Set the orb icon
+ * @param icon the drawable to be used as the icon
+ */
+ public void setOrbIcon(Drawable icon) {
+ mIconDrawable = icon;
+ mIcon.setImageDrawable(mIconDrawable);
+ }
+
+ /**
+ * Returns the orb icon
+ * @return the drawable used as the icon
+ */
+ public Drawable getOrbIcon() {
+ return mIconDrawable;
}
/**
@@ -87,4 +249,75 @@
}
}
+ /**
+ * Sets the background color of the search orb.
+ * Other colors will be provided by the framework.
+ *
+ * @param color the RGBA color
+ */
+ public void setOrbColor(int color) {
+ setOrbColors(new Colors(color, color, Color.TRANSPARENT));
+ }
+
+ /**
+ * Sets the search orb colors.
+ * Other colors are provided by the framework.
+ * @deprecated Use {@link #setOrbColors(Colors)} instead.
+ */
+ @Deprecated
+ public void setOrbColor(int color, int brightColor) {
+ setOrbColors(new Colors(color, brightColor, Color.TRANSPARENT));
+ }
+
+ /**
+ * Returns the orb color
+ * @return the RGBA color
+ */
+ public int getOrbColor() {
+ return mColors.color;
+ }
+
+ /**
+ * Set the {@link Colors} used to display the search orb.
+ */
+ public void setOrbColors(Colors colors) {
+ mColors = colors;
+ mIcon.setColorFilter(mColors.iconColor);
+
+ if (mColorAnimator == null) {
+ setOrbViewColor(mColors.color);
+ } else {
+ enableOrbColorAnimation(true);
+ }
+ }
+
+ /**
+ * Returns the {@link Colors} used to display the search orb.
+ */
+ public Colors getOrbColors() {
+ return mColors;
+ }
+
+ private void enableOrbColorAnimation(boolean enable) {
+ if (mColorAnimator != null) {
+ mColorAnimator.end();
+ mColorAnimator = null;
+ }
+ if (enable) {
+ // TODO: set interpolator (material if available)
+ mColorAnimator = ValueAnimator.ofObject(mColorEvaluator,
+ mColors.color, mColors.brightColor, mColors.color);
+ mColorAnimator.setRepeatCount(ValueAnimator.INFINITE);
+ mColorAnimator.setDuration(mPulseDurationMs * 2);
+ mColorAnimator.addUpdateListener(mUpdateListener);
+ mColorAnimator.start();
+ }
+ }
+
+ private void setOrbViewColor(int color) {
+ if (mSearchOrbView.getBackground() instanceof GradientDrawable) {
+ ((GradientDrawable) mSearchOrbView.getBackground()).setColor(color);
+ }
+ }
+
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java b/v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
index 00ed8d4..fe80918 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
@@ -13,9 +13,9 @@
*/
package android.support.v17.leanback.widget;
-import android.content.Context;
import android.os.Build;
import android.view.ViewGroup;
+import android.view.View;
/**
@@ -25,6 +25,7 @@
final static ShadowHelper sInstance = new ShadowHelper();
boolean mSupportsShadow;
+ boolean mUsesZShadow;
ShadowHelperVersionImpl mImpl;
/**
@@ -36,6 +37,8 @@
public Object addShadow(ViewGroup shadowContainer);
+ public void setZ(View view, float focusLevel);
+
public void setShadowFocusLevel(Object impl, float level);
}
@@ -61,6 +64,11 @@
// do nothing
}
+ @Override
+ public void setZ(View view, float focusLevel) {
+ // do nothing
+ }
+
}
/**
@@ -83,13 +91,50 @@
ShadowHelperJbmr2.setShadowFocusLevel(impl, level);
}
+ @Override
+ public void setZ(View view, float focusLevel) {
+ // Not supported
+ }
+
+ }
+
+ /**
+ * Implementation used on api 21 (and above).
+ */
+ private static final class ShadowHelperApi21Impl implements ShadowHelperVersionImpl {
+
+ @Override
+ public void prepareParent(ViewGroup parent) {
+ // do nothing
+ }
+
+ @Override
+ public Object addShadow(ViewGroup shadowContainer) {
+ return ShadowHelperApi21.addShadow(shadowContainer);
+ }
+
+ @Override
+ public void setShadowFocusLevel(Object impl, float level) {
+ ShadowHelperApi21.setShadowFocusLevel(impl, level);
+ }
+
+ @Override
+ public void setZ(View view, float focusLevel) {
+ ShadowHelperApi21.setZ(view, focusLevel);
+ }
+
}
/**
* Returns the ShadowHelper.
*/
private ShadowHelper() {
- if (Build.VERSION.SDK_INT >= 18) {
+ // TODO: we should use version number once "L" is published
+ if ("L".equals(Build.VERSION.RELEASE)) {
+ mSupportsShadow = true;
+ mUsesZShadow = true;
+ mImpl = new ShadowHelperApi21Impl();
+ } else if (Build.VERSION.SDK_INT >= 18) {
mSupportsShadow = true;
mImpl = new ShadowHelperJbmr2Impl();
} else {
@@ -106,6 +151,10 @@
return mSupportsShadow;
}
+ public boolean usesZShadow() {
+ return mUsesZShadow;
+ }
+
public void prepareParent(ViewGroup parent) {
mImpl.prepareParent(parent);
}
@@ -117,4 +166,11 @@
public void setShadowFocusLevel(Object impl, float level) {
mImpl.setShadowFocusLevel(impl, level);
}
+
+ /**
+ * Set the view z coordinate with the given focus level from 0..1.
+ */
+ public void setZ(View view, float focusLevel) {
+ mImpl.setZ(view, focusLevel);
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java b/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
index 62bc191..4ba0cc6 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
@@ -19,6 +19,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.graphics.Rect;
/**
* ShadowOverlayContainer Provides a SDK version independent wrapper container
@@ -49,6 +50,7 @@
private View mColorDimOverlay;
private Object mShadowImpl;
private View mWrappedView;
+ private static final Rect sTempRect = new Rect();
public ShadowOverlayContainer(Context context) {
this(context, null, 0);
@@ -195,6 +197,13 @@
child.layout(0, 0, width, height);
}
}
+ if (mWrappedView != null) {
+ sTempRect.left = (int) mWrappedView.getPivotX();
+ sTempRect.top = (int) mWrappedView.getPivotY();
+ offsetDescendantRectToMyCoords(mWrappedView, sTempRect);
+ setPivotX(sTempRect.left);
+ setPivotY(sTempRect.top);
+ }
}
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java b/v17/leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java
new file mode 100644
index 0000000..46165d5
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SpeechOrbView.java
@@ -0,0 +1,121 @@
+package android.support.v17.leanback.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
+import android.support.v17.leanback.R;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+public class SpeechOrbView extends FrameLayout implements View.OnClickListener {
+ private OnClickListener mListener;
+ private View mSpeechOrbView;
+ private final float mFocusedZoom;
+ private final float mSoundLevelMaxZoom;
+ private final int mNotRecordingColor;
+ private final int mRecordingColor;
+ private ImageView mIcon;
+
+ private int mCurrentLevel = 0;
+ private boolean mListening = false;
+
+ public SpeechOrbView(Context context) {
+ this(context, null);
+ }
+
+ public SpeechOrbView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SpeechOrbView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View root = inflater.inflate(R.layout.lb_speech_orb, this, true);
+ mSpeechOrbView = root.findViewById(R.id.lb_speech_orb);
+ mIcon = (ImageView)root.findViewById(R.id.lb_speech_icon);
+
+ setFocusable(true);
+ setClipChildren(false);
+
+ Resources resources = context.getResources();
+ mFocusedZoom =
+ resources.getFraction(R.fraction.lb_search_bar_speech_orb_focused_zoom, 1, 1);
+ mSoundLevelMaxZoom =
+ resources.getFraction(R.fraction.lb_search_bar_speech_orb_max_level_zoom, 1, 1);
+ mNotRecordingColor = resources.getColor(R.color.lb_speech_orb_not_recording);
+ mRecordingColor = resources.getColor(R.color.lb_speech_orb_recording);
+
+ setOnClickListener(this);
+ showNotListening();
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (null != mListener) {
+ mListener.onClick(view);
+ }
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+ final float zoom = gainFocus ? mFocusedZoom : 1f;
+ mSpeechOrbView.animate().scaleX(zoom).scaleY(zoom).setDuration(200).start();
+ if (gainFocus) {
+ mIcon.setImageResource(R.drawable.lb_ic_search_mic);
+ } else {
+ mIcon.setImageResource(R.drawable.lb_ic_search_mic_out);
+ }
+ }
+
+ /**
+ * Set the on click listener for the orb
+ * @param listener The listener.
+ */
+ public void setOnOrbClickedListener(OnClickListener listener) {
+ mListener = listener;
+ }
+
+ public void showListening() {
+ setOrbColor(mRecordingColor);
+ mSpeechOrbView.setScaleX(1f);
+ mSpeechOrbView.setScaleY(1f);
+ mListening = true;
+ }
+
+ public void showNotListening() {
+ setOrbColor(mNotRecordingColor);
+ mSpeechOrbView.setScaleX(1f);
+ mSpeechOrbView.setScaleY(1f);
+ mListening = false;
+ }
+
+ public void setSoundLevel(int level) {
+ if (!mListening) return;
+
+ // Either ease towards the target level, or decay away from it depending on whether
+ // its higher or lower than the current.
+ if (level > mCurrentLevel) {
+ mCurrentLevel = mCurrentLevel + ((level - mCurrentLevel) / 4);
+ } else {
+ mCurrentLevel = (int) (mCurrentLevel * 0.95f);
+ }
+
+ float zoom = mFocusedZoom + ((mSoundLevelMaxZoom - mFocusedZoom) * mCurrentLevel) / 100;
+ mSpeechOrbView.setScaleX(zoom);
+ mSpeechOrbView.setScaleY(zoom);
+ }
+
+ public void setOrbColor(int color) {
+ if (mSpeechOrbView.getBackground() instanceof GradientDrawable) {
+ ((GradientDrawable) mSpeechOrbView.getBackground()).setColor(color);
+ }
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java b/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
index b4de4a6..5404418 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
@@ -234,12 +234,12 @@
}
protected final Location appendItemToRow(int itemIndex, int rowIndex) {
- mProvider.createItem(itemIndex, rowIndex, true);
Location loc = new Location(rowIndex);
if (mLocations.size() == 0) {
mFirstIndex = itemIndex;
}
mLocations.addLast(loc);
+ mProvider.createItem(itemIndex, rowIndex, true);
return loc;
}
@@ -269,10 +269,10 @@
}
protected final Location prependItemToRow(int itemIndex, int rowIndex) {
- mProvider.createItem(itemIndex, rowIndex, false);
Location loc = new Location(rowIndex);
mFirstIndex = itemIndex;
mLocations.addFirst(loc);
+ mProvider.createItem(itemIndex, rowIndex, false);
return loc;
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/TitleView.java b/v17/leanback/src/android/support/v17/leanback/widget/TitleView.java
new file mode 100644
index 0000000..1c61fa0
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/TitleView.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package android.support.v17.leanback.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.v17.leanback.R;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * Title view for a leanback fragment.
+ * @hide
+ */
+public class TitleView extends FrameLayout {
+
+ private ImageView mBadgeView;
+ private TextView mTextView;
+ private SearchOrbView mSearchOrbView;
+
+ public TitleView(Context context) {
+ this(context, null);
+ }
+
+ public TitleView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.browseTitleViewStyle);
+ }
+
+ public TitleView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View rootView = inflater.inflate(R.layout.lb_title_view, this);
+
+ mBadgeView = (ImageView) rootView.findViewById(R.id.browse_badge);
+ mTextView = (TextView) rootView.findViewById(R.id.browse_title);
+ mSearchOrbView = (SearchOrbView) rootView.findViewById(R.id.browse_orb);
+
+ setClipToPadding(false);
+ setClipChildren(false);
+ }
+
+ /**
+ * Sets the title text.
+ */
+ public void setTitle(String titleText) {
+ mTextView.setText(titleText);
+ }
+
+ /**
+ * Returns the title text.
+ */
+ public CharSequence getTitle() {
+ return mTextView.getText();
+ }
+
+ /**
+ * Sets the badge drawable.
+ * If non-null, the drawable is displayed instead of the title text.
+ */
+ public void setBadgeDrawable(Drawable drawable) {
+ mBadgeView.setImageDrawable(drawable);
+ if (drawable != null) {
+ mBadgeView.setVisibility(View.VISIBLE);
+ mTextView.setVisibility(View.GONE);
+ } else {
+ mBadgeView.setVisibility(View.GONE);
+ mTextView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Returns the badge drawable.
+ */
+ public Drawable getBadgeDrawable() {
+ return mBadgeView.getDrawable();
+ }
+
+ /**
+ * Sets the listener to be called when the search affordance is clicked.
+ */
+ public void setOnSearchClickedListener(View.OnClickListener listener) {
+ mSearchOrbView.setOnOrbClickedListener(listener);
+ }
+
+ /**
+ * Returns the view for the search affordance.
+ */
+ public View getSearchAffordanceView() {
+ return mSearchOrbView;
+ }
+
+ /**
+ * Sets the {@link SearchOrbView.Colors} used to draw the search affordance.
+ */
+ public void setSearchAffordanceColors(SearchOrbView.Colors colors) {
+ mSearchOrbView.setOrbColors(colors);
+ }
+
+ /**
+ * Returns the {@link SearchOrbView.Colors} used to draw the search affordance.
+ */
+ public SearchOrbView.Colors getSearchAffordanceColors() {
+ return mSearchOrbView.getOrbColors();
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
index 586ebf9..b421d19 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
@@ -46,8 +46,10 @@
private int mNumColumns = -1;
private int mZoomFactor;
private boolean mShadowEnabled = true;
- private OnItemSelectedListener mOnItemSelectedListener;
private OnItemClickedListener mOnItemClickedListener;
+ private OnItemSelectedListener mOnItemSelectedListener;
+ private OnItemViewSelectedListener mOnItemViewSelectedListener;
+ private OnItemViewClickedListener mOnItemViewClickedListener;
public VerticalGridPresenter() {
this(FocusHighlight.ZOOM_FACTOR_MEDIUM);
@@ -103,6 +105,15 @@
return ShadowOverlayContainer.supportsShadow();
}
+ /**
+ * Returns true if SDK >= L, where Z shadow is enabled so that Z order is enabled
+ * on each child of vertical grid. If subclass returns false in isUsingDefaultShadow()
+ * and does not use Z-shadow on SDK >= L, it should override isUsingZOrder() return false.
+ */
+ public boolean isUsingZOrder() {
+ return ShadowHelper.getInstance().usesZShadow();
+ }
+
final boolean needsDefaultShadow() {
return isUsingDefaultShadow() && getShadowEnabled();
}
@@ -142,6 +153,13 @@
}
};
+ /**
+ * Called after a {@link VerticalGridPresenter.ViewHolder} is created.
+ * Subclasses may override this method and start by calling
+ * super.initializeGridViewHolder(ViewHolder).
+ *
+ * @param vh The ViewHolder to initialize for the vertical grid.
+ */
protected void initializeGridViewHolder(ViewHolder vh) {
if (mNumColumns == -1) {
throw new IllegalStateException("Number of columns must be set");
@@ -155,6 +173,7 @@
ShadowOverlayContainer.prepareParentForShadow(vh.getGridView());
((ViewGroup) vh.view).setClipChildren(false);
}
+ vh.getGridView().setFocusDrawingOrderEnabled(!isUsingZOrder());
FocusHighlightHelper.setupBrowseItemFocusHighlight(vh.mItemBridgeAdapter, mZoomFactor);
final ViewHolder gridViewHolder = vh;
@@ -169,19 +188,30 @@
@Override
public void onCreate(final ItemBridgeAdapter.ViewHolder itemViewHolder) {
// Only when having an OnItemClickListner, we attach the OnClickListener.
- if (getOnItemClickedListener() != null) {
+ if (getOnItemClickedListener() != null || getOnItemViewClickedListener() != null) {
final View itemView = itemViewHolder.getViewHolder().view;
itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (getOnItemClickedListener() != null) {
// Row is always null
- getOnItemClickedListener().onItemClicked(itemViewHolder.mItem, null);
+ getOnItemClickedListener().onItemClicked(itemViewHolder.mItem,
+ null);
+ }
+ if (getOnItemViewClickedListener() != null) {
+ // Row is always null
+ getOnItemViewClickedListener().onItemClicked(
+ itemViewHolder.mHolder, itemViewHolder.mItem, null, null);
}
}
});
}
}
+
+ @Override
+ public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
+ viewHolder.itemView.setActivated(true);
+ }
});
}
@@ -204,6 +234,7 @@
/**
* Sets the item selected listener.
* Since this is a grid the row parameter is always null.
+ * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)}
*/
public final void setOnItemSelectedListener(OnItemSelectedListener listener) {
mOnItemSelectedListener = listener;
@@ -211,34 +242,81 @@
/**
* Returns the item selected listener.
+ * @deprecated Use {@link #getOnItemViewSelectedListener()}
*/
public final OnItemSelectedListener getOnItemSelectedListener() {
return mOnItemSelectedListener;
}
/**
+ * Sets the item selected listener.
+ * Since this is a grid the row parameter is always null.
+ */
+ public final void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) {
+ mOnItemViewSelectedListener = listener;
+ }
+
+ /**
+ * Returns the item selected listener.
+ */
+ public final OnItemViewSelectedListener getOnItemViewSelectedListener() {
+ return mOnItemViewSelectedListener;
+ }
+
+ /**
* Sets the item clicked listener.
* OnItemClickedListener will override {@link View.OnClickListener} that
* item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
* So in general, developer should choose one of the listeners but not both.
+ * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)}
*/
public final void setOnItemClickedListener(OnItemClickedListener listener) {
mOnItemClickedListener = listener;
}
/**
+ * Sets the item clicked listener.
+ * OnItemViewClickedListener will override {@link View.OnClickListener} that
+ * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}.
+ * So in general, developer should choose one of the listeners but not both.
+ */
+ public final void setOnItemViewClickedListener(OnItemViewClickedListener listener) {
+ mOnItemViewClickedListener = listener;
+ }
+
+ /**
* Returns the item clicked listener.
+ * @deprecated Use {@link #getOnItemViewClickedListener()}
*/
public final OnItemClickedListener getOnItemClickedListener() {
return mOnItemClickedListener;
}
+ /**
+ * Returns the item clicked listener.
+ */
+ public final OnItemViewClickedListener getOnItemViewClickedListener() {
+ return mOnItemViewClickedListener;
+ }
+
private void selectChildView(ViewHolder vh, View view) {
if (getOnItemSelectedListener() != null) {
ItemBridgeAdapter.ViewHolder ibh = (view == null) ? null :
- (ItemBridgeAdapter.ViewHolder) vh.getGridView().getChildViewHolder(view);
-
- getOnItemSelectedListener().onItemSelected(ibh == null ? null : ibh.mItem, null);
+ (ItemBridgeAdapter.ViewHolder) vh.getGridView().getChildViewHolder(view);
+ if (ibh == null) {
+ getOnItemSelectedListener().onItemSelected(null, null);
+ } else {
+ getOnItemSelectedListener().onItemSelected(ibh.mItem, null);
+ }
}
- };
+ if (getOnItemViewSelectedListener() != null) {
+ ItemBridgeAdapter.ViewHolder ibh = (view == null) ? null :
+ (ItemBridgeAdapter.ViewHolder) vh.getGridView().getChildViewHolder(view);
+ if (ibh == null) {
+ getOnItemViewSelectedListener().onItemSelected(null, null, null, null);
+ } else {
+ getOnItemViewSelectedListener().onItemSelected(ibh.mHolder, ibh.mItem, null, null);
+ }
+ }
+ }
}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java
index 0b3f453..e349354 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java
@@ -18,6 +18,7 @@
import android.support.v17.leanback.R;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
+import android.util.TypedValue;
/**
* A view that shows items in a vertically scrolling list. The items come from
@@ -42,13 +43,24 @@
protected void initAttributes(Context context, AttributeSet attrs) {
initBaseGridViewAttributes(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbVerticalGridView);
- setColumnWidth(a.getDimensionPixelSize(R.styleable.lbVerticalGridView_columnWidth, 0));
+ setColumnWidth(a);
setNumColumns(a.getInt(R.styleable.lbVerticalGridView_numberOfColumns, 1));
a.recycle();
}
+ void setColumnWidth(TypedArray array) {
+ TypedValue typedValue = array.peekValue(R.styleable.lbVerticalGridView_columnWidth);
+ int size;
+ if (typedValue != null && typedValue.type == TypedValue.TYPE_DIMENSION) {
+ size = array.getDimensionPixelSize(R.styleable.lbVerticalGridView_columnWidth, 0);
+ } else {
+ size = array.getInt(R.styleable.lbVerticalGridView_columnWidth, 0);
+ }
+ setColumnWidth(size);
+ }
+
/**
- * Set the number of columns.
+ * Set the number of columns. Defaults to one.
*/
public void setNumColumns(int numColumns) {
mLayoutManager.setNumRows(numColumns);
@@ -57,6 +69,9 @@
/**
* Set the column width.
+ *
+ * @param width May be WRAP_CONTENT, or a size in pixels. If zero,
+ * column width will be fixed based on number of columns and view width.
*/
public void setColumnWidth(int width) {
mLayoutManager.setRowHeight(width);
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java b/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
index 7d79fc5..79a2c1a 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
@@ -44,6 +44,14 @@
* Left or top edge of first child, typically should be zero.
*/
private int mMinEdge;
+ /**
+ * Max Scroll value
+ */
+ private int mMaxScroll;
+ /**
+ * Min Scroll value
+ */
+ private int mMinScroll;
private int mWindowAlignment = WINDOW_ALIGN_BOTH_EDGE;
@@ -101,8 +109,22 @@
mMinEdge = minEdge;
}
- public void invalidateScrollMin() {
+ final public int getMinEdge() {
+ return mMinEdge;
+ }
+
+ /** set minScroll, Integer.MIN_VALUE means unknown*/
+ final public void setMinScroll(int minScroll) {
+ mMinScroll = minScroll;
+ }
+
+ final public int getMinScroll() {
+ return mMinScroll;
+ }
+
+ final public void invalidateScrollMin() {
mMinEdge = Integer.MIN_VALUE;
+ mMinScroll = Integer.MIN_VALUE;
}
/** update max edge, Integer.MAX_VALUE means unknown*/
@@ -110,8 +132,22 @@
mMaxEdge = maxEdge;
}
- public void invalidateScrollMax() {
+ final public int getMaxEdge() {
+ return mMaxEdge;
+ }
+
+ /** update max scroll, Integer.MAX_VALUE means unknown*/
+ final public void setMaxScroll(int maxScroll) {
+ mMaxScroll = maxScroll;
+ }
+
+ final public int getMaxScroll() {
+ return mMaxScroll;
+ }
+
+ final public void invalidateScrollMax() {
mMaxEdge = Integer.MAX_VALUE;
+ mMaxScroll = Integer.MAX_VALUE;
}
final public float updateScrollCenter(float scrollTarget) {
@@ -158,11 +194,11 @@
return mSize - mPaddingLow - mPaddingHigh;
}
- final public int getSystemScrollPos() {
- return getSystemScrollPos((int) mScrollCenter);
+ final public int getSystemScrollPos(boolean isFirst, boolean isLast) {
+ return getSystemScrollPos((int) mScrollCenter, isFirst, isLast);
}
- final public int getSystemScrollPos(int scrollCenter) {
+ final public int getSystemScrollPos(int scrollCenter, boolean isFirst, boolean isLast) {
int middlePosition;
if (mWindowAlignmentOffset >= 0) {
middlePosition = mWindowAlignmentOffset - mPaddingLow;
@@ -186,7 +222,7 @@
}
if (!isMinUnknown) {
if ((mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0 &&
- scrollCenter - mMinEdge <= middlePosition) {
+ (isFirst || scrollCenter - mMinEdge <= middlePosition)) {
// scroll center is within half of view port size: align the left edge
// of first child to the left edge of view port
return mMinEdge - mPaddingLow;
@@ -194,7 +230,7 @@
}
if (!isMaxUnknown) {
if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0 &&
- mMaxEdge - scrollCenter <= afterMiddlePosition) {
+ (isLast || mMaxEdge - scrollCenter <= afterMiddlePosition)) {
// scroll center is very close to the right edge of view port : align the
// right edge of last children (plus expanded size) to view port's right
return mMaxEdge -mPaddingLow - (clientSize);
diff --git a/v4/Android.mk b/v4/Android.mk
index d1fad84..b78ab12 100644
--- a/v4/Android.mk
+++ b/v4/Android.mk
@@ -14,7 +14,7 @@
LOCAL_PATH := $(call my-dir)
-# A common helper sub-library that only uses base (API 4) APIs.
+# A helper sub-library that makes direct use of Donut APIs.
include $(CLEAR_VARS)
LOCAL_MODULE := android-support-v4-donut
LOCAL_SDK_VERSION := 4
@@ -73,12 +73,22 @@
# -----------------------------------------------------------------------
+# A helper sub-library that makes direct use of Honeycomb MR1 APIs.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v4-honeycomb-mr1
+LOCAL_SDK_VERSION := 12
+LOCAL_SRC_FILES := $(call all-java-files-under, honeycomb_mr1)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4-honeycomb
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# -----------------------------------------------------------------------
+
# A helper sub-library that makes direct use of Honeycomb MR2 APIs.
include $(CLEAR_VARS)
LOCAL_MODULE := android-support-v4-honeycomb-mr2
LOCAL_SDK_VERSION := 13
LOCAL_SRC_FILES := $(call all-java-files-under, honeycomb_mr2)
-LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4-honeycomb
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4-honeycomb-mr1
include $(BUILD_STATIC_JAVA_LIBRARY)
# -----------------------------------------------------------------------
@@ -143,7 +153,7 @@
# -----------------------------------------------------------------------
-# A helper sub-library that makes direct use of the upcoming API
+# A helper sub-library that makes direct use of the upcoming API.
# TODO: Apply a real name and SDK version when available
include $(CLEAR_VARS)
LOCAL_MODULE := android-support-v4-api20
@@ -154,6 +164,17 @@
# -----------------------------------------------------------------------
+# A helper sub-library that makes direct use of the upcoming API.
+# TODO: Apply a real name and SDK version when available
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v4-api21
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, api21)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4-api20
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# -----------------------------------------------------------------------
+
# Here is the final static library that apps can link against.
include $(CLEAR_VARS)
LOCAL_MODULE := android-support-v4
@@ -162,6 +183,6 @@
LOCAL_SRC_FILES := $(call all-java-files-under, java) \
$(call all-Iaidl-files-under, java)
-LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4-api20
+LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4-api21
LOCAL_STATIC_JAVA_LIBRARIES += android-support-annotations
include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/v4/api20/android/support/v4/app/NotificationCompatApi20.java b/v4/api20/android/support/v4/app/NotificationCompatApi20.java
index da35b28..41e1255 100644
--- a/v4/api20/android/support/v4/app/NotificationCompatApi20.java
+++ b/v4/api20/android/support/v4/app/NotificationCompatApi20.java
@@ -38,7 +38,9 @@
PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
int mProgressMax, int mProgress, boolean mProgressIndeterminate,
boolean useChronometer, int priority, CharSequence subText, boolean localOnly,
- Bundle extras, String groupKey, boolean groupSummary, String sortKey) {
+ String category, ArrayList<String> people, Bundle extras, int color,
+ int visibility, Notification publicVersion, String groupKey, boolean groupSummary,
+ String sortKey) {
b = new Notification.Builder(context)
.setWhen(n.when)
.setSmallIcon(n.icon, n.iconLevel)
@@ -65,10 +67,17 @@
.setPriority(priority)
.setProgress(mProgressMax, mProgress, mProgressIndeterminate)
.setLocalOnly(localOnly)
+ .setCategory(category)
.setExtras(extras)
+ .setColor(color)
+ .setVisibility(visibility)
+ .setPublicVersion(publicVersion)
.setGroup(groupKey)
.setGroupSummary(groupSummary)
.setSortKey(sortKey);
+ for (String person: people) {
+ b.addPerson(person);
+ }
}
@Override
diff --git a/v4/api21/android/support/v4/app/ActivityCompat21.java b/v4/api21/android/support/v4/app/ActivityCompat21.java
new file mode 100644
index 0000000..0b86b49
--- /dev/null
+++ b/v4/api21/android/support/v4/app/ActivityCompat21.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.app.Activity;
+import android.app.SharedElementListener;
+import android.view.View;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.List;
+import java.util.Map;
+
+class ActivityCompat21 {
+
+ public static void finishAfterTransition(Activity activity) {
+ activity.finishAfterTransition();
+ }
+
+ public static void setEnterSharedElementListener(Activity activity,
+ SharedElementListener21 listener) {
+ activity.setEnterSharedElementListener(createListener(listener));
+ }
+
+ public static void setExitSharedElementListener(Activity activity,
+ SharedElementListener21 listener) {
+ activity.setExitSharedElementListener(createListener(listener));
+ }
+
+ public static void postponeEnterTransition(Activity activity) {
+ activity.postponeEnterTransition();
+ }
+
+ public static void startPostponedEnterTransition(Activity activity) {
+ activity.startPostponedEnterTransition();
+ }
+
+ public abstract static class SharedElementListener21 {
+ public abstract void setSharedElementStart(List<String> sharedElementNames,
+ List<View> sharedElements, List<View> sharedElementSnapshots);
+
+ public abstract void setSharedElementEnd(List<String> sharedElementNames,
+ List<View> sharedElements, List<View> sharedElementSnapshots);
+
+ public abstract void handleRejectedSharedElements(List<View> rejectedSharedElements);
+
+ public abstract void remapSharedElements(List<String> names,
+ Map<String, View> sharedElements);
+ }
+
+ private static SharedElementListener createListener(SharedElementListener21 listener) {
+ SharedElementListener newListener = null;
+ if (listener != null) {
+ newListener = new SharedElementListenerImpl(listener);
+ }
+ return newListener;
+ }
+
+ private static class SharedElementListenerImpl extends SharedElementListener {
+ private SharedElementListener21 mListener;
+
+ public SharedElementListenerImpl(SharedElementListener21 listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void setSharedElementStart(List<String> sharedElementNames,
+ List<View> sharedElements, List<View> sharedElementSnapshots) {
+ mListener.setSharedElementStart(sharedElementNames, sharedElements,
+ sharedElementSnapshots);
+ }
+
+ @Override
+ public void setSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements,
+ List<View> sharedElementSnapshots) {
+ mListener.setSharedElementEnd(sharedElementNames, sharedElements,
+ sharedElementSnapshots);
+ }
+
+ @Override
+ public void handleRejectedSharedElements(List<View> rejectedSharedElements) {
+ mListener.handleRejectedSharedElements(rejectedSharedElements);
+ }
+
+ @Override
+ public void remapSharedElements(List<String> names, Map<String, View> sharedElements) {
+ mListener.remapSharedElements(names, sharedElements);
+ }
+ }
+}
diff --git a/v4/api21/android/support/v4/app/ActivityOptionsCompat21.java b/v4/api21/android/support/v4/app/ActivityOptionsCompat21.java
new file mode 100644
index 0000000..d629e65
--- /dev/null
+++ b/v4/api21/android/support/v4/app/ActivityOptionsCompat21.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.app.ActivityOptions;
+import android.app.Activity;
+import android.os.Bundle;
+import android.util.Pair;
+import android.view.View;
+
+class ActivityOptionsCompat21 {
+
+ private final ActivityOptions mActivityOptions;
+
+ public static ActivityOptionsCompat21 makeSceneTransitionAnimation(Activity activity,
+ View sharedElement, String sharedElementName) {
+ return new ActivityOptionsCompat21(
+ ActivityOptions.makeSceneTransitionAnimation(activity, sharedElement,
+ sharedElementName));
+ }
+
+ public static ActivityOptionsCompat21 makeSceneTransitionAnimation(Activity activity,
+ View[] sharedElements, String[] sharedElementNames) {
+ Pair[] pairs = null;
+ if (sharedElements != null) {
+ pairs = new Pair[sharedElements.length];
+ for (int i = 0; i < pairs.length; i++) {
+ pairs[i] = Pair.create(sharedElements[i], sharedElementNames[i]);
+ }
+ }
+ return new ActivityOptionsCompat21(
+ ActivityOptions.makeSceneTransitionAnimation(activity, pairs));
+ }
+
+ private ActivityOptionsCompat21(ActivityOptions activityOptions) {
+ mActivityOptions = activityOptions;
+ }
+
+ public Bundle toBundle() {
+ return mActivityOptions.toBundle();
+ }
+
+ public void update(ActivityOptionsCompat21 otherOptions) {
+ mActivityOptions.update(otherOptions.mActivityOptions);
+ }
+}
diff --git a/v4/api21/android/support/v4/app/FragmentTransitionCompat21.java b/v4/api21/android/support/v4/app/FragmentTransitionCompat21.java
new file mode 100644
index 0000000..5b88840
--- /dev/null
+++ b/v4/api21/android/support/v4/app/FragmentTransitionCompat21.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.app.Activity;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.lang.Override;
+import java.lang.String;
+import java.util.ArrayList;
+import java.util.Collection;
+import android.transition.Transition;
+import android.transition.TransitionSet;
+import android.transition.TransitionManager;
+import android.graphics.Rect;
+import android.util.ArrayMap;
+import android.transition.TransitionInflater;
+
+class FragmentTransitionCompat21 {
+
+ public static String getTransitionName(View view) {
+ return view.getTransitionName();
+ }
+
+ public static Object beginTransition(Activity activity, int transitionId, ViewGroup sceneRoot,
+ ArrayList<View> hiddenFragmentViews, ArrayList<String> sourceNames,
+ ArrayList<String> targetNames) {
+ if (transitionId <= 0 || sceneRoot == null) {
+ return null;
+ }
+ TransitionState state = new TransitionState();
+ // get Transition scene root and create Transitions
+ state.sceneRoot = sceneRoot;
+ captureTransitioningViews(state.transitioningViews, state.sceneRoot);
+
+ state.exitTransition = TransitionInflater.from(activity)
+ .inflateTransition(transitionId);
+ state.sharedElementTransition = TransitionInflater.from(activity)
+ .inflateTransition(transitionId);
+ state.enterTransition = TransitionInflater.from(activity)
+ .inflateTransition(transitionId);
+ // Adding a non-existent target view makes sure that the transitions don't target
+ // any views by default. They'll only target the views we tell add. If we don't
+ // add any, then no views will be targeted.
+ View nonExistentView = new View(activity);
+ state.enterTransition.addTarget(nonExistentView);
+ state.exitTransition.addTarget(nonExistentView);
+ state.sharedElementTransition.addTarget(nonExistentView);
+
+ setSharedElementEpicenter(state.enterTransition, state);
+
+ state.excludingTransition = new TransitionSet()
+ .addTransition(state.exitTransition)
+ .addTransition(state.enterTransition);
+ state.excludingTransition.addListener(new ResetNamesListener(state.namedViews));
+
+ if (sourceNames != null) {
+ // Map shared elements.
+ findNamedViews(state.namedViews, state.sceneRoot);
+ state.namedViews.retainAll(sourceNames);
+ View epicenterView = state.namedViews.get(sourceNames.get(0));
+ if (epicenterView != null) {
+ // The epicenter is only the first shared element.
+ setEpicenter(state.exitTransition, epicenterView);
+ setEpicenter(state.sharedElementTransition, epicenterView);
+ }
+ state.transitioningViews.removeAll(state.namedViews.values());
+ state.excludingTransition.addTransition(state.sharedElementTransition);
+ addTransitioningViews(state.sharedElementTransition, state.namedViews.values());
+ }
+
+ // Adds the (maybe) exiting views, not including the shared element.
+ // If some stay, that's ok.
+ addTransitioningViews(state.exitTransition, state.transitioningViews);
+
+ setNameOverrides(state, sourceNames, targetNames);
+
+ // Don't include any subtree in the views that are hidden when capturing the
+ // view hierarchy transitions. They should be as if not there.
+ excludeHiddenFragments(activity, hiddenFragmentViews, state, true);
+
+ TransitionManager.beginDelayedTransition(state.sceneRoot, state.excludingTransition);
+ return state;
+ }
+
+ public static void updateTransitionEndState(Activity activity,
+ ArrayList<View> shownFragmentViews, ArrayList<View> hiddenFragmentViews,
+ Object stateObj, ArrayList<String> names) {
+ if (!(stateObj instanceof TransitionState)) {
+ return;
+ }
+ TransitionState state = (TransitionState) stateObj;
+ // Find all views that are entering.
+ ArrayList<View> enteringViews = new ArrayList<View>();
+ captureTransitioningViews(enteringViews, state.sceneRoot);
+ enteringViews.removeAll(state.transitioningViews);
+
+ state.namedViews.clear();
+
+ if (names != null) {
+ // find all shared elements.
+ findNamedViews(state.namedViews, state.sceneRoot);
+ state.namedViews.retainAll(names);
+ if (!state.namedViews.isEmpty()) {
+ enteringViews.removeAll(state.namedViews.values());
+ addTransitioningViews(state.sharedElementTransition, state.namedViews.values());
+ // now we know the epicenter of the entering transition.
+ state.enteringEpicenterView = state.namedViews.get(names.get(0));
+
+ // Change the names of the shared elements temporarily so that the shared element
+ // names can match.
+ int count = state.nameOverrides.size();
+ for (int i = 0; i < count; i++) {
+ String toName = state.nameOverrides.valueAt(i);
+ View view = state.namedViews.get(toName);
+ if (view != null) {
+ view.setTransitionName(state.nameOverrides.keyAt(i));
+ }
+ }
+ }
+ }
+
+ // Add all entering views to the enter transition.
+ addTransitioningViews(state.enterTransition, enteringViews);
+
+ // Don't allow capturing state for the newly-hidden fragments.
+ excludeHiddenFragments(activity, hiddenFragmentViews, state, false);
+
+ // Allow capturing state for the newly-shown fragments
+ includeVisibleFragments(shownFragmentViews, state.excludingTransition);
+ }
+
+ private static void addTransitioningViews(Transition transition, Collection<View> views) {
+ for (View view : views) {
+ transition.addTarget(view);
+ }
+ }
+
+ private static void excludeHiddenFragments(Activity activity,
+ ArrayList<View> hiddenFragmentViews, TransitionState state, boolean forceExclude) {
+ for (int i = hiddenFragmentViews.size() - 1; i >= 0; i--) {
+ View view = hiddenFragmentViews.get(i);
+ if (forceExclude || !state.hiddenViews.contains(view)) {
+ state.excludingTransition.excludeTarget(view, true);
+ state.hiddenViews.add(view);
+ }
+ }
+ if (forceExclude && state.hiddenViews.isEmpty()) {
+ state.excludingTransition.excludeTarget(new View(activity), true);
+ }
+ }
+
+ private static void includeVisibleFragments(ArrayList<View> shownFragmentViews,
+ Transition transition) {
+ for (int i = shownFragmentViews.size() - 1; i >= 0; i--) {
+ View view = shownFragmentViews.get(i);
+ transition.excludeTarget(view, false);
+ }
+ }
+
+ private static void setEpicenter(Transition transition, View view) {
+ final Rect epicenter = getBoundsOnScreen(view);
+
+ transition.setEpicenterCallback(new Transition.EpicenterCallback() {
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ return epicenter;
+ }
+ });
+ }
+
+ private static void setSharedElementEpicenter(Transition transition,
+ final TransitionState state) {
+ transition.setEpicenterCallback(new Transition.EpicenterCallback() {
+ private Rect mEpicenter;
+
+ @Override
+ public Rect onGetEpicenter(Transition transition) {
+ if (mEpicenter == null && state.enteringEpicenterView != null) {
+ mEpicenter = getBoundsOnScreen(state.enteringEpicenterView);
+ }
+ return mEpicenter;
+ }
+ });
+ }
+
+ private static Rect getBoundsOnScreen(View view) {
+ Rect epicenter = new Rect();
+ int[] loc = new int[2];
+ view.getLocationOnScreen(loc);
+ // not as good as View.getBoundsOnScreen, but that's not public
+ epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight());
+ return epicenter;
+ }
+
+ private static void setNameOverride(ArrayMap<String, String> overrides, String source,
+ String target) {
+ for (int index = 0; index < overrides.size(); index++) {
+ if (source.equals(overrides.valueAt(index))) {
+ overrides.setValueAt(index, target);
+ return;
+ }
+ }
+ overrides.put(source, target);
+ }
+
+ public static void setNameOverrides(Object stateObj, ArrayList<String> sourceNames,
+ ArrayList<String> targetNames) {
+ if (sourceNames != null && stateObj instanceof TransitionState) {
+ TransitionState state = (TransitionState) stateObj;
+ for (int i = 0; i < sourceNames.size(); i++) {
+ String source = sourceNames.get(i);
+ String target = targetNames.get(i);
+ setNameOverride(state.nameOverrides, source, target);
+ }
+ }
+ }
+
+ private static void captureTransitioningViews(ArrayList<View> transitioningViews, View view) {
+ if (view.getVisibility() == View.VISIBLE) {
+ if (view instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) view;
+ if (viewGroup.isTransitionGroup()) {
+ transitioningViews.add(viewGroup);
+ } else {
+ int count = viewGroup.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = viewGroup.getChildAt(i);
+ captureTransitioningViews(transitioningViews, child);
+ }
+ }
+ } else {
+ transitioningViews.add(view);
+ }
+ }
+ }
+
+ private static void findNamedViews(ArrayMap<String, View> namedViews, View view) {
+ if (view.getVisibility() == View.VISIBLE) {
+ String transitionName = view.getTransitionName();
+ if (transitionName != null) {
+ namedViews.put(transitionName, view);
+ }
+ if (view instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) view;
+ int count = viewGroup.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View child = viewGroup.getChildAt(i);
+ findNamedViews(namedViews, child);
+ }
+ }
+ }
+ }
+
+ private static class TransitionState {
+ public ArrayList<View> hiddenViews = new ArrayList<View>();
+ public ArrayList<View> transitioningViews = new ArrayList<View>();
+ public ArrayMap<String, View> namedViews = new ArrayMap<String, View>();
+ public Transition exitTransition;
+ public Transition sharedElementTransition;
+ public Transition enterTransition;
+ public TransitionSet excludingTransition;
+ public ViewGroup sceneRoot;
+ public View enteringEpicenterView;
+ public ArrayMap<String, String> nameOverrides = new ArrayMap<String, String>();
+ }
+
+ private static class ResetNamesListener implements Transition.TransitionListener {
+ private ArrayMap<String, View> mRenamedViews;
+
+ public ResetNamesListener(ArrayMap<String, View> renamedViews) {
+ mRenamedViews = renamedViews;
+ }
+
+ @Override
+ public void onTransitionStart(Transition transition) {
+ transition.removeListener(this);
+ int count = mRenamedViews.size();
+ for (int i = 0; i < count; i++) {
+ View view = mRenamedViews.valueAt(i);
+ String name = mRenamedViews.keyAt(i);
+ view.setTransitionName(name);
+ }
+ }
+
+ @Override
+ public void onTransitionCancel(Transition transition) {
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(Transition transition) {
+ }
+
+ @Override
+ public void onTransitionResume(Transition transition) {
+ }
+ }
+}
diff --git a/v4/api21/android/support/v4/content/ContextCompatApi21.java b/v4/api21/android/support/v4/content/ContextCompatApi21.java
new file mode 100644
index 0000000..be2de1a
--- /dev/null
+++ b/v4/api21/android/support/v4/content/ContextCompatApi21.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.content;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+class ContextCompatApi21 {
+ public static Drawable getDrawable(Context context, int id) {
+ return context.getDrawable(id);
+ }
+}
diff --git a/v4/api21/android/support/v4/content/res/ResourcesCompatApi21.java b/v4/api21/android/support/v4/content/res/ResourcesCompatApi21.java
new file mode 100644
index 0000000..645b633
--- /dev/null
+++ b/v4/api21/android/support/v4/content/res/ResourcesCompatApi21.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.content.res;
+
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.graphics.drawable.Drawable;
+
+class ResourcesCompatApi21 {
+ public static Drawable getDrawable(Resources res, int id, Theme theme) {
+ return res.getDrawable(id, theme);
+ }
+}
diff --git a/v4/api21/android/support/v4/media/MediaMetadataCompatApi21.java b/v4/api21/android/support/v4/media/MediaMetadataCompatApi21.java
new file mode 100644
index 0000000..4332d3e
--- /dev/null
+++ b/v4/api21/android/support/v4/media/MediaMetadataCompatApi21.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media;
+
+import android.graphics.Bitmap;
+import android.media.MediaMetadata;
+import android.media.Rating;
+
+import java.util.Set;
+
+class MediaMetadataCompatApi21 {
+ public static Set<String> keySet(Object metadataObj) {
+ return ((MediaMetadata)metadataObj).keySet();
+ }
+
+ public static Bitmap getBitmap(Object metadataObj, String key) {
+ return ((MediaMetadata)metadataObj).getBitmap(key);
+ }
+
+ public static long getLong(Object metadataObj, String key) {
+ return ((MediaMetadata)metadataObj).getLong(key);
+ }
+
+ public static Object getRating(Object metadataObj, String key) {
+ return ((MediaMetadata)metadataObj).getRating(key);
+ }
+
+ public static String getString(Object metadataObj, String key) {
+ return ((MediaMetadata)metadataObj).getString(key);
+ }
+
+ public static class Builder {
+ public static Object newInstance() {
+ return new MediaMetadata.Builder();
+ }
+
+ public static void putBitmap(Object builderObj, String key, Bitmap value) {
+ ((MediaMetadata.Builder)builderObj).putBitmap(key, value);
+ }
+
+ public static void putLong(Object builderObj, String key, long value) {
+ ((MediaMetadata.Builder)builderObj).putLong(key, value);
+ }
+
+ public static void putRating(Object builderObj, String key, Object ratingObj) {
+ ((MediaMetadata.Builder)builderObj).putRating(key, (Rating)ratingObj);
+ }
+
+ public static void putString(Object builderObj, String key, String value) {
+ ((MediaMetadata.Builder)builderObj).putString(key, value);
+ }
+
+ public static Object build(Object builderObj) {
+ return ((MediaMetadata.Builder)builderObj).build();
+ }
+ }
+}
diff --git a/v4/api21/android/support/v4/media/RatingCompatApi21.java b/v4/api21/android/support/v4/media/RatingCompatApi21.java
new file mode 100644
index 0000000..6c8b9b2
--- /dev/null
+++ b/v4/api21/android/support/v4/media/RatingCompatApi21.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media;
+
+import android.media.Rating;
+
+class RatingCompatApi21 {
+ public static Object newUnratedRating(int ratingStyle) {
+ return Rating.newUnratedRating(ratingStyle);
+ }
+
+ public static Object newHeartRating(boolean hasHeart) {
+ return Rating.newHeartRating(hasHeart);
+ }
+
+ public static Object newThumbRating(boolean thumbIsUp) {
+ return Rating.newThumbRating(thumbIsUp);
+ }
+
+ public static Object newStarRating(int starRatingStyle, float starRating) {
+ return Rating.newStarRating(starRatingStyle, starRating);
+ }
+
+ public static Object newPercentageRating(float percent) {
+ return Rating.newPercentageRating(percent);
+ }
+
+ public static boolean isRated(Object ratingObj) {
+ return ((Rating)ratingObj).isRated();
+ }
+
+ public static int getRatingStyle(Object ratingObj) {
+ return ((Rating)ratingObj).getRatingStyle();
+ }
+
+ public static boolean hasHeart(Object ratingObj) {
+ return ((Rating)ratingObj).hasHeart();
+ }
+
+ public static boolean isThumbUp(Object ratingObj) {
+ return ((Rating)ratingObj).isThumbUp();
+ }
+
+ public static float getStarRating(Object ratingObj) {
+ return ((Rating)ratingObj).getStarRating();
+ }
+
+ public static float getPercentRating(Object ratingObj) {
+ return ((Rating)ratingObj).getPercentRating();
+ }
+}
diff --git a/v4/api21/android/support/v4/media/VolumeProviderCompatApi21.java b/v4/api21/android/support/v4/media/VolumeProviderCompatApi21.java
new file mode 100644
index 0000000..7f2f5b9
--- /dev/null
+++ b/v4/api21/android/support/v4/media/VolumeProviderCompatApi21.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media;
+
+import android.media.VolumeProvider;
+
+class VolumeProviderCompatApi21 {
+ public static Object createVolumeProvider(int volumeControl, int maxVolume,
+ final Delegate delegate) {
+ return new VolumeProvider(volumeControl, maxVolume) {
+ @Override
+ public int onGetCurrentVolume() {
+ return delegate.onGetCurrentVolume();
+ }
+
+ @Override
+ public void onSetVolumeTo(int volume) {
+ delegate.onSetVolumeTo(volume);
+ }
+
+ @Override
+ public void onAdjustVolumeBy(int delta) {
+ delegate.onAdjustVolumeBy(delta);
+ }
+ };
+ }
+
+ public static void notifyVolumeChanged(Object volumeProviderObj) {
+ ((VolumeProvider)volumeProviderObj).notifyVolumeChanged();
+ }
+
+ public interface Delegate {
+ int onGetCurrentVolume();
+ void onSetVolumeTo(int volume);
+ void onAdjustVolumeBy(int delta);
+ }
+}
diff --git a/v4/api21/android/support/v4/media/session/MediaControllerCompatApi21.java b/v4/api21/android/support/v4/media/session/MediaControllerCompatApi21.java
new file mode 100644
index 0000000..6f54512
--- /dev/null
+++ b/v4/api21/android/support/v4/media/session/MediaControllerCompatApi21.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.session;
+
+import android.media.MediaMetadata;
+import android.media.Rating;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.view.KeyEvent;
+
+class MediaControllerCompatApi21 {
+ public static Object fromToken(Object sessionToken) {
+ return MediaController.fromToken((MediaSession.Token)sessionToken);
+ }
+
+ public static Object createCallback(Callback callback) {
+ return new CallbackProxy<Callback>(callback);
+ }
+
+ public static void addCallback(Object controllerObj, Object callbackObj, Handler handler) {
+ ((MediaController)controllerObj).addCallback(
+ (MediaController.Callback)callbackObj, handler);
+ }
+
+ public static void removeCallback(Object controllerObj, Object callbackObj) {
+ ((MediaController)controllerObj).removeCallback((MediaController.Callback)callbackObj);
+ }
+
+ public static Object getTransportControls(Object controllerObj) {
+ return ((MediaController)controllerObj).getTransportControls();
+ }
+
+ public static Object getPlaybackState(Object controllerObj) {
+ return ((MediaController)controllerObj).getPlaybackState();
+ }
+
+ public static Object getMetadata(Object controllerObj) {
+ return ((MediaController)controllerObj).getMetadata();
+ }
+
+ public static int getRatingType(Object controllerObj) {
+ return ((MediaController)controllerObj).getRatingType();
+ }
+
+ public static Object getVolumeInfo(Object controllerObj) {
+ return ((MediaController)controllerObj).getVolumeInfo();
+ }
+
+ public static boolean dispatchMediaButtonEvent(Object controllerObj, KeyEvent event) {
+ return ((MediaController)controllerObj).dispatchMediaButtonEvent(event);
+ }
+
+ public static void sendControlCommand(Object controllerObj,
+ String command, Bundle params, ResultReceiver cb) {
+ ((MediaController)controllerObj).sendControlCommand(command, params, cb);
+ }
+
+ public static class TransportControls {
+ public static void play(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).play();
+ }
+
+ public static void pause(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).pause();
+ }
+
+ public static void stop(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).stop();
+ }
+
+ public static void seekTo(Object controlsObj, long pos) {
+ ((MediaController.TransportControls)controlsObj).seekTo(pos);
+ }
+
+ public static void fastForward(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).fastForward();
+ }
+
+ public static void rewind(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).rewind();
+ }
+
+ public static void skipToNext(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).skipToNext();
+ }
+
+ public static void skipToPrevious(Object controlsObj) {
+ ((MediaController.TransportControls)controlsObj).skipToPrevious();
+ }
+
+ public static void setRating(Object controlsObj, Object ratingObj) {
+ ((MediaController.TransportControls)controlsObj).setRating((Rating)ratingObj);
+ }
+ }
+
+ public static class VolumeInfo {
+ public static int getVolumeType(Object volumeInfoObj) {
+ return ((MediaController.VolumeInfo)volumeInfoObj).getVolumeType();
+ }
+
+ public static int getAudioStream(Object volumeInfoObj) {
+ return ((MediaController.VolumeInfo)volumeInfoObj).getAudioStream();
+ }
+
+ public static int getVolumeControl(Object volumeInfoObj) {
+ return ((MediaController.VolumeInfo)volumeInfoObj).getVolumeControl();
+ }
+
+ public static int getMaxVolume(Object volumeInfoObj) {
+ return ((MediaController.VolumeInfo)volumeInfoObj).getMaxVolume();
+ }
+
+ public static int getCurrentVolume(Object volumeInfoObj) {
+ return ((MediaController.VolumeInfo)volumeInfoObj).getCurrentVolume();
+ }
+ }
+
+ public static interface Callback {
+ public void onSessionEvent(String event, Bundle extras);
+ public void onPlaybackStateChanged(Object stateObj);
+ public void onMetadataChanged(Object metadataObj);
+ }
+
+ static class CallbackProxy<T extends Callback> extends MediaController.Callback {
+ protected final T mCallback;
+
+ public CallbackProxy(T callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ mCallback.onSessionEvent(event, extras);
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ mCallback.onPlaybackStateChanged(state);
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadata metadata) {
+ mCallback.onMetadataChanged(metadata);
+ }
+ }
+}
diff --git a/v4/api21/android/support/v4/media/session/MediaSessionCompatApi21.java b/v4/api21/android/support/v4/media/session/MediaSessionCompatApi21.java
new file mode 100644
index 0000000..fcaa190
--- /dev/null
+++ b/v4/api21/android/support/v4/media/session/MediaSessionCompatApi21.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.session;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaMetadata;
+import android.media.Rating;
+import android.media.VolumeProvider;
+import android.media.session.MediaSession;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+
+class MediaSessionCompatApi21 {
+ public static Object createSession(Context context, String tag) {
+ MediaSessionManager mgr = (MediaSessionManager)context.getSystemService(
+ Context.MEDIA_SESSION_SERVICE);
+ return mgr.createSession(tag);
+ }
+
+ public static Object createCallback(Callback callback) {
+ return new CallbackProxy<Callback>(callback);
+ }
+
+ public static void addCallback(Object sessionObj, Object callbackObj, Handler handler) {
+ ((MediaSession)sessionObj).addCallback((MediaSession.Callback)callbackObj, handler);
+ }
+
+ public static void removeCallback(Object sessionObj, Object callbackObj) {
+ ((MediaSession)sessionObj).removeCallback((MediaSession.Callback)callbackObj);
+ }
+
+ public static void setFlags(Object sessionObj, int flags) {
+ ((MediaSession)sessionObj).setFlags(flags);
+ }
+
+ public static void setPlaybackToLocal(Object sessionObj, int stream) {
+ ((MediaSession)sessionObj).setPlaybackToLocal(stream);
+ }
+
+ public static void setPlaybackToRemote(Object sessionObj, Object volumeProviderObj) {
+ ((MediaSession)sessionObj).setPlaybackToRemote((VolumeProvider)volumeProviderObj);
+ }
+
+ public static void setActive(Object sessionObj, boolean active) {
+ ((MediaSession)sessionObj).setActive(active);
+ }
+
+ public static boolean isActive(Object sessionObj) {
+ return ((MediaSession)sessionObj).isActive();
+ }
+
+ public static void sendSessionEvent(Object sessionObj, String event, Bundle extras) {
+ ((MediaSession)sessionObj).sendSessionEvent(event, extras);
+ }
+
+ public static void release(Object sessionObj) {
+ ((MediaSession)sessionObj).release();
+ }
+
+ public static Parcelable getSessionToken(Object sessionObj) {
+ return ((MediaSession)sessionObj).getSessionToken();
+ }
+
+ public static Object createTransportControlsCallback(TransportControlsCallback callback) {
+ return new TransportControlsCallbackProxy<TransportControlsCallback>(callback);
+ }
+
+ public static void addTransportControlsCallback(Object sessionObj, Object callbackObj,
+ Handler handler) {
+ ((MediaSession)sessionObj).addTransportControlsCallback(
+ (MediaSession.TransportControlsCallback)callbackObj, handler);
+ }
+
+ public static void removeTransportControlsCallback(Object sessionObj, Object callbackObj) {
+ ((MediaSession)sessionObj).removeTransportControlsCallback(
+ (MediaSession.TransportControlsCallback)callbackObj);
+ }
+
+ public static void setPlaybackState(Object sessionObj, Object stateObj) {
+ ((MediaSession)sessionObj).setPlaybackState((PlaybackState)stateObj);
+ }
+
+ public static void setMetadata(Object sessionObj, Object metadataObj) {
+ ((MediaSession)sessionObj).setMetadata((MediaMetadata)metadataObj);
+ }
+
+ public static interface Callback {
+ public void onMediaButtonEvent(Intent mediaButtonIntent);
+ public void onControlCommand(String command, Bundle extras, ResultReceiver cb);
+ }
+
+ public static interface TransportControlsCallback {
+ public void onPlay();
+ public void onPause();
+ public void onSkipToNext();
+ public void onSkipToPrevious();
+ public void onFastForward();
+ public void onRewind();
+ public void onStop();
+ public void onSeekTo(long pos);
+ public void onSetRating(Object ratingObj);
+ }
+
+ static class CallbackProxy<T extends Callback> extends MediaSession.Callback {
+ protected final T mCallback;
+
+ public CallbackProxy(T callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onMediaButtonEvent(Intent mediaButtonIntent) {
+ mCallback.onMediaButtonEvent(mediaButtonIntent);
+ }
+
+ @Override
+ public void onControlCommand(String command, Bundle extras, ResultReceiver cb) {
+ mCallback.onControlCommand(command, extras, cb);
+ }
+ }
+
+ static class TransportControlsCallbackProxy<T extends TransportControlsCallback>
+ extends MediaSession.TransportControlsCallback {
+ protected final T mCallback;
+
+ public TransportControlsCallbackProxy(T callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onPlay() {
+ mCallback.onPlay();
+ }
+
+ @Override
+ public void onPause() {
+ mCallback.onPause();
+ }
+
+ @Override
+ public void onSkipToNext() {
+ mCallback.onSkipToNext();
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ mCallback.onSkipToPrevious();
+ }
+
+ @Override
+ public void onFastForward() {
+ mCallback.onFastForward();
+ }
+
+ @Override
+ public void onRewind() {
+ mCallback.onRewind();
+ }
+
+ @Override
+ public void onStop() {
+ mCallback.onStop();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ mCallback.onSeekTo(pos);
+ }
+
+ @Override
+ public void onSetRating(Rating rating) {
+ mCallback.onSetRating(rating);
+ }
+ }
+}
diff --git a/v4/api21/android/support/v4/media/session/PlaybackStateCompatApi21.java b/v4/api21/android/support/v4/media/session/PlaybackStateCompatApi21.java
new file mode 100644
index 0000000..e17b9b9
--- /dev/null
+++ b/v4/api21/android/support/v4/media/session/PlaybackStateCompatApi21.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.session;
+
+import android.media.session.PlaybackState;
+import android.os.SystemClock;
+
+class PlaybackStateCompatApi21 {
+ public static int getState(Object stateObj) {
+ return ((PlaybackState)stateObj).getState();
+ }
+
+ public static long getPosition(Object stateObj) {
+ return ((PlaybackState)stateObj).getPosition();
+ }
+
+ public static long getBufferPosition(Object stateObj) {
+ return ((PlaybackState)stateObj).getBufferPosition();
+ }
+
+ public static float getPlaybackSpeed(Object stateObj) {
+ return ((PlaybackState)stateObj).getPlaybackSpeed();
+ }
+
+ public static long getActions(Object stateObj) {
+ return ((PlaybackState)stateObj).getActions();
+ }
+
+ public static CharSequence getErrorMessage(Object stateObj) {
+ return ((PlaybackState)stateObj).getErrorMessage();
+ }
+
+ public static long getLastPositionUpdateTime(Object stateObj) {
+ return ((PlaybackState)stateObj).getLastPositionUpdateTime();
+ }
+
+ public static Object newInstance(int state, long position, long bufferPosition,
+ float speed, long actions, CharSequence errorMessage, long updateTime) {
+ PlaybackState.Builder stateObj = new PlaybackState.Builder();
+ stateObj.setState(state, position, speed, updateTime);
+ stateObj.setBufferPosition(bufferPosition);
+ stateObj.setActions(actions);
+ stateObj.setErrorMessage(errorMessage);
+ return stateObj.build();
+ }
+}
diff --git a/v4/api21/android/support/v4/view/ViewCompatApi21.java b/v4/api21/android/support/v4/view/ViewCompatApi21.java
new file mode 100644
index 0000000..6837d21
--- /dev/null
+++ b/v4/api21/android/support/v4/view/ViewCompatApi21.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.view;
+
+import android.view.View;
+
+class ViewCompatApi21 {
+
+ public static void setTransitionName(View view, String transitionName) {
+ view.setTransitionName(transitionName);
+ }
+
+ public static String getTransitionName(View view) {
+ return view.getTransitionName();
+ }
+}
diff --git a/v4/api21/android/support/v4/view/ViewGroupCompatApi21.java b/v4/api21/android/support/v4/view/ViewGroupCompatApi21.java
new file mode 100644
index 0000000..5ebd187
--- /dev/null
+++ b/v4/api21/android/support/v4/view/ViewGroupCompatApi21.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.view;
+
+import android.view.ViewGroup;
+
+class ViewGroupCompatApi21 {
+
+ public static void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) {
+ group.setTransitionGroup(isTransitionGroup);
+ }
+
+ public static boolean isTransitionGroup(ViewGroup group) {
+ return group.isTransitionGroup();
+ }
+}
diff --git a/v4/api21/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java b/v4/api21/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java
new file mode 100644
index 0000000..0c5d7b6
--- /dev/null
+++ b/v4/api21/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.view.accessibility;
+
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import java.util.List;
+
+/**
+ * Api21-specific AccessibilityNodeInfo API implementation.
+ */
+class AccessibilityNodeInfoCompatApi21 {
+ static List<Object> getActionList(Object info) {
+ Object result = ((AccessibilityNodeInfo) info).getActionList();
+ return (List<Object>) result;
+ }
+
+ static void addAction(Object info, int id, CharSequence label) {
+ AccessibilityNodeInfo.AccessibilityAction aa =
+ new AccessibilityNodeInfo.AccessibilityAction(id, label);
+ ((AccessibilityNodeInfo) info).addAction(aa);
+ }
+
+ static class AccessibilityAction {
+ static int getId(Object action) {
+ return ((AccessibilityNodeInfo.AccessibilityAction) action).getId();
+ }
+
+ static CharSequence getLabel(Object action) {
+ return ((AccessibilityNodeInfo.AccessibilityAction) action).getLabel();
+ }
+ }
+}
diff --git a/v4/build.gradle b/v4/build.gradle
index 0cac202..fed5c24 100644
--- a/v4/build.gradle
+++ b/v4/build.gradle
@@ -1,6 +1,11 @@
apply plugin: 'android-library'
archivesBaseName = 'support-v4'
+// create a jar task for the code internal implementation
+tasks.create(name: "internalJar", type: Jar) {
+ baseName "internal_impl"
+}
+
// --------------------------
// TO ADD NEW PLATFORM SPECIFIC CODE, UPDATE THIS:
// create and configure the sourcesets/dependencies for platform-specific code.
@@ -14,7 +19,8 @@
def froyoSS = createApiSourceset('froyo', 'froyo', '8', eclairMr1SS)
def gingerbreadSS = createApiSourceset('gingerbread', 'gingerbread', '9', froyoSS)
def honeycombSS = createApiSourceset('honeycomb', 'honeycomb', '11', gingerbreadSS)
-def honeycombMr2SS = createApiSourceset('honeycombmr2', 'honeycomb_mr2', '13', honeycombSS)
+def honeycombMr1SS = createApiSourceset('honeycombmr1', 'honeycomb_mr1', '12', honeycombSS)
+def honeycombMr2SS = createApiSourceset('honeycombmr2', 'honeycomb_mr2', '13', honeycombMr1SS)
def icsSS = createApiSourceset('ics', 'ics', '14', honeycombMr2SS)
def icsMr1SS = createApiSourceset('icsmr1', 'ics-mr1', '15', icsSS)
def jbSS = createApiSourceset('jellybean', 'jellybean', '16', icsMr1SS)
@@ -22,6 +28,7 @@
def jbMr2SS = createApiSourceset('jellybeanmr2', 'jellybean-mr2', '18', jbMr1SS)
def kitkatSS = createApiSourceset('kitkat', 'kitkat', '19', jbMr2SS)
def api20SS = createApiSourceset('api20', 'api20', 'current', kitkatSS)
+def api21SS = createApiSourceset('api21', 'api21', 'current', api20SS)
def createApiSourceset(String name, String folder, String apiLevel, SourceSet previousSource) {
@@ -35,6 +42,9 @@
setupDependencies(configName, previousSource)
}
ext.allSS.add(sourceSet)
+
+ internalJar.from sourceSet.output
+
return sourceSet
}
@@ -43,15 +53,6 @@
project.getDependencies().add(configName, previousSourceSet.compileClasspath)
}
-// create a jar task for the code above
-tasks.create(name: "internalJar", type: Jar) {
- baseName "internal_impl"
-}
-
-ext.allSS.each { ss ->
- internalJar.from ss.output
-}
-
dependencies {
compile project(':support-annotations')
diff --git a/v4/honeycomb/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java b/v4/honeycomb/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java
index 9569656..bd27954 100644
--- a/v4/honeycomb/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java
+++ b/v4/honeycomb/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java
@@ -20,6 +20,7 @@
import android.R;
import android.app.ActionBar;
import android.app.Activity;
+import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build;
diff --git a/v4/honeycomb/android/support/v4/os/AsyncTaskCompatHoneycomb.java b/v4/honeycomb/android/support/v4/os/AsyncTaskCompatHoneycomb.java
new file mode 100644
index 0000000..5b05a0e
--- /dev/null
+++ b/v4/honeycomb/android/support/v4/os/AsyncTaskCompatHoneycomb.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.os;
+
+import android.os.AsyncTask;
+
+/**
+ * Implementation of AsyncTask compatibility that can call Honeycomb APIs.
+ */
+class AsyncTaskCompatHoneycomb {
+
+ static <Params, Progress, Result> void executeParallel(
+ AsyncTask<Params, Progress, Result> task,
+ Params... params) {
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
+ }
+
+}
diff --git a/v4/honeycomb_mr1/android/support/v4/graphics/BitmapCompatHoneycombMr1.java b/v4/honeycomb_mr1/android/support/v4/graphics/BitmapCompatHoneycombMr1.java
new file mode 100644
index 0000000..94358d5
--- /dev/null
+++ b/v4/honeycomb_mr1/android/support/v4/graphics/BitmapCompatHoneycombMr1.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.graphics;
+
+import android.graphics.Bitmap;
+
+/**
+ * Implementation of BitmapCompat that can use Honeycomb MR1 APIs.
+ */
+class BitmapCompatHoneycombMr1 {
+
+ static int getAllocationByteCount(Bitmap bitmap) {
+ return bitmap.getByteCount();
+ }
+
+}
diff --git a/v4/ics-mr1/android/support/v4/speech/tts/TextToSpeechICSMR1.java b/v4/ics-mr1/android/support/v4/speech/tts/TextToSpeechICSMR1.java
new file mode 100644
index 0000000..bbad4c1
--- /dev/null
+++ b/v4/ics-mr1/android/support/v4/speech/tts/TextToSpeechICSMR1.java
@@ -0,0 +1,74 @@
+package android.support.v4.speech.tts;
+
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.UtteranceProgressListener;
+import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
+
+import java.util.Locale;
+import java.util.Set;
+
+/** Helper class for TTS functionality introduced in ICS MR1 */
+class TextToSpeechICSMR1 {
+ /**
+ * Call {@link TextToSpeech#getFeatures} if available.
+ *
+ * @return {@link TextToSpeech#getFeatures} or null on older devices.
+ */
+ static Set<String> getFeatures(TextToSpeech tts, Locale locale) {
+ if (android.os.Build.VERSION.SDK_INT >=
+ android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+ return tts.getFeatures(locale);
+ }
+ return null;
+ }
+
+ public static final String KEY_FEATURE_EMBEDDED_SYNTHESIS = "embeddedTts";
+ public static final String KEY_FEATURE_NETWORK_SYNTHESIS = "networkTts";
+
+ static interface UtteranceProgressListenerICSMR1 {
+ void onDone(String utteranceId);
+ void onError(String utteranceId);
+ void onStart(String utteranceId);
+ }
+
+ /**
+ * Call {@link TextToSpeech#setOnUtteranceProgressListener} if ICS-MR1 or newer.
+ *
+ * On pre ICS-MR1 devices,{@link TextToSpeech#setOnUtteranceCompletedListener} is
+ * used to emulate its behavior - at the end of synthesis we call
+ * {@link UtteranceProgressListenerICSMR1#onStart(String)} and
+ * {@link UtteranceProgressListenerICSMR1#onDone(String)} one after the other.
+ * Errors can't be detected.
+ */
+ static void setUtteranceProgressListener(TextToSpeech tts,
+ final UtteranceProgressListenerICSMR1 listener) {
+ if (android.os.Build.VERSION.SDK_INT >=
+ android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
+ tts.setOnUtteranceProgressListener(new UtteranceProgressListener() {
+ @Override
+ public void onStart(String utteranceId) {
+ listener.onStart(utteranceId);
+ }
+
+ @Override
+ public void onError(String utteranceId) {
+ listener.onError(utteranceId);
+ }
+
+ @Override
+ public void onDone(String utteranceId) {
+ listener.onDone(utteranceId);
+ }
+ });
+ } else {
+ tts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener() {
+ @Override
+ public void onUtteranceCompleted(String utteranceId) {
+ // Emulate onStart. Clients are expecting it will happen.
+ listener.onStart(utteranceId);
+ listener.onDone(utteranceId);
+ }
+ });
+ }
+ }
+}
diff --git a/v4/ics/android/support/v4/speech/tts/TextToSpeechICS.java b/v4/ics/android/support/v4/speech/tts/TextToSpeechICS.java
new file mode 100644
index 0000000..4e0c041
--- /dev/null
+++ b/v4/ics/android/support/v4/speech/tts/TextToSpeechICS.java
@@ -0,0 +1,27 @@
+package android.support.v4.speech.tts;
+
+import android.content.Context;
+import android.os.Build;
+import android.speech.tts.TextToSpeech;
+import android.speech.tts.TextToSpeech.OnInitListener;
+import android.util.Log;
+
+/** Helper class for TTS functionality introduced in ICS */
+class TextToSpeechICS {
+ private static final String TAG = "android.support.v4.speech.tts";
+
+ static TextToSpeech construct(Context context, OnInitListener onInitListener,
+ String engineName) {
+ if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+ if (engineName == null) {
+ return new TextToSpeech(context, onInitListener);
+ } else {
+ Log.w(TAG, "Can't specify tts engine on this device");
+ return new TextToSpeech(context, onInitListener);
+ }
+ } else {
+ return new TextToSpeech(context, onInitListener, engineName);
+ }
+ }
+
+}
diff --git a/v4/java/android/support/v4/app/ActivityCompat.java b/v4/java/android/support/v4/app/ActivityCompat.java
index f456a1b..48f2fd9 100644
--- a/v4/java/android/support/v4/app/ActivityCompat.java
+++ b/v4/java/android/support/v4/app/ActivityCompat.java
@@ -22,6 +22,10 @@
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
+import android.view.View;
+
+import java.util.List;
+import java.util.Map;
/**
* Helper for accessing features in {@link android.app.Activity}
@@ -137,4 +141,105 @@
}
}
+ /**
+ * Reverses the Activity Scene entry Transition and triggers the calling Activity
+ * to reverse its exit Transition. When the exit Transition completes,
+ * {@link Activity#finish()} is called. If no entry Transition was used, finish() is called
+ * immediately and the Activity exit Transition is run.
+ *
+ * <p>On Android 4.4 or lower, this method only finishes the Activity with no
+ * special exit transition.</p>
+ */
+ public static void finishAfterTransition(Activity activity) {
+ if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
+ ActivityCompat21.finishAfterTransition(activity);
+ } else {
+ activity.finish();
+ }
+ }
+
+ /**
+ * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,
+ * android.view.View, String)} was used to start an Activity, <var>listener</var>
+ * will be called to handle shared elements on the <i>launched</i> Activity. This requires
+ * {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS}.
+ *
+ * @param listener Used to manipulate shared element transitions on the launched Activity.
+ */
+ public static void setEnterSharedElementListener(Activity activity,
+ SharedElementListener listener) {
+ if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
+ ActivityCompat21.setEnterSharedElementListener(activity, createListener(listener));
+ }
+ }
+
+ /**
+ * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,
+ * android.view.View, String)} was used to start an Activity, <var>listener</var>
+ * will be called to handle shared elements on the <i>launching</i> Activity. Most
+ * calls will only come when returning from the started Activity.
+ * This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS}.
+ *
+ * @param listener Used to manipulate shared element transitions on the launching Activity.
+ */
+ public static void setExitSharedElementListener(Activity activity,
+ SharedElementListener listener) {
+ if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
+ ActivityCompat21.setExitSharedElementListener(activity, createListener(listener));
+ }
+ }
+
+ public static void postponeEnterTransition(Activity activity) {
+ if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
+ ActivityCompat21.postponeEnterTransition(activity);
+ }
+ }
+
+ public static void startPostponedEnterTransition(Activity activity) {
+ if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
+ ActivityCompat21.startPostponedEnterTransition(activity);
+ }
+ }
+
+ private static ActivityCompat21.SharedElementListener21 createListener(
+ SharedElementListener listener) {
+ ActivityCompat21.SharedElementListener21 newListener = null;
+ if (listener != null) {
+ newListener = new SharedElementListener21Impl(listener);
+ }
+ return newListener;
+ }
+
+ private static class SharedElementListener21Impl
+ extends ActivityCompat21.SharedElementListener21 {
+ private SharedElementListener mListener;
+
+ public SharedElementListener21Impl(SharedElementListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void setSharedElementStart(List<String> sharedElementNames,
+ List<View> sharedElements, List<View> sharedElementSnapshots) {
+ mListener.setSharedElementStart(sharedElementNames, sharedElements,
+ sharedElementSnapshots);
+ }
+
+ @Override
+ public void setSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements,
+ List<View> sharedElementSnapshots) {
+ mListener.setSharedElementEnd(sharedElementNames, sharedElements,
+ sharedElementSnapshots);
+ }
+
+ @Override
+ public void handleRejectedSharedElements(List<View> rejectedSharedElements) {
+ mListener.handleRejectedSharedElements(rejectedSharedElements);
+ }
+
+ @Override
+ public void remapSharedElements(List<String> names, Map<String, View> sharedElements) {
+ mListener.remapSharedElements(names, sharedElements);
+ }
+ }
}
diff --git a/v4/java/android/support/v4/app/ActivityOptionsCompat.java b/v4/java/android/support/v4/app/ActivityOptionsCompat.java
index f426a69..783aa4a 100644
--- a/v4/java/android/support/v4/app/ActivityOptionsCompat.java
+++ b/v4/java/android/support/v4/app/ActivityOptionsCompat.java
@@ -16,10 +16,12 @@
package android.support.v4.app;
+import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
+import android.support.v4.util.Pair;
import android.view.View;
/**
@@ -108,6 +110,69 @@
return new ActivityOptionsCompat();
}
+ /**
+ * Create an ActivityOptions to transition between Activities using cross-Activity scene
+ * animations. This method carries the position of one shared element to the started Activity.
+ * The position of <code>sharedElement</code> will be used as the epicenter for the
+ * exit Transition. The position of the shared element in the launched Activity will be the
+ * epicenter of its entering Transition.
+ *
+ * <p>This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS} to be
+ * enabled on the calling Activity to cause an exit transition. The same must be in
+ * the called Activity to get an entering transition.</p>
+ * @param activity The Activity whose window contains the shared elements.
+ * @param sharedElement The View to transition to the started Activity. sharedElement must
+ * have a non-null sharedElementName.
+ * @param sharedElementName The shared element name as used in the target Activity. This may
+ * be null if it has the same name as sharedElement.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ */
+ public static ActivityOptionsCompat makeSceneTransitionAnimation(Activity activity,
+ View sharedElement, String sharedElementName) {
+ if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
+ return new ActivityOptionsCompat.ActivityOptionsImpl21(
+ ActivityOptionsCompat21.makeSceneTransitionAnimation(activity,
+ sharedElement, sharedElementName));
+ }
+ return new ActivityOptionsCompat();
+ }
+
+ /**
+ * Create an ActivityOptions to transition between Activities using cross-Activity scene
+ * animations. This method carries the position of multiple shared elements to the started
+ * Activity. The position of the first element in sharedElements
+ * will be used as the epicenter for the exit Transition. The position of the associated
+ * shared element in the launched Activity will be the epicenter of its entering Transition.
+ *
+ * <p>This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS} to be
+ * enabled on the calling Activity to cause an exit transition. The same must be in
+ * the called Activity to get an entering transition.</p>
+ * @param activity The Activity whose window contains the shared elements.
+ * @param sharedElements The names of the shared elements to transfer to the called
+ * Activity and their associated Views. The Views must each have
+ * a unique shared element name.
+ * @return Returns a new ActivityOptions object that you can use to
+ * supply these options as the options Bundle when starting an activity.
+ */
+ public static ActivityOptionsCompat makeSceneTransitionAnimation(Activity activity,
+ Pair<View, String>... sharedElements) {
+ if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
+ View[] views = null;
+ String[] names = null;
+ if (sharedElements != null) {
+ views = new View[sharedElements.length];
+ names = new String[sharedElements.length];
+ for (int i = 0; i < sharedElements.length; i++) {
+ views[i] = sharedElements[i].first;
+ names[i] = sharedElements[i].second;
+ }
+ }
+ return new ActivityOptionsCompat.ActivityOptionsImpl21(
+ ActivityOptionsCompat21.makeSceneTransitionAnimation(activity, views, names));
+ }
+ return new ActivityOptionsCompat();
+ }
private static class ActivityOptionsImplJB extends ActivityOptionsCompat {
private final ActivityOptionsCompatJB mImpl;
@@ -130,6 +195,27 @@
}
}
+ private static class ActivityOptionsImpl21 extends ActivityOptionsCompat {
+ private final ActivityOptionsCompat21 mImpl;
+
+ ActivityOptionsImpl21(ActivityOptionsCompat21 impl) {
+ mImpl = impl;
+ }
+
+ @Override
+ public Bundle toBundle() {
+ return mImpl.toBundle();
+ }
+
+ @Override
+ public void update(ActivityOptionsCompat otherOptions) {
+ if (otherOptions instanceof ActivityOptionsCompat.ActivityOptionsImpl21) {
+ ActivityOptionsCompat.ActivityOptionsImpl21
+ otherImpl = (ActivityOptionsCompat.ActivityOptionsImpl21)otherOptions;
+ mImpl.update(otherImpl.mImpl);
+ }
+ }
+ }
protected ActivityOptionsCompat() {
}
diff --git a/v4/java/android/support/v4/app/BackStackRecord.java b/v4/java/android/support/v4/app/BackStackRecord.java
index bd13dd7..62d99df 100644
--- a/v4/java/android/support/v4/app/BackStackRecord.java
+++ b/v4/java/android/support/v4/app/BackStackRecord.java
@@ -16,11 +16,15 @@
package android.support.v4.app;
+import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.util.LogWriter;
+import android.support.v4.util.Pair;
import android.text.TextUtils;
import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -36,6 +40,10 @@
final CharSequence mBreadCrumbTitleText;
final int mBreadCrumbShortTitleRes;
final CharSequence mBreadCrumbShortTitleText;
+ final int mCustomTransition;
+ final int mSceneRoot;
+ final ArrayList<String> mSharedElementSourceNames;
+ final ArrayList<String> mSharedElementTargetNames;
public BackStackState(FragmentManagerImpl fm, BackStackRecord bse) {
int numRemoved = 0;
@@ -78,6 +86,10 @@
mBreadCrumbTitleText = bse.mBreadCrumbTitleText;
mBreadCrumbShortTitleRes = bse.mBreadCrumbShortTitleRes;
mBreadCrumbShortTitleText = bse.mBreadCrumbShortTitleText;
+ mCustomTransition = bse.mCustomTransition;
+ mSceneRoot = bse.mSceneRoot;
+ mSharedElementSourceNames = bse.mSharedElementSourceNames;
+ mSharedElementTargetNames = bse.mSharedElementTargetNames;
}
public BackStackState(Parcel in) {
@@ -90,6 +102,10 @@
mBreadCrumbTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
mBreadCrumbShortTitleRes = in.readInt();
mBreadCrumbShortTitleText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ mCustomTransition = in.readInt();
+ mSceneRoot = in.readInt();
+ mSharedElementSourceNames = in.createStringArrayList();
+ mSharedElementTargetNames = in.createStringArrayList();
}
public BackStackRecord instantiate(FragmentManagerImpl fm) {
@@ -134,6 +150,10 @@
bse.mBreadCrumbTitleText = mBreadCrumbTitleText;
bse.mBreadCrumbShortTitleRes = mBreadCrumbShortTitleRes;
bse.mBreadCrumbShortTitleText = mBreadCrumbShortTitleText;
+ bse.mCustomTransition = mCustomTransition;
+ bse.mSceneRoot = mSceneRoot;
+ bse.mSharedElementSourceNames = mSharedElementSourceNames;
+ bse.mSharedElementTargetNames = mSharedElementTargetNames;
bse.bumpBackStackNesting(1);
return bse;
}
@@ -152,6 +172,10 @@
TextUtils.writeToParcel(mBreadCrumbTitleText, dest, 0);
dest.writeInt(mBreadCrumbShortTitleRes);
TextUtils.writeToParcel(mBreadCrumbShortTitleText, dest, 0);
+ dest.writeInt(mCustomTransition);
+ dest.writeInt(mSceneRoot);
+ dest.writeStringList(mSharedElementSourceNames);
+ dest.writeStringList(mSharedElementTargetNames);
}
public static final Parcelable.Creator<BackStackState> CREATOR
@@ -216,6 +240,11 @@
int mBreadCrumbShortTitleRes;
CharSequence mBreadCrumbShortTitleText;
+ int mCustomTransition;
+ int mSceneRoot;
+ ArrayList<String> mSharedElementSourceNames;
+ ArrayList<String> mSharedElementTargetNames;
+
@Override
public String toString() {
StringBuilder sb = new StringBuilder(128);
@@ -493,6 +522,54 @@
return this;
}
+ @Override
+ public FragmentTransaction setCustomTransition(int sceneRootId, int transitionId) {
+ mSceneRoot = sceneRootId;
+ mCustomTransition = transitionId;
+ return this;
+ }
+
+ @Override
+ public FragmentTransaction setSharedElement(View sharedElement, String name) {
+ if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
+ String transitionName = FragmentTransitionCompat21.getTransitionName(sharedElement);
+ if (transitionName == null) {
+ throw new IllegalArgumentException("Unique transitionNames are required for all" +
+ " sharedElements");
+ }
+ mSharedElementSourceNames = new ArrayList<String>(1);
+ mSharedElementSourceNames.add(transitionName);
+
+ mSharedElementTargetNames = new ArrayList<String>(1);
+ mSharedElementTargetNames.add(name);
+ }
+ return this;
+ }
+
+ @Override
+ public FragmentTransaction setSharedElements(Pair<View, String>... sharedElements) {
+ if (sharedElements == null || sharedElements.length == 0) {
+ mSharedElementSourceNames = null;
+ mSharedElementTargetNames = null;
+ } else if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
+ ArrayList<String> sourceNames = new ArrayList<String>(sharedElements.length);
+ ArrayList<String> targetNames = new ArrayList<String>(sharedElements.length);
+ for (int i = 0; i < sharedElements.length; i++) {
+ String transitionName =
+ FragmentTransitionCompat21.getTransitionName(sharedElements[i].first);
+ if (transitionName == null) {
+ throw new IllegalArgumentException("Unique transitionNames are required for all"
+ + " sharedElements");
+ }
+ sourceNames.add(transitionName);
+ targetNames.add(sharedElements[i].second);
+ }
+ mSharedElementSourceNames = sourceNames;
+ mSharedElementTargetNames = targetNames;
+ }
+ return this;
+ }
+
public FragmentTransaction setTransitionStyle(int styleRes) {
mTransitionStyle = styleRes;
return this;
@@ -607,12 +684,18 @@
bumpBackStackNesting(1);
+ Object state = beginTransition(mSharedElementSourceNames, mSharedElementTargetNames);
+
+ int transitionStyle = state != null ? 0 : mTransitionStyle;
+ int transition = state != null ? 0 : mTransition;
Op op = mHead;
while (op != null) {
+ int enterAnim = state != null ? 0 : op.enterAnim;
+ int exitAnim = state != null ? 0 : op.exitAnim;
switch (op.cmd) {
case OP_ADD: {
Fragment f = op.fragment;
- f.mNextAnim = op.enterAnim;
+ f.mNextAnim = enterAnim;
mManager.addFragment(f, false);
} break;
case OP_REPLACE: {
@@ -630,46 +713,46 @@
op.removed = new ArrayList<Fragment>();
}
op.removed.add(old);
- old.mNextAnim = op.exitAnim;
+ old.mNextAnim = exitAnim;
if (mAddToBackStack) {
old.mBackStackNesting += 1;
if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of "
+ old + " to " + old.mBackStackNesting);
}
- mManager.removeFragment(old, mTransition, mTransitionStyle);
+ mManager.removeFragment(old, transition, transitionStyle);
}
}
}
}
if (f != null) {
- f.mNextAnim = op.enterAnim;
+ f.mNextAnim = enterAnim;
mManager.addFragment(f, false);
}
} break;
case OP_REMOVE: {
Fragment f = op.fragment;
- f.mNextAnim = op.exitAnim;
- mManager.removeFragment(f, mTransition, mTransitionStyle);
+ f.mNextAnim = exitAnim;
+ mManager.removeFragment(f, transition, transitionStyle);
} break;
case OP_HIDE: {
Fragment f = op.fragment;
- f.mNextAnim = op.exitAnim;
- mManager.hideFragment(f, mTransition, mTransitionStyle);
+ f.mNextAnim = exitAnim;
+ mManager.hideFragment(f, transition, transitionStyle);
} break;
case OP_SHOW: {
Fragment f = op.fragment;
- f.mNextAnim = op.enterAnim;
- mManager.showFragment(f, mTransition, mTransitionStyle);
+ f.mNextAnim = enterAnim;
+ mManager.showFragment(f, transition, transitionStyle);
} break;
case OP_DETACH: {
Fragment f = op.fragment;
- f.mNextAnim = op.exitAnim;
- mManager.detachFragment(f, mTransition, mTransitionStyle);
+ f.mNextAnim = exitAnim;
+ mManager.detachFragment(f, transition, transitionStyle);
} break;
case OP_ATTACH: {
Fragment f = op.fragment;
- f.mNextAnim = op.enterAnim;
- mManager.attachFragment(f, mTransition, mTransitionStyle);
+ f.mNextAnim = enterAnim;
+ mManager.attachFragment(f, transition, transitionStyle);
} break;
default: {
throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
@@ -679,15 +762,66 @@
op = op.next;
}
- mManager.moveToState(mManager.mCurState, mTransition,
- mTransitionStyle, true);
+ mManager.moveToState(mManager.mCurState, transition, transitionStyle, true);
if (mAddToBackStack) {
mManager.addBackStackState(this);
}
+
+ if (state != null) {
+ updateTransitionEndState(state, mSharedElementTargetNames);
+ }
}
- public void popFromBackStack(boolean doStateMove) {
+ private Object beginTransition(ArrayList<String> sourceNames, ArrayList<String> targetNames) {
+ Object state = null;
+ if ((Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L"))
+ && mCustomTransition != 0 && mSceneRoot != 0) {
+ View rootView = mManager.mContainer.findViewById(mSceneRoot);
+ if (!(rootView instanceof ViewGroup)) {
+ throw new IllegalArgumentException("SceneRoot is not a ViewGroup");
+ }
+ ArrayList<View> hiddenFragmentViews = new ArrayList<View>();
+ if (mManager.mAdded != null) {
+ for (int i = mManager.mAdded.size() - 1; i >= 0; i--) {
+ Fragment fragment = mManager.mAdded.get(i);
+ if (fragment.mView != null) {
+ if (fragment.mHidden) {
+ hiddenFragmentViews.add(fragment.mView);
+ }
+ }
+ }
+ }
+ state = FragmentTransitionCompat21.beginTransition(mManager.mActivity,
+ mCustomTransition, (ViewGroup) rootView, hiddenFragmentViews,
+ sourceNames, targetNames);
+ }
+ return state;
+ }
+
+ private void updateTransitionEndState(Object stateObj, ArrayList<String> targetNames) {
+ if ((Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L"))
+ && stateObj != null) {
+ ArrayList<View> hiddenFragmentViews = new ArrayList<View>();
+ ArrayList<View> shownFragmentViews = new ArrayList<View>();
+ if (mManager.mAdded != null) {
+ for (int i = mManager.mAdded.size() - 1; i >= 0; i--) {
+ Fragment fragment = mManager.mAdded.get(i);
+ if (fragment.mView != null) {
+ if (fragment.mHidden) {
+ hiddenFragmentViews.add(fragment.mView);
+ } else {
+ shownFragmentViews.add(fragment.mView);
+ }
+ }
+ }
+ }
+ FragmentTransitionCompat21.updateTransitionEndState(mManager.mActivity,
+ shownFragmentViews, hiddenFragmentViews, stateObj, targetNames);
+ }
+ }
+
+ public Object popFromBackStack(boolean doStateMove, Object state) {
if (FragmentManagerImpl.DEBUG) {
Log.v(TAG, "popFromBackStack: " + this);
LogWriter logw = new LogWriter(TAG);
@@ -695,62 +829,71 @@
dump(" ", null, pw, null);
}
+ if (state == null) {
+ state = beginTransition(mSharedElementTargetNames, mSharedElementSourceNames);
+ } else if (Build.VERSION.SDK_INT >= 21 || Build.VERSION.CODENAME.equals("L")) {
+ FragmentTransitionCompat21.setNameOverrides(state, mSharedElementTargetNames,
+ mSharedElementSourceNames);
+ }
+
bumpBackStackNesting(-1);
+ int transitionStyle = state != null ? 0 : mTransitionStyle;
+ int transition = state != null ? 0 : mTransition;
Op op = mTail;
while (op != null) {
+ int popEnterAnim = state != null ? 0 : op.popEnterAnim;
+ int popExitAnim= state != null ? 0 : op.popExitAnim;
switch (op.cmd) {
case OP_ADD: {
Fragment f = op.fragment;
- f.mNextAnim = op.popExitAnim;
+ f.mNextAnim = popExitAnim;
mManager.removeFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition),
- mTransitionStyle);
+ FragmentManagerImpl.reverseTransit(transition), transitionStyle);
} break;
case OP_REPLACE: {
Fragment f = op.fragment;
if (f != null) {
- f.mNextAnim = op.popExitAnim;
+ f.mNextAnim = popExitAnim;
mManager.removeFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition),
- mTransitionStyle);
+ FragmentManagerImpl.reverseTransit(transition), transitionStyle);
}
if (op.removed != null) {
for (int i=0; i<op.removed.size(); i++) {
Fragment old = op.removed.get(i);
- old.mNextAnim = op.popEnterAnim;
+ old.mNextAnim = popEnterAnim;
mManager.addFragment(old, false);
}
}
} break;
case OP_REMOVE: {
Fragment f = op.fragment;
- f.mNextAnim = op.popEnterAnim;
+ f.mNextAnim = popEnterAnim;
mManager.addFragment(f, false);
} break;
case OP_HIDE: {
Fragment f = op.fragment;
- f.mNextAnim = op.popEnterAnim;
+ f.mNextAnim = popEnterAnim;
mManager.showFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle);
+ FragmentManagerImpl.reverseTransit(transition), transitionStyle);
} break;
case OP_SHOW: {
Fragment f = op.fragment;
- f.mNextAnim = op.popExitAnim;
+ f.mNextAnim = popExitAnim;
mManager.hideFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle);
+ FragmentManagerImpl.reverseTransit(transition), transitionStyle);
} break;
case OP_DETACH: {
Fragment f = op.fragment;
- f.mNextAnim = op.popEnterAnim;
+ f.mNextAnim = popEnterAnim;
mManager.attachFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle);
+ FragmentManagerImpl.reverseTransit(transition), transitionStyle);
} break;
case OP_ATTACH: {
Fragment f = op.fragment;
- f.mNextAnim = op.popEnterAnim;
+ f.mNextAnim = popEnterAnim;
mManager.detachFragment(f,
- FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle);
+ FragmentManagerImpl.reverseTransit(transition), transitionStyle);
} break;
default: {
throw new IllegalArgumentException("Unknown cmd: " + op.cmd);
@@ -762,13 +905,18 @@
if (doStateMove) {
mManager.moveToState(mManager.mCurState,
- FragmentManagerImpl.reverseTransit(mTransition), mTransitionStyle, true);
+ FragmentManagerImpl.reverseTransit(transition), transitionStyle, true);
+ if (state != null) {
+ updateTransitionEndState(state, mSharedElementSourceNames);
+ state = null;
+ }
}
if (mIndex >= 0) {
mManager.freeBackStackIndex(mIndex);
mIndex = -1;
}
+ return state;
}
public String getName() {
diff --git a/v4/java/android/support/v4/app/FragmentActivity.java b/v4/java/android/support/v4/app/FragmentActivity.java
index ad57bb8..1a29bc8 100644
--- a/v4/java/android/support/v4/app/FragmentActivity.java
+++ b/v4/java/android/support/v4/app/FragmentActivity.java
@@ -178,11 +178,65 @@
*/
public void onBackPressed() {
if (!mFragments.popBackStackImmediate()) {
- finish();
+ supportFinishAfterTransition();
}
}
/**
+ * Reverses the Activity Scene entry Transition and triggers the calling Activity
+ * to reverse its exit Transition. When the exit Transition completes,
+ * {@link #finish()} is called. If no entry Transition was used, finish() is called
+ * immediately and the Activity exit Transition is run.
+ *
+ * <p>On Android 4.4 or lower, this method only finishes the Activity with no
+ * special exit transition.</p>
+ */
+ public void supportFinishAfterTransition() {
+ ActivityCompat.finishAfterTransition(this);
+ }
+
+ /**
+ * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,
+ * android.view.View, String)} was used to start an Activity, <var>listener</var>
+ * will be called to handle shared elements on the <i>launched</i> Activity. This requires
+ * {@link Window#FEATURE_CONTENT_TRANSITIONS}.
+ *
+ * @param listener Used to manipulate shared element transitions on the launched Activity.
+ */
+ public void setEnterSharedElementListener(SharedElementListener listener) {
+ ActivityCompat.setEnterSharedElementListener(this, listener);
+ }
+
+ /**
+ * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,
+ * android.view.View, String)} was used to start an Activity, <var>listener</var>
+ * will be called to handle shared elements on the <i>launching</i> Activity. Most
+ * calls will only come when returning from the started Activity.
+ * This requires {@link Window#FEATURE_CONTENT_TRANSITIONS}.
+ *
+ * @param listener Used to manipulate shared element transitions on the launching Activity.
+ */
+ public void setExitSharedElementListener(SharedElementListener listener) {
+ ActivityCompat.setExitSharedElementListener(this, listener);
+ }
+
+ /**
+ * Support library version of {@link android.app.Activity#postponeEnterTransition()} that works
+ * only on API 21 and later.
+ */
+ public void supportPostponeEnterTransition() {
+ ActivityCompat.postponeEnterTransition(this);
+ }
+
+ /**
+ * Support library version of {@link android.app.Activity#startPostponedEnterTransition()}
+ * that only works with API 21 and later.
+ */
+ public void supportStartPostponedEnterTransition() {
+ ActivityCompat.startPostponedEnterTransition(this);
+ }
+
+ /**
* Dispatch configuration change to all fragments.
*/
@Override
diff --git a/v4/java/android/support/v4/app/FragmentManager.java b/v4/java/android/support/v4/app/FragmentManager.java
index 2def8ed..b381a36 100644
--- a/v4/java/android/support/v4/app/FragmentManager.java
+++ b/v4/java/android/support/v4/app/FragmentManager.java
@@ -1531,7 +1531,7 @@
return false;
}
final BackStackRecord bss = mBackStack.remove(last);
- bss.popFromBackStack(true);
+ bss.popFromBackStack(true, null);
reportBackStackChanged();
} else {
int index = -1;
@@ -1575,9 +1575,10 @@
states.add(mBackStack.remove(i));
}
final int LAST = states.size()-1;
+ Object transitionState = null;
for (int i=0; i<=LAST; i++) {
if (DEBUG) Log.v(TAG, "Popping back stack state: " + states.get(i));
- states.get(i).popFromBackStack(i == LAST);
+ transitionState = states.get(i).popFromBackStack(i == LAST, transitionState);
}
reportBackStackChanged();
}
@@ -1792,6 +1793,7 @@
fs.mSavedFragmentState.setClassLoader(mActivity.getClassLoader());
f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray(
FragmentManagerImpl.VIEW_STATE_TAG);
+ f.mSavedFragmentState = fs.mSavedFragmentState;
}
}
}
diff --git a/v4/java/android/support/v4/app/FragmentTransaction.java b/v4/java/android/support/v4/app/FragmentTransaction.java
index d984d36..41f64ce 100644
--- a/v4/java/android/support/v4/app/FragmentTransaction.java
+++ b/v4/java/android/support/v4/app/FragmentTransaction.java
@@ -22,6 +22,8 @@
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.StyleRes;
+import android.support.v4.util.Pair;
+import android.view.View;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -192,7 +194,37 @@
*/
public abstract FragmentTransaction setCustomAnimations(@AnimRes int enter,
@AnimRes int exit, @AnimRes int popEnter, @AnimRes int popExit);
-
+
+ /**
+ * Set a {@link android.transition.Transition} resource id to use with this transaction.
+ * <var>transitionId</var> will be played for fragments when going forward and when popping
+ * the back stack.
+ * @param sceneRootId The ID of the element acting as the scene root for the transition.
+ * This should be a ViewGroup containing all Fragments in the transaction.
+ * @param transitionId The resource ID for the Transition used during the Fragment transaction.
+ */
+ public abstract FragmentTransaction setCustomTransition(int sceneRootId, int transitionId);
+
+ /**
+ * Used with {@link #setCustomTransition(int, int)} to map a View from a removed or hidden
+ * Fragment to a View from a shown or added Fragment.
+ * <var>sharedElement</var> must have a unique transitionName in the View hierarchy.
+ * @param sharedElement A View in a disappearing Fragment to match with a View in an
+ * appearing Fragment.
+ * @param name The transitionName for a View in an appearing Fragment to match to the shared
+ * element.
+ */
+ public abstract FragmentTransaction setSharedElement(View sharedElement, String name);
+
+ /**
+ * Used with {@link #setCustomTransition(int, int)} to map multiple Views from removed or hidden
+ * Fragments to a Views from a shown or added Fragments. Views in
+ * <var>sharedElements</var> must have unique transitionNames in the View hierarchy.
+ * @param sharedElements Pairs of Views in disappearing Fragments to transitionNames in
+ * appearing Fragments.
+ */
+ public abstract FragmentTransaction setSharedElements(Pair<View, String>... sharedElements);
+
/**
* Select a standard transition animation for this transaction. May be
* one of {@link #TRANSIT_NONE}, {@link #TRANSIT_FRAGMENT_OPEN},
diff --git a/v4/java/android/support/v4/app/NotificationCompat.java b/v4/java/android/support/v4/app/NotificationCompat.java
index 5a91277..dedf2e8 100644
--- a/v4/java/android/support/v4/app/NotificationCompat.java
+++ b/v4/java/android/support/v4/app/NotificationCompat.java
@@ -20,6 +20,7 @@
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Bitmap;
+import android.graphics.Color;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
@@ -48,6 +49,11 @@
* Use the default notification sound. This will ignore any sound set using
* {@link Builder#setSound}
*
+ * <p>
+ * A notification that is noisy is more likely to be presented as a heads-up notification,
+ * on some platforms.
+ * </p>
+ *
* @see Builder#setDefaults
*/
public static final int DEFAULT_SOUND = 1;
@@ -57,6 +63,11 @@
* {@link Builder#setVibrate}. Using phone vibration requires the
* {@link android.Manifest.permission#VIBRATE VIBRATE} permission.
*
+ * <p>
+ * A notification that vibrates is more likely to be presented as a heads-up notification,
+ * on some platforms.
+ * </p>
+ *
* @see Builder#setDefaults
*/
public static final int DEFAULT_VIBRATE = 2;
@@ -281,6 +292,36 @@
*/
public static final String EXTRA_PEOPLE = "android.people";
+ /**
+ * Value of {@link Notification#color} equal to 0 (also known as
+ * {@link android.graphics.Color#TRANSPARENT Color.TRANSPARENT}),
+ * telling the system not to decorate this notification with any special color but instead use
+ * default colors when presenting this notification.
+ */
+ public static final int COLOR_DEFAULT = Color.TRANSPARENT;
+
+ /**
+ * Notification visibility: Show this notification in its entirety on all lockscreens.
+ *
+ * {@see android.app.Notification#visibility}
+ */
+ public static final int VISIBILITY_PUBLIC = 1;
+
+ /**
+ * Notification visibility: Show this notification on all lockscreens, but conceal sensitive or
+ * private information on secure lockscreens.
+ *
+ * {@see android.app.Notification#visibility}
+ */
+ public static final int VISIBILITY_PRIVATE = 0;
+
+ /**
+ * Notification visibility: Do not reveal any part of this notification on a secure lockscreen.
+ *
+ * {@see android.app.Notification#visibility}
+ */
+ public static final int VISIBILITY_SECRET = -1;
+
private static final NotificationCompatImpl IMPL;
interface NotificationCompatImpl {
@@ -462,7 +503,7 @@
b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
b.mProgressMax, b.mProgress, b.mProgressIndeterminate,
- b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mExtras,
+ b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mPeople, b.mExtras,
b.mGroupKey, b.mGroupSummary, b.mSortKey);
addActionsToBuilder(builder, b.mActions);
addStyleToBuilderJellybean(builder, b.mStyle);
@@ -513,7 +554,8 @@
b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
b.mProgressMax, b.mProgress, b.mProgressIndeterminate,
- b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mExtras,
+ b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mCategory,
+ b.mPeople, b.mExtras, b.mColor, b.mVisibility, b.mPublicVersion,
b.mGroupKey, b.mGroupSummary, b.mSortKey);
addActionsToBuilder(builder, b.mActions);
addStyleToBuilderJellybean(builder, b.mStyle);
@@ -598,7 +640,8 @@
}
static {
- if (Build.VERSION.SDK_INT >= 20) {
+ // TODO: Replace this if clause when SDK_INT is incremented to 20.
+ if (Build.VERSION.RELEASE.equals("L")) {
IMPL = new NotificationCompatImplApi20();
} else if (Build.VERSION.SDK_INT >= 19) {
IMPL = new NotificationCompatImplKitKat();
@@ -658,9 +701,14 @@
String mSortKey;
ArrayList<Action> mActions = new ArrayList<Action>();
boolean mLocalOnly = false;
+ String mCategory;
Bundle mExtras;
+ int mColor = COLOR_DEFAULT;
+ int mVisibility = VISIBILITY_PRIVATE;
+ Notification mPublicVersion;
Notification mNotification = new Notification();
+ public ArrayList<String> mPeople;
/**
* Constructor.
@@ -680,6 +728,7 @@
mNotification.when = System.currentTimeMillis();
mNotification.audioStreamType = Notification.STREAM_DEFAULT;
mPriority = PRIORITY_DEFAULT;
+ mPeople = new ArrayList<String>();
}
/**
@@ -836,6 +885,11 @@
* to turn it off and use a normal notification, as this can be extremely
* disruptive.
*
+ * <p>
+ * On some platforms, the system UI may choose to display a heads-up notification,
+ * instead of launching this intent, while the user is using the device.
+ * </p>
+ *
* @param intent The pending intent to launch.
* @param highPriority Passing true will cause this notification to be sent
* even if other notifications are suppressed.
@@ -876,6 +930,11 @@
/**
* Set the sound to play. It will play on the default stream.
+ *
+ * <p>
+ * On some platforms, a notification that is noisy is more likely to be presented
+ * as a heads-up notification.
+ * </p>
*/
public Builder setSound(Uri sound) {
mNotification.sound = sound;
@@ -886,6 +945,11 @@
/**
* Set the sound to play. It will play on the stream you supply.
*
+ * <p>
+ * On some platforms, a notification that is noisy is more likely to be presented
+ * as a heads-up notification.
+ * </p>
+ *
* @see Notification#STREAM_DEFAULT
* @see AudioManager for the <code>STREAM_</code> constants.
*/
@@ -898,6 +962,11 @@
/**
* Set the vibration pattern to use.
*
+ * <p>
+ * On some platforms, a notification that vibrates is more likely to be presented
+ * as a heads-up notification.
+ * </p>
+ *
* @see android.os.Vibrator for a discussion of the <code>pattern</code>
* parameter.
*/
@@ -969,6 +1038,18 @@
}
/**
+ * Set the notification category.
+ *
+ * <p>Must be one of the predefined notification categories (see the <code>CATEGORY_*</code>
+ * constants in {@link Notification}) that best describes this notification.
+ * May be used by the system for ranking and filtering.
+ */
+ public Builder setCategory(String category) {
+ mCategory = category;
+ return this;
+ }
+
+ /**
* Set the default notification options that will be used.
* <p>
* The value should be one or more of the following fields combined with
@@ -1017,6 +1098,16 @@
}
/**
+ * Add a person that is relevant to this notification.
+ *
+ * @see Notification#EXTRA_PEOPLE
+ */
+ public Builder addPerson(String handle) {
+ mPeople.add(handle);
+ return this;
+ }
+
+ /**
* Set this notification to be part of a group of notifications sharing the same key.
* Grouped notifications may display in a cluster or stack on devices which
* support such rendering.
@@ -1172,6 +1263,43 @@
}
/**
+ * Sets {@link Notification#color}.
+ *
+ * @param argb The accent color to use
+ *
+ * @return The same Builder.
+ */
+ public Builder setColor(int argb) {
+ mColor = argb;
+ return this;
+ }
+
+ /**
+ * Sets {@link Notification#visibility}.
+ *
+ * @param visibility One of {@link Notification#VISIBILITY_PRIVATE} (the default),
+ * {@link Notification#VISIBILITY_PUBLIC}, or
+ * {@link Notification#VISIBILITY_SECRET}.
+ */
+ public Builder setVisibility(int visibility) {
+ mVisibility = visibility;
+ return this;
+ }
+
+ /**
+ * Supply a replacement Notification whose contents should be shown in insecure contexts
+ * (i.e. atop the secure lockscreen). See {@link Notification#visibility} and
+ * {@link #VISIBILITY_PUBLIC}.
+ *
+ * @param n A replacement notification, presumably with some or all info redacted.
+ * @return The same Builder.
+ */
+ public Builder setPublicVersion(Notification n) {
+ mPublicVersion = n;
+ return this;
+ }
+
+ /**
* Apply an extender to this notification builder. Extenders may be used to add
* metadata or change options on this builder.
*/
diff --git a/v4/java/android/support/v4/app/SharedElementListener.java b/v4/java/android/support/v4/app/SharedElementListener.java
new file mode 100644
index 0000000..06b1c9b
--- /dev/null
+++ b/v4/java/android/support/v4/app/SharedElementListener.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.app;
+
+import android.view.View;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Listener provided in
+ * {@link FragmentActivity#setEnterSharedElementListener(SharedElementListener)} and
+ * {@link FragmentActivity#setExitSharedElementListener(SharedElementListener)}
+ * to monitor the Activity transitions. The events can be used to customize Activity
+ * Transition behavior.
+ */
+public abstract class SharedElementListener {
+
+ /**
+ * Called to allow the listener to customize the start state of the shared element when
+ * transferring in shared element state.
+ * <p>
+ * The shared element will start at the size and position of the shared element
+ * in the launching Activity or Fragment. It will also transfer ImageView scaleType
+ * and imageMatrix if the shared elements in the calling and called Activities are
+ * ImageViews. Some applications may want to make additional changes, such as
+ * changing the clip bounds, scaling, or rotation if the shared element end state
+ * does not map well to the start state.
+ * </p>
+ *
+ * @param sharedElementNames The names of the shared elements that were accepted into
+ * the View hierarchy.
+ * @param sharedElements The shared elements that are part of the View hierarchy.
+ * @param sharedElementSnapshots The Views containing snap shots of the shared element
+ * from the launching Window. These elements will not
+ * be part of the scene, but will be positioned relative
+ * to the Window decor View.
+ */
+ public void setSharedElementStart(List<String> sharedElementNames,
+ List<View> sharedElements, List<View> sharedElementSnapshots) {}
+
+ /**
+ * Called to allow the listener to customize the end state of the shared element when
+ * transferring in shared element state.
+ * <p>
+ * Any customization done in
+ * {@link #setSharedElementStart(java.util.List, java.util.List, java.util.List)}
+ * may need to be modified to the final state of the shared element if it is not
+ * automatically corrected by layout. For example, rotation or scale will not
+ * be affected by layout and if changed in {@link #setSharedElementStart(java.util.List,
+ * java.util.List, java.util.List)}, it will also have to be set here again to correct
+ * the end state.
+ * </p>
+ *
+ * @param sharedElementNames The names of the shared elements that were accepted into
+ * the View hierarchy.
+ * @param sharedElements The shared elements that are part of the View hierarchy.
+ * @param sharedElementSnapshots The Views containing snap shots of the shared element
+ * from the launching Window. These elements will not
+ * be part of the scene, but will be positioned relative
+ * to the Window decor View.
+ */
+ public void setSharedElementEnd(List<String> sharedElementNames,
+ List<View> sharedElements, List<View> sharedElementSnapshots) {}
+
+ /**
+ * Called after {@link #remapSharedElements(java.util.List, java.util.Map)} when
+ * transferring shared elements in. Any shared elements that have no mapping will be in
+ * <var>rejectedSharedElements</var>. The elements remaining in
+ * <var>rejectedSharedElements</var> will be transitioned out of the Scene. If a
+ * View is removed from <var>rejectedSharedElements</var>, it must be handled by the
+ * <code>SharedElementListener</code>.
+ * <p>
+ * Views in rejectedSharedElements will have their position and size set to the
+ * position of the calling shared element, relative to the Window decor View and contain
+ * snapshots of the View from the calling Activity or Fragment. This
+ * view may be safely added to the decor View's overlay to remain in position.
+ * </p>
+ *
+ * @param rejectedSharedElements Views containing visual information of shared elements
+ * that are not part of the entering scene. These Views
+ * are positioned relative to the Window decor View. A
+ * View removed from this list will not be transitioned
+ * automatically.
+ */
+ public void handleRejectedSharedElements(List<View> rejectedSharedElements) {}
+
+ /**
+ * Lets the ActivityTransitionListener adjust the mapping of shared element names to
+ * Views.
+ *
+ * @param names The names of all shared elements transferred from the calling Activity
+ * to the started Activity.
+ * @param sharedElements The mapping of shared element names to Views. The best guess
+ * will be filled into sharedElements based on the transitionNames.
+ */
+ public void remapSharedElements(List<String> names, Map<String, View> sharedElements) {}
+}
diff --git a/v4/java/android/support/v4/content/ContextCompat.java b/v4/java/android/support/v4/content/ContextCompat.java
index 8fbf4bb..7119c3c 100644
--- a/v4/java/android/support/v4/content/ContextCompat.java
+++ b/v4/java/android/support/v4/content/ContextCompat.java
@@ -18,6 +18,8 @@
import android.content.Context;
import android.content.Intent;
+import android.content.res.Resources.Theme;
+import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
@@ -297,4 +299,24 @@
}
return cur;
}
+
+ /**
+ * Return a drawable object associated with a particular resource ID.
+ * <p>
+ * Starting in {@link android.os.Build.VERSION_CODES#L}, the returned
+ * drawable will be styled for the specified Context's theme.
+ *
+ * @param id The desired resource identifier, as generated by the aapt tool.
+ * This integer encodes the package, type, and resource entry.
+ * The value 0 is an invalid identifier.
+ * @return Drawable An object that can be used to draw this resource.
+ */
+ public final Drawable getDrawable(Context context, int id) {
+ final int version = Build.VERSION.SDK_INT;
+ if (version >= 21) {
+ return ContextCompatApi21.getDrawable(context, id);
+ } else {
+ return context.getResources().getDrawable(id);
+ }
+ }
}
diff --git a/v4/java/android/support/v4/content/res/ResourcesCompat.java b/v4/java/android/support/v4/content/res/ResourcesCompat.java
new file mode 100644
index 0000000..2dbd334
--- /dev/null
+++ b/v4/java/android/support/v4/content/res/ResourcesCompat.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.content.res;
+
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.content.res.Resources.Theme;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+
+/**
+ * Helper for accessing features in {@link android.content.res.Resources}
+ * introduced after API level 4 in a backwards compatible fashion.
+ */
+public class ResourcesCompat {
+ /**
+ * Return a drawable object associated with a particular resource ID and
+ * styled for the specified theme. Various types of objects will be
+ * returned depending on the underlying resource -- for example, a solid
+ * color, PNG image, scalable image, etc.
+ * <p>
+ * Prior to API level 21, the theme will not be applied and this method
+ * simply calls through to {@link Resources#getDrawable(int)}.
+ *
+ * @param id The desired resource identifier, as generated by the aapt
+ * tool. This integer encodes the package, type, and resource
+ * entry. The value 0 is an invalid identifier.
+ * @param theme The theme used to style the drawable attributes, may be {@code null}.
+ * @return Drawable An object that can be used to draw this resource.
+ * @throws NotFoundException Throws NotFoundException if the given ID does
+ * not exist.
+ */
+ public Drawable getDrawable(Resources res, int id, Theme theme)
+ throws NotFoundException {
+ final int version = Build.VERSION.SDK_INT;
+ if (version >= 21) {
+ return ResourcesCompatApi21.getDrawable(res, id, theme);
+ } else {
+ return res.getDrawable(id);
+ }
+ }
+}
diff --git a/v4/java/android/support/v4/graphics/BitmapCompat.java b/v4/java/android/support/v4/graphics/BitmapCompat.java
new file mode 100644
index 0000000..5b1cb68
--- /dev/null
+++ b/v4/java/android/support/v4/graphics/BitmapCompat.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.graphics;
+
+import android.graphics.Bitmap;
+
+/**
+ * Helper for accessing features in {@link android.graphics.Bitmap}
+ * introduced after API level 4 in a backwards compatible fashion.
+ */
+public class BitmapCompat {
+ /**
+ * Interface for the full API.
+ */
+ interface BitmapImpl {
+ public boolean hasMipMap(Bitmap bitmap);
+ public void setHasMipMap(Bitmap bitmap, boolean hasMipMap);
+ public int getAllocationByteCount(Bitmap bitmap);
+ }
+
+ static class BaseBitmapImpl implements BitmapImpl {
+ @Override
+ public boolean hasMipMap(Bitmap bitmap) {
+ return false;
+ }
+
+ @Override
+ public void setHasMipMap(Bitmap bitmap, boolean hasMipMap) {
+ }
+
+ @Override
+ public int getAllocationByteCount(Bitmap bitmap) {
+ return bitmap.getRowBytes() * bitmap.getHeight();
+ }
+ }
+
+ static class HcMr1BitmapCompatImpl extends BaseBitmapImpl {
+ @Override
+ public int getAllocationByteCount(Bitmap bitmap) {
+ return BitmapCompatHoneycombMr1.getAllocationByteCount(bitmap);
+ }
+ }
+
+ static class JbMr2BitmapCompatImpl extends HcMr1BitmapCompatImpl {
+ @Override
+ public boolean hasMipMap(Bitmap bitmap){
+ return BitmapCompatJellybeanMR2.hasMipMap(bitmap);
+ }
+
+ @Override
+ public void setHasMipMap(Bitmap bitmap, boolean hasMipMap) {
+ BitmapCompatJellybeanMR2.setHasMipMap(bitmap, hasMipMap);
+ }
+ }
+
+ static class KitKatBitmapCompatImpl extends JbMr2BitmapCompatImpl {
+ @Override
+ public int getAllocationByteCount(Bitmap bitmap) {
+ return BitmapCompatKitKat.getAllocationByteCount(bitmap);
+ }
+ }
+
+ /**
+ * Select the correct implementation to use for the current platform.
+ */
+ static final BitmapImpl IMPL;
+ static {
+ final int version = android.os.Build.VERSION.SDK_INT;
+ if (version >= 19) {
+ IMPL = new KitKatBitmapCompatImpl();
+ } else if (version >= 18) {
+ IMPL = new JbMr2BitmapCompatImpl();
+ } else if (version >= 12) {
+ IMPL = new HcMr1BitmapCompatImpl();
+ } else {
+ IMPL = new BaseBitmapImpl();
+ }
+ }
+
+ public static boolean hasMipMap(Bitmap bitmap) {
+ return IMPL.hasMipMap(bitmap);
+ }
+
+ public static void setHasMipMap(Bitmap bitmap, boolean hasMipMap) {
+ IMPL.setHasMipMap(bitmap, hasMipMap);
+ }
+
+ /**
+ * Returns the size of the allocated memory used to store this bitmap's pixels in a backwards
+ * compatible way.
+ *
+ * @param bitmap the bitmap in which to return it's allocation size
+ * @return the allocation size in bytes
+ */
+ public static int getAllocationByteCount(Bitmap bitmap) {
+ return IMPL.getAllocationByteCount(bitmap);
+ }
+}
\ No newline at end of file
diff --git a/v4/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java b/v4/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java
new file mode 100644
index 0000000..a8174a3
--- /dev/null
+++ b/v4/java/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.graphics.drawable;
+
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.support.v4.graphics.BitmapCompat;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.Gravity;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+
+/**
+ * A Drawable that wraps a bitmap and can be drawn with rounded corners. You can create a
+ * RoundedBitmapDrawable from a file path, an input stream, or from a
+ * {@link android.graphics.Bitmap} object.
+ * <p>
+ * Also see the {@link android.graphics.Bitmap} class, which handles the management and
+ * transformation of raw bitmap graphics, and should be used when drawing to a
+ * {@link android.graphics.Canvas}.
+ * </p>
+ */
+public class RoundedBitmapDrawable extends Drawable {
+
+ private static final String TAG = "RoundedBitmapDrawable";
+ private static final int DEFAULT_PAINT_FLAGS =
+ Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG;
+ private Bitmap mBitmap;
+ private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
+ private int mGravity = Gravity.FILL;
+ private Paint mPaint = new Paint(DEFAULT_PAINT_FLAGS);
+ private BitmapShader mBitmapShader;
+ private float mCornerRadius;
+
+ private final Rect mDstRect = new Rect(); // Gravity.apply() sets this
+ private final RectF mDstRectF = new RectF();
+
+ private boolean mApplyGravity = true;
+
+ // These are scaled to match the target density.
+ private int mBitmapWidth;
+ private int mBitmapHeight;
+
+
+ /**
+ * Returns a new drawable by creating it from a bitmap, setting initial target density based on
+ * the display metrics of the resources.
+ */
+ public static RoundedBitmapDrawable createRoundedBitmapDrawable(Resources res, Bitmap bitmap) {
+ return new RoundedBitmapDrawable(res, bitmap);
+ }
+
+ /**
+ * Returns a new drawable, creating it by opening a given file path and decoding the bitmap.
+ */
+ public static RoundedBitmapDrawable createRoundedBitmapDrawable(Resources res,
+ String filepath) {
+ final RoundedBitmapDrawable drawable =
+ createRoundedBitmapDrawable(res, BitmapFactory.decodeFile(filepath));
+ if (drawable.mBitmap == null) {
+ Log.w(TAG, "BitmapDrawable cannot decode " + filepath);
+ }
+ return drawable;
+ }
+
+
+ /**
+ * Returns a new drawable, creating it by decoding a bitmap from the given input stream.
+ */
+ public static RoundedBitmapDrawable createRoundedBitmapDrawable(Resources res,
+ java.io.InputStream is) {
+ final RoundedBitmapDrawable drawable =
+ createRoundedBitmapDrawable(res, BitmapFactory.decodeStream(is));
+ if (drawable.mBitmap == null) {
+ Log.w(TAG, "BitmapDrawable cannot decode " + is);
+ }
+ return drawable;
+ }
+
+ /**
+ * Returns the paint used to render this drawable.
+ */
+ public final Paint getPaint() {
+ return mPaint;
+ }
+
+ /**
+ * Returns the bitmap used by this drawable to render. May be null.
+ */
+ public final Bitmap getBitmap() {
+ return mBitmap;
+ }
+
+ private void computeBitmapSize() {
+ mBitmapWidth = mBitmap.getScaledWidth(mTargetDensity);
+ mBitmapHeight = mBitmap.getScaledHeight(mTargetDensity);
+ }
+
+ /**
+ * Set the density scale at which this drawable will be rendered. This
+ * method assumes the drawable will be rendered at the same density as the
+ * specified canvas.
+ *
+ * @param canvas The Canvas from which the density scale must be obtained.
+ *
+ * @see android.graphics.Bitmap#setDensity(int)
+ * @see android.graphics.Bitmap#getDensity()
+ */
+ public void setTargetDensity(Canvas canvas) {
+ setTargetDensity(canvas.getDensity());
+ }
+
+ /**
+ * Set the density scale at which this drawable will be rendered.
+ *
+ * @param metrics The DisplayMetrics indicating the density scale for this drawable.
+ *
+ * @see android.graphics.Bitmap#setDensity(int)
+ * @see android.graphics.Bitmap#getDensity()
+ */
+ public void setTargetDensity(DisplayMetrics metrics) {
+ setTargetDensity(metrics.densityDpi);
+ }
+
+ /**
+ * Set the density at which this drawable will be rendered.
+ *
+ * @param density The density scale for this drawable.
+ *
+ * @see android.graphics.Bitmap#setDensity(int)
+ * @see android.graphics.Bitmap#getDensity()
+ */
+ public void setTargetDensity(int density) {
+ if (mTargetDensity != density) {
+ mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density;
+ if (mBitmap != null) {
+ computeBitmapSize();
+ }
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Get the gravity used to position/stretch the bitmap within its bounds.
+ *
+ * @return the gravity applied to the bitmap
+ *
+ * @see android.view.Gravity
+ */
+ public int getGravity() {
+ return mGravity;
+ }
+
+ /**
+ * Set the gravity used to position/stretch the bitmap within its bounds.
+ *
+ * @param gravity the gravity
+ *
+ * @see android.view.Gravity
+ */
+ public void setGravity(int gravity) {
+ if (mGravity != gravity) {
+ mGravity = gravity;
+ mApplyGravity = true;
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Enables or disables the mipmap hint for this drawable's bitmap.
+ * See {@link Bitmap#setHasMipMap(boolean)} for more information.
+ *
+ * If the bitmap is null, or the current API version does not support setting a mipmap hint,
+ * calling this method has no effect.
+ *
+ * @param mipMap True if the bitmap should use mipmaps, false otherwise.
+ *
+ * @see #hasMipMap()
+ */
+ public void setMipMap(boolean mipMap) {
+ if (mBitmap != null) {
+ BitmapCompat.setHasMipMap(mBitmap, mipMap);
+ invalidateSelf();
+ }
+ }
+
+ /**
+ * Indicates whether the mipmap hint is enabled on this drawable's bitmap.
+ *
+ * @return True if the mipmap hint is set, false otherwise. If the bitmap
+ * is null, this method always returns false.
+ *
+ * @see #setMipMap(boolean)
+ */
+ public boolean hasMipMap() {
+ return mBitmap != null && BitmapCompat.hasMipMap(mBitmap);
+ }
+
+ /**
+ * Enables or disables anti-aliasing for this drawable. Anti-aliasing affects
+ * the edges of the bitmap only so it applies only when the drawable is rotated.
+ *
+ * @param aa True if the bitmap should be anti-aliased, false otherwise.
+ *
+ * @see #hasAntiAlias()
+ */
+ public void setAntiAlias(boolean aa) {
+ mPaint.setAntiAlias(aa);
+ invalidateSelf();
+ }
+
+ /**
+ * Indicates whether anti-aliasing is enabled for this drawable.
+ *
+ * @return True if anti-aliasing is enabled, false otherwise.
+ *
+ * @see #setAntiAlias(boolean)
+ */
+ public boolean hasAntiAlias() {
+ return mPaint.isAntiAlias();
+ }
+
+ @Override
+ public void setFilterBitmap(boolean filter) {
+ mPaint.setFilterBitmap(filter);
+ invalidateSelf();
+ }
+
+ @Override
+ public void setDither(boolean dither) {
+ mPaint.setDither(dither);
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final Bitmap bitmap = mBitmap;
+ if (bitmap == null) {
+ return;
+ }
+
+ final Paint paint = mPaint;
+
+ final Shader shader = paint.getShader();
+ if (shader == null) {
+ if (mApplyGravity) {
+ GravityCompat.apply(mGravity, mBitmapWidth, mBitmapHeight,
+ getBounds(), mDstRect, ViewCompat.LAYOUT_DIRECTION_LTR);
+ mDstRectF.set(mDstRect);
+ mApplyGravity = false;
+ }
+
+ canvas.drawBitmap(bitmap, null, mDstRect, paint);
+
+ } else {
+ if (mApplyGravity) {
+ copyBounds(mDstRect);
+ mDstRectF.set(mDstRect);
+ mApplyGravity = false;
+ }
+
+ canvas.drawRoundRect(mDstRectF, mCornerRadius, mCornerRadius, paint);
+ }
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ final int oldAlpha = mPaint.getAlpha();
+ if (alpha != oldAlpha) {
+ mPaint.setAlpha(alpha);
+ invalidateSelf();
+ }
+ }
+
+ public int getAlpha() {
+ return mPaint.getAlpha();
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ invalidateSelf();
+ }
+
+ public ColorFilter getColorFilter() {
+ return mPaint.getColorFilter();
+ }
+
+ /**
+ * Sets the corner radius to be applied when drawing the bitmap.
+ */
+ public void setCornerRadius(float cornerRadius) {
+ if (isGreaterThanZero(cornerRadius)) {
+ mPaint.setShader(mBitmapShader);
+ } else {
+ mPaint.setShader(null);
+ }
+ mCornerRadius = cornerRadius;
+ }
+
+ /**
+ * @return The corner radius applied when drawing the bitmap.
+ */
+ public float getCornerRadius() {
+ return mCornerRadius;
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mBitmapWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mBitmapHeight;
+ }
+
+ @Override
+ public int getOpacity() {
+ if (mGravity != Gravity.FILL) {
+ return PixelFormat.TRANSLUCENT;
+ }
+ Bitmap bm = mBitmap;
+ return (bm == null
+ || bm.hasAlpha()
+ || mPaint.getAlpha() < 255
+ || isGreaterThanZero(mCornerRadius))
+ ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE;
+ }
+
+ private RoundedBitmapDrawable(Resources res, Bitmap bitmap) {
+ if (res != null) {
+ mTargetDensity = res.getDisplayMetrics().densityDpi;
+ }
+
+ mBitmap = bitmap;
+ if (mBitmap != null) {
+ computeBitmapSize();
+ mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+ } else {
+ mBitmapWidth = mBitmapHeight = -1;
+ }
+ }
+
+ private boolean isGreaterThanZero(float toCompare) {
+ return Float.compare(toCompare, +0.0f) > 0;
+ }
+}
diff --git a/v4/java/android/support/v4/media/MediaMetadataCompat.java b/v4/java/android/support/v4/media/MediaMetadataCompat.java
new file mode 100644
index 0000000..7f15276
--- /dev/null
+++ b/v4/java/android/support/v4/media/MediaMetadataCompat.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.media;
+
+import android.graphics.Bitmap;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.util.ArrayMap;
+import android.util.Log;
+
+import java.util.Set;
+
+/**
+ * Contains metadata about an item, such as the title, artist, etc.
+ */
+public final class MediaMetadataCompat implements Parcelable {
+ private static final String TAG = "MediaMetadata";
+
+ /**
+ * The title of the media.
+ */
+ public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+
+ /**
+ * The artist of the media.
+ */
+ public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+
+ /**
+ * The duration of the media in ms. A negative duration indicates that the
+ * duration is unknown (or infinite).
+ */
+ public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+
+ /**
+ * The album title for the media.
+ */
+ public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+
+ /**
+ * The author of the media.
+ */
+ public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+
+ /**
+ * The writer of the media.
+ */
+ public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+
+ /**
+ * The composer of the media.
+ */
+ public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+
+ /**
+ * The compilation status of the media.
+ */
+ public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
+
+ /**
+ * The date the media was created or published as TODO determine format.
+ */
+ public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
+
+ /**
+ * The year the media was created or published as a long.
+ */
+ public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+
+ /**
+ * The genre of the media.
+ */
+ public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+
+ /**
+ * The track number for the media.
+ */
+ public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+
+ /**
+ * The number of tracks in the media's original source.
+ */
+ public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+
+ /**
+ * The disc number for the media's original source.
+ */
+ public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+
+ /**
+ * The artist for the album of the media's original source.
+ */
+ public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+
+ /**
+ * The artwork for the media as a {@link Bitmap}.
+ */
+ public static final String METADATA_KEY_ART = "android.media.metadata.ART";
+
+ /**
+ * The artwork for the media as a Uri style String.
+ */
+ public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+
+ /**
+ * The artwork for the album of the media's original source as a
+ * {@link Bitmap}.
+ */
+ public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+
+ /**
+ * The artwork for the album of the media's original source as a Uri style
+ * String.
+ */
+ public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+
+ /**
+ * The user's rating for the media.
+ *
+ * @see RatingCompat
+ */
+ public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+
+ /**
+ * The overall rating for the media.
+ *
+ * @see RatingCompat
+ */
+ public static final String METADATA_KEY_RATING = "android.media.metadata.RATING";
+
+ private static final int METADATA_TYPE_LONG = 0;
+ private static final int METADATA_TYPE_STRING = 1;
+ private static final int METADATA_TYPE_BITMAP = 2;
+ private static final int METADATA_TYPE_RATING = 3;
+ private static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
+
+ static {
+ METADATA_KEYS_TYPE = new ArrayMap<String, Integer>();
+ METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_STRING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING);
+ }
+
+ private final Bundle mBundle;
+ private Object mMetadataObj;
+
+ private MediaMetadataCompat(Bundle bundle) {
+ mBundle = new Bundle(bundle);
+ }
+
+ private MediaMetadataCompat(Parcel in) {
+ mBundle = in.readBundle();
+ }
+
+ /**
+ * Returns true if the given key is contained in the metadata
+ *
+ * @param key a String key
+ * @return true if the key exists in this metadata, false otherwise
+ */
+ public boolean containsKey(String key) {
+ return mBundle.containsKey(key);
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of
+ * the desired type exists for the given key or a null value is explicitly
+ * associated with the key.
+ *
+ * @param key The key the value is stored under
+ * @return a String value, or null
+ */
+ public String getString(String key) {
+ return mBundle.getString(key);
+ }
+
+ /**
+ * Returns the value associated with the given key, or 0L if no long exists
+ * for the given key.
+ *
+ * @param key The key the value is stored under
+ * @return a long value
+ */
+ public long getLong(String key) {
+ return mBundle.getLong(key, 0);
+ }
+
+ /**
+ * Return a {@link RatingCompat} for the given key or null if no rating exists for
+ * the given key.
+ *
+ * @param key The key the value is stored under
+ * @return A {@link RatingCompat} or null
+ */
+ public RatingCompat getRating(String key) {
+ RatingCompat rating = null;
+ try {
+ rating = mBundle.getParcelable(key);
+ } catch (Exception e) {
+ // ignore, value was not a bitmap
+ Log.w(TAG, "Failed to retrieve a key as Rating.", e);
+ }
+ return rating;
+ }
+
+ /**
+ * Return a {@link Bitmap} for the given key or null if no bitmap exists for
+ * the given key.
+ *
+ * @param key The key the value is stored under
+ * @return A {@link Bitmap} or null
+ */
+ public Bitmap getBitmap(String key) {
+ Bitmap bmp = null;
+ try {
+ bmp = mBundle.getParcelable(key);
+ } catch (Exception e) {
+ // ignore, value was not a bitmap
+ Log.w(TAG, "Failed to retrieve a key as Bitmap.", e);
+ }
+ return bmp;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeBundle(mBundle);
+ }
+
+ /**
+ * Get the number of fields in this metadata.
+ *
+ * @return The number of fields in the metadata.
+ */
+ public int size() {
+ return mBundle.size();
+ }
+
+ /**
+ * Returns a Set containing the Strings used as keys in this metadata.
+ *
+ * @return a Set of String keys
+ */
+ public Set<String> keySet() {
+ return mBundle.keySet();
+ }
+
+ /**
+ * Creates an instance from a framework {@link android.media.MediaMetadata} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @param metadataObj A {@link android.media.MediaMetadata} object, or null if none.
+ * @return An equivalent {@link MediaMetadataCompat} object, or null if none.
+ */
+ public static MediaMetadataCompat fromMediaMetadata(Object metadataObj) {
+ if (metadataObj == null || Build.VERSION.SDK_INT < 21) {
+ return null;
+ }
+
+ Builder builder = new Builder();
+ for (String key : MediaMetadataCompatApi21.keySet(metadataObj)) {
+ Integer type = METADATA_KEYS_TYPE.get(key);
+ if (type != null) {
+ switch (type) {
+ case METADATA_TYPE_BITMAP:
+ builder.putBitmap(key,
+ MediaMetadataCompatApi21.getBitmap(metadataObj, key));
+ break;
+ case METADATA_TYPE_LONG:
+ builder.putLong(key,
+ MediaMetadataCompatApi21.getLong(metadataObj, key));
+ break;
+ case METADATA_TYPE_RATING:
+ builder.putRating(key, RatingCompat.fromRating(
+ MediaMetadataCompatApi21.getRating(metadataObj, key)));
+ break;
+ case METADATA_TYPE_STRING:
+ builder.putString(key,
+ MediaMetadataCompatApi21.getString(metadataObj, key));
+ break;
+ }
+ }
+ }
+ MediaMetadataCompat metadata = builder.build();
+ metadata.mMetadataObj = metadataObj;
+ return metadata;
+ }
+
+ /**
+ * Gets the underlying framework {@link android.media.MediaMetadata} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @return An equivalent {@link android.media.MediaMetadata} object, or null if none.
+ */
+ public Object getMediaMetadata() {
+ if (mMetadataObj != null || Build.VERSION.SDK_INT < 21) {
+ return mMetadataObj;
+ }
+
+ Object builderObj = MediaMetadataCompatApi21.Builder.newInstance();
+ for (String key : keySet()) {
+ Integer type = METADATA_KEYS_TYPE.get(key);
+ if (type != null) {
+ switch (type) {
+ case METADATA_TYPE_BITMAP:
+ MediaMetadataCompatApi21.Builder.putBitmap(builderObj, key,
+ getBitmap(key));
+ break;
+ case METADATA_TYPE_LONG:
+ MediaMetadataCompatApi21.Builder.putLong(builderObj, key,
+ getLong(key));
+ break;
+ case METADATA_TYPE_RATING:
+ MediaMetadataCompatApi21.Builder.putRating(builderObj, key,
+ getRating(key).getRating());
+ break;
+ case METADATA_TYPE_STRING:
+ MediaMetadataCompatApi21.Builder.putString(builderObj, key,
+ getString(key));
+ break;
+ }
+ }
+ }
+ mMetadataObj = MediaMetadataCompatApi21.Builder.build(builderObj);
+ return mMetadataObj;
+ }
+
+ public static final Parcelable.Creator<MediaMetadataCompat> CREATOR =
+ new Parcelable.Creator<MediaMetadataCompat>() {
+ @Override
+ public MediaMetadataCompat createFromParcel(Parcel in) {
+ return new MediaMetadataCompat(in);
+ }
+
+ @Override
+ public MediaMetadataCompat[] newArray(int size) {
+ return new MediaMetadataCompat[size];
+ }
+ };
+
+ /**
+ * Use to build MediaMetadata objects. The system defined metadata keys must
+ * use the appropriate data type.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+
+ /**
+ * Create an empty Builder. Any field that should be included in the
+ * {@link MediaMetadataCompat} must be added.
+ */
+ public Builder() {
+ mBundle = new Bundle();
+ }
+
+ /**
+ * Create a Builder using a {@link MediaMetadataCompat} instance to set the
+ * initial values. All fields in the source metadata will be included in
+ * the new metadata. Fields can be overwritten by adding the same key.
+ *
+ * @param source
+ */
+ public Builder(MediaMetadataCompat source) {
+ mBundle = new Bundle(source.mBundle);
+ }
+
+ /**
+ * Put a String value into the metadata. Custom keys may be used, but if
+ * the METADATA_KEYs defined in this class are used they may only be one
+ * of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ALBUM}</li>
+ * <li>{@link #METADATA_KEY_AUTHOR}</li>
+ * <li>{@link #METADATA_KEY_WRITER}</li>
+ * <li>{@link #METADATA_KEY_COMPOSER}</li>
+ * <li>{@link #METADATA_KEY_DATE}</li>
+ * <li>{@link #METADATA_KEY_GENRE}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putString(String key, String value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_STRING) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a String");
+ }
+ }
+ mBundle.putString(key, value);
+ return this;
+ }
+
+ /**
+ * Put a long value into the metadata. Custom keys may be used, but if
+ * the METADATA_KEYs defined in this class are used they may only be one
+ * of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_DURATION}</li>
+ * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li>
+ * <li>{@link #METADATA_KEY_NUM_TRACKS}</li>
+ * <li>{@link #METADATA_KEY_DISC_NUMBER}</li>
+ * <li>{@link #METADATA_KEY_YEAR}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putLong(String key, long value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a long");
+ }
+ }
+ mBundle.putLong(key, value);
+ return this;
+ }
+
+ /**
+ * Put a {@link RatingCompat} into the metadata. Custom keys may be used, but
+ * if the METADATA_KEYs defined in this class are used they may only be
+ * one of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_RATING}</li>
+ * <li>{@link #METADATA_KEY_USER_RATING}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putRating(String key, RatingCompat value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a Rating");
+ }
+ }
+ mBundle.putParcelable(key, value);
+ return this;
+ }
+
+ /**
+ * Put a {@link Bitmap} into the metadata. Custom keys may be used, but
+ * if the METADATA_KEYs defined in this class are used they may only be
+ * one of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_ART}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The Bitmap to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putBitmap(String key, Bitmap value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a Bitmap");
+ }
+ }
+ mBundle.putParcelable(key, value);
+ return this;
+ }
+
+ /**
+ * Creates a {@link MediaMetadataCompat} instance with the specified fields.
+ *
+ * @return The new MediaMetadata instance
+ */
+ public MediaMetadataCompat build() {
+ return new MediaMetadataCompat(mBundle);
+ }
+ }
+
+}
diff --git a/v4/java/android/support/v4/media/RatingCompat.java b/v4/java/android/support/v4/media/RatingCompat.java
new file mode 100644
index 0000000..c7da6ec
--- /dev/null
+++ b/v4/java/android/support/v4/media/RatingCompat.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media;
+
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+/**
+ * A class to encapsulate rating information used as content metadata.
+ * A rating is defined by its rating style (see {@link #RATING_HEART},
+ * {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
+ * {@link #RATING_5_STARS} or {@link #RATING_PERCENTAGE}) and the actual rating value (which may
+ * be defined as "unrated"), both of which are defined when the rating instance is constructed
+ * through one of the factory methods.
+ */
+public final class RatingCompat implements Parcelable {
+ private final static String TAG = "Rating";
+
+ /**
+ * Indicates a rating style is not supported. A Rating will never have this
+ * type, but can be used by other classes to indicate they do not support
+ * Rating.
+ */
+ public final static int RATING_NONE = 0;
+
+ /**
+ * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to
+ * indicate the content referred to is a favorite (or not).
+ */
+ public final static int RATING_HEART = 1;
+
+ /**
+ * A rating style for "thumb up" vs "thumb down".
+ */
+ public final static int RATING_THUMB_UP_DOWN = 2;
+
+ /**
+ * A rating style with 0 to 3 stars.
+ */
+ public final static int RATING_3_STARS = 3;
+
+ /**
+ * A rating style with 0 to 4 stars.
+ */
+ public final static int RATING_4_STARS = 4;
+
+ /**
+ * A rating style with 0 to 5 stars.
+ */
+ public final static int RATING_5_STARS = 5;
+
+ /**
+ * A rating style expressed as a percentage.
+ */
+ public final static int RATING_PERCENTAGE = 6;
+
+ private final static float RATING_NOT_RATED = -1.0f;
+
+ private final int mRatingStyle;
+ private final float mRatingValue;
+
+ private Object mRatingObj; // framework Rating object
+
+ private RatingCompat(int ratingStyle, float rating) {
+ mRatingStyle = ratingStyle;
+ mRatingValue = rating;
+ }
+
+ @Override
+ public String toString() {
+ return "Rating:style=" + mRatingStyle + " rating="
+ + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue));
+ }
+
+ @Override
+ public int describeContents() {
+ return mRatingStyle;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mRatingStyle);
+ dest.writeFloat(mRatingValue);
+ }
+
+ public static final Parcelable.Creator<RatingCompat> CREATOR
+ = new Parcelable.Creator<RatingCompat>() {
+ /**
+ * Rebuilds a Rating previously stored with writeToParcel().
+ * @param p Parcel object to read the Rating from
+ * @return a new Rating created from the data in the parcel
+ */
+ @Override
+ public RatingCompat createFromParcel(Parcel p) {
+ return new RatingCompat(p.readInt(), p.readFloat());
+ }
+
+ @Override
+ public RatingCompat[] newArray(int size) {
+ return new RatingCompat[size];
+ }
+ };
+
+ /**
+ * Return a Rating instance with no rating.
+ * Create and return a new Rating instance with no rating known for the given
+ * rating style.
+ * @param ratingStyle one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN},
+ * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
+ * or {@link #RATING_PERCENTAGE}.
+ * @return null if an invalid rating style is passed, a new Rating instance otherwise.
+ */
+ public static RatingCompat newUnratedRating(int ratingStyle) {
+ switch(ratingStyle) {
+ case RATING_HEART:
+ case RATING_THUMB_UP_DOWN:
+ case RATING_3_STARS:
+ case RATING_4_STARS:
+ case RATING_5_STARS:
+ case RATING_PERCENTAGE:
+ return new RatingCompat(ratingStyle, RATING_NOT_RATED);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Return a Rating instance with a heart-based rating.
+ * Create and return a new Rating instance with a rating style of {@link #RATING_HEART},
+ * and a heart-based rating.
+ * @param hasHeart true for a "heart selected" rating, false for "heart unselected".
+ * @return a new Rating instance.
+ */
+ public static RatingCompat newHeartRating(boolean hasHeart) {
+ return new RatingCompat(RATING_HEART, hasHeart ? 1.0f : 0.0f);
+ }
+
+ /**
+ * Return a Rating instance with a thumb-based rating.
+ * Create and return a new Rating instance with a {@link #RATING_THUMB_UP_DOWN}
+ * rating style, and a "thumb up" or "thumb down" rating.
+ * @param thumbIsUp true for a "thumb up" rating, false for "thumb down".
+ * @return a new Rating instance.
+ */
+ public static RatingCompat newThumbRating(boolean thumbIsUp) {
+ return new RatingCompat(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f);
+ }
+
+ /**
+ * Return a Rating instance with a star-based rating.
+ * Create and return a new Rating instance with one of the star-base rating styles
+ * and the given integer or fractional number of stars. Non integer values can for instance
+ * be used to represent an average rating value, which might not be an integer number of stars.
+ * @param starRatingStyle one of {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
+ * {@link #RATING_5_STARS}.
+ * @param starRating a number ranging from 0.0f to 3.0f, 4.0f or 5.0f according to
+ * the rating style.
+ * @return null if the rating style is invalid, or the rating is out of range,
+ * a new Rating instance otherwise.
+ */
+ public static RatingCompat newStarRating(int starRatingStyle, float starRating) {
+ float maxRating = -1.0f;
+ switch(starRatingStyle) {
+ case RATING_3_STARS:
+ maxRating = 3.0f;
+ break;
+ case RATING_4_STARS:
+ maxRating = 4.0f;
+ break;
+ case RATING_5_STARS:
+ maxRating = 5.0f;
+ break;
+ default:
+ Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating");
+ return null;
+ }
+ if ((starRating < 0.0f) || (starRating > maxRating)) {
+ Log.e(TAG, "Trying to set out of range star-based rating");
+ return null;
+ }
+ return new RatingCompat(starRatingStyle, starRating);
+ }
+
+ /**
+ * Return a Rating instance with a percentage-based rating.
+ * Create and return a new Rating instance with a {@link #RATING_PERCENTAGE}
+ * rating style, and a rating of the given percentage.
+ * @param percent the value of the rating
+ * @return null if the rating is out of range, a new Rating instance otherwise.
+ */
+ public static RatingCompat newPercentageRating(float percent) {
+ if ((percent < 0.0f) || (percent > 100.0f)) {
+ Log.e(TAG, "Invalid percentage-based rating value");
+ return null;
+ } else {
+ return new RatingCompat(RATING_PERCENTAGE, percent);
+ }
+ }
+
+ /**
+ * Return whether there is a rating value available.
+ * @return true if the instance was not created with {@link #newUnratedRating(int)}.
+ */
+ public boolean isRated() {
+ return mRatingValue >= 0.0f;
+ }
+
+ /**
+ * Return the rating style.
+ * @return one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN},
+ * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
+ * or {@link #RATING_PERCENTAGE}.
+ */
+ public int getRatingStyle() {
+ return mRatingStyle;
+ }
+
+ /**
+ * Return whether the rating is "heart selected".
+ * @return true if the rating is "heart selected", false if the rating is "heart unselected",
+ * if the rating style is not {@link #RATING_HEART} or if it is unrated.
+ */
+ public boolean hasHeart() {
+ if (mRatingStyle != RATING_HEART) {
+ return false;
+ } else {
+ return (mRatingValue == 1.0f);
+ }
+ }
+
+ /**
+ * Return whether the rating is "thumb up".
+ * @return true if the rating is "thumb up", false if the rating is "thumb down",
+ * if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated.
+ */
+ public boolean isThumbUp() {
+ if (mRatingStyle != RATING_THUMB_UP_DOWN) {
+ return false;
+ } else {
+ return (mRatingValue == 1.0f);
+ }
+ }
+
+ /**
+ * Return the star-based rating value.
+ * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is
+ * not star-based, or if it is unrated.
+ */
+ public float getStarRating() {
+ switch (mRatingStyle) {
+ case RATING_3_STARS:
+ case RATING_4_STARS:
+ case RATING_5_STARS:
+ if (isRated()) {
+ return mRatingValue;
+ }
+ default:
+ return -1.0f;
+ }
+ }
+
+ /**
+ * Return the percentage-based rating value.
+ * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is
+ * not percentage-based, or if it is unrated.
+ */
+ public float getPercentRating() {
+ if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) {
+ return -1.0f;
+ } else {
+ return mRatingValue;
+ }
+ }
+
+ /**
+ * Creates an instance from a framework {@link android.media.Rating} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @param ratingObj A {@link android.media.Rating} object, or null if none.
+ * @return An equivalent {@link RatingCompat} object, or null if none.
+ */
+ public static RatingCompat fromRating(Object ratingObj) {
+ if (ratingObj == null || Build.VERSION.SDK_INT < 21) {
+ return null;
+ }
+
+ final int ratingStyle = RatingCompatApi21.getRatingStyle(ratingObj);
+ final RatingCompat rating;
+ if (RatingCompatApi21.isRated(ratingObj)) {
+ switch (ratingStyle) {
+ case RATING_HEART:
+ rating = newHeartRating(RatingCompatApi21.hasHeart(ratingObj));
+ break;
+ case RATING_THUMB_UP_DOWN:
+ rating = newThumbRating(RatingCompatApi21.isThumbUp(ratingObj));
+ break;
+ case RATING_3_STARS:
+ case RATING_4_STARS:
+ case RATING_5_STARS:
+ rating = newStarRating(ratingStyle,
+ RatingCompatApi21.getStarRating(ratingObj));
+ break;
+ case RATING_PERCENTAGE:
+ rating = newPercentageRating(RatingCompatApi21.getPercentRating(ratingObj));
+ break;
+ default:
+ return null;
+ }
+ } else {
+ rating = newUnratedRating(ratingStyle);
+ }
+ rating.mRatingObj = ratingObj;
+ return rating;
+ }
+
+ /**
+ * Gets the underlying framework {@link android.media.Rating} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @return An equivalent {@link android.media.Rating} object, or null if none.
+ */
+ public Object getRating() {
+ if (mRatingObj != null || Build.VERSION.SDK_INT < 21) {
+ return mRatingObj;
+ }
+
+ if (isRated()) {
+ switch (mRatingStyle) {
+ case RATING_HEART:
+ mRatingObj = RatingCompatApi21.newHeartRating(hasHeart());
+ break;
+ case RATING_THUMB_UP_DOWN:
+ mRatingObj = RatingCompatApi21.newThumbRating(isThumbUp());
+ break;
+ case RATING_3_STARS:
+ case RATING_4_STARS:
+ case RATING_5_STARS:
+ mRatingObj = RatingCompatApi21.newStarRating(mRatingStyle, getStarRating());
+ break;
+ case RATING_PERCENTAGE:
+ mRatingObj = RatingCompatApi21.newPercentageRating(getPercentRating());
+ default:
+ return null;
+ }
+ } else {
+ mRatingObj = RatingCompatApi21.newUnratedRating(mRatingStyle);
+ }
+ return mRatingObj;
+ }
+}
\ No newline at end of file
diff --git a/v4/java/android/support/v4/media/TransportMediator.java b/v4/java/android/support/v4/media/TransportMediator.java
index 789ea7b..16bf94b 100644
--- a/v4/java/android/support/v4/media/TransportMediator.java
+++ b/v4/java/android/support/v4/media/TransportMediator.java
@@ -84,28 +84,28 @@
public static final int KEYCODE_MEDIA_RECORD = 130;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS
- * RemoveControlClient.FLAG_KEY_MEDIA_PREVIOUS */
+ * RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS */
public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND
- * RemoveControlClient.FLAG_KEY_MEDIA_REWIND */
+ * RemoteControlClient.FLAG_KEY_MEDIA_REWIND */
public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY
- * RemoveControlClient.FLAG_KEY_MEDIA_PLAY */
+ * RemoteControlClient.FLAG_KEY_MEDIA_PLAY */
public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE
- * RemoveControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE */
+ * RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE */
public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE
- * RemoveControlClient.FLAG_KEY_MEDIA_PAUSE */
+ * RemoteControlClient.FLAG_KEY_MEDIA_PAUSE */
public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP
- * RemoveControlClient.FLAG_KEY_MEDIA_STOP */
+ * RemoteControlClient.FLAG_KEY_MEDIA_STOP */
public final static int FLAG_KEY_MEDIA_STOP = 1 << 5;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD
- * RemoveControlClient.FLAG_KEY_MEDIA_FAST_FORWARD */
+ * RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD */
public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6;
/** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT
- * RemoveControlClient.FLAG_KEY_MEDIA_NEXT */
+ * RemoteControlClient.FLAG_KEY_MEDIA_NEXT */
public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7;
static boolean isMediaKey(int keyCode) {
diff --git a/v4/java/android/support/v4/media/VolumeProviderCompat.java b/v4/java/android/support/v4/media/VolumeProviderCompat.java
new file mode 100644
index 0000000..24f44ed
--- /dev/null
+++ b/v4/java/android/support/v4/media/VolumeProviderCompat.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media;
+
+import android.os.Build;
+import android.support.v4.media.session.MediaSessionCompat;
+
+/**
+ * Handles requests to adjust or set the volume on a session. This is also used
+ * to push volume updates back to the session after a request has been handled.
+ * You can set a volume provider on a session by calling
+ * {@link MediaSessionCompat#setPlaybackToRemote}.
+ */
+public abstract class VolumeProviderCompat {
+ /**
+ * The volume is fixed and can not be modified. Requests to change volume
+ * should be ignored.
+ */
+ public static final int VOLUME_CONTROL_FIXED = 0;
+
+ /**
+ * The volume control uses relative adjustment via
+ * {@link #onAdjustVolumeBy(int)}. Attempts to set the volume to a specific
+ * value should be ignored.
+ */
+ public static final int VOLUME_CONTROL_RELATIVE = 1;
+
+ /**
+ * The volume control uses an absolute value. It may be adjusted using
+ * {@link #onAdjustVolumeBy(int)} or set directly using
+ * {@link #onSetVolumeTo(int)}.
+ */
+ public static final int VOLUME_CONTROL_ABSOLUTE = 2;
+
+ private final int mControlType;
+ private final int mMaxVolume;
+ private Callback mCallback;
+
+ private Object mVolumeProviderObj;
+
+ /**
+ * Create a new volume provider for handling volume events. You must specify
+ * the type of volume control and the maximum volume that can be used.
+ *
+ * @param volumeControl The method for controlling volume that is used by
+ * this provider.
+ * @param maxVolume The maximum allowed volume.
+ */
+ public VolumeProviderCompat(int volumeControl, int maxVolume) {
+ mControlType = volumeControl;
+ mMaxVolume = maxVolume;
+ }
+
+ /**
+ * Get the current volume of the remote playback.
+ *
+ * @return The current volume.
+ */
+ public abstract int onGetCurrentVolume();
+
+ /**
+ * Get the volume control type that this volume provider uses.
+ *
+ * @return The volume control type for this volume provider
+ */
+ public final int getVolumeControl() {
+ return mControlType;
+ }
+
+ /**
+ * Get the maximum volume this provider allows.
+ *
+ * @return The max allowed volume.
+ */
+ public final int getMaxVolume() {
+ return mMaxVolume;
+ }
+
+ /**
+ * Notify the callback that the remote playback's volume has been changed.
+ */
+ public final void notifyVolumeChanged() {
+ if (mCallback != null) {
+ mCallback.onVolumeChanged(this);
+ }
+ }
+
+ /**
+ * Override to handle requests to set the volume of the current output.
+ *
+ * @param volume The volume to set the output to.
+ */
+ public void onSetVolumeTo(int volume) {
+ }
+
+ /**
+ * Override to handle requests to adjust the volume of the current
+ * output.
+ *
+ * @param delta The amount to change the volume
+ */
+ public void onAdjustVolumeBy(int delta) {
+ }
+
+ /**
+ * Sets a callback to receive volume changes.
+ * <p>
+ * Used internally by the support library.
+ * <p>
+ */
+ public void setCallback(Callback callback) {
+ mCallback = callback;
+ }
+
+ /**
+ * Gets the underlying framework {@link android.media.VolumeProvider} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @return An equivalent {@link android.media.VolumeProvider} object, or null if none.
+ */
+ public Object getVolumeProvider() {
+ if (mVolumeProviderObj != null || Build.VERSION.SDK_INT < 21) {
+ return mVolumeProviderObj;
+ }
+
+ mVolumeProviderObj = VolumeProviderCompatApi21.createVolumeProvider(
+ mControlType, mMaxVolume, new VolumeProviderCompatApi21.Delegate() {
+ @Override
+ public int onGetCurrentVolume() {
+ return VolumeProviderCompat.this.onGetCurrentVolume();
+ }
+
+ @Override
+ public void onSetVolumeTo(int volume) {
+ VolumeProviderCompat.this.onSetVolumeTo(volume);
+ }
+
+ @Override
+ public void onAdjustVolumeBy(int delta) {
+ VolumeProviderCompat.this.onAdjustVolumeBy(delta);
+ }
+ });
+ return mVolumeProviderObj;
+ }
+
+ /**
+ * Listens for changes to the volume.
+ */
+ public static abstract class Callback {
+ public abstract void onVolumeChanged(VolumeProviderCompat volumeProvider);
+ }
+}
\ No newline at end of file
diff --git a/v4/java/android/support/v4/media/session/MediaControllerCompat.java b/v4/java/android/support/v4/media/session/MediaControllerCompat.java
new file mode 100644
index 0000000..0b75e08
--- /dev/null
+++ b/v4/java/android/support/v4/media/session/MediaControllerCompat.java
@@ -0,0 +1,617 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.session;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.text.TextUtils;
+import android.view.KeyEvent;
+
+/**
+ * Allows an app to interact with an ongoing media session. Media buttons and
+ * other commands can be sent to the session. A callback may be registered to
+ * receive updates from the session, such as metadata and play state changes.
+ * <p>
+ * A MediaController can be created if you have a {@link MediaSessionCompat.Token}
+ * from the session owner.
+ * <p>
+ * MediaController objects are thread-safe.
+ * <p>
+ * This is a helper for accessing features in {@link android.media.session.MediaSession}
+ * introduced after API level 4 in a backwards compatible fashion.
+ */
+public final class MediaControllerCompat {
+ private final MediaControllerImpl mImpl;
+
+ /**
+ * Creates a media controller from a session.
+ *
+ * @param session The session to be controlled.
+ */
+ public MediaControllerCompat(Context context, MediaSessionCompat session) {
+ if (session == null) {
+ throw new IllegalArgumentException("session must not be null");
+ }
+
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mImpl = new MediaControllerImplApi21(context, session);
+ } else {
+ mImpl = new MediaControllerImplBase();
+ }
+ }
+
+ /**
+ * Creates a media controller from a session token which may have
+ * been obtained from another process.
+ *
+ * @param sessionToken The token of the session to be controlled.
+ * @throws RemoteException if the session is not accessible.
+ */
+ public MediaControllerCompat(Context context, MediaSessionCompat.Token sessionToken)
+ throws RemoteException {
+ if (sessionToken == null) {
+ throw new IllegalArgumentException("sessionToken must not be null");
+ }
+
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mImpl = new MediaControllerImplApi21(context, sessionToken);
+ } else {
+ mImpl = new MediaControllerImplBase();
+ }
+ }
+
+ /**
+ * Get a {@link TransportControls} instance for this session.
+ *
+ * @return A controls instance
+ */
+ public TransportControls getTransportControls() {
+ return mImpl.getTransportControls();
+ }
+
+ /**
+ * Send the specified media button event to the session. Only media keys can
+ * be sent by this method, other keys will be ignored.
+ *
+ * @param keyEvent The media button event to dispatch.
+ * @return true if the event was sent to the session, false otherwise.
+ */
+ public boolean dispatchMediaButtonEvent(KeyEvent keyEvent) {
+ if (keyEvent == null) {
+ throw new IllegalArgumentException("KeyEvent may not be null");
+ }
+ return mImpl.dispatchMediaButtonEvent(keyEvent);
+ }
+
+ /**
+ * Get the current playback state for this session.
+ *
+ * @return The current PlaybackState or null
+ */
+ public PlaybackStateCompat getPlaybackState() {
+ return mImpl.getPlaybackState();
+ }
+
+ /**
+ * Get the current metadata for this session.
+ *
+ * @return The current MediaMetadata or null.
+ */
+ public MediaMetadataCompat getMetadata() {
+ return mImpl.getMetadata();
+ }
+
+ /**
+ * Get the rating type supported by the session. One of:
+ * <ul>
+ * <li>{@link RatingCompat#RATING_NONE}</li>
+ * <li>{@link RatingCompat#RATING_HEART}</li>
+ * <li>{@link RatingCompat#RATING_THUMB_UP_DOWN}</li>
+ * <li>{@link RatingCompat#RATING_3_STARS}</li>
+ * <li>{@link RatingCompat#RATING_4_STARS}</li>
+ * <li>{@link RatingCompat#RATING_5_STARS}</li>
+ * <li>{@link RatingCompat#RATING_PERCENTAGE}</li>
+ * </ul>
+ *
+ * @return The supported rating type
+ */
+ public int getRatingType() {
+ return mImpl.getRatingType();
+ }
+
+ /**
+ * Get the current volume info for this session.
+ *
+ * @return The current volume info or null.
+ */
+ public VolumeInfo getVolumeInfo() {
+ return mImpl.getVolumeInfo();
+ }
+
+ /**
+ * Adds a callback to receive updates from the Session. Updates will be
+ * posted on the caller's thread.
+ *
+ * @param callback The callback object, must not be null.
+ */
+ public void addCallback(Callback callback) {
+ addCallback(callback, null);
+ }
+
+ /**
+ * Adds a callback to receive updates from the session. Updates will be
+ * posted on the specified handler's thread.
+ *
+ * @param callback The callback object, must not be null.
+ * @param handler The handler to post updates on. If null the callers thread
+ * will be used.
+ */
+ public void addCallback(Callback callback, Handler handler) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null");
+ }
+ if (handler == null) {
+ handler = new Handler();
+ }
+ mImpl.addCallback(callback, handler);
+ }
+
+ /**
+ * Stop receiving updates on the specified callback. If an update has
+ * already been posted you may still receive it after calling this method.
+ *
+ * @param callback The callback to remove
+ */
+ public void removeCallback(Callback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback cannot be null");
+ }
+ mImpl.removeCallback(callback);
+ }
+
+ /**
+ * Sends a generic command to the session. It is up to the session creator
+ * to decide what commands and parameters they will support. As such,
+ * commands should only be sent to sessions that the controller owns.
+ *
+ * @param command The command to send
+ * @param params Any parameters to include with the command
+ * @param cb The callback to receive the result on
+ */
+ public void sendControlCommand(String command, Bundle params, ResultReceiver cb) {
+ if (TextUtils.isEmpty(command)) {
+ throw new IllegalArgumentException("command cannot be null or empty");
+ }
+ mImpl.sendControlCommand(command, params, cb);
+ }
+
+ /**
+ * Gets the underlying framework {@link android.media.session.MediaController} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @return The underlying {@link android.media.session.MediaController} object,
+ * or null if none.
+ */
+ public Object getMediaController() {
+ return mImpl.getMediaController();
+ }
+
+ /**
+ * Callback for receiving updates on from the session. A Callback can be
+ * registered using {@link #addCallback}
+ */
+ public static abstract class Callback {
+ final Object mCallbackObj;
+
+ public Callback() {
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21());
+ } else {
+ mCallbackObj = null;
+ }
+ }
+
+ /**
+ * Override to handle custom events sent by the session owner without a
+ * specified interface. Controllers should only handle these for
+ * sessions they own.
+ *
+ * @param event The event from the session.
+ * @param extras Optional parameters for the event.
+ */
+ public void onSessionEvent(String event, Bundle extras) {
+ }
+
+ /**
+ * Override to handle changes in playback state.
+ *
+ * @param state The new playback state of the session
+ */
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ }
+
+ /**
+ * Override to handle changes to the current metadata.
+ *
+ * @param metadata The current metadata for the session or null if none.
+ * @see MediaMetadata
+ */
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ }
+
+ private class StubApi21 implements MediaControllerCompatApi21.Callback {
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ Callback.this.onSessionEvent(event, extras);
+ }
+
+ @Override
+ public void onPlaybackStateChanged(Object stateObj) {
+ Callback.this.onPlaybackStateChanged(
+ PlaybackStateCompat.fromPlaybackState(stateObj));
+ }
+
+ @Override
+ public void onMetadataChanged(Object metadataObj) {
+ Callback.this.onMetadataChanged(
+ MediaMetadataCompat.fromMediaMetadata(metadataObj));
+ }
+ }
+ }
+
+ /**
+ * Interface for controlling media playback on a session. This allows an app
+ * to send media transport commands to the session.
+ */
+ public static abstract class TransportControls {
+ TransportControls() {
+ }
+
+ /**
+ * Request that the player start its playback at its current position.
+ */
+ public abstract void play();
+
+ /**
+ * Request that the player pause its playback and stay at its current
+ * position.
+ */
+ public abstract void pause();
+
+ /**
+ * Request that the player stop its playback; it may clear its state in
+ * whatever way is appropriate.
+ */
+ public abstract void stop();
+
+ /**
+ * Move to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
+ */
+ public abstract void seekTo(long pos);
+
+ /**
+ * Start fast forwarding. If playback is already fast forwarding this
+ * may increase the rate.
+ */
+ public abstract void fastForward();
+
+ /**
+ * Skip to the next item.
+ */
+ public abstract void skipToNext();
+
+ /**
+ * Start rewinding. If playback is already rewinding this may increase
+ * the rate.
+ */
+ public abstract void rewind();
+
+ /**
+ * Skip to the previous item.
+ */
+ public abstract void skipToPrevious();
+
+ /**
+ * Rate the current content. This will cause the rating to be set for
+ * the current user. The Rating type must match the type returned by
+ * {@link #getRatingType()}.
+ *
+ * @param rating The rating to set for the current content
+ */
+ public abstract void setRating(RatingCompat rating);
+ }
+
+ /**
+ * Holds information about the way volume is handled for this session.
+ */
+ public static final class VolumeInfo {
+ private final int mVolumeType;
+ private final int mAudioStream;
+ private final int mVolumeControl;
+ private final int mMaxVolume;
+ private final int mCurrentVolume;
+
+ VolumeInfo(int type, int stream, int control, int max, int current) {
+ mVolumeType = type;
+ mAudioStream = stream;
+ mVolumeControl = control;
+ mMaxVolume = max;
+ mCurrentVolume = current;
+ }
+
+ /**
+ * Get the type of volume handling, either local or remote. One of:
+ * <ul>
+ * <li>{@link MediaSessionCompat#VOLUME_TYPE_LOCAL}</li>
+ * <li>{@link MediaSessionCompat#VOLUME_TYPE_REMOTE}</li>
+ * </ul>
+ *
+ * @return The type of volume handling this session is using.
+ */
+ public int getVolumeType() {
+ return mVolumeType;
+ }
+
+ /**
+ * Get the stream this is currently controlling volume on. When the volume
+ * type is {@link MediaSessionCompat#VOLUME_TYPE_REMOTE} this value does not
+ * have meaning and should be ignored.
+ *
+ * @return The stream this session is playing on.
+ */
+ public int getAudioStream() {
+ return mAudioStream;
+ }
+
+ /**
+ * Get the type of volume control that can be used. One of:
+ * <ul>
+ * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}</li>
+ * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}</li>
+ * <li>{@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}</li>
+ * </ul>
+ *
+ * @return The type of volume control that may be used with this
+ * session.
+ */
+ public int getVolumeControl() {
+ return mVolumeControl;
+ }
+
+ /**
+ * Get the maximum volume that may be set for this session.
+ *
+ * @return The maximum allowed volume where this session is playing.
+ */
+ public int getMaxVolume() {
+ return mMaxVolume;
+ }
+
+ /**
+ * Get the current volume for this session.
+ *
+ * @return The current volume where this session is playing.
+ */
+ public int getCurrentVolume() {
+ return mCurrentVolume;
+ }
+ }
+
+ interface MediaControllerImpl {
+ void addCallback(Callback callback, Handler handler);
+ void removeCallback(Callback callback);
+ boolean dispatchMediaButtonEvent(KeyEvent keyEvent);
+ TransportControls getTransportControls();
+ PlaybackStateCompat getPlaybackState();
+ MediaMetadataCompat getMetadata();
+ int getRatingType();
+ VolumeInfo getVolumeInfo();
+ void sendControlCommand(String command, Bundle params, ResultReceiver cb);
+ Object getMediaController();
+ }
+
+ // TODO: compatibility implementation
+ static class MediaControllerImplBase implements MediaControllerImpl {
+ @Override
+ public void addCallback(Callback callback, Handler handler) {
+ }
+
+ @Override
+ public void removeCallback(Callback callback) {
+ }
+
+ @Override
+ public boolean dispatchMediaButtonEvent(KeyEvent event) {
+ return false;
+ }
+
+ @Override
+ public TransportControls getTransportControls() {
+ return null;
+ }
+
+ @Override
+ public PlaybackStateCompat getPlaybackState() {
+ return null;
+ }
+
+ @Override
+ public MediaMetadataCompat getMetadata() {
+ return null;
+ }
+
+ @Override
+ public int getRatingType() {
+ return 0;
+ }
+
+ @Override
+ public VolumeInfo getVolumeInfo() {
+ return null;
+ }
+
+ @Override
+ public void sendControlCommand(String command, Bundle params, ResultReceiver cb) {
+ }
+
+ @Override
+ public Object getMediaController() {
+ return null;
+ }
+ }
+
+ static class MediaControllerImplApi21 implements MediaControllerImpl {
+ private final Object mControllerObj;
+
+ public MediaControllerImplApi21(Context context, MediaSessionCompat session) {
+ mControllerObj = MediaControllerCompatApi21.fromToken(
+ session.getSessionToken().getToken());
+ }
+
+ public MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken)
+ throws RemoteException {
+ // TODO: refactor framework implementation
+ mControllerObj = MediaControllerCompatApi21.fromToken(
+ sessionToken.getToken());
+ if (mControllerObj == null) throw new RemoteException();
+ }
+
+ @Override
+ public void addCallback(Callback callback, Handler handler) {
+ MediaControllerCompatApi21.addCallback(mControllerObj, callback.mCallbackObj, handler);
+ }
+
+ @Override
+ public void removeCallback(Callback callback) {
+ MediaControllerCompatApi21.removeCallback(mControllerObj, callback.mCallbackObj);
+ }
+
+ @Override
+ public boolean dispatchMediaButtonEvent(KeyEvent event) {
+ return MediaControllerCompatApi21.dispatchMediaButtonEvent(mControllerObj, event);
+ }
+
+ @Override
+ public TransportControls getTransportControls() {
+ Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj);
+ return controlsObj != null ? new TransportControlsApi21(controlsObj) : null;
+ }
+
+ @Override
+ public PlaybackStateCompat getPlaybackState() {
+ Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj);
+ return stateObj != null ? PlaybackStateCompat.fromPlaybackState(stateObj) : null;
+ }
+
+ @Override
+ public MediaMetadataCompat getMetadata() {
+ Object metadataObj = MediaControllerCompatApi21.getMetadata(mControllerObj);
+ return metadataObj != null ? MediaMetadataCompat.fromMediaMetadata(metadataObj) : null;
+ }
+
+ @Override
+ public int getRatingType() {
+ return MediaControllerCompatApi21.getRatingType(mControllerObj);
+ }
+
+ @Override
+ public VolumeInfo getVolumeInfo() {
+ Object volumeInfoObj = MediaControllerCompatApi21.getVolumeInfo(mControllerObj);
+ return volumeInfoObj != null ? new VolumeInfo(
+ MediaControllerCompatApi21.VolumeInfo.getVolumeType(volumeInfoObj),
+ MediaControllerCompatApi21.VolumeInfo.getAudioStream(volumeInfoObj),
+ MediaControllerCompatApi21.VolumeInfo.getVolumeControl(volumeInfoObj),
+ MediaControllerCompatApi21.VolumeInfo.getMaxVolume(volumeInfoObj),
+ MediaControllerCompatApi21.VolumeInfo.getCurrentVolume(volumeInfoObj)) : null;
+ }
+
+ @Override
+ public void sendControlCommand(String command, Bundle params, ResultReceiver cb) {
+ MediaControllerCompatApi21.sendControlCommand(mControllerObj,
+ command, params, cb);
+ }
+
+ @Override
+ public Object getMediaController() {
+ return mControllerObj;
+ }
+ }
+
+ static class TransportControlsApi21 extends TransportControls {
+ private final Object mControlsObj;
+
+ public TransportControlsApi21(Object controlsObj) {
+ mControlsObj = controlsObj;
+ }
+
+ @Override
+ public void play() {
+ MediaControllerCompatApi21.TransportControls.play(mControlsObj);
+ }
+
+ @Override
+ public void pause() {
+ MediaControllerCompatApi21.TransportControls.pause(mControlsObj);
+ }
+
+ @Override
+ public void stop() {
+ MediaControllerCompatApi21.TransportControls.stop(mControlsObj);
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ MediaControllerCompatApi21.TransportControls.seekTo(mControlsObj, pos);
+ }
+
+ @Override
+ public void fastForward() {
+ MediaControllerCompatApi21.TransportControls.fastForward(mControlsObj);
+ }
+
+ @Override
+ public void rewind() {
+ MediaControllerCompatApi21.TransportControls.rewind(mControlsObj);
+ }
+
+ @Override
+ public void skipToNext() {
+ MediaControllerCompatApi21.TransportControls.skipToNext(mControlsObj);
+ }
+
+ @Override
+ public void skipToPrevious() {
+ MediaControllerCompatApi21.TransportControls.skipToPrevious(mControlsObj);
+ }
+
+ @Override
+ public void setRating(RatingCompat rating) {
+ MediaControllerCompatApi21.TransportControls.setRating(mControlsObj,
+ rating != null ? rating.getRating() : null);
+ }
+ }
+}
diff --git a/v4/java/android/support/v4/media/session/MediaSessionCompat.java b/v4/java/android/support/v4/media/session/MediaSessionCompat.java
new file mode 100644
index 0000000..afd6aac
--- /dev/null
+++ b/v4/java/android/support/v4/media/session/MediaSessionCompat.java
@@ -0,0 +1,711 @@
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.session;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.RatingCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.text.TextUtils;
+
+/**
+ * Allows interaction with media controllers, volume keys, media buttons, and
+ * transport controls.
+ * <p>
+ * A MediaSession should be created when an app wants to publish media playback
+ * information or handle media keys. In general an app only needs one session
+ * for all playback, though multiple sessions can be created to provide finer
+ * grain controls of media.
+ * <p>
+ * Once a session is created the owner of the session may pass its
+ * {@link #getSessionToken() session token} to other processes to allow them to
+ * create a {@link MediaControllerCompat} to interact with the session.
+ * <p>
+ * To receive commands, media keys, and other events a {@link Callback} must be
+ * set with {@link #addCallback(Callback)}. To receive transport control
+ * commands a {@link TransportControlsCallback} must be set with
+ * {@link #addTransportControlsCallback}.
+ * <p>
+ * When an app is finished performing playback it must call {@link #release()}
+ * to clean up the session and notify any controllers.
+ * <p>
+ * MediaSession objects are thread safe.
+ * <p>
+ * This is a helper for accessing features in {@link android.media.session.MediaSession}
+ * introduced after API level 4 in a backwards compatible fashion.
+ */
+public class MediaSessionCompat {
+ private final MediaSessionImpl mImpl;
+
+ /**
+ * Set this flag on the session to indicate that it can handle media button
+ * events.
+ */
+ public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0;
+
+ /**
+ * Set this flag on the session to indicate that it handles transport
+ * control commands through a {@link TransportControlsCallback}.
+ * The callback can be retrieved by calling {@link #addTransportControlsCallback}.
+ */
+ public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1;
+
+ /**
+ * The session uses local playback.
+ */
+ public static final int VOLUME_TYPE_LOCAL = 1;
+
+ /**
+ * The session uses remote playback.
+ */
+ public static final int VOLUME_TYPE_REMOTE = 2;
+
+ /**
+ * Creates a new session.
+ *
+ * @param context The context.
+ * @param tag A short name for debugging purposes.
+ */
+ public MediaSessionCompat(Context context, String tag) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+ if (TextUtils.isEmpty(tag)) {
+ throw new IllegalArgumentException("tag must not be null or empty");
+ }
+
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mImpl = new MediaSessionImplApi21(context, tag);
+ } else {
+ mImpl = new MediaSessionImplBase();
+ }
+ }
+
+ /**
+ * Add a callback to receive updates on for the MediaSession. This includes
+ * media button and volume events. The caller's thread will be used to post
+ * events.
+ *
+ * @param callback The callback object
+ */
+ public void addCallback(Callback callback) {
+ addCallback(callback, null);
+ }
+
+ /**
+ * Add a callback to receive updates for the MediaSession. This includes
+ * media button and volume events.
+ *
+ * @param callback The callback to receive updates on.
+ * @param handler The handler that events should be posted on.
+ */
+ public void addCallback(Callback callback, Handler handler) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ mImpl.addCallback(callback, handler != null ? handler : new Handler());
+ }
+
+ /**
+ * Remove a callback. It will no longer receive updates.
+ *
+ * @param callback The callback to remove.
+ */
+ public void removeCallback(Callback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ mImpl.removeCallback(callback);
+ }
+
+ /**
+ * Set any flags for the session.
+ *
+ * @param flags The flags to set for this session.
+ */
+ public void setFlags(int flags) {
+ mImpl.setFlags(flags);
+ }
+
+ /**
+ * Set the stream this session is playing on. This will affect the system's
+ * volume handling for this session. If {@link #setPlaybackToRemote} was
+ * previously called it will stop receiving volume commands and the system
+ * will begin sending volume changes to the appropriate stream.
+ * <p>
+ * By default sessions are on {@link AudioManager#STREAM_MUSIC}.
+ *
+ * @param stream The {@link AudioManager} stream this session is playing on.
+ */
+ public void setPlaybackToLocal(int stream) {
+ mImpl.setPlaybackToLocal(stream);
+ }
+
+ /**
+ * Configure this session to use remote volume handling. This must be called
+ * to receive volume button events, otherwise the system will adjust the
+ * current stream volume for this session. If {@link #setPlaybackToLocal}
+ * was previously called that stream will stop receiving volume changes for
+ * this session.
+ *
+ * @param volumeProvider The provider that will handle volume changes. May
+ * not be null.
+ */
+ public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) {
+ if (volumeProvider == null) {
+ throw new IllegalArgumentException("volumeProvider may not be null!");
+ }
+ mImpl.setPlaybackToRemote(volumeProvider);
+ }
+
+ /**
+ * Set if this session is currently active and ready to receive commands. If
+ * set to false your session's controller may not be discoverable. You must
+ * set the session to active before it can start receiving media button
+ * events or transport commands.
+ *
+ * @param active Whether this session is active or not.
+ */
+ public void setActive(boolean active) {
+ mImpl.setActive(active);
+ }
+
+ /**
+ * Get the current active state of this session.
+ *
+ * @return True if the session is active, false otherwise.
+ */
+ public boolean isActive() {
+ return mImpl.isActive();
+ }
+
+ /**
+ * Send a proprietary event to all MediaControllers listening to this
+ * Session. It's up to the Controller/Session owner to determine the meaning
+ * of any events.
+ *
+ * @param event The name of the event to send
+ * @param extras Any extras included with the event
+ */
+ public void sendSessionEvent(String event, Bundle extras) {
+ if (TextUtils.isEmpty(event)) {
+ throw new IllegalArgumentException("event cannot be null or empty");
+ }
+ mImpl.sendSessionEvent(event, extras);
+ }
+
+ /**
+ * This must be called when an app has finished performing playback. If
+ * playback is expected to start again shortly the session can be left open,
+ * but it must be released if your activity or service is being destroyed.
+ */
+ public void release() {
+ mImpl.release();
+ }
+
+ /**
+ * Retrieve a token object that can be used by apps to create a
+ * {@link MediaControllerCompat} for interacting with this session. The owner of
+ * the session is responsible for deciding how to distribute these tokens.
+ *
+ * @return A token that can be used to create a MediaController for this
+ * session.
+ */
+ public Token getSessionToken() {
+ return mImpl.getSessionToken();
+ }
+
+ /**
+ * Add a callback to receive transport controls on, such as play, rewind, or
+ * fast forward.
+ *
+ * @param callback The callback object.
+ */
+ public void addTransportControlsCallback(TransportControlsCallback callback) {
+ addTransportControlsCallback(callback, null);
+ }
+
+ /**
+ * Add a callback to receive transport controls on, such as play, rewind, or
+ * fast forward. The updates will be posted to the specified handler. If no
+ * handler is provided they will be posted to the caller's thread.
+ *
+ * @param callback The callback to receive updates on.
+ * @param handler The handler to post the updates on.
+ */
+ public void addTransportControlsCallback(TransportControlsCallback callback,
+ Handler handler) {
+ if (callback == null) {
+ throw new IllegalArgumentException("Callback cannot be null");
+ }
+ mImpl.addTransportControlsCallback(callback, handler != null ? handler : new Handler());
+ }
+
+ /**
+ * Stop receiving transport controls on the specified callback. If an update
+ * has already been posted you may still receive it after this call returns.
+ *
+ * @param callback The callback to stop receiving updates on.
+ */
+ public void removeTransportControlsCallback(TransportControlsCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("Callback cannot be null");
+ }
+ mImpl.removeTransportControlsCallback(callback);
+ }
+
+ /**
+ * Update the current playback state.
+ *
+ * @param state The current state of playback
+ */
+ public void setPlaybackState(PlaybackStateCompat state) {
+ mImpl.setPlaybackState(state);
+ }
+
+ /**
+ * Update the current metadata. New metadata can be created using
+ * {@link android.media.MediaMetadata.Builder}.
+ *
+ * @param metadata The new metadata
+ */
+ public void setMetadata(MediaMetadataCompat metadata) {
+ mImpl.setMetadata(metadata);
+ }
+
+ /**
+ * Gets the underlying framework {@link android.media.session.MediaSession} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @return The underlying {@link android.media.session.MediaSession} object,
+ * or null if none.
+ */
+ public Object getMediaSession() {
+ return mImpl.getMediaSession();
+ }
+
+ /**
+ * Receives generic commands or updates from controllers and the system.
+ * Callbacks may be registered using {@link #addCallback}.
+ */
+ public abstract static class Callback {
+ final Object mCallbackObj;
+
+ public Callback() {
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mCallbackObj = MediaSessionCompatApi21.createCallback(new StubApi21());
+ } else {
+ mCallbackObj = null;
+ }
+ }
+
+ /**
+ * Called when a media button is pressed and this session has the
+ * highest priority or a controller sends a media button event to the
+ * session. TODO determine if using Intents identical to the ones
+ * RemoteControlClient receives is useful
+ * <p>
+ * The intent will be of type {@link Intent#ACTION_MEDIA_BUTTON} with a
+ * KeyEvent in {@link Intent#EXTRA_KEY_EVENT}
+ *
+ * @param mediaButtonIntent an intent containing the KeyEvent as an
+ * extra
+ */
+ public void onMediaButtonEvent(Intent mediaButtonIntent) {
+ }
+
+ /**
+ * Called when a controller has sent a custom command to this session.
+ * The owner of the session may handle custom commands but is not
+ * required to.
+ *
+ * @param command The command name.
+ * @param extras Optional parameters for the command, may be null.
+ * @param cb A result receiver to which a result may be sent by the command, may be null.
+ */
+ public void onControlCommand(String command, Bundle extras, ResultReceiver cb) {
+ }
+
+ private class StubApi21 implements MediaSessionCompatApi21.Callback {
+ @Override
+ public void onMediaButtonEvent(Intent mediaButtonIntent) {
+ Callback.this.onMediaButtonEvent(mediaButtonIntent);
+ }
+
+ @Override
+ public void onControlCommand(
+ String command, Bundle extras, ResultReceiver cb) {
+ Callback.this.onControlCommand(command, extras, cb);
+ }
+ }
+ }
+
+ /**
+ * Receives transport control commands. Callbacks may be registered using
+ * {@link #addTransportControlsCallback}.
+ */
+ public static abstract class TransportControlsCallback {
+ final Object mCallbackObj;
+
+ public TransportControlsCallback() {
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mCallbackObj = MediaSessionCompatApi21.createTransportControlsCallback(
+ new StubApi21());
+ } else {
+ mCallbackObj = null;
+ }
+ }
+
+ /**
+ * Override to handle requests to begin playback.
+ */
+ public void onPlay() {
+ }
+
+ /**
+ * Override to handle requests to pause playback.
+ */
+ public void onPause() {
+ }
+
+ /**
+ * Override to handle requests to skip to the next media item.
+ */
+ public void onSkipToNext() {
+ }
+
+ /**
+ * Override to handle requests to skip to the previous media item.
+ */
+ public void onSkipToPrevious() {
+ }
+
+ /**
+ * Override to handle requests to fast forward.
+ */
+ public void onFastForward() {
+ }
+
+ /**
+ * Override to handle requests to rewind.
+ */
+ public void onRewind() {
+ }
+
+ /**
+ * Override to handle requests to stop playback.
+ */
+ public void onStop() {
+ }
+
+ /**
+ * Override to handle requests to seek to a specific position in ms.
+ *
+ * @param pos New position to move to, in milliseconds.
+ */
+ public void onSeekTo(long pos) {
+ }
+
+ /**
+ * Override to handle the item being rated.
+ *
+ * @param rating
+ */
+ public void onSetRating(RatingCompat rating) {
+ }
+
+ private class StubApi21 implements MediaSessionCompatApi21.TransportControlsCallback {
+ @Override
+ public void onPlay() {
+ TransportControlsCallback.this.onPlay();
+ }
+
+ @Override
+ public void onPause() {
+ TransportControlsCallback.this.onPause();
+ }
+
+ @Override
+ public void onSkipToNext() {
+ TransportControlsCallback.this.onSkipToNext();
+ }
+
+ @Override
+ public void onSkipToPrevious() {
+ TransportControlsCallback.this.onSkipToPrevious();
+ }
+
+ @Override
+ public void onFastForward() {
+ TransportControlsCallback.this.onFastForward();
+ }
+
+ @Override
+ public void onRewind() {
+ TransportControlsCallback.this.onRewind();
+ }
+
+ @Override
+ public void onStop() {
+ TransportControlsCallback.this.onStop();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ TransportControlsCallback.this.onSeekTo(pos);
+ }
+
+ @Override
+ public void onSetRating(Object ratingObj) {
+ TransportControlsCallback.this.onSetRating(RatingCompat.fromRating(ratingObj));
+ }
+ }
+ }
+
+ /**
+ * Represents an ongoing session. This may be passed to apps by the session
+ * owner to allow them to create a {@link MediaControllerCompat} to communicate with
+ * the session.
+ */
+ public static final class Token implements Parcelable {
+ private final Parcelable mInner;
+
+ Token(Parcelable inner) {
+ mInner = inner;
+ }
+
+ @Override
+ public int describeContents() {
+ return mInner.describeContents();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mInner, flags);
+ }
+
+ /**
+ * Gets the underlying framework {@link android.media.session.MediaSession.Token} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @return The underlying {@link android.media.session.MediaSession.Token} object,
+ * or null if none.
+ */
+ public Object getToken() {
+ return mInner;
+ }
+
+ public static final Parcelable.Creator<Token> CREATOR
+ = new Parcelable.Creator<Token>() {
+ @Override
+ public Token createFromParcel(Parcel in) {
+ return new Token(in.readParcelable(null));
+ }
+
+ @Override
+ public Token[] newArray(int size) {
+ return new Token[size];
+ }
+ };
+ }
+
+ interface MediaSessionImpl {
+ void addCallback(Callback callback, Handler handler);
+ void removeCallback(Callback callback);
+ void setFlags(int flags);
+ void setPlaybackToLocal(int stream);
+ void setPlaybackToRemote(VolumeProviderCompat volumeProvider);
+ void setActive(boolean active);
+ boolean isActive();
+ void sendSessionEvent(String event, Bundle extras);
+ void release();
+ Token getSessionToken();
+ void addTransportControlsCallback(TransportControlsCallback callback, Handler handler);
+ void removeTransportControlsCallback(TransportControlsCallback callback);
+ void setPlaybackState(PlaybackStateCompat state);
+ void setMetadata(MediaMetadataCompat metadata);
+ Object getMediaSession();
+ }
+
+ // TODO: compatibility implementation
+ static class MediaSessionImplBase implements MediaSessionImpl {
+ @Override
+ public void addCallback(Callback callback, Handler handler) {
+ }
+
+ @Override
+ public void removeCallback(Callback callback) {
+ }
+
+ @Override
+ public void setFlags(int flags) {
+ }
+
+ @Override
+ public void setPlaybackToLocal(int stream) {
+ }
+
+ @Override
+ public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) {
+ }
+
+ @Override
+ public void setActive(boolean active) {
+ }
+
+ @Override
+ public boolean isActive() {
+ return false;
+ }
+
+ @Override
+ public void sendSessionEvent(String event, Bundle extras) {
+ }
+
+ @Override
+ public void release() {
+ }
+
+ @Override
+ public Token getSessionToken() {
+ return null;
+ }
+
+ @Override
+ public void addTransportControlsCallback(TransportControlsCallback callback,
+ Handler handler) {
+ }
+
+ @Override
+ public void removeTransportControlsCallback(TransportControlsCallback callback) {
+ }
+
+ @Override
+ public void setPlaybackState(PlaybackStateCompat state) {
+ }
+
+ @Override
+ public void setMetadata(MediaMetadataCompat metadata) {
+ }
+
+ @Override
+ public Object getMediaSession() {
+ return null;
+ }
+ }
+
+ static class MediaSessionImplApi21 implements MediaSessionImpl {
+ private final Object mSessionObj;
+ private final Token mToken;
+
+ public MediaSessionImplApi21(Context context, String tag) {
+ mSessionObj = MediaSessionCompatApi21.createSession(context, tag);
+ mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj));
+ }
+
+ @Override
+ public void addCallback(Callback callback, Handler handler) {
+ MediaSessionCompatApi21.addCallback(mSessionObj, callback.mCallbackObj, handler);
+ }
+
+ @Override
+ public void removeCallback(Callback callback) {
+ MediaSessionCompatApi21.removeCallback(mSessionObj, callback.mCallbackObj);
+ }
+
+ @Override
+ public void setFlags(int flags) {
+ MediaSessionCompatApi21.setFlags(mSessionObj, flags);
+ }
+
+ @Override
+ public void setPlaybackToLocal(int stream) {
+ MediaSessionCompatApi21.setPlaybackToLocal(mSessionObj, stream);
+ }
+
+ @Override
+ public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) {
+ MediaSessionCompatApi21.setPlaybackToRemote(mSessionObj,
+ volumeProvider.getVolumeProvider());
+ }
+
+ @Override
+ public void setActive(boolean active) {
+ MediaSessionCompatApi21.setActive(mSessionObj, active);
+ }
+
+ @Override
+ public boolean isActive() {
+ return MediaSessionCompatApi21.isActive(mSessionObj);
+ }
+
+ @Override
+ public void sendSessionEvent(String event, Bundle extras) {
+ MediaSessionCompatApi21.sendSessionEvent(mSessionObj, event, extras);
+ }
+
+ @Override
+ public void release() {
+ MediaSessionCompatApi21.release(mSessionObj);
+ }
+
+ @Override
+ public Token getSessionToken() {
+ return mToken;
+ }
+
+ @Override
+ public void addTransportControlsCallback(TransportControlsCallback callback,
+ Handler handler) {
+ MediaSessionCompatApi21.addTransportControlsCallback(
+ mSessionObj, callback.mCallbackObj, handler);
+ }
+
+ @Override
+ public void removeTransportControlsCallback(TransportControlsCallback callback) {
+ MediaSessionCompatApi21.removeTransportControlsCallback(
+ mSessionObj, callback.mCallbackObj);
+ }
+
+ @Override
+ public void setPlaybackState(PlaybackStateCompat state) {
+ MediaSessionCompatApi21.setPlaybackState(mSessionObj, state.getPlaybackState());
+ }
+
+ @Override
+ public void setMetadata(MediaMetadataCompat metadata) {
+ MediaSessionCompatApi21.setMetadata(mSessionObj, metadata.getMediaMetadata());
+ }
+
+ @Override
+ public Object getMediaSession() {
+ return mSessionObj;
+ }
+ }
+}
diff --git a/v4/java/android/support/v4/media/session/PlaybackStateCompat.java b/v4/java/android/support/v4/media/session/PlaybackStateCompat.java
new file mode 100644
index 0000000..9e75ab3
--- /dev/null
+++ b/v4/java/android/support/v4/media/session/PlaybackStateCompat.java
@@ -0,0 +1,497 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.media.session;
+
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.text.TextUtils;
+
+/**
+ * Playback state for a {@link MediaSessionCompat}. This includes a state like
+ * {@link PlaybackStateCompat#STATE_PLAYING}, the current playback position,
+ * and the current control capabilities.
+ */
+public final class PlaybackStateCompat implements Parcelable {
+ /**
+ * Indicates this performer supports the stop command.
+ *
+ * @see Builder#setActions
+ */
+ public static final long ACTION_STOP = 1 << 0;
+
+ /**
+ * Indicates this performer supports the pause command.
+ *
+ * @see Builder#setActions
+ */
+ public static final long ACTION_PAUSE = 1 << 1;
+
+ /**
+ * Indicates this performer supports the play command.
+ *
+ * @see Builder#setActions
+ */
+ public static final long ACTION_PLAY = 1 << 2;
+
+ /**
+ * Indicates this performer supports the rewind command.
+ *
+ * @see Builder#setActions
+ */
+ public static final long ACTION_REWIND = 1 << 3;
+
+ /**
+ * Indicates this performer supports the previous command.
+ *
+ * @see Builder#setActions
+ */
+ public static final long ACTION_SKIP_TO_PREVIOUS = 1 << 4;
+
+ /**
+ * Indicates this performer supports the next command.
+ *
+ * @see Builder#setActions
+ */
+ public static final long ACTION_SKIP_TO_NEXT = 1 << 5;
+
+ /**
+ * Indicates this performer supports the fast forward command.
+ *
+ * @see Builder#setActions
+ */
+ public static final long ACTION_FAST_FORWARD = 1 << 6;
+
+ /**
+ * Indicates this performer supports the set rating command.
+ *
+ * @see Builder#setActions
+ */
+ public static final long ACTION_SET_RATING = 1 << 7;
+
+ /**
+ * Indicates this performer supports the seek to command.
+ *
+ * @see Builder#setActions
+ */
+ public static final long ACTION_SEEK_TO = 1 << 8;
+
+ /**
+ * Indicates this performer supports the play/pause toggle command.
+ *
+ * @see Builder#setActions
+ */
+ public static final long ACTION_PLAY_PAUSE = 1 << 9;
+
+ /**
+ * This is the default playback state and indicates that no media has been
+ * added yet, or the performer has been reset and has no content to play.
+ *
+ * @see Builder#setState
+ */
+ public final static int STATE_NONE = 0;
+
+ /**
+ * State indicating this item is currently stopped.
+ *
+ * @see Builder#setState
+ */
+ public final static int STATE_STOPPED = 1;
+
+ /**
+ * State indicating this item is currently paused.
+ *
+ * @see Builder#setState
+ */
+ public final static int STATE_PAUSED = 2;
+
+ /**
+ * State indicating this item is currently playing.
+ *
+ * @see Builder#setState
+ */
+ public final static int STATE_PLAYING = 3;
+
+ /**
+ * State indicating this item is currently fast forwarding.
+ *
+ * @see Builder#setState
+ */
+ public final static int STATE_FAST_FORWARDING = 4;
+
+ /**
+ * State indicating this item is currently rewinding.
+ *
+ * @see Builder#setState
+ */
+ public final static int STATE_REWINDING = 5;
+
+ /**
+ * State indicating this item is currently buffering and will begin playing
+ * when enough data has buffered.
+ *
+ * @see Builder#setState
+ */
+ public final static int STATE_BUFFERING = 6;
+
+ /**
+ * State indicating this item is currently in an error state. The error
+ * message should also be set when entering this state.
+ *
+ * @see Builder#setState
+ */
+ public final static int STATE_ERROR = 7;
+
+ /**
+ * State indicating the class doing playback is currently connecting to a
+ * route. Depending on the implementation you may return to the previous
+ * state when the connection finishes or enter {@link #STATE_NONE}. If
+ * the connection failed {@link #STATE_ERROR} should be used.
+ * @hide
+ */
+ public final static int STATE_CONNECTING = 8;
+
+ /**
+ * State indicating the player is currently skipping to the previous item.
+ *
+ * @see Builder#setState
+ */
+ public final static int STATE_SKIPPING_TO_PREVIOUS = 9;
+
+ /**
+ * State indicating the player is currently skipping to the next item.
+ *
+ * @see Builder#setState
+ */
+ public final static int STATE_SKIPPING_TO_NEXT = 10;
+
+ /**
+ * Use this value for the position to indicate the position is not known.
+ */
+ public final static long PLAYBACK_POSITION_UNKNOWN = -1;
+
+ private final int mState;
+ private final long mPosition;
+ private final long mBufferPosition;
+ private final float mSpeed;
+ private final long mActions;
+ private final CharSequence mErrorMessage;
+ private final long mUpdateTime;
+
+ private Object mStateObj;
+
+ private PlaybackStateCompat(int state, long position, long bufferPosition,
+ float rate, long actions, CharSequence errorMessage, long updateTime) {
+ mState = state;
+ mPosition = position;
+ mBufferPosition = bufferPosition;
+ mSpeed = rate;
+ mActions = actions;
+ mErrorMessage = errorMessage;
+ mUpdateTime = updateTime;
+ }
+
+ private PlaybackStateCompat(Parcel in) {
+ mState = in.readInt();
+ mPosition = in.readLong();
+ mSpeed = in.readFloat();
+ mUpdateTime = in.readLong();
+ mBufferPosition = in.readLong();
+ mActions = in.readLong();
+ mErrorMessage = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder bob = new StringBuilder("PlaybackState {");
+ bob.append("state=").append(mState);
+ bob.append(", position=").append(mPosition);
+ bob.append(", buffered position=").append(mBufferPosition);
+ bob.append(", speed=").append(mSpeed);
+ bob.append(", updated=").append(mUpdateTime);
+ bob.append(", actions=").append(mActions);
+ bob.append(", error=").append(mErrorMessage);
+ bob.append("}");
+ return bob.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mState);
+ dest.writeLong(mPosition);
+ dest.writeFloat(mSpeed);
+ dest.writeLong(mUpdateTime);
+ dest.writeLong(mBufferPosition);
+ dest.writeLong(mActions);
+ TextUtils.writeToParcel(mErrorMessage, dest, flags);
+ }
+
+ /**
+ * Get the current state of playback. One of the following:
+ * <ul>
+ * <li> {@link PlaybackStateCompat#STATE_NONE}</li>
+ * <li> {@link PlaybackStateCompat#STATE_STOPPED}</li>
+ * <li> {@link PlaybackStateCompat#STATE_PLAYING}</li>
+ * <li> {@link PlaybackStateCompat#STATE_PAUSED}</li>
+ * <li> {@link PlaybackStateCompat#STATE_FAST_FORWARDING}</li>
+ * <li> {@link PlaybackStateCompat#STATE_REWINDING}</li>
+ * <li> {@link PlaybackStateCompat#STATE_BUFFERING}</li>
+ * <li> {@link PlaybackStateCompat#STATE_ERROR}</li>
+ */
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * Get the current playback position in ms.
+ */
+ public long getPosition() {
+ return mPosition;
+ }
+
+ /**
+ * Get the current buffer position in ms. This is the farthest playback
+ * point that can be reached from the current position using only buffered
+ * content.
+ */
+ public long getBufferPosition() {
+ return mBufferPosition;
+ }
+
+ /**
+ * Get the current playback speed as a multiple of normal playback. This
+ * should be negative when rewinding. A value of 1 means normal playback and
+ * 0 means paused.
+ *
+ * @return The current speed of playback.
+ */
+ public float getPlaybackSpeed() {
+ return mSpeed;
+ }
+
+ /**
+ * Get the current actions available on this session. This should use a
+ * bitmask of the available actions.
+ * <ul>
+ * <li> {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_REWIND}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_PLAY}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_PAUSE}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_STOP}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SEEK_TO}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SET_RATING}</li>
+ * </ul>
+ */
+ public long getActions() {
+ return mActions;
+ }
+
+ /**
+ * Get a user readable error message. This should be set when the state is
+ * {@link PlaybackStateCompat#STATE_ERROR}.
+ */
+ public CharSequence getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ /**
+ * Get the elapsed real time at which position was last updated. If the
+ * position has never been set this will return 0;
+ *
+ * @return The last time the position was updated.
+ */
+ public long getLastPositionUpdateTime() {
+ return mUpdateTime;
+ }
+
+ /**
+ * Creates an instance from a framework {@link android.media.session.PlaybackState} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @param stateObj A {@link android.media.session.PlaybackState} object, or null if none.
+ * @return An equivalent {@link PlaybackStateCompat} object, or null if none.
+ */
+ public static PlaybackStateCompat fromPlaybackState(Object stateObj) {
+ if (stateObj == null || Build.VERSION.SDK_INT < 21) {
+ return null;
+ }
+
+ PlaybackStateCompat state = new PlaybackStateCompat(
+ PlaybackStateCompatApi21.getState(stateObj),
+ PlaybackStateCompatApi21.getPosition(stateObj),
+ PlaybackStateCompatApi21.getBufferPosition(stateObj),
+ PlaybackStateCompatApi21.getPlaybackSpeed(stateObj),
+ PlaybackStateCompatApi21.getActions(stateObj),
+ PlaybackStateCompatApi21.getErrorMessage(stateObj),
+ PlaybackStateCompatApi21.getLastPositionUpdateTime(stateObj));
+ state.mStateObj = stateObj;
+ return state;
+ }
+
+ /**
+ * Gets the underlying framework {@link android.media.session.PlaybackState} object.
+ * <p>
+ * This method is only supported on API 21+.
+ * </p>
+ *
+ * @return An equivalent {@link android.media.session.PlaybackState} object, or null if none.
+ */
+ public Object getPlaybackState() {
+ if (mStateObj != null || Build.VERSION.SDK_INT < 21) {
+ return mStateObj;
+ }
+
+ mStateObj = PlaybackStateCompatApi21.newInstance(mState, mPosition, mBufferPosition,
+ mSpeed, mActions, mErrorMessage, mUpdateTime);
+ return mStateObj;
+ }
+
+ public static final Parcelable.Creator<PlaybackStateCompat> CREATOR =
+ new Parcelable.Creator<PlaybackStateCompat>() {
+ @Override
+ public PlaybackStateCompat createFromParcel(Parcel in) {
+ return new PlaybackStateCompat(in);
+ }
+
+ @Override
+ public PlaybackStateCompat[] newArray(int size) {
+ return new PlaybackStateCompat[size];
+ }
+ };
+
+ /**
+ * Builder for {@link PlaybackStateCompat} objects.
+ */
+ public static final class Builder {
+ private int mState;
+ private long mPosition;
+ private long mBufferPosition;
+ private float mRate;
+ private long mActions;
+ private CharSequence mErrorMessage;
+ private long mUpdateTime;
+
+ /**
+ * Create an empty Builder.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Create a Builder using a {@link PlaybackStateCompat} instance to set the
+ * initial values.
+ *
+ * @param source The playback state to copy.
+ */
+ public Builder(PlaybackStateCompat source) {
+ mState = source.mState;
+ mPosition = source.mPosition;
+ mRate = source.mSpeed;
+ mUpdateTime = source.mUpdateTime;
+ mBufferPosition = source.mBufferPosition;
+ mActions = source.mActions;
+ mErrorMessage = source.mErrorMessage;
+ }
+
+ /**
+ * Set the current state of playback.
+ * <p>
+ * The position must be in ms and indicates the current playback position
+ * within the track. If the position is unknown use
+ * {@link #PLAYBACK_POSITION_UNKNOWN}.
+ * <p>
+ * The rate is a multiple of normal playback and should be 0 when paused and
+ * negative when rewinding. Normal playback rate is 1.0.
+ * <p>
+ * The state must be one of the following:
+ * <ul>
+ * <li> {@link PlaybackStateCompat#STATE_NONE}</li>
+ * <li> {@link PlaybackStateCompat#STATE_STOPPED}</li>
+ * <li> {@link PlaybackStateCompat#STATE_PLAYING}</li>
+ * <li> {@link PlaybackStateCompat#STATE_PAUSED}</li>
+ * <li> {@link PlaybackStateCompat#STATE_FAST_FORWARDING}</li>
+ * <li> {@link PlaybackStateCompat#STATE_REWINDING}</li>
+ * <li> {@link PlaybackStateCompat#STATE_BUFFERING}</li>
+ * <li> {@link PlaybackStateCompat#STATE_ERROR}</li>
+ * </ul>
+ *
+ * @param state The current state of playback.
+ * @param position The position in the current track in ms.
+ * @param playbackRate The current rate of playback as a multiple of normal
+ * playback.
+ */
+ public void setState(int state, long position, float playbackRate) {
+ this.mState = state;
+ this.mPosition = position;
+ this.mRate = playbackRate;
+ mUpdateTime = SystemClock.elapsedRealtime();
+ }
+
+ /**
+ * Set the current buffer position in ms. This is the farthest playback
+ * point that can be reached from the current position using only buffered
+ * content.
+ */
+ public void setBufferPosition(long bufferPosition) {
+ mBufferPosition = bufferPosition;
+ }
+
+ /**
+ * Set the current capabilities available on this session. This should use a
+ * bitmask of the available capabilities.
+ * <ul>
+ * <li> {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_REWIND}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_PLAY}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_PAUSE}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_STOP}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_FAST_FORWARD}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SEEK_TO}</li>
+ * <li> {@link PlaybackStateCompat#ACTION_SET_RATING}</li>
+ * </ul>
+ */
+ public void setActions(long capabilities) {
+ mActions = capabilities;
+ }
+
+ /**
+ * Set a user readable error message. This should be set when the state is
+ * {@link PlaybackStateCompat#STATE_ERROR}.
+ */
+ public void setErrorMessage(CharSequence errorMessage) {
+ mErrorMessage = errorMessage;
+ }
+
+ /**
+ * Creates the playback state object.
+ */
+ public PlaybackStateCompat build() {
+ return new PlaybackStateCompat(mState, mPosition, mBufferPosition,
+ mRate, mActions, mErrorMessage, mUpdateTime);
+ }
+ }
+}
diff --git a/v4/java/android/support/v4/os/AsyncTaskCompat.java b/v4/java/android/support/v4/os/AsyncTaskCompat.java
new file mode 100644
index 0000000..2af8905
--- /dev/null
+++ b/v4/java/android/support/v4/os/AsyncTaskCompat.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.os;
+
+import android.os.AsyncTask;
+import android.os.Build;
+
+/**
+ * Helper for accessing features in {@link android.os.AsyncTask}
+ * introduced after API level 4 in a backwards compatible fashion.
+ */
+public class AsyncTaskCompat {
+
+ /**
+ * Executes the task with the specified parameters, allowing multiple tasks to run in parallel
+ * on a pool of threads managed by {@link android.os.AsyncTask}.
+ *
+ * @param task The {@link android.os.AsyncTask} to execute.
+ * @param params The parameters of the task.
+ * @return the instance of AsyncTask.
+ */
+ public static <Params, Progress, Result> AsyncTask<Params, Progress, Result> executeParallel(
+ AsyncTask<Params, Progress, Result> task,
+ Params... params) {
+ if (task == null) {
+ throw new IllegalArgumentException("task can not be null");
+ }
+
+ if (Build.VERSION.SDK_INT >= 11) {
+ // From API 11 onwards, we need to manually select the THREAD_POOL_EXECUTOR
+ AsyncTaskCompatHoneycomb.executeParallel(task, params);
+ } else {
+ // Before API 11, all tasks were run in parallel
+ task.execute(params);
+ }
+
+ return task;
+ }
+
+}
diff --git a/v4/java/android/support/v4/util/Pair.java b/v4/java/android/support/v4/util/Pair.java
new file mode 100644
index 0000000..7f9e61e
--- /dev/null
+++ b/v4/java/android/support/v4/util/Pair.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.util;
+
+/**
+ * Container to ease passing around a tuple of two objects. This object provides a sensible
+ * implementation of equals(), returning true if equals() is true on each of the contained
+ * objects.
+ */
+public class Pair<F, S> {
+ public final F first;
+ public final S second;
+
+ /**
+ * Constructor for a Pair.
+ *
+ * @param first the first object in the Pair
+ * @param second the second object in the pair
+ */
+ public Pair(F first, S second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ /**
+ * Checks the two objects for equality by delegating to their respective
+ * {@link Object#equals(Object)} methods.
+ *
+ * @param o the {@link Pair} to which this one is to be checked for equality
+ * @return true if the underlying objects of the Pair are both considered
+ * equal
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Pair)) {
+ return false;
+ }
+ Pair<?, ?> p = (Pair<?, ?>) o;
+ return objectsEqual(p.first, first) && objectsEqual(p.second, second);
+ }
+
+ private static boolean objectsEqual(Object a, Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ /**
+ * Compute a hash code using the hash codes of the underlying objects
+ *
+ * @return a hashcode of the Pair
+ */
+ @Override
+ public int hashCode() {
+ return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode());
+ }
+
+ /**
+ * Convenience method for creating an appropriately typed pair.
+ * @param a the first object in the Pair
+ * @param b the second object in the pair
+ * @return a Pair that is templatized with the types of a and b
+ */
+ public static <A, B> Pair <A, B> create(A a, B b) {
+ return new Pair<A, B>(a, b);
+ }
+}
diff --git a/v4/java/android/support/v4/view/ViewCompat.java b/v4/java/android/support/v4/view/ViewCompat.java
index 84455a0..52ad11b 100644
--- a/v4/java/android/support/v4/view/ViewCompat.java
+++ b/v4/java/android/support/v4/view/ViewCompat.java
@@ -317,6 +317,8 @@
public void setPivotY(View view, float value);
public float getPivotX(View view);
public float getPivotY(View view);
+ public void setTransitionName(View view, String transitionName);
+ public String getTransitionName(View view);
}
static class BaseViewCompatImpl implements ViewCompatImpl {
@@ -646,6 +648,15 @@
public float getPivotY(View view) {
return 0;
}
+
+ @Override
+ public void setTransitionName(View view, String transitionName) {
+ }
+
+ @Override
+ public String getTransitionName(View view) {
+ return null;
+ }
}
static class EclairMr1ViewCompatImpl extends BaseViewCompatImpl {
@@ -976,10 +987,24 @@
}
}
+ static class Api21ViewCompatImpl extends KitKatViewCompatImpl {
+ @Override
+ public void setTransitionName(View view, String transitionName) {
+ ViewCompatApi21.setTransitionName(view, transitionName);
+ }
+
+ @Override
+ public String getTransitionName(View view) {
+ return ViewCompatApi21.getTransitionName(view);
+ }
+ }
+
static final ViewCompatImpl IMPL;
static {
final int version = android.os.Build.VERSION.SDK_INT;
- if (version >= 19) {
+ if (version >= 21 || android.os.Build.VERSION.CODENAME.equals("L")) {
+ IMPL = new Api21ViewCompatImpl();
+ } else if (version >= 19) {
IMPL = new KitKatViewCompatImpl();
} else if (version >= 17) {
IMPL = new JbMr1ViewCompatImpl();
@@ -1891,7 +1916,7 @@
* <p>Prior to API 11 this will have no effect.</p>
*
*/
- public float getPivotX(View view) {
+ public static float getPivotX(View view) {
return IMPL.getPivotX(view);
}
@@ -1906,7 +1931,7 @@
*
* @param value The x location of the pivot point.
*/
- public void setPivotX(View view, float value) {
+ public static void setPivotX(View view, float value) {
IMPL.setPivotX(view, value);
}
@@ -1918,7 +1943,7 @@
*
* @return The y location of the pivot point.
*/
- public float getPivotY(View view) {
+ public static float getPivotY(View view) {
return IMPL.getPivotY(view);
}
@@ -1933,37 +1958,62 @@
*
* @param value The y location of the pivot point.
*/
- public void setPivotY(View view, float value) {
+ public static void setPivotY(View view, float value) {
IMPL.setPivotX(view, value);
}
- public float getRotation(View view) {
+ public static float getRotation(View view) {
return IMPL.getRotation(view);
}
- public float getRotationX(View view) {
+ public static float getRotationX(View view) {
return IMPL.getRotationX(view);
}
- public float getRotationY(View view) {
+ public static float getRotationY(View view) {
return IMPL.getRotationY(view);
}
- public float getScaleX(View view) {
+ public static float getScaleX(View view) {
return IMPL.getScaleX(view);
}
- public float getScaleY(View view) {
+ public static float getScaleY(View view) {
return IMPL.getScaleY(view);
}
- public float getX(View view) {
+ public static float getX(View view) {
return IMPL.getX(view);
}
- public float getY(View view) {
+ public static float getY(View view) {
return IMPL.getY(view);
}
+ /**
+ * Sets the name of the View to be used to identify Views in Transitions.
+ * Names should be unique in the View hierarchy.
+ *
+ * @param view The View against which to invoke the method.
+ * @param transitionName The name of the View to uniquely identify it for Transitions.
+ */
+ public static void setTransitionName(View view, String transitionName) {
+ IMPL.setTransitionName(view, transitionName);
+ }
+
+ /**
+ * Returns the name of the View to be used to identify Views in Transitions.
+ * Names should be unique in the View hierarchy.
+ *
+ * <p>This returns null if the View has not been given a name.</p>
+ *
+ * @param view The View against which to invoke the method.
+ * @return The name used of the View to be used to identify Views in Transitions or null
+ * if no name has been given.
+ */
+ public static String getTransitionName(View view) {
+ return IMPL.getTransitionName(view);
+ }
+
// TODO: getters for various view properties (rotation, etc)
}
diff --git a/v4/java/android/support/v4/view/ViewGroupCompat.java b/v4/java/android/support/v4/view/ViewGroupCompat.java
index 2c9150b..07dde0c 100644
--- a/v4/java/android/support/v4/view/ViewGroupCompat.java
+++ b/v4/java/android/support/v4/view/ViewGroupCompat.java
@@ -50,6 +50,8 @@
public void setMotionEventSplittingEnabled(ViewGroup group, boolean split);
public int getLayoutMode(ViewGroup group);
public void setLayoutMode(ViewGroup group, int mode);
+ public void setTransitionGroup(ViewGroup group, boolean isTransitionGroup);
+ public boolean isTransitionGroup(ViewGroup group);
}
static class ViewGroupCompatStubImpl implements ViewGroupCompatImpl {
@@ -71,6 +73,15 @@
public void setLayoutMode(ViewGroup group, int mode) {
// no-op, didn't exist. Views only support clip bounds.
}
+
+ @Override
+ public void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) {
+ }
+
+ @Override
+ public boolean isTransitionGroup(ViewGroup group) {
+ return false;
+ }
}
static class ViewGroupCompatHCImpl extends ViewGroupCompatStubImpl {
@@ -100,10 +111,24 @@
}
}
+ static class ViewGroupCompatApi21Impl extends ViewGroupCompatJellybeanMR2Impl {
+ @Override
+ public void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) {
+ ViewGroupCompatApi21.setTransitionGroup(group, isTransitionGroup);
+ }
+
+ @Override
+ public boolean isTransitionGroup(ViewGroup group) {
+ return ViewGroupCompatApi21.isTransitionGroup(group);
+ }
+ }
+
static final ViewGroupCompatImpl IMPL;
static {
final int version = Build.VERSION.SDK_INT;
- if (version >= 18) {
+ if (version >= 21 || android.os.Build.VERSION.CODENAME.equals("L")) {
+ IMPL = new ViewGroupCompatApi21Impl();
+ } else if (version >= 18) {
IMPL = new ViewGroupCompatJellybeanMR2Impl();
} else if (version >= 14) {
IMPL = new ViewGroupCompatIcsImpl();
@@ -189,4 +214,25 @@
public static void setLayoutMode(ViewGroup group, int mode) {
IMPL.setLayoutMode(group, mode);
}
+
+ /**
+ * Changes whether or not this ViewGroup should be treated as a single entity during
+ * Activity Transitions.
+ * @param isTransitionGroup Whether or not the ViewGroup should be treated as a unit
+ * in Activity transitions. If false, the ViewGroup won't transition,
+ * only its children. If true, the entire ViewGroup will transition
+ * together.
+ */
+ public static void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) {
+ IMPL.setTransitionGroup(group, isTransitionGroup);
+ }
+
+ /**
+ * Returns true if this ViewGroup should be considered as a single entity for removal
+ * when executing an Activity transition. If this is false, child elements will move
+ * individually during the transition.
+ */
+ public static boolean isTransitionGroup(ViewGroup group) {
+ return IMPL.isTransitionGroup(group);
+ }
}
diff --git a/v4/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java b/v4/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java
index 43aab0a..9924b31 100644
--- a/v4/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java
+++ b/v4/java/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java
@@ -22,8 +22,6 @@
import android.support.v4.accessibilityservice.AccessibilityServiceInfoCompat;
import android.support.v4.view.ViewCompat;
import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -34,6 +32,112 @@
*/
public class AccessibilityNodeInfoCompat {
+ public static class AccessibilityActionCompat {
+ private final Object mAction;
+
+ private AccessibilityActionCompat(Object action) {
+ mAction = action;
+ }
+
+ /**
+ * Gets the id for this action.
+ *
+ * @return The action id.
+ */
+ public int getId() {
+ return AccessibilityNodeInfoCompatApi21.AccessibilityAction.getId(mAction);
+ }
+
+ /**
+ * Gets the label for this action. Its purpose is to describe the
+ * action to user.
+ *
+ * @return The label.
+ */
+ public CharSequence getLabel() {
+ return AccessibilityNodeInfoCompatApi21.AccessibilityAction.getLabel(mAction);
+ }
+ }
+
+ public static class CollectionInfoCompat {
+ private final Object mInfo;
+
+ private CollectionInfoCompat(Object info) {
+ mInfo = info;
+ }
+
+ public int getColumnCount() {
+ return AccessibilityNodeInfoCompatKitKat.CollectionInfo.getColumnCount(mInfo);
+ }
+
+ public int getRowCount() {
+ return AccessibilityNodeInfoCompatKitKat.CollectionInfo.getRowCount(mInfo);
+ }
+
+ public boolean isHierarchical() {
+ return AccessibilityNodeInfoCompatKitKat.CollectionInfo.isHierarchical(mInfo);
+ }
+ }
+
+ public static class CollectionItemInfoCompat {
+ private final Object mInfo;
+
+ private CollectionItemInfoCompat(Object info) {
+ mInfo = info;
+ }
+
+ public int getColumnIndex() {
+ return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getColumnIndex(mInfo);
+ }
+
+ public int getColumnSpan() {
+ return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getColumnSpan(mInfo);
+ }
+
+ public int getRowIndex() {
+ return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getRowIndex(mInfo);
+ }
+
+ public int getRowSpan() {
+ return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getRowSpan(mInfo);
+ }
+
+ public boolean isHeading() {
+ return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.isHeading(mInfo);
+ }
+ }
+
+ public static class RangeInfoCompat {
+ /** Range type: integer. */
+ public static final int RANGE_TYPE_INT = 0;
+ /** Range type: float. */
+ public static final int RANGE_TYPE_FLOAT = 1;
+ /** Range type: percent with values from zero to one.*/
+ public static final int RANGE_TYPE_PERCENT = 2;
+
+ private final Object mInfo;
+
+ private RangeInfoCompat(Object info) {
+ mInfo = info;
+ }
+
+ public float getCurrent() {
+ return AccessibilityNodeInfoCompatKitKat.RangeInfo.getCurrent(mInfo);
+ }
+
+ public float getMax() {
+ return AccessibilityNodeInfoCompatKitKat.RangeInfo.getMax(mInfo);
+ }
+
+ public float getMin() {
+ return AccessibilityNodeInfoCompatKitKat.RangeInfo.getMin(mInfo);
+ }
+
+ public int getType() {
+ return AccessibilityNodeInfoCompatKitKat.RangeInfo.getType(mInfo);
+ }
+ }
+
static interface AccessibilityNodeInfoImpl {
public Object obtain();
public Object obtain(View source);
@@ -99,6 +203,11 @@
public void setViewIdResourceName(Object info, String viewId);
public int getLiveRegion(Object info);
public void setLiveRegion(Object info, int mode);
+ public Object getCollectionInfo(Object info);
+ public Object getCollectionItemInfo(Object info);
+ public Object getRangeInfo(Object info);
+ public List<Object> getActionList(Object info);
+ public void addAction(Object info, int id, CharSequence label);
}
static class AccessibilityNodeInfoStubImpl implements AccessibilityNodeInfoImpl {
@@ -421,6 +530,31 @@
public void setLiveRegion(Object info, int mode) {
// No-op
}
+
+ @Override
+ public Object getCollectionInfo(Object info) {
+ return null;
+ }
+
+ @Override
+ public Object getCollectionItemInfo(Object info) {
+ return null;
+ }
+
+ @Override
+ public Object getRangeInfo(Object info) {
+ return null;
+ }
+
+ @Override
+ public List<Object> getActionList(Object info) {
+ return null;
+ }
+
+ @Override
+ public void addAction(Object info, int id, CharSequence label) {
+ }
+
}
static class AccessibilityNodeInfoIcsImpl extends AccessibilityNodeInfoStubImpl {
@@ -658,6 +792,13 @@
public void recycle(Object info) {
AccessibilityNodeInfoCompatIcs.recycle(info);
}
+
+ @Override
+ public void addAction(Object info, int id, CharSequence label) {
+ if (Integer.bitCount(id) == 1) {
+ addAction(info, id);
+ }
+ }
}
static class AccessibilityNodeInfoJellybeanImpl extends AccessibilityNodeInfoIcsImpl {
@@ -750,10 +891,39 @@
public void setLiveRegion(Object info, int mode) {
AccessibilityNodeInfoCompatKitKat.setLiveRegion(info, mode);
}
+
+ @Override
+ public Object getCollectionInfo(Object info) {
+ return AccessibilityNodeInfoCompatKitKat.getCollectionInfo(info);
+ }
+
+ @Override
+ public Object getCollectionItemInfo(Object info) {
+ return AccessibilityNodeInfoCompatKitKat.getCollectionItemInfo(info);
+ }
+
+ @Override
+ public Object getRangeInfo(Object info) {
+ return AccessibilityNodeInfoCompatKitKat.getRangeInfo(info);
+ }
+ }
+
+ static class AccessibilityNodeInfoApi21Impl extends AccessibilityNodeInfoKitKatImpl {
+ @Override
+ public List<Object> getActionList(Object info) {
+ return AccessibilityNodeInfoCompatApi21.getActionList(info);
+ }
+
+ @Override
+ public void addAction(Object info, int id, CharSequence label) {
+ AccessibilityNodeInfoCompatApi21.addAction(info, id, label);
+ }
}
static {
- if (Build.VERSION.SDK_INT >= 19) { // KitKat
+ if (Build.VERSION.CODENAME.equals("L")) { // b/15469684 filed to change to API 21
+ IMPL = new AccessibilityNodeInfoApi21Impl();
+ } else if (Build.VERSION.SDK_INT >= 19) { // KitKat
IMPL = new AccessibilityNodeInfoKitKatImpl();
} else if (Build.VERSION.SDK_INT >= 18) { // JellyBean MR2
IMPL = new AccessibilityNodeInfoJellybeanMr2Impl();
@@ -956,6 +1126,22 @@
*/
public static final int ACTION_SET_SELECTION = 0x00020000;
+ /**
+ * Action that sets the text of the node. Performing the action without argument, using <code>
+ * null</code> or empty {@link CharSequence} will clear the text. This action will also put the
+ * cursor at the end of text.
+ * <p>
+ * <strong>Arguments:</strong> {@link #ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE}<br>
+ * <strong>Example:</strong>
+ * <code><pre><p>
+ * Bundle arguments = new Bundle();
+ * arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
+ * "android");
+ * info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments);
+ * </code></pre></p>
+ */
+ public static final int ACTION_SET_TEXT = 0x00200000;
+
// Action arguments
/**
@@ -1019,6 +1205,18 @@
public static final String ACTION_ARGUMENT_SELECTION_END_INT =
"ACTION_ARGUMENT_SELECTION_END_INT";
+ /**
+ * Argument for specifying the text content to set
+ * <p>
+ * <strong>Type:</strong> CharSequence<br>
+ * <strong>Actions:</strong> {@link #ACTION_SET_TEXT}
+ * </p>
+ *
+ * @see #ACTION_SET_TEXT
+ */
+ public static final String ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE =
+ "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE";
+
// Focus types
/**
@@ -1943,6 +2141,58 @@
IMPL.setLiveRegion(mInfo, mode);
}
+ /**
+ * Gets the collection info if the node is a collection. A collection
+ * child is always a collection item.
+ *
+ * @return The collection info.
+ */
+ public CollectionInfoCompat getCollectionInfo() {
+ Object info = IMPL.getCollectionInfo(mInfo);
+ if (info == null) return null;
+ return new CollectionInfoCompat(info);
+ }
+
+ /**
+ * Gets the collection item info if the node is a collection item. A collection
+ * item is always a child of a collection.
+ *
+ * @return The collection item info.
+ */
+ public CollectionItemInfoCompat getCollectionItemInfo() {
+ Object info = IMPL.getCollectionItemInfo(mInfo);
+ if (info == null) return null;
+ return new CollectionItemInfoCompat(info);
+ }
+
+ /**
+ * Gets the range info if this node is a range.
+ *
+ * @return The range.
+ */
+ public RangeInfoCompat getRangeInfo() {
+ Object info = IMPL.getRangeInfo(mInfo);
+ if (info == null) return null;
+ return new RangeInfoCompat(info);
+ }
+
+ /**
+ * Gets the actions that can be performed on the node.
+ *
+ * @return A list of AccessibilityActions.
+ */
+ public List<AccessibilityActionCompat> getActionList() {
+ List<AccessibilityActionCompat> result = new ArrayList<AccessibilityActionCompat>();
+ List<Object> actions = IMPL.getActionList(mInfo);
+ final int actionCount = actions.size();
+ for (int i = 0; i < actionCount; i++) {
+ Object action = actions.get(i);
+ result.add(new AccessibilityActionCompat(action));
+ }
+ return result;
+ }
+
+
@Override
public int hashCode() {
return (mInfo == null) ? 0 : mInfo.hashCode();
diff --git a/v4/java/android/support/v4/widget/ExploreByTouchHelper.java b/v4/java/android/support/v4/widget/ExploreByTouchHelper.java
index b894399..7adbc6f 100644
--- a/v4/java/android/support/v4/widget/ExploreByTouchHelper.java
+++ b/v4/java/android/support/v4/widget/ExploreByTouchHelper.java
@@ -577,7 +577,8 @@
* @param x The view-relative x coordinate
* @param y The view-relative y coordinate
* @return virtual view identifier for the logical item under
- * coordinates (x,y)
+ * coordinates (x,y) or {@link View#NO_ID} if there is no item at
+ * the given coordinates
*/
protected abstract int getVirtualViewAt(float x, float y);
diff --git a/v4/java/android/support/v4/widget/SwipeProgressBar.java b/v4/java/android/support/v4/widget/SwipeProgressBar.java
index b3b04cd..ba60d89 100644
--- a/v4/java/android/support/v4/widget/SwipeProgressBar.java
+++ b/v4/java/android/support/v4/widget/SwipeProgressBar.java
@@ -99,7 +99,8 @@
void setTriggerPercentage(float triggerPercentage) {
mTriggerPercentage = triggerPercentage;
mStartTime = 0;
- ViewCompat.postInvalidateOnAnimation(mParent);
+ ViewCompat.postInvalidateOnAnimation(
+ mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
}
/**
@@ -227,7 +228,8 @@
drawTrigger(canvas, cx, cy);
}
// Keep running until we finish out the last cycle.
- ViewCompat.postInvalidateOnAnimation(mParent);
+ ViewCompat.postInvalidateOnAnimation(
+ mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom);
} else {
// Otherwise if we're in the middle of a trigger, draw that.
if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) {
diff --git a/v4/java/android/support/v4/widget/ViewDragHelper.java b/v4/java/android/support/v4/widget/ViewDragHelper.java
index 6b116f2..e56bfdf 100644
--- a/v4/java/android/support/v4/widget/ViewDragHelper.java
+++ b/v4/java/android/support/v4/widget/ViewDragHelper.java
@@ -1004,15 +1004,38 @@
final float dx = x - mInitialMotionX[pointerId];
final float dy = y - mInitialMotionY[pointerId];
+ final View toCapture = findTopChildUnder((int) x, (int) y);
+ final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
+ if (pastSlop) {
+ // check the callback's
+ // getView[Horizontal|Vertical]DragRange methods to know
+ // if you can move at all along an axis, then see if it
+ // would clamp to the same value. If you can't move at
+ // all in every dimension with a nonzero range, bail.
+ final int oldLeft = toCapture.getLeft();
+ final int targetLeft = oldLeft + (int) dx;
+ final int newLeft = mCallback.clampViewPositionHorizontal(toCapture,
+ targetLeft, (int) dx);
+ final int oldTop = toCapture.getTop();
+ final int targetTop = oldTop + (int) dy;
+ final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop,
+ (int) dy);
+ final int horizontalDragRange = mCallback.getViewHorizontalDragRange(
+ toCapture);
+ final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
+ if ((horizontalDragRange == 0 || horizontalDragRange > 0
+ && newLeft == oldLeft) && (verticalDragRange == 0
+ || verticalDragRange > 0 && newTop == oldTop)) {
+ break;
+ }
+ }
reportNewEdgeDrags(dx, dy, pointerId);
if (mDragState == STATE_DRAGGING) {
// Callback might have started an edge drag
break;
}
- final View toCapture = findTopChildUnder((int) x, (int) y);
- if (toCapture != null && checkTouchSlop(toCapture, dx, dy) &&
- tryCaptureViewForDrag(toCapture, pointerId)) {
+ if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
break;
}
}
@@ -1446,4 +1469,4 @@
return result;
}
-}
+}
\ No newline at end of file
diff --git a/v4/jellybean-mr1/android/support/v4/media/routing/MediaRouterJellybeanMr1.java b/v4/jellybean-mr1/android/support/v4/media/routing/MediaRouterJellybeanMr1.java
new file mode 100644
index 0000000..6e5cfd5
--- /dev/null
+++ b/v4/jellybean-mr1/android/support/v4/media/routing/MediaRouterJellybeanMr1.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.routing;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Display;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+class MediaRouterJellybeanMr1 extends MediaRouterJellybean {
+ private static final String TAG = "MediaRouterJellybeanMr1";
+
+ public static Object createCallback(Callback callback) {
+ return new CallbackProxy<Callback>(callback);
+ }
+
+ public static final class RouteInfo {
+ public static boolean isEnabled(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).isEnabled();
+ }
+
+ public static Display getPresentationDisplay(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getPresentationDisplay();
+ }
+ }
+
+ public static interface Callback extends MediaRouterJellybean.Callback {
+ public void onRoutePresentationDisplayChanged(Object routeObj);
+ }
+
+ /**
+ * Workaround the fact that the version of MediaRouter.addCallback() that accepts a
+ * flag to perform an active scan does not exist in JB MR1 so we need to force
+ * wifi display scans directly through the DisplayManager.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class ActiveScanWorkaround implements Runnable {
+ // Time between wifi display scans when actively scanning in milliseconds.
+ private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000;
+
+ private final DisplayManager mDisplayManager;
+ private final Handler mHandler;
+ private Method mScanWifiDisplaysMethod;
+
+ private boolean mActivelyScanningWifiDisplays;
+
+ public ActiveScanWorkaround(Context context, Handler handler) {
+ if (Build.VERSION.SDK_INT != 17) {
+ throw new UnsupportedOperationException();
+ }
+
+ mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ mHandler = handler;
+ try {
+ mScanWifiDisplaysMethod = DisplayManager.class.getMethod("scanWifiDisplays");
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public void setActiveScanRouteTypes(int routeTypes) {
+ // On JB MR1, there is no API to scan wifi display routes.
+ // Instead we must make a direct call into the DisplayManager to scan
+ // wifi displays on this version but only when live video routes are requested.
+ // See also the JellybeanMr2Impl implementation of this method.
+ // This was fixed in JB MR2 by adding a new overload of addCallback() to
+ // enable active scanning on request.
+ if ((routeTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
+ if (!mActivelyScanningWifiDisplays) {
+ if (mScanWifiDisplaysMethod != null) {
+ mActivelyScanningWifiDisplays = true;
+ mHandler.post(this);
+ } else {
+ Log.w(TAG, "Cannot scan for wifi displays because the "
+ + "DisplayManager.scanWifiDisplays() method is "
+ + "not available on this device.");
+ }
+ }
+ } else {
+ if (mActivelyScanningWifiDisplays) {
+ mActivelyScanningWifiDisplays = false;
+ mHandler.removeCallbacks(this);
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ if (mActivelyScanningWifiDisplays) {
+ try {
+ mScanWifiDisplaysMethod.invoke(mDisplayManager);
+ } catch (IllegalAccessException ex) {
+ Log.w(TAG, "Cannot scan for wifi displays.", ex);
+ } catch (InvocationTargetException ex) {
+ Log.w(TAG, "Cannot scan for wifi displays.", ex);
+ }
+ mHandler.postDelayed(this, WIFI_DISPLAY_SCAN_INTERVAL);
+ }
+ }
+ }
+
+ /**
+ * Workaround the fact that the isConnecting() method does not exist in JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class IsConnectingWorkaround {
+ private Method mGetStatusCodeMethod;
+ private int mStatusConnecting;
+
+ public IsConnectingWorkaround() {
+ if (Build.VERSION.SDK_INT != 17) {
+ throw new UnsupportedOperationException();
+ }
+
+ try {
+ Field statusConnectingField =
+ android.media.MediaRouter.RouteInfo.class.getField("STATUS_CONNECTING");
+ mStatusConnecting = statusConnectingField.getInt(null);
+ mGetStatusCodeMethod =
+ android.media.MediaRouter.RouteInfo.class.getMethod("getStatusCode");
+ } catch (NoSuchFieldException ex) {
+ } catch (NoSuchMethodException ex) {
+ } catch (IllegalAccessException ex) {
+ }
+ }
+
+ public boolean isConnecting(Object routeObj) {
+ android.media.MediaRouter.RouteInfo route =
+ (android.media.MediaRouter.RouteInfo)routeObj;
+
+ if (mGetStatusCodeMethod != null) {
+ try {
+ int statusCode = (Integer)mGetStatusCodeMethod.invoke(route);
+ return statusCode == mStatusConnecting;
+ } catch (IllegalAccessException ex) {
+ } catch (InvocationTargetException ex) {
+ }
+ }
+
+ // Assume not connecting.
+ return false;
+ }
+ }
+
+ static class CallbackProxy<T extends Callback>
+ extends MediaRouterJellybean.CallbackProxy<T> {
+ public CallbackProxy(T callback) {
+ super(callback);
+ }
+
+ @Override
+ public void onRoutePresentationDisplayChanged(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRoutePresentationDisplayChanged(route);
+ }
+ }
+}
diff --git a/v4/jellybean-mr2/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java b/v4/jellybean-mr2/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java
index a470a2d..cd6bded 100644
--- a/v4/jellybean-mr2/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java
+++ b/v4/jellybean-mr2/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java
@@ -20,6 +20,7 @@
import android.R;
import android.app.ActionBar;
import android.app.Activity;
+import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.Log;
@@ -51,7 +52,15 @@
}
public static Drawable getThemeUpIndicator(Activity activity) {
- final TypedArray a = activity.obtainStyledAttributes(THEME_ATTRS);
+ final ActionBar actionBar = activity.getActionBar();
+ final Context context;
+ if (actionBar != null) {
+ context = actionBar.getThemedContext();
+ } else {
+ context = activity;
+ }
+
+ final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS);
final Drawable result = a.getDrawable(0);
a.recycle();
return result;
diff --git a/v4/jellybean-mr2/android/support/v4/graphics/BitmapCompatJellybeanMR2.java b/v4/jellybean-mr2/android/support/v4/graphics/BitmapCompatJellybeanMR2.java
new file mode 100644
index 0000000..21f0d49
--- /dev/null
+++ b/v4/jellybean-mr2/android/support/v4/graphics/BitmapCompatJellybeanMR2.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.graphics;
+
+import android.graphics.Bitmap;
+
+class BitmapCompatJellybeanMR2 {
+ public static boolean hasMipMap(Bitmap bitmap) {
+ return bitmap.hasMipMap();
+ }
+
+ public static void setHasMipMap(Bitmap bitmap, boolean hasMipMap) {
+ bitmap.setHasMipMap(hasMipMap);
+ }
+}
diff --git a/v4/jellybean-mr2/android/support/v4/media/routing/MediaRouterJellybeanMr2.java b/v4/jellybean-mr2/android/support/v4/media/routing/MediaRouterJellybeanMr2.java
new file mode 100644
index 0000000..92a1607
--- /dev/null
+++ b/v4/jellybean-mr2/android/support/v4/media/routing/MediaRouterJellybeanMr2.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.routing;
+
+class MediaRouterJellybeanMr2 extends MediaRouterJellybeanMr1 {
+ public static Object getDefaultRoute(Object routerObj) {
+ return ((android.media.MediaRouter)routerObj).getDefaultRoute();
+ }
+
+ public static void addCallback(Object routerObj, int types, Object callbackObj, int flags) {
+ ((android.media.MediaRouter)routerObj).addCallback(types,
+ (android.media.MediaRouter.Callback)callbackObj, flags);
+ }
+
+ public static final class RouteInfo {
+ public static CharSequence getDescription(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getDescription();
+ }
+
+ public static boolean isConnecting(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).isConnecting();
+ }
+ }
+
+ public static final class UserRouteInfo {
+ public static void setDescription(Object routeObj, CharSequence description) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setDescription(description);
+ }
+ }
+}
diff --git a/v4/jellybean/android/support/v4/media/routing/MediaRouterJellybean.java b/v4/jellybean/android/support/v4/media/routing/MediaRouterJellybean.java
new file mode 100644
index 0000000..3cf6727
--- /dev/null
+++ b/v4/jellybean/android/support/v4/media/routing/MediaRouterJellybean.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.media.routing;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+class MediaRouterJellybean {
+ private static final String TAG = "MediaRouterJellybean";
+
+ public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
+ public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
+ public static final int ROUTE_TYPE_USER = 0x00800000;
+
+ public static final int ALL_ROUTE_TYPES =
+ MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO
+ | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO
+ | MediaRouterJellybean.ROUTE_TYPE_USER;
+
+ public static Object getMediaRouter(Context context) {
+ return context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getRoutes(Object routerObj) {
+ final android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+ final int count = router.getRouteCount();
+ List out = new ArrayList(count);
+ for (int i = 0; i < count; i++) {
+ out.add(router.getRouteAt(i));
+ }
+ return out;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getCategories(Object routerObj) {
+ final android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+ final int count = router.getCategoryCount();
+ List out = new ArrayList(count);
+ for (int i = 0; i < count; i++) {
+ out.add(router.getCategoryAt(i));
+ }
+ return out;
+ }
+
+ public static Object getSelectedRoute(Object routerObj, int type) {
+ return ((android.media.MediaRouter)routerObj).getSelectedRoute(type);
+ }
+
+ public static void selectRoute(Object routerObj, int types, Object routeObj) {
+ ((android.media.MediaRouter)routerObj).selectRoute(types,
+ (android.media.MediaRouter.RouteInfo)routeObj);
+ }
+
+ public static void addCallback(Object routerObj, int types, Object callbackObj) {
+ ((android.media.MediaRouter)routerObj).addCallback(types,
+ (android.media.MediaRouter.Callback)callbackObj);
+ }
+
+ public static void removeCallback(Object routerObj, Object callbackObj) {
+ ((android.media.MediaRouter)routerObj).removeCallback(
+ (android.media.MediaRouter.Callback)callbackObj);
+ }
+
+ public static Object createRouteCategory(Object routerObj,
+ String name, boolean isGroupable) {
+ return ((android.media.MediaRouter)routerObj).createRouteCategory(name, isGroupable);
+ }
+
+ public static Object createUserRoute(Object routerObj, Object categoryObj) {
+ return ((android.media.MediaRouter)routerObj).createUserRoute(
+ (android.media.MediaRouter.RouteCategory)categoryObj);
+ }
+
+ public static void addUserRoute(Object routerObj, Object routeObj) {
+ ((android.media.MediaRouter)routerObj).addUserRoute(
+ (android.media.MediaRouter.UserRouteInfo)routeObj);
+ }
+
+ public static void removeUserRoute(Object routerObj, Object routeObj) {
+ ((android.media.MediaRouter)routerObj).removeUserRoute(
+ (android.media.MediaRouter.UserRouteInfo)routeObj);
+ }
+
+ public static Object createCallback(Callback callback) {
+ return new CallbackProxy<Callback>(callback);
+ }
+
+ public static Object createVolumeCallback(VolumeCallback callback) {
+ return new VolumeCallbackProxy<VolumeCallback>(callback);
+ }
+
+ public static final class RouteInfo {
+ public static CharSequence getName(Object routeObj, Context context) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getName(context);
+ }
+
+ public static CharSequence getStatus(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getStatus();
+ }
+
+ public static int getSupportedTypes(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getSupportedTypes();
+ }
+
+ public static Object getCategory(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getCategory();
+ }
+
+ public static Drawable getIconDrawable(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getIconDrawable();
+ }
+
+ public static int getPlaybackType(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackType();
+ }
+
+ public static int getPlaybackStream(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackStream();
+ }
+
+ public static int getVolume(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getVolume();
+ }
+
+ public static int getVolumeMax(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeMax();
+ }
+
+ public static int getVolumeHandling(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeHandling();
+ }
+
+ public static Object getTag(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getTag();
+ }
+
+ public static void setTag(Object routeObj, Object tag) {
+ ((android.media.MediaRouter.RouteInfo)routeObj).setTag(tag);
+ }
+
+ public static void requestSetVolume(Object routeObj, int volume) {
+ ((android.media.MediaRouter.RouteInfo)routeObj).requestSetVolume(volume);
+ }
+
+ public static void requestUpdateVolume(Object routeObj, int direction) {
+ ((android.media.MediaRouter.RouteInfo)routeObj).requestUpdateVolume(direction);
+ }
+
+ public static Object getGroup(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getGroup();
+ }
+
+ public static boolean isGroup(Object routeObj) {
+ return routeObj instanceof android.media.MediaRouter.RouteGroup;
+ }
+ }
+
+ public static final class RouteGroup {
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getGroupedRoutes(Object groupObj) {
+ final android.media.MediaRouter.RouteGroup group =
+ (android.media.MediaRouter.RouteGroup)groupObj;
+ final int count = group.getRouteCount();
+ List out = new ArrayList(count);
+ for (int i = 0; i < count; i++) {
+ out.add(group.getRouteAt(i));
+ }
+ return out;
+ }
+ }
+
+ public static final class UserRouteInfo {
+ public static void setName(Object routeObj, CharSequence name) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setName(name);
+ }
+
+ public static void setStatus(Object routeObj, CharSequence status) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setStatus(status);
+ }
+
+ public static void setIconDrawable(Object routeObj, Drawable icon) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setIconDrawable(icon);
+ }
+
+ public static void setPlaybackType(Object routeObj, int type) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackType(type);
+ }
+
+ public static void setPlaybackStream(Object routeObj, int stream) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackStream(stream);
+ }
+
+ public static void setVolume(Object routeObj, int volume) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolume(volume);
+ }
+
+ public static void setVolumeMax(Object routeObj, int volumeMax) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeMax(volumeMax);
+ }
+
+ public static void setVolumeHandling(Object routeObj, int volumeHandling) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeHandling(volumeHandling);
+ }
+
+ public static void setVolumeCallback(Object routeObj, Object volumeCallbackObj) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeCallback(
+ (android.media.MediaRouter.VolumeCallback)volumeCallbackObj);
+ }
+
+ public static void setRemoteControlClient(Object routeObj, Object rccObj) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setRemoteControlClient(
+ (android.media.RemoteControlClient)rccObj);
+ }
+ }
+
+ public static final class RouteCategory {
+ public static CharSequence getName(Object categoryObj, Context context) {
+ return ((android.media.MediaRouter.RouteCategory)categoryObj).getName(context);
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getRoutes(Object categoryObj) {
+ ArrayList out = new ArrayList();
+ ((android.media.MediaRouter.RouteCategory)categoryObj).getRoutes(out);
+ return out;
+ }
+
+ public static int getSupportedTypes(Object categoryObj) {
+ return ((android.media.MediaRouter.RouteCategory)categoryObj).getSupportedTypes();
+ }
+
+ public static boolean isGroupable(Object categoryObj) {
+ return ((android.media.MediaRouter.RouteCategory)categoryObj).isGroupable();
+ }
+ }
+
+ public static interface Callback {
+ public void onRouteSelected(int type, Object routeObj);
+ public void onRouteUnselected(int type, Object routeObj);
+ public void onRouteAdded(Object routeObj);
+ public void onRouteRemoved(Object routeObj);
+ public void onRouteChanged(Object routeObj);
+ public void onRouteGrouped(Object routeObj, Object groupObj, int index);
+ public void onRouteUngrouped(Object routeObj, Object groupObj);
+ public void onRouteVolumeChanged(Object routeObj);
+ }
+
+ public static interface VolumeCallback {
+ public void onVolumeSetRequest(Object routeObj, int volume);
+ public void onVolumeUpdateRequest(Object routeObj, int direction);
+ }
+
+ /**
+ * Workaround for limitations of selectRoute() on JB and JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class SelectRouteWorkaround {
+ private Method mSelectRouteIntMethod;
+
+ public SelectRouteWorkaround() {
+ if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
+ throw new UnsupportedOperationException();
+ }
+ try {
+ mSelectRouteIntMethod = android.media.MediaRouter.class.getMethod(
+ "selectRouteInt", int.class, android.media.MediaRouter.RouteInfo.class);
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public void selectRoute(Object routerObj, int types, Object routeObj) {
+ android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+ android.media.MediaRouter.RouteInfo route =
+ (android.media.MediaRouter.RouteInfo)routeObj;
+
+ int routeTypes = route.getSupportedTypes();
+ if ((routeTypes & ROUTE_TYPE_USER) == 0) {
+ // Handle non-user routes.
+ // On JB and JB MR1, the selectRoute() API only supports programmatically
+ // selecting user routes. So instead we rely on the hidden selectRouteInt()
+ // method on these versions of the platform.
+ // This limitation was removed in JB MR2.
+ if (mSelectRouteIntMethod != null) {
+ try {
+ mSelectRouteIntMethod.invoke(router, types, route);
+ return; // success!
+ } catch (IllegalAccessException ex) {
+ Log.w(TAG, "Cannot programmatically select non-user route. "
+ + "Media routing may not work.", ex);
+ } catch (InvocationTargetException ex) {
+ Log.w(TAG, "Cannot programmatically select non-user route. "
+ + "Media routing may not work.", ex);
+ }
+ } else {
+ Log.w(TAG, "Cannot programmatically select non-user route "
+ + "because the platform is missing the selectRouteInt() "
+ + "method. Media routing may not work.");
+ }
+ }
+
+ // Default handling.
+ router.selectRoute(types, route);
+ }
+ }
+
+ /**
+ * Workaround the fact that the getDefaultRoute() method does not exist in JB and JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class GetDefaultRouteWorkaround {
+ private Method mGetSystemAudioRouteMethod;
+
+ public GetDefaultRouteWorkaround() {
+ if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
+ throw new UnsupportedOperationException();
+ }
+ try {
+ mGetSystemAudioRouteMethod =
+ android.media.MediaRouter.class.getMethod("getSystemAudioRoute");
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public Object getDefaultRoute(Object routerObj) {
+ android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+
+ if (mGetSystemAudioRouteMethod != null) {
+ try {
+ return mGetSystemAudioRouteMethod.invoke(router);
+ } catch (IllegalAccessException ex) {
+ } catch (InvocationTargetException ex) {
+ }
+ }
+
+ // Could not find the method or it does not work.
+ // Return the first route and hope for the best.
+ return router.getRouteAt(0);
+ }
+ }
+
+ static class CallbackProxy<T extends Callback>
+ extends android.media.MediaRouter.Callback {
+ protected final T mCallback;
+
+ public CallbackProxy(T callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onRouteSelected(android.media.MediaRouter router,
+ int type, android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteSelected(type, route);
+ }
+
+ @Override
+ public void onRouteUnselected(android.media.MediaRouter router,
+ int type, android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteUnselected(type, route);
+ }
+
+ @Override
+ public void onRouteAdded(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteAdded(route);
+ }
+
+ @Override
+ public void onRouteRemoved(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteRemoved(route);
+ }
+
+ @Override
+ public void onRouteChanged(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteChanged(route);
+ }
+
+ @Override
+ public void onRouteGrouped(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route,
+ android.media.MediaRouter.RouteGroup group, int index) {
+ mCallback.onRouteGrouped(route, group, index);
+ }
+
+ @Override
+ public void onRouteUngrouped(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route,
+ android.media.MediaRouter.RouteGroup group) {
+ mCallback.onRouteUngrouped(route, group);
+ }
+
+ @Override
+ public void onRouteVolumeChanged(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteVolumeChanged(route);
+ }
+ }
+
+ static class VolumeCallbackProxy<T extends VolumeCallback>
+ extends android.media.MediaRouter.VolumeCallback {
+ protected final T mCallback;
+
+ public VolumeCallbackProxy(T callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onVolumeSetRequest(android.media.MediaRouter.RouteInfo route,
+ int volume) {
+ mCallback.onVolumeSetRequest(route, volume);
+ }
+
+ @Override
+ public void onVolumeUpdateRequest(android.media.MediaRouter.RouteInfo route,
+ int direction) {
+ mCallback.onVolumeUpdateRequest(route, direction);
+ }
+ }
+}
diff --git a/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.java b/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.java
index 350b275..9ad8992 100644
--- a/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.java
+++ b/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.java
@@ -40,7 +40,8 @@
PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
int mProgressMax, int mProgress, boolean mProgressIndeterminate,
boolean useChronometer, int priority, CharSequence subText, boolean localOnly,
- Bundle extras, String groupKey, boolean groupSummary, String sortKey) {
+ ArrayList<String> people, Bundle extras, String groupKey, boolean groupSummary,
+ String sortKey) {
b = new Notification.Builder(context)
.setWhen(n.when)
.setSmallIcon(n.icon, n.iconLevel)
@@ -70,6 +71,10 @@
if (extras != null) {
mExtras.putAll(extras);
}
+ if (people != null && !people.isEmpty()) {
+ mExtras.putStringArray(Notification.EXTRA_PEOPLE,
+ people.toArray(new String[people.size()]));
+ }
if (localOnly) {
mExtras.putBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY, true);
}
diff --git a/v4/kitkat/android/support/v4/graphics/BitmapCompatKitKat.java b/v4/kitkat/android/support/v4/graphics/BitmapCompatKitKat.java
new file mode 100644
index 0000000..2d4a66e
--- /dev/null
+++ b/v4/kitkat/android/support/v4/graphics/BitmapCompatKitKat.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v4.graphics;
+
+import android.graphics.Bitmap;
+
+/**
+ * Implementation of BitmapCompat that can use KitKat APIs.
+ */
+class BitmapCompatKitKat {
+
+ static int getAllocationByteCount(Bitmap bitmap) {
+ return bitmap.getAllocationByteCount();
+ }
+
+}
diff --git a/v4/kitkat/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java b/v4/kitkat/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java
index 618a02d..4f77b8b 100644
--- a/v4/kitkat/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java
+++ b/v4/kitkat/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java
@@ -22,11 +22,77 @@
* KitKat-specific AccessibilityNodeInfo API implementation.
*/
class AccessibilityNodeInfoCompatKitKat {
- public static int getLiveRegion(Object info) {
+ static int getLiveRegion(Object info) {
return ((AccessibilityNodeInfo) info).getLiveRegion();
}
- public static void setLiveRegion(Object info, int mode) {
+ static void setLiveRegion(Object info, int mode) {
((AccessibilityNodeInfo) info).setLiveRegion(mode);
}
+
+ static Object getCollectionInfo(Object info) {
+ return ((AccessibilityNodeInfo) info).getCollectionInfo();
+ }
+
+ static Object getCollectionItemInfo(Object info) {
+ return ((AccessibilityNodeInfo) info).getCollectionItemInfo();
+ }
+
+ static Object getRangeInfo(Object info) {
+ return ((AccessibilityNodeInfo) info).getRangeInfo();
+ }
+
+ static class CollectionInfo {
+ static int getColumnCount(Object info) {
+ return ((AccessibilityNodeInfo.CollectionInfo) info).getColumnCount();
+ }
+
+ static int getRowCount(Object info) {
+ return ((AccessibilityNodeInfo.CollectionInfo) info).getRowCount();
+ }
+
+ static boolean isHierarchical(Object info) {
+ return ((AccessibilityNodeInfo.CollectionInfo) info).isHierarchical();
+ }
+ }
+
+ static class CollectionItemInfo {
+ static int getColumnIndex(Object info) {
+ return ((AccessibilityNodeInfo.CollectionItemInfo) info).getColumnIndex();
+ }
+
+ static int getColumnSpan(Object info) {
+ return ((AccessibilityNodeInfo.CollectionItemInfo) info).getColumnSpan();
+ }
+
+ static int getRowIndex(Object info) {
+ return ((AccessibilityNodeInfo.CollectionItemInfo) info).getRowIndex();
+ }
+
+ static int getRowSpan(Object info) {
+ return ((AccessibilityNodeInfo.CollectionItemInfo) info).getRowSpan();
+ }
+
+ static boolean isHeading(Object info) {
+ return ((AccessibilityNodeInfo.CollectionItemInfo) info).isHeading();
+ }
+ }
+
+ static class RangeInfo {
+ static float getCurrent(Object info) {
+ return ((AccessibilityNodeInfo.RangeInfo) info).getCurrent();
+ }
+
+ static float getMax(Object info) {
+ return ((AccessibilityNodeInfo.RangeInfo) info).getMax();
+ }
+
+ static float getMin(Object info) {
+ return ((AccessibilityNodeInfo.RangeInfo) info).getMin();
+ }
+
+ static int getType(Object info) {
+ return ((AccessibilityNodeInfo.RangeInfo) info).getType();
+ }
+ }
}
diff --git a/v4/tests/java/android/support/v4/widget/GingerbreadScrollerCompatTest.java b/v4/tests/java/android/support/v4/widget/GingerbreadScrollerCompatTest.java
index a5c96b0..d4420fe 100644
--- a/v4/tests/java/android/support/v4/widget/GingerbreadScrollerCompatTest.java
+++ b/v4/tests/java/android/support/v4/widget/GingerbreadScrollerCompatTest.java
@@ -20,6 +20,6 @@
public class GingerbreadScrollerCompatTest extends ScrollerCompatTestBase {
public GingerbreadScrollerCompatTest() {
- super(Build.VERSION_CODES.GINGERBREAD);
+ super(9);
}
}
diff --git a/v4/tests/java/android/support/v4/widget/IcsScrollerCompatTest.java b/v4/tests/java/android/support/v4/widget/IcsScrollerCompatTest.java
index ccdc68d..7d0e9a5 100644
--- a/v4/tests/java/android/support/v4/widget/IcsScrollerCompatTest.java
+++ b/v4/tests/java/android/support/v4/widget/IcsScrollerCompatTest.java
@@ -20,6 +20,6 @@
public class IcsScrollerCompatTest extends ScrollerCompatTestBase {
public IcsScrollerCompatTest() {
- super(Build.VERSION_CODES.ICE_CREAM_SANDWICH);
+ super(14);
}
}
diff --git a/v7/appcompat/res/values-af/strings.xml b/v7/appcompat/res/values-af/strings.xml
new file mode 100644
index 0000000..f7348c4
--- /dev/null
+++ b/v7/appcompat/res/values-af/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Klaar"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navigeer tuis"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navigeer op"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Nog opsies"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Soek"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Soeknavraag"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Vee navraag uit"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Dien navraag in"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Stemsoektog"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Kies \'n program"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Sien alles"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Deel met %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Deel met"</string>
+</resources>
diff --git a/v7/appcompat/res/values-am/strings.xml b/v7/appcompat/res/values-am/strings.xml
new file mode 100644
index 0000000..e849b31
--- /dev/null
+++ b/v7/appcompat/res/values-am/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"ተከናውኗል"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"ወደ መነሻ ይዳስሱ"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ወደ ላይ ይዳስሱ"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"ተጨማሪ አማራጮች"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"ፍለጋ"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"የፍለጋ ጥያቄ"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"መጠይቅ አጽዳ"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"መጠይቅ ያስረክቡ"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"የድምፅ ፍለጋ"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"መተግበሪያ ይምረጡ"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"ሁሉንም ይመልከቱ"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"ከ%s ጋር ያጋሩ"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ከሚከተለው ጋር ያጋሩ"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ar/strings.xml b/v7/appcompat/res/values-ar/strings.xml
new file mode 100644
index 0000000..93dde04
--- /dev/null
+++ b/v7/appcompat/res/values-ar/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"تم"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"التنقل إلى الشاشة الرئيسية"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"التنقل إلى أعلى"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"خيارات إضافية"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"بحث"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"طلب البحث"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"محو طلب البحث"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"إرسال طلب البحث"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"البحث الصوتي"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"اختيار تطبيق"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"عرض الكل"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"مشاركة مع %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"مشاركة مع"</string>
+</resources>
diff --git a/v7/appcompat/res/values-bg/strings.xml b/v7/appcompat/res/values-bg/strings.xml
new file mode 100644
index 0000000..de3bde8
--- /dev/null
+++ b/v7/appcompat/res/values-bg/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Готово"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Придвижване към „Начало“"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Придвижване нагоре"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Още опции"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Търсене"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Заявка за търсене"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Изчистване на заявката"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Изпращане на заявката"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Гласово търсене"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Изберете приложение"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Вижте всички"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Споделяне със: %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Споделяне със:"</string>
+</resources>
diff --git a/v7/appcompat/res/values-bn-rBD/strings.xml b/v7/appcompat/res/values-bn-rBD/strings.xml
new file mode 100644
index 0000000..393240f
--- /dev/null
+++ b/v7/appcompat/res/values-bn-rBD/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"সম্পন্ন হয়েছে"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"হোম এ নেভিগেট করুন"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"উপরের দিকে নেভিগেট করুন"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"আরো বিকল্প"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"অনুসন্ধান করুন"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ক্যোয়ারী অনুসন্ধান করুন"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"ক্যোয়ারী সাফ করুন"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ক্যোয়ারী জমা দিন"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"ভয়েস অনুসন্ধান"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"একটি অ্যাপ্লিকেশান চয়ন করুন"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"সবগুলো দেখুন"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s এর সাথে ভাগ করুন"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"এর সাথে ভাগ করুন"</string>
+</resources>
diff --git a/v7/appcompat/res/values-bn-rWB/strings.xml b/v7/appcompat/res/values-bn-rWB/strings.xml
new file mode 100644
index 0000000..393240f
--- /dev/null
+++ b/v7/appcompat/res/values-bn-rWB/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"সম্পন্ন হয়েছে"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"হোম এ নেভিগেট করুন"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"উপরের দিকে নেভিগেট করুন"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"আরো বিকল্প"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"অনুসন্ধান করুন"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ক্যোয়ারী অনুসন্ধান করুন"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"ক্যোয়ারী সাফ করুন"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ক্যোয়ারী জমা দিন"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"ভয়েস অনুসন্ধান"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"একটি অ্যাপ্লিকেশান চয়ন করুন"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"সবগুলো দেখুন"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s এর সাথে ভাগ করুন"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"এর সাথে ভাগ করুন"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ca/strings.xml b/v7/appcompat/res/values-ca/strings.xml
new file mode 100644
index 0000000..bfd4cb0
--- /dev/null
+++ b/v7/appcompat/res/values-ca/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Fet"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navega a la pàgina d\'inici"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navega cap a dalt"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Més opcions"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Cerca"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Consulta de cerca"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Esborra la consulta"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Envia la consulta"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Cerca per veu"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Selecciona una aplicació"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Mostra\'ls tots"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Comparteix amb %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Comparteix amb"</string>
+</resources>
diff --git a/v7/appcompat/res/values-cs/strings.xml b/v7/appcompat/res/values-cs/strings.xml
new file mode 100644
index 0000000..1465fdc
--- /dev/null
+++ b/v7/appcompat/res/values-cs/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Hotovo"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Přejít na plochu"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Přejít nahoru"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Více možností"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Hledat"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Vyhledávací dotaz"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Smazat dotaz"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Odeslat dotaz"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Hlasové vyhledávání"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Vybrat aplikaci"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Zobrazit vše"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Sdílet pomocí %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Sdílet pomocí"</string>
+</resources>
diff --git a/v7/appcompat/res/values-da/strings.xml b/v7/appcompat/res/values-da/strings.xml
new file mode 100644
index 0000000..b178513
--- /dev/null
+++ b/v7/appcompat/res/values-da/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Luk"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Naviger hjem"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Naviger op"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Flere muligheder"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Søg"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Søgeforespørgsel"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Ryd forespørgslen"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Indsend forespørgslen"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Stemmesøgning"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Vælg en app"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Se alle"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Del med %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Del med"</string>
+</resources>
diff --git a/v7/appcompat/res/values-de/strings.xml b/v7/appcompat/res/values-de/strings.xml
new file mode 100644
index 0000000..6da4b71
--- /dev/null
+++ b/v7/appcompat/res/values-de/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Fertig"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Zur Startseite"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Nach oben"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Weitere Optionen"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Suchen"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Suchanfrage"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Suchanfrage löschen"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Suchanfrage senden"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Sprachsuche"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"App auswählen"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Alle ansehen"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Freigeben für %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Freigeben für"</string>
+</resources>
diff --git a/v7/appcompat/res/values-el/strings.xml b/v7/appcompat/res/values-el/strings.xml
new file mode 100644
index 0000000..4c0e286
--- /dev/null
+++ b/v7/appcompat/res/values-el/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Τέλος"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Πλοήγηση στην αρχική σελίδα"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Πλοήγηση προς τα επάνω"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Περισσότερες επιλογές"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Αναζήτηση"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Ερώτημα αναζήτησης"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Διαγραφή ερωτήματος"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Υποβολή ερωτήματος"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Φωνητική αναζήτηση"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Επιλέξτε κάποια εφαρμογή"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Προβολή όλων"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Κοινή χρήση με %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Κοινή χρήση με"</string>
+</resources>
diff --git a/v7/appcompat/res/values-en-rGB/strings.xml b/v7/appcompat/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..3ec0b0e
--- /dev/null
+++ b/v7/appcompat/res/values-en-rGB/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Finished"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navigate home"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navigate up"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"More options"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Search"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Search query"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Clear query"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Submit query"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Voice search"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Choose an app"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"See all"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Share with %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Share with"</string>
+</resources>
diff --git a/v7/appcompat/res/values-en-rIN/strings.xml b/v7/appcompat/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..3ec0b0e
--- /dev/null
+++ b/v7/appcompat/res/values-en-rIN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Finished"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navigate home"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navigate up"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"More options"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Search"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Search query"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Clear query"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Submit query"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Voice search"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Choose an app"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"See all"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Share with %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Share with"</string>
+</resources>
diff --git a/v7/appcompat/res/values-es-rUS/strings.xml b/v7/appcompat/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..6ab7942
--- /dev/null
+++ b/v7/appcompat/res/values-es-rUS/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Listo"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navegar a la página principal"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navegar hacia arriba"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Más opciones"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Búsqueda"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Consulta de búsqueda"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Eliminar la consulta"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Enviar consulta"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Búsqueda por voz"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Elige una aplicación."</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Ver todo"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Compartir con %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Compartir con"</string>
+</resources>
diff --git a/v7/appcompat/res/values-es/strings.xml b/v7/appcompat/res/values-es/strings.xml
new file mode 100644
index 0000000..ed15b35
--- /dev/null
+++ b/v7/appcompat/res/values-es/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Listo"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Ir a la pantalla de inicio"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Desplazarse hacia arriba"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Más opciones"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Buscar"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Consulta"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Borrar consulta"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Enviar consulta"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Búsqueda por voz"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Seleccionar una aplicación"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Ver todo"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Compartir con %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Compartir con"</string>
+</resources>
diff --git a/v7/appcompat/res/values-et-rEE/strings.xml b/v7/appcompat/res/values-et-rEE/strings.xml
new file mode 100644
index 0000000..2ae925d
--- /dev/null
+++ b/v7/appcompat/res/values-et-rEE/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Valmis"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navigeerimine avaekraanile"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navigeerimine üles"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Rohkem valikuid"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Otsing"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Otsingupäring"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Päringu tühistamine"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Päringu esitamine"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Häälotsing"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Valige rakendus"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Kuva kõik"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Jagamine kasutajaga %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Jagamine:"</string>
+</resources>
diff --git a/v7/appcompat/res/values-eu-rES/strings.xml b/v7/appcompat/res/values-eu-rES/strings.xml
new file mode 100644
index 0000000..ee6ac4d
--- /dev/null
+++ b/v7/appcompat/res/values-eu-rES/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Eginda"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Joan orri nagusira"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Joan gora"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Aukera gehiago"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Bilatu"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Bilaketa-kontsulta"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Garbitu kontsulta"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Bidali kontsulta"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Ahots bidezko bilaketa"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Aukeratu aplikazio bat"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Ikusi guztiak"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Partekatu %s erabiltzailearekin"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Partekatu hauekin"</string>
+</resources>
diff --git a/v7/appcompat/res/values-eu-rPV/strings.xml b/v7/appcompat/res/values-eu-rPV/strings.xml
new file mode 100644
index 0000000..ee6ac4d
--- /dev/null
+++ b/v7/appcompat/res/values-eu-rPV/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Eginda"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Joan orri nagusira"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Joan gora"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Aukera gehiago"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Bilatu"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Bilaketa-kontsulta"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Garbitu kontsulta"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Bidali kontsulta"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Ahots bidezko bilaketa"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Aukeratu aplikazio bat"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Ikusi guztiak"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Partekatu %s erabiltzailearekin"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Partekatu hauekin"</string>
+</resources>
diff --git a/v7/appcompat/res/values-fa/strings.xml b/v7/appcompat/res/values-fa/strings.xml
new file mode 100644
index 0000000..8e10e92
--- /dev/null
+++ b/v7/appcompat/res/values-fa/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"انجام شد"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"پیمایش به صفحه اصلی"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"پیمایش به بالا"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"گزینههای بیشتر"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"جستجو"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"عبارت جستجو"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"پاک کردن عبارت جستجو"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ارسال عبارت جستجو"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"جستجوی شفاهی"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"انتخاب برنامه"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"مشاهده همه"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"اشتراکگذاری با %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"اشتراکگذاری با"</string>
+</resources>
diff --git a/v7/appcompat/res/values-fi/strings.xml b/v7/appcompat/res/values-fi/strings.xml
new file mode 100644
index 0000000..6755cea
--- /dev/null
+++ b/v7/appcompat/res/values-fi/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Valmis"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Siirry etusivulle"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Siirry ylös"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Lisää"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Haku"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Hakulauseke"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Tyhjennä kysely"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Lähetä kysely"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Puhehaku"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Valitse sovellus"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Näytä kaikki"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Jakaminen: %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Jakaminen:"</string>
+</resources>
diff --git a/v7/appcompat/res/values-fr-rCA/strings.xml b/v7/appcompat/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..417705a
--- /dev/null
+++ b/v7/appcompat/res/values-fr-rCA/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Terminé"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Revenir à l\'accueil"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Revenir en haut de la page"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Plus d\'options"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Rechercher"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Requête de recherche"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Effacer la requête"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Envoyer la requête"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Recherche vocale"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Sélectionnez une application"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Voir toutes les chaînes"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Partager avec %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Partager avec"</string>
+</resources>
diff --git a/v7/appcompat/res/values-fr/strings.xml b/v7/appcompat/res/values-fr/strings.xml
new file mode 100644
index 0000000..27b8f38
--- /dev/null
+++ b/v7/appcompat/res/values-fr/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"OK"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Revenir à l\'accueil"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Revenir en haut de la page"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Plus d\'options"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Rechercher"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Requête de recherche"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Effacer la requête"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Envoyer la requête"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Recherche vocale"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Sélectionner une application"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Tout afficher"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Partager avec %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Partager avec"</string>
+</resources>
diff --git a/v7/appcompat/res/values-gl-rES/strings.xml b/v7/appcompat/res/values-gl-rES/strings.xml
new file mode 100644
index 0000000..1d0d50d
--- /dev/null
+++ b/v7/appcompat/res/values-gl-rES/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Feito"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Ir á páxina de inicio"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Desprazarse cara arriba"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Máis opcións"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Buscar"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Consulta de busca"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Borrar consulta"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Enviar consulta"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Busca de voz"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Escoller unha aplicación"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Ver todas"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Compartir con %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Compartir con"</string>
+</resources>
diff --git a/v7/appcompat/res/values-gl-rGA/strings.xml b/v7/appcompat/res/values-gl-rGA/strings.xml
new file mode 100644
index 0000000..1d0d50d
--- /dev/null
+++ b/v7/appcompat/res/values-gl-rGA/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Feito"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Ir á páxina de inicio"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Desprazarse cara arriba"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Máis opcións"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Buscar"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Consulta de busca"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Borrar consulta"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Enviar consulta"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Busca de voz"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Escoller unha aplicación"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Ver todas"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Compartir con %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Compartir con"</string>
+</resources>
diff --git a/v7/appcompat/res/values-hi/strings.xml b/v7/appcompat/res/values-hi/strings.xml
new file mode 100644
index 0000000..b236ebb
--- /dev/null
+++ b/v7/appcompat/res/values-hi/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"पूर्ण"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"मुखपृष्ठ पर नेविगेट करें"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ऊपर नेविगेट करें"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"अधिक विकल्प"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"खोजें"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"खोज क्वेरी"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"क्वेरी साफ़ करें"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"क्वेरी सबमिट करें"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"ध्वनि खोज"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"कोई एप्लिकेशन चुनें"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"सभी देखें"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s के साथ साझा करें"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"इसके द्वारा साझा करें"</string>
+</resources>
diff --git a/v7/appcompat/res/values-hr/strings.xml b/v7/appcompat/res/values-hr/strings.xml
new file mode 100644
index 0000000..680e39f
--- /dev/null
+++ b/v7/appcompat/res/values-hr/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Gotovo"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Idi na početnu"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Idi gore"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Dodatne opcije"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Pretraživanje"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Upit za pretraživanje"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Izbriši upit"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Pošalji upit"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Glasovno pretraživanje"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Odabir aplikacije"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Prikaži sve"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Dijeljenje sa: %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Dijeljenje sa"</string>
+</resources>
diff --git a/v7/appcompat/res/values-hu/strings.xml b/v7/appcompat/res/values-hu/strings.xml
new file mode 100644
index 0000000..52dafb0
--- /dev/null
+++ b/v7/appcompat/res/values-hu/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Kész"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Ugrás a főoldalra"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Felfelé mozgatás"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"További lehetőségek"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Keresés"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Keresési lekérdezés"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Lekérdezés törlése"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Lekérdezés küldése"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Hangalapú keresés"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Válasszon ki egy alkalmazást"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Összes megtekintése"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Megosztás a következővel: %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Megosztás a következővel:"</string>
+</resources>
diff --git a/v7/appcompat/res/values-hy-rAM/strings.xml b/v7/appcompat/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000..6c0ee27
--- /dev/null
+++ b/v7/appcompat/res/values-hy-rAM/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Կատարված է"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Ուղղվել տուն"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Ուղղվել վերև"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Այլ ընտրանքներ"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Որոնել"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Որոնման հարցում"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Մաքրել հարցումը"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Ուղարկել հարցումը"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Ձայնային որոնում"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Ընտրել ծրագիր"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Տեսնել բոլորը"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Տարածել ըստ %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Տարածել"</string>
+</resources>
diff --git a/v7/appcompat/res/values-in/strings.xml b/v7/appcompat/res/values-in/strings.xml
new file mode 100644
index 0000000..9481e83
--- /dev/null
+++ b/v7/appcompat/res/values-in/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Selesai"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navigasi ke beranda"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navigasi naik"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Opsi lain"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Telusuri"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Kueri penelusuran"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Hapus kueri"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Kirim kueri"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Penelusuran suara"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Pilih aplikasi"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Lihat semua"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Bagikan dengan %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Bagikan dengan"</string>
+</resources>
diff --git a/v7/appcompat/res/values-is-rIS/strings.xml b/v7/appcompat/res/values-is-rIS/strings.xml
new file mode 100644
index 0000000..8cac570
--- /dev/null
+++ b/v7/appcompat/res/values-is-rIS/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Lokið"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Fara heim"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Fara upp"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Fleiri valkostir"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Leita"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Leitarfyrirspurn"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Hreinsa fyrirspurn"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Senda fyrirspurn"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Raddleit"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Veldu forrit"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Sjá allt"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Deila með %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Deila með"</string>
+</resources>
diff --git a/v7/appcompat/res/values-it/strings.xml b/v7/appcompat/res/values-it/strings.xml
new file mode 100644
index 0000000..a8b0f2c
--- /dev/null
+++ b/v7/appcompat/res/values-it/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Fine"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Vai alla home page"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Vai in alto"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Altre opzioni"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Cerca"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Query di ricerca"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Cancella query"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Invia query"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Ricerca vocale"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Scegli un\'applicazione"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Visualizza tutte"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Condividi con %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Condividi con"</string>
+</resources>
diff --git a/v7/appcompat/res/values-iw/strings.xml b/v7/appcompat/res/values-iw/strings.xml
new file mode 100644
index 0000000..1af07df
--- /dev/null
+++ b/v7/appcompat/res/values-iw/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"בוצע"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"נווט לדף הבית"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"נווט למעלה"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"עוד אפשרויות"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"חפש"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"שאילתת חיפוש"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"מחק שאילתה"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"שלח שאילתה"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"חיפוש קולי"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"בחר אפליקציה"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"ראה הכול"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"שתף עם %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"שתף עם"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ja/strings.xml b/v7/appcompat/res/values-ja/strings.xml
new file mode 100644
index 0000000..659358a
--- /dev/null
+++ b/v7/appcompat/res/values-ja/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"完了"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"ホームへ移動"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"上へ移動"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"その他のオプション"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"検索"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"検索キーワード"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"検索キーワードを削除"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"検索キーワードを送信"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"音声検索"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"アプリの選択"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"すべて表示"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%sと共有"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"共有"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ka-rGE/strings.xml b/v7/appcompat/res/values-ka-rGE/strings.xml
new file mode 100644
index 0000000..0c430b1
--- /dev/null
+++ b/v7/appcompat/res/values-ka-rGE/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"დასრულდა"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"მთავარზე ნავიგაცია"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ზემოთ ნავიგაცია"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"მეტი ვარიანტები"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"ძიება"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ძიების მოთხოვნა"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"მოთხოვნის გასუფთავება"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"მოთხოვნის გადაგზავნა"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"ხმოვანი ძიება"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"აპის არჩევა"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"ყველას ნახვა"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s-თან გაზიარება"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"გაზიარება:"</string>
+</resources>
diff --git a/v7/appcompat/res/values-kk-rKZ/strings.xml b/v7/appcompat/res/values-kk-rKZ/strings.xml
new file mode 100644
index 0000000..d3ad9e8
--- /dev/null
+++ b/v7/appcompat/res/values-kk-rKZ/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Орындалды"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Негізгі бетте қозғалу"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Жоғары қозғалу"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Басқа опциялар"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Іздеу"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Сұрақты іздеу"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Сұрақты жою"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Сұрақты жіберу"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Дауыс арқылы іздеу"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Қолданбаны таңдау"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Барлығын көру"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s бөлісу"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Бөлісу"</string>
+</resources>
diff --git a/v7/appcompat/res/values-km-rKH/strings.xml b/v7/appcompat/res/values-km-rKH/strings.xml
new file mode 100644
index 0000000..df78372
--- /dev/null
+++ b/v7/appcompat/res/values-km-rKH/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"រួចរាល់"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"រកមើលទៅដើម"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"រកមើលឡើងលើ"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"ជម្រើសច្រើនទៀត"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"ស្វែងរក"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ស្វែងរកសំណួរ"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"សម្អាតសំណួរ"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ដាក់ស្នើសំណួរ"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"ការស្វែងរកសំឡេង"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"ជ្រើសកម្មវិធី"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"មើលទាំងអស់"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"ចែករំលែកជាមួយ %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ចែករំលែកជាមួយ"</string>
+</resources>
diff --git a/v7/appcompat/res/values-kn-rIN/strings.xml b/v7/appcompat/res/values-kn-rIN/strings.xml
new file mode 100644
index 0000000..6af7a39
--- /dev/null
+++ b/v7/appcompat/res/values-kn-rIN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"ಮುಗಿದಿದೆ"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"ಮುಖಪುಟವನ್ನು ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ಮೇಲಕ್ಕೆ ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"ಹುಡುಕು"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ಪ್ರಶ್ನೆಯನ್ನು ಹುಡುಕಿ"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"ಪ್ರಶ್ನೆಯನ್ನು ತೆರವುಗೊಳಿಸು"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ಪ್ರಶ್ನೆಯನ್ನು ಸಲ್ಲಿಸು"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"ಧ್ವನಿ ಹುಡುಕಾಟ"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"ಒಂದು ಅಪ್ಲಿಕೇಶನ್ ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"ಎಲ್ಲವನ್ನೂ ನೋಡಿ"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s ಜೊತೆಗೆ ಹಂಚಿಕೊಳ್ಳಿ"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ಇವರೊಂದಿಗೆ ಹಂಚಿಕೊಳ್ಳಿ"</string>
+</resources>
diff --git a/v7/appcompat/res/values-kn-rKA/strings.xml b/v7/appcompat/res/values-kn-rKA/strings.xml
new file mode 100644
index 0000000..6af7a39
--- /dev/null
+++ b/v7/appcompat/res/values-kn-rKA/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"ಮುಗಿದಿದೆ"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"ಮುಖಪುಟವನ್ನು ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ಮೇಲಕ್ಕೆ ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"ಹುಡುಕು"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ಪ್ರಶ್ನೆಯನ್ನು ಹುಡುಕಿ"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"ಪ್ರಶ್ನೆಯನ್ನು ತೆರವುಗೊಳಿಸು"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ಪ್ರಶ್ನೆಯನ್ನು ಸಲ್ಲಿಸು"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"ಧ್ವನಿ ಹುಡುಕಾಟ"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"ಒಂದು ಅಪ್ಲಿಕೇಶನ್ ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"ಎಲ್ಲವನ್ನೂ ನೋಡಿ"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s ಜೊತೆಗೆ ಹಂಚಿಕೊಳ್ಳಿ"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ಇವರೊಂದಿಗೆ ಹಂಚಿಕೊಳ್ಳಿ"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ko/strings.xml b/v7/appcompat/res/values-ko/strings.xml
new file mode 100644
index 0000000..d331975
--- /dev/null
+++ b/v7/appcompat/res/values-ko/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"완료"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"홈 탐색"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"위로 탐색"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"옵션 더보기"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"검색"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"검색어"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"검색어 삭제"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"검색어 보내기"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"음성 검색"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"앱 선택"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"전체 보기"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s와(과) 공유"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"공유 대상"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ky-rKG/strings.xml b/v7/appcompat/res/values-ky-rKG/strings.xml
new file mode 100644
index 0000000..52abd9f
--- /dev/null
+++ b/v7/appcompat/res/values-ky-rKG/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Даяр"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Үйгө багыттоо"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Жогору"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Көбүрөөк мүмкүнчүлүктөр"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Издөө"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Издөө талаптары"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Талаптарды тазалоо"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Талап жөнөтүү"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Үн аркылуу издөө"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Колдонмо тандоо"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Бардыгын көрүү"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s аркылуу бөлүшүү"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Бөлүшүү"</string>
+</resources>
diff --git a/v7/appcompat/res/values-lo-rLA/strings.xml b/v7/appcompat/res/values-lo-rLA/strings.xml
new file mode 100644
index 0000000..7eb42ea
--- /dev/null
+++ b/v7/appcompat/res/values-lo-rLA/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"ແລ້ວໆ"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"ກັບໄປໜ້າຫຼັກ"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ຂຶ້ນເທິງ"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"ໂຕເລືອກອື່ນ"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"ຊອກຫາ"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ຊອກຫາ"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"ລຶບຂໍ້ຄວາມຊອກຫາ"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ສົ່ງການຊອກຫາ"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"ຊອກຫາດ້ວຍສຽງ"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"ເລືອກແອັບຯ"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"ເບິ່ງທັງຫມົດ"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"ແບ່ງປັນກັບ %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ແບ່ງປັນກັບ"</string>
+</resources>
diff --git a/v7/appcompat/res/values-lt/strings.xml b/v7/appcompat/res/values-lt/strings.xml
new file mode 100644
index 0000000..c4738a7
--- /dev/null
+++ b/v7/appcompat/res/values-lt/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Atlikta"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Eiti į pagrindinį puslapį"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Eiti į viršų"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Daugiau parinkčių"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Paieška"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Paieškos užklausa"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Išvalyti užklausą"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Pateikti užklausą"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Paieška balsu"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Pasirinkti programą"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Peržiūrėti viską"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Bendrinti naudojant „%s“"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Bendrinti naudojant"</string>
+</resources>
diff --git a/v7/appcompat/res/values-lv/strings.xml b/v7/appcompat/res/values-lv/strings.xml
new file mode 100644
index 0000000..c33858a
--- /dev/null
+++ b/v7/appcompat/res/values-lv/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Gatavs"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Pārvietoties uz sākuma ekrānu"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Pārvietoties augšup"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Vairāk opciju"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Meklēt"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Meklēšanas vaicājums"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Notīrīt vaicājumu"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Iesniegt vaicājumu"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Meklēšana ar balsi"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Izvēlieties lietotni"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Skatīt visu"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Kopīgot ar %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Kopīgot ar:"</string>
+</resources>
diff --git a/v7/appcompat/res/values-mk-rMK/strings.xml b/v7/appcompat/res/values-mk-rMK/strings.xml
new file mode 100644
index 0000000..632728a
--- /dev/null
+++ b/v7/appcompat/res/values-mk-rMK/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Готово"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Движи се кон дома"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Движи се нагоре"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Повеќе опции"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Пребарај"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Пребарај барање"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Исчисти барање"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Поднеси барање"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Гласовно пребарување"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Избери апликација"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Види ги сите"</string>
+ <!-- String.format failed for translation -->
+ <!-- no translation found for abc_shareactionprovider_share_with_application (7165123711973476752) -->
+ <skip />
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Сподели со"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ml-rIN/strings.xml b/v7/appcompat/res/values-ml-rIN/strings.xml
new file mode 100644
index 0000000..2fb368e
--- /dev/null
+++ b/v7/appcompat/res/values-ml-rIN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"പൂർത്തിയാക്കി"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"ഹോമിലേക്ക് നാവിഗേറ്റുചെയ്യുക"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"മുകളിലേക്ക് നാവിഗേറ്റുചെയ്യുക"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"കൂടുതല് ഓപ്ഷനുകള്"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"തിരയൽ"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"തിരയൽ അന്വേഷണം"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"അന്വേഷണം മായ്ക്കുക"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"അന്വേഷണം സമർപ്പിക്കുക"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"വോയ്സ് തിരയൽ"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"ഒരു അപ്ലിക്കേഷൻ തിരഞ്ഞെടുക്കുക"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"എല്ലാം കാണുക"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s എന്നതുമായി പങ്കിടുക"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ഇവരുമായി പങ്കിടുക"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ml-rKL/strings.xml b/v7/appcompat/res/values-ml-rKL/strings.xml
new file mode 100644
index 0000000..2fb368e
--- /dev/null
+++ b/v7/appcompat/res/values-ml-rKL/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"പൂർത്തിയാക്കി"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"ഹോമിലേക്ക് നാവിഗേറ്റുചെയ്യുക"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"മുകളിലേക്ക് നാവിഗേറ്റുചെയ്യുക"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"കൂടുതല് ഓപ്ഷനുകള്"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"തിരയൽ"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"തിരയൽ അന്വേഷണം"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"അന്വേഷണം മായ്ക്കുക"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"അന്വേഷണം സമർപ്പിക്കുക"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"വോയ്സ് തിരയൽ"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"ഒരു അപ്ലിക്കേഷൻ തിരഞ്ഞെടുക്കുക"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"എല്ലാം കാണുക"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s എന്നതുമായി പങ്കിടുക"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"ഇവരുമായി പങ്കിടുക"</string>
+</resources>
diff --git a/v7/appcompat/res/values-mn-rMN/strings.xml b/v7/appcompat/res/values-mn-rMN/strings.xml
new file mode 100644
index 0000000..203e959
--- /dev/null
+++ b/v7/appcompat/res/values-mn-rMN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Дууссан"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Нүүр хуудас руу шилжих"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Дээш шилжих"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Нэмэлт сонголтууд"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Хайх"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Хайх асуулга"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Асуулгыг цэвэрлэх"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Асуулгыг илгээх"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Дуут хайлт"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Апп сонгох"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Бүгдийг харах"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s-тай хуваалцах"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Хуваалцах"</string>
+</resources>
diff --git a/v7/appcompat/res/values-mr-rIN/strings.xml b/v7/appcompat/res/values-mr-rIN/strings.xml
new file mode 100644
index 0000000..41271d4
--- /dev/null
+++ b/v7/appcompat/res/values-mr-rIN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"पूर्ण झाले"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"मुख्यपृष्ठ नेव्हिगेट करा"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"वर नेव्हिगेट करा"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"अधिक पर्याय"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"शोध"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"शोध क्वेरी"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"क्वेरी स्पष्ट करा"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"क्वेरी सबमिट करा"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"व्हॉइस शोध"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"एक अॅप निवडा"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"सर्व पहा"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s सह सामायिक करा"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"यांच्यासह सामायिक करा"</string>
+</resources>
diff --git a/v7/appcompat/res/values-mr-rMH/strings.xml b/v7/appcompat/res/values-mr-rMH/strings.xml
new file mode 100644
index 0000000..41271d4
--- /dev/null
+++ b/v7/appcompat/res/values-mr-rMH/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"पूर्ण झाले"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"मुख्यपृष्ठ नेव्हिगेट करा"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"वर नेव्हिगेट करा"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"अधिक पर्याय"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"शोध"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"शोध क्वेरी"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"क्वेरी स्पष्ट करा"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"क्वेरी सबमिट करा"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"व्हॉइस शोध"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"एक अॅप निवडा"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"सर्व पहा"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s सह सामायिक करा"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"यांच्यासह सामायिक करा"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ms-rMY/strings.xml b/v7/appcompat/res/values-ms-rMY/strings.xml
new file mode 100644
index 0000000..b174068
--- /dev/null
+++ b/v7/appcompat/res/values-ms-rMY/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Selesai"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navigasi skrin utama"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navigasi ke atas"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Lagi pilihan"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Cari"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Pertanyaan carian"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Kosongkan pertanyaan"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Serah pertanyaan"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Carian suara"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Pilih apl"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Lihat semua"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Kongsi dengan %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Kongsi dengan"</string>
+</resources>
diff --git a/v7/appcompat/res/values-my-rMM/strings.xml b/v7/appcompat/res/values-my-rMM/strings.xml
new file mode 100644
index 0000000..d487f50
--- /dev/null
+++ b/v7/appcompat/res/values-my-rMM/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"ပြီးဆုံးပါပြီ"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"မူလနေရာကို သွားရန်"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"အပေါ်သို့သွားရန်"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"ပိုမိုရွေးချယ်စရာများ"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"ရှာဖွေရန်"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ရှာစရာ အချက်အလက်နေရာ"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"ရှာစရာ အချက်အလက်များ ရှင်းလင်းရန်"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ရှာဖွေစရာ အချက်အလက်ကို အတည်ပြုရန်"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"အသံဖြင့် ရှာဖွေခြင်း"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"အပလီကေးရှင်း တစ်ခုခုကို ရွေးချယ်ပါ"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"အားလုံးကို ကြည့်ရန်"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s ကို မျှဝေပါရန်"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"မျှဝေဖို့ ရွေးပါ"</string>
+</resources>
diff --git a/v7/appcompat/res/values-nb/strings.xml b/v7/appcompat/res/values-nb/strings.xml
new file mode 100644
index 0000000..6630acf
--- /dev/null
+++ b/v7/appcompat/res/values-nb/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Ferdig"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Gå til startsiden"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Gå opp"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Flere alternativer"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Søk"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Søkeord"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Slett søket"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Utfør søket"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Talesøk"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Velg en app"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Se alle"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Del med %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Del med"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ne-rNP/strings.xml b/v7/appcompat/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000..69d10dc
--- /dev/null
+++ b/v7/appcompat/res/values-ne-rNP/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"सम्पन्न भयो"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"गृह खोज्नुहोस्"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"माथि खोज्नुहोस्"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"थप विकल्पहरू"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"खोज्नुहोस्"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"जिज्ञासाको खोज गर्नुहोस्"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"प्रश्न हटाउनुहोस्"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"जिज्ञासा पेस गर्नुहोस्"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"भ्वाइस खोजी"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"एउटा अनुप्रयोग छान्नुहोस्"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"सबै हेर्नुहोस्"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s सँग साझेदारी गर्नुहोस्"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"साझेदारी गर्नुहोस्..."</string>
+</resources>
diff --git a/v7/appcompat/res/values-nl/strings.xml b/v7/appcompat/res/values-nl/strings.xml
new file mode 100644
index 0000000..1375f9e
--- /dev/null
+++ b/v7/appcompat/res/values-nl/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Gereed"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navigeren naar startpositie"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Omhoog navigeren"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Meer opties"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Zoeken"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Zoekopdracht"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Zoekopdracht wissen"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Zoekopdracht verzenden"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Gesproken zoekopdracht"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Een app selecteren"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Alles weergeven"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Delen met %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Delen met"</string>
+</resources>
diff --git a/v7/appcompat/res/values-pl/strings.xml b/v7/appcompat/res/values-pl/strings.xml
new file mode 100644
index 0000000..aa1ba79
--- /dev/null
+++ b/v7/appcompat/res/values-pl/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Gotowe"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Przejdź do strony głównej"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Przejdź wyżej"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Więcej opcji"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Szukaj"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Wyszukiwane hasło"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Wyczyść zapytanie"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Wyślij zapytanie"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Wyszukiwanie głosowe"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Wybierz aplikację"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Zobacz wszystkie"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Udostępnij dla %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Udostępnij dla"</string>
+</resources>
diff --git a/v7/appcompat/res/values-pt-rPT/strings.xml b/v7/appcompat/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..0d63f5f
--- /dev/null
+++ b/v7/appcompat/res/values-pt-rPT/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Concluído"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navegar para a página inicial"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navegar para cima"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Mais opções"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Pesquisar"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Consulta de pesquisa"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Limpar consulta"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Enviar consulta"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Pesquisa por voz"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Escolher uma aplicação"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Ver tudo"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Partilhar com %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Partilhar com"</string>
+</resources>
diff --git a/v7/appcompat/res/values-pt/strings.xml b/v7/appcompat/res/values-pt/strings.xml
new file mode 100644
index 0000000..88b09ea
--- /dev/null
+++ b/v7/appcompat/res/values-pt/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Concluído"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navegar para a página inicial"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navegar para cima"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Mais opções"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Pesquisar"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Consulta de pesquisa"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Limpar consulta"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Enviar consulta"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Pesquisa por voz"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Selecione um aplicativo"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Ver tudo"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Compartilhar com %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Compartilhar com"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ro/strings.xml b/v7/appcompat/res/values-ro/strings.xml
new file mode 100644
index 0000000..36a7b31
--- /dev/null
+++ b/v7/appcompat/res/values-ro/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Terminat"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Navigați la ecranul de pornire"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navigați în sus"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Mai multe opțiuni"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Căutați"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Interogare de căutare"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Ștergeți interogarea"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Trimiteți interogarea"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Căutare vocală"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Alegeți o aplicaţie"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Afișați-le pe toate"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Trimiteți la %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Trimiteți la"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ru/strings.xml b/v7/appcompat/res/values-ru/strings.xml
new file mode 100644
index 0000000..5c22e5e
--- /dev/null
+++ b/v7/appcompat/res/values-ru/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Готово"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Перейти на главный экран"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Перейти вверх"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Другие параметры"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Поиск"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Поисковый запрос"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Удалить запрос"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Отправить запрос"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Голосовой поиск"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Выбрать приложение"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Показать все"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Открыть доступ пользователю %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Открыть доступ"</string>
+</resources>
diff --git a/v7/appcompat/res/values-si-rLK/strings.xml b/v7/appcompat/res/values-si-rLK/strings.xml
new file mode 100644
index 0000000..a6809c3
--- /dev/null
+++ b/v7/appcompat/res/values-si-rLK/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"අවසාන වූ"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"ගෙදරට සංචාලනය කරන්න"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"ඉහලට සංචාලනය කරන්න"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"තවත් විකල්ප"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"සෙවීම"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"සෙවුම් විමසුම"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"විමසුම හිස් කරන්න"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"විමසුම යොමු කරන්න"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"හඬ සෙවීම"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"යෙදුමක් තෝරන්න"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"සියල්ල බලන්න"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s සමඟ බෙදාගන්න"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"සමඟ බෙදාගන්න"</string>
+</resources>
diff --git a/v7/appcompat/res/values-sk/strings.xml b/v7/appcompat/res/values-sk/strings.xml
new file mode 100644
index 0000000..253f3e5
--- /dev/null
+++ b/v7/appcompat/res/values-sk/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Hotovo"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Prejsť na plochu"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Prejsť hore"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Ďalšie možnosti"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Hľadať"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Vyhľadávací dopyt"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Vymazať dopyt"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Odoslať dopyt"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Hlasové vyhľadávanie"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Zvoľte aplikáciu"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Zobraziť všetko"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Zdieľať pomocou %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Zdieľať pomocou"</string>
+</resources>
diff --git a/v7/appcompat/res/values-sl/strings.xml b/v7/appcompat/res/values-sl/strings.xml
new file mode 100644
index 0000000..8e3e23e
--- /dev/null
+++ b/v7/appcompat/res/values-sl/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Končano"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Krmarjenje domov"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Krmarjenje navzgor"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Več možnosti"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Iskanje"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Iskalna poizvedba"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Izbris poizvedbe"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Pošiljanje poizvedbe"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Glasovno iskanje"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Izbira aplikacije"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Pokaži vse"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Deljenje z:"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Deljenje z"</string>
+</resources>
diff --git a/v7/appcompat/res/values-sr/strings.xml b/v7/appcompat/res/values-sr/strings.xml
new file mode 100644
index 0000000..213c939
--- /dev/null
+++ b/v7/appcompat/res/values-sr/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Готово"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Одлазак на Почетну"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Кретање нагоре"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Још опција"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Претрага"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Упит за претрагу"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Брисање упита"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Слање упита"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Гласовна претрага"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Избор апликације"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Прикажи све"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Дели са апликацијом %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Дели са"</string>
+</resources>
diff --git a/v7/appcompat/res/values-sv/strings.xml b/v7/appcompat/res/values-sv/strings.xml
new file mode 100644
index 0000000..49c7a5d
--- /dev/null
+++ b/v7/appcompat/res/values-sv/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Klart"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Visa startsidan"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Navigera uppåt"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Fler alternativ"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Sök"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Sökfråga"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Ta bort frågan"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Skicka fråga"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Röstsökning"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Välj en app"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Visa alla"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Dela med %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Dela med"</string>
+</resources>
diff --git a/v7/appcompat/res/values-sw/strings.xml b/v7/appcompat/res/values-sw/strings.xml
new file mode 100644
index 0000000..6455ba5
--- /dev/null
+++ b/v7/appcompat/res/values-sw/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Nimemaliza"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Nenda mwanzo"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Nenda juu"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Chaguo zaidi"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Tafuta"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Hoja ya utafutaji"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Futa hoja"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Wasilisha hoja"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Tafuta kwa kutamka"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Chagua programu"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Angalia zote"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Shiriki na %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Shiriki na:"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ta-rIN/strings.xml b/v7/appcompat/res/values-ta-rIN/strings.xml
new file mode 100644
index 0000000..4d7d94e
--- /dev/null
+++ b/v7/appcompat/res/values-ta-rIN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"முடிந்தது"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"முகப்பிற்கு வழிசெலுத்து"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"மேலே வழிசெலுத்து"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"மேலும் விருப்பங்கள்"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"தேடு"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"தேடல் வினவல்"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"வினவலை அழி"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"வினவலைச் சமர்ப்பி"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"குரல் தேடல்"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"பயன்பாட்டைத் தேர்வுசெய்க"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"எல்லாம் காட்டு"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s உடன் பகிர்"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"இதனுடன் பகிர்"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ta-rTN/strings.xml b/v7/appcompat/res/values-ta-rTN/strings.xml
new file mode 100644
index 0000000..4d7d94e
--- /dev/null
+++ b/v7/appcompat/res/values-ta-rTN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"முடிந்தது"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"முகப்பிற்கு வழிசெலுத்து"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"மேலே வழிசெலுத்து"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"மேலும் விருப்பங்கள்"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"தேடு"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"தேடல் வினவல்"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"வினவலை அழி"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"வினவலைச் சமர்ப்பி"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"குரல் தேடல்"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"பயன்பாட்டைத் தேர்வுசெய்க"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"எல்லாம் காட்டு"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s உடன் பகிர்"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"இதனுடன் பகிர்"</string>
+</resources>
diff --git a/v7/appcompat/res/values-te-rAP/strings.xml b/v7/appcompat/res/values-te-rAP/strings.xml
new file mode 100644
index 0000000..f6b1775
--- /dev/null
+++ b/v7/appcompat/res/values-te-rAP/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"పూర్తయింది"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"హోమ్కు నావిగేట్ చేయండి"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"పైకి నావిగేట్ చేయండి"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"మరిన్ని ఎంపికలు"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"శోధించు"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ప్రశ్న శోధించండి"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"ప్రశ్నను క్లియర్ చేయి"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ప్రశ్నని సమర్పించు"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"వాయిస్ శోధన"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"అనువర్తనాన్ని ఎంచుకోండి"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"అన్నీ చూడండి"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%sతో భాగస్వామ్యం చేయి"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"వీరితో భాగస్వామ్యం చేయి"</string>
+</resources>
diff --git a/v7/appcompat/res/values-te-rIN/strings.xml b/v7/appcompat/res/values-te-rIN/strings.xml
new file mode 100644
index 0000000..f6b1775
--- /dev/null
+++ b/v7/appcompat/res/values-te-rIN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"పూర్తయింది"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"హోమ్కు నావిగేట్ చేయండి"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"పైకి నావిగేట్ చేయండి"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"మరిన్ని ఎంపికలు"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"శోధించు"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ప్రశ్న శోధించండి"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"ప్రశ్నను క్లియర్ చేయి"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ప్రశ్నని సమర్పించు"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"వాయిస్ శోధన"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"అనువర్తనాన్ని ఎంచుకోండి"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"అన్నీ చూడండి"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%sతో భాగస్వామ్యం చేయి"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"వీరితో భాగస్వామ్యం చేయి"</string>
+</resources>
diff --git a/v7/appcompat/res/values-th/strings.xml b/v7/appcompat/res/values-th/strings.xml
new file mode 100644
index 0000000..275dc57
--- /dev/null
+++ b/v7/appcompat/res/values-th/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"เสร็จสิ้น"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"นำทางไปหน้าแรก"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"นำทางขึ้น"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"ตัวเลือกอื่น"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"ค้นหา"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"ข้อความค้นหา"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"ล้างข้อความค้นหา"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"ส่งข้อความค้นหา"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"ค้นหาด้วยเสียง"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"เลือกแอป"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"ดูทั้งหมด"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"แชร์กับ %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"แชร์กับ"</string>
+</resources>
diff --git a/v7/appcompat/res/values-tl/strings.xml b/v7/appcompat/res/values-tl/strings.xml
new file mode 100644
index 0000000..e0705d6
--- /dev/null
+++ b/v7/appcompat/res/values-tl/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Tapos na"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Mag-navigate patungo sa home"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Mag-navigate pataas"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Higit pang mga opsyon"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Maghanap"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Query sa paghahanap"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"I-clear ang query"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Isumite ang query"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Paghahanap gamit ang boses"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Pumili ng isang app"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Tingnan lahat"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Ibahagi sa/kay %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Ibahagi sa/kay"</string>
+</resources>
diff --git a/v7/appcompat/res/values-tr/strings.xml b/v7/appcompat/res/values-tr/strings.xml
new file mode 100644
index 0000000..61cb966
--- /dev/null
+++ b/v7/appcompat/res/values-tr/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Tamamlandı"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Ana ekrana git"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Yukarı git"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Diğer seçenekler"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Ara"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Arama sorgusu"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Sorguyu temizle"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Sorguyu gönder"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Sesli arama"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Bir uygulama seçin"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Tümünü göster"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s ile paylaş"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Şununla paylaş"</string>
+</resources>
diff --git a/v7/appcompat/res/values-uk/strings.xml b/v7/appcompat/res/values-uk/strings.xml
new file mode 100644
index 0000000..f670140
--- /dev/null
+++ b/v7/appcompat/res/values-uk/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Готово"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Перейти на головний"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Перейти вгору"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Інші опції"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Пошук"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Пошуковий запит"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Очистити запит"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Надіслати запит"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Голосовий пошук"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Вибрати програму"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Переглянути всі"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Надіслати через %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Надіслати через"</string>
+</resources>
diff --git a/v7/appcompat/res/values-ur-rPK/strings.xml b/v7/appcompat/res/values-ur-rPK/strings.xml
new file mode 100644
index 0000000..f209747
--- /dev/null
+++ b/v7/appcompat/res/values-ur-rPK/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"ہو گیا"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"ہوم پر نیویگیٹ کریں"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"اوپر نیویگیٹ کریں"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"مزید اختیارات"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"تلاش کریں"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"استفسار تلاش کریں"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"استفسار صاف کریں"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"استفسار جمع کرائیں"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"صوتی تلاش"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"ایک ایپ منتخب کریں"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"سبھی دیکھیں"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"%s کے ساتھ اشتراک کریں"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"اشتراک کریں مع"</string>
+</resources>
diff --git a/v7/appcompat/res/values-uz-rUZ/strings.xml b/v7/appcompat/res/values-uz-rUZ/strings.xml
new file mode 100644
index 0000000..84d9541
--- /dev/null
+++ b/v7/appcompat/res/values-uz-rUZ/strings.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Tayyor"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Boshiga o‘tish"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Yuqoriga o‘tish"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Qo‘shimcha sozlamalar"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Izlash"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"So‘rovni izlash"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"So‘rovni tozalash"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"So‘rov yaratish"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Ovozli qidiruv"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Dastur tanlang"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Barchasini ko‘rish"</string>
+ <!-- String.format failed for translation -->
+ <!-- no translation found for abc_shareactionprovider_share_with_application (7165123711973476752) -->
+ <skip />
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Bo‘lishish:"</string>
+</resources>
diff --git a/v7/appcompat/res/values-v11/themes_base.xml b/v7/appcompat/res/values-v11/themes_base.xml
index ddd9d59..0539a1a 100644
--- a/v7/appcompat/res/values-v11/themes_base.xml
+++ b/v7/appcompat/res/values-v11/themes_base.xml
@@ -16,6 +16,20 @@
<resources>
+ <!--
+ Theme in the "Theme.Platform.AppCompat" family are designed to be aliases for the default
+ theme on a given platform version. They should not set any styleable attributes. Instead
+ you should create a "Theme.Base" theme which inherits from a "Theme.Platform" theme.
+ -->
+ <eat-comment/>
+ <style name="Theme.Platform.AppCompat" parent="android:Theme.Holo" />
+
+ <style name="Theme.Platform.AppCompat.Light" parent="android:Theme.Holo.Light" />
+
+ <style name="Theme.Platform.AppCompat.Dialog" parent="android:Theme.Holo.Dialog" />
+
+ <style name="Theme.Platform.AppCompat.Light.Dialog" parent="android:Theme.Holo.Light.Dialog" />
+
<!-- Themes in the "Theme.Base" family vary based on the current platform
version to provide the correct basis on each device. You probably don't
want to use them directly in your apps.
@@ -28,7 +42,7 @@
<eat-comment/>
<!-- Base platform-dependent theme -->
- <style name="Theme.Base" parent="android:Theme.Holo">
+ <style name="Theme.Base" parent="Theme.Platform.AppCompat">
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
@@ -46,7 +60,7 @@
</style>
<!-- Base platform-dependent theme providing a light-themed activity. -->
- <style name="Theme.Base.Light" parent="android:Theme.Holo.Light">
+ <style name="Theme.Base.Light" parent="Theme.Platform.AppCompat.Light">
<item name="android:windowNoTitle">true</item>
<item name="android:windowActionBar">false</item>
@@ -63,7 +77,7 @@
<item name="selectableItemBackground">?android:attr/selectableItemBackground</item>
</style>
- <style name="Theme.Base.AppCompat.Dialog.FixedSize" parent="android:Theme.Holo.Dialog">
+ <style name="Theme.Base.AppCompat.Dialog.FixedSize" parent="Theme.Platform.AppCompat.Dialog">
<item name="windowFixedWidthMajor">@dimen/dialog_fixed_width_major</item>
<item name="windowFixedWidthMinor">@dimen/dialog_fixed_width_minor</item>
<item name="windowFixedHeightMajor">@dimen/dialog_fixed_height_major</item>
@@ -77,7 +91,7 @@
</style>
<style name="Theme.Base.AppCompat.Dialog.Light.FixedSize"
- parent="android:Theme.Holo.Light.Dialog">
+ parent="Theme.Platform.AppCompat.Light.Dialog">
<item name="windowFixedWidthMajor">@dimen/dialog_fixed_width_major</item>
<item name="windowFixedWidthMinor">@dimen/dialog_fixed_width_minor</item>
<item name="windowFixedHeightMajor">@dimen/dialog_fixed_height_major</item>
diff --git a/v7/appcompat/res/values-v14/themes_base.xml b/v7/appcompat/res/values-v14/themes_base.xml
index 3b30074..e182b2e 100644
--- a/v7/appcompat/res/values-v14/themes_base.xml
+++ b/v7/appcompat/res/values-v14/themes_base.xml
@@ -16,6 +16,23 @@
<resources>
+ <!--
+ Theme in the "Theme.Platform.AppCompat" family are designed to be aliases for the default
+ theme on a given platform version. They should not set any styleable attributes. Instead
+ you should create a "Theme.Base" theme which inherits from a "Theme.Platform" theme.
+ -->
+ <eat-comment/>
+
+ <style name="Theme.Platform.AppCompat.Light.DarkActionBar"
+ parent="android:Theme.Holo.Light.DarkActionBar" />
+
+ <style name="Theme.Platform.AppCompat.DialogWhenLarge"
+ parent="android:Theme.Holo.DialogWhenLarge" />
+
+ <style name="Theme.Platform.AppCompat.Light.DialogWhenLarge"
+ parent="android:Theme.Holo.Light.DialogWhenLarge" />
+
+
<!-- Themes in the "Theme.Base" family vary based on the current platform
version to provide the correct basis on each device. You probably don't
want to use them directly in your apps.
@@ -28,7 +45,7 @@
<eat-comment/>
<!-- Base platform-dependent theme providing an action bar in a dark-themed activity. -->
- <style name="Theme.Base.AppCompat" parent="android:Theme.Holo">
+ <style name="Theme.Base.AppCompat" parent="Theme.Platform.AppCompat">
<!-- Copy system flag values for our use -->
<item name="windowActionBar">?android:attr/windowActionBar</item>
<item name="actionBarSize">?android:attr/actionBarSize</item>
@@ -58,7 +75,7 @@
</style>
<!-- Base platform-dependent theme providing an action bar in a light-themed activity. -->
- <style name="Theme.Base.AppCompat.Light" parent="android:Theme.Holo.Light">
+ <style name="Theme.Base.AppCompat.Light" parent="Theme.Platform.AppCompat.Light">
<!-- Copy system flag values for our use -->
<item name="windowActionBar">?android:attr/windowActionBar</item>
<item name="actionBarSize">?android:attr/actionBarSize</item>
@@ -89,7 +106,7 @@
<!-- Base platform-dependent theme providing a dark action bar in a light-themed activity. -->
<style name="Theme.Base.AppCompat.Light.DarkActionBar"
- parent="android:Theme.Holo.Light.DarkActionBar">
+ parent="Theme.Platform.AppCompat.Light.DarkActionBar">
<!-- Copy system flag values for our use -->
<item name="windowActionBar">?android:attr/windowActionBar</item>
<item name="actionBarSize">?android:attr/actionBarSize</item>
@@ -132,7 +149,7 @@
-->
<style name="Theme.Base.AppCompat.DialogWhenLarge.Base"
- parent="android:Theme.Holo.DialogWhenLarge">
+ parent="Theme.Platform.AppCompat.DialogWhenLarge">
<!-- Copy system flag values for our use -->
<item name="windowActionBar">?android:attr/windowActionBar</item>
<item name="actionBarSize">?android:attr/actionBarSize</item>
@@ -158,7 +175,7 @@
</style>
<style name="Theme.Base.AppCompat.Light.DialogWhenLarge.Base"
- parent="android:Theme.Holo.Light.DialogWhenLarge">
+ parent="Theme.Platform.AppCompat.Light.DialogWhenLarge">
<!-- Copy system flag values for our use -->
<item name="windowActionBar">?android:attr/windowActionBar</item>
<item name="actionBarSize">?android:attr/actionBarSize</item>
diff --git a/v7/appcompat/res/values-v21/styles_base.xml b/v7/appcompat/res/values-v21/styles_base.xml
new file mode 100644
index 0000000..e00b809
--- /dev/null
+++ b/v7/appcompat/res/values-v21/styles_base.xml
@@ -0,0 +1,241 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!-- Like in themes_base.xml, the namespace "*.AppCompat.Base" is used to
+ define base styles for the platform version. The "*.AppCompat"
+ variants are for direct use or use as parent styles by the app. -->
+ <eat-comment/>
+
+ <style name="Widget.AppCompat.Base.ActionBar"
+ parent="android:Widget.Material.ActionBar">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionBar"
+ parent="android:Widget.Material.Light.ActionBar">
+ </style>
+
+ <style name="Widget.AppCompat.Base.ActionBar.Solid"
+ parent="android:Widget.Material.ActionBar.Solid">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionBar.Solid"
+ parent="android:Widget.Material.Light.ActionBar.Solid">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionBar.Solid.Inverse"
+ parent="android:Widget.Material.Light.ActionBar.Solid">
+ </style>
+
+ <style name="Widget.AppCompat.Base.ActionBar.TabBar"
+ parent="android:Widget.Material.ActionBar.TabBar">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionBar.TabBar"
+ parent="android:Widget.Material.Light.ActionBar.TabBar">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionBar.TabBar.Inverse"
+ parent="android:Widget.Material.Light.ActionBar.TabBar">
+ </style>
+
+ <style name="Widget.AppCompat.Base.ActionBar.TabView"
+ parent="android:Widget.Material.ActionBar.TabView">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionBar.TabView"
+ parent="android:Widget.Material.Light.ActionBar.TabView">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionBar.TabView.Inverse"
+ parent="android:Widget.Material.Light.ActionBar.TabView">
+ </style>
+
+ <style name="Widget.AppCompat.Base.ActionBar.TabText"
+ parent="android:Widget.Material.ActionBar.TabText">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionBar.TabText"
+ parent="android:Widget.Material.Light.ActionBar.TabText">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionBar.TabText.Inverse"
+ parent="android:Widget.Material.Light.ActionBar.TabText">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionMode.Inverse"
+ parent="android:Widget.Material.Light.ActionMode">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Widget.Base.ActionBar.Menu"
+ parent="android:TextAppearance.Material.Widget.ActionBar.Menu">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Widget.Base.ActionBar.Title"
+ parent="android:TextAppearance.Material.Widget.ActionBar.Title">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Widget.Base.ActionBar.Subtitle"
+ parent="android:TextAppearance.Material.Widget.ActionBar.Subtitle">
+ </style>
+
+
+ <!--
+ TODO Hidden
+ <style name="TextAppearance.AppCompat.Widget.Base.ActionBar.Title.Inverse"
+ parent="android:TextAppearance.Material.Widget.ActionBar.Title.Inverse">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Widget.Base.ActionBar.Subtitle.Inverse"
+ parent="android:TextAppearance.Material.Widget.ActionBar.Subtitle.Inverse">
+ </style>
+ -->
+
+ <style name="TextAppearance.AppCompat.Widget.Base.ActionMode.Title"
+ parent="android:TextAppearance.Material.Widget.ActionMode.Title">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Widget.Base.ActionMode.Subtitle"
+ parent="android:TextAppearance.Material.Widget.ActionMode.Subtitle">
+ </style>
+
+ <!--
+ TODO Hidden
+ <style name="TextAppearance.AppCompat.Widget.Base.ActionMode.Title.Inverse"
+ parent="android:TextAppearance.Material.Widget.ActionMode.Title.Inverse">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Widget.Base.ActionMode.Subtitle.Inverse"
+ parent="android:TextAppearance.Material.Widget.ActionMode.Subtitle.Inverse">
+ </style>
+ -->
+
+ <!-- Action Button Styles -->
+
+ <style name="Widget.AppCompat.Base.ActionButton"
+ parent="android:Widget.Material.ActionButton">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionButton"
+ parent="android:Widget.Material.Light.ActionButton">
+ </style>
+
+ <style name="Widget.AppCompat.Base.ActionButton.CloseMode"
+ parent="android:Widget.Material.ActionButton.CloseMode">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionButton.CloseMode"
+ parent="android:Widget.Material.Light.ActionButton.CloseMode">
+ </style>
+
+ <style name="Widget.AppCompat.Base.ActionButton.Overflow"
+ parent="android:Widget.Material.ActionButton.Overflow">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ActionButton.Overflow"
+ parent="android:Widget.Material.Light.ActionButton.Overflow">
+ </style>
+
+ <!-- Spinner Widgets -->
+
+ <style name="Widget.AppCompat.Base.ListView.DropDown"
+ parent="android:Widget.Material.ListView.DropDown"/>
+
+ <style name="Widget.AppCompat.Light.Base.ListView.DropDown"
+ parent="android:Widget.Material.ListView.DropDown"/>
+
+ <style name="Widget.AppCompat.Base.DropDownItem.Spinner"
+ parent="android:Widget.Material.DropDownItem.Spinner"/>
+
+ <style name="Widget.AppCompat.Light.Base.DropDownItem.Spinner"
+ parent="android:Widget.Material.Light.DropDownItem.Spinner"/>
+
+ <style name="Widget.AppCompat.Base.Spinner"
+ parent="android:Widget.Material.Spinner" />
+
+ <style name="Widget.AppCompat.Light.Base.Spinner"
+ parent="android:Widget.Material.Light.Spinner"/>
+
+ <style name="Widget.AppCompat.Base.ListView.Menu"
+ parent="android:Widget.ListView.Menu" />
+
+ <!-- Popup Menu -->
+
+ <style name="Widget.AppCompat.Base.ListPopupWindow"
+ parent="android:Widget.Material.ListPopupWindow">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.ListPopupWindow"
+ parent="android:Widget.Material.Light.ListPopupWindow">
+ </style>
+
+ <style name="Widget.AppCompat.Base.PopupMenu" parent="android:Widget.Material.PopupMenu">
+ </style>
+
+ <style name="Widget.AppCompat.Light.Base.PopupMenu"
+ parent="android:Widget.Material.Light.PopupMenu">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Base.Widget.PopupMenu.Large"
+ parent="android:TextAppearance.Material.Widget.PopupMenu.Large">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Base.Widget.PopupMenu.Small"
+ parent="android:TextAppearance.Material.Widget.PopupMenu.Small">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Light.Base.Widget.PopupMenu.Large"
+ parent="android:TextAppearance.Material.Widget.PopupMenu.Large">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Light.Base.Widget.PopupMenu.Small"
+ parent="android:TextAppearance.Material.Widget.PopupMenu.Small">
+ </style>
+
+ <!-- Search View result styles -->
+
+ <style name="TextAppearance.AppCompat.Base.SearchResult.Title"
+ parent="@android:TextAppearance.Material.SearchResult.Title">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Base.SearchResult.Subtitle"
+ parent="@android:TextAppearance.Material.SearchResult.Subtitle">
+ </style>
+
+ <!--
+ TextAppearance.Material.Light.SearchResult.* are private so we extend from the default
+ versions instead (which are exactly the same).
+ -->
+ <style name="TextAppearance.AppCompat.Light.Base.SearchResult.Title"
+ parent="TextAppearance.AppCompat.Base.SearchResult.Title">
+ </style>
+
+ <style name="TextAppearance.AppCompat.Light.Base.SearchResult.Subtitle"
+ parent="TextAppearance.AppCompat.Base.SearchResult.Subtitle">
+ </style>
+
+ <!-- TODO. Needs updating for QP -->
+ <style name="Widget.AppCompat.Base.ActivityChooserView" parent="">
+ <item name="android:gravity">center</item>
+ <item name="android:background">@drawable/abc_ab_share_pack_holo_dark</item>
+ <item name="android:divider">?attr/dividerVertical</item>
+ <item name="android:showDividers">middle</item>
+ <item name="android:dividerPadding">6dip</item>
+ </style>
+
+</resources>
diff --git a/v7/appcompat/res/values-v21/themes_base.xml b/v7/appcompat/res/values-v21/themes_base.xml
new file mode 100644
index 0000000..6155225
--- /dev/null
+++ b/v7/appcompat/res/values-v21/themes_base.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!--
+ Theme in the "Theme.Platform.AppCompat" family are designed to be aliases for the default
+ theme on a given platform version. They should not set any styleable attributes. Instead
+ you should create a "Theme.Base" theme which inherits from a "Theme.Platform" theme.
+ -->
+ <eat-comment/>
+ <style name="Theme.Platform.AppCompat" parent="android:Theme.Material" />
+
+ <style name="Theme.Platform.AppCompat.Light" parent="android:Theme.Material.Light" />
+
+ <style name="Theme.Platform.AppCompat.Light.DarkActionBar"
+ parent="android:Theme.Material.Light.DarkActionBar" />
+
+ <style name="Theme.Platform.AppCompat.DialogWhenLarge"
+ parent="android:Theme.Material.DialogWhenLarge" />
+
+ <style name="Theme.Platform.AppCompat.Light.DialogWhenLarge"
+ parent="android:Theme.Material.Light.DialogWhenLarge" />
+
+ <style name="Theme.Platform.AppCompat.Dialog"
+ parent="android:Theme.Material.Dialog" />
+
+ <style name="Theme.Platform.AppCompat.Light.Dialog"
+ parent="android:Theme.Material.Light.Dialog" />
+
+</resources>
diff --git a/v7/appcompat/res/values-vi/strings.xml b/v7/appcompat/res/values-vi/strings.xml
new file mode 100644
index 0000000..0840f73
--- /dev/null
+++ b/v7/appcompat/res/values-vi/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Xong"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Điều hướng về trang chủ"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Điều hướng lên trên"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Thêm tùy chọn"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Tìm kiếm"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Tìm kiếm truy vấn"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Xóa truy vấn"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Gửi truy vấn"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Tìm kiếm bằng giọng nói"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Chọn một ứng dụng"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Xem tất cả"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Chia sẻ với %s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Chia sẻ với"</string>
+</resources>
diff --git a/v7/appcompat/res/values-zh-rCN/strings.xml b/v7/appcompat/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..87b36b0
--- /dev/null
+++ b/v7/appcompat/res/values-zh-rCN/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"完成"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"转到主屏幕"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"转到上一层级"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"更多选项"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"搜索"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"搜索查询"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"清除查询"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"提交查询"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"语音搜索"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"选择应用"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"查看全部"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"通过%s分享"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"分享方式"</string>
+</resources>
diff --git a/v7/appcompat/res/values-zh-rHK/strings.xml b/v7/appcompat/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..f6a367d
--- /dev/null
+++ b/v7/appcompat/res/values-zh-rHK/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"完成"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"瀏覽主頁"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"向上瀏覽"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"更多選項"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"搜尋"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"搜尋查詢"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"清除查詢"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"提交查詢"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"語音搜尋"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"選擇應用程式"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"顯示全部"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"與「%s」分享"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"分享對象"</string>
+</resources>
diff --git a/v7/appcompat/res/values-zh-rTW/strings.xml b/v7/appcompat/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..c804ccf
--- /dev/null
+++ b/v7/appcompat/res/values-zh-rTW/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"完成"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"瀏覽首頁"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"向上瀏覽"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"更多選項"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"搜尋"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"搜尋查詢"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"清除查詢"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"提交查詢"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"語音搜尋"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"選擇應用程式"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"查看全部"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"與「%s」分享"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"選擇分享對象"</string>
+</resources>
diff --git a/v7/appcompat/res/values-zu/strings.xml b/v7/appcompat/res/values-zu/strings.xml
new file mode 100644
index 0000000..92eac7e
--- /dev/null
+++ b/v7/appcompat/res/values-zu/strings.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="abc_action_mode_done" msgid="4076576682505996667">"Kwenziwe"</string>
+ <string name="abc_action_bar_home_description" msgid="4600421777120114993">"Zulazulela ekhaya"</string>
+ <string name="abc_action_bar_up_description" msgid="1594238315039666878">"Zulazulela phezulu"</string>
+ <string name="abc_action_menu_overflow_description" msgid="3588849162933574182">"Izinketho eziningi"</string>
+ <string name="abc_searchview_description_search" msgid="8264924765203268293">"Sesha"</string>
+ <string name="abc_searchview_description_query" msgid="2550479030709304392">"Umbuzo wosesho"</string>
+ <string name="abc_searchview_description_clear" msgid="3691816814315814921">"Sula inkinga"</string>
+ <string name="abc_searchview_description_submit" msgid="8928215447528550784">"Hambisa umbuzo"</string>
+ <string name="abc_searchview_description_voice" msgid="893419373245838918">"Ukusesha ngezwi"</string>
+ <string name="abc_activitychooserview_choose_application" msgid="2031811694353399454">"Khetha uhlelo lokusebenza"</string>
+ <string name="abc_activity_chooser_view_see_all" msgid="7468859129482906941">"Buka konke"</string>
+ <string name="abc_shareactionprovider_share_with_application" msgid="7165123711973476752">"Yabelana no-%s"</string>
+ <string name="abc_shareactionprovider_share_with" msgid="3421042268587513524">"Yabelana no-"</string>
+</resources>
diff --git a/v7/appcompat/res/values/themes_base.xml b/v7/appcompat/res/values/themes_base.xml
index c48b57c..f707c3a 100644
--- a/v7/appcompat/res/values/themes_base.xml
+++ b/v7/appcompat/res/values/themes_base.xml
@@ -16,6 +16,23 @@
<resources>
+ <!--
+ Theme in the "Theme.Platform.AppCompat" family are designed to be aliases for the default
+ theme on a given platform version. They should not set any styleable attributes. Instead
+ you should create a "Theme.Base" theme which inherits from a "Theme.Platform" theme.
+ -->
+ <eat-comment/>
+ <style name="Theme.Platform.AppCompat" parent="android:Theme" />
+
+ <style name="Theme.Platform.AppCompat.Light" parent="android:Theme.Light" />
+
+ <style name="Theme.Platform.AppCompat.Dialog"
+ parent="android:Theme.Dialog" />
+
+ <style name="Theme.Platform.AppCompat.Light.Dialog"
+ parent="Theme.Platform.AppCompat.Dialog" />
+
+
<!-- Themes in the "Theme.Base" family vary based on the current platform
version to provide the correct basis on each device. You probably don't
want to use them directly in your apps.
@@ -24,10 +41,10 @@
directly by apps. -->
<eat-comment/>
- <style name="Theme.Base" parent="android:Theme">
+ <style name="Theme.Base" parent="Theme.Platform.AppCompat">
</style>
- <style name="Theme.Base.Light" parent="android:Theme.Light">
+ <style name="Theme.Base.Light" parent="Theme.Platform.AppCompat.Light">
</style>
<!-- Base platform-dependent theme providing an action bar in a dark-themed activity. -->
@@ -206,7 +223,7 @@
parent="Theme.Base.AppCompat.Light">
</style>
- <style name="Theme.Base.AppCompat.Dialog.FixedSize" parent="android:Theme.Dialog">
+ <style name="Theme.Base.AppCompat.Dialog.FixedSize" parent="Theme.Platform.AppCompat.Dialog">
<item name="windowFixedWidthMajor">@dimen/dialog_fixed_width_major</item>
<item name="windowFixedWidthMinor">@dimen/dialog_fixed_width_minor</item>
<item name="windowFixedHeightMajor">@dimen/dialog_fixed_height_major</item>
diff --git a/v7/cardview/Android.mk b/v7/cardview/Android.mk
new file mode 100644
index 0000000..5c8da8c
--- /dev/null
+++ b/v7/cardview/Android.mk
@@ -0,0 +1,75 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+# Build the resources using the current SDK version.
+# We do this here because the final static library must be compiled with an older
+# SDK version than the resources. The resources library and the R class that it
+# contains will not be linked into the final static library.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v7-cardview-res
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, dummy)
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_JAR_EXCLUDE_FILES := none
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# A helper sub-library to resolve cyclic dependencies between CardView and platform dependent
+# implementations
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v7-cardview-base
+LOCAL_SDK_VERSION := 7
+LOCAL_SRC_FILES := $(call all-java-files-under, base)
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# A helper sub-library that makes direct use of Eclair MR1 APIs
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v7-cardview-eclair-mr1
+LOCAL_SDK_VERSION := 7
+LOCAL_SRC_FILES := $(call all-java-files-under, eclair-mr1)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-cardview-base
+LOCAL_JAVA_LIBRARIES := android-support-v7-cardview-res
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# A helper sub-library that makes direct use of JB MR1 APIs
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v7-cardview-jellybean-mr1
+LOCAL_SDK_VERSION := 17
+LOCAL_SRC_FILES := $(call all-java-files-under, jellybean-mr1)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-cardview-eclair-mr1
+LOCAL_JAVA_LIBRARIES := android-support-v7-cardview-res
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# A helper sub-library that makes direct use of L APIs
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v7-cardview-api21
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, api21)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-cardview-base \
+ android-support-v7-cardview-jellybean-mr1
+LOCAL_JAVA_LIBRARIES := android-support-v7-cardview-res
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# Here is the final static library that apps can link against.
+# The R class is automatically excluded from the generated library.
+# Applications that use this library must specify LOCAL_RESOURCE_DIR
+# in their makefiles to include the resources in their package.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v7-cardview
+LOCAL_SDK_VERSION := 7
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-cardview-api21
+LOCAL_JAVA_LIBRARIES := android-support-v7-cardview-res
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v7/cardview/AndroidManifest.xml
similarity index 68%
copy from v17/leanback/res/drawable/lb_transition_action_bg.xml
copy to v7/cardview/AndroidManifest.xml
index 36688bc..3ef02dc 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v7/cardview/AndroidManifest.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2014 The Android Open Source Project
+<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,8 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.v7.cardview">
+ <uses-sdk android:minSdkVersion="7"/>
+ <application />
+</manifest>
diff --git a/v7/cardview/api21/android/support/v7/widget/CardViewApi21.java b/v7/cardview/api21/android/support/v7/widget/CardViewApi21.java
new file mode 100644
index 0000000..4126fe3
--- /dev/null
+++ b/v7/cardview/api21/android/support/v7/widget/CardViewApi21.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+import android.content.Context;
+import android.view.View;
+import android.support.v7.cardview.R;
+
+class CardViewApi21 implements CardViewImpl {
+
+ @Override
+ public void initialize(CardViewDelegate cardView, Context context, int backgroundColor,
+ float radius) {
+ cardView.setBackgroundDrawable(new RoundRectDrawable(backgroundColor, radius));
+ View view = (View) cardView;
+ view.setClipToOutline(true);
+ view.setElevation(context.getResources().getDimension(R.dimen.cardview_elevation));
+ }
+
+ @Override
+ public void setRadius(CardViewDelegate cardView, float radius) {
+ ((RoundRectDrawable) (cardView.getBackground())).setRadius(radius);
+ }
+
+ @Override
+ public void initStatic() {
+ }
+
+ @Override
+ public float getRadius(CardViewDelegate cardView) {
+ return ((RoundRectDrawable) (cardView.getBackground())).getRadius();
+ }
+
+}
\ No newline at end of file
diff --git a/v7/cardview/api21/android/support/v7/widget/RoundRectDrawable.java b/v7/cardview/api21/android/support/v7/widget/RoundRectDrawable.java
new file mode 100644
index 0000000..83d73bd
--- /dev/null
+++ b/v7/cardview/api21/android/support/v7/widget/RoundRectDrawable.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+
+/**
+ * Very simple drawable that draws a rounded rectangle background with arbitrary corners and also
+ * reports proper outline for L.
+ * <p>
+ * Simpler and uses less resources compared to GradientDrawable or ShapeDrawable.
+ */
+class RoundRectDrawable extends Drawable {
+ float mRadius;
+ final Paint mPaint;
+ final RectF mBounds;
+
+ public RoundRectDrawable(int backgroundColor, float radius) {
+ mRadius = radius;
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
+ mPaint.setColor(backgroundColor);
+ mBounds = new RectF();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ canvas.drawRoundRect(mBounds, mRadius, mRadius, mPaint);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ mBounds.set(bounds.left, bounds.top, bounds.right, bounds.bottom);
+ }
+
+ @Override
+ public boolean getOutline(Outline outline) {
+ outline.setRoundRect(getBounds(), mRadius);
+ return true;
+ }
+
+ public void setRadius(float radius) {
+ if (radius == mRadius) {
+ return;
+ }
+ mRadius = radius;
+ invalidateSelf();
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ // not supported because older versions do not support
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ // not supported because older versions do not support
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+
+ public float getRadius() {
+ return mRadius;
+ }
+}
diff --git a/v7/cardview/base/android/support/v7/widget/CardViewDelegate.java b/v7/cardview/base/android/support/v7/widget/CardViewDelegate.java
new file mode 100644
index 0000000..beedbd1
--- /dev/null
+++ b/v7/cardview/base/android/support/v7/widget/CardViewDelegate.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+import android.graphics.drawable.Drawable;
+
+/**
+ * Interface provided by CardView to implementations.
+ * <p>
+ * Necessary to resolve circular dependency between base CardView and platform implementations.
+ */
+interface CardViewDelegate {
+ void setBackgroundDrawable(Drawable paramDrawable);
+ Drawable getBackground();
+}
\ No newline at end of file
diff --git a/v7/cardview/base/android/support/v7/widget/CardViewImpl.java b/v7/cardview/base/android/support/v7/widget/CardViewImpl.java
new file mode 100644
index 0000000..96b2eb5
--- /dev/null
+++ b/v7/cardview/base/android/support/v7/widget/CardViewImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+import android.content.Context;
+
+/**
+ * Interface for platform specific CardView implementations.
+ */
+interface CardViewImpl {
+ void initialize(CardViewDelegate cardView, Context context, int backgroundColor, float radius);
+
+ void setRadius(CardViewDelegate cardView, float radius);
+
+ float getRadius(CardViewDelegate cardView);
+
+ void initStatic();
+}
diff --git a/v7/cardview/build.gradle b/v7/cardview/build.gradle
new file mode 100644
index 0000000..b0d4401
--- /dev/null
+++ b/v7/cardview/build.gradle
@@ -0,0 +1,98 @@
+apply plugin: 'android-library'
+
+archivesBaseName = 'cardview-v7'
+
+android {
+ // WARNING: should be 7
+ compileSdkVersion 'current'
+
+ buildToolsVersion "19.0.1"
+
+ defaultConfig {
+ minSdkVersion 7
+ // TODO: get target from branch
+ //targetSdkVersion 19
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ main.java.srcDirs = ['base', 'eclair-mr1', 'jellybean-mr1', 'api21', 'src']
+ main.aidl.srcDirs = ['base', 'eclair-mr1', 'jellybean-mr1', 'api21', 'src']
+ main.res.srcDirs = ['res']
+
+ androidTest.setRoot('tests')
+ androidTest.java.srcDir 'tests/java'
+ }
+
+ lintOptions {
+ // TODO: fix errors and reenable.
+ abortOnError false
+ }
+}
+
+android.libraryVariants.all { variant ->
+ def name = variant.buildType.name
+
+ if (name.equals(com.android.builder.BuilderConstants.DEBUG)) {
+ return; // Skip debug builds.
+ }
+ def suffix = name.capitalize()
+
+ def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
+ dependsOn variant.javaCompile
+ from variant.javaCompile.destinationDir
+ from 'LICENSE.txt'
+ }
+ def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
+ source android.sourceSets.main.allJava
+ classpath = files(variant.javaCompile.classpath.files) + files(
+ "${android.plugin.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
+ }
+
+ def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
+ classifier = 'javadoc'
+ from 'build/docs/javadoc'
+ }
+
+ def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
+ classifier = 'sources'
+ from android.sourceSets.main.allSource
+ }
+
+ artifacts.add('archives', javadocJarTask);
+ artifacts.add('archives', sourcesJarTask);
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ repository(url: uri(rootProject.ext.supportRepoOut)) {
+ }
+
+ pom.project {
+ name 'Android Support CardView v7'
+ description "Android Support CardView v7"
+ url 'http://developer.android.com/tools/extras/support-library.html'
+ inceptionYear '2011'
+
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+
+ scm {
+ url "http://source.android.com"
+ connection "scm:git:https://android.googlesource.com/platform/frameworks/support"
+ }
+ developers {
+ developer {
+ name 'The Android Open Source Project'
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/v7/cardview/dummy/Dummy.java b/v7/cardview/dummy/Dummy.java
new file mode 100644
index 0000000..be16dc7
--- /dev/null
+++ b/v7/cardview/dummy/Dummy.java
@@ -0,0 +1 @@
+// Dummy java file used to build the resource library.
diff --git a/v7/cardview/eclair-mr1/android/support/v7/widget/CardViewEclairMr1.java b/v7/cardview/eclair-mr1/android/support/v7/widget/CardViewEclairMr1.java
new file mode 100644
index 0000000..d5d98be
--- /dev/null
+++ b/v7/cardview/eclair-mr1/android/support/v7/widget/CardViewEclairMr1.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+class CardViewEclairMr1 implements CardViewImpl {
+
+ final RectF sCornerRect = new RectF();
+
+ @Override
+ public void initStatic() {
+ // Draws a round rect using 7 draw operations. This is faster than using
+ // canvas.drawRoundRect before JBMR1 because API 11-16 used alpha mask textures to draw
+ // shapes.
+ RoundRectDrawableWithShadow.sRoundRectHelper
+ = new RoundRectDrawableWithShadow.RoundRectHelper() {
+ @Override
+ public void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius,
+ Paint paint) {
+ final float twoRadius = cornerRadius * 2;
+ final float innerWidth = bounds.width() - twoRadius;
+ final float innerHeight = bounds.height() - twoRadius;
+ sCornerRect.set(bounds.left, bounds.top,
+ bounds.left + cornerRadius * 2, bounds.top + cornerRadius * 2);
+
+ canvas.drawArc(sCornerRect, 180, 90, true, paint);
+ sCornerRect.offset(innerWidth, 0);
+ canvas.drawArc(sCornerRect, 270, 90, true, paint);
+ sCornerRect.offset(0, innerHeight);
+ canvas.drawArc(sCornerRect, 0, 90, true, paint);
+ sCornerRect.offset(-innerWidth, 0);
+ canvas.drawArc(sCornerRect, 90, 90, true, paint);
+
+ //draw top and bottom pieces
+ canvas.drawRect(bounds.left + cornerRadius, bounds.top,
+ bounds.right - cornerRadius, bounds.top + cornerRadius,
+ paint);
+ canvas.drawRect(bounds.left + cornerRadius,
+ bounds.bottom - cornerRadius, bounds.right - cornerRadius,
+ bounds.bottom, paint);
+
+ //center
+ canvas.drawRect(bounds.left, bounds.top + cornerRadius,
+ bounds.right, bounds.bottom - cornerRadius, paint);
+ }
+ };
+ }
+
+ @Override
+ public void initialize(CardViewDelegate cardView, Context context, int backgroundColor,
+ float radius) {
+ RoundRectDrawableWithShadow background = new RoundRectDrawableWithShadow(
+ context.getResources(), backgroundColor, radius);
+ cardView.setBackgroundDrawable(background);
+ }
+
+ @Override
+ public void setRadius(CardViewDelegate cardView, float radius) {
+ ((RoundRectDrawableWithShadow) cardView.getBackground()).setCornerRadius(radius);
+ }
+
+ @Override
+ public float getRadius(CardViewDelegate cardView) {
+ return ((RoundRectDrawableWithShadow) cardView.getBackground()).getCornerRadius();
+ }
+}
\ No newline at end of file
diff --git a/v7/cardview/eclair-mr1/android/support/v7/widget/RoundRectDrawableWithShadow.java b/v7/cardview/eclair-mr1/android/support/v7/widget/RoundRectDrawableWithShadow.java
new file mode 100644
index 0000000..98be3c5
--- /dev/null
+++ b/v7/cardview/eclair-mr1/android/support/v7/widget/RoundRectDrawableWithShadow.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PixelFormat;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.support.v7.cardview.R;
+
+/**
+ * A rounded rectangle drawable which also includes a shadow around.
+ */
+class RoundRectDrawableWithShadow extends Drawable {
+ final static float SHADOW_MULTIPLIER = 1.5f;
+ /*
+ * This helper is set by CardView implementations.
+ * <p>
+ * Prior to API 17, canvas.drawRoundRect is expensive; which is why we need this interface
+ * to draw efficient rounded rectangles before 17.
+ * */
+ static RoundRectHelper sRoundRectHelper;
+
+ Paint mPaint;
+
+ Paint mCornerShadowPaint;
+
+ Paint mEdgeShadowPaint;
+
+ final RectF mPreShadowBounds;
+
+ float mCornerRadius;
+
+ Path mCornerShadowPath;
+
+ float mShadowSize;
+
+ private boolean mDirty = true;
+
+ private final int mShadowStartColor;
+
+ private final int mShadowEndColor;
+
+ RoundRectDrawableWithShadow(Resources resources, int backgroundColor, float radius) {
+ mShadowStartColor = resources.getColor(R.color.cardview_shadow_start_color);
+ mShadowEndColor = resources.getColor(R.color.cardview_shadow_end_color);
+ mShadowSize = resources.getDimension(R.dimen.cardview_shadow_size) * SHADOW_MULTIPLIER;
+
+
+ mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
+ mPaint.setColor(backgroundColor);
+ mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
+ mCornerShadowPaint.setStyle(Paint.Style.FILL);
+ mCornerShadowPaint.setDither(true);
+ mCornerRadius = radius;
+ mPreShadowBounds = new RectF();
+ mEdgeShadowPaint = new Paint(mCornerShadowPaint);
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ // not supported
+ }
+
+ @Override
+ protected void onBoundsChange(Rect bounds) {
+ super.onBoundsChange(bounds);
+ mDirty = true;
+ }
+
+ @Override
+ public boolean getPadding(Rect padding) {
+ final int topShadow = (int) Math.ceil(mShadowSize * (1 / (SHADOW_MULTIPLIER * 2)));
+ final int sideShadow = (int) Math.ceil(mShadowSize - topShadow);
+ padding.set(sideShadow, topShadow, sideShadow, (int) Math.ceil(mShadowSize));
+ return true;
+ }
+
+ @Override
+ public void setColorFilter(ColorFilter cf) {
+ // not supported
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+
+ void setCornerRadius(float radius) {
+ if (mCornerRadius == radius) {
+ return;
+ }
+ mCornerRadius = radius;
+ mDirty = true;
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mDirty) {
+ buildComponents(getBounds());
+ mDirty = false;
+ }
+ drawShadow(canvas);
+ final float offset = mShadowSize * (1 - 1 / (SHADOW_MULTIPLIER * 2));
+ final float horizontalOffset = mShadowSize - offset;
+ canvas.translate(0, -offset);
+ mPreShadowBounds.bottom += offset;
+ mPreShadowBounds.left -= horizontalOffset;
+ mPreShadowBounds.right += horizontalOffset;
+ sRoundRectHelper.drawRoundRect(canvas, mPreShadowBounds, mCornerRadius, mPaint);
+ mPreShadowBounds.bottom -= offset;
+ mPreShadowBounds.left += horizontalOffset;
+ mPreShadowBounds.right -= horizontalOffset;
+ canvas.translate(0, offset);
+ }
+
+ private void drawShadow(Canvas canvas) {
+ int saved = canvas.save();
+
+ float cornerPathSize = 2 * (mCornerRadius + mShadowSize);
+ float edgeShadowTop = -mCornerRadius - mShadowSize;
+ final Rect bounds = getBounds();
+
+ // LT
+ canvas.translate(mPreShadowBounds.left + mCornerRadius,
+ mPreShadowBounds.top + mCornerRadius);
+ canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
+ canvas.drawRect(0, edgeShadowTop,
+ bounds.right - cornerPathSize, -mCornerRadius,
+ mEdgeShadowPaint);
+ // RB
+ canvas.rotate(180f);
+ canvas.translate(-bounds.width() + cornerPathSize, -bounds.height() + cornerPathSize);
+ canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
+ canvas.drawRect(0, edgeShadowTop,
+ bounds.right - cornerPathSize, -mCornerRadius + mShadowSize,
+ mEdgeShadowPaint);
+
+ // LB
+ canvas.rotate(90f);
+ canvas.translate(0, -bounds.width() + cornerPathSize);
+ canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
+ canvas.drawRect(0, edgeShadowTop,
+ bounds.bottom - cornerPathSize, -mCornerRadius,
+ mEdgeShadowPaint);
+
+ // RT
+ canvas.rotate(180f);
+ canvas.translate(-bounds.height() + cornerPathSize, -bounds.width() + cornerPathSize);
+ canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
+ canvas.drawRect(0, edgeShadowTop,
+ bounds.bottom - cornerPathSize, -mCornerRadius,
+ mEdgeShadowPaint);
+
+ canvas.restoreToCount(saved);
+ }
+
+ private void buildShadowCorners() {
+ RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
+ RectF outerBounds = new RectF(innerBounds);
+ outerBounds.inset(-mShadowSize, -mShadowSize);
+
+ if (mCornerShadowPath == null) {
+ mCornerShadowPath = new Path();
+ } else {
+ mCornerShadowPath.reset();
+ }
+ mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
+ mCornerShadowPath.moveTo(-mCornerRadius, 0);
+ mCornerShadowPath.rLineTo(-mShadowSize, 0);
+ // outer arc
+ mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
+ // inner arc
+ mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
+ mCornerShadowPath.close();
+
+ float startRatio = mCornerRadius / (mCornerRadius + mShadowSize);
+ mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize,
+ new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
+ new float[]{0f, startRatio, 1f}
+ , Shader.TileMode.CLAMP));
+
+ // we offset the content shadowSize/2 pixels up to make it more realistic.
+ // this is why edge shadow shader has some extra space
+ // When drawing bottom edge shadow, we use that extra space.
+ mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0,
+ -mCornerRadius - mShadowSize,
+ new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor},
+ new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
+ }
+
+ private void buildComponents(Rect bounds) {
+ mPreShadowBounds.set(bounds.left + mShadowSize, bounds.top + mShadowSize,
+ bounds.right - mShadowSize, bounds.bottom - mShadowSize);
+ buildShadowCorners();
+ }
+
+ public float getCornerRadius() {
+ return mCornerRadius;
+ }
+
+ static interface RoundRectHelper {
+ void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius, Paint paint);
+ }
+}
diff --git a/v7/cardview/jellybean-mr1/android/support/v7/widget/CardViewJellybeanMr1.java b/v7/cardview/jellybean-mr1/android/support/v7/widget/CardViewJellybeanMr1.java
new file mode 100644
index 0000000..8ea6e3e
--- /dev/null
+++ b/v7/cardview/jellybean-mr1/android/support/v7/widget/CardViewJellybeanMr1.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.v7.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+
+class CardViewJellybeanMr1 extends CardViewEclairMr1 {
+
+ @Override
+ public void initStatic() {
+ RoundRectDrawableWithShadow.sRoundRectHelper
+ = new RoundRectDrawableWithShadow.RoundRectHelper() {
+ @Override
+ public void drawRoundRect(Canvas canvas, RectF bounds, float cornerRadius,
+ Paint paint) {
+ canvas.drawRoundRect(bounds, cornerRadius, cornerRadius, paint);
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v7/cardview/res/values/attrs.xml
similarity index 62%
copy from v17/leanback/res/drawable/lb_transition_action_bg.xml
copy to v7/cardview/res/values/attrs.xml
index 36688bc..2a8373f 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v7/cardview/res/values/attrs.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2014 The Android Open Source Project
+<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,7 +14,11 @@
limitations under the License.
-->
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+<resources>
+ <declare-styleable name="CardView">
+ <!-- Background color for CardView. -->
+ <attr name="cardBackgroundColor" format="color" />
+ <!-- Corner radius for CardView. -->
+ <attr name="cardCornerRadius" format="dimension" />
+ </declare-styleable>
+</resources>
\ No newline at end of file
diff --git a/v7/cardview/res/values/colors.xml b/v7/cardview/res/values/colors.xml
new file mode 100644
index 0000000..3ed7087
--- /dev/null
+++ b/v7/cardview/res/values/colors.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!-- Background color for light CardView. -->
+ <color name="cardview_light_background">#FFFAFAFA</color>
+ <!-- Background color for dark CardView. -->
+ <color name="cardview_dark_background">#FF202020</color>
+ <!-- Shadow color for the first pixels around CardView. -->
+ <color name="cardview_shadow_start_color">#37000000</color>
+ <!-- Shadow color for the furthest pixels around CardView. -->
+ <color name="cardview_shadow_end_color">#03000000</color>
+</resources>
\ No newline at end of file
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v7/cardview/res/values/dimens.xml
similarity index 60%
copy from v17/leanback/res/drawable/lb_transition_action_bg.xml
copy to v7/cardview/res/values/dimens.xml
index 36688bc..5fbd42a 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v7/cardview/res/values/dimens.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2014 The Android Open Source Project
+<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,7 +14,11 @@
limitations under the License.
-->
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+<resources>
+ <!-- Default radius for CardView corners. -->
+ <dimen name="cardview_default_radius">2dp</dimen>
+ <!-- Elevation value to use for CardViews on L+. -->
+ <dimen name="cardview_elevation">2dp</dimen>
+ <!-- Shadow size for CardView on pre L. -->
+ <dimen name="cardview_shadow_size">2dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/v7/cardview/res/values/styles.xml b/v7/cardview/res/values/styles.xml
new file mode 100644
index 0000000..f8a7909
--- /dev/null
+++ b/v7/cardview/res/values/styles.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+ <style name="CardView">
+ <item name="cardBackgroundColor">@color/cardview_light_background</item>
+ </style>
+ <style name="CardView.Light">
+ <item name="cardBackgroundColor">@color/cardview_light_background</item>
+ </style>
+ <style name="CardView.Dark">
+ <item name="cardBackgroundColor">@color/cardview_dark_background</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/v7/cardview/src/.readme b/v7/cardview/src/.readme
new file mode 100644
index 0000000..4bcebad
--- /dev/null
+++ b/v7/cardview/src/.readme
@@ -0,0 +1,2 @@
+This hidden file is there to ensure there is an src folder.
+Once we support binary library this will go away.
\ No newline at end of file
diff --git a/v7/cardview/src/android/support/v7/widget/CardView.java b/v7/cardview/src/android/support/v7/widget/CardView.java
new file mode 100644
index 0000000..03f494f
--- /dev/null
+++ b/v7/cardview/src/android/support/v7/widget/CardView.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.support.v7.cardview.R;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+/**
+ * A ViewGroup with a rounded corner background and shadow behind.
+ * <p>
+ * CardView uses <code>elevation</code> property on L for shadows and falls back to a custom shadow
+ * implementation on older platforms.
+ * <p>
+ * Due to expensive nature of rounded corner clipping, on platforms before L, CardView does not clip
+ * its children that intersect with rounded corners. Instead, it adds padding to avoid such
+ * intersection.
+ *
+ * @attr ref android.support.v7.cardview.R.styleable#CardView_cardBackgroundColor
+ * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius
+ */
+public class CardView extends FrameLayout implements CardViewDelegate {
+
+ private final static CardViewImpl IMPL;
+ static {
+ if ("L".equals(Build.VERSION.CODENAME) || Build.VERSION.SDK_INT >= 21) {
+ IMPL = new CardViewApi21();
+ } else if (Build.VERSION.SDK_INT >= 17) {
+ IMPL = new CardViewJellybeanMr1();
+ } else {
+ IMPL = new CardViewEclairMr1();
+ }
+ IMPL.initStatic();
+ }
+
+ public CardView(Context context) {
+ super(context);
+ initialize(context, null, 0);
+ }
+
+ public CardView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initialize(context, attrs, 0);
+ }
+
+ public CardView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(context, attrs, defStyleAttr);
+ }
+
+ private void initialize(Context context, AttributeSet attrs, int defStyleAttr) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CardView, defStyleAttr,
+ R.style.CardView_Light);
+ int backgroundColor = a.getColor(R.styleable.CardView_cardBackgroundColor, 0);
+ float radius = a.getDimension(R.styleable.CardView_cardCornerRadius, 0);
+
+ a.recycle();
+ IMPL.initialize(this, context, backgroundColor, radius);
+ }
+
+ /**
+ * Updates the corner radius of the CardView.
+ *
+ * @param radius The radius in pixels of the corners of the rectangle shape
+ *
+ * @attr ref android.support.v7.cardview.R.styleable#CardView_cardCornerRadius
+ *
+ * @see #setRadius(float)
+ *
+ */
+ public void setRadius(float radius) {
+ IMPL.setRadius(this, radius);
+ }
+
+ /**
+ * Returns the corner radius of the CardView.
+ *
+ * @return Corner radius of the CardView
+ *
+ * @see #getRadius()
+ */
+ public float getRadius() {
+ return IMPL.getRadius(this);
+ }
+}
diff --git a/v7/gridlayout/build.gradle b/v7/gridlayout/build.gradle
index 381d16b..bb586d1 100644
--- a/v7/gridlayout/build.gradle
+++ b/v7/gridlayout/build.gradle
@@ -6,9 +6,8 @@
compile project(':support-v4')
}
-
android {
- compileSdkVersion 19
+ compileSdkVersion 'current'
sourceSets {
main.manifest.srcFile 'AndroidManifest.xml'
diff --git a/v7/mediarouter/build.gradle b/v7/mediarouter/build.gradle
index 9abe938..9ad1915 100644
--- a/v7/mediarouter/build.gradle
+++ b/v7/mediarouter/build.gradle
@@ -32,7 +32,7 @@
jellybeanmr1Compile getAndroidPrebuilt('17')
jellybeanmr1Compile sourceSets.jellybean.output
- jellybeanmr2Compile getAndroidPrebuilt('18')
+ jellybeanmr2Compile getAndroidPrebuilt('current')
jellybeanmr2Compile sourceSets.jellybean.output
jellybeanmr2Compile sourceSets.jellybeanmr1.output
@@ -40,7 +40,7 @@
}
android {
- compileSdkVersion 19
+ compileSdkVersion 'current'
sourceSets {
main.manifest.srcFile 'AndroidManifest.xml'
diff --git a/v7/mediarouter/res/values-af/strings.xml b/v7/mediarouter/res/values-af/strings.xml
new file mode 100644
index 0000000..885af10
--- /dev/null
+++ b/v7/mediarouter/res/values-af/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Stelsel"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Toestelle"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Media-uitvoer"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Koppel aan toestel"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Soek tans vir toestelle…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Ontkoppel"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-am/strings.xml b/v7/mediarouter/res/values-am/strings.xml
new file mode 100644
index 0000000..0027737
--- /dev/null
+++ b/v7/mediarouter/res/values-am/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ስርዓት"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"መሣሪያዎች"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"የሚዲያ ውፅዓት"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"ከመሳሪያ ጋር ያገናኙ"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"መሳሪያዎችን በመፈለግ ላይ…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"ግንኙነት አቋርጥ"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ar/strings.xml b/v7/mediarouter/res/values-ar/strings.xml
new file mode 100644
index 0000000..9289a35
--- /dev/null
+++ b/v7/mediarouter/res/values-ar/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"النظام"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"الأجهزة"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"المنفذ الإعلامي"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"الاتصال بجهاز"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"جارٍ البحث عن الأجهزة…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"قطع الاتصال"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-bg/strings.xml b/v7/mediarouter/res/values-bg/strings.xml
new file mode 100644
index 0000000..ff1401e
--- /dev/null
+++ b/v7/mediarouter/res/values-bg/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Система"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Устройства"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Изходяща мултимедия"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Свързване с устройство"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Търсят се устройства…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Прекратяване на връзката"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-bn-rBD/strings.xml b/v7/mediarouter/res/values-bn-rBD/strings.xml
new file mode 100644
index 0000000..d0b2c32
--- /dev/null
+++ b/v7/mediarouter/res/values-bn-rBD/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"সিস্টেম"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ডিভাইসগুলি"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"মিডিয়া আউটপুট"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"ডিভাইসে সংযোগ করুন"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"ডিভাইসগুলি অনুসন্ধান করা হচ্ছে…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"সংযোগ বিচ্ছিন্ন করুন"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-bn-rWB/strings.xml b/v7/mediarouter/res/values-bn-rWB/strings.xml
new file mode 100644
index 0000000..d0b2c32
--- /dev/null
+++ b/v7/mediarouter/res/values-bn-rWB/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"সিস্টেম"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ডিভাইসগুলি"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"মিডিয়া আউটপুট"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"ডিভাইসে সংযোগ করুন"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"ডিভাইসগুলি অনুসন্ধান করা হচ্ছে…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"সংযোগ বিচ্ছিন্ন করুন"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ca/strings.xml b/v7/mediarouter/res/values-ca/strings.xml
new file mode 100644
index 0000000..dd485de
--- /dev/null
+++ b/v7/mediarouter/res/values-ca/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositius"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Sortida de contingut multimèdia"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Connecta al dispositiu"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"S\'estan cercant dispositius…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Desconnecta"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-cs/strings.xml b/v7/mediarouter/res/values-cs/strings.xml
new file mode 100644
index 0000000..4687100
--- /dev/null
+++ b/v7/mediarouter/res/values-cs/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Systém"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Zařízení"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Výstup médií"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Připojení k zařízení"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Vyhledávání zařízení…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Odpojit"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-da/strings.xml b/v7/mediarouter/res/values-da/strings.xml
new file mode 100644
index 0000000..fd3b0fb
--- /dev/null
+++ b/v7/mediarouter/res/values-da/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Enheder"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Medieudgang"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Opret forbindelse til enheden"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Søger efter enheder..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Afbryd forbindelsen"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-de/strings.xml b/v7/mediarouter/res/values-de/strings.xml
new file mode 100644
index 0000000..9df0ebf
--- /dev/null
+++ b/v7/mediarouter/res/values-de/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Geräte"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Medienausgabe"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Mit Gerät verbinden"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Geräte werden gesucht…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Verbindung aufheben"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-el/strings.xml b/v7/mediarouter/res/values-el/strings.xml
new file mode 100644
index 0000000..5a61395
--- /dev/null
+++ b/v7/mediarouter/res/values-el/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Σύστημα"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Συσκευές"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Έξοδος μέσων"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Σύνδεση με τη συσκευή"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Αναζήτηση συσκευών…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Αποσύνδεση"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-en-rGB/strings.xml b/v7/mediarouter/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..b9af4bf
--- /dev/null
+++ b/v7/mediarouter/res/values-en-rGB/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Devices"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Media output"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Connect to device"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Searching for devices…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Disconnect"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-en-rIN/strings.xml b/v7/mediarouter/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..b9af4bf
--- /dev/null
+++ b/v7/mediarouter/res/values-en-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Devices"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Media output"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Connect to device"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Searching for devices…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Disconnect"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-es-rUS/strings.xml b/v7/mediarouter/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..211b400
--- /dev/null
+++ b/v7/mediarouter/res/values-es-rUS/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Salida multimedia"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Conectar al dispositivo"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Buscando dispositivos…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Desconectar"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-es/strings.xml b/v7/mediarouter/res/values-es/strings.xml
new file mode 100644
index 0000000..d3a1639
--- /dev/null
+++ b/v7/mediarouter/res/values-es/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Salida multimedia"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Conectar a dispositivo"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Buscando dispositivos…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Desconectar"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-et-rEE/strings.xml b/v7/mediarouter/res/values-et-rEE/strings.xml
new file mode 100644
index 0000000..7dbdf74
--- /dev/null
+++ b/v7/mediarouter/res/values-et-rEE/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Süsteem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Seadmed"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Meediaväljund"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Seadmega ühendamine"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Seadmete otsimine …"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Katkesta ühendus"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-eu-rES/strings.xml b/v7/mediarouter/res/values-eu-rES/strings.xml
new file mode 100644
index 0000000..728672d
--- /dev/null
+++ b/v7/mediarouter/res/values-eu-rES/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Gailuak"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Multimedia-irteera"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Konektatu gailura"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Gailuak bilatzen…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Deskonektatu"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-eu-rPV/strings.xml b/v7/mediarouter/res/values-eu-rPV/strings.xml
new file mode 100644
index 0000000..728672d
--- /dev/null
+++ b/v7/mediarouter/res/values-eu-rPV/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Gailuak"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Multimedia-irteera"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Konektatu gailura"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Gailuak bilatzen…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Deskonektatu"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-fa/strings.xml b/v7/mediarouter/res/values-fa/strings.xml
new file mode 100644
index 0000000..2ffed50
--- /dev/null
+++ b/v7/mediarouter/res/values-fa/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"سیستم"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"دستگاهها"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"خروجی رسانه"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"برقراری ارتباط با دستگاه"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"در حال جستجو برای دستگاهها..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"قطع ارتباط"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-fi/strings.xml b/v7/mediarouter/res/values-fi/strings.xml
new file mode 100644
index 0000000..0692c2f
--- /dev/null
+++ b/v7/mediarouter/res/values-fi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Järjestelmä"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Laitteet"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Median äänentoisto"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Yhdistä laitteeseen"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Etsitään laitteita…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Katkaise yhteys"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-fr-rCA/strings.xml b/v7/mediarouter/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..9fa3c9c
--- /dev/null
+++ b/v7/mediarouter/res/values-fr-rCA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Système"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Appareils"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Sortie multimédia"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Connexion au périphérique"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Recherche d\'appareils en cours…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Déconnecter"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-fr/strings.xml b/v7/mediarouter/res/values-fr/strings.xml
new file mode 100644
index 0000000..5607a1c
--- /dev/null
+++ b/v7/mediarouter/res/values-fr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Système"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Appareils"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Sortie multimédia"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Connecter à l\'appareil"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Recherche d\'appareils en cours…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Déconnecter"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-gl-rES/strings.xml b/v7/mediarouter/res/values-gl-rES/strings.xml
new file mode 100644
index 0000000..d700c14
--- /dev/null
+++ b/v7/mediarouter/res/values-gl-rES/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Saída multimedia"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Conectar co dispositivo"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Buscando dispositivos…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Desconectar"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-gl-rGA/strings.xml b/v7/mediarouter/res/values-gl-rGA/strings.xml
new file mode 100644
index 0000000..d700c14
--- /dev/null
+++ b/v7/mediarouter/res/values-gl-rGA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Saída multimedia"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Conectar co dispositivo"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Buscando dispositivos…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Desconectar"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-hi/strings.xml b/v7/mediarouter/res/values-hi/strings.xml
new file mode 100644
index 0000000..8acc2bb
--- /dev/null
+++ b/v7/mediarouter/res/values-hi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"सिस्टम"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"उपकरण"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"मीडिया आउटपुट"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"उपकरण से कनेक्ट करें"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"उपकरणों की खोज हो रही है…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"डिस्कनेक्ट करें"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-hr/strings.xml b/v7/mediarouter/res/values-hr/strings.xml
new file mode 100644
index 0000000..2946433
--- /dev/null
+++ b/v7/mediarouter/res/values-hr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sustav"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Uređaji"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Medijski izlaz"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Povezivanje s uređajem"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Traženje uređaja…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Prekini vezu"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-hu/strings.xml b/v7/mediarouter/res/values-hu/strings.xml
new file mode 100644
index 0000000..b68fe16
--- /dev/null
+++ b/v7/mediarouter/res/values-hu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Rendszer"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Eszközök"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Médiakimenet"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Csatlakozás adott eszközhöz"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Eszközkeresés…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Leválasztás"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-hy-rAM/strings.xml b/v7/mediarouter/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000..77f1136
--- /dev/null
+++ b/v7/mediarouter/res/values-hy-rAM/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Համակարգ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Սարքեր"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Մեդիա արտածում"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Միանալ սարքին"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Որոնվում են սարքեր..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Անջատել"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-in/strings.xml b/v7/mediarouter/res/values-in/strings.xml
new file mode 100644
index 0000000..1d3b387
--- /dev/null
+++ b/v7/mediarouter/res/values-in/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Perangkat"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Keluaran media"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Sambungkan ke perangkat"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Menelusuri perangkat…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Putuskan sambungan"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-is-rIS/strings.xml b/v7/mediarouter/res/values-is-rIS/strings.xml
new file mode 100644
index 0000000..45d7329
--- /dev/null
+++ b/v7/mediarouter/res/values-is-rIS/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Kerfi"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Tæki"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Margmiðlunarúttak"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Tengjast tæki"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Leitar að tækjum…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Aftengja"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-it/strings.xml b/v7/mediarouter/res/values-it/strings.xml
new file mode 100644
index 0000000..bd58755
--- /dev/null
+++ b/v7/mediarouter/res/values-it/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivi"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Uscita media"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Connetti al dispositivo"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Ricerca di dispositivi…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Disconnetti"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-iw/strings.xml b/v7/mediarouter/res/values-iw/strings.xml
new file mode 100644
index 0000000..59753b4
--- /dev/null
+++ b/v7/mediarouter/res/values-iw/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"מערכת"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"מכשירים"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"פלט מדיה"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"התחבר למכשיר"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"מחפש מכשירים…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"התנתק"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ja/strings.xml b/v7/mediarouter/res/values-ja/strings.xml
new file mode 100644
index 0000000..1367489
--- /dev/null
+++ b/v7/mediarouter/res/values-ja/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"システム"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"端末"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"メディア出力"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"端末に接続"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"端末を検索しています…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"接続を解除"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ka-rGE/strings.xml b/v7/mediarouter/res/values-ka-rGE/strings.xml
new file mode 100644
index 0000000..413257e
--- /dev/null
+++ b/v7/mediarouter/res/values-ka-rGE/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"სისტემა"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"მოწყობილობები"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"მედია გამოსასვლელი"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"მოწყობილობასთან დაკავშირება"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"მოწყობილობების ძიება…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"კავშირის გაწყვეტა"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-kk-rKZ/strings.xml b/v7/mediarouter/res/values-kk-rKZ/strings.xml
new file mode 100644
index 0000000..e8da02a
--- /dev/null
+++ b/v7/mediarouter/res/values-kk-rKZ/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Жүйе"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Құрылғылар"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Meдиа құрылғылары"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Құрылғыға жалғау"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Құрылғыларды іздеуде…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Ажырату"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-km-rKH/strings.xml b/v7/mediarouter/res/values-km-rKH/strings.xml
new file mode 100644
index 0000000..e001dde
--- /dev/null
+++ b/v7/mediarouter/res/values-km-rKH/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ប្រព័ន្ធ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ឧបករណ៍"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"លទ្ធផលមេឌៀ"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"ភ្ជាប់ឧបករណ៍"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"កំពុងស្វែងរកឧបករណ៍..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"ផ្ដាច់"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-kn-rIN/strings.xml b/v7/mediarouter/res/values-kn-rIN/strings.xml
new file mode 100644
index 0000000..147ebc8
--- /dev/null
+++ b/v7/mediarouter/res/values-kn-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ಸಿಸ್ಟಂ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ಸಾಧನಗಳು"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"ಮಾಧ್ಯಮ ಔಟ್ಪುಟ್"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"ಸಾಧನಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಿ"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"ಸಾಧನಗಳನ್ನು ಹುಡುಕಲಾಗುತ್ತಿದೆ…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"ಸಂಪರ್ಕ ಕಡಿತಗೊಳಿಸು"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-kn-rKA/strings.xml b/v7/mediarouter/res/values-kn-rKA/strings.xml
new file mode 100644
index 0000000..147ebc8
--- /dev/null
+++ b/v7/mediarouter/res/values-kn-rKA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ಸಿಸ್ಟಂ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ಸಾಧನಗಳು"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"ಮಾಧ್ಯಮ ಔಟ್ಪುಟ್"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"ಸಾಧನಕ್ಕೆ ಸಂಪರ್ಕಪಡಿಸಿ"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"ಸಾಧನಗಳನ್ನು ಹುಡುಕಲಾಗುತ್ತಿದೆ…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"ಸಂಪರ್ಕ ಕಡಿತಗೊಳಿಸು"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ko/strings.xml b/v7/mediarouter/res/values-ko/strings.xml
new file mode 100644
index 0000000..21f82a0
--- /dev/null
+++ b/v7/mediarouter/res/values-ko/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"시스템"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"기기"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"미디어 출력"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"기기에 연결"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"기기 검색 중…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"연결 해제"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ky-rKG/strings.xml b/v7/mediarouter/res/values-ky-rKG/strings.xml
new file mode 100644
index 0000000..4a587ac
--- /dev/null
+++ b/v7/mediarouter/res/values-ky-rKG/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Систем"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Түзмөктөр"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Медиа чыгаруу"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Түзмөккө туташуу"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Түзмөктөр изделүүдө..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Ажыратуу"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-lo-rLA/strings.xml b/v7/mediarouter/res/values-lo-rLA/strings.xml
new file mode 100644
index 0000000..31a03cd
--- /dev/null
+++ b/v7/mediarouter/res/values-lo-rLA/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ລະບົບ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ອຸປະກອນ"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"ມີເດຍເອົ້າພຸດ"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"ເຊື່ອມຕໍ່ຫາອຸປະກອນ"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"ກຳລັງຊອກຫາອຸປະກອນ..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"ຕັດການເຊື່ອມຕໍ່"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-lt/strings.xml b/v7/mediarouter/res/values-lt/strings.xml
new file mode 100644
index 0000000..ead3b73
--- /dev/null
+++ b/v7/mediarouter/res/values-lt/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Įrenginiai"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Medijos išvestis"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Prijungimas prie įrenginio"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Ieškoma įrenginių…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Atjungti"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-lv/strings.xml b/v7/mediarouter/res/values-lv/strings.xml
new file mode 100644
index 0000000..0914990
--- /dev/null
+++ b/v7/mediarouter/res/values-lv/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistēma"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Ierīces"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Multivides izeja"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Savienojuma izveide ar ierīci"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Notiek ierīču meklēšana..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Atvienot"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-mk-rMK/strings.xml b/v7/mediarouter/res/values-mk-rMK/strings.xml
new file mode 100644
index 0000000..363f16b
--- /dev/null
+++ b/v7/mediarouter/res/values-mk-rMK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Систем"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Уреди"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Излез за медиум"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Поврзи се со уредот"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Се пребаруваат уреди..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Исклучи се"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ml-rIN/strings.xml b/v7/mediarouter/res/values-ml-rIN/strings.xml
new file mode 100644
index 0000000..d20ba1d
--- /dev/null
+++ b/v7/mediarouter/res/values-ml-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"സിസ്റ്റം"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ഉപകരണങ്ങൾ"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"മീഡിയ ഔട്ട്പുട്ട്"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"ഉപകരണത്തിലേക്ക് കണക്റ്റുചെയ്യുക"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"ഉപകരണങ്ങൾക്കായി തിരയുന്നു…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"വിച്ഛേദിക്കുക"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ml-rKL/strings.xml b/v7/mediarouter/res/values-ml-rKL/strings.xml
new file mode 100644
index 0000000..d20ba1d
--- /dev/null
+++ b/v7/mediarouter/res/values-ml-rKL/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"സിസ്റ്റം"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"ഉപകരണങ്ങൾ"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"മീഡിയ ഔട്ട്പുട്ട്"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"ഉപകരണത്തിലേക്ക് കണക്റ്റുചെയ്യുക"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"ഉപകരണങ്ങൾക്കായി തിരയുന്നു…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"വിച്ഛേദിക്കുക"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-mn-rMN/strings.xml b/v7/mediarouter/res/values-mn-rMN/strings.xml
new file mode 100644
index 0000000..4eecdb4
--- /dev/null
+++ b/v7/mediarouter/res/values-mn-rMN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Систем"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Төхөөрөмжүүд"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Медиа гаралт"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Төхөөрөмжтэй холбох"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Төхөөрөмжүүдийг хайж байна…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Салгах"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-mr-rIN/strings.xml b/v7/mediarouter/res/values-mr-rIN/strings.xml
new file mode 100644
index 0000000..9187b5d
--- /dev/null
+++ b/v7/mediarouter/res/values-mr-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"सिस्टम"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"डिव्हाइसेस"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"माध्यम आउटपुट"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"डिव्हाइसला कनेक्ट करा"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"डिव्हाइसेस शोधत आहे…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"डिस्कनेक्ट करा"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-mr-rMH/strings.xml b/v7/mediarouter/res/values-mr-rMH/strings.xml
new file mode 100644
index 0000000..9187b5d
--- /dev/null
+++ b/v7/mediarouter/res/values-mr-rMH/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"सिस्टम"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"डिव्हाइसेस"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"माध्यम आउटपुट"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"डिव्हाइसला कनेक्ट करा"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"डिव्हाइसेस शोधत आहे…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"डिस्कनेक्ट करा"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ms-rMY/strings.xml b/v7/mediarouter/res/values-ms-rMY/strings.xml
new file mode 100644
index 0000000..dadaa30
--- /dev/null
+++ b/v7/mediarouter/res/values-ms-rMY/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Peranti"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Output media"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Sambung kepada peranti"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Mencari peranti..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Putuskan sambungan"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-my-rMM/strings.xml b/v7/mediarouter/res/values-my-rMM/strings.xml
new file mode 100644
index 0000000..c417d57
--- /dev/null
+++ b/v7/mediarouter/res/values-my-rMM/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"စနစ်"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"စက်ပစ္စည်းများ"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"မီဒီယာထွက်ပေါက်"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"စက်တစ်ခုကို ချိတ်ဆက်ပါ"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"စက်ပစ္စည်းများကို ရှာဖွေနေပါသည်"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"ချိတ်ဆက်ခြင်းရပ်တန့်ရန်"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-nb/strings.xml b/v7/mediarouter/res/values-nb/strings.xml
new file mode 100644
index 0000000..fa4d9a4
--- /dev/null
+++ b/v7/mediarouter/res/values-nb/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Enheter"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Medieutgang"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Koble til enheten"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Søker etter enheter …"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Koble fra"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ne-rNP/strings.xml b/v7/mediarouter/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000..3fe9ac3
--- /dev/null
+++ b/v7/mediarouter/res/values-ne-rNP/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"प्रणाली"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"उपकरणहरू"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"मिडियाको उत्पादन"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"उपकरणसँग जडान गर्नुहोस्"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"उपकरणहरूका लागि खोजी गरिँदै..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"विच्छेदन गर्नुहोस्"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-nl/strings.xml b/v7/mediarouter/res/values-nl/strings.xml
new file mode 100644
index 0000000..5572449
--- /dev/null
+++ b/v7/mediarouter/res/values-nl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Systeem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Apparaten"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Media-uitvoer"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Verbinding maken met apparaat"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Zoeken naar apparaten…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Verbinding verbreken"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-pl/strings.xml b/v7/mediarouter/res/values-pl/strings.xml
new file mode 100644
index 0000000..95a1d03
--- /dev/null
+++ b/v7/mediarouter/res/values-pl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Urządzenia"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Wyjście multimediów"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Połącz z urządzeniem"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Szukam urządzeń…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Rozłącz"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-pt-rPT/strings.xml b/v7/mediarouter/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..54b1dfc
--- /dev/null
+++ b/v7/mediarouter/res/values-pt-rPT/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Saída de som multimédia"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Ligar ao dispositivo"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"A pesquisar dispositivos…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Desassociar"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-pt/strings.xml b/v7/mediarouter/res/values-pt/strings.xml
new file mode 100644
index 0000000..3ce1c38
--- /dev/null
+++ b/v7/mediarouter/res/values-pt/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistema"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispositivos"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Saída de mídia"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Conectar ao dispositivo"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Procurando dispositivos…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Desconectar"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ro/strings.xml b/v7/mediarouter/res/values-ro/strings.xml
new file mode 100644
index 0000000..4c9e4b9
--- /dev/null
+++ b/v7/mediarouter/res/values-ro/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Dispozitive"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Rezultate media"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Conectați-vă la dispozitiv"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Se caută dispozitive..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Deconectați-vă"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ru/strings.xml b/v7/mediarouter/res/values-ru/strings.xml
new file mode 100644
index 0000000..5cc2bba
--- /dev/null
+++ b/v7/mediarouter/res/values-ru/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Система"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Устройства"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Перенаправлять поток мультимедиа"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Подключение к устройству"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Поиск устройств…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Отключить"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-si-rLK/strings.xml b/v7/mediarouter/res/values-si-rLK/strings.xml
new file mode 100644
index 0000000..2eba3c8
--- /dev/null
+++ b/v7/mediarouter/res/values-si-rLK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"පද්ධතිය"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"උපාංග"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"මාධ්ය ප්රතිදානය"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"උපාංගයට සම්බන්ධ වන්න"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"උපාංග සඳහා සොයමින්…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"විසන්ධි කරන්න"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-sk/strings.xml b/v7/mediarouter/res/values-sk/strings.xml
new file mode 100644
index 0000000..668800f
--- /dev/null
+++ b/v7/mediarouter/res/values-sk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Systém"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Zariadenia"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Výstup médií"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Pripojenie k zariadeniu"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Prebieha vyhľadávanie zariadení…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Odpojiť"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-sl/strings.xml b/v7/mediarouter/res/values-sl/strings.xml
new file mode 100644
index 0000000..3e3e8bb
--- /dev/null
+++ b/v7/mediarouter/res/values-sl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Naprave"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Izhod za predstavnost"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Povezovanje z napravo"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Iskanje naprav …"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Prekini povezavo"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-sr/strings.xml b/v7/mediarouter/res/values-sr/strings.xml
new file mode 100644
index 0000000..320f3e8
--- /dev/null
+++ b/v7/mediarouter/res/values-sr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Систем"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Уређаји"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Излаз медија"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Повежите са уређајем"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Претраживање уређаја…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Прекини везу"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-sv/strings.xml b/v7/mediarouter/res/values-sv/strings.xml
new file mode 100644
index 0000000..910c6f1
--- /dev/null
+++ b/v7/mediarouter/res/values-sv/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Enheter"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Medieuppspelning"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Anslut till enhet"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Söker efter enheter ..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Koppla från"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-sw/strings.xml b/v7/mediarouter/res/values-sw/strings.xml
new file mode 100644
index 0000000..fcbc590
--- /dev/null
+++ b/v7/mediarouter/res/values-sw/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Mfumo"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Vifaa"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Towe la vyombo vya habari"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Unganisha kwenye kifaa"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Inatafuta vifaa..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Tenganisha"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ta-rIN/strings.xml b/v7/mediarouter/res/values-ta-rIN/strings.xml
new file mode 100644
index 0000000..d5d1386
--- /dev/null
+++ b/v7/mediarouter/res/values-ta-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"அமைப்பு"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"சாதனங்கள்"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"மீடியா வெளியீடு"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"சாதனத்துடன் இணைக்கவும்"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"சாதனங்களைத் தேடுகிறது..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"துண்டி"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ta-rTN/strings.xml b/v7/mediarouter/res/values-ta-rTN/strings.xml
new file mode 100644
index 0000000..d5d1386
--- /dev/null
+++ b/v7/mediarouter/res/values-ta-rTN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"அமைப்பு"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"சாதனங்கள்"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"மீடியா வெளியீடு"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"சாதனத்துடன் இணைக்கவும்"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"சாதனங்களைத் தேடுகிறது..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"துண்டி"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-te-rAP/strings.xml b/v7/mediarouter/res/values-te-rAP/strings.xml
new file mode 100644
index 0000000..9fa6e90
--- /dev/null
+++ b/v7/mediarouter/res/values-te-rAP/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"సిస్టమ్"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"పరికరాలు"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"మీడియా అవుట్పుట్"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"పరికరానికి కనెక్ట్ చేయండి"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"పరికరాల కోసం శోధిస్తోంది…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"డిస్కనెక్ట్ చేయి"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-te-rIN/strings.xml b/v7/mediarouter/res/values-te-rIN/strings.xml
new file mode 100644
index 0000000..9fa6e90
--- /dev/null
+++ b/v7/mediarouter/res/values-te-rIN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"సిస్టమ్"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"పరికరాలు"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"మీడియా అవుట్పుట్"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"పరికరానికి కనెక్ట్ చేయండి"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"పరికరాల కోసం శోధిస్తోంది…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"డిస్కనెక్ట్ చేయి"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-th/strings.xml b/v7/mediarouter/res/values-th/strings.xml
new file mode 100644
index 0000000..78e5a73
--- /dev/null
+++ b/v7/mediarouter/res/values-th/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"ระบบ"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"อุปกรณ์"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"เอาต์พุตสื่อ"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"เชื่อมต่อกับอุปกรณ์"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"กำลังค้นหาอุปกรณ์…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"ยกเลิกการเชื่อมต่อ"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-tl/strings.xml b/v7/mediarouter/res/values-tl/strings.xml
new file mode 100644
index 0000000..0953787
--- /dev/null
+++ b/v7/mediarouter/res/values-tl/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"System"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Mga Device"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Output ng media"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Kumonekta sa device"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Naghahanap ng mga device…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Idiskonekta"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-tr/strings.xml b/v7/mediarouter/res/values-tr/strings.xml
new file mode 100644
index 0000000..12faaa6
--- /dev/null
+++ b/v7/mediarouter/res/values-tr/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Sistem"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Cihazlar"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Medya çıkışı"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Cihaza bağlanın"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Cihaz arayın…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Bağlantıyı kes"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-uk/strings.xml b/v7/mediarouter/res/values-uk/strings.xml
new file mode 100644
index 0000000..b036dea
--- /dev/null
+++ b/v7/mediarouter/res/values-uk/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Система"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Пристрої"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Вивід медіа-даних"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Під’єднатися до пристрою"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Пошук пристроїв…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Від’єднатися"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-ur-rPK/strings.xml b/v7/mediarouter/res/values-ur-rPK/strings.xml
new file mode 100644
index 0000000..bce0e0c
--- /dev/null
+++ b/v7/mediarouter/res/values-ur-rPK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"سسٹم"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"آلات"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"میڈیا آؤٹ پٹ"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"آلہ سے مربوط ہوں"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"آلات تلاش کر رہا ہے…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"غیر مربوط کریں"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-uz-rUZ/strings.xml b/v7/mediarouter/res/values-uz-rUZ/strings.xml
new file mode 100644
index 0000000..f191fd9
--- /dev/null
+++ b/v7/mediarouter/res/values-uz-rUZ/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Tizim"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Qurilmalar"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Media chiqish"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Qurilmaga ulanish"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Qurilmalar izlanmoqda…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Uzish"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-vi/strings.xml b/v7/mediarouter/res/values-vi/strings.xml
new file mode 100644
index 0000000..a58d0e4
--- /dev/null
+++ b/v7/mediarouter/res/values-vi/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Hệ thống"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Thiết bị"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Đầu ra phương tiện"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Kết nối với thiết bị"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Đang tìm kiếm thiết bị…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Ngắt kết nối"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-zh-rCN/strings.xml b/v7/mediarouter/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..71c4407
--- /dev/null
+++ b/v7/mediarouter/res/values-zh-rCN/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"系统"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"设备"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"媒体输出线路"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"连接到设备"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"正在搜索设备…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"断开连接"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-zh-rHK/strings.xml b/v7/mediarouter/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..f499169
--- /dev/null
+++ b/v7/mediarouter/res/values-zh-rHK/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"系統"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"裝置"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"媒體輸出"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"連線至裝置"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"正在搜尋裝置…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"中斷連線"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-zh-rTW/strings.xml b/v7/mediarouter/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..a847615
--- /dev/null
+++ b/v7/mediarouter/res/values-zh-rTW/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"系統"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"裝置"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"媒體輸出"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"連線至裝置"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"正在搜尋裝置..."</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"中斷連線"</string>
+</resources>
diff --git a/v7/mediarouter/res/values-zu/strings.xml b/v7/mediarouter/res/values-zu/strings.xml
new file mode 100644
index 0000000..be195be
--- /dev/null
+++ b/v7/mediarouter/res/values-zu/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2013 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="mr_system_route_name" msgid="5441529851481176817">"Isistimu"</string>
+ <string name="mr_user_route_category_name" msgid="7498112907524977311">"Amadivayisi"</string>
+ <string name="mr_media_route_button_content_description" msgid="4271159405637008602">"Okukhiphayo kwabezindaba"</string>
+ <string name="mr_media_route_chooser_title" msgid="7106830097177242655">"Xhumeka kudivayisi"</string>
+ <string name="mr_media_route_chooser_searching" msgid="7553005460920830010">"Iseshela amadivayisi…"</string>
+ <string name="mr_media_route_controller_disconnect" msgid="109793632378378069">"Nqamula"</string>
+</resources>
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
index e9807ac..3a87f02 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
@@ -111,7 +111,7 @@
*
* @param routes The list of routes to filter in-place, never null.
*/
- public void onFilterRoutes(List<MediaRouter.RouteInfo> routes) {
+ public void onFilterRoutes(@NonNull List<MediaRouter.RouteInfo> routes) {
for (int i = routes.size(); i-- > 0; ) {
if (!onFilterRoute(routes.get(i))) {
routes.remove(i);
diff --git a/v7/palette/.classpath b/v7/palette/.classpath
new file mode 100644
index 0000000..43cb38c
--- /dev/null
+++ b/v7/palette/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="gen"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
+ <classpathentry kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
+ <classpathentry kind="output" path="bin/classes"/>
+</classpath>
diff --git a/v7/palette/.project b/v7/palette/.project
new file mode 100644
index 0000000..76e11a6
--- /dev/null
+++ b/v7/palette/.project
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>android-support-v7-palette</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>com.android.ide.eclipse.adt.ApkBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>com.android.ide.eclipse.adt.AndroidNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/v7/palette/Android.mk b/v7/palette/Android.mk
new file mode 100644
index 0000000..877e518
--- /dev/null
+++ b/v7/palette/Android.mk
@@ -0,0 +1,23 @@
+# Copyright (C) 2014 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+# Here is the final static library that apps can link against.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v7-palette
+LOCAL_SDK_VERSION := 7
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+LOCAL_JAVA_LIBRARIES += android-support-v4
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v7/palette/AndroidManifest.xml
similarity index 68%
copy from v17/leanback/res/drawable/lb_transition_action_bg.xml
copy to v7/palette/AndroidManifest.xml
index 36688bc..20e14c2 100644
--- a/v17/leanback/res/drawable/lb_transition_action_bg.xml
+++ b/v7/palette/AndroidManifest.xml
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
-<!--
- Copyright (C) 2014 The Android Open Source Project
+<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,8 +13,9 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
-<transition xmlns:android="http://schemas.android.com/apk/res/android">
- <item android:drawable="@android:color/transparent" />
- <item android:drawable="@drawable/lb_action_bg_focused" />
-</transition>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.v7.palette">
+ <uses-sdk android:minSdkVersion="7"/>
+ <application>
+ </application>
+</manifest>
diff --git a/v7/palette/README.txt b/v7/palette/README.txt
new file mode 100644
index 0000000..4a8b5e4
--- /dev/null
+++ b/v7/palette/README.txt
@@ -0,0 +1 @@
+Library Project including Palette for color extraction from images
diff --git a/v7/palette/build.gradle b/v7/palette/build.gradle
new file mode 100644
index 0000000..863028a
--- /dev/null
+++ b/v7/palette/build.gradle
@@ -0,0 +1,93 @@
+apply plugin: 'android-library'
+
+archivesBaseName = 'palette-v7'
+
+dependencies {
+ compile project(':support-v4')
+}
+
+android {
+ compileSdkVersion 7
+ buildToolsVersion "19.0.1"
+
+ defaultConfig {
+ minSdkVersion 7
+ }
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ main.java.srcDir 'src'
+ }
+
+ lintOptions {
+ // TODO: fix errors and reenable.
+ abortOnError false
+ }
+}
+
+android.libraryVariants.all { variant ->
+ def name = variant.buildType.name
+
+ if (name.equals(com.android.builder.BuilderConstants.DEBUG)) {
+ return; // Skip debug builds.
+ }
+ def suffix = name.capitalize()
+
+ def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
+ dependsOn variant.javaCompile
+ from variant.javaCompile.destinationDir
+ from 'LICENSE.txt'
+ }
+ def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
+ source android.sourceSets.main.allJava
+ classpath = files(variant.javaCompile.classpath.files) + files(
+ "${android.plugin.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
+ }
+
+ def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
+ classifier = 'javadoc'
+ from 'build/docs/javadoc'
+ }
+
+ def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
+ classifier = 'sources'
+ from android.sourceSets.main.allSource
+ }
+
+ artifacts.add('archives', javadocJarTask);
+ artifacts.add('archives', sourcesJarTask);
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ repository(url: uri(rootProject.ext.supportRepoOut)) {
+ }
+
+ pom.project {
+ name 'Android Support Palette v7'
+ description "Android Support for extracting color palettes from images"
+ url 'http://developer.android.com/tools/extras/support-library.html'
+ inceptionYear '2011'
+
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+
+ scm {
+ url "http://source.android.com"
+ connection "scm:git:https://android.googlesource.com/platform/frameworks/support"
+ }
+ developers {
+ developer {
+ name 'The Android Open Source Project'
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/v7/palette/project.properties b/v7/palette/project.properties
new file mode 100644
index 0000000..1e106c3
--- /dev/null
+++ b/v7/palette/project.properties
@@ -0,0 +1,15 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-7
+android.library=true
\ No newline at end of file
diff --git a/v7/palette/src/android/support/v7/graphics/ColorCutQuantizer.java b/v7/palette/src/android/support/v7/graphics/ColorCutQuantizer.java
new file mode 100644
index 0000000..865af59
--- /dev/null
+++ b/v7/palette/src/android/support/v7/graphics/ColorCutQuantizer.java
@@ -0,0 +1,445 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.graphics;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.util.SparseIntArray;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.PriorityQueue;
+
+import android.support.v7.graphics.Palette.Swatch;
+
+/**
+ * An color quantizer based on the Median-cut algorithm, but optimized for picking out distinct
+ * colors rather than representation colors.
+ *
+ * The color space is represented as a 3-dimensional cube with each dimension being an RGB
+ * component. The cube is then repeatedly divided until we have reduced the color space to the
+ * requested number of colors. An average color is then generated from each cube.
+ *
+ * What makes this different to median-cut is that median-cut divided cubes so that all of the cubes
+ * have roughly the same population, where this quantizer divides boxes based on their color volume.
+ * This means that the color space is divided into distinct colors, rather than representative
+ * colors.
+ */
+final class ColorCutQuantizer {
+
+ private static final String LOG_TAG = ColorCutQuantizer.class.getSimpleName();
+
+ private final float[] mTempHsl = new float[3];
+
+ private static final float BLACK_MAX_LIGHTNESS = 0.05f;
+ private static final float WHITE_MIN_LIGHTNESS = 0.95f;
+
+ private static final int COMPONENT_RED = -3;
+ private static final int COMPONENT_GREEN = -2;
+ private static final int COMPONENT_BLUE = -1;
+
+ private final int[] mColors;
+ private final SparseIntArray mColorPopulations;
+
+ private final List<Swatch> mQuantizedColors;
+
+ /**
+ * Factory-method to generate a {@link ColorCutQuantizer} from a {@link Bitmap} object.
+ *
+ * @param bitmap Bitmap to extract the pixel data from
+ * @param maxColors The maximum number of colors that should be in the result palette.
+ */
+ static ColorCutQuantizer fromBitmap(Bitmap bitmap, int maxColors) {
+ final int width = bitmap.getWidth();
+ final int height = bitmap.getHeight();
+
+ final int[] pixels = new int[width * height];
+ bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
+
+ return new ColorCutQuantizer(new ColorHistogram(pixels), maxColors);
+ }
+
+ /**
+ * Private constructor.
+ *
+ * @param colorHistogram histogram representing an image's pixel data
+ * @param maxColors The maximum number of colors that should be in the result palette.
+ */
+ private ColorCutQuantizer(ColorHistogram colorHistogram, int maxColors) {
+ final int rawColorCount = colorHistogram.getNumberOfColors();
+ final int[] rawColors = colorHistogram.getColors();
+ final int[] rawColorCounts = colorHistogram.getColorCounts();
+
+ // First, lets pack the populations into a SparseIntArray so that they can be easily
+ // retrieved without knowing a color's index
+ mColorPopulations = new SparseIntArray(rawColorCount);
+ for (int i = 0; i < rawColors.length; i++) {
+ mColorPopulations.append(rawColors[i], rawColorCounts[i]);
+ }
+
+ // Now go through all of the colors and keep those which we do not want to ignore
+ mColors = new int[rawColorCount];
+ int validColorCount = 0;
+ for (int color : rawColors) {
+ if (!shouldIgnoreColor(color)) {
+ mColors[validColorCount++] = color;
+ }
+ }
+
+ if (validColorCount <= maxColors) {
+ // The image has fewer colors than the maximum requested, so just return the colors
+ mQuantizedColors = new ArrayList<Swatch>();
+ for (final int color : mColors) {
+ mQuantizedColors.add(new Swatch(color, mColorPopulations.get(color)));
+ }
+ } else {
+ // We need use quantization to reduce the number of colors
+ mQuantizedColors = quantizePixels(validColorCount - 1, maxColors);
+ }
+ }
+
+ /**
+ * @return the list of quantized colors
+ */
+ List<Swatch> getQuantizedColors() {
+ return mQuantizedColors;
+ }
+
+ private List<Swatch> quantizePixels(int maxColorIndex, int maxColors) {
+ // Create the priority queue which is sorted by volume descending. This means we always
+ // split the largest box in the queue
+ final PriorityQueue<Vbox> pq = new PriorityQueue<Vbox>(maxColors, VBOX_COMPARATOR_VOLUME);
+
+ // To start, offer a box which contains all of the colors
+ pq.offer(new Vbox(0, maxColorIndex));
+
+ // Now go through the boxes, splitting them until we have reached maxColors or there are no
+ // more boxes to split
+ splitBoxes(pq, maxColors);
+
+ // Finally, return the average colors of the color boxes
+ return generateAverageColors(pq);
+ }
+
+ /**
+ * Iterate through the {@link java.util.Queue}, popping
+ * {@link ColorCutQuantizer.Vbox} objects from the queue
+ * and splitting them. Once split, the new box and the remaining box are offered back to the
+ * queue.
+ *
+ * @param queue {@link java.util.PriorityQueue} to poll for boxes
+ * @param maxSize Maximum amount of boxes to split
+ */
+ private void splitBoxes(final PriorityQueue<Vbox> queue, final int maxSize) {
+ while (queue.size() < maxSize) {
+ final Vbox vbox = queue.poll();
+
+ if (vbox != null && vbox.canSplit()) {
+ // First split the box, and offer the result
+ queue.offer(vbox.splitBox());
+ // Then offer the box back
+ queue.offer(vbox);
+ } else {
+ // If we get here then there are no more boxes to split, so return
+ return;
+ }
+ }
+ }
+
+ private List<Swatch> generateAverageColors(Collection<Vbox> vboxes) {
+ ArrayList<Swatch> colors = new ArrayList<Swatch>(vboxes.size());
+ for (Vbox vbox : vboxes) {
+ Swatch color = vbox.getAverageColor();
+ if (!shouldIgnoreColor(color)) {
+ // As we're averaging a color box, we can still get colors which we do not want, so
+ // we check again here
+ colors.add(color);
+ }
+ }
+ return colors;
+ }
+
+ /**
+ * Represents a tightly fitting box around a color space.
+ */
+ private class Vbox {
+ private int lowerIndex;
+ private int upperIndex;
+
+ private int minRed, maxRed;
+ private int minGreen, maxGreen;
+ private int minBlue, maxBlue;
+
+ Vbox(int lowerIndex, int upperIndex) {
+ this.lowerIndex = lowerIndex;
+ this.upperIndex = upperIndex;
+ fitBox();
+ }
+
+ int getVolume() {
+ return (maxRed - minRed + 1) * (maxGreen - minGreen + 1) * (maxBlue - minBlue + 1);
+ }
+
+ boolean canSplit() {
+ return getColorCount() > 1;
+ }
+
+ int getColorCount() {
+ return upperIndex - lowerIndex;
+ }
+
+ /**
+ * Recomputes the boundaries of this box to tightly fit the colors within the box.
+ */
+ void fitBox() {
+ // Reset the min and max to opposite values
+ minRed = minGreen = minBlue = 0xFF;
+ maxRed = maxGreen = maxBlue = 0x0;
+
+ for (int i = lowerIndex; i <= upperIndex; i++) {
+ final int color = mColors[i];
+ int r = Color.red(color);
+ int g = Color.green(color);
+ int b = Color.blue(color);
+ if (r > maxRed) {
+ maxRed = r;
+ }
+ if (r < minRed) {
+ minRed = r;
+ }
+ if (g > maxGreen) {
+ maxGreen = g;
+ }
+ if (g < minGreen) {
+ minGreen = g;
+ }
+ if (b > maxBlue) {
+ maxBlue = b;
+ }
+ if (b < minBlue) {
+ minBlue = b;
+ }
+ }
+ }
+
+ /**
+ * Split this color box at the mid-point along it's longest dimension
+ *
+ * @return the new ColorBox
+ */
+ Vbox splitBox() {
+ if (!canSplit()) {
+ throw new IllegalStateException("Can not split a box with only 1 color");
+ }
+
+ // find median along the longest dimension
+ final int splitPoint = findSplitPoint();
+
+ Vbox newBox = new Vbox(splitPoint + 1, upperIndex);
+
+ // Now change this box's upperIndex and recompute the color boundaries
+ upperIndex = splitPoint;
+ fitBox();
+
+ return newBox;
+ }
+
+ /**
+ * @return the dimension which this box is largest in
+ */
+ int getLongestColorDimension() {
+ final int redLength = maxRed - minRed;
+ final int greenLength = maxGreen - minGreen;
+ final int blueLength = maxBlue - minBlue;
+
+ if (redLength >= greenLength && redLength >= blueLength) {
+ return COMPONENT_RED;
+ } else if (greenLength >= redLength && greenLength >= blueLength) {
+ return COMPONENT_GREEN;
+ } else {
+ return COMPONENT_BLUE;
+ }
+ }
+
+ /**
+ * Finds the point within this box's lowerIndex and upperIndex index of where to split.
+ *
+ * This is calculated by finding the longest color dimension, and then sorting the
+ * sub-array based on that dimension value in each color. The colors are then iterated over
+ * until a color is found with at least the midpoint of the whole box's dimension midpoint.
+ *
+ * @return the index of the colors array to split from
+ */
+ int findSplitPoint() {
+ final int longestDimension = getLongestColorDimension();
+
+ // We need to sort the colors in this box based on the longest color dimension.
+ // As we can't use a Comparator to define the sort logic, we modify each color so that
+ // it's most significant is the desired dimension
+ modifySignificantOctet(longestDimension, lowerIndex, upperIndex);
+
+ // Now sort...
+ Arrays.sort(mColors, lowerIndex, upperIndex + 1);
+
+ // Now revert all of the colors so that they are packed as RGB again
+ modifySignificantOctet(longestDimension, lowerIndex, upperIndex);
+
+ final int dimensionMidPoint = midPoint(longestDimension);
+
+ for (int i = lowerIndex; i < upperIndex; i++) {
+ final int color = mColors[i];
+
+ switch (longestDimension) {
+ case COMPONENT_RED:
+ if (Color.red(color) >= dimensionMidPoint) {
+ return i;
+ }
+ break;
+ case COMPONENT_GREEN:
+ if (Color.green(color) >= dimensionMidPoint) {
+ return i;
+ }
+ break;
+ case COMPONENT_BLUE:
+ if (Color.blue(color) > dimensionMidPoint) {
+ return i;
+ }
+ break;
+ }
+ }
+
+ return lowerIndex;
+ }
+
+ /**
+ * @return the average color of this box.
+ */
+ Swatch getAverageColor() {
+ int redSum = 0;
+ int greenSum = 0;
+ int blueSum = 0;
+ int totalPopulation = 0;
+
+ for (int i = lowerIndex; i <= upperIndex; i++) {
+ final int color = mColors[i];
+ final int colorPopulation = mColorPopulations.get(color);
+
+ totalPopulation += colorPopulation;
+ redSum += colorPopulation * Color.red(color);
+ greenSum += colorPopulation * Color.green(color);
+ blueSum += colorPopulation * Color.blue(color);
+ }
+
+ final int redAverage = Math.round(redSum / (float) totalPopulation);
+ final int greenAverage = Math.round(greenSum / (float) totalPopulation);
+ final int blueAverage = Math.round(blueSum / (float) totalPopulation);
+
+ return new Swatch(redAverage, greenAverage, blueAverage, totalPopulation);
+ }
+
+ /**
+ * @return the midpoint of this box in the given {@code dimension}
+ */
+ int midPoint(int dimension) {
+ switch (dimension) {
+ case COMPONENT_RED:
+ default:
+ return (minRed + maxRed) / 2;
+ case COMPONENT_GREEN:
+ return (minGreen + maxGreen) / 2;
+ case COMPONENT_BLUE:
+ return (minBlue + maxBlue) / 2;
+ }
+ }
+ }
+
+ /**
+ * Modify the significant octet in a packed color int. Allows sorting based on the value of a
+ * single color component.
+ *
+ * @see Vbox#findSplitPoint()
+ */
+ private void modifySignificantOctet(final int dimension, int lowIndex, int highIndex) {
+ switch (dimension) {
+ case COMPONENT_RED:
+ // Already in RGB, no need to do anything
+ break;
+ case COMPONENT_GREEN:
+ // We need to do a RGB to GRB swap, or vice-versa
+ for (int i = lowIndex; i <= highIndex; i++) {
+ final int color = mColors[i];
+ mColors[i] = Color.rgb((color >> 8) & 0xFF, (color >> 16) & 0xFF, color & 0xFF);
+ }
+ break;
+ case COMPONENT_BLUE:
+ // We need to do a RGB to BGR swap, or vice-versa
+ for (int i = lowIndex; i <= highIndex; i++) {
+ final int color = mColors[i];
+ mColors[i] = Color.rgb(color & 0xFF, (color >> 8) & 0xFF, (color >> 16) & 0xFF);
+ }
+ break;
+ }
+ }
+
+ private boolean shouldIgnoreColor(int color) {
+ ColorUtils.RGBtoHSL(Color.red(color), Color.green(color), Color.blue(color), mTempHsl);
+ return shouldIgnoreColor(mTempHsl);
+ }
+
+ private static boolean shouldIgnoreColor(Swatch color) {
+ return shouldIgnoreColor(color.getHsl());
+ }
+
+ private static boolean shouldIgnoreColor(float[] hslColor) {
+ return isWhite(hslColor) || isBlack(hslColor) || isNearRedILine(hslColor);
+ }
+
+ /**
+ * @return true if the color represents a color which is close to black.
+ */
+ private static boolean isBlack(float[] hslColor) {
+ return hslColor[2] <= BLACK_MAX_LIGHTNESS;
+ }
+
+ /**
+ * @return true if the color represents a color which is close to white.
+ */
+ private static boolean isWhite(float[] hslColor) {
+ return hslColor[2] >= WHITE_MIN_LIGHTNESS;
+ }
+
+ /**
+ * @return true if the color lies close to the red side of the I line.
+ */
+ private static boolean isNearRedILine(float[] hslColor) {
+ return hslColor[0] >= 10f && hslColor[0] <= 37f && hslColor[1] <= 0.82f;
+ }
+
+ /**
+ * Comparator which sorts {@link Vbox} instances based on their volume, in descending order
+ */
+ private static final Comparator<Vbox> VBOX_COMPARATOR_VOLUME = new Comparator<Vbox>() {
+ @Override
+ public int compare(Vbox lhs, Vbox rhs) {
+ return rhs.getVolume() - lhs.getVolume();
+ }
+ };
+
+}
diff --git a/v7/palette/src/android/support/v7/graphics/ColorHistogram.java b/v7/palette/src/android/support/v7/graphics/ColorHistogram.java
new file mode 100644
index 0000000..bcc43fe
--- /dev/null
+++ b/v7/palette/src/android/support/v7/graphics/ColorHistogram.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.graphics;
+
+import java.util.Arrays;
+
+/**
+ * Class which provides a histogram for RGB values.
+ */
+final class ColorHistogram {
+
+ private final int[] mColors;
+ private final int[] mColorCounts;
+ private final int mNumberColors;
+
+ /**
+ * A new {@link ColorHistogram} instance.
+ *
+ * @param pixels array of image contents
+ */
+ ColorHistogram(final int[] pixels) {
+ // Sort the pixels to enable counting below
+ Arrays.sort(pixels);
+
+ // Count number of distinct colors
+ mNumberColors = countDistinctColors(pixels);
+
+ // Create arrays
+ mColors = new int[mNumberColors];
+ mColorCounts = new int[mNumberColors];
+
+ // Finally count the frequency of each color
+ countFrequencies(pixels);
+ }
+
+ /**
+ * @return number of distinct colors in the image.
+ */
+ int getNumberOfColors() {
+ return mNumberColors;
+ }
+
+ /**
+ * @return an array containing all of the distinct colors in the image.
+ */
+ int[] getColors() {
+ return mColors;
+ }
+
+ /**
+ * @return an array containing the frequency of a distinct colors within the image.
+ */
+ int[] getColorCounts() {
+ return mColorCounts;
+ }
+
+ private static int countDistinctColors(final int[] pixels) {
+ if (pixels.length < 2) {
+ // If we have less than 2 pixels we can stop here
+ return pixels.length;
+ }
+
+ // If we have at least 2 pixels, we have a minimum of 1 color...
+ int colorCount = 1;
+ int currentColor = pixels[0];
+
+ // Now iterate from the second pixel to the end, counting distinct colors
+ for (int i = 1; i < pixels.length; i++) {
+ // If we encounter a new color, increase the population
+ if (pixels[i] != currentColor) {
+ currentColor = pixels[i];
+ colorCount++;
+ }
+ }
+
+ return colorCount;
+ }
+
+ private void countFrequencies(final int[] pixels) {
+ if (pixels.length == 0) {
+ return;
+ }
+
+ int currentColorIndex = 0;
+ int currentColor = pixels[0];
+
+ mColors[currentColorIndex] = currentColor;
+ mColorCounts[currentColorIndex] = 1;
+
+ if (pixels.length == 1) {
+ // If we only have one pixel, we can stop here
+ return;
+ }
+
+ // Now iterate from the second pixel to the end, population distinct colors
+ for (int i = 1; i < pixels.length; i++) {
+ if (pixels[i] == currentColor) {
+ // We've hit the same color as before, increase population
+ mColorCounts[currentColorIndex]++;
+ } else {
+ // We've hit a new color, increase index
+ currentColor = pixels[i];
+
+ currentColorIndex++;
+ mColors[currentColorIndex] = currentColor;
+ mColorCounts[currentColorIndex] = 1;
+ }
+ }
+ }
+
+}
diff --git a/v7/palette/src/android/support/v7/graphics/ColorUtils.java b/v7/palette/src/android/support/v7/graphics/ColorUtils.java
new file mode 100644
index 0000000..34b075e
--- /dev/null
+++ b/v7/palette/src/android/support/v7/graphics/ColorUtils.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.graphics;
+
+import android.graphics.Color;
+
+final class ColorUtils {
+
+ private ColorUtils() {}
+
+ /**
+ * @return luma value according to to XYZ color space in the range 0.0 - 1.0
+ */
+ static float calculateXyzLuma(int color) {
+ return (0.2126f * Color.red(color) +
+ 0.7152f * Color.green(color) +
+ 0.0722f * Color.blue(color)) / 255f;
+ }
+
+ static float calculateContrast(int color1, int color2) {
+ return Math.abs(ColorUtils.calculateXyzLuma(color1) - ColorUtils.calculateXyzLuma(color2));
+ }
+
+ static void RGBtoHSL(int r, int g, int b, float[] hsl) {
+ final float rf = r / 255f;
+ final float gf = g / 255f;
+ final float bf = b / 255f;
+
+ final float max = Math.max(rf, Math.max(gf, bf));
+ final float min = Math.min(rf, Math.min(gf, bf));
+ final float deltaMaxMin = max - min;
+
+ float h, s;
+ float l = (max + min) / 2f;
+
+ if (max == min) {
+ // Monochromatic
+ h = s = 0f;
+ } else {
+ if (max == rf) {
+ h = ((gf - bf) / deltaMaxMin) % 6f;
+ } else if (max == gf) {
+ h = ((bf - rf) / deltaMaxMin) + 2f;
+ } else {
+ h = ((rf - gf) / deltaMaxMin) + 4f;
+ }
+
+ s = deltaMaxMin / (1f - Math.abs(2f * l - 1f));
+ }
+
+ hsl[0] = (h * 60f) % 360f;
+ hsl[1] = s;
+ hsl[2] = l;
+ }
+
+ static int HSLtoRGB (float[] hsl) {
+ final float h = hsl[0];
+ final float s = hsl[1];
+ final float l = hsl[2];
+
+ final float c = (1f - Math.abs(2 * l - 1f)) * s;
+ final float m = l - 0.5f * c;
+ final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
+
+ final int hueSegment = (int) h / 60;
+
+ int r = 0, g = 0, b = 0;
+
+ switch (hueSegment) {
+ case 0:
+ r = Math.round(255 * (c + m));
+ g = Math.round(255 * (x + m));
+ b = Math.round(255 * m);
+ break;
+ case 1:
+ r = Math.round(255 * (x + m));
+ g = Math.round(255 * (c + m));
+ b = Math.round(255 * m);
+ break;
+ case 2:
+ r = Math.round(255 * m);
+ g = Math.round(255 * (c + m));
+ b = Math.round(255 * (x + m));
+ break;
+ case 3:
+ r = Math.round(255 * m);
+ g = Math.round(255 * (x + m));
+ b = Math.round(255 * (c + m));
+ break;
+ case 4:
+ r = Math.round(255 * (x + m));
+ g = Math.round(255 * m);
+ b = Math.round(255 * (c + m));
+ break;
+ case 5:
+ case 6:
+ r = Math.round(255 * (c + m));
+ g = Math.round(255 * m);
+ b = Math.round(255 * (x + m));
+ break;
+ }
+
+ r = Math.max(0, Math.min(255, r));
+ g = Math.max(0, Math.min(255, g));
+ b = Math.max(0, Math.min(255, b));
+
+ return Color.rgb(r, g, b);
+ }
+
+}
diff --git a/v7/palette/src/android/support/v7/graphics/Palette.java b/v7/palette/src/android/support/v7/graphics/Palette.java
new file mode 100644
index 0000000..a645796
--- /dev/null
+++ b/v7/palette/src/android/support/v7/graphics/Palette.java
@@ -0,0 +1,537 @@
+/*
+ * Copyright 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.graphics;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.os.AsyncTask;
+import android.support.v4.os.AsyncTaskCompat;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A helper class to extract prominent colors from an image.
+ * <p>
+ * A number of colors with different profiles are extracted from the image:
+ * <ul>
+ * <li>Vibrant</li>
+ * <li>Vibrant Dark</li>
+ * <li>Vibrant Light</li>
+ * <li>Muted</li>
+ * <li>Muted Dark</li>
+ * <li>Muted Light</li>
+ * </ul>
+ * These can be retrieved from the appropriate getter method.
+ *
+ * <p>
+ * Instances can be created with the synchronous factory methods {@link #generate(Bitmap)} and
+ * {@link #generate(Bitmap, int)}.
+ * <p>
+ * These should be called on a background thread, ideally the one in
+ * which you load your images on. Sometimes that is not possible, so asynchronous factory methods
+ * have also been provided: {@link #generateAsync(Bitmap, PaletteAsyncListener)} and
+ * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)}. These can be used as so:
+ *
+ * <pre>
+ * Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() {
+ * public void onGenerated(Palette palette) {
+ * // Do something with colors...
+ * }
+ * });
+ * </pre>
+ */
+public final class Palette {
+
+ /**
+ * Listener to be used with {@link #generateAsync(Bitmap, PaletteAsyncListener)} or
+ * {@link #generateAsync(Bitmap, int, PaletteAsyncListener)}
+ */
+ public interface PaletteAsyncListener {
+
+ /**
+ * Called when the {@link Palette} has been generated.
+ */
+ void onGenerated(Palette palette);
+ }
+
+ private static final int CALCULATE_BITMAP_MIN_DIMENSION = 100;
+ private static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
+
+ private static final float TARGET_DARK_LUMA = 0.26f;
+ private static final float MAX_DARK_LUMA = 0.45f;
+
+ private static final float MIN_LIGHT_LUMA = 0.55f;
+ private static final float TARGET_LIGHT_LUMA = 0.74f;
+
+ private static final float MIN_NORMAL_LUMA = 0.3f;
+ private static final float TARGET_NORMAL_LUMA = 0.5f;
+ private static final float MAX_NORMAL_LUMA = 0.7f;
+
+ private static final float TARGET_MUTED_SATURATION = 0.3f;
+ private static final float MAX_MUTED_SATURATION = 0.4f;
+
+ private static final float TARGET_VIBRANT_SATURATION = 1f;
+ private static final float MIN_VIBRANT_SATURATION = 0.35f;
+
+ private static final float WEIGHT_SATURATION = 3f;
+ private static final float WEIGHT_LUMA = 6f;
+ private static final float WEIGHT_POPULATION = 1f;
+
+ private final List<Swatch> mSwatches;
+ private final int mHighestPopulation;
+
+ private Swatch mVibrantSwatch;
+ private Swatch mMutedSwatch;
+
+ private Swatch mDarkVibrantSwatch;
+ private Swatch mDarkMutedSwatch;
+
+ private Swatch mLightVibrantSwatch;
+ private Swatch mLightMutedColor;
+
+ /**
+ * Generate a {@link Palette} from a {@link Bitmap} using the default number of colors.
+ */
+ public static Palette generate(Bitmap bitmap) {
+ return generate(bitmap, DEFAULT_CALCULATE_NUMBER_COLORS);
+ }
+
+ /**
+ * Generate a {@link Palette} from a {@link Bitmap} using the specified {@code numColors}.
+ * Good values for {@code numColors} depend on the source image type.
+ * For landscapes, a good values are in the range 12-16. For images which are largely made up
+ * of people's faces then this value should be increased to 24-32.
+ *
+ * @param numColors The maximum number of colors in the generated palette. Increasing this
+ * number will increase the time needed to compute the values.
+ */
+ public static Palette generate(Bitmap bitmap, int numColors) {
+ checkBitmapParam(bitmap);
+ checkNumberColorsParam(numColors);
+
+ // First we'll scale down the bitmap so it's shortest dimension is 100px
+ final Bitmap scaledBitmap = scaleBitmapDown(bitmap);
+
+ // Now generate a quantizer from the Bitmap
+ ColorCutQuantizer quantizer = ColorCutQuantizer.fromBitmap(scaledBitmap, numColors);
+
+ // If created a new bitmap, recycle it
+ if (scaledBitmap != bitmap) {
+ scaledBitmap.recycle();
+ }
+
+ // Now return a ColorExtractor instance
+ return new Palette(quantizer.getQuantizedColors());
+ }
+
+ /**
+ * Generate a {@link Palette} asynchronously. {@link PaletteAsyncListener#onGenerated(Palette)}
+ * will be called with the created instance. The resulting {@link Palette} is the same as
+ * what would be created by calling {@link #generate(Bitmap)}.
+ *
+ * @param listener Listener to be invoked when the {@link Palette} has been generated.
+ *
+ * @return the {@link android.os.AsyncTask} used to asynchronously generate the instance.
+ */
+ public static AsyncTask<Bitmap, Void, Palette> generateAsync(
+ Bitmap bitmap, PaletteAsyncListener listener) {
+ return generateAsync(bitmap, DEFAULT_CALCULATE_NUMBER_COLORS, listener);
+ }
+
+ /**
+ * Generate a {@link Palette} asynchronously. {@link PaletteAsyncListener#onGenerated(Palette)}
+ * will be called with the created instance. The resulting {@link Palette} is the same as what
+ * would be created by calling {@link #generate(Bitmap, int)}.
+ *
+ * @param listener Listener to be invoked when the {@link Palette} has been generated.
+ *
+ * @return the {@link android.os.AsyncTask} used to asynchronously generate the instance.
+ */
+ public static AsyncTask<Bitmap, Void, Palette> generateAsync(
+ final Bitmap bitmap, final int numColors, final PaletteAsyncListener listener) {
+ checkBitmapParam(bitmap);
+ checkNumberColorsParam(numColors);
+ checkAsyncListenerParam(listener);
+
+ return AsyncTaskCompat.executeParallel(
+ new AsyncTask<Bitmap, Void, Palette>() {
+ @Override
+ protected Palette doInBackground(Bitmap... params) {
+ return generate(params[0], numColors);
+ }
+
+ @Override
+ protected void onPostExecute(Palette colorExtractor) {
+ listener.onGenerated(colorExtractor);
+ }
+ }, bitmap);
+ }
+
+ private Palette(List<Swatch> swatches) {
+ mSwatches = swatches;
+ mHighestPopulation = findMaxPopulation();
+
+ mVibrantSwatch = findColor(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA,
+ TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
+
+ mLightVibrantSwatch = findColor(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f,
+ TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
+
+ mDarkVibrantSwatch = findColor(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA,
+ TARGET_VIBRANT_SATURATION, MIN_VIBRANT_SATURATION, 1f);
+
+ mMutedSwatch = findColor(TARGET_NORMAL_LUMA, MIN_NORMAL_LUMA, MAX_NORMAL_LUMA,
+ TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
+
+ mLightMutedColor = findColor(TARGET_LIGHT_LUMA, MIN_LIGHT_LUMA, 1f,
+ TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
+
+ mDarkMutedSwatch = findColor(TARGET_DARK_LUMA, 0f, MAX_DARK_LUMA,
+ TARGET_MUTED_SATURATION, 0f, MAX_MUTED_SATURATION);
+
+ // Now try and generate any missing colors
+ generateEmptySwatches();
+ }
+
+ /**
+ * Returns all of the swatches which make up the palette.
+ */
+ public List<Swatch> getSwatches() {
+ return Collections.unmodifiableList(mSwatches);
+ }
+
+ /**
+ * Returns the most vibrant swatch in the palette. Might be null.
+ */
+ public Swatch getVibrantSwatch() {
+ return mVibrantSwatch;
+ }
+
+ /**
+ * Returns a light and vibrant swatch from the palette. Might be null.
+ */
+ public Swatch getLightVibrantSwatch() {
+ return mLightVibrantSwatch;
+ }
+
+ /**
+ * Returns a dark and vibrant swatch from the palette. Might be null.
+ */
+ public Swatch getDarkVibrantSwatch() {
+ return mDarkVibrantSwatch;
+ }
+
+ /**
+ * Returns a muted swatch from the palette. Might be null.
+ */
+ public Swatch getMutedSwatch() {
+ return mMutedSwatch;
+ }
+
+ /**
+ * Returns a muted and light swatch from the palette. Might be null.
+ */
+ public Swatch getLightMutedSwatch() {
+ return mLightMutedColor;
+ }
+
+ /**
+ * Returns a muted and dark swatch from the palette. Might be null.
+ */
+ public Swatch getDarkMutedSwatch() {
+ return mDarkMutedSwatch;
+ }
+
+ /**
+ * Returns the most vibrant color in the palette as an RGB packed int.
+ *
+ * @param defaultColor value to return if the swatch isn't available
+ */
+ public int getVibrantColor(int defaultColor) {
+ return mVibrantSwatch != null ? mVibrantSwatch.getRgb() : defaultColor;
+ }
+
+ /**
+ * Returns a light and vibrant color from the palette as an RGB packed int.
+ *
+ * @param defaultColor value to return if the swatch isn't available
+ */
+ public int getLightVibrantColor(int defaultColor) {
+ return mLightVibrantSwatch != null ? mLightVibrantSwatch.getRgb() : defaultColor;
+ }
+
+ /**
+ * Returns a dark and vibrant color from the palette as an RGB packed int.
+ *
+ * @param defaultColor value to return if the swatch isn't available
+ */
+ public int getDarkVibrantColor(int defaultColor) {
+ return mDarkVibrantSwatch != null ? mDarkVibrantSwatch.getRgb() : defaultColor;
+ }
+
+ /**
+ * Returns a muted color from the palette as an RGB packed int.
+ *
+ * @param defaultColor value to return if the swatch isn't available
+ */
+ public int getMutedColor(int defaultColor) {
+ return mMutedSwatch != null ? mMutedSwatch.getRgb() : defaultColor;
+ }
+
+ /**
+ * Returns a muted and light color from the palette as an RGB packed int.
+ *
+ * @param defaultColor value to return if the swatch isn't available
+ */
+ public int getLightMutedColor(int defaultColor) {
+ return mLightMutedColor != null ? mLightMutedColor.getRgb() : defaultColor;
+ }
+
+ /**
+ * Returns a muted and dark color from the palette as an RGB packed int.
+ *
+ * @param defaultColor value to return if the swatch isn't available
+ */
+ public int getDarkMutedColor(int defaultColor) {
+ return mDarkMutedSwatch != null ? mDarkMutedSwatch.getRgb() : defaultColor;
+ }
+
+ /**
+ * @return true if we have already selected {@code swatch}
+ */
+ private boolean isAlreadySelected(Swatch swatch) {
+ return mVibrantSwatch == swatch || mDarkVibrantSwatch == swatch ||
+ mLightVibrantSwatch == swatch || mMutedSwatch == swatch ||
+ mDarkMutedSwatch == swatch || mLightMutedColor == swatch;
+ }
+
+ private Swatch findColor(float targetLuma, float minLuma, float maxLuma,
+ float targetSaturation, float minSaturation, float maxSaturation) {
+ Swatch max = null;
+ float maxValue = 0f;
+
+ for (Swatch swatch : mSwatches) {
+ final float sat = swatch.getHsl()[1];
+ final float luma = swatch.getHsl()[2];
+
+ if (sat >= minSaturation && sat <= maxSaturation &&
+ luma >= minLuma && luma <= maxLuma &&
+ !isAlreadySelected(swatch)) {
+ float thisValue = createComparisonValue(sat, targetSaturation, luma, targetLuma,
+ swatch.getPopulation(), mHighestPopulation);
+ if (max == null || thisValue > maxValue) {
+ max = swatch;
+ maxValue = thisValue;
+ }
+ }
+ }
+
+ return max;
+ }
+
+ /**
+ * Try and generate any missing swatches from the swatches we did find.
+ */
+ private void generateEmptySwatches() {
+ if (mVibrantSwatch == null) {
+ // If we do not have a vibrant color...
+ if (mDarkVibrantSwatch != null) {
+ // ...but we do have a dark vibrant, generate the value by modifying the luma
+ final float[] newHsl = copyHslValues(mDarkVibrantSwatch);
+ newHsl[2] = TARGET_NORMAL_LUMA;
+ mVibrantSwatch = new Swatch(ColorUtils.HSLtoRGB(newHsl), 0);
+ }
+ }
+
+ if (mDarkVibrantSwatch == null) {
+ // If we do not have a dark vibrant color...
+ if (mVibrantSwatch != null) {
+ // ...but we do have a vibrant, generate the value by modifying the luma
+ final float[] newHsl = copyHslValues(mVibrantSwatch);
+ newHsl[2] = TARGET_DARK_LUMA;
+ mDarkVibrantSwatch = new Swatch(ColorUtils.HSLtoRGB(newHsl), 0);
+ }
+ }
+ }
+
+ /**
+ * Find the {@link Swatch} with the highest population value and return the population.
+ */
+ private int findMaxPopulation() {
+ int population = 0;
+ for (Swatch swatch : mSwatches) {
+ population = Math.max(population, swatch.getPopulation());
+ }
+ return population;
+ }
+
+ /**
+ * Scale the bitmap down so that it's smallest dimension is
+ * {@value #CALCULATE_BITMAP_MIN_DIMENSION}px. If {@code bitmap} is smaller than this, than it
+ * is returned.
+ */
+ private static Bitmap scaleBitmapDown(Bitmap bitmap) {
+ final int minDimension = Math.min(bitmap.getWidth(), bitmap.getHeight());
+
+ if (minDimension <= CALCULATE_BITMAP_MIN_DIMENSION) {
+ // If the bitmap is small enough already, just return it
+ return bitmap;
+ }
+
+ final float scaleRatio = CALCULATE_BITMAP_MIN_DIMENSION / (float) minDimension;
+ return Bitmap.createScaledBitmap(bitmap,
+ Math.round(bitmap.getWidth() * scaleRatio),
+ Math.round(bitmap.getHeight() * scaleRatio),
+ false);
+ }
+
+ private static float createComparisonValue(float saturation, float targetSaturation,
+ float luma, float targetLuma,
+ int population, int highestPopulation) {
+ return weightedMean(
+ invertDiff(saturation, targetSaturation), WEIGHT_SATURATION,
+ invertDiff(luma, targetLuma), WEIGHT_LUMA,
+ population / (float) highestPopulation, WEIGHT_POPULATION
+ );
+ }
+
+ /**
+ * Copy a {@link Swatch}'s HSL values into a new float[].
+ */
+ private static float[] copyHslValues(Swatch color) {
+ final float[] newHsl = new float[3];
+ System.arraycopy(color.getHsl(), 0, newHsl, 0, 3);
+ return newHsl;
+ }
+
+ /**
+ * Returns a value in the range 0-1. 1 is returned when {@code value} equals the
+ * {@code targetValue} and then decreases as the absolute difference between {@code value} and
+ * {@code targetValue} increases.
+ *
+ * @param value the item's value
+ * @param targetValue the value which we desire
+ */
+ private static float invertDiff(float value, float targetValue) {
+ return 1f - Math.abs(value - targetValue);
+ }
+
+ private static float weightedMean(float... values) {
+ float sum = 0f;
+ float sumWeight = 0f;
+
+ for (int i = 0; i < values.length; i += 2) {
+ float value = values[i];
+ float weight = values[i + 1];
+
+ sum += (value * weight);
+ sumWeight += weight;
+ }
+
+ return sum / sumWeight;
+ }
+
+ private static void checkBitmapParam(Bitmap bitmap) {
+ if (bitmap == null) {
+ throw new IllegalArgumentException("bitmap can not be null");
+ }
+ if (bitmap.isRecycled()) {
+ throw new IllegalArgumentException("bitmap can not be recycled");
+ }
+ }
+
+ private static void checkNumberColorsParam(int numColors) {
+ if (numColors < 1) {
+ throw new IllegalArgumentException("numColors must be 1 of greater");
+ }
+ }
+
+ private static void checkAsyncListenerParam(PaletteAsyncListener listener) {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener can not be null");
+ }
+ }
+
+ /**
+ * Represents a color swatch generated from an image's palette. The RGB color can be retrieved
+ * by calling {@link #getRgb()}.
+ */
+ public static final class Swatch {
+
+ final int mRed, mGreen, mBlue;
+ final int mRgb;
+ final int mPopulation;
+
+ private float[] mHsl;
+
+ Swatch(int rgbColor, int population) {
+ mRed = Color.red(rgbColor);
+ mGreen = Color.green(rgbColor);
+ mBlue = Color.blue(rgbColor);
+ mRgb = rgbColor;
+ mPopulation = population;
+ }
+
+ Swatch(int red, int green, int blue, int population) {
+ mRed = red;
+ mGreen = green;
+ mBlue = blue;
+ mRgb = Color.rgb(red, green, blue);
+ mPopulation = population;
+ }
+
+ /**
+ * @return this swatch's RGB color value
+ */
+ public int getRgb() {
+ return mRgb;
+ }
+
+ /**
+ * Return this swatch's HSL values.
+ * hsv[0] is Hue [0 .. 360)
+ * hsv[1] is Saturation [0...1]
+ * hsv[2] is Lightness [0...1]
+ */
+ public float[] getHsl() {
+ if (mHsl == null) {
+ // Lazily generate HSL values from RGB
+ mHsl = new float[3];
+ ColorUtils.RGBtoHSL(mRed, mGreen, mBlue, mHsl);
+ }
+ return mHsl;
+ }
+
+ /**
+ * @return the number of pixels represented by this swatch
+ */
+ public int getPopulation() {
+ return mPopulation;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(getClass().getSimpleName()).append(" ")
+ .append("[").append(Integer.toHexString(getRgb())).append(']')
+ .append("[HSL: ").append(Arrays.toString(getHsl())).append(']')
+ .append("[Population: ").append(mPopulation).append(']').toString();
+ }
+ }
+
+}
diff --git a/v7/recyclerview/build.gradle b/v7/recyclerview/build.gradle
index f7d0607..40a58ad 100644
--- a/v7/recyclerview/build.gradle
+++ b/v7/recyclerview/build.gradle
@@ -10,6 +10,10 @@
compileSdkVersion 7
buildToolsVersion "19.0.1"
+ defaultConfig {
+ minSdkVersion 7
+ }
+
sourceSets {
main.manifest.srcFile 'AndroidManifest.xml'
main.java.srcDir 'src'
@@ -21,4 +25,71 @@
// TODO: fix errors and reenable.
abortOnError false
}
-}
\ No newline at end of file
+}
+
+android.libraryVariants.all { variant ->
+ def name = variant.buildType.name
+
+ if (name.equals(com.android.builder.BuilderConstants.DEBUG)) {
+ return; // Skip debug builds.
+ }
+ def suffix = name.capitalize()
+
+ def jarTask = project.tasks.create(name: "jar${suffix}", type: Jar){
+ dependsOn variant.javaCompile
+ from variant.javaCompile.destinationDir
+ from 'LICENSE.txt'
+ }
+ def javadocTask = project.tasks.create(name: "javadoc${suffix}", type: Javadoc) {
+ source android.sourceSets.main.allJava
+ classpath = files(variant.javaCompile.classpath.files) + files(
+ "${android.plugin.sdkDirectory}/platforms/${android.compileSdkVersion}/android.jar")
+ }
+
+ def javadocJarTask = project.tasks.create(name: "javadocJar${suffix}", type: Jar) {
+ classifier = 'javadoc'
+ from 'build/docs/javadoc'
+ }
+
+ def sourcesJarTask = project.tasks.create(name: "sourceJar${suffix}", type: Jar) {
+ classifier = 'sources'
+ from android.sourceSets.main.allSource
+ }
+
+ artifacts.add('archives', javadocJarTask);
+ artifacts.add('archives', sourcesJarTask);
+}
+
+uploadArchives {
+ repositories {
+ mavenDeployer {
+ repository(url: uri(rootProject.ext.supportRepoOut)) {
+ }
+
+ pom.project {
+ name 'Android Support RecyclerView v7'
+ description "Android Support RecyclerView v7"
+ url 'http://developer.android.com/tools/extras/support-library.html'
+ inceptionYear '2011'
+
+ licenses {
+ license {
+ name 'The Apache Software License, Version 2.0'
+ url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
+ distribution 'repo'
+ }
+ }
+
+ scm {
+ url "http://source.android.com"
+ connection "scm:git:https://android.googlesource.com/platform/frameworks/support"
+ }
+ developers {
+ developer {
+ name 'The Android Open Source Project'
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java b/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
new file mode 100644
index 0000000..5a56e1f
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/AdapterHelper.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import android.support.v4.util.Pools;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static android.support.v7.widget.RecyclerView.*;
+
+/**
+ * Helper class that can enqueue and process adapter update operations.
+ * <p>
+ * To support animations, RecyclerView presents an older version the Adapter to best represent
+ * previous state of the layout. Sometimes, this is not trivial when items are removed that were
+ * not laid out, in which case, RecyclerView has no way of providing that item's view for
+ * animations.
+ * <p>
+ * AdapterHelper creates an UpdateOp for each adapter data change then pre-processes them. During
+ * pre processing, AdapterHelper finds out which UpdateOps can be deferred to second layout pass
+ * and which cannot. For the UpdateOps that cannot be deferred, AdapterHelper will change them
+ * according to previously deferred operation and dispatch them before the first layout pass. It
+ * also takes care of updating deferred UpdateOps since order of operations is changed by this
+ * process.
+ * <p>
+ * Although operations may be forwarded to LayoutManager in different orders, resulting data set
+ * is guaranteed to be the consistent.
+ */
+class AdapterHelper {
+
+ final static int POSITION_TYPE_INVISIBLE = 0;
+
+ final static int POSITION_TYPE_NEW_OR_LAID_OUT = 1;
+
+ private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
+
+ final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
+
+ final ArrayList<UpdateOp> mPostponedList = new ArrayList<UpdateOp>();
+
+ final Callback mCallback;
+
+ Runnable mOnItemProcessedCallback;
+
+ final boolean mDisableRecycler;
+
+ AdapterHelper(Callback callback) {
+ this(callback, false);
+ }
+
+ AdapterHelper(Callback callback, boolean disableRecycler) {
+ mCallback = callback;
+ mDisableRecycler = disableRecycler;
+ }
+
+ AdapterHelper addUpdateOp(UpdateOp... ops) {
+ Collections.addAll(mPendingUpdates, ops);
+ return this;
+ }
+
+ void reset() {
+ recycleUpdateOpsAndClearList(mPendingUpdates);
+ recycleUpdateOpsAndClearList(mPostponedList);
+ }
+
+ void preProcess() {
+ final int count = mPendingUpdates.size();
+ for (int i = 0; i < count; i++) {
+ UpdateOp op = mPendingUpdates.get(i);
+ switch (op.cmd) {
+ case UpdateOp.ADD:
+ applyAdd(op);
+ break;
+ case UpdateOp.REMOVE:
+ applyRemove(op);
+ break;
+ case UpdateOp.UPDATE:
+ applyUpdate(op);
+ break;
+ }
+ if (mOnItemProcessedCallback != null) {
+ mOnItemProcessedCallback.run();
+ }
+ }
+ mPendingUpdates.clear();
+ }
+
+ void consumePostponedUpdates() {
+ final int count = mPostponedList.size();
+ for (int i = 0; i < count; i++) {
+ mCallback.onDispatchSecondPass(mPostponedList.get(i));
+ }
+ recycleUpdateOpsAndClearList(mPostponedList);
+ }
+
+ private void applyRemove(UpdateOp op) {
+ int tmpStart = op.positionStart;
+ int tmpCount = 0;
+ int tmpEnd = op.positionStart + op.itemCount;
+ int type = -1;
+ for (int position = op.positionStart; position < tmpEnd; position++) {
+ boolean typeChanged = false;
+ ViewHolder vh = mCallback.findViewHolder(position);
+ if (vh != null || isNewlyAdded(position)) {
+ // If a ViewHolder exists or this is a newly added item, we can defer this update
+ // to post layout stage.
+ // * For existing ViewHolders, we'll fake its existence in the pre-layout phase.
+ // * For items that are added and removed in the same process cycle, they won't
+ // have any effect in pre-layout since their add ops are already deferred to
+ // post-layout pass.
+ if (type == POSITION_TYPE_INVISIBLE) {
+ // Looks like we have other updates that we cannot merge with this one.
+ // Create an UpdateOp and dispatch it to LayoutManager.
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
+ mCallback.offsetPositionsForRemovingInvisible(newOp.positionStart,
+ newOp.itemCount);
+ dispatch(newOp);
+ typeChanged = true;
+ }
+ type = POSITION_TYPE_NEW_OR_LAID_OUT;
+ } else {
+ // This update cannot be recovered because we don't have a ViewHolder representing
+ // this position. Instead, post it to LayoutManager immediately
+ if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
+ // Looks like we have other updates that we cannot merge with this one.
+ // Create UpdateOp op and dispatch it to LayoutManager.
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
+ mCallback.offsetPositionsForRemovingLaidOutOrNewView(newOp.positionStart,
+ newOp.itemCount);
+ postpone(newOp);
+ typeChanged = true;
+ }
+ type = POSITION_TYPE_INVISIBLE;
+ }
+ if (typeChanged) {
+ position -= tmpCount; // also equal to tmpStart
+ tmpEnd -= tmpCount;
+ tmpCount = 1;
+ } else {
+ tmpCount++;
+ }
+ }
+ if (tmpCount != op.itemCount) { // all 1 effect
+ recycleUpdateOp(op);
+ op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
+ }
+ if (type == POSITION_TYPE_INVISIBLE) {
+ mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
+ dispatch(op);
+ } else {
+ mCallback.offsetPositionsForRemovingLaidOutOrNewView(op.positionStart, op.itemCount);
+ postpone(op);
+ }
+ }
+
+ private void applyUpdate(UpdateOp op) {
+ int tmpStart = op.positionStart;
+ int tmpCount = 0;
+ int tmpEnd = op.positionStart + op.itemCount;
+ int type = -1;
+ for (int position = op.positionStart; position < tmpEnd; position++) {
+ ViewHolder vh = mCallback.findViewHolder(position);
+ if (vh != null || isNewlyAdded(position)) { // deferred
+ if (type == POSITION_TYPE_INVISIBLE) {
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount);
+ mCallback.markViewHoldersUpdated(newOp.positionStart, newOp.itemCount);
+ dispatch(newOp);
+ // tmpStart is still same since dispatch already shifts elements
+ position -= newOp.itemCount; // also equal to tmpStart
+ tmpEnd -= newOp.itemCount;
+ tmpCount = 0;
+ }
+ type = POSITION_TYPE_NEW_OR_LAID_OUT;
+ } else { // applied
+ if (type == POSITION_TYPE_NEW_OR_LAID_OUT) {
+ UpdateOp newOp = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
+ mCallback.markViewHoldersUpdated(newOp.positionStart, newOp.itemCount);
+ postpone(newOp);
+ // both type-new and type-laid-out are deferred. This is why we are
+ // resetting out position to here.
+ position -= newOp.itemCount; // also equal to tmpStart
+ tmpEnd -= newOp.itemCount;
+ tmpCount = 0;
+ }
+ type = POSITION_TYPE_INVISIBLE;
+ }
+ tmpCount++;
+ }
+ if (tmpCount != op.itemCount) { // all 1 effect
+ recycleUpdateOp(op);
+ op = obtainUpdateOp(UpdateOp.REMOVE, tmpStart, tmpCount);
+ }
+ mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
+ if (type == POSITION_TYPE_INVISIBLE) {
+ dispatch(op);
+ } else {
+ postpone(op);
+ }
+ }
+
+ private void dispatch(UpdateOp op) {
+ // tricky part.
+ // traverse all postpones and revert their changes on this op if necessary, apply updated
+ // dispatch to them since now they are after this op.
+ final int count = mPostponedList.size();
+ for (int i = count - 1; i >= 0; i--) {
+ UpdateOp postponed = mPostponedList.get(i);
+ if (postponed.positionStart <= op.positionStart) {
+ op.positionStart += postponed.itemCount;
+ } else {
+ postponed.positionStart -= op.itemCount;
+ }
+ }
+ mCallback.onDispatchFirstPass(op);
+ recycleUpdateOp(op);
+ }
+
+ private boolean isNewlyAdded(int position) {
+ final int count = mPostponedList.size();
+ for (int i = 0; i < count; i++) {
+ UpdateOp op = mPostponedList.get(i);
+ if (op.cmd != UpdateOp.ADD) {
+ continue;
+ }
+ int updatedStart = findPositionOffset(op.positionStart, i + 1);
+ if (updatedStart <= position && updatedStart + op.itemCount > position) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void applyAdd(UpdateOp op) {
+ mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
+ postpone(op);
+ }
+
+ private void postpone(UpdateOp op) {
+ mPostponedList.add(op);
+ }
+
+ boolean hasPendingUpdates() {
+ return mPendingUpdates.size() > 0;
+ }
+
+ int findPositionOffset(int position) {
+ return findPositionOffset(position, 0);
+ }
+
+ int findPositionOffset(int position, int firstPostponedItem) {
+ int offsetPosition = position;
+ int count = mPostponedList.size();
+ for (int i = firstPostponedItem; i < count; ++i) {
+ UpdateOp op = mPostponedList.get(i);
+ if (op.positionStart <= offsetPosition) {
+ if (op.cmd == UpdateOp.REMOVE) {
+ offsetPosition -= op.itemCount;
+ } else if (op.cmd == UpdateOp.ADD) {
+ offsetPosition += op.itemCount;
+ }
+ }
+ }
+ return offsetPosition;
+ }
+
+ /**
+ * @return True if updates should be processed.
+ */
+ boolean onItemRangeChanged(int positionStart, int itemCount) {
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount));
+ return mPendingUpdates.size() == 1;
+ }
+
+ /**
+ * @return True if updates should be processed.
+ */
+ boolean onItemRangeInserted(int positionStart, int itemCount) {
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount));
+ return mPendingUpdates.size() == 1;
+ }
+
+ /**
+ * @return True if updates should be processed.
+ */
+ boolean onItemRangeRemoved(int positionStart, int itemCount) {
+ mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount));
+ return mPendingUpdates.size() == 1;
+ }
+
+ /**
+ * Skips pre-processing and applies all updates in one pass.
+ */
+ void consumeUpdatesInOnePass() {
+ // we still consume postponed updates (if there is) in case there was a pre-process call
+ // w/o a matching consumePostponedUpdates.
+ consumePostponedUpdates();
+ final int count = mPendingUpdates.size();
+ for (int i = 0; i < count; i++) {
+ UpdateOp op = mPendingUpdates.get(i);
+ switch (op.cmd) {
+ case UpdateOp.ADD:
+ mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
+ mCallback.onDispatchSecondPass(op);
+ break;
+ case UpdateOp.REMOVE:
+ mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
+ mCallback.onDispatchSecondPass(op);
+ break;
+ case UpdateOp.UPDATE:
+ mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount);
+ break;
+ }
+ if (mOnItemProcessedCallback != null) {
+ mOnItemProcessedCallback.run();
+ }
+ }
+ recycleUpdateOpsAndClearList(mPendingUpdates);
+ }
+
+ /**
+ * Queued operation to happen when child views are updated.
+ */
+ static class UpdateOp {
+
+ static final int ADD = 0;
+
+ static final int REMOVE = 1;
+
+ static final int UPDATE = 2;
+
+ static final int POOL_SIZE = 30;
+
+ int cmd;
+
+ int positionStart;
+
+ int itemCount;
+
+ UpdateOp(int cmd, int positionStart, int itemCount) {
+ this.cmd = cmd;
+ this.positionStart = positionStart;
+ this.itemCount = itemCount;
+ }
+
+ String cmdToString() {
+ switch (cmd) {
+ case ADD:
+ return "add";
+ case REMOVE:
+ return "rm";
+ case UPDATE:
+ return "up";
+ }
+ return "??";
+ }
+
+ @Override
+ public String toString() {
+ return "[" + cmdToString() + ",s:" + positionStart + "c:" + itemCount + "]";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ UpdateOp op = (UpdateOp) o;
+
+ if (cmd != op.cmd) {
+ return false;
+ }
+ if (itemCount != op.itemCount) {
+ return false;
+ }
+ if (positionStart != op.positionStart) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = cmd;
+ result = 31 * result + positionStart;
+ result = 31 * result + itemCount;
+ return result;
+ }
+ }
+
+ UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount) {
+ UpdateOp op = mUpdateOpPool.acquire();
+ if (op == null) {
+ op = new UpdateOp(cmd, positionStart, itemCount);
+ } else {
+ op.cmd = cmd;
+ op.positionStart = positionStart;
+ op.itemCount = itemCount;
+ }
+ return op;
+ }
+
+ void recycleUpdateOp(UpdateOp op) {
+ if (!mDisableRecycler) {
+ mUpdateOpPool.release(op);
+ }
+ }
+
+ void recycleUpdateOpsAndClearList(List<UpdateOp> ops) {
+ final int count = ops.size();
+ for (int i = 0; i < count; i++) {
+ recycleUpdateOp(ops.get(i));
+ }
+ ops.clear();
+ }
+
+ /**
+ * Contract between AdapterHelper and RecyclerView.
+ */
+ static interface Callback {
+
+ ViewHolder findViewHolder(int position);
+
+ void offsetPositionsForRemovingInvisible(int positionStart, int itemCount);
+
+ void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount);
+
+ void markViewHoldersUpdated(int positionStart, int itemCount);
+
+ void onDispatchFirstPass(UpdateOp updateOp);
+
+ void onDispatchSecondPass(UpdateOp updateOp);
+
+ void offsetPositionsForAdd(int positionStart, int itemCount);
+ }
+}
\ No newline at end of file
diff --git a/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java b/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
new file mode 100644
index 0000000..9bff13e
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/ChildHelper.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to manage children.
+ * <p>
+ * It wraps a RecyclerView and adds ability to hide some children. There are two sets of methods
+ * provided by this class. <b>Regular</b> methods are the ones that replicate ViewGroup methods
+ * like getChildAt, getChildCount etc. These methods ignore hidden children.
+ * <p>
+ * When RecyclerView needs direct access to the view group children, it can call unfiltered
+ * methods like get getUnfilteredChildCount or getUnfilteredChildAt.
+ */
+class ChildHelper {
+
+ private static final boolean DEBUG = false;
+
+ private static final String TAG = "ChildrenHelper";
+
+ final Callback mCallback;
+
+ final Bucket mBucket;
+
+ final List<View> mHiddenViews;
+
+ ChildHelper(Callback callback) {
+ mCallback = callback;
+ mBucket = new Bucket();
+ mHiddenViews = new ArrayList<View>();
+ }
+
+ /**
+ * Adds a view to the ViewGroup
+ *
+ * @param child View to add.
+ * @param hidden If set to true, this item will be invisible from regular methods.
+ */
+ void addView(View child, boolean hidden) {
+ addView(child, -1, hidden);
+ }
+
+ /**
+ * Add a view to the ViewGroup at an index
+ *
+ * @param child View to add.
+ * @param index Index of the child from the regular perspective (excluding hidden views).
+ * ChildHelper offsets this index to actual ViewGroup index.
+ * @param hidden If set to true, this item will be invisible from regular methods.
+ */
+ void addView(View child, int index, boolean hidden) {
+ final int offset;
+ if (index < 0) {
+ offset = mCallback.getChildCount();
+ } else {
+ offset = getOffset(index);
+ }
+ mCallback.addView(child, offset);
+ mBucket.insert(offset, hidden);
+ if (hidden) {
+ mHiddenViews.add(child);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "addViewAt " + index + ",h:" + hidden + ", " + this);
+ }
+ }
+
+ private int getOffset(int index) {
+ if (index < 0) {
+ return -1; //anything below 0 won't work as diff will be undefined.
+ }
+ final int limit = mCallback.getChildCount();
+ int offset = index;
+ while (offset < limit) {
+ final int removedBefore = mBucket.countOnesBefore(offset);
+ final int diff = index - (offset - removedBefore);
+ if (diff == 0) {
+ while (mBucket.get(offset)) { // ensure this offset is not hidden
+ offset ++;
+ }
+ return offset;
+ } else {
+ offset += diff;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Removes the provided View from underlying RecyclerView.
+ *
+ * @param view The view to remove.
+ */
+ void removeView(View view) {
+ int index = mCallback.indexOfChild(view);
+ if (index < 0) {
+ return;
+ }
+ mCallback.removeViewAt(index);
+ if (mBucket.remove(index)) {
+ mHiddenViews.remove(view);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "remove View off:" + index + "," + this);
+ }
+ }
+
+ /**
+ * Removes the view at the provided index from RecyclerView.
+ *
+ * @param index Index of the child from the regular perspective (excluding hidden views).
+ * ChildHelper offsets this index to actual ViewGroup index.
+ */
+ void removeViewAt(int index) {
+ final int offset = getOffset(index);
+ final View view = mCallback.getChildAt(offset);
+ if (view == null) {
+ return;
+ }
+ mCallback.removeViewAt(offset);
+ if (mBucket.remove(offset)) {
+ mHiddenViews.remove(view);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "removeViewAt " + index + ", off:" + offset + ", " + this);
+ }
+ }
+
+ /**
+ * Returns the child at provided index.
+ *
+ * @param index Index of the child to return in regular perspective.
+ */
+ View getChildAt(int index) {
+ final int offset = getOffset(index);
+ return mCallback.getChildAt(offset);
+ }
+
+ /**
+ * Removes all views from the ViewGroup including the hidden ones.
+ */
+ void removeAllViewsUnfiltered() {
+ mCallback.removeAllViews();
+ mBucket.reset();
+ mHiddenViews.clear();
+ if (DEBUG) {
+ Log.d(TAG, "removeAllViewsUnfiltered");
+ }
+ }
+
+ /**
+ * This can be used to find a disappearing view by position.
+ *
+ * @param position The adapter position of the item.
+ * @param type View type, can be {@link RecyclerView#INVALID_TYPE}.
+ * @return A hidden view with a valid ViewHolder that matches the position and type.
+ */
+ View findHiddenNonRemovedView(int position, int type) {
+ final int count = mHiddenViews.size();
+ for (int i = 0; i < count; i++) {
+ final View view = mHiddenViews.get(i);
+ RecyclerView.ViewHolder holder = mCallback.getChildViewHolder(view);
+ if (holder.getPosition() == position && !holder.isInvalid() &&
+ (type == RecyclerView.INVALID_TYPE || holder.getItemViewType() == type)) {
+ return view;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Attaches the provided view to the underlying ViewGroup.
+ *
+ * @param child Child to attach.
+ * @param index Index of the child to attach in regular perspective.
+ * @param layoutParams LayoutParams for the child.
+ * @param hidden If set to true, this item will be invisible to the regular methods.
+ */
+ void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,
+ boolean hidden) {
+ final int offset;
+ if (index < 0) {
+ offset = mCallback.getChildCount();
+ } else {
+ offset = getOffset(index);
+ }
+ mCallback.attachViewToParent(child, offset, layoutParams);
+ mBucket.insert(offset, hidden);
+ if (DEBUG) {
+ Log.d(TAG, "attach view to parent index:" + index + ",off:" + offset + "," +
+ "h:" + hidden + ", " + this);
+ }
+ }
+
+ /**
+ * Returns the number of children that are not hidden.
+ *
+ * @return Number of children that are not hidden.
+ * @see #getChildAt(int)
+ */
+ int getChildCount() {
+ return mCallback.getChildCount() - mHiddenViews.size();
+ }
+
+ /**
+ * Returns the total number of children.
+ *
+ * @return The total number of children including the hidden views.
+ * @see #getUnfilteredChildAt(int)
+ */
+ int getUnfilteredChildCount() {
+ return mCallback.getChildCount();
+ }
+
+ /**
+ * Returns a child by ViewGroup offset. ChildHelper won't offset this index.
+ *
+ * @param index ViewGroup index of the child to return.
+ * @return The view in the provided index.
+ */
+ View getUnfilteredChildAt(int index) {
+ return mCallback.getChildAt(index);
+ }
+
+ /**
+ * Detaches the view at the provided index.
+ *
+ * @param index Index of the child to return in regular perspective.
+ */
+ void detachViewFromParent(int index) {
+ final int offset = getOffset(index);
+ mCallback.detachViewFromParent(offset);
+ mBucket.remove(offset);
+ if (DEBUG) {
+ Log.d(TAG, "detach view from parent " + index + ", off:" + offset);
+ }
+ }
+
+ /**
+ * Returns the index of the child in regular perspective.
+ *
+ * @param child The child whose index will be returned.
+ * @return The regular perspective index of the child or -1 if it does not exists.
+ */
+ int indexOfChild(View child) {
+ final int index = mCallback.indexOfChild(child);
+ if (index == -1) {
+ return -1;
+ }
+ if (mBucket.get(index)) {
+ if (DEBUG) {
+ throw new IllegalArgumentException("cannot get index of a hidden child");
+ } else {
+ return -1;
+ }
+ }
+ // reverse the index
+ return index - mBucket.countOnesBefore(index);
+ }
+
+ /**
+ * Marks a child view as hidden.
+ *
+ * @param view The view to hide.
+ */
+ void hide(View view) {
+ final int offset = mCallback.indexOfChild(view);
+ if (offset < 0) {
+ throw new IllegalArgumentException("view is not a child, cannot hide " + view);
+ }
+ mBucket.set(offset);
+ mHiddenViews.add(view);
+ if (DEBUG) {
+ Log.d(TAG, "hiding child " + view + " at offset " + offset+ ", " + this);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return mBucket.toString();
+ }
+
+ /**
+ * Removes a view from the ViewGroup if it is hidden.
+ *
+ * @param view The view to remove.
+ * @return True if the View is found and it is hidden. False otherwise.
+ */
+ boolean removeViewIfHidden(View view) {
+ final int index = mCallback.indexOfChild(view);
+ if (index < -1) {
+ if (DEBUG) {
+ if (mHiddenViews.contains(view)) {
+ throw new IllegalStateException("view is in hidden list but not in view group");
+ }
+ }
+ return true;
+ }
+ if (mBucket.get(index)) {
+ mBucket.remove(index);
+ mCallback.removeViewAt(index);
+ if (!mHiddenViews.remove(view) && DEBUG) {
+ throw new IllegalStateException(
+ "removed a hidden view but it is not in hidden views list");
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Bitset implementation that provides methods to offset indices.
+ */
+ static class Bucket {
+
+ final static int BITS_PER_WORD = Long.SIZE;
+
+ final static long LAST_BIT = 1L << (Long.SIZE - 1);
+
+ long mData = 0;
+
+ Bucket next;
+
+ void set(int index) {
+ if (index >= BITS_PER_WORD) {
+ ensureNext();
+ next.set(index - BITS_PER_WORD);
+ } else {
+ mData |= 1L << index;
+ }
+ }
+
+ private void ensureNext() {
+ if (next == null) {
+ next = new Bucket();
+ }
+ }
+
+ void clear(int index) {
+ if (index >= BITS_PER_WORD) {
+ if (next != null) {
+ next.clear(index - BITS_PER_WORD);
+ }
+ } else {
+ mData &= ~(1L << index);
+ }
+
+ }
+
+ boolean get(int index) {
+ if (index >= BITS_PER_WORD) {
+ ensureNext();
+ return next.get(index - BITS_PER_WORD);
+ } else {
+ return (mData & (1L << index)) != 0;
+ }
+ }
+
+ void reset() {
+ mData = 0;
+ if (next != null) {
+ next.reset();
+ }
+ }
+
+ void insert(int index, boolean value) {
+ if (index >= BITS_PER_WORD) {
+ ensureNext();
+ next.insert(index - BITS_PER_WORD, value);
+ } else {
+ final boolean lastBit = (mData & LAST_BIT) != 0;
+ long mask = (1L << index) - 1;
+ final long before = mData & mask;
+ final long after = ((mData & ~mask)) << 1;
+ mData = before | after;
+ if (value) {
+ set(index);
+ } else {
+ clear(index);
+ }
+ if (lastBit || next != null) {
+ ensureNext();
+ next.insert(0, lastBit);
+ }
+ }
+ }
+
+ boolean remove(int index) {
+ if (index >= BITS_PER_WORD) {
+ ensureNext();
+ return next.remove(index - BITS_PER_WORD);
+ } else {
+ long mask = (1L << index);
+ final boolean value = (mData & mask) != 0;
+ mData &= ~mask;
+ mask = mask - 1;
+ final long before = mData & mask;
+ // cannot use >> because it adds one.
+ final long after = Long.rotateRight(mData & ~mask, 1);
+ mData = before | after;
+ if (next != null) {
+ if (next.get(0)) {
+ set(BITS_PER_WORD - 1);
+ }
+ next.remove(0);
+ }
+ return value;
+ }
+ }
+
+ int countOnesBefore(int index) {
+ if (next == null) {
+ if (index >= BITS_PER_WORD) {
+ return Long.bitCount(mData);
+ }
+ return Long.bitCount(mData & ((1L << index) - 1));
+ }
+ if (index < BITS_PER_WORD) {
+ return Long.bitCount(mData & ((1L << index) - 1));
+ } else {
+ return next.countOnesBefore(index - BITS_PER_WORD) + Long.bitCount(mData);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return next == null ? Long.toBinaryString(mData)
+ : next.toString() + "xx" + Long.toBinaryString(mData);
+ }
+ }
+
+ static interface Callback {
+
+ int getChildCount();
+
+ void addView(View child, int index);
+
+ int indexOfChild(View view);
+
+ void removeViewAt(int index);
+
+ View getChildAt(int offset);
+
+ void removeAllViews();
+
+ RecyclerView.ViewHolder getChildViewHolder(View view);
+
+ void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams);
+
+ void detachViewFromParent(int offset);
+ }
+}
diff --git a/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java b/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java
index dad6438..73271f5 100644
--- a/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java
+++ b/v7/recyclerview/src/android/support/v7/widget/DefaultItemAnimator.java
@@ -34,13 +34,16 @@
private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<ViewHolder>();
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<MoveInfo>();
+ private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<ChangeInfo>();
private ArrayList<ViewHolder> mAdditions = new ArrayList<ViewHolder>();
private ArrayList<MoveInfo> mMoves = new ArrayList<MoveInfo>();
+ private ArrayList<ChangeInfo> mChanges = new ArrayList<ChangeInfo>();
private ArrayList<ViewHolder> mAddAnimations = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mMoveAnimations = new ArrayList<ViewHolder>();
private ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<ViewHolder>();
+ private ArrayList<ChangeInfo> mChangeAnimations = new ArrayList<ChangeInfo>();
private static class MoveInfo {
public ViewHolder holder;
@@ -55,10 +58,20 @@
}
}
+ private static class ChangeInfo {
+ public ViewHolder oldHolder, newHolder;
+
+ private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
+ this.oldHolder = oldHolder;
+ this.newHolder = newHolder;
+ }
+ }
+
@Override
public void runPendingAnimations() {
boolean removalsPending = !mPendingRemovals.isEmpty();
boolean movesPending = !mPendingMoves.isEmpty();
+ boolean changesPending = !mPendingChanges.isEmpty();
boolean additionsPending = !mPendingAdditions.isEmpty();
if (!removalsPending && !movesPending && !additionsPending) {
// nothing to animate
@@ -90,6 +103,26 @@
mover.run();
}
}
+ // Next, change stuff, to run in parallel with move animations
+ if (changesPending) {
+ mChanges.addAll(mPendingChanges);
+ mPendingChanges.clear();
+ Runnable changer = new Runnable() {
+ @Override
+ public void run() {
+ for (ChangeInfo change : mChanges) {
+ animateChangeImpl(change);
+ }
+ mChanges.clear();
+ }
+ };
+ if (removalsPending) {
+ ViewHolder holder = mChanges.get(0).oldHolder;
+ ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration());
+ } else {
+ changer.run();
+ }
+ }
// Next, add stuff
if (additionsPending) {
mAdditions.addAll(mPendingAdditions);
@@ -103,10 +136,12 @@
}
};
if (removalsPending || movesPending) {
+ long removeDuration = removalsPending ? getRemoveDuration() : 0;
+ long moveDuration = movesPending ? getMoveDuration() : 0;
+ long changeDuration = changesPending ? getChangeDuration() : 0;
+ long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
View view = mAdditions.get(0).itemView;
- ViewCompat.postOnAnimationDelayed(view, adder,
- (removalsPending ? getRemoveDuration() : 0) +
- (movesPending ? getMoveDuration() : 0));
+ ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
} else {
adder.run();
}
@@ -151,6 +186,7 @@
public void onAnimationCancel(View view) {
ViewCompat.setAlpha(view, 1);
}
+
@Override
public void onAnimationEnd(View view) {
dispatchAddFinished(holder);
@@ -216,15 +252,121 @@
}
@Override
+ public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder) {
+ if (newHolder != null && newHolder.itemView != null) {
+ ViewCompat.setAlpha(newHolder.itemView, 0);
+ }
+ mPendingChanges.add(new ChangeInfo(oldHolder, newHolder));
+ return true;
+ }
+
+ private void animateChangeImpl(final ChangeInfo changeInfo) {
+ final ViewHolder holder = changeInfo.oldHolder;
+ final View view = holder.itemView;
+ final ViewHolder newHolder = changeInfo.newHolder;
+ final View newView = newHolder != null ? newHolder.itemView : null;
+ ViewCompat.animate(view).cancel();
+ if (newView != null) {
+ ViewCompat.animate(newView).cancel();
+ }
+ ViewCompat.animate(view).setDuration(getChangeDuration()).
+ alpha(0).setListener(new VpaListenerAdapter() {
+ @Override
+ public void onAnimationCancel(View view) {
+ if (newView != null) {
+ ViewCompat.animate(newView).cancel();
+ }
+ }
+ @Override
+ public void onAnimationEnd(View view) {
+ ViewCompat.setAlpha(view, 1);
+ dispatchChangeFinished(holder);
+ mChangeAnimations.remove(changeInfo);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ if (newView != null) {
+ ViewCompat.animate(newView).setDuration(getChangeDuration()).
+ alpha(1).setListener(new VpaListenerAdapter() {
+ @Override
+ public void onAnimationEnd(View view) {
+ ViewCompat.setAlpha(newView, 1);
+ }
+ }).start();
+ }
+ mChangeAnimations.add(changeInfo);
+ }
+
+ @Override
public void endAnimation(ViewHolder item) {
final View view = item.itemView;
ViewCompat.animate(view).cancel();
+ for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
+ MoveInfo moveInfo = mPendingMoves.get(i);
+ if (moveInfo.holder == item) {
+ ViewCompat.setTranslationY(view, 0);
+ ViewCompat.setTranslationX(view, 0);
+ dispatchMoveFinished(item);
+ mPendingMoves.remove(item);
+ break;
+ }
+ }
+ for (int i = mPendingChanges.size() - 1; i >= 0; i--) {
+ ChangeInfo changeInfo = mPendingChanges.get(i);
+ if (changeInfo.oldHolder == item || changeInfo.newHolder == item) {
+ View newView = changeInfo.newHolder != null ? changeInfo.newHolder.itemView : null;
+ ViewCompat.setAlpha(view, 1);
+ if (newView != null) {
+ ViewCompat.setAlpha(newView, 1);
+ }
+ dispatchChangeFinished(changeInfo.oldHolder);
+ mPendingChanges.remove(changeInfo);
+ break;
+ }
+ }
+ if (mPendingRemovals.contains(item)) {
+ dispatchRemoveFinished(item);
+ mPendingRemovals.remove(item);
+ }
+ if (mPendingAdditions.contains(item)) {
+ ViewCompat.setAlpha(view, 1);
+ dispatchAddFinished(item);
+ mPendingAdditions.remove(item);
+ }
if (mMoveAnimations.contains(item)) {
ViewCompat.setTranslationY(view, 0);
ViewCompat.setTranslationX(view, 0);
dispatchMoveFinished(item);
mMoveAnimations.remove(item);
}
+ for (int i = mChanges.size() - 1; i >= 0; i--) {
+ ChangeInfo changeInfo = mChanges.get(i);
+ if (changeInfo.oldHolder == item || changeInfo.newHolder == item) {
+ View newView = changeInfo.newHolder != null ? changeInfo.newHolder.itemView : null;
+ ViewCompat.setAlpha(view, 1);
+ if (newView != null) {
+ ViewCompat.setAlpha(newView, 1);
+ }
+ dispatchChangeFinished(changeInfo.oldHolder);
+ mChanges.remove(changeInfo);
+ break;
+ }
+ }
+ for (int i = mMoves.size() - 1; i >= 0; i--) {
+ MoveInfo moveInfo = mMoves.get(i);
+ if (moveInfo.holder == item) {
+ ViewCompat.setTranslationY(view, 0);
+ ViewCompat.setTranslationX(view, 0);
+ dispatchMoveFinished(item);
+ mMoves.remove(i);
+ break;
+ }
+ }
+ if (mAdditions.contains(item)) {
+ ViewCompat.setAlpha(view, 1);
+ dispatchAddFinished(item);
+ mAdditions.remove(item);
+ }
if (mRemoveAnimations.contains(item)) {
ViewCompat.setAlpha(view, 1);
dispatchRemoveFinished(item);
@@ -235,6 +377,19 @@
dispatchAddFinished(item);
mAddAnimations.remove(item);
}
+ for (int i = mChangeAnimations.size() - 1; i >= 0; i--) {
+ ChangeInfo changeInfo = mChangeAnimations.get(i);
+ if (changeInfo.oldHolder == item || changeInfo.newHolder == item) {
+ View newView = changeInfo.newHolder != null ? changeInfo.newHolder.itemView : null;
+ ViewCompat.setAlpha(view, 1);
+ if (newView != null) {
+ ViewCompat.setAlpha(newView, 1);
+ }
+ dispatchChangeFinished(changeInfo.oldHolder);
+ mChangeAnimations.remove(changeInfo);
+ break;
+ }
+ }
dispatchFinishedWhenDone();
}
@@ -243,8 +398,10 @@
return (!mMoveAnimations.isEmpty() ||
!mRemoveAnimations.isEmpty() ||
!mAddAnimations.isEmpty() ||
+ !mChangeAnimations.isEmpty() ||
!mMoves.isEmpty() ||
- !mAdditions.isEmpty());
+ !mAdditions.isEmpty() ||
+ !mChanges.isEmpty());
}
/**
@@ -260,39 +417,97 @@
@Override
public void endAnimations() {
+ int count = mPendingMoves.size();
+ for (int i = count - 1; i >= 0; i--) {
+ MoveInfo item = mPendingMoves.get(i);
+ View view = item.holder.itemView;
+ ViewCompat.setTranslationY(view, 0);
+ ViewCompat.setTranslationX(view, 0);
+ dispatchMoveFinished(item.holder);
+ mPendingMoves.remove(i);
+ }
+ count = mPendingRemovals.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ViewHolder item = mPendingRemovals.get(i);
+ dispatchRemoveFinished(item);
+ mPendingRemovals.remove(i);
+ }
+ count = mPendingAdditions.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ViewHolder item = mPendingAdditions.get(i);
+ View view = item.itemView;
+ ViewCompat.setAlpha(view, 1);
+ dispatchAddFinished(item);
+ mPendingAdditions.remove(i);
+ }
+ count = mPendingChanges.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ChangeInfo item = mPendingChanges.get(i);
+ View newView = item.newHolder != null ? item.newHolder.itemView : null;
+ if (newView != null) {
+ ViewCompat.setAlpha(newView, 1);
+ }
+ dispatchChangeFinished(item.oldHolder);
+ mPendingChanges.remove(i);
+ }
if (!isRunning()) {
return;
}
- int count = mMoveAnimations.size();
+
+ count = mMoves.size();
+ for (int i = count - 1; i >= 0; i--) {
+ MoveInfo moveInfo = mMoves.get(i);
+ ViewHolder item = moveInfo.holder;
+ View view = item.itemView;
+ ViewCompat.setTranslationY(view, 0);
+ ViewCompat.setTranslationX(view, 0);
+ dispatchMoveFinished(moveInfo.holder);
+ mMoves.remove(i);
+ }
+ count = mAdditions.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ViewHolder item = mAdditions.get(i);
+ View view = item.itemView;
+ ViewCompat.setAlpha(view, 1);
+ dispatchAddFinished(item);
+ mAdditions.remove(i);
+ }
+ count = mChanges.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ChangeInfo item = mChanges.get(i);
+ View newView = item.newHolder != null ? item.newHolder.itemView : null;
+ if (newView != null) {
+ ViewCompat.setAlpha(newView, 1);
+ }
+ dispatchChangeFinished(item.oldHolder);
+ mChanges.remove(i);
+ }
+
+ count = mMoveAnimations.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mMoveAnimations.get(i);
View view = item.itemView;
ViewCompat.animate(view).cancel();
- ViewCompat.setTranslationY(view, 0);
- ViewCompat.setTranslationX(view, 0);
- dispatchMoveFinished(item);
- mMoveAnimations.remove(item);
}
count = mRemoveAnimations.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mRemoveAnimations.get(i);
View view = item.itemView;
ViewCompat.animate(view).cancel();
- ViewCompat.setAlpha(view, 1);
- dispatchRemoveFinished(item);
- mRemoveAnimations.remove(item);
}
count = mAddAnimations.size();
for (int i = count - 1; i >= 0; i--) {
ViewHolder item = mAddAnimations.get(i);
View view = item.itemView;
ViewCompat.animate(view).cancel();
- ViewCompat.setAlpha(view, 1);
- dispatchAddFinished(item);
- mAddAnimations.remove(item);
}
- mMoves.clear();
- mAdditions.clear();
+ count = mChangeAnimations.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ChangeInfo item = mChangeAnimations.get(i);
+ View oldView = item.oldHolder.itemView;
+ ViewCompat.animate(oldView).cancel();
+ }
+
dispatchAnimationsFinished();
}
diff --git a/v7/recyclerview/src/android/support/v7/widget/LayoutState.java b/v7/recyclerview/src/android/support/v7/widget/LayoutState.java
new file mode 100644
index 0000000..e62a80a
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/LayoutState.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific languag`e governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+import android.view.View;
+
+/**
+ * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
+ * space.
+ */
+class LayoutState {
+
+ final static String TAG = "LayoutState";
+
+ final static int LAYOUT_START = -1;
+
+ final static int LAYOUT_END = 1;
+
+ final static int INVALID_LAYOUT = Integer.MIN_VALUE;
+
+ final static int ITEM_DIRECTION_HEAD = -1;
+
+ final static int ITEM_DIRECTION_TAIL = 1;
+
+ final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE;
+
+ /**
+ * Number of pixels that we should fill, in the layout direction.
+ */
+ int mAvailable;
+
+ /**
+ * Current position on the adapter to get the next item.
+ */
+ int mCurrentPosition;
+
+ /**
+ * Defines the direction in which the data adapter is traversed.
+ * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
+ */
+ int mItemDirection;
+
+ /**
+ * Defines the direction in which the layout is filled.
+ * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
+ */
+ int mLayoutDirection;
+
+ /**
+ * Used if you want to pre-layout items that are not yet visible.
+ * The difference with {@link #mAvailable} is that, when recycling, distance rendered for
+ * {@link #mExtra} is not considered not to recycle visible children.
+ */
+ int mExtra = 0;
+
+ /**
+ * @return true if there are more items in the data adapter
+ */
+ boolean hasMore(RecyclerView.State state) {
+ return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
+ }
+
+ /**
+ * Gets the view for the next element that we should render.
+ * Also updates current item index to the next item, based on {@link #mItemDirection}
+ *
+ * @return The next element that we should render.
+ */
+ View next(RecyclerView.Recycler recycler) {
+ final View view = recycler.getViewForPosition(mCurrentPosition);
+ mCurrentPosition += mItemDirection;
+ return view;
+ }
+}
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
index 2e3b739..c557aa3 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -24,7 +24,6 @@
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.LinearLayout;
import java.util.List;
@@ -38,9 +37,9 @@
private static final boolean DEBUG = false;
- public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
+ public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
- public static final int VERTICAL = LinearLayout.VERTICAL;
+ public static final int VERTICAL = OrientationHelper.VERTICAL;
public static final int INVALID_OFFSET = Integer.MIN_VALUE;
@@ -60,19 +59,19 @@
private int mOrientation;
/**
- * Helper class that keeps temporary rendering state.
- * It does not keep state after rendering is complete but we still keep a reference to re-use
+ * Helper class that keeps temporary layout state.
+ * It does not keep state after layout is complete but we still keep a reference to re-use
* the same object.
*/
- private RenderState mRenderState;
+ private LayoutState mLayoutState;
/**
* Many calculations are made depending on orientation. To keep it clean, this interface
* helps {@link LinearLayoutManager} make those decisions.
* Based on {@link #mOrientation}, an implementation is lazily created in
- * {@link #ensureRenderState} method.
+ * {@link #ensureLayoutState} method.
*/
- private OrientationHelper mOrientationHelper;
+ OrientationHelper mOrientationHelper;
/**
* We need to track this so that we can ignore current position when it changes.
@@ -113,6 +112,8 @@
*/
private int mPendingScrollPositionOffset = INVALID_OFFSET;
+ private boolean mRecycleChildrenOnDetach;
+
private SavedState mPendingSavedState = null;
/**
@@ -128,7 +129,7 @@
* @param context Current context, will be used to access resources.
* @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
* #VERTICAL}.
- * @param reverseLayout When set to true, renders the layout from end to start.
+ * @param reverseLayout When set to true, layouts from end to start.
*/
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
setOrientation(orientation);
@@ -144,17 +145,58 @@
ViewGroup.LayoutParams.WRAP_CONTENT);
}
+ /**
+ * Returns whether LinearLayoutManager will recycle its children when it is detached from
+ * RecyclerView.
+ *
+ * @return true if LinearLayoutManager will recycle its children when it is detached from
+ * RecyclerView.
+ */
+ public boolean getRecycleChildrenOnDetach() {
+ return mRecycleChildrenOnDetach;
+ }
+
+ /**
+ * Set whether LinearLayoutManager will recycle its children when it is detached from
+ * RecyclerView.
+ * <p>
+ * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set
+ * this flag to <code>true</code> so that views will be avilable to other RecyclerViews
+ * immediately.
+ * <p>
+ * Note that, setting this flag will result in a performance drop if RecyclerView
+ * is restored.
+ *
+ * @param recycleChildrenOnDetach Whether children should be recycled in detach or not.
+ */
+ public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
+ if (mPendingSavedState != null &&
+ mPendingSavedState.mRecycleChildrenOnDetach != recycleChildrenOnDetach) {
+ // override pending state
+ mPendingSavedState.mRecycleChildrenOnDetach = recycleChildrenOnDetach;
+ }
+ mRecycleChildrenOnDetach = recycleChildrenOnDetach;
+ }
+
+ @Override
+ public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
+ super.onDetachedFromWindow(view, recycler);
+ if (mRecycleChildrenOnDetach) {
+ removeAndRecycleAllViews(recycler);
+ recycler.clear();
+ }
+ }
+
@Override
public Parcelable onSaveInstanceState() {
if (mPendingSavedState != null) {
return new SavedState(mPendingSavedState);
}
SavedState state = new SavedState();
+ state.mRecycleChildrenOnDetach = mRecycleChildrenOnDetach;
if (getChildCount() > 0) {
boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout;
- state.mOrientation = mOrientation;
state.mAnchorLayoutFromEnd = didLayoutFromEnd;
-
if (didLayoutFromEnd) {
final View refChild = getChildClosestToEnd();
state.mAnchorOffset = mOrientationHelper.getEndAfterPadding() -
@@ -167,11 +209,11 @@
mOrientationHelper.getStartAfterPadding();
}
} else {
- state.mAnchorPosition = 0;
- state.mAnchorOffset = 0;
+ state.invalidateAnchor();
}
state.mStackFromEnd = mStackFromEnd;
state.mReverseLayout = mReverseLayout;
+ state.mOrientation = mOrientation;
return state;
}
@@ -208,6 +250,14 @@
* Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)}
*/
public void setStackFromEnd(boolean stackFromEnd) {
+ assertNotInLayoutOrScroll(null);
+ if (mPendingSavedState != null && mPendingSavedState.mStackFromEnd != stackFromEnd) {
+ // override pending state
+ mPendingSavedState.mStackFromEnd = stackFromEnd;
+ }
+ if (mStackFromEnd == stackFromEnd) {
+ return;
+ }
mStackFromEnd = stackFromEnd;
requestLayout();
}
@@ -235,7 +285,12 @@
*/
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
- throw new IllegalArgumentException("invalid orientation.");
+ throw new IllegalArgumentException("invalid orientation:" + orientation);
+ }
+ assertNotInLayoutOrScroll(null);
+ if (mPendingSavedState != null && mPendingSavedState.mOrientation != orientation) {
+ // override pending state
+ mPendingSavedState.mOrientation = orientation;
}
if (orientation == mOrientation) {
return;
@@ -272,11 +327,11 @@
/**
* Used to reverse item traversal and layout order.
* This behaves similar to the layout change for RTL views. When set to true, first item is
- * rendered at the end of the UI, second item is render before it etc.
+ * laid out at the end of the UI, second item is laid out before it etc.
*
* For horizontal layouts, it depends on the layout direction.
* When set to true, If {@link android.support.v7.widget.RecyclerView} is LTR, than it will
- * render from RTL, if {@link android.support.v7.widget.RecyclerView}} is RTL, it will render
+ * layout from RTL, if {@link android.support.v7.widget.RecyclerView}} is RTL, it will layout
* from LTR.
*
* If you are looking for the exact same behavior of
@@ -284,6 +339,11 @@
* {@link #setStackFromEnd(boolean)}
*/
public void setReverseLayout(boolean reverseLayout) {
+ assertNotInLayoutOrScroll(null);
+ if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) {
+ // override pending state
+ mPendingSavedState.mReverseLayout = reverseLayout;
+ }
if (reverseLayout == mReverseLayout) {
return;
}
@@ -309,14 +369,14 @@
}
/**
- * <p>Returns the amount of extra space that should be rendered by LinearLayoutManager.
+ * <p>Returns the amount of extra space that should be laid out by LinearLayoutManager.
* By default, {@link android.support.v7.widget.LinearLayoutManager} lays out 1 extra page of
* items while smooth scrolling and 0 otherwise. You can override this method to implement your
* custom layout pre-cache logic.</p>
* <p>Laying out invisible elements will eventually come with performance cost. On the other
* hand, in places like smooth scrolling to an unknown location, this extra content helps
* LayoutManager to calculate a much smoother scrolling; which improves user experience.</p>
- * <p>You can also use this if you are trying to pre-render your upcoming views.</p>
+ * <p>You can also use this if you are trying to pre-layout your upcoming views.</p>
*
* @return The extra space that should be laid out (in pixels).
*/
@@ -367,7 +427,7 @@
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
- // create render state
+ // create layout state
if (DEBUG) {
Log.d(TAG, "is pre layout:" + state.isPreLayout());
}
@@ -375,10 +435,13 @@
setOrientation(mPendingSavedState.mOrientation);
setReverseLayout(mPendingSavedState.mReverseLayout);
setStackFromEnd(mPendingSavedState.mStackFromEnd);
- mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
+ setRecycleChildrenOnDetach(mPendingSavedState.mRecycleChildrenOnDetach);
+ if (mPendingSavedState.hasValidAnchor()) {
+ mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
+ }
}
- ensureRenderState();
+ ensureLayoutState();
// resolve layout direction
resolveShouldLayoutReverse();
@@ -403,7 +466,7 @@
// if child is visible, try to make it a reference child and ensure it is fully visible.
// if child is not visible, align it depending on its virtual position.
anchorItemPosition = mPendingScrollPosition;
- if (mPendingSavedState != null) {
+ if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
// Anchor offset depends on how that child was laid out. Here, we update it
// according to our current view bounds
layoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
@@ -467,13 +530,25 @@
}
} else if (getChildCount() > 0 && !stackFromEndChanged) {
if (layoutFromEnd) {
- View referenceChild = getChildClosestToEnd();
- anchorCoordinate = mOrientationHelper.getDecoratedEnd(referenceChild);
- anchorItemPosition = getPosition(referenceChild);
+ View referenceChild = findReferenceChildClosestToEnd(state);
+ if (referenceChild != null) {
+ anchorCoordinate = mOrientationHelper.getDecoratedEnd(referenceChild);
+ anchorItemPosition = getPosition(referenceChild);
+ } else {
+ anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() :
+ mOrientationHelper.getStartAfterPadding();
+ anchorItemPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
+ }
} else {
- View referenceChild = getChildClosestToStart();
- anchorCoordinate = mOrientationHelper.getDecoratedStart(referenceChild);
- anchorItemPosition = getPosition(referenceChild);
+ View referenceChild = findReferenceChildClosestToStart(state);
+ if (referenceChild != null) {
+ anchorCoordinate = mOrientationHelper.getDecoratedStart(referenceChild);
+ anchorItemPosition = getPosition(referenceChild);
+ } else {
+ anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() :
+ mOrientationHelper.getStartAfterPadding();
+ anchorItemPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
+ }
}
} else {
anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() :
@@ -482,8 +557,8 @@
}
detachAndScrapAttachedViews(recycler);
- final int extraForStart;
- final int extraForEnd;
+ int extraForStart;
+ int extraForEnd;
final int extra = getExtraLayoutSpace(state);
boolean before = state.getTargetScrollPosition() < anchorItemPosition;
if (before == mShouldReverseLayout) {
@@ -493,22 +568,36 @@
extraForStart = extra;
extraForEnd = 0;
}
- // first fill towards start
- updateRenderStateToFillStart(anchorItemPosition, anchorCoordinate);
- mRenderState.mExtra = extraForStart;
- if (!layoutFromEnd) {
- mRenderState.mCurrentPosition += mRenderState.mItemDirection;
- }
- fill(recycler, mRenderState, state, false);
- int startOffset = mRenderState.mOffset;
- // fill towards end
- updateRenderStateToFillEnd(anchorItemPosition, anchorCoordinate);
- mRenderState.mExtra = extraForEnd;
+ int startOffset;
+ int endOffset;
if (layoutFromEnd) {
- mRenderState.mCurrentPosition += mRenderState.mItemDirection;
+ // fill towards start
+ updateLayoutStateToFillStart(anchorItemPosition, anchorCoordinate);
+ mLayoutState.mExtra = extraForStart;
+ fill(recycler, mLayoutState, state, false);
+ startOffset = mLayoutState.mOffset;
+ extraForEnd += mLayoutState.mAvailable;
+ // fill towards end
+ updateLayoutStateToFillEnd(anchorItemPosition, anchorCoordinate);
+ mLayoutState.mExtra = extraForEnd;
+ mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+ fill(recycler, mLayoutState, state, false);
+ endOffset = mLayoutState.mOffset;
+ } else {
+ // fill towards end
+ updateLayoutStateToFillEnd(anchorItemPosition, anchorCoordinate);
+ mLayoutState.mExtra = extraForEnd;
+ fill(recycler, mLayoutState, state, false);
+ endOffset = mLayoutState.mOffset;
+ extraForStart += mLayoutState.mAvailable;
+ // fill towards start
+ updateLayoutStateToFillStart(anchorItemPosition, anchorCoordinate);
+ mLayoutState.mExtra = extraForStart;
+ mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+ fill(recycler, mLayoutState, state, false);
+ startOffset = mLayoutState.mOffset;
}
- fill(recycler, mRenderState, state, false);
- int endOffset = mRenderState.mOffset;
+
// changes may cause gaps on the UI, try to fix them.
if (getChildCount() > 0) {
// because layout from end may be changed by scroll to position
@@ -535,7 +624,7 @@
// and layout them accordingly so that animations can work as expected.
// This case may happen if new views are added or an existing view expands and pushes
// another view out of bounds.
- if (getChildCount() > 0 && !state.isPreLayout() && supportsItemAnimations()) {
+ if (state.willRunPredictiveAnimations() && getChildCount() > 0 && !state.isPreLayout()) {
// to make the logic simpler, we calculate the size of children and call fill.
int scrapExtraStart = 0, scrapExtraEnd = 0;
final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
@@ -545,8 +634,8 @@
RecyclerView.ViewHolder scrap = scrapList.get(i);
final int position = scrap.getPosition();
final int direction = position < firstChildPos != mShouldReverseLayout
- ? RenderState.LAYOUT_START : RenderState.LAYOUT_END;
- if (direction == RenderState.LAYOUT_START) {
+ ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
+ if (direction == LayoutState.LAYOUT_START) {
scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
} else {
scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
@@ -557,33 +646,31 @@
Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart
+ " towards start and " + scrapExtraEnd + " towards end");
}
- mRenderState.mScrapList = scrapList;
+ mLayoutState.mScrapList = scrapList;
if (scrapExtraStart > 0) {
View anchor = getChildClosestToStart();
- updateRenderStateToFillStart(getPosition(anchor), startOffset);
- mRenderState.mExtra = scrapExtraStart;
- mRenderState.mAvailable = 0;
- mRenderState.mCurrentPosition += mShouldReverseLayout ? 1 : -1;
- fill(recycler, mRenderState, state, false);
+ updateLayoutStateToFillStart(getPosition(anchor), startOffset);
+ mLayoutState.mExtra = scrapExtraStart;
+ mLayoutState.mAvailable = 0;
+ mLayoutState.mCurrentPosition += mShouldReverseLayout ? 1 : -1;
+ fill(recycler, mLayoutState, state, false);
}
if (scrapExtraEnd > 0) {
View anchor = getChildClosestToEnd();
- updateRenderStateToFillEnd(getPosition(anchor),
- endOffset);
- mRenderState.mExtra = scrapExtraEnd;
- mRenderState.mAvailable = 0;
- mRenderState.mCurrentPosition += mShouldReverseLayout ? -1 : 1;
- fill(recycler, mRenderState, state, false);
+ updateLayoutStateToFillEnd(getPosition(anchor), endOffset);
+ mLayoutState.mExtra = scrapExtraEnd;
+ mLayoutState.mAvailable = 0;
+ mLayoutState.mCurrentPosition += mShouldReverseLayout ? -1 : 1;
+ fill(recycler, mLayoutState, state, false);
}
- mRenderState.mScrapList = null;
+ mLayoutState.mScrapList = null;
}
mPendingScrollPosition = RecyclerView.NO_POSITION;
mPendingScrollPositionOffset = INVALID_OFFSET;
mLastStackFromEnd = mStackFromEnd;
mPendingSavedState = null; // we don't need this anymore
-
if (DEBUG) {
validateChildOrder();
}
@@ -639,24 +726,24 @@
return fixOffset;
}
- private void updateRenderStateToFillEnd(int itemPosition, int offset) {
- mRenderState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
- mRenderState.mItemDirection = mShouldReverseLayout ? RenderState.ITEM_DIRECTION_HEAD :
- RenderState.ITEM_DIRECTION_TAIL;
- mRenderState.mCurrentPosition = itemPosition;
- mRenderState.mLayoutDirection = RenderState.LAYOUT_END;
- mRenderState.mOffset = offset;
- mRenderState.mScrollingOffset = RenderState.SCOLLING_OFFSET_NaN;
+ private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
+ mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
+ mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
+ LayoutState.ITEM_DIRECTION_TAIL;
+ mLayoutState.mCurrentPosition = itemPosition;
+ mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
+ mLayoutState.mOffset = offset;
+ mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN;
}
- private void updateRenderStateToFillStart(int itemPosition, int offset) {
- mRenderState.mAvailable = offset - mOrientationHelper.getStartAfterPadding();
- mRenderState.mCurrentPosition = itemPosition;
- mRenderState.mItemDirection = mShouldReverseLayout ? RenderState.ITEM_DIRECTION_TAIL :
- RenderState.ITEM_DIRECTION_HEAD;
- mRenderState.mLayoutDirection = RenderState.LAYOUT_START;
- mRenderState.mOffset = offset;
- mRenderState.mScrollingOffset = RenderState.SCOLLING_OFFSET_NaN;
+ private void updateLayoutStateToFillStart(int itemPosition, int offset) {
+ mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding();
+ mLayoutState.mCurrentPosition = itemPosition;
+ mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :
+ LayoutState.ITEM_DIRECTION_HEAD;
+ mLayoutState.mLayoutDirection = LayoutState.LAYOUT_START;
+ mLayoutState.mOffset = offset;
+ mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN;
}
@@ -665,13 +752,12 @@
return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
}
- private void ensureRenderState() {
- if (mRenderState == null) {
- mRenderState = new RenderState();
+ void ensureLayoutState() {
+ if (mLayoutState == null) {
+ mLayoutState = new LayoutState();
}
if (mOrientationHelper == null) {
- mOrientationHelper = mOrientation == HORIZONTAL ? createHorizontalOrientationHelper()
- : createVerticalOrientationHelper();
+ mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
}
}
@@ -693,6 +779,9 @@
public void scrollToPosition(int position) {
mPendingScrollPosition = position;
mPendingScrollPositionOffset = INVALID_OFFSET;
+ if (mPendingSavedState != null) {
+ mPendingSavedState.invalidateAnchor();
+ }
requestLayout();
}
@@ -713,6 +802,9 @@
public void scrollToPositionWithOffset(int position, int offset) {
mPendingScrollPosition = position;
mPendingScrollPositionOffset = offset;
+ if (mPendingSavedState != null) {
+ mPendingSavedState.invalidateAnchor();
+ }
requestLayout();
}
@@ -779,49 +871,49 @@
return state.getItemCount();
}
- private void updateRenderState(int layoutDirection, int requiredSpace,
+ private void updateLayoutState(int layoutDirection, int requiredSpace,
boolean canUseExistingSpace, RecyclerView.State state) {
- mRenderState.mExtra = getExtraLayoutSpace(state);
- mRenderState.mLayoutDirection = layoutDirection;
+ mLayoutState.mExtra = getExtraLayoutSpace(state);
+ mLayoutState.mLayoutDirection = layoutDirection;
int fastScrollSpace;
- if (layoutDirection == RenderState.LAYOUT_END) {
+ if (layoutDirection == LayoutState.LAYOUT_END) {
// get the first child in the direction we are going
final View child = getChildClosestToEnd();
// the direction in which we are traversing children
- mRenderState.mItemDirection = mShouldReverseLayout ? RenderState.ITEM_DIRECTION_HEAD
- : RenderState.ITEM_DIRECTION_TAIL;
- mRenderState.mCurrentPosition = getPosition(child) + mRenderState.mItemDirection;
- mRenderState.mOffset = mOrientationHelper.getDecoratedEnd(child);
+ mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
+ : LayoutState.ITEM_DIRECTION_TAIL;
+ mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
+ mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
// calculate how much we can scroll without adding new children (independent of layout)
fastScrollSpace = mOrientationHelper.getDecoratedEnd(child)
- mOrientationHelper.getEndAfterPadding();
} else {
final View child = getChildClosestToStart();
- mRenderState.mItemDirection = mShouldReverseLayout ? RenderState.ITEM_DIRECTION_TAIL
- : RenderState.ITEM_DIRECTION_HEAD;
- mRenderState.mCurrentPosition = getPosition(child) + mRenderState.mItemDirection;
- mRenderState.mOffset = mOrientationHelper.getDecoratedStart(child);
+ mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
+ : LayoutState.ITEM_DIRECTION_HEAD;
+ mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
+ mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
fastScrollSpace = -mOrientationHelper.getDecoratedStart(child)
+ mOrientationHelper.getStartAfterPadding();
}
- mRenderState.mAvailable = requiredSpace;
+ mLayoutState.mAvailable = requiredSpace;
if (canUseExistingSpace) {
- mRenderState.mAvailable -= fastScrollSpace;
+ mLayoutState.mAvailable -= fastScrollSpace;
}
- mRenderState.mScrollingOffset = fastScrollSpace;
+ mLayoutState.mScrollingOffset = fastScrollSpace;
}
private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
- ensureRenderState();
- final int layoutDirection = dy > 0 ? RenderState.LAYOUT_END : RenderState.LAYOUT_START;
+ ensureLayoutState();
+ final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
final int absDy = Math.abs(dy);
- updateRenderState(layoutDirection, absDy, true, state);
- final int freeScroll = mRenderState.mScrollingOffset;
- final int consumed = freeScroll + fill(recycler, mRenderState, state, false);
+ updateLayoutState(layoutDirection, absDy, true, state);
+ final int freeScroll = mLayoutState.mScrollingOffset;
+ final int consumed = freeScroll + fill(recycler, mLayoutState, state, false);
if (consumed < 0) {
if (DEBUG) {
Log.d(TAG, "Don't have any more elements to scroll");
@@ -836,6 +928,13 @@
return scrolled;
}
+ @Override
+ public void assertNotInLayoutOrScroll(String message) {
+ if (mPendingSavedState == null) {
+ super.assertNotInLayoutOrScroll(message);
+ }
+ }
+
/**
* Recycles children between given indices.
*
@@ -877,7 +976,8 @@
}
return;
}
- final int limit = mOrientationHelper.getStartAfterPadding() + dt;
+ // ignore padding, ViewGroup may not clip children.
+ final int limit = dt;
final int childCount = getChildCount();
if (mShouldReverseLayout) {
for (int i = childCount - 1; i >= 0; i--) {
@@ -916,7 +1016,7 @@
}
return;
}
- final int limit = mOrientationHelper.getEndAfterPadding() - dt;
+ final int limit = mOrientationHelper.getEnd() - dt;
if (mShouldReverseLayout) {
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
@@ -938,64 +1038,72 @@
}
/**
- * Helper method to call appropriate recycle method depending on current render layout
- * direction
+ * Helper method to call appropriate recycle method depending on current layout direction
*
* @param recycler Current recycler that is attached to RecyclerView
- * @param renderState Current render state. Right now, this object does not change but
+ * @param layoutState Current layout state. Right now, this object does not change but
* we may consider moving it out of this view so passing around as a
- * parameter for now, rather than accessing {@link #mRenderState}
+ * parameter for now, rather than accessing {@link #mLayoutState}
* @see #recycleViewsFromStart(android.support.v7.widget.RecyclerView.Recycler, int)
* @see #recycleViewsFromEnd(android.support.v7.widget.RecyclerView.Recycler, int)
- * @see android.support.v7.widget.LinearLayoutManager.RenderState#mLayoutDirection
+ * @see android.support.v7.widget.LinearLayoutManager.LayoutState#mLayoutDirection
*/
- private void recycleByRenderState(RecyclerView.Recycler recycler, RenderState renderState) {
- if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
- recycleViewsFromEnd(recycler, renderState.mScrollingOffset);
+ private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
+ if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+ recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
} else {
- recycleViewsFromStart(recycler, renderState.mScrollingOffset);
+ recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
}
}
/**
- * The magic functions :). Fills the given layout, defined by the renderState. This is fairly
+ * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
* independent from the rest of the {@link android.support.v7.widget.LinearLayoutManager}
* and with little change, can be made publicly available as a helper class.
*
* @param recycler Current recycler that is attached to RecyclerView
- * @param renderState Configuration on how we should fill out the available space.
+ * @param layoutState Configuration on how we should fill out the available space.
* @param state Context passed by the RecyclerView to control scroll steps.
* @param stopOnFocusable If true, filling stops in the first focusable new child
* @return Number of pixels that it added. Useful for scoll functions.
*/
- private int fill(RecyclerView.Recycler recycler, RenderState renderState,
- RecyclerView.State state,
- boolean stopOnFocusable) {
+ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
+ RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
- final int start = renderState.mAvailable;
- if (renderState.mScrollingOffset != RenderState.SCOLLING_OFFSET_NaN) {
+ final int start = layoutState.mAvailable;
+ if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
- if (renderState.mAvailable < 0) {
- renderState.mScrollingOffset += renderState.mAvailable;
+ if (layoutState.mAvailable < 0) {
+ layoutState.mScrollingOffset += layoutState.mAvailable;
}
- recycleByRenderState(recycler, renderState);
+ recycleByLayoutState(recycler, layoutState);
}
- int remainingSpace = renderState.mAvailable + renderState.mExtra;
- while (remainingSpace > 0 && renderState.hasMore(state)) {
- View view = renderState.next(recycler);
+ int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
+ while (remainingSpace > 0 && layoutState.hasMore(state)) {
+ View view = layoutState.next(recycler);
if (view == null) {
+ if (DEBUG && layoutState.mScrapList == null) {
+ throw new RuntimeException("received null view when unexpected");
+ }
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
break;
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
- if (!params.isItemRemoved() && mRenderState.mScrapList == null) {
- if (mShouldReverseLayout == (renderState.mLayoutDirection
- == RenderState.LAYOUT_START)) {
+ if (mLayoutState.mScrapList == null) {
+ if (mShouldReverseLayout == (layoutState.mLayoutDirection
+ == LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
+ } else {
+ if (mShouldReverseLayout == (layoutState.mLayoutDirection
+ == LayoutState.LAYOUT_START)) {
+ addDisappearingView(view);
+ } else {
+ addDisappearingView(view, 0);
+ }
}
measureChildWithMargins(view, 0, 0);
int consumed = mOrientationHelper.getDecoratedMeasurement(view);
@@ -1008,23 +1116,23 @@
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
- if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
- bottom = renderState.mOffset;
- top = renderState.mOffset - consumed;
+ if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+ bottom = layoutState.mOffset;
+ top = layoutState.mOffset - consumed;
} else {
- top = renderState.mOffset;
- bottom = renderState.mOffset + consumed;
+ top = layoutState.mOffset;
+ bottom = layoutState.mOffset + consumed;
}
} else {
top = getPaddingTop();
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
- if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
- right = renderState.mOffset;
- left = renderState.mOffset - consumed;
+ if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
+ right = layoutState.mOffset;
+ left = layoutState.mOffset - consumed;
} else {
- left = renderState.mOffset;
- right = renderState.mOffset + consumed;
+ left = layoutState.mOffset;
+ right = layoutState.mOffset + consumed;
}
}
// We calculate everything with View's bounding box (which includes decor and margins)
@@ -1036,33 +1144,43 @@
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
}
- renderState.mOffset += consumed * renderState.mLayoutDirection;
+ layoutState.mOffset += consumed * layoutState.mLayoutDirection;
- if (!params.isItemRemoved()) {
- renderState.mAvailable -= consumed;
+ /**
+ * Consume the available space if:
+ * * view is not removed
+ * * OR we are laying out scrap children
+ * * OR we are not doing pre-layout
+ */
+ if (!params.isItemRemoved() || mLayoutState.mScrapList != null ||
+ !state.isPreLayout()) {
+ layoutState.mAvailable -= consumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= consumed;
}
- if (renderState.mScrollingOffset != RenderState.SCOLLING_OFFSET_NaN) {
- renderState.mScrollingOffset += consumed;
- if (renderState.mAvailable < 0) {
- renderState.mScrollingOffset += renderState.mAvailable;
+ if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
+ layoutState.mScrollingOffset += consumed;
+ if (layoutState.mAvailable < 0) {
+ layoutState.mScrollingOffset += layoutState.mAvailable;
}
- recycleByRenderState(recycler, renderState);
+ recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && view.isFocusable()) {
break;
}
- if (state != null && state.getTargetScrollPosition() == getPosition(view)) {
+ // some deleted views may have position -1. Make sure state has a real target scroll
+ // position
+ if (state.getTargetScrollPosition() != RecyclerView.NO_POSITION &&
+ state.getTargetScrollPosition() == getPosition(view)) {
break;
}
}
if (DEBUG) {
validateChildOrder();
}
- return start - renderState.mAvailable;
+ return start - layoutState.mAvailable;
}
/**
@@ -1072,32 +1190,32 @@
* {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
* {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
* or 0 for not applicable
- * @return {@link RenderState#LAYOUT_START} or {@link RenderState#LAYOUT_END} if focus direction
- * is applicable to current state, {@link RenderState#INVALID_LAYOUT} otherwise.
+ * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction
+ * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise.
*/
private int convertFocusDirectionToLayoutDirection(int focusDirection) {
switch (focusDirection) {
case View.FOCUS_BACKWARD:
- return RenderState.LAYOUT_START;
+ return LayoutState.LAYOUT_START;
case View.FOCUS_FORWARD:
- return RenderState.LAYOUT_END;
+ return LayoutState.LAYOUT_END;
case View.FOCUS_UP:
- return mOrientation == VERTICAL ? RenderState.LAYOUT_START
- : RenderState.INVALID_LAYOUT;
+ return mOrientation == VERTICAL ? LayoutState.LAYOUT_START
+ : LayoutState.INVALID_LAYOUT;
case View.FOCUS_DOWN:
- return mOrientation == VERTICAL ? RenderState.LAYOUT_END
- : RenderState.INVALID_LAYOUT;
+ return mOrientation == VERTICAL ? LayoutState.LAYOUT_END
+ : LayoutState.INVALID_LAYOUT;
case View.FOCUS_LEFT:
- return mOrientation == HORIZONTAL ? RenderState.LAYOUT_START
- : RenderState.INVALID_LAYOUT;
+ return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START
+ : LayoutState.INVALID_LAYOUT;
case View.FOCUS_RIGHT:
- return mOrientation == HORIZONTAL ? RenderState.LAYOUT_END
- : RenderState.INVALID_LAYOUT;
+ return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END
+ : LayoutState.INVALID_LAYOUT;
default:
if (DEBUG) {
Log.d(TAG, "Unknown focus request:" + focusDirection);
}
- return RenderState.INVALID_LAYOUT;
+ return LayoutState.INVALID_LAYOUT;
}
}
@@ -1122,33 +1240,185 @@
return getChildAt(mShouldReverseLayout ? 0 : getChildCount() - 1);
}
+
+ /**
+ * Among the children that are suitable to be considered as an anchor child, returns the one
+ * closest to the end of the layout.
+ * <p>
+ * Due to ambiguous adapter updates or children being removed, some children's positions may be
+ * invalid. This method is a best effort to find a position within adapter bounds if possible.
+ *
+ * @return A View that can be used an an anchor View.
+ */
+ private View findReferenceChildClosestToEnd(RecyclerView.State state) {
+ return mShouldReverseLayout ? findFirstReferenceChild(state.getItemCount()) :
+ findLastReferenceChild(state.getItemCount());
+ }
+
+ /**
+ * Among the children that are suitable to be considered as an anchor child, returns the one
+ * closest to the start of the layout.
+ * <p>
+ * Due to ambiguous adapter updates or children being removed, some children's positions may be
+ * invalid. This method is a best effort to find a position within adapter bounds if possible.
+ *
+ * @return A View that can be used an an anchor View.
+ */
+ private View findReferenceChildClosestToStart(RecyclerView.State state) {
+ return mShouldReverseLayout ? findLastReferenceChild(state.getItemCount()) :
+ findFirstReferenceChild(state.getItemCount());
+ }
+
+
+ private View findFirstReferenceChild(int itemCount) {
+ final int limit = getChildCount();
+ for (int i = 0; i < limit; i++) {
+ final View view = getChildAt(i);
+ final int position = getPosition(view);
+ if (position >= 0 && position < itemCount) {
+ return view;
+ }
+ }
+ return null;
+ }
+
+ private View findLastReferenceChild(int itemCount) {
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ final View view = getChildAt(i);
+ final int position = getPosition(view);
+ if (position >= 0 && position < itemCount) {
+ return view;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the adapter position of the first visible view.
+ * <p>
+ * Note that, this value is not affected by layout orientation or item order traversal.
+ * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
+ * not in the layout.
+ * <p>
+ * If RecyclerView has item decorators, they will be considered in calculations as well.
+ * <p>
+ * LinearLayoutManager may pre-cache some views that are not necessarily visible. Those views
+ * are ignored in this method.
+ *
+ * @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if
+ * there aren't any visible items.
+ * @see #findFirstCompletelyVisibleItemPosition()
+ * @see #findLastVisibleItemPosition()
+ */
+ public int findFirstVisibleItemPosition() {
+ return findOneVisibleChild(0, getChildCount(), false);
+ }
+
+ /**
+ * Returns the adapter position of the first fully visible view.
+ * <p>
+ * Note that bounds check is only performed in the current orientation. That means, if
+ * LinearLayoutManager is horizontal, it will only check the view's left and right edges.
+ *
+ * @return The adapter position of the first fully visible item or
+ * {@link RecyclerView#NO_POSITION} if there aren't any visible items.
+ * @see #findFirstVisibleItemPosition()
+ * @see #findLastCompletelyVisibleItemPosition()
+ */
+ public int findFirstCompletelyVisibleItemPosition() {
+ return findOneVisibleChild(0, getChildCount(), true);
+ }
+
+ /**
+ * Returns the adapter position of the last visible view.
+ * <p>
+ * Note that, this value is not affected by layout orientation or item order traversal.
+ * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
+ * not in the layout.
+ * <p>
+ * If RecyclerView has item decorators, they will be considered in calculations as well.
+ * <p>
+ * LinearLayoutManager may pre-cache some views that are not necessarily visible. Those views
+ * are ignored in this method.
+ *
+ * @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if
+ * there aren't any visible items.
+ * @see #findLastCompletelyVisibleItemPosition()
+ * @see #findFirstVisibleItemPosition()
+ */
+ public int findLastVisibleItemPosition() {
+ return findOneVisibleChild(getChildCount() - 1, -1, false);
+ }
+
+ /**
+ * Returns the adapter position of the last fully visible view.
+ * <p>
+ * Note that bounds check is only performed in the current orientation. That means, if
+ * LinearLayoutManager is horizontal, it will only check the view's left and right edges.
+ *
+ * @return The adapter position of the last fully visible view or
+ * {@link RecyclerView#NO_POSITION} if there aren't any visible items.
+ * @see #findLastVisibleItemPosition()
+ * @see #findFirstCompletelyVisibleItemPosition()
+ */
+ public int findLastCompletelyVisibleItemPosition() {
+ return findOneVisibleChild(getChildCount() - 1, -1, true);
+ }
+
+ int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) {
+ final int start = mOrientationHelper.getStartAfterPadding();
+ final int end = mOrientationHelper.getEndAfterPadding();
+ final int next = toIndex > fromIndex ? 1 : -1;
+ for (int i = fromIndex; i != toIndex; i+=next) {
+ final View child = getChildAt(i);
+ final int childStart = mOrientationHelper.getDecoratedStart(child);
+ final int childEnd = mOrientationHelper.getDecoratedEnd(child);
+ if (childStart < end && childEnd > start) {
+ if (completelyVisible) {
+ if (childStart >= start && childEnd <= end) {
+ return getPosition(child);
+ }
+ } else {
+ return getPosition(child);
+ }
+ }
+ }
+ return RecyclerView.NO_POSITION;
+ }
+
@Override
public View onFocusSearchFailed(View focused, int focusDirection,
- RecyclerView.Recycler recycler,
- RecyclerView.State state) {
+ RecyclerView.Recycler recycler, RecyclerView.State state) {
resolveShouldLayoutReverse();
if (getChildCount() == 0) {
return null;
}
final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection);
- if (layoutDir == RenderState.INVALID_LAYOUT) {
+ if (layoutDir == LayoutState.INVALID_LAYOUT) {
return null;
}
final View referenceChild;
- if (layoutDir == RenderState.LAYOUT_START) {
- referenceChild = getChildClosestToStart();
+ if (layoutDir == LayoutState.LAYOUT_START) {
+ referenceChild = findReferenceChildClosestToStart(state);
} else {
- referenceChild = getChildClosestToEnd();
+ referenceChild = findReferenceChildClosestToEnd(state);
}
- ensureRenderState();
+ if (referenceChild == null) {
+ if (DEBUG) {
+ Log.d(TAG,
+ "Cannot find a child with a valid position to be used for focus search.");
+ }
+ return null;
+ }
+ ensureLayoutState();
final int maxScroll = (int) (MAX_SCROLL_FACTOR * (mOrientationHelper.getEndAfterPadding() -
mOrientationHelper.getStartAfterPadding()));
- updateRenderState(layoutDir, maxScroll, false, state);
- mRenderState.mScrollingOffset = RenderState.SCOLLING_OFFSET_NaN;
- fill(recycler, mRenderState, state, true);
+ updateLayoutState(layoutDir, maxScroll, false, state);
+ mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN;
+ fill(recycler, mLayoutState, state, true);
final View nextFocus;
- if (layoutDir == RenderState.LAYOUT_START) {
+ if (layoutDir == LayoutState.LAYOUT_START) {
nextFocus = getChildClosestToStart();
} else {
nextFocus = getChildClosestToEnd();
@@ -1224,7 +1494,7 @@
}
@Override
- public boolean supportsItemAnimations() {
+ public boolean supportsPredictiveItemAnimations() {
return true;
}
@@ -1232,9 +1502,9 @@
* Helper class that keeps temporary state while {LayoutManager} is filling out the empty
* space.
*/
- private static class RenderState {
+ private static class LayoutState {
- final static String TAG = "LinearLayoutManager#RenderState";
+ final static String TAG = "LinearLayoutManager#LayoutState";
final static int LAYOUT_START = -1;
@@ -1249,7 +1519,7 @@
final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE;
/**
- * Pixel offset where rendering should start
+ * Pixel offset where layout should start
*/
int mOffset;
@@ -1276,7 +1546,7 @@
int mLayoutDirection;
/**
- * Used when RenderState is constructed in a scrolling state.
+ * Used when LayoutState is constructed in a scrolling state.
* It should be set the amount of scrolling we can make without creating a new view.
* Settings this is required for efficient view recycling.
*/
@@ -1284,13 +1554,13 @@
/**
* Used if you want to pre-layout items that are not yet visible.
- * The difference with {@link #mAvailable} is that, when recycling, distance rendered for
+ * The difference with {@link #mAvailable} is that, when recycling, distance laid out for
* {@link #mExtra} is not considered to avoid recycling visible children.
*/
int mExtra = 0;
/**
- * When LLM needs to layout particular views, it sets this list in which case, RenderState
+ * When LLM needs to layout particular views, it sets this list in which case, LayoutState
* will only return views from this list and return null if it cannot find an item.
*/
List<RecyclerView.ViewHolder> mScrapList = null;
@@ -1303,10 +1573,10 @@
}
/**
- * Gets the view for the next element that we should render.
+ * Gets the view for the next element that we should layout.
* Also updates current item index to the next item, based on {@link #mItemDirection}
*
- * @return The next element that we should render.
+ * @return The next element that we should layout.
*/
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
@@ -1358,167 +1628,6 @@
}
}
- private OrientationHelper createVerticalOrientationHelper() {
- return new OrientationHelper() {
- @Override
- public int getEndAfterPadding() {
- return getHeight() - getPaddingBottom();
- }
-
- @Override
- public void offsetChildren(int amount) {
- offsetChildrenVertical(amount);
- }
-
- @Override
- public int getStartAfterPadding() {
- return getPaddingTop();
- }
-
- @Override
- public int getDecoratedMeasurement(View view) {
- final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
- view.getLayoutParams();
- return getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
- }
-
- @Override
- public int getDecoratedMeasurementInOther(View view) {
- final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
- view.getLayoutParams();
- return getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin;
- }
-
- @Override
- public int getDecoratedEnd(View view) {
- final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
- view.getLayoutParams();
- return getDecoratedBottom(view) + params.bottomMargin;
- }
-
- @Override
- public int getDecoratedStart(View view) {
- final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
- view.getLayoutParams();
- return getDecoratedTop(view) - params.topMargin;
- }
-
- @Override
- public int getTotalSpace() {
- return getHeight() - getPaddingTop() - getPaddingBottom();
- }
- };
- }
-
- private OrientationHelper createHorizontalOrientationHelper() {
- return new OrientationHelper() {
- @Override
- public int getEndAfterPadding() {
- return getWidth() - getPaddingRight();
- }
-
- @Override
- public void offsetChildren(int amount) {
- offsetChildrenHorizontal(amount);
- }
-
- @Override
- public int getStartAfterPadding() {
- return getPaddingLeft();
- }
-
- @Override
- public int getDecoratedMeasurement(View view) {
- final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
- view.getLayoutParams();
- return getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin;
- }
-
- @Override
- public int getDecoratedMeasurementInOther(View view) {
- final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
- view.getLayoutParams();
- return getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin;
- }
-
- @Override
- public int getDecoratedEnd(View view) {
- final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
- view.getLayoutParams();
- return getDecoratedRight(view) + params.rightMargin;
- }
-
- @Override
- public int getDecoratedStart(View view) {
- final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
- view.getLayoutParams();
- return getDecoratedLeft(view) - params.leftMargin;
- }
-
- @Override
- public int getTotalSpace() {
- return getWidth() - getPaddingLeft() - getPaddingRight();
- }
- };
- }
-
-
- /**
- * Helper interface to offload orientation based decisions
- */
- private static interface OrientationHelper {
-
- /**
- * @param view The view element to check
- * @return The first pixel of the element
- * @see #getDecoratedEnd(android.view.View)
- */
- int getDecoratedStart(View view);
-
- /**
- * @param view The view element to check
- * @return The last pixel of the element
- * @see #getDecoratedStart(android.view.View)
- */
- int getDecoratedEnd(View view);
-
- /**
- * @param view The view element to check
- * @return Total space occupied by this view
- */
- int getDecoratedMeasurement(View view);
-
- /**
- * @param view The view element to check
- * @return Total space occupied by this view in the perpendicular orientation to current one
- */
- int getDecoratedMeasurementInOther(View view);
-
- /**
- * @return The very first pixel we can draw.
- */
- int getStartAfterPadding();
-
- /**
- * @return The last pixel we can draw
- */
- int getEndAfterPadding();
-
- /**
- * Offsets all children's positions by the given amount
- *
- * @param amount Value to add to each child's layout parameters
- */
- void offsetChildren(int amount);
-
- /**
- * Returns the total space to layout.
- *
- * @return Total space to layout children
- */
- int getTotalSpace();
- }
-
static class SavedState implements Parcelable {
int mOrientation;
@@ -1533,6 +1642,8 @@
boolean mAnchorLayoutFromEnd;
+ boolean mRecycleChildrenOnDetach;
+
public SavedState() {
@@ -1545,6 +1656,7 @@
mReverseLayout = in.readInt() == 1;
mStackFromEnd = in.readInt() == 1;
mAnchorLayoutFromEnd = in.readInt() == 1;
+ mRecycleChildrenOnDetach = in.readInt() == 1;
}
public SavedState(SavedState other) {
@@ -1554,6 +1666,15 @@
mReverseLayout = other.mReverseLayout;
mStackFromEnd = other.mStackFromEnd;
mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
+ mRecycleChildrenOnDetach = other.mRecycleChildrenOnDetach;
+ }
+
+ boolean hasValidAnchor() {
+ return mAnchorPosition >= 0;
+ }
+
+ void invalidateAnchor() {
+ mAnchorPosition = RecyclerView.NO_POSITION;
}
@Override
@@ -1569,6 +1690,7 @@
dest.writeInt(mReverseLayout ? 1 : 0);
dest.writeInt(mStackFromEnd ? 1 : 0);
dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
+ dest.writeInt(mRecycleChildrenOnDetach ? 1 : 0);
}
public static final Parcelable.Creator<SavedState> CREATOR
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java b/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
index 308bae6..ed4c950 100644
--- a/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
@@ -170,7 +170,7 @@
// area under curve (1-(1-x)^2) can be calculated as (1 - x/3) * x * x
// which gives 0.100028 when x = .3356
// this is why we divide linear scrolling time with .3356
- return (int) (calculateTimeForScrolling(dx) / .3356);
+ return (int) Math.ceil(calculateTimeForScrolling(dx) / .3356);
}
/**
@@ -181,7 +181,10 @@
* @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
*/
protected int calculateTimeForScrolling(int dx) {
- return Math.round(Math.abs(dx) * MILLISECONDS_PER_PX);
+ // In a case where dx is very small, rounding may return 0 although dx > 0.
+ // To avoid that issue, ceil the result so that if dx > 0, we'll always return positive
+ // time.
+ return (int) Math.ceil(Math.abs(dx) * MILLISECONDS_PER_PX);
}
/**
diff --git a/v7/recyclerview/src/android/support/v7/widget/OrientationHelper.java b/v7/recyclerview/src/android/support/v7/widget/OrientationHelper.java
new file mode 100644
index 0000000..3d800fe
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/OrientationHelper.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import android.view.View;
+import android.widget.LinearLayout;
+
+/**
+ * Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
+ * <p>
+ * It is developed to easily support vertical and horizontal orientations in a LayoutManager but
+ * can also be used to abstract calls around view bounds and child measurements with margins and
+ * decorations.
+ *
+ * @see #createHorizontalHelper(RecyclerView.LayoutManager)
+ * @see #createVerticalHelper(RecyclerView.LayoutManager)
+ */
+public abstract class OrientationHelper {
+
+ protected final RecyclerView.LayoutManager mLayoutManager;
+
+ public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
+
+ public static final int VERTICAL = LinearLayout.VERTICAL;
+
+ private OrientationHelper(RecyclerView.LayoutManager layoutManager) {
+ mLayoutManager = layoutManager;
+ }
+
+ /**
+ * Returns the start of the view including its decoration and margin.
+ * <p>
+ * For example, for the horizontal helper, if a View's left is at pixel 20, has 2px left
+ * decoration and 3px left margin, returned value will be 15px.
+ *
+ * @param view The view element to check
+ * @return The first pixel of the element
+ * @see #getDecoratedEnd(android.view.View)
+ */
+ public abstract int getDecoratedStart(View view);
+
+ /**
+ * Returns the end of the view including its decoration and margin.
+ * <p>
+ * For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right
+ * decoration and 3px right margin, returned value will be 205.
+ *
+ * @param view The view element to check
+ * @return The last pixel of the element
+ * @see #getDecoratedStart(android.view.View)
+ */
+ public abstract int getDecoratedEnd(View view);
+
+ /**
+ * Returns the space occupied by this View in the current orientation including decorations and
+ * margins.
+ *
+ * @param view The view element to check
+ * @return Total space occupied by this view
+ * @see #getDecoratedMeasurementInOther(View)
+ */
+ public abstract int getDecoratedMeasurement(View view);
+
+ /**
+ * Returns the space occupied by this View in the perpendicular orientation including
+ * decorations and margins.
+ *
+ * @param view The view element to check
+ * @return Total space occupied by this view in the perpendicular orientation to current one
+ * @see #getDecoratedMeasurement(View)
+ */
+ public abstract int getDecoratedMeasurementInOther(View view);
+
+ /**
+ * Returns the start position of the layout after the start padding is added.
+ *
+ * @return The very first pixel we can draw.
+ */
+ public abstract int getStartAfterPadding();
+
+ /**
+ * Returns the end position of the layout after the end padding is removed.
+ *
+ * @return The end boundary for this layout.
+ */
+ public abstract int getEndAfterPadding();
+
+ /**
+ * Returns the end position of the layout without taking padding into account.
+ *
+ * @return The end boundary for this layout without considering padding.
+ */
+ public abstract int getEnd();
+
+ /**
+ * Offsets all children's positions by the given amount.
+ *
+ * @param amount Value to add to each child's layout parameters
+ */
+ public abstract void offsetChildren(int amount);
+
+ /**
+ * Returns the total space to layout. This number is the difference between
+ * {@link #getEndAfterPadding()} and {@link #getStartAfterPadding()}.
+ *
+ * @return Total space to layout children
+ */
+ public abstract int getTotalSpace();
+
+ /**
+ * Offsets the child in this orientation.
+ *
+ * @param view View to offset
+ * @param offset offset amount
+ */
+ public abstract void offsetChild(View view, int offset);
+
+ /**
+ * Returns the padding at the end of the layout. For horizontal helper, this is the right
+ * padding and for vertical helper, this is the bottom padding. This method does not check
+ * whether the layout is RTL or not.
+ *
+ * @return The padding at the end of the layout.
+ */
+ public abstract int getEndPadding();
+
+ /**
+ * Creates an OrientationHelper for the given LayoutManager and orientation.
+ *
+ * @param layoutManager LayoutManager to attach to
+ * @param orientation Desired orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
+ * @return A new OrientationHelper
+ */
+ public static OrientationHelper createOrientationHelper(
+ RecyclerView.LayoutManager layoutManager, int orientation) {
+ switch (orientation) {
+ case HORIZONTAL:
+ return createHorizontalHelper(layoutManager);
+ case VERTICAL:
+ return createVerticalHelper(layoutManager);
+ }
+ throw new IllegalArgumentException("invalid orientation");
+ }
+
+ /**
+ * Creates a horizontal OrientationHelper for the given LayoutManager.
+ *
+ * @param layoutManager The LayoutManager to attach to.
+ * @return A new OrientationHelper
+ */
+ public static OrientationHelper createHorizontalHelper(
+ RecyclerView.LayoutManager layoutManager) {
+ return new OrientationHelper(layoutManager) {
+ @Override
+ public int getEndAfterPadding() {
+ return mLayoutManager.getWidth() - mLayoutManager.getPaddingRight();
+ }
+
+ @Override
+ public int getEnd() {
+ return mLayoutManager.getWidth();
+ }
+
+ @Override
+ public void offsetChildren(int amount) {
+ mLayoutManager.offsetChildrenHorizontal(amount);
+ }
+
+ @Override
+ public int getStartAfterPadding() {
+ return mLayoutManager.getPaddingLeft();
+ }
+
+ @Override
+ public int getDecoratedMeasurement(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ + params.rightMargin;
+ }
+
+ @Override
+ public int getDecoratedMeasurementInOther(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ + params.bottomMargin;
+ }
+
+ @Override
+ public int getDecoratedEnd(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedRight(view) + params.rightMargin;
+ }
+
+ @Override
+ public int getDecoratedStart(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedLeft(view) - params.leftMargin;
+ }
+
+ @Override
+ public int getTotalSpace() {
+ return mLayoutManager.getWidth() - mLayoutManager.getPaddingLeft()
+ - mLayoutManager.getPaddingRight();
+ }
+
+ @Override
+ public void offsetChild(View view, int offset) {
+ view.offsetLeftAndRight(offset);
+ }
+
+ @Override
+ public int getEndPadding() {
+ return mLayoutManager.getPaddingRight();
+ }
+ };
+ }
+
+ /**
+ * Creates a vertical OrientationHelper for the given LayoutManager.
+ *
+ * @param layoutManager The LayoutManager to attach to.
+ * @return A new OrientationHelper
+ */
+ public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
+ return new OrientationHelper(layoutManager) {
+ @Override
+ public int getEndAfterPadding() {
+ return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
+ }
+
+ @Override
+ public int getEnd() {
+ return mLayoutManager.getHeight();
+ }
+
+ @Override
+ public void offsetChildren(int amount) {
+ mLayoutManager.offsetChildrenVertical(amount);
+ }
+
+ @Override
+ public int getStartAfterPadding() {
+ return mLayoutManager.getPaddingTop();
+ }
+
+ @Override
+ public int getDecoratedMeasurement(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin
+ + params.bottomMargin;
+ }
+
+ @Override
+ public int getDecoratedMeasurementInOther(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin
+ + params.rightMargin;
+ }
+
+ @Override
+ public int getDecoratedEnd(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
+ }
+
+ @Override
+ public int getDecoratedStart(View view) {
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ return mLayoutManager.getDecoratedTop(view) - params.topMargin;
+ }
+
+ @Override
+ public int getTotalSpace() {
+ return mLayoutManager.getHeight() - mLayoutManager.getPaddingTop()
+ - mLayoutManager.getPaddingBottom();
+ }
+
+ @Override
+ public void offsetChild(View view, int offset) {
+ view.offsetTopAndBottom(offset);
+ }
+
+ @Override
+ public int getEndPadding() {
+ return mLayoutManager.getPaddingBottom();
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
index b96757e..0c85b79 100644
--- a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -27,12 +27,13 @@
import android.os.Parcelable;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
-import android.support.v4.util.Pools;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.EdgeEffectCompat;
import android.support.v4.widget.ScrollerCompat;
+import static android.support.v7.widget.AdapterHelper.UpdateOp;
+import static android.support.v7.widget.AdapterHelper.Callback;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
@@ -92,10 +93,16 @@
private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
- private final Recycler mRecycler = new Recycler();
+ final Recycler mRecycler = new Recycler();
private SavedState mPendingSavedState;
+ AdapterHelper mAdapterHelper;
+
+ ChildHelper mChildHelper;
+
+ final List<View> mDisappearingViewsInLayoutPass = new ArrayList<View>();
+
/**
* Note: this Runnable is only ever posted if:
* 1) We've been through first layout
@@ -104,18 +111,29 @@
*/
private final Runnable mUpdateChildViewsRunnable = new Runnable() {
public void run() {
- eatRequestLayout();
- updateChildViews();
- resumeRequestLayout(true);
+ if (!mAdapterHelper.hasPendingUpdates()) {
+ return;
+ }
+ if (!mFirstLayoutComplete) {
+ // a layout request will happen, we should not do layout here.
+ return;
+ }
+ if (mDataSetHasChangedAfterLayout) {
+ dispatchLayout();
+ } else {
+ eatRequestLayout();
+ mAdapterHelper.preProcess();
+ if (!mLayoutRequestEaten) {
+ // We run this after pre-processing is complete so that ViewHolders have their
+ // final adapter positions. No need to run it if a layout is already requested.
+ rebindUpdatedViewHolders();
+ }
+ resumeRequestLayout(true);
+ }
}
};
private final Rect mTempRect = new Rect();
-
- private final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
- private final ArrayList<UpdateOp> mPendingLayoutUpdates = new ArrayList<UpdateOp>();
- private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
-
private Adapter mAdapter;
private LayoutManager mLayout;
private RecyclerListener mRecyclerListener;
@@ -131,6 +149,22 @@
private boolean mAdapterUpdateDuringMeasure;
private final boolean mPostUpdatesOnAnimation;
+ /**
+ * Set to true when an adapter data set changed notification is received.
+ * In that case, we cannot run any animations since we don't know what happened.
+ */
+ private boolean mDataSetHasChangedAfterLayout = false;
+
+ /**
+ * This variable is set to true during a dispatchLayout and/or scroll.
+ * Some methods should not be called during these periods (e.g. adapter data change).
+ * Doing so will create hard to find bugs so we better check it and throw an exception.
+ *
+ * @see #assertInLayoutOrScroll(String)
+ * @see #assertNotInLayoutOrScroll(String)
+ */
+ private boolean mRunningLayoutOrScroll = false;
+
private EdgeEffectCompat mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
ItemAnimator mItemAnimator = new DefaultItemAnimator();
@@ -171,15 +205,13 @@
private final ViewFlinger mViewFlinger = new ViewFlinger();
- private final State mState = new State();
+ final State mState = new State();
private OnScrollListener mScrollListener;
// For use in item animations
boolean mItemsAddedOrRemoved = false;
boolean mItemsChanged = false;
- int mAnimatingViewIndex = -1;
- int mNumAnimatingViews = 0;
boolean mInPreLayout = false;
private ItemAnimator.ItemAnimatorListener mItemAnimatorListener =
new ItemAnimatorRestoreListener();
@@ -222,6 +254,116 @@
setWillNotDraw(ViewCompat.getOverScrollMode(this) == ViewCompat.OVER_SCROLL_NEVER);
mItemAnimator.setListener(mItemAnimatorListener);
+ initAdapterManager();
+ initChildrenHelper();
+ }
+
+ private void initChildrenHelper() {
+ mChildHelper = new ChildHelper(new ChildHelper.Callback() {
+ @Override
+ public int getChildCount() {
+ return RecyclerView.this.getChildCount();
+ }
+
+ @Override
+ public void addView(View child, int index) {
+ RecyclerView.this.addView(child, index);
+ }
+
+ @Override
+ public int indexOfChild(View view) {
+ return RecyclerView.this.indexOfChild(view);
+ }
+
+ @Override
+ public void removeViewAt(int index) {
+ RecyclerView.this.removeViewAt(index);
+ }
+
+ @Override
+ public View getChildAt(int offset) {
+ return RecyclerView.this.getChildAt(offset);
+ }
+
+ @Override
+ public void removeAllViews() {
+ RecyclerView.this.removeAllViews();
+ }
+
+ @Override
+ public ViewHolder getChildViewHolder(View view) {
+ return getChildViewHolderInt(view);
+ }
+
+ @Override
+ public void attachViewToParent(View child, int index,
+ ViewGroup.LayoutParams layoutParams) {
+ RecyclerView.this.attachViewToParent(child, index, layoutParams);
+ }
+
+ @Override
+ public void detachViewFromParent(int offset) {
+ RecyclerView.this.detachViewFromParent(offset);
+ }
+ });
+ }
+
+ void initAdapterManager() {
+ mAdapterHelper = new AdapterHelper(new Callback() {
+ @Override
+ public ViewHolder findViewHolder(int position) {
+ return findViewHolderForPosition(position, true);
+ }
+
+ @Override
+ public void offsetPositionsForRemovingInvisible(int start, int count) {
+ offsetPositionRecordsForRemove(start, count, true);
+ mItemsAddedOrRemoved = true;
+ mState.mDeletedInvisibleItemCountSincePreviousLayout += count;
+ }
+
+ @Override
+ public void offsetPositionsForRemovingLaidOutOrNewView(int positionStart, int itemCount) {
+ offsetPositionRecordsForRemove(positionStart, itemCount, false);
+ mItemsAddedOrRemoved = true;
+ }
+
+ @Override
+ public void markViewHoldersUpdated(int positionStart, int itemCount) {
+ viewRangeUpdate(positionStart, itemCount);
+ mItemsChanged = true;
+ }
+
+ @Override
+ public void onDispatchFirstPass(UpdateOp op) {
+ dispatchUpdate(op);
+ }
+
+ void dispatchUpdate(UpdateOp op) {
+ switch (op.cmd) {
+ case UpdateOp.ADD:
+ mLayout.onItemsAdded(RecyclerView.this, op.positionStart, op.itemCount);
+ break;
+ case UpdateOp.REMOVE:
+ mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount);
+ break;
+ case UpdateOp.UPDATE:
+ mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount);
+ break;
+ }
+ }
+
+ @Override
+ public void onDispatchSecondPass(UpdateOp op) {
+ dispatchUpdate(op);
+ }
+
+ @Override
+ public void offsetPositionsForAdd(int positionStart, int itemCount) {
+ offsetPositionRecordsForInsert(positionStart, itemCount);
+ mItemsAddedOrRemoved = true;
+ }
+ });
}
/**
@@ -252,6 +394,18 @@
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
}
+ // end all running animations
+ if (mItemAnimator != null) {
+ mItemAnimator.endAnimations();
+ }
+ // Since animations are ended, mLayout.children should be equal to recyclerView.children.
+ // This may not be true if item animator's end does not work as expected. (e.g. not release
+ // children instantly). It is safer to use mLayout's child count.
+ if (mLayout != null) {
+ mLayout.removeAndRecycleAllViews(mRecycler);
+ mLayout.removeAndRecycleScrapInt(mRecycler, true);
+ }
+ mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
@@ -306,22 +460,23 @@
if (layout == mLayout) {
return;
}
-
- mRecycler.clear();
- removeAllViews();
+ // TODO We should do this switch a dispachLayout pass and animate children. There is a good
+ // chance that LayoutManagers will re-use views.
if (mLayout != null) {
if (mIsAttached) {
- mLayout.onDetachedFromWindow(this);
+ mLayout.onDetachedFromWindow(this, mRecycler);
}
- mLayout.mRecyclerView = null;
+ mLayout.setRecyclerView(null);
}
+ mRecycler.clear();
+ mChildHelper.removeAllViewsUnfiltered();
mLayout = layout;
if (layout != null) {
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout +
" is already attached to a RecyclerView: " + layout.mRecyclerView);
}
- layout.mRecyclerView = this;
+ mLayout.setRecyclerView(this);
if (mIsAttached) {
mLayout.onAttachedToWindow(this);
}
@@ -361,23 +516,13 @@
* @param view The view to be removed
*/
private void addAnimatingView(View view) {
- boolean alreadyAdded = false;
- if (mNumAnimatingViews > 0) {
- for (int i = mAnimatingViewIndex; i < getChildCount(); ++i) {
- if (getChildAt(i) == view) {
- alreadyAdded = true;
- break;
- }
- }
- }
- if (!alreadyAdded) {
- if (mNumAnimatingViews == 0) {
- mAnimatingViewIndex = getChildCount();
- }
- ++mNumAnimatingViews;
- addView(view);
- }
+ final boolean alreadyParented = view.getParent() == this;
mRecycler.unscrapView(getChildViewHolder(view));
+ if (!alreadyParented) {
+ mChildHelper.addView(view, true);
+ } else {
+ mChildHelper.hide(view);
+ }
}
/**
@@ -386,32 +531,15 @@
* @see #addAnimatingView(View)
*/
private void removeAnimatingView(View view) {
- if (mNumAnimatingViews > 0) {
- for (int i = mAnimatingViewIndex; i < getChildCount(); ++i) {
- if (getChildAt(i) == view) {
- removeViewAt(i);
- --mNumAnimatingViews;
- if (mNumAnimatingViews == 0) {
- mAnimatingViewIndex = -1;
- }
- mRecycler.recycleView(view);
- return;
- }
+ eatRequestLayout();
+ if (mChildHelper.removeViewIfHidden(view)) {
+ mRecycler.unscrapView(getChildViewHolderInt(view));
+ mRecycler.recycleView(view);
+ if (DEBUG) {
+ Log.d(TAG, "after removing animated view: " + view + ", " + this);
}
}
- }
-
- private View getAnimatingView(int position, int type) {
- if (mNumAnimatingViews > 0) {
- for (int i = mAnimatingViewIndex; i < getChildCount(); ++i) {
- final View view = getChildAt(i);
- ViewHolder holder = getChildViewHolder(view);
- if (holder.getPosition() == position && holder.getItemViewType() == type) {
- return view;
- }
- }
- }
- return null;
+ resumeRequestLayout(false);
}
/**
@@ -449,6 +577,17 @@
}
/**
+ * Sets a new {@link ViewCacheExtension} to be used by the Recycler.
+ *
+ * @param extension ViewCacheExtension to be used or null if you want to clear the existing one.
+ *
+ * @see {@link ViewCacheExtension#getViewForPositionAndType(Recycler, int, int)}
+ */
+ public void setViewCacheExtension(ViewCacheExtension extension) {
+ mRecycler.setViewCacheExtension(extension);
+ }
+
+ /**
* Set the number of offscreen views to retain before adding them to the potentially shared
* {@link #getRecycledViewPool() recycled view pool}.
*
@@ -484,6 +623,7 @@
if (mScrollListener != null) {
mScrollListener.onScrollStateChanged(state);
}
+ mLayout.onScrollStateChanged(state);
}
/**
@@ -609,15 +749,28 @@
}
/**
+ * Helper method reflect data changes to the state.
+ * <p>
+ * Adapter changes during a scroll may trigger a crash because scroll assumes no data change
+ * but data actually changed.
+ * <p>
+ * This method consumes all deferred changes to avoid that case.
+ */
+ private void consumePendingUpdateOperations() {
+ if (mAdapterHelper.hasPendingUpdates()) {
+ mUpdateChildViewsRunnable.run();
+ }
+ }
+
+ /**
* Does not perform bounds checking. Used by internal methods that have already validated input.
*/
void scrollByInternal(int x, int y) {
- if (mItemAnimator != null) {
- mItemAnimator.endAnimations();
- }
int overscrollX = 0, overscrollY = 0;
+ consumePendingUpdateOperations();
if (mAdapter != null) {
eatRequestLayout();
+ mRunningLayoutOrScroll = true;
if (x != 0) {
final int hresult = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
overscrollX = x - hresult;
@@ -626,13 +779,35 @@
final int vresult = mLayout.scrollVerticallyBy(y, mRecycler, mState);
overscrollY = y - vresult;
}
+ if (supportsChangeAnimations()) {
+ // Fix up shadow views used by changing animations
+ int count = mChildHelper.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View view = mChildHelper.getChildAt(i);
+ ViewHolder holder = getChildViewHolder(view);
+ if (holder != null && holder.mShadowingHolder != null) {
+ ViewHolder shadowingHolder = holder.mShadowingHolder;
+ View shadowingView = shadowingHolder != null ? shadowingHolder.itemView : null;
+ if (shadowingView != null) {
+ int left = view.getLeft();
+ int top = view.getTop();
+ if (left != shadowingView.getLeft() || top != shadowingView.getTop()) {
+ shadowingView.layout(left, top,
+ left + shadowingView.getWidth(),
+ top + shadowingView.getHeight());
+ }
+ }
+ }
+ }
+ }
+ mRunningLayoutOrScroll = false;
resumeRequestLayout(false);
}
-
if (!mItemDecorations.isEmpty()) {
invalidate();
}
if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
+ considerReleasingGlowsOnScroll(x, y);
pullGlows(overscrollX, overscrollY);
}
if (mScrollListener != null && (x != 0 || y != 0)) {
@@ -889,6 +1064,25 @@
}
}
+ private void considerReleasingGlowsOnScroll(int dx, int dy) {
+ boolean needsInvalidate = false;
+ if (mLeftGlow != null && !mLeftGlow.isFinished() && dx > 0) {
+ needsInvalidate = mLeftGlow.onRelease();
+ }
+ if (mRightGlow != null && !mRightGlow.isFinished() && dx < 0) {
+ needsInvalidate |= mRightGlow.onRelease();
+ }
+ if (mTopGlow != null && !mTopGlow.isFinished() && dy > 0) {
+ needsInvalidate |= mTopGlow.onRelease();
+ }
+ if (mBottomGlow != null && !mBottomGlow.isFinished() && dy < 0) {
+ needsInvalidate |= mBottomGlow.onRelease();
+ }
+ if (needsInvalidate) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
void absorbGlows(int velocityX, int velocityY) {
if (velocityX < 0) {
if (mLeftGlow == null) {
@@ -947,7 +1141,7 @@
@Override
public void requestChildFocus(View child, View focused) {
- if (!mLayout.onRequestChildFocus(this, child, focused)) {
+ if (!mLayout.onRequestChildFocus(this, mState, child, focused)) {
mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
offsetDescendantRectToMyCoords(focused, mTempRect);
offsetRectIntoDescendantCoords(child, mTempRect);
@@ -985,16 +1179,49 @@
mFirstLayoutComplete = false;
stopScroll();
- // TODO Mark what our target position was if relevant, then we can jump there
- // on reattach.
mIsAttached = false;
if (mLayout != null) {
- mLayout.onDetachedFromWindow(this);
+ mLayout.onDetachedFromWindow(this, mRecycler);
}
removeCallbacks(mItemAnimatorRunner);
}
/**
+ * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+ * {@link IllegalStateException} if it <b>is not</b>.
+ *
+ * @param message The message for the exception. Can be null.
+ * @see #assertNotInLayoutOrScroll(String)
+ */
+ void assertInLayoutOrScroll(String message) {
+ if (!mRunningLayoutOrScroll) {
+ if (message == null) {
+ throw new IllegalStateException("Cannot call this method unless RecyclerView is "
+ + "computing a layout or scrolling");
+ }
+ throw new IllegalStateException(message);
+
+ }
+ }
+
+ /**
+ * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+ * {@link IllegalStateException} if it <b>is</b>.
+ *
+ * @param message The message for the exception. Can be null.
+ * @see #assertInLayoutOrScroll(String)
+ */
+ void assertNotInLayoutOrScroll(String message) {
+ if (mRunningLayoutOrScroll) {
+ if (message == null) {
+ throw new IllegalStateException("Cannot call this method while RecyclerView is "
+ + "computing a layout or scrolling");
+ }
+ throw new IllegalStateException(message);
+ }
+ }
+
+ /**
* Add an {@link OnItemTouchListener} to intercept touch events before they are dispatched
* to child views or this view's standard scrolling behavior.
*
@@ -1264,12 +1491,15 @@
protected void onMeasure(int widthSpec, int heightSpec) {
if (mAdapterUpdateDuringMeasure) {
eatRequestLayout();
- updateChildViews();
+ mAdapterHelper.preProcess();
mAdapterUpdateDuringMeasure = false;
resumeRequestLayout(false);
}
- mLayout.onMeasure(widthSpec, heightSpec);
+ if (mAdapter != null) {
+ mState.mItemCount = mAdapter.getItemCount();
+ }
+ mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final int widthSize = getMeasuredWidth();
final int heightSize = getMeasuredHeight();
@@ -1285,7 +1515,7 @@
* to the items in this RecyclerView. By default, RecyclerView instantiates and
* uses an instance of {@link DefaultItemAnimator}. Whether item animations are
* enabled for the RecyclerView depends on the ItemAnimator and whether
- * the LayoutManager {@link LayoutManager#supportsItemAnimations()
+ * the LayoutManager {@link LayoutManager#supportsPredictiveItemAnimations()
* supports item animations}.
*
* @param animator The ItemAnimator being set. If null, no animations will occur
@@ -1293,6 +1523,7 @@
*/
public void setItemAnimator(ItemAnimator animator) {
if (mItemAnimator != null) {
+ mItemAnimator.endAnimations();
mItemAnimator.setListener(null);
}
mItemAnimator = animator;
@@ -1314,6 +1545,10 @@
return mItemAnimator;
}
+ private boolean supportsChangeAnimations() {
+ return mItemAnimator != null && mItemAnimator.getSupportsChangeAnimations();
+ }
+
/**
* Post a runnable to the next frame to run pending item animations. Only the first such
* request will be posted, governed by the mPostedAnimatorRunner flag.
@@ -1325,8 +1560,8 @@
}
}
- private boolean itemAnimationsEnabled() {
- return (mItemAnimator != null && mLayout.supportsItemAnimations());
+ private boolean predictiveItemAnimationsEnabled() {
+ return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
}
/**
@@ -1356,45 +1591,89 @@
Log.e(TAG, "No adapter attached; skipping layout");
return;
}
-
+ mDisappearingViewsInLayoutPass.clear();
eatRequestLayout();
-
- final boolean animateChanges = itemAnimationsEnabled() && mItemsAddedOrRemoved
- && !mItemsChanged;
+ mRunningLayoutOrScroll = true;
+ // simple animations are a subset of advanced animations (which will cause a
+ // prelayout step)
+ boolean animationTypeSupported = (mItemsAddedOrRemoved && !mItemsChanged) ||
+ (mItemsAddedOrRemoved || (mItemsChanged && supportsChangeAnimations()));
+ mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator != null &&
+ (mDataSetHasChangedAfterLayout || animationTypeSupported ||
+ mLayout.mRequestedSimpleAnimations) &&
+ (!mDataSetHasChangedAfterLayout || mAdapter.hasStableIds());
+ mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations &&
+ animationTypeSupported && !mDataSetHasChangedAfterLayout &&
+ predictiveItemAnimationsEnabled();
+ ArrayMap<Long, ViewHolder> oldChangedHolders =
+ mState.mRunSimpleAnimations && mItemsChanged && supportsChangeAnimations() ?
+ new ArrayMap<Long, ViewHolder>() : null;
mItemsAddedOrRemoved = mItemsChanged = false;
ArrayMap<View, Rect> appearingViewInitialBounds = null;
- mState.mInPreLayout = animateChanges;
+ mState.mInPreLayout = mState.mRunPredictiveAnimations;
mState.mItemCount = mAdapter.getItemCount();
- if (animateChanges) {
+ if (mDataSetHasChangedAfterLayout) {
+ // Processing these items have no value since data set changed unexpectedly.
+ // Instead, we just reset it.
+ mAdapterHelper.reset();
+ markKnownViewsInvalid();
+ mLayout.onItemsChanged(this);
+ }
+
+ if (mState.mRunSimpleAnimations) {
// Step 0: Find out where all non-removed items are, pre-layout
mState.mPreLayoutHolderMap.clear();
mState.mPostLayoutHolderMap.clear();
- int count = getChildCount();
+ int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
- final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+ if (holder.shouldIgnore() || (holder.isInvalid() && !mAdapter.hasStableIds())) {
+ continue;
+ }
final View view = holder.itemView;
mState.mPreLayoutHolderMap.put(holder, new ItemHolderInfo(holder,
- view.getLeft(), view.getTop(), view.getRight(), view.getBottom(),
- holder.mPosition));
+ view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
}
-
+ }
+ if (mState.mRunPredictiveAnimations) {
// Step 1: run prelayout: This will use the old positions of items. The layout manager
// is expected to layout everything, even removed items (though not to add removed
// items back to the container). This gives the pre-layout position of APPEARING views
// which come into existence as part of the real layout.
- mInPreLayout = true;
+
+ // Save old positions so that LayoutManager can run its mapping logic.
+ saveOldPositions();
+ // Make sure any pending data updates are flushed before laying out.
+ mAdapterHelper.preProcess();
+ if (oldChangedHolders != null) {
+ int count = mChildHelper.getChildCount();
+ for (int i = 0; i < count; ++i) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+ if (holder.isChanged() && !holder.isRemoved() && !holder.shouldIgnore()) {
+ long key = mAdapter.hasStableIds() ? holder.getItemId() :
+ (long) holder.mPosition;
+ oldChangedHolders.put(key, holder);
+ mState.mPreLayoutHolderMap.remove(holder);
+ }
+ }
+ }
+
+ mInPreLayout = true;
final boolean didStructureChange = mState.mStructureChanged;
mState.mStructureChanged = false;
- // temporarly disable flag because we are asking for previous layout
+ // temporarily disable flag because we are asking for previous layout
mLayout.onLayoutChildren(mRecycler, mState);
mState.mStructureChanged = didStructureChange;
mInPreLayout = false;
appearingViewInitialBounds = new ArrayMap<View, Rect>();
- for (int i = 0; i < getChildCount(); ++i) {
+ for (int i = 0; i < mChildHelper.getChildCount(); ++i) {
boolean found = false;
- View child = getChildAt(i);
+ View child = mChildHelper.getChildAt(i);
+ if (getChildViewHolderInt(child).shouldIgnore()) {
+ continue;
+ }
for (int j = 0; j < mState.mPreLayoutHolderMap.size(); ++j) {
ViewHolder holder = mState.mPreLayoutHolderMap.keyAt(j);
if (holder.itemView == child) {
@@ -1407,10 +1686,27 @@
child.getRight(), child.getBottom()));
}
}
+ // we don't process disappearing list because they may re-appear in post layout pass.
+ clearOldPositions();
+ mAdapterHelper.consumePostponedUpdates();
+ } else {
+ clearOldPositions();
+ mAdapterHelper.consumeUpdatesInOnePass();
+ if (oldChangedHolders != null) {
+ int count = mChildHelper.getChildCount();
+ for (int i = 0; i < count; ++i) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+ if (holder.isChanged() && !holder.isRemoved() && !holder.shouldIgnore()) {
+ long key = mAdapter.hasStableIds() ? holder.getItemId() :
+ (long) holder.mPosition;
+ oldChangedHolders.put(key, holder);
+ mState.mPreLayoutHolderMap.remove(holder);
+ }
+ }
+ }
}
- clearOldPositions();
- dispatchLayoutUpdates();
mState.mItemCount = mAdapter.getItemCount();
+ mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
@@ -1419,17 +1715,30 @@
mState.mStructureChanged = false;
mPendingSavedState = null;
- if (animateChanges) {
- // Step 3: Find out where things are now, post-layout
- int count = getChildCount();
- for (int i = 0; i < count; ++i) {
- ViewHolder holder = getChildViewHolderInt(getChildAt(i));
- final View view = holder.itemView;
- mState.mPostLayoutHolderMap.put(holder, new ItemHolderInfo(holder,
- view.getLeft(), view.getTop(), view.getRight(), view.getBottom(),
- holder.mPosition));
- }
+ // onLayoutChildren may have caused client code to disable item animations; re-check
+ mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
+ if (mState.mRunSimpleAnimations) {
+ // Step 3: Find out where things are now, post-layout
+ ArrayMap<Long, ViewHolder> newChangedHolders = oldChangedHolders != null ?
+ new ArrayMap<Long, ViewHolder>() : null;
+ int count = mChildHelper.getChildCount();
+ for (int i = 0; i < count; ++i) {
+ ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+ if (holder.shouldIgnore()) {
+ continue;
+ }
+ final View view = holder.itemView;
+ long key = mAdapter.hasStableIds() ? holder.getItemId() :
+ (long) holder.mPosition;
+ if (newChangedHolders != null && oldChangedHolders.get(key) != null) {
+ newChangedHolders.put(key, holder);
+ } else {
+ mState.mPostLayoutHolderMap.put(holder, new ItemHolderInfo(holder,
+ view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
+ }
+ }
+ processDisappearingList();
// Step 4: Animate DISAPPEARING and REMOVED items
int preLayoutCount = mState.mPreLayoutHolderMap.size();
for (int i = preLayoutCount - 1; i >= 0; i--) {
@@ -1454,9 +1763,9 @@
if ((mState.mPreLayoutHolderMap.isEmpty() ||
!mState.mPreLayoutHolderMap.containsKey(itemHolder))) {
mState.mPostLayoutHolderMap.removeAt(i);
-
- animateAppearance(itemHolder,
- appearingViewInitialBounds.get(itemHolder.itemView),
+ Rect initialBounds = (appearingViewInitialBounds != null) ?
+ appearingViewInitialBounds.get(itemHolder.itemView) : null;
+ animateAppearance(itemHolder, initialBounds,
info.left, info.top);
}
}
@@ -1481,17 +1790,73 @@
}
}
}
+ // Step 7: Animate CHANGING items
+ count = oldChangedHolders != null ? oldChangedHolders.size() : 0;
+ for (int i = 0; i < count; ++i) {
+ long key = oldChangedHolders.keyAt(i);
+ ViewHolder oldHolder = oldChangedHolders.get(key);
+ View oldView = oldHolder.itemView;
+ if (!oldHolder.shouldIgnore() &&
+ mRecycler.mChangedScrap.contains(oldHolder)) {
+ oldHolder.setIsRecyclable(false);
+ removeDetachedView(oldView, false);
+ addAnimatingView(oldView);
+ mRecycler.unscrapView(oldHolder);
+ if (DEBUG) {
+ Log.d(TAG, "CHANGED: " + oldHolder + " with view " + oldView);
+ }
+ ViewHolder newHolder = newChangedHolders.get(key);
+ oldHolder.mShadowedHolder = newHolder;
+ if (newHolder != null && !newHolder.shouldIgnore()) {
+ newHolder.setIsRecyclable(false);
+ newHolder.mShadowingHolder = oldHolder;
+ }
+ if (mItemAnimator.animateChange(oldHolder, newHolder)) {
+ postAnimationRunner();
+ }
+ }
+ }
}
resumeRequestLayout(false);
- mLayout.removeAndRecycleScrapInt(mRecycler);
+ mLayout.removeAndRecycleScrapInt(mRecycler, !mState.mRunPredictiveAnimations);
mState.mPreviousLayoutItemCount = mState.mItemCount;
- mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
+ mDataSetHasChangedAfterLayout = false;
+ mState.mRunSimpleAnimations = false;
+ mState.mRunPredictiveAnimations = false;
+ mRunningLayoutOrScroll = false;
+ mLayout.mRequestedSimpleAnimations = false;
+ if (mRecycler.mChangedScrap != null) {
+ mRecycler.mChangedScrap.clear();
+ }
+ }
+
+ /**
+ * A LayoutManager may want to layout a view just to animate disappearance.
+ * This method handles those views and triggers remove animation on them.
+ */
+ private void processDisappearingList() {
+ final int count = mDisappearingViewsInLayoutPass.size();
+ for (int i = 0; i < count; i ++) {
+ View view = mDisappearingViewsInLayoutPass.get(i);
+ ViewHolder vh = getChildViewHolderInt(view);
+ final ItemHolderInfo info = mState.mPreLayoutHolderMap.remove(vh);
+ if (!mState.isPreLayout()) {
+ mState.mPostLayoutHolderMap.remove(vh);
+ }
+ if (info != null) {
+ animateDisappearance(info);
+ } else {
+ // let it disappear from the position it becomes visible
+ animateDisappearance(new ItemHolderInfo(vh, view.getLeft(), view.getTop(),
+ view.getRight(), view.getBottom()));
+ }
+ }
+ mDisappearingViewsInLayoutPass.clear();
}
private void animateAppearance(ViewHolder itemHolder, Rect beforeBounds, int afterLeft,
int afterTop) {
View newItemView = itemHolder.itemView;
-
if (beforeBounds != null &&
(beforeBounds.left != afterLeft || beforeBounds.top != afterTop)) {
// slide items in if before/after locations differ
@@ -1540,13 +1905,13 @@
Log.d(TAG, "REMOVED: " + disappearingItem.holder +
" with view " + disappearingItemView);
}
+ disappearingItem.holder.setIsRecyclable(false);
if (mItemAnimator.animateRemove(disappearingItem.holder)) {
postAnimationRunner();
}
}
}
-
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
eatRequestLayout();
@@ -1565,9 +1930,9 @@
}
void markItemDecorInsetsDirty() {
- final int childCount = getChildCount();
+ final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
- final View child = getChildAt(i);
+ final View child = mChildHelper.getUnfilteredChildAt(i);
((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}
}
@@ -1578,9 +1943,10 @@
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
- mItemDecorations.get(i).onDrawOver(c, this);
+ mItemDecorations.get(i).onDrawOver(c, this, mState);
}
-
+ // TODO If padding is not 0 and chilChildrenToPadding is false, to draw glows properly, we
+ // need find children closest to edges. Not sure if it is worth the effort.
boolean needsInvalidate = false;
if (mLeftGlow != null && !mLeftGlow.isFinished()) {
final int restore = c.save();
@@ -1592,6 +1958,7 @@
if (mTopGlow != null && !mTopGlow.isFinished()) {
c.translate(getPaddingLeft(), getPaddingTop());
needsInvalidate |= mTopGlow != null && mTopGlow.draw(c);
+ c.translate(-getPaddingLeft(), -getPaddingTop());
}
if (mRightGlow != null && !mRightGlow.isFinished()) {
final int restore = c.save();
@@ -1621,7 +1988,7 @@
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
- mItemDecorations.get(i).onDraw(c, this);
+ mItemDecorations.get(i).onDraw(c, this, mState);
}
}
@@ -1654,105 +2021,41 @@
return mLayout.generateLayoutParams(p);
}
- private int findPositionOffset(int position) {
- int offset = 0;
- int count = mPendingLayoutUpdates.size();
- for (int i = 0; i < count; ++i) {
- UpdateOp op = mPendingLayoutUpdates.get(i);
- if (op.positionStart <= position) {
- if (op.cmd == UpdateOp.REMOVE) {
- offset -= op.itemCount;
- } else if (op.cmd == UpdateOp.ADD) {
- offset += op.itemCount;
- }
+ void saveOldPositions() {
+ final int childCount = mChildHelper.getUnfilteredChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (DEBUG && holder.mPosition == -1 && !holder.isRemoved()) {
+ throw new IllegalStateException("view holder cannot have position -1 unless it"
+ + " is removed");
+ }
+ if (!holder.shouldIgnore()) {
+ holder.saveOldPosition();
}
}
- return position + offset;
- }
-
- void dispatchLayoutUpdates() {
- final int opCount = mPendingLayoutUpdates.size();
- for (int i = 0; i < opCount; i++) {
- final UpdateOp op = mPendingLayoutUpdates.get(i);
- switch (op.cmd) {
- case UpdateOp.ADD:
- mLayout.onItemsAdded(this, op.positionStart, op.itemCount);
- break;
- case UpdateOp.REMOVE:
- mLayout.onItemsRemoved(this, op.positionStart, op.itemCount);
- break;
- case UpdateOp.UPDATE:
- // TODO: tell the layout manager
- break;
- }
- recycleUpdateOp(op);
- }
- mPendingLayoutUpdates.clear();
- }
-
- void updateChildViews() {
- final int opCount = mPendingUpdates.size();
- for (int i = 0; i < opCount; i++) {
- final UpdateOp op = mPendingUpdates.get(i);
- switch (op.cmd) {
- case UpdateOp.ADD:
- if (DEBUG) {
- Log.d(TAG, "UpdateOp.ADD start=" + op.positionStart + " count=" +
- op.itemCount);
- }
- offsetPositionRecordsForInsert(op.positionStart, op.itemCount);
- mItemsAddedOrRemoved = true;
- break;
- case UpdateOp.REMOVE:
- if (DEBUG) {
- Log.d(TAG, "UpdateOp.REMOVE start=" + op.positionStart + " count=" +
- op.itemCount);
- }
- for (int j = 0; j < op.itemCount; ++j) {
- ViewHolder holder = findViewHolderForPosition(op.positionStart + j, true);
- if (holder != null) {
- holder.setIsRecyclable(false);
- } else {
- mState.mDeletedInvisibleItemCountSincePreviousLayout ++;
- }
- }
- offsetPositionRecordsForRemove(op.positionStart, op.itemCount);
- mItemsAddedOrRemoved = true;
- break;
- case UpdateOp.UPDATE:
- if (DEBUG) {
- Log.d(TAG, "UpdateOp.UPDATE start=" + op.positionStart + " count=" +
- op.itemCount);
- }
- viewRangeUpdate(op.positionStart, op.itemCount);
- mItemsChanged = true;
- break;
- }
- mPendingLayoutUpdates.add(op);
- // TODO: recycle the op if no animator (also don't bother stashing in pending layout updates?)
- }
- mPendingUpdates.clear();
}
void clearOldPositions() {
- final int childCount = getChildCount();
+ final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
- final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
- holder.clearOldPosition();
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (!holder.shouldIgnore()) {
+ holder.clearOldPosition();
+ }
}
mRecycler.clearOldPositions();
}
void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
- final int childCount = getChildCount();
+ final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
- final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
- if (holder != null && holder.mPosition >= positionStart) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.shouldIgnore() && holder.mPosition >= positionStart) {
if (DEBUG) {
Log.d(TAG, "offsetPositionRecordsForInsert attached child " + i + " holder " +
holder + " now at position " + (holder.mPosition + itemCount));
}
- holder.offsetPosition(itemCount);
+ holder.offsetPosition(itemCount, false);
mState.mStructureChanged = true;
}
}
@@ -1760,19 +2063,20 @@
requestLayout();
}
- void offsetPositionRecordsForRemove(int positionStart, int itemCount) {
+ void offsetPositionRecordsForRemove(int positionStart, int itemCount,
+ boolean applyToPreLayout) {
final int positionEnd = positionStart + itemCount;
- final int childCount = getChildCount();
+ final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
- final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
- if (holder != null) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.shouldIgnore()) {
if (holder.mPosition >= positionEnd) {
if (DEBUG) {
Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i +
" holder " + holder + " now at position " +
(holder.mPosition - itemCount));
}
- holder.offsetPosition(-itemCount);
+ holder.offsetPosition(-itemCount, applyToPreLayout);
mState.mStructureChanged = true;
} else if (holder.mPosition >= positionStart) {
if (DEBUG) {
@@ -1781,10 +2085,11 @@
}
holder.addFlags(ViewHolder.FLAG_REMOVED);
mState.mStructureChanged = true;
+ holder.offsetPosition(-itemCount, applyToPreLayout);
}
}
}
- mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount);
+ mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount, applyToPreLayout);
requestLayout();
}
@@ -1795,60 +2100,78 @@
* @param itemCount Number of views that must explicitly be rebound
*/
void viewRangeUpdate(int positionStart, int itemCount) {
- final int childCount = getChildCount();
+ final int childCount = mChildHelper.getUnfilteredChildCount();
final int positionEnd = positionStart + itemCount;
for (int i = 0; i < childCount; i++) {
- final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
- if (holder == null) {
+ final View child = mChildHelper.getUnfilteredChildAt(i);
+ final ViewHolder holder = getChildViewHolderInt(child);
+ if (holder == null || holder.shouldIgnore()) {
continue;
}
-
- final int position = holder.getPosition();
- if (position >= positionStart && position < positionEnd) {
+ if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
+ // We re-bind these view holders after pre-processing is complete so that
+ // ViewHolders have their final positions assigned.
holder.addFlags(ViewHolder.FLAG_UPDATE);
- // Binding an attached view will request a layout if needed.
- mAdapter.bindViewHolder(holder, holder.getPosition());
+ if (supportsChangeAnimations()) {
+ holder.addFlags(ViewHolder.FLAG_CHANGED);
+ }
+ // lp cannot be null since we get ViewHolder from it.
+ ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}
}
mRecycler.viewRangeUpdate(positionStart, itemCount);
}
+ void rebindUpdatedViewHolders() {
+ final int childCount = mChildHelper.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
+ // validate type is correct
+ if (holder == null || holder.shouldIgnore()) {
+ continue;
+ }
+ if (holder.isRemoved() || holder.isInvalid()) {
+ requestLayout();
+ } else if (holder.needsUpdate()) {
+ final int type = mAdapter.getItemViewType(holder.mPosition);
+ if (holder.getItemViewType() == type) {
+ // Binding an attached view will request a layout if needed.
+ if (!holder.isChanged() || !supportsChangeAnimations()) {
+ mAdapter.bindViewHolder(holder, holder.mPosition);
+ } else {
+ // Don't rebind changed holders if change animations are enabled.
+ // We want the old contents for the animation and will get a new
+ // holder for the new contents.
+ requestLayout();
+ }
+ } else {
+ // binding to a new view will need re-layout anyways. We can as well trigger
+ // it here so that it happens during layout
+ holder.addFlags(ViewHolder.FLAG_INVALID);
+ requestLayout();
+ }
+ }
+ }
+ }
+
/**
* Mark all known views as invalid. Used in response to a, "the whole world might have changed"
* data change event.
*/
void markKnownViewsInvalid() {
- final int childCount = getChildCount();
-
+ final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
- final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
- if (holder != null) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.shouldIgnore()) {
holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
}
}
+ markItemDecorInsetsDirty();
mRecycler.markKnownViewsInvalid();
}
/**
- * Schedule an update of data from the adapter to occur on the next frame.
- * On newer platform versions this happens via the postOnAnimation mechanism and RecyclerView
- * attempts to avoid relayouts if possible.
- * On older platform versions the RecyclerView requests a layout the same way ListView does.
- */
- void postAdapterUpdate(UpdateOp op) {
- mPendingUpdates.add(op);
- if (mPendingUpdates.size() == 1) {
- if (mPostUpdatesOnAnimation && mHasFixedSize && mIsAttached) {
- ViewCompat.postOnAnimation(this, mUpdateChildViewsRunnable);
- } else {
- mAdapterUpdateDuringMeasure = true;
- requestLayout();
- }
- }
- }
-
- /**
* Retrieve the {@link ViewHolder} for the given child view.
*
* @param child Child of this RecyclerView to query for its ViewHolder
@@ -1906,20 +2229,23 @@
}
ViewHolder findViewHolderForPosition(int position, boolean checkNewPosition) {
- final int childCount = getChildCount();
+ final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
- final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
- if (holder != null) {
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
+ if (holder != null && !holder.isRemoved()) {
if (checkNewPosition) {
if (holder.mPosition == position) {
return holder;
}
- } else if(holder.getPosition() == position) {
+ } else if (holder.getPosition() == position) {
return holder;
}
}
}
- return mRecycler.findViewHolderForPosition(position);
+ // This method should not query cached views. It creates a problem during adapter updates
+ // when we are dealing with already laid out views. Also, for the public method, it is more
+ // reasonable to return null if position is not laid out.
+ return null;
}
/**
@@ -1932,14 +2258,16 @@
* is no such item.
*/
public ViewHolder findViewHolderForItemId(long id) {
- final int childCount = getChildCount();
+ final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
- final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
+ final ViewHolder holder = getChildViewHolderInt(mChildHelper.getUnfilteredChildAt(i));
if (holder != null && holder.getItemId() == id) {
return holder;
}
}
- return mRecycler.findViewHolderForItemId(id);
+ // this method should not query cached views. They are not children so they
+ // should not be returned in this public method
+ return null;
}
/**
@@ -1950,9 +2278,9 @@
* @return The child view under (x, y) or null if no matching child is found
*/
public View findChildViewUnder(float x, float y) {
- final int count = getChildCount();
+ final int count = mChildHelper.getChildCount();
for (int i = count - 1; i >= 0; i--) {
- final View child = getChildAt(i);
+ final View child = mChildHelper.getChildAt(i);
final float translationX = ViewCompat.getTranslationX(child);
final float translationY = ViewCompat.getTranslationY(child);
if (x >= child.getLeft() + translationX &&
@@ -1972,9 +2300,9 @@
* @param dy Vertical pixel offset to apply to the bounds of all child views
*/
public void offsetChildrenVertical(int dy) {
- final int childCount = getChildCount();
+ final int childCount = mChildHelper.getChildCount();
for (int i = 0; i < childCount; i++) {
- getChildAt(i).offsetTopAndBottom(dy);
+ mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
}
}
@@ -2010,9 +2338,9 @@
* @param dx Horizontal pixel offset to apply to the bounds of all child views
*/
public void offsetChildrenHorizontal(int dx) {
- final int childCount = getChildCount();
+ final int childCount = mChildHelper.getChildCount();
for (int i = 0; i < childCount; i++) {
- getChildAt(i).offsetLeftAndRight(dx);
+ mChildHelper.getChildAt(i).offsetLeftAndRight(dx);
}
}
@@ -2027,7 +2355,7 @@
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
- mItemDecorations.get(i).getItemOffsets(mTempRect, lp.getViewPosition(), this);
+ mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
@@ -2057,8 +2385,9 @@
@Override
public void run() {
disableRunOnAnimationRequests();
- // keep a local reference so that if it is changed during onAnimation method, it wont cause
- // unexpected behaviors
+ consumePendingUpdateOperations();
+ // keep a local reference so that if it is changed during onAnimation method, it won't
+ // cause unexpected behaviors
final ScrollerCompat scroller = mScroller;
final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
if (scroller.computeScrollOffset()) {
@@ -2071,6 +2400,7 @@
int overscrollX = 0, overscrollY = 0;
if (mAdapter != null) {
eatRequestLayout();
+ mRunningLayoutOrScroll = true;
if (dx != 0) {
final int hresult = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
overscrollX = dx - hresult;
@@ -2079,17 +2409,43 @@
final int vresult = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
overscrollY = dy - vresult;
}
+ if (supportsChangeAnimations()) {
+ // Fix up shadow views used by changing animations
+ int count = mChildHelper.getChildCount();
+ for (int i = 0; i < count; i++) {
+ View view = mChildHelper.getChildAt(i);
+ ViewHolder holder = getChildViewHolder(view);
+ if (holder != null && holder.mShadowingHolder != null) {
+ View shadowingView = holder.mShadowingHolder != null ?
+ holder.mShadowingHolder.itemView : null;
+ if (shadowingView != null) {
+ int left = view.getLeft();
+ int top = view.getTop();
+ if (left != shadowingView.getLeft() ||
+ top != shadowingView.getTop()) {
+ shadowingView.layout(left, top,
+ left + shadowingView.getWidth(),
+ top + shadowingView.getHeight());
+ }
+ }
+ }
+ }
+ }
if (smoothScroller != null && !smoothScroller.isPendingInitialRun() &&
smoothScroller.isRunning()) {
smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
}
+ mRunningLayoutOrScroll = false;
resumeRequestLayout(false);
}
if (!mItemDecorations.isEmpty()) {
invalidate();
}
-
+ if (ViewCompat.getOverScrollMode(RecyclerView.this) !=
+ ViewCompat.OVER_SCROLL_NEVER) {
+ considerReleasingGlowsOnScroll(dx, dy);
+ }
if (overscrollX != 0 || overscrollY != 0) {
final int vel = (int) scroller.getCurrVelocity();
@@ -2223,31 +2579,53 @@
private class RecyclerViewDataObserver extends AdapterDataObserver {
@Override
public void onChanged() {
+ assertNotInLayoutOrScroll(null);
if (mAdapter.hasStableIds()) {
- // TODO Determine what actually changed
- markKnownViewsInvalid();
+ // TODO Determine what actually changed.
+ // This is more important to implement now since this callback will disable all
+ // animations because we cannot rely on positions.
mState.mStructureChanged = true;
- requestLayout();
+ mDataSetHasChangedAfterLayout = true;
} else {
- markKnownViewsInvalid();
mState.mStructureChanged = true;
+ mDataSetHasChangedAfterLayout = true;
+ }
+ if (!mAdapterHelper.hasPendingUpdates()) {
requestLayout();
}
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
- postAdapterUpdate(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount));
+ assertNotInLayoutOrScroll(null);
+ if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount)) {
+ triggerUpdateProcessor();
+ }
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
- postAdapterUpdate(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount));
+ assertNotInLayoutOrScroll(null);
+ if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
+ triggerUpdateProcessor();
+ }
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
- postAdapterUpdate(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount));
+ assertNotInLayoutOrScroll(null);
+ if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
+ triggerUpdateProcessor();
+ }
+ }
+
+ void triggerUpdateProcessor() {
+ if (mPostUpdatesOnAnimation && mHasFixedSize && mIsAttached) {
+ ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
+ } else {
+ mAdapterUpdateDuringMeasure = true;
+ requestLayout();
+ }
}
}
@@ -2284,17 +2662,24 @@
return null;
}
+ int size() {
+ int count = 0;
+ for (int i = 0; i < mScrap.size(); i ++) {
+ ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i);
+ if (viewHolders != null) {
+ count += viewHolders.size();
+ }
+ }
+ return count;
+ }
+
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapHeapForType(viewType);
if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
return;
}
-
- scrap.mPosition = NO_POSITION;
- scrap.mOldPosition = NO_POSITION;
- scrap.mItemId = NO_ID;
- scrap.clearFlagsForSharedPool();
+ scrap.resetInternal();
scrapHeap.add(scrap);
}
@@ -2341,8 +2726,9 @@
*/
public final class Recycler {
private final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<ViewHolder>();
+ private ArrayList<ViewHolder> mChangedScrap = null;
- private final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
+ final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
private final List<ViewHolder>
mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);
@@ -2351,6 +2737,8 @@
private RecycledViewPool mRecyclerPool;
+ private ViewCacheExtension mViewCacheExtension;
+
private static final int DEFAULT_CACHE_SIZE = 2;
/**
@@ -2359,7 +2747,7 @@
*/
public void clear() {
mAttachedScrap.clear();
- mCachedViews.clear();
+ recycleAndClearCachedViews();
}
/**
@@ -2369,6 +2757,11 @@
*/
public void setViewCacheSize(int viewCount) {
mViewCacheMax = viewCount;
+ // first, try the views that can be recycled
+ for (int i = mCachedViews.size() - 1; i >= 0 && mCachedViews.size() > viewCount; i--) {
+ tryToRecycleCachedViewAt(i);
+ }
+ // if we could not recycle enough of them, remove some.
while (mCachedViews.size() > viewCount) {
mCachedViews.remove(mCachedViews.size() - 1);
}
@@ -2384,48 +2777,185 @@
}
/**
- * Obtain a view initialized for the given position.
- *
- * <p>This method should be used by {@link LayoutManager} implementations to obtain
- * views to represent data from an {@link Adapter}.</p>
+ * Helper method for getViewForPosition.
+ * <p>
+ * Checks whether a given view holder can be used for the provided position.
*
- * <p>The Recycler may reuse a scrap or detached view from a shared pool if one is
+ * @param holder ViewHolder
+ * @return true if ViewHolder matches the provided position, false otherwise
+ */
+ boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
+ // if it is a removed holder, nothing to verify since we cannot ask adapter anymore
+ // if it is not removed, verify the type and id.
+ if (holder.isRemoved()) {
+ return true;
+ }
+ if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
+ throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
+ + "adapter position" + holder);
+ }
+ final int type = mAdapter.getItemViewType(holder.mPosition);
+ if (type != holder.getItemViewType()) {
+ return false;
+ }
+ if (mAdapter.hasStableIds()) {
+ return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
+ }
+ return true;
+ }
+
+ /**
+ * Binds the given View to the position. The View can be a View previously retrieved via
+ * {@link #getViewForPosition(int)} or created by
+ * {@link Adapter#onCreateViewHolder(ViewGroup, int)}.
+ * <p>
+ * Generally, a LayoutManager should acquire its views via {@link #getViewForPosition(int)}
+ * and let the RecyclerView handle caching. This is a helper method for LayoutManager who
+ * wants to handle its own recycling logic.
+ * <p>
+ * Note that, {@link #getViewForPosition(int)} already binds the View to the position so
+ * you don't need to call this method unless you want to bind this View to another position.
+ *
+ * @param view The view to update.
+ * @param position The position of the item to bind to this View.
+ */
+ public void bindViewToPosition(View view, int position) {
+ ViewHolder holder = getChildViewHolderInt(view);
+ if (holder == null) {
+ throw new IllegalArgumentException("The view does not have a ViewHolder. You cannot"
+ + " pass arbitrary views to this method, they should be created by the "
+ + "Adapter");
+ }
+ final int offsetPosition = mAdapterHelper.findPositionOffset(position);
+ if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
+ throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ + "position " + position + "(offset:" + offsetPosition + ")."
+ + "state:" + mState.getItemCount());
+ }
+ mAdapter.bindViewHolder(holder, offsetPosition);
+ if (mState.isPreLayout()) {
+ holder.mPreLayoutPosition = position;
+ }
+
+ ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
+ if (lp == null) {
+ lp = generateDefaultLayoutParams();
+ holder.itemView.setLayoutParams(lp);
+ } else if (!checkLayoutParams(lp)) {
+ lp = generateLayoutParams(lp);
+ holder.itemView.setLayoutParams(lp);
+ }
+ ((LayoutParams) lp).mInsetsDirty = true;
+ ((LayoutParams) lp).mViewHolder = holder;
+ }
+
+ /**
+ * Obtain a view initialized for the given position.
+ *
+ * This method should be used by {@link LayoutManager} implementations to obtain
+ * views to represent data from an {@link Adapter}.
+ * <p>
+ * The Recycler may reuse a scrap or detached view from a shared pool if one is
* available for the correct view type. If the adapter has not indicated that the
* data at the given position has changed, the Recycler will attempt to hand back
- * a scrap view that was previously initialized for that data without rebinding.</p>
+ * a scrap view that was previously initialized for that data without rebinding.
*
* @param position Position to obtain a view for
* @return A view representing the data at <code>position</code> from <code>adapter</code>
*/
public View getViewForPosition(int position) {
+ return getViewForPosition(position, false);
+ }
+
+ View getViewForPosition(int position, boolean dryRun) {
+ if (position < 0 || position >= mState.getItemCount()) {
+ throw new IndexOutOfBoundsException("Invalid item position " + position
+ + "(" + position + "). Item count:" + mState.getItemCount());
+ }
ViewHolder holder;
-
- final int type = mAdapter.getItemViewType(position);
- if (mAdapter.hasStableIds()) {
- final long id = mAdapter.getItemId(position);
- holder = getScrapViewForId(id, type);
- } else {
- holder = getScrapViewForPosition(position, type);
+ // 1) Find from scrap by position
+ holder = getScrapViewForPosition(position, INVALID_TYPE, dryRun);
+ if (holder != null) {
+ if (!validateViewHolderForOffsetPosition(holder)) {
+ // recycle this scrap
+ if (!dryRun) {
+ // we would like to recycle this but need to make sure it is not used by
+ // animation logic etc.
+ holder.addFlags(ViewHolder.FLAG_INVALID);
+ if (holder.isScrap()) {
+ removeDetachedView(holder.itemView, false);
+ holder.unScrap();
+ } else if (holder.wasReturnedFromScrap()) {
+ holder.clearReturnedFromScrapFlag();
+ }
+ recycleViewHolder(holder);
+ }
+ holder = null;
+ }
}
-
if (holder == null) {
- holder = mAdapter.createViewHolder(RecyclerView.this, type);
- if (DEBUG) Log.d(TAG, "getViewForPosition created new ViewHolder");
+ final int offsetPosition = mAdapterHelper.findPositionOffset(position);
+ if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
+ throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
+ + "position " + position + "(offset:" + offsetPosition + ")."
+ + "state:" + mState.getItemCount());
+ }
+
+ final int type = mAdapter.getItemViewType(offsetPosition);
+ // 2) Find from scrap via stable ids, if exists
+ if (mAdapter.hasStableIds()) {
+ holder = getScrapViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);
+ if (holder != null) {
+ // update position
+ holder.mPosition = offsetPosition;
+ }
+ }
+ if (holder == null && mViewCacheExtension != null) {
+ // We are NOT sending the offsetPosition because LayoutManager does not
+ // know it.
+ final View view = mViewCacheExtension
+ .getViewForPositionAndType(this, position, type);
+ if (view != null) {
+ holder = getChildViewHolder(view);
+ if (holder == null) {
+ throw new IllegalArgumentException("getViewForPositionAndType returned"
+ + " a view which does not have a ViewHolder");
+ } else if (holder.shouldIgnore()) {
+ throw new IllegalArgumentException("getViewForPositionAndType returned"
+ + " a view that is ignored. You must call stopIgnoring before"
+ + " returning this view.");
+ }
+ }
+ }
+ if (holder == null) { // fallback to recycler
+ // try recycler.
+ // Head to the shared pool.
+ if (DEBUG) {
+ Log.d(TAG, "getViewForPosition(" + position + ") fetching from shared "
+ + "pool");
+ }
+ holder = getRecycledViewPool()
+ .getRecycledView(mAdapter.getItemViewType(offsetPosition));
+ if (holder != null) {
+ holder.resetInternal();
+ }
+ }
+ if (holder == null) {
+ holder = mAdapter.createViewHolder(RecyclerView.this,
+ mAdapter.getItemViewType(offsetPosition));
+ if (DEBUG) {
+ Log.d(TAG, "getViewForPosition created new ViewHolder");
+ }
+ }
}
- if (!holder.isBound() || holder.needsUpdate()) {
- if (DEBUG) {
- Log.d(TAG, "getViewForPosition unbound holder or needs update; updating...");
- }
- int offsetPosition = findPositionOffset(position);
- if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
- if (DEBUG) Log.d(TAG, "getViewForPosition: invalid position, returning null");
- return null;
- }
- // TODO: think through when getOffsetPosition() is called. I use it here because
- // existing views have already been offset appropriately through the mOldOffset
- // mechanism, but new views do not have this mechanism.
+ if (!holder.isRemoved() && (!holder.isBound() || holder.needsUpdate() ||
+ holder.isInvalid())) {
+ final int offsetPosition = mAdapterHelper.findPositionOffset(position);
mAdapter.bindViewHolder(holder, offsetPosition);
+ if (mState.isPreLayout()) {
+ holder.mPreLayoutPosition = position;
+ }
}
ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
@@ -2453,28 +2983,67 @@
recycleViewHolder(getChildViewHolderInt(view));
}
+ void recycleAndClearCachedViews() {
+ final int count = mCachedViews.size();
+ for (int i = count - 1; i >= 0; i--) {
+ tryToRecycleCachedViewAt(i);
+ }
+ mCachedViews.clear();
+ }
+
+ /**
+ * Tries to recyle a cached view and removes the view from the list if and only if it
+ * is recycled.
+ *
+ * @param cachedViewIndex The index of the view in cached views list
+ * @return True if item is recycled
+ */
+ boolean tryToRecycleCachedViewAt(int cachedViewIndex) {
+ if (DEBUG) {
+ Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
+ }
+ ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
+ if (DEBUG) {
+ Log.d(TAG, "CachedViewHolder to be recycled(if recycleable): " + viewHolder);
+ }
+ if (viewHolder.isRecyclable()) {
+ getRecycledViewPool().putRecycledView(viewHolder);
+ dispatchViewRecycled(viewHolder);
+ mCachedViews.remove(cachedViewIndex);
+ return true;
+ }
+ return false;
+ }
+
void recycleViewHolder(ViewHolder holder) {
if (holder.isScrap() || holder.itemView.getParent() != null) {
throw new IllegalArgumentException(
- "Scrapped or attached views may not be recycled.");
+ "Scrapped or attached views may not be recycled. isScrap:"
+ + holder.isScrap() + " isAttached:"
+ + (holder.itemView.getParent() != null));
}
- if (mCachedViews.size() < mViewCacheMax && !holder.isInvalid() &&
- (mInPreLayout || !holder.isRemoved())) {
+ if (holder.shouldIgnore()) {
+ throw new IllegalArgumentException("Trying to recycle an ignored view holder");
+ }
+
+ boolean cached = false;
+ if (!holder.isInvalid() && (mInPreLayout || !holder.isRemoved()) &&
+ !holder.isChanged()) {
// Retire oldest cached views first
if (mCachedViews.size() == mViewCacheMax && !mCachedViews.isEmpty()) {
for (int i = 0; i < mCachedViews.size(); i++) {
- final ViewHolder cachedView = mCachedViews.get(i);
- if (cachedView.isRecyclable()) {
- mCachedViews.remove(i);
- getRecycledViewPool().putRecycledView(cachedView);
- dispatchViewRecycled(cachedView);
+ if (tryToRecycleCachedViewAt(i)) {
break;
}
}
}
- mCachedViews.add(holder);
- } else if (holder.isRecyclable()) {
+ if (mCachedViews.size() < mViewCacheMax) {
+ mCachedViews.add(holder);
+ cached = true;
+ }
+ }
+ if (!cached && holder.isRecyclable()) {
getRecycledViewPool().putRecycledView(holder);
dispatchViewRecycled(holder);
}
@@ -2492,6 +3061,7 @@
void quickRecycleScrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
holder.mScrapContainer = null;
+ holder.clearReturnedFromScrapFlag();
recycleViewHolder(holder);
}
@@ -2506,8 +3076,20 @@
*/
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
+ if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
+ throw new IllegalArgumentException("Called scrap view with an invalid view."
+ + " Invalid views cannot be reused from scrap, they should rebound from"
+ + " recycler pool.");
+ }
holder.setScrapContainer(this);
- mAttachedScrap.add(holder);
+ if (!holder.isChanged() || !supportsChangeAnimations()) {
+ mAttachedScrap.add(holder);
+ } else {
+ if (mChangedScrap == null) {
+ mChangedScrap = new ArrayList<ViewHolder>();
+ }
+ mChangedScrap.add(holder);
+ }
}
/**
@@ -2517,8 +3099,13 @@
* until it is explicitly removed and recycled.</p>
*/
void unscrapView(ViewHolder holder) {
- mAttachedScrap.remove(holder);
+ if (!holder.isChanged() || !supportsChangeAnimations() || mChangedScrap == null) {
+ mAttachedScrap.remove(holder);
+ } else {
+ mChangedScrap.remove(holder);
+ }
holder.mScrapContainer = null;
+ holder.clearReturnedFromScrapFlag();
}
int getScrapCount() {
@@ -2533,32 +3120,36 @@
mAttachedScrap.clear();
}
- ViewHolder getScrapViewForPosition(int position, int type) {
+ /**
+ * Returns a scrap view for the position. If type is not INVALID_TYPE, it also checks if
+ * ViewHolder's type matches the provided type.
+ *
+ * @param position Item position
+ * @param type View type
+ * @param dryRun Does a dry run, finds the ViewHolder but does not remove
+ * @return a ViewHolder that can be re-used for this position.
+ */
+ ViewHolder getScrapViewForPosition(int position, int type, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
- if (holder.getPosition() == position && !holder.isInvalid() &&
- (mInPreLayout || !holder.isRemoved())) {
- if (holder.getItemViewType() != type) {
+ if (!holder.wasReturnedFromScrap() && holder.getPosition() == position
+ && !holder.isInvalid() && (mInPreLayout || !holder.isRemoved())) {
+ if (type != INVALID_TYPE && holder.getItemViewType() != type) {
Log.e(TAG, "Scrap view for position " + position + " isn't dirty but has" +
" wrong view type! (found " + holder.getItemViewType() +
" but expected " + type + ")");
break;
}
- mAttachedScrap.remove(i);
- holder.setScrapContainer(null);
- if (DEBUG) {
- Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type +
- ") found exact match in scrap: " + holder);
- }
+ holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
- if (mNumAnimatingViews != 0) {
- View view = getAnimatingView(position, type);
+ if (!dryRun) {
+ View view = mChildHelper.findHiddenNonRemovedView(position, type);
if (view != null) {
// ending the animation should cause it to get recycled before we reuse it
mItemAnimator.endAnimation(getChildViewHolder(view));
@@ -2569,26 +3160,11 @@
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
- if (holder.getPosition() == position) {
- mCachedViews.remove(i);
- if (holder.isInvalid() && holder.getItemViewType() != type) {
- // Can't use it. We don't know where it's been.
- if (DEBUG) {
- Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type +
- ") found position match, but holder is invalid with type " +
- holder.getItemViewType());
- }
-
- if (holder.isRecyclable()) {
- getRecycledViewPool().putRecycledView(holder);
- }
- // Even if the holder wasn't officially recycleable, dispatch that it
- // was recycled anyway in case there are resources to unbind.
- dispatchViewRecycled(holder);
-
- // Drop out of the cache search and try something else instead,
- // we won't find another match here.
- break;
+ // invalid view holders may be in cache if adapter has stable ids as they can be
+ // retrieved via getScrapViewForId
+ if (!holder.isInvalid() && holder.getPosition() == position) {
+ if (!dryRun) {
+ mCachedViews.remove(i);
}
if (DEBUG) {
Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type +
@@ -2597,45 +3173,57 @@
return holder;
}
}
-
- // Give up. Head to the shared pool.
- if (DEBUG) {
- Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type +
- ") fetching from shared pool");
- }
- return getRecycledViewPool().getRecycledView(type);
+ return null;
}
- ViewHolder getScrapViewForId(long id, int type) {
+ ViewHolder getScrapViewForId(long id, int type, boolean dryRun) {
// Look in our attached views first
final int count = mAttachedScrap.size();
- for (int i = 0; i < count; i++) {
+ for (int i = count - 1; i >= 0; i--) {
final ViewHolder holder = mAttachedScrap.get(i);
- if (holder.getItemId() == id) {
+ if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
if (type == holder.getItemViewType()) {
- mAttachedScrap.remove(i);
- holder.setScrapContainer(null);
+ holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
+ if (holder.isRemoved()) {
+ // this might be valid in two cases:
+ // > item is removed but we are in pre-layout pass
+ // >> do nothing. return as is. make sure we don't rebind
+ // > item is removed then added to another position and we are in
+ // post layout.
+ // >> remove removed and invalid flags, add update flag to rebind
+ // because item was invisible to us and we don't know what happened in
+ // between.
+ if (!mState.isPreLayout()) {
+ holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE |
+ ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
+ }
+ }
return holder;
- } else {
- break;
+ } else if (!dryRun) {
+ // Recycle this scrap. Type mismatch.
+ mAttachedScrap.remove(i);
+ removeDetachedView(holder.itemView, false);
+ quickRecycleScrapView(holder.itemView);
}
}
}
// Search the first-level cache
final int cacheSize = mCachedViews.size();
- for (int i = 0; i < cacheSize; i++) {
+ for (int i = cacheSize - 1; i >= 0; i--) {
final ViewHolder holder = mCachedViews.get(i);
if (holder.getItemId() == id) {
- mCachedViews.remove(i);
- return holder;
+ if (type == holder.getItemViewType()) {
+ if (!dryRun) {
+ mCachedViews.remove(i);
+ }
+ return holder;
+ } else if (!dryRun) {
+ tryToRecycleCachedViewAt(i);
+ }
}
}
-
- // That didn't work, look for an unordered view of the right type instead.
- // The holder's position won't match so the calling code will need to have
- // the adapter rebind it.
- return getRecycledViewPool().getRecycledView(type);
+ return null;
}
void dispatchViewRecycled(ViewHolder holder) {
@@ -2645,6 +3233,9 @@
if (mAdapter != null) {
mAdapter.onViewRecycled(holder);
}
+ if (mState != null) {
+ mState.onViewRecycled(holder);
+ }
if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder);
}
@@ -2662,12 +3253,18 @@
Log.d(TAG, "offsetPositionRecordsForInsert cached " + i + " holder " +
holder + " now at position " + (holder.mPosition + count));
}
- holder.offsetPosition(count);
+ holder.offsetPosition(count, true);
}
}
}
- void offsetPositionRecordsForRemove(int removedFrom, int count) {
+ /**
+ * @param removedFrom Remove start index
+ * @param count Remove count
+ * @param applyToPreLayout If true, changes will affect ViewHolder's pre-layout position, if
+ * false, they'll be applied before the second layout pass
+ */
+ void offsetPositionRecordsForRemove(int removedFrom, int count, boolean applyToPreLayout) {
final int removedEnd = removedFrom + count;
final int cachedCount = mCachedViews.size();
for (int i = cachedCount - 1; i >= 0; i--) {
@@ -2679,21 +3276,31 @@
" holder " + holder + " now at position " +
(holder.mPosition - count));
}
- holder.offsetPosition(-count);
+ holder.offsetPosition(-count, applyToPreLayout);
} else if (holder.getPosition() >= removedFrom) {
// Item for this view was removed. Dump it from the cache.
- if (DEBUG) {
+ if (!tryToRecycleCachedViewAt(i)) {
+ // if we cannot recycle it, at least invalidate so that we won't return
+ // it by position.
+ holder.addFlags(ViewHolder.FLAG_INVALID);
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForRemove cached " + i +
+ " holder " + holder + " now flagged as invalid because it "
+ + "could not be recycled");
+ }
+ } else if (DEBUG) {
Log.d(TAG, "offsetPositionRecordsForRemove cached " + i +
" holder " + holder + " now placed in pool");
}
- mCachedViews.remove(i);
- getRecycledViewPool().putRecycledView(holder);
- dispatchViewRecycled(holder);
}
}
}
}
+ void setViewCacheExtension(ViewCacheExtension extension) {
+ mViewCacheExtension = extension;
+ }
+
void setRecycledViewPool(RecycledViewPool pool) {
if (mRecyclerPool != null) {
mRecyclerPool.detach();
@@ -2711,34 +3318,6 @@
return mRecyclerPool;
}
- ViewHolder findViewHolderForPosition(int position) {
- final int cachedCount = mCachedViews.size();
- for (int i = 0; i < cachedCount; i++) {
- final ViewHolder holder = mCachedViews.get(i);
- if (holder != null && holder.getPosition() == position) {
- mCachedViews.remove(i);
- return holder;
- }
- }
- return null;
- }
-
- ViewHolder findViewHolderForItemId(long id) {
- if (!mAdapter.hasStableIds()) {
- return null;
- }
-
- final int cachedCount = mCachedViews.size();
- for (int i = 0; i < cachedCount; i++) {
- final ViewHolder holder = mCachedViews.get(i);
- if (holder != null && holder.getItemId() == id) {
- mCachedViews.remove(i);
- return holder;
- }
- }
- return null;
- }
-
void viewRangeUpdate(int positionStart, int itemCount) {
final int positionEnd = positionStart + itemCount;
final int cachedCount = mCachedViews.size();
@@ -2751,18 +3330,34 @@
final int pos = holder.getPosition();
if (pos >= positionStart && pos < positionEnd) {
holder.addFlags(ViewHolder.FLAG_UPDATE);
+ if (supportsChangeAnimations()) {
+ holder.addFlags(ViewHolder.FLAG_CHANGED);
+ }
}
}
}
void markKnownViewsInvalid() {
- final int cachedCount = mCachedViews.size();
- for (int i = 0; i < cachedCount; i++) {
- final ViewHolder holder = mCachedViews.get(i);
- if (holder != null) {
- holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+ if (mAdapter != null && mAdapter.hasStableIds()) {
+ final int cachedCount = mCachedViews.size();
+ for (int i = 0; i < cachedCount; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder != null) {
+ holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+ }
+ }
+ } else {
+ // we cannot re-use cached views in this case. Recycle the ones we can and flag
+ // the remaining as invalid so that they can be recycled later on (when their
+ // animations end.)
+ for (int i = mCachedViews.size() - 1; i >= 0; i--) {
+ if (!tryToRecycleCachedViewAt(i)) {
+ final ViewHolder holder = mCachedViews.get(i);
+ holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+ }
}
}
+
}
void clearOldPositions() {
@@ -2775,6 +3370,40 @@
}
/**
+ * ViewCacheExtension is a helper class to provide an additional layer of view caching that can
+ * ben controlled by the developer.
+ * <p>
+ * When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
+ * first level cache to find a matching View. If it cannot find a suitable View, Recycler will
+ * call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
+ * {@link RecycledViewPool}.
+ * <p>
+ * Note that, Recycler never sends Views to this method to be cached. It is developers
+ * responsibility to decide whether they want to keep their Views in this custom cache or let
+ * the default recycling policy handle it.
+ */
+ public abstract static class ViewCacheExtension {
+
+ /**
+ * Returns a View that can be binded to the given Adapter position.
+ * <p>
+ * This method should <b>not</b> create a new View. Instead, it is expected to return
+ * an already created View that can be re-used for the given type and position.
+ * If the View is marked as ignored, it should first call
+ * {@link LayoutManager#stopIgnoringView(View)} before returning the View.
+ * <p>
+ * RecyclerView will re-bind the returned View to the position if necessary.
+ *
+ * @param recycler The Recycler that can be used to bind the View
+ * @param position The adapter position
+ * @param type The type of the View, defined by adapter
+ * @return A View that is bound to the given position or NULL if there is no View to re-use
+ * @see LayoutManager#ignoreView(View)
+ */
+ abstract public View getViewForPositionAndType(Recycler recycler, int position, int type);
+ }
+
+ /**
* Base class for an Adapter
*
* <p>Adapters provide a binding from an app-specific data set to views that are displayed
@@ -3079,11 +3708,25 @@
* layout managers are provided for general use.
*/
public static abstract class LayoutManager {
+ ChildHelper mChildHelper;
RecyclerView mRecyclerView;
@Nullable
SmoothScroller mSmoothScroller;
+ private boolean mRequestedSimpleAnimations = false;
+
+ void setRecyclerView(RecyclerView recyclerView) {
+ if (recyclerView == null) {
+ mRecyclerView = null;
+ mChildHelper = null;
+ } else {
+ mRecyclerView = recyclerView;
+ mChildHelper = recyclerView.mChildHelper;
+ }
+
+ }
+
/**
* Calls {@code RecyclerView#requestLayout} on the underlying RecyclerView
*/
@@ -3094,20 +3737,52 @@
}
/**
+ * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+ * {@link IllegalStateException} if it <b>is not</b>.
+ *
+ * @param message The message for the exception. Can be null.
+ * @see #assertNotInLayoutOrScroll(String)
+ */
+ public void assertInLayoutOrScroll(String message) {
+ if (mRecyclerView != null) {
+ mRecyclerView.assertInLayoutOrScroll(message);
+ }
+ }
+
+ /**
+ * Checks if RecyclerView is in the middle of a layout or scroll and throws an
+ * {@link IllegalStateException} if it <b>is</b>.
+ *
+ * @param message The message for the exception. Can be null.
+ * @see #assertInLayoutOrScroll(String)
+ */
+ public void assertNotInLayoutOrScroll(String message) {
+ if (mRecyclerView != null) {
+ mRecyclerView.assertNotInLayoutOrScroll(message);
+ }
+ }
+
+ /**
* Returns whether this LayoutManager supports automatic item animations.
* A LayoutManager wishing to support item animations should obey certain
- * rules as outlined in {@link #onLayout(boolean, int, int, int, int)}.
+ * rules as outlined in {@link #onLayoutChildren(Recycler, State)}.
* The default return value is <code>false</code>, so subclasses of LayoutManager
- * will not get automatic item animations by default.
+ * will not get predictive item animations by default.
*
- * Whether item animations are enabled in a RecyclerView is determined both
+ * <p>Whether item animations are enabled in a RecyclerView is determined both
* by the return value from this method and the
* {@link RecyclerView#setItemAnimator(ItemAnimator) ItemAnimator} set on the
- * RecyclerView itself.
+ * RecyclerView itself. If the RecyclerView has a non-null ItemAnimator but this
+ * method returns false, then simple item animations will be enabled, in which
+ * views that are moving onto or off of the screen are simply faded in/out. If
+ * the RecyclerView has a non-null ItemAnimator and this method returns true,
+ * then there will be two calls to {@link #onLayoutChildren(Recycler, State)} to
+ * setup up the information needed to more intelligently predict where appearing
+ * and disappearing views should be animated from/to.</p>
*
- * @return true if automatic item animations should be enabled, false otherwise
+ * @return true if predictive item animations should be enabled, false otherwise
*/
- public boolean supportsItemAnimations() {
+ public boolean supportsPredictiveItemAnimations() {
return false;
}
@@ -3124,6 +3799,15 @@
}
/**
+ * @deprecated
+ * override {@link #onDetachedFromWindow(RecyclerView, Recycler)}
+ */
+ @Deprecated
+ public void onDetachedFromWindow(RecyclerView view) {
+
+ }
+
+ /**
* Called when this LayoutManager is detached from its parent RecyclerView or when
* its parent RecyclerView is detached from its window.
*
@@ -3131,30 +3815,48 @@
* </p>
*
* @param view The RecyclerView this LayoutManager is bound to
+ * @param recycler The recycler to use if you prefer to recycle your children instead of
+ * keeping them around.
*/
- public void onDetachedFromWindow(RecyclerView view) {
+ public void onDetachedFromWindow(RecyclerView view, Recycler recycler) {
+ onDetachedFromWindow(view);
}
/**
* Lay out all relevant child views from the given adapter.
*
- * <p>Special care should be taken when automatic animations of added/removed items
- * are desired, which is the default behavior of RecyclerView (see
- * {@link #setItemAnimator(ItemAnimator)}). When there is an itemAnimator set on
- * the RecyclerView, there will be two calls to onLayoutChildren(). The first such
- * call is intended to get the "pre layout" information for items which will come
- * info view as a result of the real layout (referred to as APPEARING views).
- * Views will remember their old pre-layout position to allow this pre-layout
- * to happen correctly. The only requirement for this layout pass
- * is that any items which are marked {@link LayoutParams#isItemRemoved() removed} should be
- * positioned and laid out with the other views, but not added to the children.
- * Similarly, the other views should be positioned appropriately, taking into account
- * any removed views. Also, the bounds within which the views are being laid out
- * should be extended by these removed view sizes, to allow layout to continue until
- * all views can be laid out.</p>
+ * The LayoutManager is in charge of the behavior of item animations. By default,
+ * RecyclerView has a non-null {@link #getItemAnimator() ItemAnimator}, and simple
+ * item animations are enabled. This means that add/remove operations on the
+ * adapter will result in animations to add new or appearing items, removed or
+ * disappearing items, and moved items. If a LayoutManager returns false from
+ * {@link #supportsPredictiveItemAnimations()}, which is the default, and runs a
+ * normal layout operation during {@link #onLayoutChildren(Recycler, State)}, the
+ * RecyclerView will have enough information to run those animations in a simple
+ * way. For example, the default ItemAnimator, {@link DefaultItemAnimator}, will
+ * simple fade views in and out, whether they are actuall added/removed or whether
+ * they are moved on or off the screen due to other add/remove operations.
+ *
+ * <p>A LayoutManager wanting a better item animation experience, where items can be
+ * animated onto and off of the screen according to where the items exist when they
+ * are not on screen, then the LayoutManager should return true from
+ * {@link #supportsPredictiveItemAnimations()} and add additional logic to
+ * {@link #onLayoutChildren(Recycler, State)}. Supporting predictive animations
+ * means that {@link #onLayoutChildren(Recycler, State)} will be called twice;
+ * once as a "pre" layout step to determine where items would have been prior to
+ * a real layout, and again to do the "real" layout. In the pre-layout phase,
+ * items will remember their pre-layout positions to allow them to be laid out
+ * appropriately. Also, {@link LayoutParams#isItemRemoved() removed} items will
+ * be returned from the scrap to help determine correct placement of other items.
+ * These removed items should not be added to the child list, but should be used
+ * to help calculate correct positioning of other views, including views that
+ * were not previously onscreen (referred to as APPEARING views), but whose
+ * pre-layout offscreen position can be determined given the extra
+ * information about the pre-layout removed views.</p>
*
* <p>The second layout pass is the real layout in which only non-removed views
- * will be used. The only additional requirement during this pass is to note which
+ * will be used. The only additional requirement during this pass is, if
+ * {@link #supportsPredictiveItemAnimations()} returns true, to note which
* views exist in the child list prior to layout and which are not there after
* layout (referred to as DISAPPEARING views), and to position/layout those views
* appropriately, without regard to the actual bounds of the RecyclerView. This allows
@@ -3164,7 +3866,8 @@
* <p>The default LayoutManager implementations for RecyclerView handle all of these
* requirements for animations already. Clients of RecyclerView can either use one
* of these layout managers directly or look at their implementations of
- * onLayoutChildren() to see how they account for the APPEARING and DISAPPEARING views.</p>
+ * onLayoutChildren() to see how they account for the APPEARING and
+ * DISAPPEARING views.</p>
*
* @param recycler Recycler to use for fetching potentially cached views for a
* position
@@ -3361,37 +4064,53 @@
}
/**
- * Add a view to the currently attached RecyclerView if needed. LayoutManagers should
- * use this method to add views obtained from a {@link Recycler} using
- * {@link Recycler#getViewForPosition(int)}.
+ * Ends all animations on the view created by the {@link ItemAnimator}.
*
- * @param child View to add
- * @param index Index to add child at
+ * @param view The View for which the animations should be ended.
+ * @see RecyclerView.ItemAnimator#endAnimations()
*/
- public void addView(View child, int index) {
- final ViewHolder holder = getChildViewHolderInt(child);
- if (holder.isScrap()) {
- holder.unScrap();
- mRecyclerView.attachViewToParent(child, index, child.getLayoutParams());
- if (DISPATCH_TEMP_DETACH) {
- ViewCompat.dispatchFinishTemporaryDetach(child);
- }
- } else {
- mRecyclerView.addView(child, index);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- lp.mInsetsDirty = true;
- final Adapter adapter = mRecyclerView.getAdapter();
- if (adapter != null) {
- adapter.onViewAttachedToWindow(getChildViewHolderInt(child));
- }
- mRecyclerView.onChildAttachedToWindow(child);
- if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
- mSmoothScroller.onChildAttachedToWindow(child);
- }
+ public void endAnimation(View view) {
+ if (mRecyclerView.mItemAnimator != null) {
+ mRecyclerView.mItemAnimator.endAnimation(getChildViewHolderInt(view));
}
}
/**
+ * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view
+ * to the layout that is known to be going away, either because it has been
+ * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the
+ * visible portion of the container but is being laid out in order to inform RecyclerView
+ * in how to animate the item out of view.
+ * <p>
+ * Views added via this method are going to be invisible to LayoutManager after the
+ * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)}
+ * or won't be included in {@link #getChildCount()} method.
+ *
+ * @param child View to add and then remove with animation.
+ */
+ public void addDisappearingView(View child) {
+ addDisappearingView(child, -1);
+ }
+
+ /**
+ * To be called only during {@link #onLayoutChildren(Recycler, State)} to add a view
+ * to the layout that is known to be going away, either because it has been
+ * {@link Adapter#notifyItemRemoved(int) removed} or because it is actually not in the
+ * visible portion of the container but is being laid out in order to inform RecyclerView
+ * in how to animate the item out of view.
+ * <p>
+ * Views added via this method are going to be invisible to LayoutManager after the
+ * dispatchLayout pass is complete. They cannot be retrieved via {@link #getChildAt(int)}
+ * or won't be included in {@link #getChildCount()} method.
+ *
+ * @param child View to add and then remove with animation.
+ * @param index Index of the view.
+ */
+ public void addDisappearingView(View child, int index) {
+ addViewInt(child, index, true);
+ }
+
+ /**
* Add a view to the currently attached RecyclerView if needed. LayoutManagers should
* use this method to add views obtained from a {@link Recycler} using
* {@link Recycler#getViewForPosition(int)}.
@@ -3399,11 +4118,71 @@
* @param child View to add
*/
public void addView(View child) {
- if (mRecyclerView.mAnimatingViewIndex >= 0) {
- addView(child, mRecyclerView.mAnimatingViewIndex);
- mRecyclerView.mAnimatingViewIndex++;
+ addView(child, -1);
+ }
+
+ /**
+ * Add a view to the currently attached RecyclerView if needed. LayoutManagers should
+ * use this method to add views obtained from a {@link Recycler} using
+ * {@link Recycler#getViewForPosition(int)}.
+ *
+ * @param child View to add
+ * @param index Index to add child at
+ */
+ public void addView(View child, int index) {
+ addViewInt(child, index, false);
+ }
+
+ private void addViewInt(View child, int index, boolean disappearing) {
+ final ViewHolder holder = getChildViewHolderInt(child);
+ if (disappearing || holder.isRemoved()) {
+ // these views will be hidden at the end of the layout pass.
+ mRecyclerView.mDisappearingViewsInLayoutPass.add(child);
} else {
- addView(child, -1);
+ // This may look like unnecessary but may happen if layout manager supports
+ // predictive layouts and adapter removed then re-added the same item.
+ // In this case, added version will be visible in the post layout (because add is
+ // deferred) but RV will still bind it to the same View.
+ // So if a View re-appears in post layout pass, remove it from disappearing list.
+ mRecyclerView.mDisappearingViewsInLayoutPass.remove(child);
+ }
+
+ if (holder.wasReturnedFromScrap() || holder.isScrap()) {
+ if (holder.isScrap()) {
+ holder.unScrap();
+ } else {
+ holder.clearReturnedFromScrapFlag();
+ }
+ mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
+ if (DISPATCH_TEMP_DETACH) {
+ ViewCompat.dispatchFinishTemporaryDetach(child);
+ }
+ } else if (child.getParent() == mRecyclerView) { // it was not a scrap but a valid child
+ // ensure in correct position
+ int currentIndex = mChildHelper.indexOfChild(child);
+ if (index == -1) {
+ index = mChildHelper.getChildCount();
+ }
+ if (currentIndex == -1) {
+ throw new IllegalStateException("Added View has RecyclerView as parent but"
+ + " view is not a real child. Unfiltered index:"
+ + mRecyclerView.indexOfChild(child));
+ }
+ if (currentIndex != index) {
+ mRecyclerView.mLayout.moveView(currentIndex, index);
+ }
+ } else {
+ mChildHelper.addView(child, index, false);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ lp.mInsetsDirty = true;
+ final Adapter adapter = mRecyclerView.getAdapter();
+ if (adapter != null) {
+ adapter.onViewAttachedToWindow(getChildViewHolderInt(child));
+ }
+ mRecyclerView.onChildAttachedToWindow(child);
+ if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
+ mSmoothScroller.onChildAttachedToWindow(child);
+ }
}
}
@@ -3421,10 +4200,7 @@
adapter.onViewDetachedFromWindow(getChildViewHolderInt(child));
}
mRecyclerView.onChildDetachedFromWindow(child);
- mRecyclerView.removeView(child);
- if (mRecyclerView.mAnimatingViewIndex >= 0) {
- mRecyclerView.mAnimatingViewIndex--;
- }
+ mChildHelper.removeView(child);
}
/**
@@ -3436,17 +4212,14 @@
* @param index Index of the child view to remove
*/
public void removeViewAt(int index) {
- final View child = mRecyclerView.getChildAt(index);
+ final View child = getChildAt(index);
if (child != null) {
final Adapter adapter = mRecyclerView.getAdapter();
if (adapter != null) {
adapter.onViewDetachedFromWindow(getChildViewHolderInt(child));
}
mRecyclerView.onChildDetachedFromWindow(child);
- mRecyclerView.removeViewAt(index);
- if (mRecyclerView.mAnimatingViewIndex >= 0) {
- mRecyclerView.mAnimatingViewIndex--;
- }
+ mChildHelper.removeViewAt(index);
}
}
@@ -3457,19 +4230,18 @@
public void removeAllViews() {
final Adapter adapter = mRecyclerView.getAdapter();
// Only remove non-animating views
- final int childCount = mRecyclerView.getChildCount() - mRecyclerView.mNumAnimatingViews;
- if (adapter != null) {
- for (int i = 0; i < childCount; i++) {
- final View child = mRecyclerView.getChildAt(i);
+ final int childCount = getChildCount();
+
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (adapter != null) {
adapter.onViewDetachedFromWindow(getChildViewHolderInt(child));
- mRecyclerView.onChildDetachedFromWindow(child);
}
+ mRecyclerView.onChildDetachedFromWindow(child);
}
+
for (int i = childCount - 1; i >= 0; i--) {
- mRecyclerView.removeViewAt(i);
- if (mRecyclerView.mAnimatingViewIndex >= 0) {
- mRecyclerView.mAnimatingViewIndex--;
- }
+ mChildHelper.removeViewAt(i);
}
}
@@ -3484,6 +4256,16 @@
}
/**
+ * Returns the View type defined by the adapter.
+ *
+ * @param view The view to query
+ * @return The type of the view assigned by the adapter.
+ */
+ public int getItemViewType(View view) {
+ return getChildViewHolderInt(view).getItemViewType();
+ }
+
+ /**
* <p>Finds the view which represents the given adapter position.</p>
* <p>This method traverses each child since it has no information about child order.
* Override this method to improve performance if your LayoutManager keeps data about
@@ -3497,7 +4279,12 @@
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
- if (getPosition(child) == position) {
+ ViewHolder vh = getChildViewHolderInt(child);
+ if (vh == null) {
+ continue;
+ }
+ if (vh.getPosition() == position &&
+ (mRecyclerView.mState.isPreLayout() || !vh.isRemoved())) {
return child;
}
}
@@ -3543,12 +4330,9 @@
*/
public void detachViewAt(int index) {
if (DISPATCH_TEMP_DETACH) {
- ViewCompat.dispatchStartTemporaryDetach(mRecyclerView.getChildAt(index));
+ ViewCompat.dispatchStartTemporaryDetach(getChildAt(index));
}
- mRecyclerView.detachViewFromParent(index);
- if (mRecyclerView.mAnimatingViewIndex >= 0) {
- --mRecyclerView.mAnimatingViewIndex;
- }
+ mChildHelper.detachViewFromParent(index);
}
/**
@@ -3561,10 +4345,13 @@
* @param lp LayoutParams for child
*/
public void attachView(View child, int index, LayoutParams lp) {
- mRecyclerView.attachViewToParent(child, index, lp);
- if (mRecyclerView.mAnimatingViewIndex >= 0) {
- ++mRecyclerView.mAnimatingViewIndex;
+ ViewHolder vh = getChildViewHolderInt(child);
+ if (vh.isRemoved()) {
+ mRecyclerView.mDisappearingViewsInLayoutPass.add(child);
+ } else {
+ mRecyclerView.mDisappearingViewsInLayoutPass.remove(child);
}
+ mChildHelper.attachViewToParent(child, index, lp, vh.isRemoved());
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchFinishTemporaryDetach(child);
}
@@ -3604,6 +4391,22 @@
}
/**
+ * Moves a View from one position to another.
+ *
+ * @param fromIndex The View's initial index
+ * @param toIndex The View's target index
+ */
+ public void moveView(int fromIndex, int toIndex) {
+ View view = getChildAt(fromIndex);
+ if (view == null) {
+ throw new IllegalArgumentException("Cannot move a child from non-existing index:"
+ + fromIndex);
+ }
+ detachViewAt(fromIndex);
+ attachView(view, toIndex);
+ }
+
+ /**
* Detach a child view and add it to a {@link Recycler Recycler's} scrap heap.
*
* <p>Scrapping a view allows it to be rebound and reused to show updated or
@@ -3613,8 +4416,8 @@
* @param recycler Recycler to deposit the new scrap view into
*/
public void detachAndScrapView(View child, Recycler recycler) {
- detachView(child);
- recycler.scrapView(child);
+ int index = mChildHelper.indexOfChild(child);
+ scrapOrRecycleView(recycler, index, child);
}
/**
@@ -3628,8 +4431,7 @@
*/
public void detachAndScrapViewAt(int index, Recycler recycler) {
final View child = getChildAt(index);
- detachViewAt(index);
- recycler.scrapView(child);
+ scrapOrRecycleView(recycler, index, child);
}
/**
@@ -3662,8 +4464,7 @@
* @return Number of attached children
*/
public int getChildCount() {
- return mRecyclerView != null ?
- mRecyclerView.getChildCount() - mRecyclerView.mNumAnimatingViews : 0;
+ return mChildHelper != null ? mChildHelper.getChildCount() : 0;
}
/**
@@ -3672,7 +4473,7 @@
* @return Child view at index
*/
public View getChildAt(int index) {
- return mRecyclerView != null ? mRecyclerView.getChildAt(index) : null;
+ return mChildHelper != null ? mChildHelper.getChildAt(index) : null;
}
/**
@@ -3802,6 +4603,39 @@
}
/**
+ * Flags a view so that it will not be scrapped or recycled.
+ * <p>
+ * Note that View is still a child of the ViewGroup. If LayoutManager calls methods like
+ * {@link LayoutManager#offsetChildrenHorizontal(int)}, this View will be offset as well.
+ *
+ * @param view View to ignore.
+ */
+ public void ignoreView(View view) {
+ if (view.getParent() != mRecyclerView || mRecyclerView.indexOfChild(view) == -1) {
+ // checking this because calling this method on a recycled or detached view may
+ // cause loss of state.
+ throw new IllegalArgumentException("View should be fully attached to be ignored");
+ }
+ final ViewHolder vh = getChildViewHolderInt(view);
+ vh.addFlags(ViewHolder.FLAG_IGNORE);
+ mRecyclerView.mState.onViewIgnored(vh);
+ }
+
+ /**
+ * View can be scrapped and recycled again.
+ * <p>
+ * Note that calling this method removes all information in the view holder.
+ *
+ * @param view View to ignore.
+ */
+ public void stopIgnoringView(View view) {
+ final ViewHolder vh = getChildViewHolderInt(view);
+ vh.stopIgnoring();
+ vh.resetInternal();
+ vh.addFlags(ViewHolder.FLAG_INVALID);
+ }
+
+ /**
* Temporarily detach and scrap all currently attached child views. Views will be scrapped
* into the given Recycler. The Recycler may prefer to reuse scrap views before
* other views that were previously recycled.
@@ -3812,19 +4646,55 @@
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
- detachViewAt(i);
- recycler.scrapView(v);
+ scrapOrRecycleView(recycler, i, v);
}
}
- void removeAndRecycleScrapInt(Recycler recycler) {
+ private void scrapOrRecycleView(Recycler recycler, int index, View view) {
+ final ViewHolder viewHolder = getChildViewHolderInt(view);
+ if (viewHolder.shouldIgnore()) {
+ if (DEBUG) {
+ Log.d(TAG, "ignoring view " + viewHolder);
+ }
+ return;
+ }
+ if (viewHolder.isInvalid() && !viewHolder.isRemoved() &&
+ !mRecyclerView.mAdapter.hasStableIds()) {
+ removeViewAt(index);
+ recycler.recycleView(view);
+ } else {
+ detachViewAt(index);
+ recycler.scrapView(view);
+ }
+ }
+
+ /**
+ * Recycles the scrapped views.
+ * <p>
+ * When a view is detached and removed, it does not trigger a ViewGroup invalidate. This is
+ * the expected behavior if scrapped views are used for animations. Otherwise, we need to
+ * call remove and invalidate RecyclerView to ensure UI update.
+ *
+ * @param recycler Recycler
+ * @param remove Whether scrapped views should be removed from ViewGroup or not. This
+ * method will invalidate RecyclerView if it removes any scrapped child.
+ */
+ void removeAndRecycleScrapInt(Recycler recycler, boolean remove) {
final int scrapCount = recycler.getScrapCount();
for (int i = 0; i < scrapCount; i++) {
final View scrap = recycler.getScrapViewAt(i);
- ViewHolder holder = mRecyclerView.getChildViewHolder(scrap);
+ if (getChildViewHolderInt(scrap).shouldIgnore()) {
+ continue;
+ }
+ if (remove) {
+ mRecyclerView.removeDetachedView(scrap, false);
+ }
recycler.quickRecycleScrapView(scrap);
}
recycler.clearScrap();
+ if (remove && scrapCount > 0) {
+ mRecyclerView.invalidate();
+ }
}
@@ -4133,6 +5003,14 @@
}
/**
+ * @deprecated Use {@link #onRequestChildFocus(RecyclerView, State, View, View)}
+ */
+ @Deprecated
+ public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
+ return false;
+ }
+
+ /**
* Called when a descendant view of the RecyclerView requests focus.
*
* <p>A LayoutManager wishing to keep focused views aligned in a specific
@@ -4142,13 +5020,15 @@
* behavior of scrolling the focused child on screen instead of running alongside it,
* this method should return true.</p>
*
- * @param parent The RecyclerView hosting this LayoutManager
- * @param child Direct child of the RecyclerView containing the newly focused view
+ * @param parent The RecyclerView hosting this LayoutManager
+ * @param state Current state of RecyclerView
+ * @param child Direct child of the RecyclerView containing the newly focused view
* @param focused The newly focused view. This may be the same view as child
* @return true if the default scroll behavior should be suppressed
*/
- public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
- return false;
+ public boolean onRequestChildFocus(RecyclerView parent, State state, View child,
+ View focused) {
+ return onRequestChildFocus(parent, child, focused);
}
/**
@@ -4196,6 +5076,15 @@
}
/**
+ * Called when {@link Adapter#notifyDataSetChanged()} is triggered instead of giving
+ * detailed information on what has actually changed.
+ *
+ * @param recyclerView
+ */
+ public void onItemsChanged(RecyclerView recyclerView) {
+ }
+
+ /**
* Called when items have been added to the adapter. The LayoutManager may choose to
* requestLayout if the inserted items would require refreshing the currently visible set
* of child views. (e.g. currently empty space would be filled by appended items, etc.)
@@ -4217,6 +5106,16 @@
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
}
+ /**
+ * Called when items have been changed in the adapter.
+ *
+ * @param recyclerView
+ * @param positionStart
+ * @param itemCount
+ */
+ public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
+ }
+
/**
* <p>Override this method if you want to support scroll bars.</p>
@@ -4317,10 +5216,12 @@
* as UNSPECIFIED. AT_MOST measurements will be treated as EXACTLY and the RecyclerView
* will consume all available space.</p>
*
+ * @param recycler Recycler
+ * @param state Transient state of RecyclerView
* @param widthSpec Width {@link android.view.View.MeasureSpec}
* @param heightSpec Height {@link android.view.View.MeasureSpec}
*/
- public void onMeasure(int widthSpec, int heightSpec) {
+ public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final int widthSize = MeasureSpec.getSize(widthSpec);
@@ -4408,6 +5309,43 @@
mSmoothScroller = null;
}
}
+
+ /**
+ * RecyclerView calls this method to notify LayoutManager that scroll state has changed.
+ *
+ * @param state The new scroll state for RecyclerView
+ */
+ public void onScrollStateChanged(int state) {
+ }
+
+ /**
+ * Removes all views and recycles them using the given recycler.
+ * <p>
+ * If you want to clean cached views as well, you should call {@link Recycler#clear()} too.
+ *
+ * @param recycler Recycler to use to recycle children
+ * @see #removeAndRecycleView(View, Recycler)
+ * @see #removeAndRecycleViewAt(int, Recycler)
+ */
+ public void removeAndRecycleAllViews(Recycler recycler) {
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ removeAndRecycleViewAt(i, recycler);
+ }
+ }
+
+ /**
+ * A LayoutManager can call this method to force RecyclerView to run simple animations in
+ * the next layout pass, even if there is not any trigger to do so. (e.g. adapter data
+ * change).
+ * <p>
+ * Note that, calling this method will not guarantee that RecyclerView will run animations
+ * at all. For example, if there is not any {@link ItemAnimator} set, RecyclerView will
+ * not run any animations but will still clear this flag after the layout is complete.
+ *
+ */
+ public void requestSimpleAnimationsInNextLayout() {
+ mRequestedSimpleAnimations = true;
+ }
}
/**
@@ -4416,8 +5354,9 @@
* between items, highlights, visual grouping boundaries and more.
*
* <p>All ItemDecorations are drawn in the order they were added, before the item
- * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView) onDraw()} and after the items
- * (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView)}.</p>
+ * views (in {@link ItemDecoration#onDraw(Canvas, RecyclerView, RecyclerView.State) onDraw()}
+ * and after the items (in {@link ItemDecoration#onDrawOver(Canvas, RecyclerView,
+ * RecyclerView.State)}.</p>
*/
public static abstract class ItemDecoration {
/**
@@ -4427,7 +5366,17 @@
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
+ * @param state The current state of RecyclerView
*/
+ public void onDraw(Canvas c, RecyclerView parent, State state) {
+ onDraw(c, parent);
+ }
+
+ /**
+ * @deprecated
+ * Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
+ */
+ @Deprecated
public void onDraw(Canvas c, RecyclerView parent) {
}
@@ -4438,10 +5387,30 @@
*
* @param c Canvas to draw into
* @param parent RecyclerView this ItemDecoration is drawing into
+ * @param state The current state of RecyclerView.
*/
+ public void onDrawOver(Canvas c, RecyclerView parent, State state) {
+ onDrawOver(c, parent);
+ }
+
+ /**
+ * @deprecated
+ * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
+ */
+ @Deprecated
public void onDrawOver(Canvas c, RecyclerView parent) {
}
+
+ /**
+ * @deprecated
+ * Use {@link #getItemOffsets(Rect, View, RecyclerView, State)}
+ */
+ @Deprecated
+ public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
+ outRect.set(0, 0, 0, 0);
+ }
+
/**
* Retrieve any offsets for the given item. Each field of <code>outRect</code> specifies
* the number of pixels that the item view should be inset by, similar to padding or margin.
@@ -4452,11 +5421,13 @@
* before returning.</p>
*
* @param outRect Rect to receive the output.
- * @param itemPosition Adapter position of the item to offset
- * @param parent RecyclerView this ItemDecoration is decorating
+ * @param view The child view to decorate
+ * @param parent RecyclerView this ItemDecoration is decorating
+ * @param state The current state of RecyclerView.
*/
- public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
- outRect.set(0, 0, 0, 0);
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
+ getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewPosition(),
+ parent);
}
}
@@ -4539,11 +5510,16 @@
*/
public static abstract class ViewHolder {
public final View itemView;
-
int mPosition = NO_POSITION;
int mOldPosition = NO_POSITION;
long mItemId = NO_ID;
int mItemViewType = INVALID_TYPE;
+ int mPreLayoutPosition = NO_POSITION;
+
+ // The item that this holder is shadowing during an item change event/animation
+ ViewHolder mShadowedHolder = null;
+ // The item that is shadowing this holder during an item change event/animation
+ ViewHolder mShadowingHolder = null;
/**
* This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
@@ -4576,8 +5552,31 @@
*/
static final int FLAG_NOT_RECYCLABLE = 1 << 4;
+ /**
+ * This ViewHolder is returned from scrap which means we are expecting an addView call
+ * for this itemView. When returned from scrap, ViewHolder stays in the scrap list until
+ * the end of the layout pass and then recycled by RecyclerView if it is not added back to
+ * the RecyclerView.
+ */
+ static final int FLAG_RETURNED_FROM_SCRAP = 1 << 5;
+
+ /**
+ * This ViewHolder's contents have changed. This flag is used as an indication that
+ * change animations may be used, if supported by the ItemAnimator.
+ */
+ static final int FLAG_CHANGED = 1 << 6;
+
+ /**
+ * This ViewHolder is fully managed by the LayoutManager. We do not scrap, recycle or remove
+ * it unless LayoutManager is replaced.
+ * It is still fully visible to the LayoutManager.
+ */
+ static final int FLAG_IGNORE = 1 << 7;
+
private int mFlags;
+ private int mIsRecyclableCount = 0;
+
// If non-null, view is currently considered scrap and may be reused for other data by the
// scrap container.
private Recycler mScrapContainer = null;
@@ -4589,25 +5588,69 @@
this.itemView = itemView;
}
- void offsetPosition(int offset) {
+ void offsetPosition(int offset, boolean applyToPreLayout) {
if (mOldPosition == NO_POSITION) {
mOldPosition = mPosition;
}
+ if (mPreLayoutPosition == NO_POSITION) {
+ mPreLayoutPosition = mPosition;
+ }
+ if (applyToPreLayout) {
+ mPreLayoutPosition += offset;
+ }
mPosition += offset;
+ if (itemView.getLayoutParams() != null) {
+ ((LayoutParams) itemView.getLayoutParams()).mInsetsDirty = true;
+ }
}
void clearOldPosition() {
mOldPosition = NO_POSITION;
+ mPreLayoutPosition = NO_POSITION;
+ }
+
+ void saveOldPosition() {
+ if (mOldPosition == NO_POSITION) {
+ mOldPosition = mPosition;
+ }
+ }
+
+ boolean shouldIgnore() {
+ return (mFlags & FLAG_IGNORE) != 0;
}
public final int getPosition() {
- return mOldPosition == NO_POSITION ? mPosition : mOldPosition;
+ return mPreLayoutPosition == NO_POSITION ? mPosition : mPreLayoutPosition;
}
+ /**
+ * When LayoutManager supports animations, RecyclerView tracks 3 positions for ViewHolders
+ * to perform animations.
+ * <p>
+ * If a ViewHolder was laid out in the previous onLayout call, old position will keep its
+ * adapter index in the previous layout.
+ *
+ * @return The previous adapter index of the Item represented by this ViewHolder or
+ * {@link #NO_POSITION} if old position does not exists or cleared (pre-layout is
+ * complete).
+ */
+ public final int getOldPosition() {
+ return mOldPosition;
+ }
+
+ /**
+ * Returns The itemId represented by this ViewHolder.
+ *
+ * @return The the item's id if adapter has stable ids, {@link RecyclerView#NO_ID}
+ * otherwise
+ */
public final long getItemId() {
return mItemId;
}
+ /**
+ * @return The view type of this ViewHolder.
+ */
public final int getItemViewType() {
return mItemViewType;
}
@@ -4618,7 +5661,18 @@
void unScrap() {
mScrapContainer.unscrapView(this);
- mScrapContainer = null;
+ }
+
+ boolean wasReturnedFromScrap() {
+ return (mFlags & FLAG_RETURNED_FROM_SCRAP) != 0;
+ }
+
+ void clearReturnedFromScrapFlag() {
+ mFlags = mFlags & ~FLAG_RETURNED_FROM_SCRAP;
+ }
+
+ void stopIgnoring() {
+ mFlags = mFlags & ~FLAG_IGNORE;
}
void setScrapContainer(Recycler recycler) {
@@ -4633,6 +5687,10 @@
return (mFlags & FLAG_UPDATE) != 0;
}
+ boolean isChanged() {
+ return (mFlags & FLAG_CHANGED) != 0;
+ }
+
boolean isBound() {
return (mFlags & FLAG_BOUND) != 0;
}
@@ -4649,32 +5707,65 @@
mFlags |= flags;
}
- void clearFlagsForSharedPool() {
+ void resetInternal() {
mFlags = 0;
+ mPosition = NO_POSITION;
+ mOldPosition = NO_POSITION;
+ mItemId = NO_ID;
+ mPreLayoutPosition = NO_POSITION;
+ mIsRecyclableCount = 0;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("ViewHolder{" +
- Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId);
+ Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId +
+ ", oldPos=" + mOldPosition + ", pLpos:" + mPreLayoutPosition);
if (isScrap()) sb.append(" scrap");
if (isInvalid()) sb.append(" invalid");
if (!isBound()) sb.append(" unbound");
if (needsUpdate()) sb.append(" update");
if (isRemoved()) sb.append(" removed");
+ if (shouldIgnore()) sb.append(" ignored");
+ if (!isRecyclable()) sb.append(" not recyclable");
+ if (itemView.getParent() == null) sb.append(" no parent");
sb.append("}");
return sb.toString();
}
+ /**
+ * Informs the recycler whether this item can be recycled. Views which are not
+ * recyclable will not be reused for other items until setIsRecyclable() is
+ * later set to true. Calls to setIsRecyclable() should always be paired (one
+ * call to setIsRecyclabe(false) should always be matched with a later call to
+ * setIsRecyclable(true)). Pairs of calls may be nested, as the state is internally
+ * reference-counted.
+ *
+ * @param recyclable Whether this item is available to be recycled. Default value
+ * is true.
+ */
public final void setIsRecyclable(boolean recyclable) {
- // TODO: might want this to be a refcount instead
- if (recyclable) {
- mFlags &= ~FLAG_NOT_RECYCLABLE;
- } else {
+ mIsRecyclableCount = recyclable ? mIsRecyclableCount - 1 : mIsRecyclableCount + 1;
+ if (DEBUG) {
+ Log.d(TAG, "setIsRecyclable for item id:" + mItemId + "val:" + recyclable + ","
+ + "cnt:" + mIsRecyclableCount);
+ }
+ if (mIsRecyclableCount < 0) {
+ mIsRecyclableCount = 0;
+ Log.e(VIEW_LOG_TAG, "isRecyclable decremented below 0: " +
+ "unmatched pair of setIsRecyable() calls");
+ } else if (!recyclable && mIsRecyclableCount == 1) {
mFlags |= FLAG_NOT_RECYCLABLE;
+ } else if (recyclable && mIsRecyclableCount == 0) {
+ mFlags &= ~FLAG_NOT_RECYCLABLE;
}
}
+ /**
+ * @see {@link #setIsRecyclable(boolean)}
+ *
+ * @return true if this item is available to be recycled, false otherwise.
+ */
public final boolean isRecyclable() {
return (mFlags & FLAG_NOT_RECYCLABLE) == 0 &&
!ViewCompat.hasTransientState(itemView);
@@ -4682,49 +5773,12 @@
}
/**
- * Queued operation to happen when child views are updated.
- */
- private static class UpdateOp {
- public static final int ADD = 0;
- public static final int REMOVE = 1;
- public static final int UPDATE = 2;
-
- static final int POOL_SIZE = 30;
-
- public int cmd;
- public int positionStart;
- public int itemCount;
-
- public UpdateOp(int cmd, int positionStart, int itemCount) {
- this.cmd = cmd;
- this.positionStart = positionStart;
- this.itemCount = itemCount;
- }
- }
-
- UpdateOp obtainUpdateOp(int cmd, int positionStart, int itemCount) {
- UpdateOp op = mUpdateOpPool.acquire();
- if (op == null) {
- op = new UpdateOp(cmd, positionStart, itemCount);
- } else {
- op.cmd = cmd;
- op.positionStart = positionStart;
- op.itemCount = itemCount;
- }
- return op;
- }
-
- void recycleUpdateOp(UpdateOp op) {
- mUpdateOpPool.release(op);
- }
-
- /**
* {@link android.view.ViewGroup.MarginLayoutParams LayoutParams} subclass for children of
* {@link RecyclerView}. Custom {@link LayoutManager layout managers} are encouraged
* to create their own subclass of this <code>LayoutParams</code> class
* to store any additional required per-child view metadata about the layout.
*/
- public static class LayoutParams extends MarginLayoutParams {
+ public static class LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
ViewHolder mViewHolder;
final Rect mDecorInsets = new Rect();
boolean mInsetsDirty = true;
@@ -4936,7 +5990,7 @@
// verify target position
if (getChildPosition(mTargetView) == mTargetPosition) {
onTargetFound(mTargetView, mRecyclerView.mState, mRecyclingAction);
- mRecyclingAction.runInNecessary(mRecyclerView);
+ mRecyclingAction.runIfNecessary(mRecyclerView);
stop();
} else {
Log.e(TAG, "Passed over target position while smooth scrolling.");
@@ -4945,7 +5999,7 @@
}
if (mRunning) {
onSeekTargetStep(dx, dy, mRecyclerView.mState, mRecyclingAction);
- mRecyclingAction.runInNecessary(mRecyclerView);
+ mRecyclingAction.runIfNecessary(mRecyclerView);
}
}
@@ -4957,10 +6011,10 @@
}
/**
- * @see RecyclerView#getChildCount()
+ * @see RecyclerView.LayoutManager#getChildCount()
*/
public int getChildCount() {
- return mRecyclerView.getChildCount();
+ return mRecyclerView.mLayout.getChildCount();
}
/**
@@ -5085,7 +6139,7 @@
mDuration = duration;
mInterpolator = interpolator;
}
- private void runInNecessary(RecyclerView recyclerView) {
+ private void runIfNecessary(RecyclerView recyclerView) {
if (changed) {
validate();
if (mInterpolator == null) {
@@ -5225,7 +6279,7 @@
}
}
- static class SavedState extends BaseSavedState {
+ static class SavedState extends android.view.View.BaseSavedState {
Parcelable mLayoutState;
@@ -5307,6 +6361,10 @@
private boolean mInPreLayout = false;
+ private boolean mRunSimpleAnimations = false;
+
+ private boolean mRunPredictiveAnimations = false;
+
State reset() {
mTargetPosition = RecyclerView.NO_POSITION;
if (mData != null) {
@@ -5322,6 +6380,28 @@
}
/**
+ * Returns whether RecyclerView will run predictive animations in this layout pass
+ * or not.
+ *
+ * @return true if RecyclerView is calculating predictive animations to be run at the end
+ * of the layout pass.
+ */
+ public boolean willRunPredictiveAnimations() {
+ return mRunPredictiveAnimations;
+ }
+
+ /**
+ * Returns whether RecyclerView will run simple animations in this layout pass
+ * or not.
+ *
+ * @return true if RecyclerView is calculating simple animations to be run at the end of
+ * the layout pass.
+ */
+ public boolean willRunSimpleAnimations() {
+ return mRunSimpleAnimations;
+ }
+
+ /**
* Removes the mapping from the specified id, if there was any.
* @param resourceId Id of the resource you want to remove. It is suggested to use R.id.* to
* preserve cross functionality and avoid conflicts.
@@ -5401,6 +6481,16 @@
(mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout) :
mItemCount;
}
+
+ public void onViewRecycled(ViewHolder holder) {
+ mPreLayoutHolderMap.remove(holder);
+ mPostLayoutHolderMap.remove(holder);
+ }
+
+ public void onViewIgnored(ViewHolder holder) {
+ mPreLayoutHolderMap.remove(holder);
+ mPostLayoutHolderMap.remove(holder);
+ }
}
/**
@@ -5429,6 +6519,19 @@
item.setIsRecyclable(true);
removeAnimatingView(item.itemView);
}
+
+ @Override
+ public void onChangeFinished(ViewHolder item) {
+ item.setIsRecyclable(true);
+ removeAnimatingView(item.itemView);
+ item.setFlags(~ViewHolder.FLAG_CHANGED, item.mFlags);
+ ViewHolder shadowedHolder = item.mShadowedHolder;
+ if (shadowedHolder != null) {
+ shadowedHolder.setIsRecyclable(true);
+ shadowedHolder.mShadowingHolder = null;
+ }
+ item.mShadowedHolder = null;
+ }
};
/**
@@ -5454,6 +6557,9 @@
private long mAddDuration = 120;
private long mRemoveDuration = 120;
private long mMoveDuration = 250;
+ private long mChangeDuration = 250;
+
+ private boolean mSupportsChangeAnimations = false;
/**
* Gets the current duration for which all move animations will run.
@@ -5465,9 +6571,9 @@
}
/**
- * Sets the current duration for which all move animations will run.
+ * Sets the duration for which all move animations will run.
*
- * @param moveDuration The current move duration
+ * @param moveDuration The move duration
*/
public void setMoveDuration(long moveDuration) {
mMoveDuration = moveDuration;
@@ -5483,9 +6589,9 @@
}
/**
- * Sets the current duration for which all add animations will run.
+ * Sets the duration for which all add animations will run.
*
- * @param addDuration The current add duration
+ * @param addDuration The add duration
*/
public void setAddDuration(long addDuration) {
mAddDuration = addDuration;
@@ -5501,15 +6607,63 @@
}
/**
- * Sets the current duration for which all remove animations will run.
+ * Sets the duration for which all remove animations will run.
*
- * @param removeDuration The current remove duration
+ * @param removeDuration The remove duration
*/
public void setRemoveDuration(long removeDuration) {
mRemoveDuration = removeDuration;
}
/**
+ * Gets the current duration for which all change animations will run.
+ *
+ * @return The current change duration
+ */
+ public long getChangeDuration() {
+ return mChangeDuration;
+ }
+
+ /**
+ * Sets the duration for which all change animations will run.
+ *
+ * @param changeDuration The change duration
+ */
+ public void setChangeDuration(long changeDuration) {
+ mChangeDuration = changeDuration;
+ }
+
+ /**
+ * Returns whether this ItemAnimator supports animations of change events.
+ *
+ * @return true if change animations are supported, false otherwise
+ */
+ public boolean getSupportsChangeAnimations() {
+ return mSupportsChangeAnimations;
+ }
+
+ /**
+ * Sets whether this ItemAnimator supports animations of item change events.
+ * By default, ItemAnimator only supports animations when items are added or removed.
+ * By setting this property to true, actions on the data set which change the
+ * contents of items may also be animated. What those animations are is left
+ * up to the discretion of the ItemAnimator subclass, in its
+ * {@link #animateChange(ViewHolder, ViewHolder)} implementation.
+ * The value of this property is false by default.
+ *
+ * @see Adapter#notifyItemChanged(int)
+ * @see Adapter#notifyItemRangeChanged(int, int)
+ *
+ * @param supportsChangeAnimations true if change animations are supported by
+ * this ItemAnimator, false otherwise. If the property is false, the ItemAnimator
+ * will not receive a call to {@link #animateChange(ViewHolder, ViewHolder)} when
+ * changes occur.
+ */
+ public void setSupportsChangeAnimations(boolean supportsChangeAnimations) {
+ mSupportsChangeAnimations = supportsChangeAnimations;
+ }
+
+ /**
* Internal only:
* Sets the listener that must be called when the animator is finished
* animating the item (or immediately if no animation happens). This is set
@@ -5538,11 +6692,12 @@
* {@link #dispatchRemoveFinished(ViewHolder)} when done, either
* immediately (if no animation will occur) or after the animation actually finishes.
* The return value indicates whether an animation has been set up and whether the
- * ItemAnimators {@link #runPendingAnimations()} method should be called at the
+ * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
* next opportunity. This mechanism allows ItemAnimator to set up individual animations
* as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
- * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()}, and
- * {@link #animateRemove(ViewHolder) animateRemove()} come in one by one, then
+ * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+ * {@link #animateRemove(ViewHolder) animateRemove()}, and
+ * {@link #animateChange(ViewHolder, ViewHolder)} come in one by one, then
* start the animations together in the later call to {@link #runPendingAnimations()}.
*
* <p>This method may also be called for disappearing items which continue to exist in the
@@ -5562,11 +6717,12 @@
* {@link #dispatchAddFinished(ViewHolder)} when done, either
* immediately (if no animation will occur) or after the animation actually finishes.
* The return value indicates whether an animation has been set up and whether the
- * ItemAnimators {@link #runPendingAnimations()} method should be called at the
+ * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
* next opportunity. This mechanism allows ItemAnimator to set up individual animations
* as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
- * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()}, and
- * {@link #animateRemove(ViewHolder) animateRemove()} come in one by one, then
+ * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+ * {@link #animateRemove(ViewHolder) animateRemove()}, and
+ * {@link #animateChange(ViewHolder, ViewHolder)} come in one by one, then
* start the animations together in the later call to {@link #runPendingAnimations()}.
*
* <p>This method may also be called for appearing items which were already in the
@@ -5586,11 +6742,12 @@
* {@link #dispatchMoveFinished(ViewHolder)} when done, either
* immediately (if no animation will occur) or after the animation actually finishes.
* The return value indicates whether an animation has been set up and whether the
- * ItemAnimators {@link #runPendingAnimations()} method should be called at the
+ * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
* next opportunity. This mechanism allows ItemAnimator to set up individual animations
* as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
- * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()}, and
- * {@link #animateRemove(ViewHolder) animateRemove()} come in one by one, then
+ * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+ * {@link #animateRemove(ViewHolder) animateRemove()}, and
+ * {@link #animateChange(ViewHolder, ViewHolder)} come in one by one, then
* start the animations together in the later call to {@link #runPendingAnimations()}.
*
* @param holder The item that is being moved.
@@ -5601,6 +6758,29 @@
int toX, int toY);
/**
+ * Called when an item is changed in the RecyclerView, as indicated by a call to
+ * {@link Adapter#notifyItemChanged(int)} or
+ * {@link Adapter#notifyItemRangeChanged(int, int)}. Implementors can choose
+ * whether and how to animate changes, but must always call
+ * {@link #dispatchChangeFinished(ViewHolder)} with <code>oldHolder</code>when done, either
+ * immediately (if no animation will occur) or after the animation actually finishes.
+ * The return value indicates whether an animation has been set up and whether the
+ * ItemAnimator's {@link #runPendingAnimations()} method should be called at the
+ * next opportunity. This mechanism allows ItemAnimator to set up individual animations
+ * as separate calls to {@link #animateAdd(ViewHolder) animateAdd()},
+ * {@link #animateMove(ViewHolder, int, int, int, int) animateMove()},
+ * {@link #animateRemove(ViewHolder) animateRemove()}, and
+ * {@link #animateChange(ViewHolder, ViewHolder)} come in one by one, then
+ * start the animations together in the later call to {@link #runPendingAnimations()}.
+ *
+ * @param oldHolder The original item that changed.
+ * @param newHolder The new item that was created with the changed content.
+ * @return true if a later call to {@link #runPendingAnimations()} is requested,
+ * false otherwise.
+ */
+ abstract public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder);
+
+ /**
* Method to be called by subclasses when a remove animation is done.
*
* @param item The item which has been removed
@@ -5634,6 +6814,19 @@
}
/**
+ * Method to be called by subclasses when a change animation is done.
+ *
+ * @param item The item which has been changed (this is the original, or "old"
+ * ViewHolder item, not the "new" holder which was also passed into the call to
+ * {@link #animateChange(ViewHolder, ViewHolder)}).
+ */
+ public final void dispatchChangeFinished(ViewHolder item) {
+ if (mListener != null) {
+ mListener.onChangeFinished(item);
+ }
+ }
+
+ /**
* Method called when an animation on a view should be ended immediately.
* This could happen when other events, like scrolling, occur, so that
* animating views can be quickly put into their proper end locations.
@@ -5705,6 +6898,7 @@
void onRemoveFinished(ViewHolder item);
void onAddFinished(ViewHolder item);
void onMoveFinished(ViewHolder item);
+ void onChangeFinished(ViewHolder item);
}
/**
@@ -5738,16 +6932,13 @@
private static class ItemHolderInfo {
ViewHolder holder;
int left, top, right, bottom;
- int position;
- ItemHolderInfo(ViewHolder holder, int left, int top, int right, int bottom, int position) {
+ ItemHolderInfo(ViewHolder holder, int left, int top, int right, int bottom) {
this.holder = holder;
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
- this.position = position;
}
}
-
}
diff --git a/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
new file mode 100644
index 0000000..79e6e47
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -0,0 +1,1939 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+
+
+import static android.support.v7.widget.LayoutState.LAYOUT_START;
+import static android.support.v7.widget.LayoutState.LAYOUT_END;
+import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD;
+import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL;
+/**
+ * A LayoutManager that lays out children in a staggered grid formation.
+ * It supports horizontal & vertical layout as well as an ability to layout children in reverse.
+ * <p>
+ * Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps,
+ * StaggeredGridLayoutManager can offset spans independently or move items between spans. You can
+ * control this behavior via {@link #setGapStrategy(int)}.
+ */
+public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager {
+
+ public static final String TAG = "StaggeredGridLayoutManager";
+
+ private static final boolean DEBUG = false;
+
+ public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
+
+ public static final int VERTICAL = OrientationHelper.VERTICAL;
+
+ /**
+ * Does not do anything to hide gaps
+ */
+ public static final int GAP_HANDLING_NONE = 0;
+
+ /**
+ * Scroll the shorter span slower to avoid gaps in the UI.
+ * <p>
+ * For example, if LayoutManager ends up with the following layout:
+ * <code>
+ * BXC
+ * DEF
+ * </code>
+ * Where B has two spans height, if user scrolls down it will keep the positions of 2nd and 3rd
+ * columns,
+ * which will result in:
+ * <code>
+ * BXC
+ * BEF
+ * </code>
+ * instead of
+ * <code>
+ * B
+ * BEF
+ * </code>
+ */
+ public static final int GAP_HANDLING_LAZY = 1;
+
+ /**
+ * On scroll, LayoutManager checks for a view that is assigned to wrong span.
+ * When such a situation is detected, LayoutManager will wait until scroll is complete and then
+ * move children to their correct spans.
+ * <p>
+ * For example, if LayoutManager ends up with the following layout due to adapter changes:
+ * <code>
+ * AAA
+ * _BC
+ * DDD
+ * </code>
+ * It will animate to the following state:
+ * <code>
+ * AAA
+ * BC_
+ * DDD
+ * </code>
+ */
+ public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2;
+
+ private static final int INVALID_OFFSET = Integer.MIN_VALUE;
+
+ /**
+ * Number of spans
+ */
+ private int mSpanCount = -1;
+
+ private Span[] mSpans;
+
+ /**
+ * Primary orientation is the layout's orientation, secondary orientation is the orientation
+ * for spans. Having both makes code much cleaner for calculations.
+ */
+ OrientationHelper mPrimaryOrientation;
+ OrientationHelper mSecondaryOrientation;
+
+ private int mOrientation;
+
+ /**
+ * The width or height per span, depending on the orientation.
+ */
+ private int mSizePerSpan;
+
+ private LayoutState mLayoutState;
+
+ private boolean mReverseLayout = false;
+
+ /**
+ * Aggregated reverse layout value that takes RTL into account.
+ */
+ private boolean mShouldReverseLayout = false;
+
+ /**
+ * Temporary variable used during fill method to check which spans needs to be filled.
+ */
+ private BitSet mRemainingSpans;
+
+ /**
+ * When LayoutManager needs to scroll to a position, it sets this variable and requests a
+ * layout which will check this variable and re-layout accordingly.
+ */
+ private int mPendingScrollPosition = RecyclerView.NO_POSITION;
+
+ /**
+ * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is
+ * called.
+ */
+ private int mPendingScrollPositionOffset = INVALID_OFFSET;
+
+ /**
+ * Keeps the mapping between the adapter positions and spans. This is necessary to provide
+ * a consistent experience when user scrolls the list.
+ */
+ LazySpanLookup mLazySpanLookup = new LazySpanLookup();
+
+ /**
+ * how we handle gaps in UI.
+ */
+ private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
+
+ /**
+ * Saved state needs this information to properly layout on restore.
+ */
+ private boolean mLastLayoutFromEnd;
+
+ /**
+ * SavedState is not handled until a layout happens. This is where we keep it until next
+ * layout.
+ */
+ private SavedState mPendingSavedState;
+
+ /**
+ * If LayoutManager detects an unwanted gap in the layout, it sets this flag which will trigger
+ * a runnable after scrolling ends and will re-check. If invalid view state is still present,
+ * it will request a layout to fix it.
+ */
+ private boolean mHasGaps;
+
+ /**
+ * Creates a StaggeredGridLayoutManager with given parameters.
+ *
+ * @param spanCount If orientation is vertical, spanCount is number of columns. If
+ * orientation is horizontal, spanCount is number of rows.
+ * @param orientation {@link #VERTICAL} or {@link #HORIZONTAL}
+ */
+ public StaggeredGridLayoutManager(int spanCount, int orientation) {
+ mOrientation = orientation;
+ setSpanCount(spanCount);
+ }
+
+ @Override
+ public void onScrollStateChanged(int state) {
+ if (state == RecyclerView.SCROLL_STATE_IDLE && mHasGaps) {
+ // re-check for gaps
+ View gapView = hasGapsToFix(0, getChildCount());
+ if (gapView == null) {
+ mHasGaps = false; // yay, gap disappeared :)
+ // We should invalidate positions after the last visible child. No reason to
+ // re-layout.
+ final int lastVisiblePosition = mShouldReverseLayout ? getFirstChildPosition()
+ : getLastChildPosition();
+ mLazySpanLookup.invalidateAfter(lastVisiblePosition + 1);
+ } else {
+ mLazySpanLookup.invalidateAfter(getPosition(gapView));
+ requestSimpleAnimationsInNextLayout();
+ requestLayout(); // Trigger a re-layout which will fix the layout assignments.
+ }
+ }
+ }
+
+ /**
+ * Sets the number of spans for the layout. This will invalidate all of the span assignments
+ * for Views.
+ * <p>
+ * Calling this method will automatically result in a new layout request unless the spanCount
+ * parameter is equal to current span count.
+ *
+ * @param spanCount Number of spans to layout
+ */
+ public void setSpanCount(int spanCount) {
+ assertNotInLayoutOrScroll(null);
+ if (mPendingSavedState != null && mPendingSavedState.mSpanCount != spanCount) {
+ // invalidate span info in saved state
+ mPendingSavedState.invalidateSpanInfo();
+ mPendingSavedState.mSpanCount = spanCount;
+ }
+ if (spanCount != mSpanCount) {
+ invalidateSpanAssignments();
+ mSpanCount = spanCount;
+ mRemainingSpans = new BitSet(mSpanCount);
+ mSpans = new Span[mSpanCount];
+ for (int i = 0; i < mSpanCount; i++) {
+ mSpans[i] = new Span(i);
+ }
+ requestLayout();
+ }
+ }
+
+ /**
+ * Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep
+ * scroll position.
+ *
+ * @param orientation {@link OrientationHelper#HORIZONTAL} or {@link OrientationHelper#VERTICAL}
+ */
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL && orientation != VERTICAL) {
+ throw new IllegalArgumentException("invalid orientation.");
+ }
+ assertNotInLayoutOrScroll(null);
+ if (mPendingSavedState != null && mPendingSavedState.mOrientation != orientation) {
+ // override pending state
+ mPendingSavedState.mOrientation = orientation;
+ }
+ if (orientation == mOrientation) {
+ return;
+ }
+ mOrientation = orientation;
+ if (mPrimaryOrientation != null && mSecondaryOrientation != null) {
+ // swap
+ OrientationHelper tmp = mPrimaryOrientation;
+ mPrimaryOrientation = mSecondaryOrientation;
+ mSecondaryOrientation = tmp;
+ }
+ requestLayout();
+ }
+
+ /**
+ * Sets whether LayoutManager should start laying out items from the end of the UI. The order
+ * items are traversed is not affected by this call.
+ * <p>
+ * This behaves similar to the layout change for RTL views. When set to true, first item is
+ * laid out at the end of the ViewGroup, second item is laid out before it etc.
+ * <p>
+ * For horizontal layouts, it depends on the layout direction.
+ * When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if
+ * {@link RecyclerView}} is RTL, it will layout from LTR.
+ *
+ * @param reverseLayout Whether layout should be in reverse or not
+ */
+ public void setReverseLayout(boolean reverseLayout) {
+ assertNotInLayoutOrScroll(null);
+ if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) {
+ mPendingSavedState.mReverseLayout = reverseLayout;
+ }
+ mReverseLayout = reverseLayout;
+ requestLayout();
+ }
+
+ /**
+ * Returns the current gap handling strategy for StaggeredGridLayoutManager.
+ * <p>
+ * Staggered grid may have gaps in the layout as items may have different sizes. To avoid gaps,
+ * StaggeredGridLayoutManager provides 3 options. Check {@link #GAP_HANDLING_NONE},
+ * {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}, {@link #GAP_HANDLING_LAZY} for details.
+ * <p>
+ * By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}.
+ *
+ * @return Current gap handling strategy.
+ * @see #setGapStrategy(int)
+ * @see #GAP_HANDLING_NONE
+ * @see #GAP_HANDLING_LAZY
+ * @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
+ */
+ public int getGapStrategy() {
+ return mGapStrategy;
+ }
+
+ /**
+ * Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter
+ * is different than the current strategy, calling this method will trigger a layout request.
+ *
+ * @param gapStrategy The new gap handling strategy. Should be {@link #GAP_HANDLING_LAZY}
+ * , {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or
+ * {@link #GAP_HANDLING_NONE}
+ * @see #getGapStrategy()
+ */
+ public void setGapStrategy(int gapStrategy) {
+ assertNotInLayoutOrScroll(null);
+ if (mPendingSavedState != null && mPendingSavedState.mGapStrategy != gapStrategy) {
+ mPendingSavedState.mGapStrategy = gapStrategy;
+ }
+ if (gapStrategy == mGapStrategy) {
+ return;
+ }
+ if (gapStrategy != GAP_HANDLING_LAZY && gapStrategy != GAP_HANDLING_NONE &&
+ gapStrategy != GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
+ throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE "
+ + ", GAP_HANDLING_LAZY or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS");
+ }
+ mGapStrategy = gapStrategy;
+ requestLayout();
+ }
+
+ @Override
+ public void assertNotInLayoutOrScroll(String message) {
+ if (mPendingSavedState == null) {
+ super.assertNotInLayoutOrScroll(message);
+ }
+ }
+
+ /**
+ * Returns the number of spans laid out by StaggeredGridLayoutManager.
+ *
+ * @return Number of spans in the layout
+ */
+ public int getSpanCount() {
+ return mSpanCount;
+ }
+
+ /**
+ * For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items.
+ * <p>
+ * If you need to cancel current assignments, you can call this method which will clear all
+ * assignments and request a new layout.
+ */
+ public void invalidateSpanAssignments() {
+ mLazySpanLookup.clear();
+ requestLayout();
+ }
+
+ private void ensureOrientationHelper() {
+ if (mPrimaryOrientation == null) {
+ mPrimaryOrientation = OrientationHelper.createOrientationHelper(this, mOrientation);
+ mSecondaryOrientation = OrientationHelper
+ .createOrientationHelper(this, 1 - mOrientation);
+ mLayoutState = new LayoutState();
+ }
+ }
+
+ /**
+ * Calculates the views' layout order. (e.g. from end to start or start to end)
+ * RTL layout support is applied automatically. So if layout is RTL and
+ * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
+ */
+ private void resolveShouldLayoutReverse() {
+ // A == B is the same result, but we rather keep it readable
+ if (mOrientation == VERTICAL || !isLayoutRTL()) {
+ mShouldReverseLayout = mReverseLayout;
+ } else {
+ mShouldReverseLayout = !mReverseLayout;
+ }
+ }
+
+ private boolean isLayoutRTL() {
+ return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
+ }
+
+ /**
+ * Returns whether views are laid out in reverse order or not.
+ * <p>
+ * Not that this value is not affected by RecyclerView's layout direction.
+ *
+ * @return True if layout is reversed, false otherwise
+ * @see #setReverseLayout(boolean)
+ */
+ public boolean getReverseLayout() {
+ return mReverseLayout;
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ ensureOrientationHelper();
+ // Update adapter size.
+ mLazySpanLookup.mAdapterSize = state.getItemCount();
+ int anchorItemPosition;
+ int anchorOffset;
+ // This value may change if we are jumping to a position.
+ boolean layoutFromEnd;
+
+ // If set to true, spans will clear their offsets and they'll be laid out from start
+ // depending on the layout direction. Invalidating span offsets is necessary to be able
+ // to jump to a position.
+ boolean invalidateSpanOffsets = false;
+
+ if (mPendingSavedState != null) {
+ if (DEBUG) {
+ Log.d(TAG, "found saved state: " + mPendingSavedState);
+ }
+ setOrientation(mPendingSavedState.mOrientation);
+ setSpanCount(mPendingSavedState.mSpanCount);
+ setGapStrategy(mPendingSavedState.mGapStrategy);
+ setReverseLayout(mPendingSavedState.mReverseLayout);
+ resolveShouldLayoutReverse();
+
+ if (mPendingSavedState.mAnchorPosition != RecyclerView.NO_POSITION) {
+ mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
+ layoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
+ } else {
+ layoutFromEnd = mShouldReverseLayout;
+ }
+ if (mPendingSavedState.mHasSpanOffsets) {
+ for (int i = 0; i < mSpanCount; i++) {
+ mSpans[i].clear();
+ mSpans[i].setLine(mPendingSavedState.mSpanOffsets[i]);
+ }
+ }
+ if (mPendingSavedState.mSpanLookupSize > 1) {
+ mLazySpanLookup.mData = mPendingSavedState.mSpanLookup;
+ }
+
+ } else {
+ resolveShouldLayoutReverse();
+ layoutFromEnd = mShouldReverseLayout; // get updated value.
+ }
+
+ // Validate scroll position if exists.
+ if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
+ // Validate it.
+ if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
+ mPendingScrollPosition = RecyclerView.NO_POSITION;
+ mPendingScrollPositionOffset = INVALID_OFFSET;
+ }
+ }
+
+ if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
+ if (mPendingSavedState == null
+ || mPendingSavedState.mAnchorPosition == RecyclerView.NO_POSITION
+ || !mPendingSavedState.mHasSpanOffsets) {
+ // If item is visible, make it fully visible.
+ final View child = findViewByPosition(mPendingScrollPosition);
+ if (child != null) {
+ if (mPendingScrollPositionOffset != INVALID_OFFSET) {
+ // Use regular anchor position.
+ anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
+ : getFirstChildPosition();
+ if (layoutFromEnd) {
+ final int target = mPrimaryOrientation.getEndAfterPadding() -
+ mPendingScrollPositionOffset;
+ anchorOffset = target - mPrimaryOrientation.getDecoratedEnd(child);
+ } else {
+ final int target = mPrimaryOrientation.getStartAfterPadding() +
+ mPendingScrollPositionOffset;
+ anchorOffset = target - mPrimaryOrientation.getDecoratedStart(child);
+ }
+ } else {
+ final int startGap = mPrimaryOrientation.getDecoratedStart(child)
+ - mPrimaryOrientation.getStartAfterPadding();
+ final int endGap = mPrimaryOrientation.getEndAfterPadding() -
+ mPrimaryOrientation.getDecoratedEnd(child);
+ final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child);
+ // Use regular anchor item, just offset the layout.
+ anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
+ : getFirstChildPosition();
+ if (childSize > mPrimaryOrientation.getTotalSpace()) {
+ // Item does not fit. Fix depending on layout direction.
+ anchorOffset = layoutFromEnd ? mPrimaryOrientation.getEndAfterPadding()
+ : mPrimaryOrientation.getStartAfterPadding();
+ } else if (startGap < 0) {
+ anchorOffset = -startGap;
+ } else if (endGap < 0) {
+ anchorOffset = endGap;
+ } else {
+ // Nothing to do, just layout normal.
+ anchorItemPosition = mShouldReverseLayout ? getLastChildPosition()
+ : getFirstChildPosition();
+ anchorOffset = INVALID_OFFSET;
+ }
+ }
+ } else {
+ // Child is not visible. Set anchor coordinate depending on in which direction
+ // child will be visible.
+ anchorItemPosition = mPendingScrollPosition;
+ if (mPendingScrollPositionOffset == INVALID_OFFSET) {
+ final int position = calculateScrollDirectionForPosition(
+ anchorItemPosition);
+ if (position == LAYOUT_START) {
+ anchorOffset = mPrimaryOrientation.getStartAfterPadding();
+ layoutFromEnd = false;
+ } else {
+ anchorOffset = mPrimaryOrientation.getEndAfterPadding();
+ layoutFromEnd = true;
+ }
+ } else {
+ if (layoutFromEnd) {
+ anchorOffset = mPrimaryOrientation.getEndAfterPadding()
+ - mPendingScrollPositionOffset;
+ } else {
+ anchorOffset = mPrimaryOrientation.getStartAfterPadding()
+ + mPendingScrollPositionOffset;
+ }
+ }
+ invalidateSpanOffsets = true;
+ }
+ } else {
+ anchorOffset = INVALID_OFFSET;
+ anchorItemPosition = mPendingScrollPosition;
+ }
+
+ } else {
+ // We don't recycle views out of adapter order. This way, we can rely on the first or
+ // last child as the anchor position.
+ anchorItemPosition = mShouldReverseLayout
+ ? findLastReferenceChildPosition(state.getItemCount())
+ : findFirstReferenceChildPosition(state.getItemCount());
+ anchorOffset = INVALID_OFFSET;
+ }
+ if (getChildCount() > 0 && (mPendingSavedState == null ||
+ !mPendingSavedState.mHasSpanOffsets)) {
+ if (invalidateSpanOffsets || mHasGaps) {
+ for (int i = 0; i < mSpanCount; i++) {
+ // Scroll to position is set, clear.
+ mSpans[i].clear();
+ if (anchorOffset != INVALID_OFFSET) {
+ mSpans[i].setLine(anchorOffset);
+ }
+ }
+ } else {
+ for (int i = 0; i < mSpanCount; i++) {
+ mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout, anchorOffset);
+ }
+ if (DEBUG) {
+ for (int i = 0; i < mSpanCount; i++) {
+ Log.d(TAG, "cached start-end lines for " + i + ":" +
+ mSpans[i].mCachedStart + ":" + mSpans[i].mCachedEnd);
+ }
+ }
+ }
+ }
+ mSizePerSpan = mSecondaryOrientation.getTotalSpace() / mSpanCount;
+ detachAndScrapAttachedViews(recycler);
+ // Layout start.
+ updateLayoutStateToFillStart(anchorItemPosition, state);
+ if (!layoutFromEnd) {
+ mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+ }
+ fill(recycler, mLayoutState, state);
+
+ // Layout end.
+ updateLayoutStateToFillEnd(anchorItemPosition, state);
+ if (layoutFromEnd) {
+ mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
+ }
+ fill(recycler, mLayoutState, state);
+
+ if (getChildCount() > 0) {
+ if (mShouldReverseLayout) {
+ fixEndGap(recycler, state, true);
+ fixStartGap(recycler, state, false);
+ } else {
+ fixStartGap(recycler, state, true);
+ fixEndGap(recycler, state, false);
+ }
+ }
+
+ mPendingScrollPosition = RecyclerView.NO_POSITION;
+ mPendingScrollPositionOffset = INVALID_OFFSET;
+ mLastLayoutFromEnd = layoutFromEnd;
+ mPendingSavedState = null; // we don't need this anymore
+ }
+
+ /**
+ * Checks if a child is assigned to the non-optimal span.
+ *
+ * @param startChildIndex Starts checking after this child, inclusive
+ * @param endChildIndex Starts checking until this child, exclusive
+ * @return The first View that is assigned to the wrong span.
+ */
+ View hasGapsToFix(int startChildIndex, int endChildIndex) {
+ // quick reject
+ if (startChildIndex >= endChildIndex) {
+ return null;
+ }
+ final int firstChildIndex, childLimit;
+ final int nextSpanDiff = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1;
+
+ if (mShouldReverseLayout) {
+ firstChildIndex = endChildIndex - 1;
+ childLimit = startChildIndex - 1;
+ } else {
+ firstChildIndex = startChildIndex;
+ childLimit = endChildIndex;
+ }
+ final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1;
+ for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) {
+ View child = getChildAt(i);
+ final int start = mPrimaryOrientation.getDecoratedStart(child);
+ final int end = mPrimaryOrientation.getDecoratedEnd(child);
+ LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+ if (layoutParams.mFullSpan) {
+ continue; // quick reject
+ }
+ int nextSpanIndex = layoutParams.getSpanIndex() + nextSpanDiff;
+ while (nextSpanIndex >= 0 && nextSpanIndex < mSpanCount) {
+ Span nextSpan = mSpans[nextSpanIndex];
+ if (nextSpan.isEmpty(start, end)) {
+ return child;
+ }
+ nextSpanIndex += nextSpanDiff;
+ }
+ }
+ // everything looks good
+ return null;
+ }
+
+ @Override
+ public boolean supportsPredictiveItemAnimations() {
+ return true;
+ }
+
+ private void measureChildWithDecorationsAndMargin(View child, int widthSpec,
+ int heightSpec) {
+ final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + insets.left,
+ lp.rightMargin + insets.right);
+ heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + insets.top,
+ lp.bottomMargin + insets.bottom);
+ child.measure(widthSpec, heightSpec);
+ }
+
+ private int updateSpecWithExtra(int spec, int startInset, int endInset) {
+ if (startInset == 0 && endInset == 0) {
+ return spec;
+ }
+ final int mode = View.MeasureSpec.getMode(spec);
+ if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
+ return View.MeasureSpec.makeMeasureSpec(
+ View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
+ }
+ return spec;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ mPendingSavedState = (SavedState) state;
+ requestLayout();
+ } else if (DEBUG) {
+ Log.d(TAG, "invalid saved state class");
+ }
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (mPendingSavedState != null) {
+ return new SavedState(mPendingSavedState);
+ }
+ SavedState state = new SavedState();
+ state.mOrientation = mOrientation;
+ state.mReverseLayout = mReverseLayout;
+ state.mSpanCount = mSpanCount;
+ state.mAnchorLayoutFromEnd = mLastLayoutFromEnd;
+ state.mGapStrategy = mGapStrategy;
+
+ if (mLazySpanLookup != null && mLazySpanLookup.mData != null) {
+ state.mSpanLookup = mLazySpanLookup.mData;
+ state.mSpanLookupSize = state.mSpanLookup.length;
+ } else {
+ state.mSpanLookupSize = 0;
+ }
+
+ if (getChildCount() > 0) {
+ state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition()
+ : getFirstChildPosition();
+ state.mHasSpanOffsets = true;
+ state.mSpanOffsets = new int[mSpanCount];
+ for (int i = 0; i < mSpanCount; i++) {
+ state.mSpanOffsets[i] = mLastLayoutFromEnd ? mSpans[i].getEndLine()
+ : mSpans[i].getStartLine();
+ }
+ } else {
+ state.mAnchorPosition = RecyclerView.NO_POSITION;
+ state.mHasSpanOffsets = false;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "saved state:\n" + state);
+ }
+ return state;
+ }
+
+ private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state,
+ boolean canOffsetChildren) {
+ final int maxEndLine = getMaxEnd(mPrimaryOrientation.getEndAfterPadding());
+ int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine;
+ int fixOffset;
+ if (gap > 0) {
+ fixOffset = -scrollBy(-gap, recycler, state);
+ } else {
+ return; // nothing to fix
+ }
+ gap -= fixOffset;
+ if (canOffsetChildren && gap > 0) {
+ mPrimaryOrientation.offsetChildren(gap);
+ }
+ }
+
+ private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state,
+ boolean canOffsetChildren) {
+ final int minStartLine = getMinStart(mPrimaryOrientation.getStartAfterPadding());
+ int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding();
+ int fixOffset;
+ if (gap > 0) {
+ fixOffset = scrollBy(gap, recycler, state);
+ } else {
+ return; // nothing to fix
+ }
+ gap -= fixOffset;
+ if (canOffsetChildren && gap > 0) {
+ mPrimaryOrientation.offsetChildren(-gap);
+ }
+ }
+
+ private void updateLayoutStateToFillStart(int anchorPosition, RecyclerView.State state) {
+ mLayoutState.mAvailable = 0;
+ mLayoutState.mCurrentPosition = anchorPosition;
+ if (isSmoothScrolling()) {
+ final int targetPos = state.getTargetScrollPosition();
+ if (mShouldReverseLayout == targetPos < anchorPosition) {
+ mLayoutState.mExtra = 0;
+ } else {
+ mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace();
+ }
+ } else {
+ mLayoutState.mExtra = 0;
+ }
+ mLayoutState.mLayoutDirection = LAYOUT_START;
+ mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL
+ : ITEM_DIRECTION_HEAD;
+ }
+
+ private void updateLayoutStateToFillEnd(int anchorPosition, RecyclerView.State state) {
+ mLayoutState.mAvailable = 0;
+ mLayoutState.mCurrentPosition = anchorPosition;
+ if (isSmoothScrolling()) {
+ final int targetPos = state.getTargetScrollPosition();
+ if (mShouldReverseLayout == targetPos > anchorPosition) {
+ mLayoutState.mExtra = 0;
+ } else {
+ mLayoutState.mExtra = mPrimaryOrientation.getTotalSpace();
+ }
+ } else {
+ mLayoutState.mExtra = 0;
+ }
+ mLayoutState.mLayoutDirection = LAYOUT_END;
+ mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD
+ : ITEM_DIRECTION_TAIL;
+ }
+
+ @Override
+ public void offsetChildrenHorizontal(int dx) {
+ super.offsetChildrenHorizontal(dx);
+ for (int i = 0; i < mSpanCount; i++) {
+ mSpans[i].onOffset(dx);
+ }
+ }
+
+ @Override
+ public void offsetChildrenVertical(int dy) {
+ super.offsetChildrenVertical(dy);
+ for (int i = 0; i < mSpanCount; i++) {
+ mSpans[i].onOffset(dy);
+ }
+ }
+
+ @Override
+ public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
+ if (!considerSpanInvalidate(positionStart, itemCount)) {
+ // If positions are not invalidated, move span offsets.
+ mLazySpanLookup.offsetForRemoval(positionStart, itemCount);
+ }
+ }
+
+ @Override
+ public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+ if (!considerSpanInvalidate(positionStart, itemCount)) {
+ // If positions are not invalidated, move span offsets.
+ mLazySpanLookup.offsetForAddition(positionStart, itemCount);
+ }
+ }
+
+ /**
+ * Checks whether it should invalidate span assignments in response to an adapter change.
+ */
+ private boolean considerSpanInvalidate(int positionStart, int itemCount) {
+ int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition();
+ if (positionStart + itemCount <= minPosition) {
+ return false;// nothing to update.
+ }
+ int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition();
+ mLazySpanLookup.invalidateAfter(positionStart);
+ if (positionStart <= maxPosition) {
+ requestLayout();
+ }
+ return true;
+ }
+
+ private int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
+ RecyclerView.State state) {
+ mRemainingSpans.set(0, mSpanCount, true);
+ // The target position we are trying to reach.
+ final int targetLine;
+
+ /*
+ * The line until which we can recycle, as long as we add views.
+ * Keep in mind, it is still the line in layout direction which means; to calculate the
+ * actual recycle line, we should subtract/add the size in orientation.
+ */
+ final int recycleLine;
+ // Line of the furthest row.
+ if (layoutState.mLayoutDirection == LAYOUT_END) {
+ // ignore padding for recycler
+ recycleLine = mPrimaryOrientation.getEndAfterPadding() + mLayoutState.mAvailable;
+ targetLine = recycleLine + mLayoutState.mExtra + mPrimaryOrientation.getEndPadding();
+ final int defaultLine = mPrimaryOrientation.getStartAfterPadding();
+ for (int i = 0; i < mSpanCount; i++) {
+ final Span span = mSpans[i];
+ final int line = span.getEndLine(defaultLine);
+ if (line > targetLine) {
+ mRemainingSpans.set(i, false);
+ }
+ }
+ } else { // LAYOUT_START
+ // ignore padding for recycler
+ recycleLine = mPrimaryOrientation.getStartAfterPadding() - mLayoutState.mAvailable;
+ targetLine = recycleLine - mLayoutState.mExtra -
+ mPrimaryOrientation.getStartAfterPadding();
+ for (int i = 0; i < mSpanCount; i++) {
+ final Span span = mSpans[i];
+ final int defaultLine = mPrimaryOrientation.getEndAfterPadding();
+ final int line = span.getStartLine(defaultLine);
+ if (line < targetLine) {
+ mRemainingSpans.set(i, false);
+ }
+ }
+ }
+
+ final int widthSpec, heightSpec;
+ if (mOrientation == VERTICAL) {
+ widthSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY);
+ heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ } else {
+ heightSpec = View.MeasureSpec.makeMeasureSpec(mSizePerSpan, View.MeasureSpec.EXACTLY);
+ widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ }
+
+ while (layoutState.hasMore(state) && !mRemainingSpans.isEmpty()) {
+ View view = layoutState.next(recycler);
+ LayoutParams lp = ((LayoutParams) view.getLayoutParams());
+ if (layoutState.mLayoutDirection == LAYOUT_END) {
+ addView(view);
+ } else {
+ addView(view, 0);
+ }
+ if (lp.mFullSpan) {
+ final int fullSizeSpec = View.MeasureSpec.makeMeasureSpec(
+ mSecondaryOrientation.getTotalSpace(), View.MeasureSpec.EXACTLY);
+ if (mOrientation == VERTICAL) {
+ measureChildWithDecorationsAndMargin(view, fullSizeSpec, heightSpec);
+ } else {
+ measureChildWithDecorationsAndMargin(view, widthSpec, fullSizeSpec);
+ }
+ } else {
+ measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec);
+ }
+
+ final int position = getPosition(view);
+ final int spanIndex = mLazySpanLookup.getSpan(position);
+ Span currentSpan;
+ if (spanIndex == LayoutParams.INVALID_SPAN_ID) {
+ if (lp.mFullSpan) {
+ // assign full span items to first span
+ currentSpan = mSpans[0];
+ } else {
+ currentSpan = getNextSpan(layoutState);
+ }
+ mLazySpanLookup.setSpan(position, currentSpan);
+ } else {
+ currentSpan = mSpans[spanIndex];
+ }
+ final int start;
+ final int end;
+ if (layoutState.mLayoutDirection == LAYOUT_END) {
+ final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding()
+ : mPrimaryOrientation.getStartAfterPadding();
+ start = lp.mFullSpan ? getMaxEnd(def) : currentSpan.getEndLine(def);
+ end = start + mPrimaryOrientation.getDecoratedMeasurement(view);
+ if (lp.mFullSpan) {
+ for (int i = 0; i < mSpanCount; i++) {
+ mSpans[i].appendToSpan(view);
+ }
+ } else {
+ currentSpan.appendToSpan(view);
+ }
+ } else {
+ final int def = mShouldReverseLayout ? mPrimaryOrientation.getEndAfterPadding()
+ : mPrimaryOrientation.getStartAfterPadding();
+ end = lp.mFullSpan ? getMinStart(def) : currentSpan.getStartLine(def);
+ start = end - mPrimaryOrientation.getDecoratedMeasurement(view);
+ if (lp.mFullSpan) {
+ for (int i = 0; i < mSpanCount; i++) {
+ mSpans[i].prependToSpan(view);
+ }
+ } else {
+ currentSpan.prependToSpan(view);
+ }
+
+ }
+ lp.mSpan = currentSpan;
+
+ if (DEBUG) {
+ Log.d(TAG, "adding view item " + lp.getViewPosition() + " between " + start + ","
+ + end);
+ }
+
+ final int otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding()
+ : currentSpan.mIndex * mSizePerSpan + mSecondaryOrientation
+ .getStartAfterPadding();
+ final int otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view);
+ if (mOrientation == VERTICAL) {
+ layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end);
+ } else {
+ layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd);
+ }
+ if (lp.mFullSpan) {
+ for (int i = 0; i < mSpanCount; i++) {
+ updateRemainingSpans(mSpans[i], mLayoutState.mLayoutDirection, targetLine);
+ }
+ } else {
+ updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine);
+ }
+ if (mLayoutState.mLayoutDirection == LAYOUT_START) {
+ // calculate recycle line
+ int maxStart = getMaxStart(currentSpan.getStartLine());
+ recycleFromEnd(recycler, Math.max(recycleLine, maxStart) +
+ (mPrimaryOrientation.getEnd() - mPrimaryOrientation.getStartAfterPadding()));
+ } else {
+ // calculate recycle line
+ int minEnd = getMinEnd(currentSpan.getEndLine());
+ recycleFromStart(recycler, Math.min(recycleLine, minEnd) -
+ (mPrimaryOrientation.getEnd() - mPrimaryOrientation.getStartAfterPadding()));
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "fill, " + getChildCount());
+ }
+ if (mLayoutState.mLayoutDirection == LAYOUT_START) {
+ final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding());
+ return Math.max(0, mLayoutState.mAvailable + (recycleLine - minStart));
+ } else {
+ final int max = getMaxEnd(mPrimaryOrientation.getEndAfterPadding());
+ return Math.max(0, mLayoutState.mAvailable + (max - recycleLine));
+ }
+ }
+
+ private void layoutDecoratedWithMargins(View child, int left, int top, int right,
+ int bottom) {
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ layoutDecorated(child, left + lp.leftMargin, top + lp.topMargin, right - lp.rightMargin
+ , bottom - lp.bottomMargin);
+ }
+
+ private void updateRemainingSpans(Span span, int layoutDir, int targetLine) {
+ final int deletedSize = span.getDeletedSize();
+ if (layoutDir == LAYOUT_START) {
+ final int line = span.getStartLine();
+ if (line + deletedSize < targetLine) {
+ mRemainingSpans.set(span.mIndex, false);
+ }
+ } else {
+ final int line = span.getEndLine();
+ if (line - deletedSize > targetLine) {
+ mRemainingSpans.set(span.mIndex, false);
+ }
+ }
+ }
+
+ private int getMaxStart(int def) {
+ int maxStart = mSpans[0].getStartLine(def);
+ for (int i = 1; i < mSpanCount; i++) {
+ final int spanStart = mSpans[i].getStartLine(def);
+ if (spanStart > maxStart) {
+ maxStart = spanStart;
+ }
+ }
+ return maxStart;
+ }
+
+ private int getMinStart(int def) {
+ int minStart = mSpans[0].getStartLine(def);
+ for (int i = 1; i < mSpanCount; i++) {
+ final int spanStart = mSpans[i].getStartLine(def);
+ if (spanStart < minStart) {
+ minStart = spanStart;
+ }
+ }
+ return minStart;
+ }
+
+ private int getMaxEnd(int def) {
+ int maxEnd = mSpans[0].getEndLine(def);
+ for (int i = 1; i < mSpanCount; i++) {
+ final int spanEnd = mSpans[i].getEndLine(def);
+ if (spanEnd > maxEnd) {
+ maxEnd = spanEnd;
+ }
+ }
+ return maxEnd;
+ }
+
+ private int getMinEnd(int def) {
+ int minEnd = mSpans[0].getEndLine(def);
+ for (int i = 1; i < mSpanCount; i++) {
+ final int spanEnd = mSpans[i].getEndLine(def);
+ if (spanEnd < minEnd) {
+ minEnd = spanEnd;
+ }
+ }
+ return minEnd;
+ }
+
+ private void recycleFromStart(RecyclerView.Recycler recycler, int line) {
+ if (DEBUG) {
+ Log.d(TAG, "recycling from start for line " + line);
+ }
+ while (getChildCount() > 0) {
+ View child = getChildAt(0);
+ if (mPrimaryOrientation.getDecoratedEnd(child) < line) {
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mFullSpan) {
+ for (int j = 0; j < mSpanCount; j++) {
+ mSpans[j].popStart();
+ }
+ } else {
+ lp.mSpan.popStart();
+ }
+ removeAndRecycleView(child, recycler);
+ } else {
+ return;// done
+ }
+ }
+ }
+
+ private void recycleFromEnd(RecyclerView.Recycler recycler, int line) {
+ final int childCount = getChildCount();
+ int i;
+ for (i = childCount - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (mPrimaryOrientation.getDecoratedStart(child) > line) {
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (lp.mFullSpan) {
+ for (int j = 0; j < mSpanCount; j++) {
+ mSpans[j].popEnd();
+ }
+ } else {
+ lp.mSpan.popEnd();
+ }
+ removeAndRecycleView(child, recycler);
+ } else {
+ return;// done
+ }
+ }
+ }
+
+ /**
+ * Finds the span for the next view.
+ */
+ private Span getNextSpan(LayoutState layoutState) {
+ final boolean preferLastSpan = mOrientation == VERTICAL && isLayoutRTL();
+ if (layoutState.mLayoutDirection == LAYOUT_END) {
+ Span min = mSpans[0];
+ int minLine = min.getEndLine(mPrimaryOrientation.getStartAfterPadding());
+ final int defaultLine = mPrimaryOrientation.getStartAfterPadding();
+ for (int i = 1; i < mSpanCount; i++) {
+ final Span other = mSpans[i];
+ final int otherLine = other.getEndLine(defaultLine);
+ if (otherLine < minLine || (otherLine == minLine && preferLastSpan)) {
+ min = other;
+ minLine = otherLine;
+ }
+ }
+ return min;
+ } else {
+ Span max = mSpans[0];
+ int maxLine = max.getStartLine(mPrimaryOrientation.getEndAfterPadding());
+ final int defaultLine = mPrimaryOrientation.getEndAfterPadding();
+ for (int i = 1; i < mSpanCount; i++) {
+ final Span other = mSpans[i];
+ final int otherLine = other.getStartLine(defaultLine);
+ if (otherLine > maxLine || (otherLine == maxLine && !preferLastSpan)) {
+ max = other;
+ maxLine = otherLine;
+ }
+ }
+ return max;
+ }
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return mOrientation == VERTICAL;
+ }
+
+ @Override
+ public boolean canScrollHorizontally() {
+ return mOrientation == HORIZONTAL;
+ }
+
+ @Override
+ public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ return scrollBy(dx, recycler, state);
+ }
+
+ @Override
+ public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ return scrollBy(dy, recycler, state);
+ }
+
+ private int calculateScrollDirectionForPosition(int position) {
+ if (getChildCount() == 0) {
+ return mShouldReverseLayout ? LAYOUT_END : LAYOUT_START;
+ }
+ final int firstChildPos = getFirstChildPosition();
+ return position < firstChildPos != mShouldReverseLayout ? LAYOUT_START : LAYOUT_END;
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
+ int position) {
+ LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ final int direction = calculateScrollDirectionForPosition(targetPosition);
+ if (direction == 0) {
+ return null;
+ }
+ if (mOrientation == HORIZONTAL) {
+ return new PointF(direction, 0);
+ } else {
+ return new PointF(0, direction);
+ }
+ }
+ };
+ scroller.setTargetPosition(position);
+ startSmoothScroll(scroller);
+ }
+
+ @Override
+ public void scrollToPosition(int position) {
+ if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) {
+ mPendingSavedState.invalidateAnchorPositionInfo();
+ }
+ mPendingScrollPosition = position;
+ mPendingScrollPositionOffset = INVALID_OFFSET;
+ requestLayout();
+ }
+
+ /**
+ * Scroll to the specified adapter position with the given offset from layout start.
+ * <p>
+ * Note that scroll position change will not be reflected until the next layout call.
+ * <p>
+ * If you are just trying to make a position visible, use {@link #scrollToPosition(int)}.
+ *
+ * @param position Index (starting at 0) of the reference item.
+ * @param offset The distance (in pixels) between the start edge of the item view and
+ * start edge of the RecyclerView.
+ * @see #setReverseLayout(boolean)
+ * @see #scrollToPosition(int)
+ */
+ public void scrollToPositionWithOffset(int position, int offset) {
+ if (mPendingSavedState != null) {
+ mPendingSavedState.invalidateAnchorPositionInfo();
+ }
+ mPendingScrollPosition = position;
+ mPendingScrollPositionOffset = offset;
+ requestLayout();
+ }
+
+ private int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ ensureOrientationHelper();
+ final int referenceChildPosition;
+ if (dt > 0) { // layout towards end
+ mLayoutState.mLayoutDirection = LAYOUT_END;
+ mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_HEAD
+ : ITEM_DIRECTION_TAIL;
+ referenceChildPosition = getLastChildPosition();
+ } else {
+ mLayoutState.mLayoutDirection = LAYOUT_START;
+ mLayoutState.mItemDirection = mShouldReverseLayout ? ITEM_DIRECTION_TAIL
+ : ITEM_DIRECTION_HEAD;
+ referenceChildPosition = getFirstChildPosition();
+ }
+ mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection;
+ final int absDt = Math.abs(dt);
+ mLayoutState.mAvailable = absDt;
+ mLayoutState.mExtra = isSmoothScrolling() ? mPrimaryOrientation.getTotalSpace() : 0;
+ int consumed = fill(recycler, mLayoutState, state);
+ final int totalScroll;
+ if (absDt < consumed) {
+ totalScroll = dt;
+ } else if (dt < 0) {
+ totalScroll = -consumed;
+ } else { // dt > 0
+ totalScroll = consumed;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "asked " + dt + " scrolled" + totalScroll);
+ }
+
+ if (mGapStrategy == GAP_HANDLING_LAZY
+ && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD) {
+ final int targetStart = mPrimaryOrientation.getStartAfterPadding();
+ final int targetEnd = mPrimaryOrientation.getEndAfterPadding();
+ lazyOffsetSpans(-totalScroll, targetStart, targetEnd);
+ } else {
+ mPrimaryOrientation.offsetChildren(-totalScroll);
+ }
+ // always reset this if we scroll for a proper save instance state
+ mLastLayoutFromEnd = mShouldReverseLayout;
+
+ if (totalScroll != 0 && mGapStrategy != GAP_HANDLING_NONE
+ && mLayoutState.mItemDirection == ITEM_DIRECTION_HEAD && !mHasGaps) {
+ final int addedChildCount = Math.abs(mLayoutState.mCurrentPosition
+ - (referenceChildPosition + mLayoutState.mItemDirection));
+ if (addedChildCount > 0) {
+ // check if any child has been attached to wrong span. If so, trigger a re-layout
+ // after scroll
+ final View viewInWrongSpan;
+ final View referenceView = findViewByPosition(referenceChildPosition);
+ if (referenceView == null) {
+ viewInWrongSpan = hasGapsToFix(0, getChildCount());
+ } else {
+ if (mLayoutState.mLayoutDirection == LAYOUT_START) {
+ viewInWrongSpan = hasGapsToFix(0, addedChildCount);
+ } else {
+ viewInWrongSpan = hasGapsToFix(getChildCount() - addedChildCount,
+ getChildCount());
+ }
+ }
+ mHasGaps = viewInWrongSpan != null;
+ }
+ }
+ return totalScroll;
+ }
+
+ /**
+ * The actual method that implements {@link #GAP_HANDLING_LAZY}
+ */
+ private void lazyOffsetSpans(int offset, int targetStart, int targetEnd) {
+ // For each span offset children one by one.
+ // When a fullSpan item is reached, stop and wait for other spans to reach to that span.
+ // When all reach, offset fullSpan to max of others and continue.
+ int childrenToOffset = getChildCount();
+ int[] indexPerSpan = new int[mSpanCount];
+ int[] offsetPerSpan = new int[mSpanCount];
+
+ final int childOrder = offset > 0 ? ITEM_DIRECTION_TAIL : ITEM_DIRECTION_HEAD;
+ if (offset > 0) {
+ Arrays.fill(indexPerSpan, 0);
+ } else {
+ for (int i = 0; i < mSpanCount; i++) {
+ indexPerSpan[i] = mSpans[i].mViews.size() - 1;
+ }
+ }
+
+ for (int i = 0; i < mSpanCount; i++) {
+ offsetPerSpan[i] = mSpans[i].getNormalizedOffset(offset, targetStart, targetEnd);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "lazy offset start. normalized: " + Arrays.toString(offsetPerSpan));
+ }
+
+ while (childrenToOffset > 0) {
+ View fullSpanView = null;
+ for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) {
+ Span span = mSpans[spanIndex];
+ int viewIndex;
+ for (viewIndex = indexPerSpan[spanIndex];
+ viewIndex < span.mViews.size() && viewIndex >= 0; viewIndex += childOrder) {
+ View view = span.mViews.get(viewIndex);
+ if (DEBUG) {
+ Log.d(TAG, "span " + spanIndex + ", view:" + viewIndex + ", pos:"
+ + getPosition(view));
+ }
+ LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ if (lp.mFullSpan) {
+ if (DEBUG) {
+ Log.d(TAG, "stopping on full span view on index " + viewIndex
+ + " in span " + spanIndex);
+ }
+ fullSpanView = view;
+ viewIndex += childOrder;// move to next view
+ break;
+ }
+ // offset this child normally
+ mPrimaryOrientation.offsetChild(view, offsetPerSpan[spanIndex]);
+ final int nextChildIndex = viewIndex + childOrder;
+ if (nextChildIndex < span.mViews.size() && nextChildIndex >= 0) {
+ View nextView = span.mViews.get(nextChildIndex);
+ // find gap between, before offset
+ if (childOrder == ITEM_DIRECTION_HEAD) {// negative
+ offsetPerSpan[spanIndex] = Math
+ .min(0, mPrimaryOrientation.getDecoratedStart(view)
+ - mPrimaryOrientation.getDecoratedEnd(nextView));
+ } else {
+ offsetPerSpan[spanIndex] = Math
+ .max(0, mPrimaryOrientation.getDecoratedEnd(view) -
+ mPrimaryOrientation.getDecoratedStart(nextView));
+ }
+ if (DEBUG) {
+ Log.d(TAG, "offset diff:" + offsetPerSpan[spanIndex] + " between "
+ + getPosition(nextView) + " and " + getPosition(view));
+ }
+ }
+ childrenToOffset--;
+ }
+ indexPerSpan[spanIndex] = viewIndex;
+ }
+ if (fullSpanView != null) {
+ // we have to offset this view. We'll offset it as the biggest amount necessary
+ int winnerSpan = 0;
+ int winnerSpanOffset = Math.abs(offsetPerSpan[winnerSpan]);
+ for (int i = 1; i < mSpanCount; i++) {
+ final int spanOffset = Math.abs(offsetPerSpan[i]);
+ if (spanOffset > winnerSpanOffset) {
+ winnerSpan = i;
+ winnerSpanOffset = spanOffset;
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "winner offset:" + offsetPerSpan[winnerSpan] + " of " + winnerSpan);
+ }
+ mPrimaryOrientation.offsetChild(fullSpanView, offsetPerSpan[winnerSpan]);
+ childrenToOffset--;
+
+ for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) {
+ final int nextViewIndex = indexPerSpan[spanIndex];
+ final Span span = mSpans[spanIndex];
+ if (nextViewIndex < span.mViews.size() && nextViewIndex > 0) {
+ View nextView = span.mViews.get(nextViewIndex);
+ // find gap between, before offset
+ if (childOrder == ITEM_DIRECTION_HEAD) {// negative
+ offsetPerSpan[spanIndex] = Math
+ .min(0, mPrimaryOrientation.getDecoratedStart(fullSpanView)
+ - mPrimaryOrientation.getDecoratedEnd(nextView));
+ } else {
+ offsetPerSpan[spanIndex] = Math
+ .max(0, mPrimaryOrientation.getDecoratedEnd(fullSpanView) -
+ mPrimaryOrientation.getDecoratedStart(nextView));
+ }
+ }
+ }
+ }
+ }
+ for (int spanIndex = 0; spanIndex < mSpanCount; spanIndex++) {
+ mSpans[spanIndex].invalidateCache();
+ }
+ }
+
+ private int getLastChildPosition() {
+ final int childCount = getChildCount();
+ return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1));
+ }
+
+ private int getFirstChildPosition() {
+ final int childCount = getChildCount();
+ return childCount == 0 ? 0 : getPosition(getChildAt(0));
+ }
+
+ /**
+ * Finds the first View that can be used as an anchor View.
+ *
+ * @return Position of the View or 0 if it cannot find any such View.
+ */
+ private int findFirstReferenceChildPosition(int itemCount) {
+ final int limit = getChildCount();
+ for (int i = 0; i < limit; i++) {
+ final View view = getChildAt(i);
+ final int position = getPosition(view);
+ if (position >= 0 && position < itemCount) {
+ return position;
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * Finds the last View that can be used as an anchor View.
+ *
+ * @return Position of the View or 0 if it cannot find any such View.
+ */
+ private int findLastReferenceChildPosition(int itemCount) {
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ final View view = getChildAt(i);
+ final int position = getPosition(view);
+ if (position >= 0 && position < itemCount) {
+ return position;
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
+ return new LayoutParams(c, attrs);
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (lp instanceof ViewGroup.MarginLayoutParams) {
+ return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
+ } else {
+ return new LayoutParams(lp);
+ }
+ }
+
+ @Override
+ public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
+ return lp instanceof LayoutParams;
+ }
+
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+
+ /**
+ * LayoutParams used by StaggeredGridLayoutManager.
+ */
+ public static class LayoutParams extends RecyclerView.LayoutParams {
+
+ /**
+ * Span Id for Views that are not laid out yet.
+ */
+ public static final int INVALID_SPAN_ID = -1;
+
+ // Package scope to be able to access from tests.
+ Span mSpan;
+
+ boolean mFullSpan;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(ViewGroup.MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(RecyclerView.LayoutParams source) {
+ super(source);
+ }
+
+ /**
+ * When set to true, the item will layout using all span area. That means, if orientation
+ * is vertical, the view will have full width; if orientation is horizontal, the view will
+ * have full height.
+ *
+ * @param fullSpan True if this item should traverse all spans.
+ */
+ public void setFullSpan(boolean fullSpan) {
+ mFullSpan = fullSpan;
+ }
+
+ /**
+ * Returns the Span index to which this View is assigned.
+ *
+ * @return The Span index of the View. If View is not yet assigned to any span, returns
+ * {@link #INVALID_SPAN_ID}.
+ */
+ public final int getSpanIndex() {
+ if (mSpan == null) {
+ return INVALID_SPAN_ID;
+ }
+ return mSpan.mIndex;
+ }
+ }
+
+ // Package scoped to access from tests.
+ class Span {
+
+ final int INVALID_LINE = Integer.MIN_VALUE;
+
+ private ArrayList<View> mViews = new ArrayList<View>();
+
+ int mCachedStart = INVALID_LINE;
+
+ int mCachedEnd = INVALID_LINE;
+
+ int mDeletedSize = 0;
+
+ final int mIndex;
+
+ private Span(int index) {
+ mIndex = index;
+ }
+
+ int getStartLine(int def) {
+ if (mCachedStart != INVALID_LINE) {
+ return mCachedStart;
+ }
+ if (mViews.size() == 0) {
+ return def;
+ }
+ mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0));
+ return mCachedStart;
+ }
+
+ // Use this one when default value does not make sense and not having a value means a bug.
+ int getStartLine() {
+ if (mCachedStart != INVALID_LINE) {
+ return mCachedStart;
+ }
+ mCachedStart = mPrimaryOrientation.getDecoratedStart(mViews.get(0));
+ return mCachedStart;
+ }
+
+ int getEndLine(int def) {
+ if (mCachedEnd != INVALID_LINE) {
+ return mCachedEnd;
+ }
+ final int size = mViews.size();
+ if (size == 0) {
+ return def;
+ }
+ mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(size - 1));
+ return mCachedEnd;
+ }
+
+ // Use this one when default value does not make sense and not having a value means a bug.
+ int getEndLine() {
+ if (mCachedEnd != INVALID_LINE) {
+ return mCachedEnd;
+ }
+ mCachedEnd = mPrimaryOrientation.getDecoratedEnd(mViews.get(mViews.size() - 1));
+ return mCachedEnd;
+ }
+
+ void prependToSpan(View view) {
+ LayoutParams lp = getLayoutParams(view);
+ lp.mSpan = this;
+ mViews.add(0, view);
+ mCachedStart = INVALID_LINE;
+ if (mViews.size() == 1) {
+ mCachedEnd = INVALID_LINE;
+ }
+ if (lp.isItemRemoved()) {
+ mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view);
+ }
+ }
+
+ void appendToSpan(View view) {
+ LayoutParams lp = getLayoutParams(view);
+ lp.mSpan = this;
+ mViews.add(view);
+ mCachedEnd = INVALID_LINE;
+ if (mViews.size() == 1) {
+ mCachedStart = INVALID_LINE;
+ }
+ if (lp.isItemRemoved()) {
+ mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view);
+ }
+ }
+
+ // Useful method to preserve positions on a re-layout.
+ void cacheReferenceLineAndClear(boolean reverseLayout, int offset) {
+ int reference;
+ if (reverseLayout) {
+ reference = getEndLine(INVALID_LINE);
+ } else {
+ reference = getStartLine(INVALID_LINE);
+ }
+ clear();
+ if (reference == INVALID_LINE) {
+ return;
+ }
+ if (offset != INVALID_OFFSET) {
+ reference += offset;
+ }
+ mCachedStart = mCachedEnd = reference;
+ }
+
+ void clear() {
+ mViews.clear();
+ invalidateCache();
+ mDeletedSize = 0;
+ }
+
+ void invalidateCache() {
+ mCachedStart = INVALID_LINE;
+ mCachedEnd = INVALID_LINE;
+ }
+
+ void setLine(int line) {
+ mCachedEnd = mCachedStart = line;
+ }
+
+ void popEnd() {
+ final int size = mViews.size();
+ View end = mViews.remove(size - 1);
+ final LayoutParams lp = getLayoutParams(end);
+ lp.mSpan = null;
+ if (lp.isItemRemoved()) {
+ mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end);
+ }
+ if (size == 1) {
+ mCachedStart = INVALID_LINE;
+ }
+ mCachedEnd = INVALID_LINE;
+ }
+
+ void popStart() {
+ View start = mViews.remove(0);
+ final LayoutParams lp = getLayoutParams(start);
+ lp.mSpan = null;
+ if (mViews.size() == 0) {
+ mCachedEnd = INVALID_LINE;
+ }
+ if (lp.isItemRemoved()) {
+ mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start);
+ }
+ mCachedStart = INVALID_LINE;
+ }
+
+ // TODO cache this.
+ public int getDeletedSize() {
+ return mDeletedSize;
+ }
+
+ LayoutParams getLayoutParams(View view) {
+ return (LayoutParams) view.getLayoutParams();
+ }
+
+ void onOffset(int dt) {
+ if (mCachedStart != INVALID_LINE) {
+ mCachedStart += dt;
+ }
+ if (mCachedEnd != INVALID_LINE) {
+ mCachedEnd += dt;
+ }
+ }
+
+ // normalized offset is how much this span can scroll
+ int getNormalizedOffset(int dt, int targetStart, int targetEnd) {
+ if (mViews.size() == 0) {
+ return 0;
+ }
+ if (dt < 0) {
+ final int endSpace = getEndLine() - targetEnd;
+ if (endSpace <= 0) {
+ return 0;
+ }
+ return -dt > endSpace ? -endSpace : dt;
+ } else {
+ final int startSpace = targetStart - getStartLine();
+ if (startSpace <= 0) {
+ return 0;
+ }
+ return startSpace < dt ? startSpace : dt;
+ }
+ }
+
+ /**
+ * Returns if there is no child between start-end lines
+ *
+ * @param start The start line
+ * @param end The end line
+ * @return true if a new child can be added between start and end
+ */
+ boolean isEmpty(int start, int end) {
+ final int count = mViews.size();
+ for (int i = 0; i < count; i++) {
+ final View view = mViews.get(i);
+ if (mPrimaryOrientation.getDecoratedStart(view) < end &&
+ mPrimaryOrientation.getDecoratedEnd(view) > start) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ /**
+ * An array of mappings from adapter position to span.
+ * This only grows when a write happens and it grows up to the size of the adapter.
+ */
+ static class LazySpanLookup {
+
+ private static final int MIN_SIZE = 10;
+
+ int[] mData;
+
+ int mAdapterSize; // we don't want to grow beyond that, unless it grows
+
+ void invalidateAfter(int position) {
+ if (mData == null) {
+ return;
+ }
+ if (position >= mData.length) {
+ return;
+ }
+ Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID);
+ }
+
+ int getSpan(int position) {
+ if (mData == null || position >= mData.length) {
+ return LayoutParams.INVALID_SPAN_ID;
+ } else {
+ return mData[position];
+ }
+ }
+
+ void setSpan(int position, Span span) {
+ ensureSize(position);
+ mData[position] = span.mIndex;
+ }
+
+ int sizeForPosition(int position) {
+ int len = mData.length;
+ while (len <= position) {
+ len *= 2;
+ }
+ if (len > mAdapterSize) {
+ len = mAdapterSize;
+ }
+ return len;
+ }
+
+ void ensureSize(int position) {
+ if (mData == null) {
+ mData = new int[Math.max(position, MIN_SIZE) + 1];
+ Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID);
+ } else if (position >= mData.length) {
+ int[] old = mData;
+ mData = new int[sizeForPosition(position)];
+ System.arraycopy(old, 0, mData, 0, old.length);
+ Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID);
+ }
+ }
+
+ void clear() {
+ if (mData != null) {
+ Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID);
+ }
+ }
+
+ void offsetForRemoval(int positionStart, int itemCount) {
+ ensureSize(positionStart + itemCount);
+ System.arraycopy(mData, positionStart + itemCount, mData, positionStart,
+ mData.length - positionStart - itemCount);
+ Arrays.fill(mData, mData.length - itemCount, mData.length,
+ LayoutParams.INVALID_SPAN_ID);
+ }
+
+ void offsetForAddition(int positionStart, int itemCount) {
+ ensureSize(positionStart + itemCount);
+ System.arraycopy(mData, positionStart, mData, positionStart + itemCount,
+ mData.length - positionStart - itemCount);
+ Arrays.fill(mData, positionStart, positionStart + itemCount,
+ LayoutParams.INVALID_SPAN_ID);
+ }
+ }
+
+ static class SavedState implements Parcelable {
+
+ int mOrientation;
+
+ int mSpanCount;
+
+ int mGapStrategy;
+
+ int mAnchorPosition;
+
+ int[] mSpanOffsets;
+
+ int mSpanLookupSize;
+
+ int[] mSpanLookup;
+
+ boolean mReverseLayout;
+
+ boolean mAnchorLayoutFromEnd;
+
+ boolean mHasSpanOffsets;
+
+ public SavedState() {
+ }
+
+ SavedState(Parcel in) {
+ mOrientation = in.readInt();
+ mSpanCount = in.readInt();
+ mGapStrategy = in.readInt();
+ mAnchorPosition = in.readInt();
+ mHasSpanOffsets = in.readInt() == 1;
+ if (mHasSpanOffsets) {
+ mSpanOffsets = new int[mSpanCount];
+ in.readIntArray(mSpanOffsets);
+ }
+
+ mSpanLookupSize = in.readInt();
+ if (mSpanLookupSize > 0) {
+ mSpanLookup = new int[mSpanLookupSize];
+ in.readIntArray(mSpanLookup);
+ }
+ mReverseLayout = in.readInt() == 1;
+ mAnchorLayoutFromEnd = in.readInt() == 1;
+ }
+
+ public SavedState(SavedState other) {
+ mOrientation = other.mOrientation;
+ mSpanCount = other.mSpanCount;
+ mGapStrategy = other.mGapStrategy;
+ mAnchorPosition = other.mAnchorPosition;
+ mHasSpanOffsets = other.mHasSpanOffsets;
+ mSpanOffsets = other.mSpanOffsets;
+ mSpanLookupSize = other.mSpanLookupSize;
+ mSpanLookup = other.mSpanLookup;
+ mReverseLayout = other.mReverseLayout;
+ mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
+ }
+
+ void invalidateSpanInfo() {
+ mSpanOffsets = null;
+ mHasSpanOffsets = false;
+ mSpanCount = -1;
+ mSpanLookupSize = 0;
+ mSpanLookup = null;
+ }
+
+ void invalidateAnchorPositionInfo() {
+ mSpanOffsets = null;
+ mHasSpanOffsets = false;
+ mAnchorPosition = RecyclerView.NO_POSITION;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mOrientation);
+ dest.writeInt(mSpanCount);
+ dest.writeInt(mGapStrategy);
+ dest.writeInt(mAnchorPosition);
+ dest.writeInt(mHasSpanOffsets ? 1 : 0);
+ if (mHasSpanOffsets) {
+ dest.writeIntArray(mSpanOffsets);
+ }
+ dest.writeInt(mSpanLookupSize);
+ if (mSpanLookupSize > 0) {
+ dest.writeIntArray(mSpanLookup);
+ }
+ dest.writeInt(mReverseLayout ? 1 : 0);
+ dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
+ }
+
+ @Override
+ public String toString() {
+ return "SavedState{" +
+ "mOrientation=" + mOrientation +
+ ", mSpanCount=" + mSpanCount +
+ ", mGapStrategy=" + mGapStrategy +
+ ", mAnchorPosition=" + mAnchorPosition +
+ ", mSpanOffsets=" + Arrays.toString(mSpanOffsets) +
+ ", mSpanLookupSize=" + mSpanLookupSize +
+ ", mSpanLookup=" + Arrays.toString(mSpanLookup) +
+ ", mReverseLayout=" + mReverseLayout +
+ ", mAnchorLayoutFromEnd=" + mAnchorLayoutFromEnd +
+ ", mHasSpanOffsets=" + mHasSpanOffsets +
+ '}';
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/AdapterHelperTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/AdapterHelperTest.java
new file mode 100644
index 0000000..d2708c3
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/AdapterHelperTest.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import android.test.AndroidTestCase;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static android.support.v7.widget.RecyclerView.*;
+
+public class AdapterHelperTest extends AndroidTestCase {
+
+ static final int NO_POSITION = -1;
+
+ List<ViewHolder> mViewHolders;
+
+ AdapterHelper mAdapterHelper;
+
+ List<AdapterHelper.UpdateOp> mFirstPassUpdates, mSecondPassUpdates;
+
+ TestAdapter mTestAdapter;
+
+ TestAdapter mPreProcessClone; // we clone adapter pre-process to run operations to see result
+
+ @Override
+ protected void setUp() throws Exception {
+ cleanState();
+ }
+
+ private void cleanState() {
+ mViewHolders = new ArrayList<ViewHolder>();
+ mFirstPassUpdates = new ArrayList<AdapterHelper.UpdateOp>();
+ mSecondPassUpdates = new ArrayList<AdapterHelper.UpdateOp>();
+ mAdapterHelper = new AdapterHelper(new AdapterHelper.Callback() {
+ @Override
+ public RecyclerView.ViewHolder findViewHolder(int position) {
+ for (ViewHolder vh : mViewHolders) {
+ if (vh.mPosition == position) {
+ return vh;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void offsetPositionsForRemovingInvisible(int positionStart, int itemCount) {
+ final int positionEnd = positionStart + itemCount;
+ for (ViewHolder holder : mViewHolders) {
+ if (holder.mPosition >= positionEnd) {
+ holder.offsetPosition(-itemCount, true);
+ } else if (holder.mPosition >= positionStart) {
+ holder.addFlags(ViewHolder.FLAG_REMOVED);
+ holder.offsetPosition(-itemCount, true);
+ }
+ }
+ }
+
+ @Override
+ public void offsetPositionsForRemovingLaidOutOrNewView(int positionStart,
+ int itemCount) {
+ final int positionEnd = positionStart + itemCount;
+ for (ViewHolder holder : mViewHolders) {
+ if (holder.mPosition >= positionEnd) {
+ holder.offsetPosition(-itemCount, false);
+ } else if (holder.mPosition >= positionStart) {
+ holder.addFlags(ViewHolder.FLAG_REMOVED);
+ holder.offsetPosition(-itemCount, false);
+ }
+ }
+ }
+
+ @Override
+ public void markViewHoldersUpdated(int positionStart, int itemCount) {
+ final int positionEnd = positionStart + itemCount;
+ for (ViewHolder holder : mViewHolders) {
+ if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
+ holder.addFlags(ViewHolder.FLAG_UPDATE);
+ }
+ }
+ }
+
+ @Override
+ public void onDispatchFirstPass(AdapterHelper.UpdateOp updateOp) {
+ mFirstPassUpdates.add(updateOp);
+ }
+
+ @Override
+ public void onDispatchSecondPass(AdapterHelper.UpdateOp updateOp) {
+ mSecondPassUpdates.add(updateOp);
+ }
+
+ @Override
+ public void offsetPositionsForAdd(int positionStart, int itemCount) {
+ for (ViewHolder holder : mViewHolders) {
+ if (holder != null && holder.mPosition >= positionStart) {
+ holder.offsetPosition(itemCount, false);
+ }
+ }
+ }
+ }, true);
+ }
+
+ void setupBasic(int count, int visibleStart, int visibleCount) {
+ mTestAdapter = new TestAdapter(count, mAdapterHelper);
+ for (int i = 0; i < visibleCount; i++) {
+ addViewHolder(visibleStart + i);
+ }
+ mPreProcessClone = mTestAdapter.createCopy();
+ }
+
+ private void addViewHolder(int posiiton) {
+ ViewHolder viewHolder = new RecyclerViewBasicTest.MockViewHolder(
+ new TextView(getContext()));
+ viewHolder.mPosition = posiiton;
+ mViewHolders.add(viewHolder);
+ }
+
+ public void testSinglePass() {
+ setupBasic(10, 2, 3);
+ add(2, 1);
+ rm(1, 2);
+ add(1, 5);
+ mAdapterHelper.consumeUpdatesInOnePass();
+ assertDispatch(0, 3);
+ }
+
+ public void testDeleteVisible() {
+ setupBasic(10, 2, 3);
+ rm(2, 1);
+ preProcess();
+ assertDispatch(0, 1);
+ }
+
+ public void testDeleteInvisible() {
+ setupBasic(10, 3, 4);
+ rm(2, 1);
+ preProcess();
+ assertDispatch(1, 0);
+ }
+
+ public void testAddCount() {
+ setupBasic(0, 0, 0);
+ add(0, 1);
+ assertEquals(1, mAdapterHelper.mPendingUpdates.size());
+ }
+
+ public void testDeleteCount() {
+ setupBasic(1, 0, 0);
+ rm(0, 1);
+ assertEquals(1, mAdapterHelper.mPendingUpdates.size());
+ }
+
+ public void testAddProcess() {
+ setupBasic(0, 0, 0);
+ add(0, 1);
+ preProcess();
+ assertEquals(0, mAdapterHelper.mPendingUpdates.size());
+ }
+
+ public void testAddRemoveSeparate() {
+ setupBasic(10, 2, 2);
+ add(6, 1);
+ rm(5, 1);
+ preProcess();
+ assertDispatch(1, 1);
+ }
+
+ public void testSchenerio1() {
+ setupBasic(10, 3, 2);
+ rm(4, 1);
+ rm(3, 1);
+ rm(3, 1);
+ preProcess();
+ assertDispatch(1, 2);
+ }
+
+ public void testDivideDelete() {
+ setupBasic(10, 3, 4);
+ rm(2, 2);
+ preProcess();
+ assertDispatch(1, 1);
+ }
+
+ public void testSchenerio2() {
+ setupBasic(10, 3, 3); // 3-4-5
+ add(4, 2); // 3 a b 4 5
+ rm(0, 1); // (0) 3(2) a(3) b(4) 4(3) 5(4)
+ rm(1, 3); // (1,2) (x) a(1) b(2) 4(3)
+ preProcess();
+ assertDispatch(2, 2);
+ }
+
+ public void testSchenerio3() {
+ setupBasic(10, 2, 2);
+ rm(0, 5);
+ preProcess();
+ assertDispatch(2, 1);
+ assertOps(mFirstPassUpdates, rmOp(0, 2), rmOp(2, 1));
+ assertOps(mSecondPassUpdates, rmOp(0, 2));
+ }
+
+ public void testSchenerio4() {
+ setupBasic(5, 0, 5);
+ // 0 1 2 3 4
+ // 0 1 2 a b 3 4
+ // 0 2 a b 3 4
+ // 0 c d 2 a b 3 4
+ // 0 c d 2 a 4
+ // c d 2 a 4
+ // pre: 0 1 2 3 4
+ add(3, 2);
+ rm(1, 1);
+ add(1, 2);
+ rm(5, 2);
+ rm(0, 1);
+ preProcess();
+ }
+
+ public void testSchenerio5() {
+ setupBasic(5, 0, 5);
+ // 0 1 2 3 4
+ // 0 1 2 a b 3 4
+ // 0 1 b 3 4
+ // pre: 0 1 2 3 4
+ // pre w/ adap: 0 1 2 b 3 4
+ add(3, 2);
+ rm(2, 2);
+ preProcess();
+ }
+
+ public void testRandom() {
+ Random random = new Random(System.nanoTime());
+ for (int i = 0; i < 10; i++) {
+ randomTest(random, i);
+ }
+ }
+
+ public void randomTest(Random random, int opCount) {
+ cleanState();
+ final int count = 10 + random.nextInt(100);
+ final int start = random.nextInt(count - 1);
+ final int layoutCount = Math.max(1, random.nextInt(count - start));
+ setupBasic(count, start, layoutCount);
+
+ while (opCount-- > 0) {
+ final int op = random.nextInt(AdapterHelper.UpdateOp.REMOVE);
+ switch (op) {
+ case AdapterHelper.UpdateOp.REMOVE:
+ if (mTestAdapter.mItems.size() > 1) {
+ int s = random.nextInt(mTestAdapter.mItems.size() - 1);
+ int len = Math.max(1, random.nextInt(mTestAdapter.mItems.size() - s));
+ rm(s, len);
+ }
+ break;
+ case AdapterHelper.UpdateOp.ADD:
+ int s = random.nextInt(mTestAdapter.mItems.size() - 1);
+ add(s, random.nextInt(50));
+ break;
+ }
+ }
+ preProcess();
+ }
+
+ public void assertOps(List<AdapterHelper.UpdateOp> actual,
+ AdapterHelper.UpdateOp... expected) {
+ assertEquals(expected.length, actual.size());
+ for (int i = 0; i < expected.length; i++) {
+ assertEquals(expected[i], actual.get(i));
+ }
+ }
+
+ void assertDispatch(int firstPass, int secondPass) {
+ assertEquals(firstPass, mFirstPassUpdates.size());
+ assertEquals(secondPass, mSecondPassUpdates.size());
+ }
+
+ void preProcess() {
+ mAdapterHelper.preProcess();
+ mAdapterHelper.consumePostponedUpdates();
+ // now assert these two adapters have identical data.
+ mPreProcessClone.applyOps(mFirstPassUpdates, mTestAdapter);
+ mPreProcessClone.applyOps(mSecondPassUpdates, mTestAdapter);
+ assertAdaptersEqual(mTestAdapter, mPreProcessClone);
+ }
+
+ private void assertAdaptersEqual(TestAdapter a1, TestAdapter a2) {
+ assertEquals(a1.mItems.size(), a2.mItems.size());
+ for (int i = 0; i < a1.mItems.size(); i++) {
+ TestAdapter.Item item = a1.mItems.get(i);
+ assertSame(item, a2.mItems.get(i));
+ assertEquals(0, item.getUpdateCount());
+ }
+ assertEquals(0, a1.mPendingAdded.size());
+ assertEquals(0, a2.mPendingAdded.size());
+ }
+
+ AdapterHelper.UpdateOp op(int cmd, int start, int count) {
+ return new AdapterHelper.UpdateOp(cmd, start, count);
+ }
+
+ AdapterHelper.UpdateOp addOp(int start, int count) {
+ return op(AdapterHelper.UpdateOp.ADD, start, count);
+ }
+
+ AdapterHelper.UpdateOp rmOp(int start, int count) {
+ return op(AdapterHelper.UpdateOp.REMOVE, start, count);
+ }
+
+ AdapterHelper.UpdateOp upOp(int start, int count) {
+ return op(AdapterHelper.UpdateOp.UPDATE, start, count);
+ }
+
+ void add(int start, int count) {
+ mTestAdapter.add(start, count);
+ }
+
+ void rm(int start, int count) {
+ mTestAdapter.remove(start, count);
+ }
+
+ void up(int start, int count) {
+ mTestAdapter.update(start, count);
+ }
+
+ static class TestAdapter {
+
+ List<Item> mItems;
+
+ final AdapterHelper mAdapterHelper;
+
+ Queue<Item> mPendingAdded;
+
+ public TestAdapter(int initialCount, AdapterHelper container) {
+ mItems = new ArrayList<Item>();
+ mAdapterHelper = container;
+ mPendingAdded = new LinkedList<Item>();
+ for (int i = 0; i < initialCount; i++) {
+ mItems.add(new Item());
+ }
+ }
+
+ public void add(int index, int count) {
+ for (int i = 0; i < count; i++) {
+ Item item = new Item();
+ mPendingAdded.add(item);
+ mItems.add(index + i, item);
+ }
+ mAdapterHelper.addUpdateOp(new AdapterHelper.UpdateOp(
+ AdapterHelper.UpdateOp.ADD, index, count
+ ));
+ }
+
+ public void remove(int index, int count) {
+ for (int i = 0; i < count; i++) {
+ mItems.remove(index);
+ }
+ mAdapterHelper.addUpdateOp(new AdapterHelper.UpdateOp(
+ AdapterHelper.UpdateOp.REMOVE, index, count
+ ));
+ }
+
+ public void update(int index, int count) {
+ for (int i = 0; i < count; i++) {
+ mItems.get(index + i).update();
+ }
+ mAdapterHelper.addUpdateOp(new AdapterHelper.UpdateOp(
+ AdapterHelper.UpdateOp.UPDATE, index, count
+ ));
+ }
+
+ protected TestAdapter createCopy() {
+ TestAdapter adapter = new TestAdapter(0, mAdapterHelper);
+ for (Item item : mItems) {
+ adapter.mItems.add(item);
+ }
+ return adapter;
+ }
+
+ public void applyOps(List<AdapterHelper.UpdateOp> updates,
+ TestAdapter dataSource) {
+ for (AdapterHelper.UpdateOp op : updates) {
+ switch (op.cmd) {
+ case AdapterHelper.UpdateOp.ADD:
+ for (int i = 0; i < op.itemCount; i++) {
+ mItems.add(op.positionStart + i, dataSource.consumeNextAdded());
+ }
+ break;
+ case AdapterHelper.UpdateOp.REMOVE:
+ for (int i = 0; i < op.itemCount; i++) {
+ mItems.remove(op.positionStart);
+ }
+ break;
+ case AdapterHelper.UpdateOp.UPDATE:
+ for (int i = 0; i < op.itemCount; i++) {
+ mItems.get(i).handleUpdate();
+ }
+ break;
+ }
+ }
+ }
+
+ private Item consumeNextAdded() {
+ return mPendingAdded.remove();
+ }
+
+ public static class Item {
+
+ private static AtomicInteger itemCounter = new AtomicInteger();
+
+ private final int id;
+
+ private int mVersionCount = 0;
+
+ private int mUpdateCount;
+
+ public Item() {
+ id = itemCounter.incrementAndGet();
+ }
+
+ public void update() {
+ mVersionCount++;
+ }
+
+ public void handleUpdate() {
+ mVersionCount--;
+ }
+
+ public int getUpdateCount() {
+ return mUpdateCount;
+ }
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index 56b8b79..f2aa049 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import android.os.Looper;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import android.view.View;
@@ -26,6 +27,7 @@
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
abstract public class BaseRecyclerViewInstrumentationTest extends
ActivityInstrumentationTestCase2<TestActivity> {
@@ -36,15 +38,53 @@
protected RecyclerView mRecyclerView;
+ protected AdapterHelper mAdapterHelper;
+
+ Throwable mainThreadException;
+
public BaseRecyclerViewInstrumentationTest() {
this(false);
}
public BaseRecyclerViewInstrumentationTest(boolean debug) {
- super("android.support.v7.widget", TestActivity.class);
+ super("android.support.v7.recyclerview", TestActivity.class);
mDebug = debug;
}
+ void checkForMainThreadException() throws Throwable {
+ if (mainThreadException != null) {
+ throw mainThreadException;
+ }
+ }
+
+ void postExceptionToInstrumentation(Throwable t) {
+ if (mDebug) {
+ Log.e(TAG, "captured exception on main thread", t);
+ }
+ mainThreadException = t;
+ if (mRecyclerView != null && mRecyclerView
+ .getLayoutManager() instanceof TestLayoutManager) {
+ TestLayoutManager lm = (TestLayoutManager) mRecyclerView.getLayoutManager();
+ // finish all layouts so that we get the correct exception
+ while (lm.layoutLatch.getCount() > 0) {
+ lm.layoutLatch.countDown();
+ }
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (mRecyclerView != null) {
+ try {
+ removeRecyclerView();
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ }
+ }
+ getInstrumentation().waitForIdleSync();
+ super.tearDown();
+ }
+
public void removeRecyclerView() throws Throwable {
mRecyclerView = null;
runTestOnUiThread(new Runnable() {
@@ -57,6 +97,27 @@
public void setRecyclerView(final RecyclerView recyclerView) throws Throwable {
mRecyclerView = recyclerView;
+ RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool() {
+ @Override
+ public RecyclerView.ViewHolder getRecycledView(int viewType) {
+ RecyclerView.ViewHolder viewHolder = super.getRecycledView(viewType);
+ if (viewHolder == null) {
+ return null;
+ }
+ viewHolder.addFlags(RecyclerView.ViewHolder.FLAG_BOUND);
+ viewHolder.mPosition = 200;
+ viewHolder.mOldPosition = 300;
+ viewHolder.mPreLayoutPosition = 500;
+ return viewHolder;
+ }
+
+ @Override
+ public void putRecycledView(RecyclerView.ViewHolder scrap) {
+ super.putRecycledView(scrap);
+ }
+ };
+ mRecyclerView.setRecycledViewPool(pool);
+ mAdapterHelper = recyclerView.mAdapterHelper;
runTestOnUiThread(new Runnable() {
@Override
public void run() {
@@ -88,6 +149,34 @@
});
}
+ void scrollToPosition(final int position) throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getLayoutManager().scrollToPosition(position);
+ }
+ });
+ }
+
+ void smoothScrollToPosition(final int position)
+ throws Throwable {
+ Log.d(TAG, "SMOOTH scrolling to " + position);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.smoothScrollToPosition(position);
+ }
+ });
+ while (mRecyclerView.getLayoutManager().isSmoothScrolling() ||
+ mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
+ if (mDebug) {
+ Log.d(TAG, "SMOOTH scrolling step");
+ }
+ Thread.sleep(200);
+ }
+ Log.d(TAG, "SMOOTH scrolling done");
+ }
+
class TestViewHolder extends RecyclerView.ViewHolder {
Item mBindedItem;
@@ -109,6 +198,7 @@
layoutLatch.await(timeout * (mDebug ? 100 : 1), timeUnit);
assertEquals("all expected layouts should be executed at the expected time",
0, layoutLatch.getCount());
+ getInstrumentation().waitForIdleSync();
}
public void assertLayoutCount(int count, String msg, long timeout) throws Throwable {
@@ -122,7 +212,7 @@
}
public void waitForLayout(long timeout) throws Throwable {
- waitForLayout(timeout, TimeUnit.SECONDS);
+ waitForLayout(timeout * (mDebug ? 10000 : 1), TimeUnit.SECONDS);
}
@Override
@@ -141,8 +231,11 @@
if (mDebug) {
Log.d(TAG, "testing item " + i);
}
- assertSame("item position in LP should match adapter value",
- testAdapter.mItems.get(lp.getViewPosition()), item);
+ if (!lp.isItemRemoved()) {
+ RecyclerView.ViewHolder vh = mRecyclerView.getChildViewHolder(view);
+ assertSame("item position in LP should match adapter value :" + vh,
+ testAdapter.mItems.get(vh.mPosition), item);
+ }
}
}
@@ -152,36 +245,66 @@
void layoutRange(RecyclerView.Recycler recycler, int start,
int end) {
+ assertScrap(recycler);
if (mDebug) {
Log.d(TAG, "will layout items from " + start + " to " + end);
}
- for (int i = start; i < end; i++) {
+ int diff = end > start ? 1 : -1;
+ for (int i = start; i != end; i+=diff) {
if (mDebug) {
Log.d(TAG, "laying out item " + i);
}
View view = recycler.getViewForPosition(i);
assertNotNull("view should not be null for valid position. "
+ "got null view at position " + i, view);
- if (!getLp(view).isItemRemoved()) {
- addView(view);
+ if (!mRecyclerView.mState.isPreLayout()) {
+ RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) view
+ .getLayoutParams();
+ assertFalse("In post layout, getViewForPosition should never return a view "
+ + "that is removed", layoutParams != null
+ && layoutParams.isItemRemoved());
+
}
+ assertEquals("getViewForPosition should return correct position",
+ i, getPosition(view));
+ addView(view);
measureChildWithMargins(view, 0, 0);
- layoutDecorated(view, 0, (i - start) * 10, getDecoratedMeasuredWidth(view)
+ layoutDecorated(view, 0, Math.abs(i - start) * 10, getDecoratedMeasuredWidth(view)
, getDecoratedMeasuredHeight(view));
}
}
+
+ private void assertScrap(RecyclerView.Recycler recycler) {
+ if (mRecyclerView.getAdapter() != null &&
+ !mRecyclerView.getAdapter().hasStableIds()) {
+ for (RecyclerView.ViewHolder viewHolder : recycler.getScrapList()) {
+ assertFalse("Invalid scrap should be no kept", viewHolder.isInvalid());
+ }
+ }
+ }
}
static class Item {
+ final static AtomicInteger idCounter = new AtomicInteger(0);
+ final public int mId = idCounter.incrementAndGet();
- int originalIndex;
+ int mAdapterIndex;
- final String text;
+ final String mText;
- Item(int originalIndex, String text) {
- this.originalIndex = originalIndex;
- this.text = text;
+ Item(int adapterIndex, String text) {
+ mAdapterIndex = adapterIndex;
+ mText = text;
+ }
+
+ @Override
+ public String toString() {
+ return "Item{" +
+ "mId=" + mId +
+ ", originalIndex=" + mAdapterIndex +
+ ", text='" + mText + '\'' +
+ '}';
}
}
@@ -205,7 +328,7 @@
@Override
public void onBindViewHolder(TestViewHolder holder, int position) {
final Item item = mItems.get(position);
- ((TextView) (holder.itemView)).setText(item.text);
+ ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mAdapterIndex + ")");
holder.mBindedItem = item;
}
@@ -223,19 +346,32 @@
* A D E. Then it will delete 2,1 which means it will delete E.
*/
public void deleteAndNotify(final int[]... startCountTuples) throws Throwable {
- runTestOnUiThread(new Runnable() {
- @Override
- public void run() {
- for (int t = 0; t < startCountTuples.length; t++) {
- int[] tuple = startCountTuples[t];
- for (int i = 0; i < tuple[1]; i++) {
- mItems.remove(tuple[0]);
- }
- notifyItemRangeRemoved(tuple[0], tuple[1]);
- }
+ for (int[] tuple : startCountTuples) {
+ tuple[1] = -tuple[1];
+ }
+ new AddRemoveRunnable(startCountTuples).runOnMainThread();
+ }
- }
- });
+ @Override
+ public long getItemId(int position) {
+ return hasStableIds() ? mItems.get(position).mId : super.getItemId(position);
+ }
+
+ public void offsetOriginalIndices(int start, int offset) {
+ for (int i = start; i < mItems.size(); i++) {
+ mItems.get(i).mAdapterIndex += offset;
+ }
+ }
+
+ /**
+ * @param start inclusive
+ * @param end exclusive
+ * @param offset
+ */
+ public void offsetOriginalIndicesBetween(int start, int end, int offset) {
+ for (int i = start; i < end && i < mItems.size(); i++) {
+ mItems.get(i).mAdapterIndex += offset;
+ }
}
public void addAndNotify(final int start, final int count) throws Throwable {
@@ -243,28 +379,132 @@
}
public void addAndNotify(final int[]... startCountTuples) throws Throwable {
+ new AddRemoveRunnable(startCountTuples).runOnMainThread();
+ }
+
+ public void dispatchDataSetChanged() throws Throwable {
runTestOnUiThread(new Runnable() {
@Override
public void run() {
- for (int t = 0; t < startCountTuples.length; t++) {
- int[] tuple = startCountTuples[t];
- for (int i = 0; i < tuple[1]; i++) {
- mItems.add(tuple[0], new Item(i, "new item " + i));
- }
- // offset others
- for (int i = tuple[0] + tuple[1]; i < mItems.size(); i++) {
- mItems.get(i).originalIndex += tuple[1];
- }
- notifyItemRangeInserted(tuple[0], tuple[1]);
- }
-
+ notifyDataSetChanged();
}
});
}
+ public void changeAndNotify(final int start, final int count) throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ notifyItemRangeChanged(start, count);
+ }
+ });
+ }
+
+ public void changePositionsAndNotify(final int... positions) throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < positions.length; i += 1) {
+ TestAdapter.super.notifyItemRangeChanged(positions[i], 1);
+ }
+ }
+ });
+ }
+
+ /**
+ * Similar to other methods but negative count means delete and position count means add.
+ * <p>
+ * For instance, calling this method with <code>[1,1], [2,-1]</code> it will first add an
+ * item to index 1, then remove an item from index 2 (updated index 2)
+ */
+ public void addDeleteAndNotify(final int[]... startCountTuples) throws Throwable {
+ new AddRemoveRunnable(startCountTuples).runOnMainThread();
+ }
+
@Override
public int getItemCount() {
return mItems.size();
}
+
+ public void moveItems(boolean notifyChange, int[]... fromToTuples) throws Throwable {
+ for (int i = 0; i < fromToTuples.length; i += 1) {
+ int[] tuple = fromToTuples[i];
+ moveItem(tuple[0], tuple[1], false);
+ }
+ if (notifyChange) {
+ dispatchDataSetChanged();
+ }
+ }
+
+ public void moveItem(final int from, final int to, final boolean notifyChange)
+ throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Item item = mItems.remove(from);
+ mItems.add(to, item);
+ offsetOriginalIndices(from, to - 1);
+ item.mAdapterIndex = to;
+ if (notifyChange) {
+ notifyDataSetChanged();
+ }
+ }
+ });
+ }
+
+
+ private class AddRemoveRunnable implements Runnable {
+ final int[][] mStartCountTuples;
+
+ public AddRemoveRunnable(int[][] startCountTuples) {
+ mStartCountTuples = startCountTuples;
+ }
+
+ public void runOnMainThread() throws Throwable {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ run();
+ } else {
+ runTestOnUiThread(this);
+ }
+ }
+
+ @Override
+ public void run() {
+ for (int[] tuple : mStartCountTuples) {
+ if (tuple[1] < 0) {
+ delete(tuple);
+ } else {
+ add(tuple);
+ }
+ }
+ }
+
+ private void add(int[] tuple) {
+ // offset others
+ offsetOriginalIndices(tuple[0], tuple[1]);
+ for (int i = 0; i < tuple[1]; i++) {
+ mItems.add(tuple[0], new Item(i, "new item " + i));
+ }
+ notifyItemRangeInserted(tuple[0], tuple[1]);
+ }
+
+ private void delete(int[] tuple) {
+ final int count = -tuple[1];
+ offsetOriginalIndices(tuple[0] + count, tuple[1]);
+ for (int i = 0; i < count; i++) {
+ mItems.remove(tuple[0]);
+ }
+ notifyItemRangeRemoved(tuple[0], count);
+ }
+ }
}
-}
\ No newline at end of file
+
+ @Override
+ public void runTestOnUiThread(Runnable r) throws Throwable {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ r.run();
+ } else {
+ super.runTestOnUiThread(r);
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BucketTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BucketTest.java
new file mode 100644
index 0000000..bbe2cf8
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BucketTest.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import android.test.AndroidTestCase;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class BucketTest extends AndroidTestCase {
+
+ ChildHelper.Bucket mBucket;
+
+ ArrayList<Integer> mArr;
+
+ Set<Integer> mSet;
+
+ int max = 0;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mBucket = new ChildHelper.Bucket();
+ mArr = new ArrayList<Integer>();
+ Collections.addAll(mArr, 0, 1, 2, 3, 4, 5, 6, 10, 12, 13, 21, 22, 122, 14, 44, 29, 205, 19);
+ for (int i = 1; i < 4; i++) {
+ mArr.add(i * (ChildHelper.Bucket.BITS_PER_WORD - 1));
+ mArr.add(i * (ChildHelper.Bucket.BITS_PER_WORD));
+ mArr.add(i * (ChildHelper.Bucket.BITS_PER_WORD + 1));
+ mArr.add(i * ChildHelper.Bucket.BITS_PER_WORD - 1);
+ mArr.add(i * ChildHelper.Bucket.BITS_PER_WORD);
+ mArr.add(i * ChildHelper.Bucket.BITS_PER_WORD + 1);
+ }
+
+ mSet = new HashSet<Integer>();
+ max = 0;
+ for (int i = mArr.size() - 1; i >= 0; i--) {
+ if (!mSet.add(mArr.get(i))) {
+ mArr.remove(i);
+ }
+ max = Math.max(max, i);
+ }
+ }
+
+
+ public void testSetClear() {
+ for (int i : mArr) {
+ mBucket.set(i);
+ assertTrue(mBucket.get(i));
+ }
+ for (int i = 0; i < max + 100; i++) {
+ assertEquals(mBucket.get(i), mSet.contains(i));
+ }
+
+ for (int i : mArr) {
+ mBucket.clear(i);
+ assertFalse(mBucket.get(i));
+ }
+
+ for (int i = 0; i < max + 100; i++) {
+ assertFalse(mBucket.get(i));
+ }
+ }
+
+
+ public void testRemove() {
+ for (int i : mArr) {
+ mBucket.reset();
+ for (int j : mArr) {
+ mBucket.set(j);
+ }
+ mBucket.remove(i);
+ for (int j : mArr) {
+ if (i == j) {
+ continue;
+ }
+ if (j < i) {
+ assertTrue(mBucket.get(j));
+ } else {
+ assertEquals(mSet.contains(j + 1), mBucket.get(j));
+ }
+ }
+ }
+ }
+
+ public void testInsert() {
+ for (int i : mArr) {
+ for (boolean val : new boolean[]{true, false}) {
+ mBucket.reset();
+ for (int j : mArr) {
+ mBucket.set(j);
+ }
+ mBucket.insert(i, val);
+ assertEquals(mBucket.get(i), val);
+ for (int j : mArr) {
+ if (j < i) {
+ assertTrue(mBucket.get(j));
+ } else if (j == i) {
+ assertEquals(mBucket.get(j), val);
+ } else {
+ assertEquals(mSet.contains(j - 1), mBucket.get(j));
+ assertTrue(mBucket.get(j + 1));
+ }
+ }
+ }
+ }
+ }
+
+
+ public void testCountOnesBefore() {
+ assertEquals(mBucket.countOnesBefore(0), 0);
+ for (int i : mArr) {
+ mBucket.set(i);
+ max = Math.max(i, max);
+ }
+ assertEquals(mBucket.countOnesBefore(0), 0);
+ for (int i = 0; i < max + 200; i++) {
+ int count = 0;
+ for (int j : mArr) {
+ if (j < i) {
+ count++;
+ }
+ }
+ assertEquals(count, mBucket.countOnesBefore(i));
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
new file mode 100644
index 0000000..56f7218
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerTest.java
@@ -0,0 +1,751 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v7.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.View;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Includes tests for {@link LinearLayoutManager}.
+ * <p>
+ * Since most UI tests are not practical, these tests are focused on internal data representation
+ * and stability of LinearLayoutManager in response to different events (state change, scrolling
+ * etc) where it is very hard to do manual testing.
+ */
+public class LinearLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
+
+ private static final boolean DEBUG = false;
+
+ private static final String TAG = "LinearLayoutManagerTest";
+
+ WrappedLinearLayoutManager mLayoutManager;
+
+ TestAdapter mTestAdapter;
+
+ final List<Config> mBaseVariations = new ArrayList<Config>();
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ for (int orientation : new int[]{LinearLayoutManager.VERTICAL,
+ LinearLayoutManager.HORIZONTAL}) {
+ for (boolean reverseLayout : new boolean[]{false, true}) {
+ for (boolean stackFromBottom : new boolean[]{false, true}) {
+ mBaseVariations.add(new Config(orientation, reverseLayout, stackFromBottom));
+ }
+ }
+ }
+ }
+
+ protected List<Config> addConfigVariation(List<Config> base, String fieldName,
+ Object... variations)
+ throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException {
+ List<Config> newConfigs = new ArrayList<Config>();
+ Field field = Config.class.getDeclaredField(fieldName);
+ for (Config config : base) {
+ for (Object variation : variations) {
+ Config newConfig = (Config) config.clone();
+ field.set(newConfig, variation);
+ newConfigs.add(newConfig);
+ }
+ }
+ return newConfigs;
+ }
+
+ void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable {
+ mRecyclerView = new RecyclerView(getActivity());
+ mRecyclerView.setHasFixedSize(true);
+ mTestAdapter = new TestAdapter(config.mItemCount);
+ mRecyclerView.setAdapter(mTestAdapter);
+ mLayoutManager = new WrappedLinearLayoutManager(getActivity(), config.mOrientation,
+ config.mReverseLayout);
+ mLayoutManager.setStackFromEnd(config.mStackFromEnd);
+ mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach);
+ mRecyclerView.setLayoutManager(mLayoutManager);
+ if (waitForFirstLayout) {
+ waitForFirstLayout();
+ }
+ }
+
+ private void waitForFirstLayout() throws Throwable {
+ mLayoutManager.expectLayouts(1);
+ setRecyclerView(mRecyclerView);
+ mLayoutManager.waitForLayout(2);
+ }
+
+
+ public void testGetFirstLastChildrenTest() throws Throwable {
+ for (Config config : mBaseVariations) {
+ getFirstLastChildrenTest(config);
+ }
+ }
+
+ public void testDontRecycleChildrenOnDetach() throws Throwable {
+ setupByConfig(new Config().recycleChildrenOnDetach(false), true);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
+ mRecyclerView.setLayoutManager(new TestLayoutManager());
+ assertEquals("No views are recycled", recyclerSize,
+ mRecyclerView.mRecycler.getRecycledViewPool().size());
+ }
+ });
+ }
+
+ public void testRecycleChildrenOnDetach() throws Throwable {
+ setupByConfig(new Config().recycleChildrenOnDetach(true), true);
+ final int childCount = mLayoutManager.getChildCount();
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ int recyclerSize = mRecyclerView.mRecycler.getRecycledViewPool().size();
+ mRecyclerView.mRecycler.getRecycledViewPool().setMaxRecycledViews(
+ mTestAdapter.getItemViewType(0), recyclerSize + childCount);
+ mRecyclerView.setLayoutManager(new TestLayoutManager());
+ assertEquals("All children should be recycled", childCount + recyclerSize,
+ mRecyclerView.mRecycler.getRecycledViewPool().size());
+ }
+ });
+ }
+
+ public void getFirstLastChildrenTest(final Config config) throws Throwable {
+ setupByConfig(config, true);
+ Runnable viewInBoundsTest = new Runnable() {
+ @Override
+ public void run() {
+ VisibleChildren visibleChildren = mLayoutManager.traverseAndFindVisibleChildren();
+ final String boundsLog = mLayoutManager.getBoundsLog();
+ assertEquals(config + ":\nfirst visible child should match traversal result\n"
+ + boundsLog, visibleChildren.firstVisiblePosition,
+ mLayoutManager.findFirstVisibleItemPosition()
+ );
+ assertEquals(
+ config + ":\nfirst fully visible child should match traversal result\n"
+ + boundsLog, visibleChildren.firstFullyVisiblePosition,
+ mLayoutManager.findFirstCompletelyVisibleItemPosition()
+ );
+
+ assertEquals(config + ":\nlast visible child should match traversal result\n"
+ + boundsLog, visibleChildren.lastVisiblePosition,
+ mLayoutManager.findLastVisibleItemPosition()
+ );
+ assertEquals(
+ config + ":\nlast fully visible child should match traversal result\n"
+ + boundsLog, visibleChildren.lastFullyVisiblePosition,
+ mLayoutManager.findLastCompletelyVisibleItemPosition()
+ );
+ }
+ };
+ runTestOnUiThread(viewInBoundsTest);
+ // smooth scroll to end of the list and keep testing meanwhile. This will test pre-caching
+ // case
+ final int scrollPosition = config.mStackFromEnd ? 0 : mTestAdapter.getItemCount();
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.smoothScrollToPosition(scrollPosition);
+ }
+ });
+ while (mLayoutManager.isSmoothScrolling() ||
+ mRecyclerView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
+ runTestOnUiThread(viewInBoundsTest);
+ Thread.sleep(400);
+ }
+ // delete all items
+ mLayoutManager.expectLayouts(2);
+ mTestAdapter.deleteAndNotify(0, mTestAdapter.getItemCount());
+ mLayoutManager.waitForLayout(2);
+ // test empty case
+ runTestOnUiThread(viewInBoundsTest);
+ // set a new adapter with huge items to test full bounds check
+ mLayoutManager.expectLayouts(1);
+ final int totalSpace = mLayoutManager.mOrientationHelper.getTotalSpace();
+ final TestAdapter newAdapter = new TestAdapter(100) {
+ @Override
+ public void onBindViewHolder(TestViewHolder holder,
+ int position) {
+ super.onBindViewHolder(holder, position);
+ if (config.mOrientation == LinearLayoutManager.HORIZONTAL) {
+ holder.itemView.setMinimumWidth(totalSpace + 5);
+ } else {
+ holder.itemView.setMinimumHeight(totalSpace + 5);
+ }
+ }
+ };
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.setAdapter(newAdapter);
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+ runTestOnUiThread(viewInBoundsTest);
+ }
+
+ public void testSavedState() throws Throwable {
+ Thread.sleep(5000);
+ PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{
+ new PostLayoutRunnable() {
+ @Override
+ public void run() throws Throwable {
+ // do nothing
+ }
+
+ @Override
+ public String describe() {
+ return "doing nothing";
+ }
+ },
+ new PostLayoutRunnable() {
+ @Override
+ public void run() throws Throwable {
+ mLayoutManager.expectLayouts(1);
+ scrollToPosition(mTestAdapter.getItemCount() * 3 / 4);
+ mLayoutManager.waitForLayout(2);
+ }
+
+ @Override
+ public String describe() {
+ return "scroll to position";
+ }
+ },
+ new PostLayoutRunnable() {
+ @Override
+ public void run() throws Throwable {
+ mLayoutManager.expectLayouts(1);
+ scrollToPositionWithOffset(mTestAdapter.getItemCount() * 1 / 3,
+ 50);
+ mLayoutManager.waitForLayout(2);
+ }
+
+ @Override
+ public String describe() {
+ return "scroll to position with positive offset";
+ }
+ },
+ new PostLayoutRunnable() {
+ @Override
+ public void run() throws Throwable {
+ mLayoutManager.expectLayouts(1);
+ scrollToPositionWithOffset(mTestAdapter.getItemCount() * 2 / 3,
+ -50);
+ mLayoutManager.waitForLayout(2);
+ }
+
+ @Override
+ public String describe() {
+ return "scroll to position with negative offset";
+ }
+ }
+ };
+
+ PostRestoreRunnable[] postRestoreOptions = new PostRestoreRunnable[]{
+ new PostRestoreRunnable() {
+ @Override
+ public String describe() {
+ return "Doing nothing";
+ }
+ },
+ new PostRestoreRunnable() {
+ @Override
+ void onAfterRestore(Config config) throws Throwable {
+ // update config as well so that restore assertions will work
+ config.mOrientation = 1 - config.mOrientation;
+ mLayoutManager.setOrientation(config.mOrientation);
+ }
+
+ @Override
+ boolean shouldLayoutMatch(Config config) {
+ return config.mItemCount == 0;
+ }
+
+ @Override
+ public String describe() {
+ return "Changing orientation";
+ }
+ },
+ new PostRestoreRunnable() {
+ @Override
+ void onAfterRestore(Config config) throws Throwable {
+ config.mStackFromEnd = !config.mStackFromEnd;
+ mLayoutManager.setStackFromEnd(config.mStackFromEnd);
+ }
+
+ @Override
+ boolean shouldLayoutMatch(Config config) {
+ return true; //stack from end should not move items on change
+ }
+
+ @Override
+ public String describe() {
+ return "Changing stack from end";
+ }
+ },
+ new PostRestoreRunnable() {
+ @Override
+ void onAfterRestore(Config config) throws Throwable {
+ config.mReverseLayout = !config.mReverseLayout;
+ mLayoutManager.setReverseLayout(config.mReverseLayout);
+ }
+
+ @Override
+ boolean shouldLayoutMatch(Config config) {
+ return config.mItemCount == 0;
+ }
+
+ @Override
+ public String describe() {
+ return "Changing reverse layout";
+ }
+ },
+ new PostRestoreRunnable() {
+ @Override
+ void onAfterRestore(Config config) throws Throwable {
+ config.mRecycleChildrenOnDetach = !config.mRecycleChildrenOnDetach;
+ mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach);
+ }
+
+ @Override
+ boolean shouldLayoutMatch(Config config) {
+ return true;
+ }
+
+ @Override
+ String describe() {
+ return "Change shoudl recycle children";
+ }
+ },
+ new PostRestoreRunnable() {
+ int position;
+ @Override
+ void onAfterRestore(Config config) throws Throwable {
+ position = mTestAdapter.getItemCount() / 2;
+ mLayoutManager.scrollToPosition(position);
+ }
+
+ @Override
+ boolean shouldLayoutMatch(Config config) {
+ return mTestAdapter.getItemCount() == 0;
+ }
+
+ @Override
+ String describe() {
+ return "Scroll to position " + position ;
+ }
+
+ @Override
+ void onAfterReLayout(Config config) {
+ if (mTestAdapter.getItemCount() > 0) {
+ assertEquals(config + ":scrolled view should be last completely visible",
+ position,
+ config.mStackFromEnd ?
+ mLayoutManager.findLastCompletelyVisibleItemPosition()
+ : mLayoutManager.findFirstCompletelyVisibleItemPosition());
+ }
+ }
+ }
+ };
+ boolean[] waitForLayoutOptions = new boolean[]{true, false};
+ List<Config> variations = addConfigVariation(mBaseVariations, "mItemCount", 0, 300);
+ variations = addConfigVariation(variations, "mRecycleChildrenOnDetach", true);
+ for (Config config : variations) {
+ for (PostLayoutRunnable postLayoutRunnable : postLayoutOptions) {
+ for (boolean waitForLayout : waitForLayoutOptions) {
+ for (PostRestoreRunnable postRestoreRunnable : postRestoreOptions) {
+ savedStateTest((Config) config.clone(), waitForLayout, postLayoutRunnable,
+ postRestoreRunnable);
+ removeRecyclerView();
+ }
+
+ }
+ }
+ }
+ }
+
+ public void savedStateTest(Config config, boolean waitForLayout,
+ PostLayoutRunnable postLayoutOperation, PostRestoreRunnable postRestoreOperation)
+ throws Throwable {
+ if (DEBUG) {
+ Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config " +
+ config + " post layout action " + postLayoutOperation.describe() +
+ "post restore action " + postRestoreOperation.describe());
+ }
+ setupByConfig(config, false);
+ if (waitForLayout) {
+ waitForFirstLayout();
+ postLayoutOperation.run();
+ }
+ Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
+ Parcelable savedState = mRecyclerView.onSaveInstanceState();
+ // we append a suffix to the parcelable to test out of bounds
+ String parcelSuffix = UUID.randomUUID().toString();
+ Parcel parcel = Parcel.obtain();
+ savedState.writeToParcel(parcel, 0);
+ parcel.writeString(parcelSuffix);
+ removeRecyclerView();
+ // reset for reading
+ parcel.setDataPosition(0);
+ // re-create
+ savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
+ removeRecyclerView();
+
+ RecyclerView restored = new RecyclerView(getActivity());
+ // this config should be no op.
+ mLayoutManager = new WrappedLinearLayoutManager(getActivity(),
+ 1 - config.mOrientation, !config.mReverseLayout);
+ mLayoutManager.setStackFromEnd(!config.mStackFromEnd);
+ restored.setLayoutManager(mLayoutManager);
+ // use the same adapter for Rect matching
+ restored.setAdapter(mTestAdapter);
+ restored.onRestoreInstanceState(savedState);
+ postRestoreOperation.onAfterRestore(config);
+ assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
+ parcel.readString());
+ mLayoutManager.expectLayouts(1);
+ setRecyclerView(restored);
+ mLayoutManager.waitForLayout(2);
+ // calculate prefix here instead of above to include post restore changes
+ final String logPrefix = config + "\npostLayout:" + postLayoutOperation.describe() +
+ "\npostRestore:" + postRestoreOperation.describe() + "\n";
+ assertEquals(logPrefix + " on saved state, reverse layout should be preserved",
+ config.mReverseLayout, mLayoutManager.getReverseLayout());
+ assertEquals(logPrefix + " on saved state, orientation should be preserved",
+ config.mOrientation, mLayoutManager.getOrientation());
+ assertEquals(logPrefix + " on saved state, stack from end should be preserved",
+ config.mStackFromEnd, mLayoutManager.getStackFromEnd());
+ assertEquals(logPrefix + " on saved state, mRecycleChildrenOnDetach should be preserved",
+ config.mRecycleChildrenOnDetach, mLayoutManager.getRecycleChildrenOnDetach());
+ if (waitForLayout) {
+ if (postRestoreOperation.shouldLayoutMatch(config)) {
+ assertRectSetsEqual(
+ logPrefix + ": on restore, previous view positions should be preserved",
+ before, mLayoutManager.collectChildCoordinates());
+ } else {
+ assertRectSetsNotEqual(
+ logPrefix
+ + ": on restore with changes, previous view positions should NOT "
+ + "be preserved",
+ before, mLayoutManager.collectChildCoordinates());
+ }
+ postRestoreOperation.onAfterReLayout(config);
+ }
+ }
+
+ void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mLayoutManager.scrollToPositionWithOffset(position, offset);
+ }
+ });
+ }
+
+ public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
+ Map<Item, Rect> after) {
+ Throwable throwable = null;
+ try {
+ assertRectSetsEqual("NOT " + message, before, after);
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertNotNull(message + "\ntwo layout should be different", throwable);
+ }
+
+ public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("checking rectangle equality.");
+ sb.append("before:\n");
+ for (Map.Entry<Item, Rect> entry : before.entrySet()) {
+ sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
+ }
+ sb.append("after:\n");
+ for (Map.Entry<Item, Rect> entry : after.entrySet()) {
+ sb.append(entry.getKey().mAdapterIndex + ":" + entry.getValue()).append("\n");
+ }
+ message = message + "\n" + sb.toString();
+ assertEquals(message + ":\nitem counts should be equal", before.size()
+ , after.size());
+ for (Map.Entry<Item, Rect> entry : before.entrySet()) {
+ Rect afterRect = after.get(entry.getKey());
+ assertNotNull(message + ":\nSame item should be visible after simple re-layout",
+ afterRect);
+ assertEquals(message + ":\nItem should be laid out at the same coordinates",
+ entry.getValue(), afterRect);
+ }
+ }
+
+ static class VisibleChildren {
+
+ int firstVisiblePosition = RecyclerView.NO_POSITION;
+
+ int firstFullyVisiblePosition = RecyclerView.NO_POSITION;
+
+ int lastVisiblePosition = RecyclerView.NO_POSITION;
+
+ int lastFullyVisiblePosition = RecyclerView.NO_POSITION;
+
+ @Override
+ public String toString() {
+ return "VisibleChildren{" +
+ "firstVisiblePosition=" + firstVisiblePosition +
+ ", firstFullyVisiblePosition=" + firstFullyVisiblePosition +
+ ", lastVisiblePosition=" + lastVisiblePosition +
+ ", lastFullyVisiblePosition=" + lastFullyVisiblePosition +
+ '}';
+ }
+ }
+
+ abstract private class PostLayoutRunnable {
+
+ abstract void run() throws Throwable;
+
+ abstract String describe();
+ }
+
+ abstract private class PostRestoreRunnable {
+
+ void onAfterRestore(Config config) throws Throwable {
+ }
+
+ abstract String describe();
+
+ boolean shouldLayoutMatch(Config config) {
+ return true;
+ }
+
+ void onAfterReLayout(Config config) {
+
+ };
+ }
+
+ class WrappedLinearLayoutManager extends LinearLayoutManager {
+
+ CountDownLatch layoutLatch;
+
+ OrientationHelper mSecondaryOrientation;
+
+ public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
+ super(context, orientation, reverseLayout);
+ }
+
+ public void expectLayouts(int count) {
+ layoutLatch = new CountDownLatch(count);
+ }
+
+ public void waitForLayout(long timeout) throws InterruptedException {
+ waitForLayout(timeout, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void setOrientation(int orientation) {
+ super.setOrientation(orientation);
+ mSecondaryOrientation = null;
+ }
+
+ @Override
+ void ensureLayoutState() {
+ super.ensureLayoutState();
+ if (mSecondaryOrientation == null) {
+ mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
+ 1 - getOrientation());
+ }
+ }
+
+ private void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
+ layoutLatch.await(timeout, timeUnit);
+ assertEquals("all expected layouts should be executed at the expected time",
+ 0, layoutLatch.getCount());
+ }
+
+ public String getBoundsLog() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("view bounds:[start:").append(mOrientationHelper.getStartAfterPadding())
+ .append(",").append(" end").append(mOrientationHelper.getEndAfterPadding());
+ sb.append("\nchildren bounds\n");
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ sb.append("child (ind:").append(i).append(", pos:").append(getPosition(child))
+ .append("[").append("start:").append(
+ mOrientationHelper.getDecoratedStart(child)).append(", end:")
+ .append(mOrientationHelper.getDecoratedEnd(child)).append("]\n");
+ }
+ return sb.toString();
+ }
+
+ public VisibleChildren traverseAndFindVisibleChildren() {
+ int childCount = getChildCount();
+ final VisibleChildren visibleChildren = new VisibleChildren();
+ final int start = mOrientationHelper.getStartAfterPadding();
+ final int end = mOrientationHelper.getEndAfterPadding();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ final int childStart = mOrientationHelper.getDecoratedStart(child);
+ final int childEnd = mOrientationHelper.getDecoratedEnd(child);
+ final boolean fullyVisible = childStart >= start && childEnd <= end;
+ final boolean hidden = childEnd <= start || childStart >= end;
+ if (hidden) {
+ continue;
+ }
+ final int position = getPosition(child);
+ if (fullyVisible) {
+ if (position < visibleChildren.firstFullyVisiblePosition ||
+ visibleChildren.firstFullyVisiblePosition == RecyclerView.NO_POSITION) {
+ visibleChildren.firstFullyVisiblePosition = position;
+ }
+
+ if (position > visibleChildren.lastFullyVisiblePosition) {
+ visibleChildren.lastFullyVisiblePosition = position;
+ }
+ }
+
+ if (position < visibleChildren.firstVisiblePosition ||
+ visibleChildren.firstVisiblePosition == RecyclerView.NO_POSITION) {
+ visibleChildren.firstVisiblePosition = position;
+ }
+
+ if (position > visibleChildren.lastVisiblePosition) {
+ visibleChildren.lastVisiblePosition = position;
+ }
+
+ }
+ return visibleChildren;
+ }
+
+ Rect getViewBounds(View view) {
+ if (getOrientation() == HORIZONTAL) {
+ return new Rect(
+ mOrientationHelper.getDecoratedStart(view),
+ mSecondaryOrientation.getDecoratedStart(view),
+ mOrientationHelper.getDecoratedEnd(view),
+ mSecondaryOrientation.getDecoratedEnd(view));
+ } else {
+ return new Rect(
+ mSecondaryOrientation.getDecoratedStart(view),
+ mOrientationHelper.getDecoratedStart(view),
+ mSecondaryOrientation.getDecoratedEnd(view),
+ mOrientationHelper.getDecoratedEnd(view));
+ }
+
+ }
+
+ Map<Item, Rect> collectChildCoordinates() throws Throwable {
+ final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child
+ .getLayoutParams();
+ TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
+ items.put(vh.mBindedItem, getViewBounds(child));
+ }
+ }
+ });
+ return items;
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ super.onLayoutChildren(recycler, state);
+ layoutLatch.countDown();
+ }
+ }
+
+ static class Config implements Cloneable {
+
+ private static final int DEFAULT_ITEM_COUNT = 100;
+
+ private boolean mStackFromEnd;
+
+ int mOrientation = LinearLayoutManager.VERTICAL;
+
+ boolean mReverseLayout = false;
+
+ boolean mRecycleChildrenOnDetach = false;
+
+ int mItemCount = DEFAULT_ITEM_COUNT;
+
+ Config(int orientation, boolean reverseLayout, boolean stackFromEnd) {
+ mOrientation = orientation;
+ mReverseLayout = reverseLayout;
+ mStackFromEnd = stackFromEnd;
+ }
+
+ public Config() {
+
+ }
+
+ Config recycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
+ mRecycleChildrenOnDetach = recycleChildrenOnDetach;
+ return this;
+ }
+
+ Config orientation(int orientation) {
+ mOrientation = orientation;
+ return this;
+ }
+
+ Config stackFromBottom(boolean stackFromBottom) {
+ mStackFromEnd = stackFromBottom;
+ return this;
+ }
+
+ Config reverseLayout(boolean reverseLayout) {
+ mReverseLayout = reverseLayout;
+ return this;
+ }
+
+ public Config itemCount(int itemCount) {
+ mItemCount = itemCount;
+ return this;
+ }
+
+ // required by convention
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ return super.clone();
+ }
+
+ @Override
+ public String toString() {
+ return "Config{" +
+ "mStackFromEnd=" + mStackFromEnd +
+ ", mOrientation=" + mOrientation +
+ ", mReverseLayout=" + mReverseLayout +
+ ", mRecycleChildrenOnDetach=" + mRecycleChildrenOnDetach +
+ ", mItemCount=" + mItemCount +
+ '}';
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
index 7d43610..6ef8628 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewAnimationsTest.java
@@ -17,11 +17,20 @@
package android.support.v7.widget;
import android.content.Context;
+import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
public class RecyclerViewAnimationsTest extends BaseRecyclerViewInstrumentationTest {
@@ -29,8 +38,6 @@
private static final String TAG = "RecyclerViewAnimationsTest";
- Throwable mainThreadException;
-
AnimationLayoutManager mLayoutManager;
TestAdapter mTestAdapter;
@@ -44,21 +51,25 @@
super.setUp();
}
- void checkForMainThreadException() throws Throwable {
- if (mainThreadException != null) {
- throw mainThreadException;
- }
- }
-
RecyclerView setupBasic(int itemCount) throws Throwable {
return setupBasic(itemCount, 0, itemCount);
}
RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount)
throws Throwable {
- final RecyclerView recyclerView = new TestRecyclerView(getActivity());
+ return setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null);
+ }
+
+ RecyclerView setupBasic(int itemCount, int firstLayoutStartIndex, int firstLayoutItemCount,
+ TestAdapter testAdapter)
+ throws Throwable {
+ final TestRecyclerView recyclerView = new TestRecyclerView(getActivity());
recyclerView.setHasFixedSize(true);
- mTestAdapter = new TestAdapter(itemCount);
+ if (testAdapter == null) {
+ mTestAdapter = new TestAdapter(itemCount);
+ } else {
+ mTestAdapter = testAdapter;
+ }
recyclerView.setAdapter(mTestAdapter);
mLayoutManager = new AnimationLayoutManager();
recyclerView.setLayoutManager(mLayoutManager);
@@ -66,61 +77,168 @@
mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = firstLayoutItemCount;
mLayoutManager.expectLayouts(1);
+ recyclerView.expectDraw(1);
setRecyclerView(recyclerView);
mLayoutManager.waitForLayout(2);
+ recyclerView.waitForDraw(1);
+ mLayoutManager.mOnLayoutCallbacks.reset();
+ getInstrumentation().waitForIdleSync();
+ assertEquals("extra layouts should not happen", 1, mLayoutManager.getTotalLayoutCount());
+ assertEquals("all expected children should be laid out", firstLayoutItemCount,
+ mLayoutManager.getChildCount());
return recyclerView;
}
- public void testAddInvisibleAndVisible() throws Throwable {
- setupBasic(10, 1, 7);
- mLayoutManager.expectLayouts(2);
- mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
- mTestAdapter.addAndNotify(0, 1);// add a new item 0 // invisible
- mTestAdapter.addAndNotify(7, 1);// add a new item after 5th (old 5, new 6)
- mLayoutManager.waitForLayout(2);
- }
-
- public void testAddInvisible() throws Throwable {
- setupBasic(10, 1, 7);
+ public void testNotifyDataSetChanged() throws Throwable {
+ setupBasic(10, 3, 4);
+ int layoutCount = mLayoutManager.mTotalLayoutCount;
mLayoutManager.expectLayouts(1);
- mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
- mTestAdapter.addAndNotify(0, 1);// add a new item 0
- mTestAdapter.addAndNotify(8, 1);// add a new item after 6th (old 6, new 7)
- mLayoutManager.waitForLayout(2);
- }
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mTestAdapter.deleteAndNotify(4, 1);
+ mTestAdapter.dispatchDataSetChanged();
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ }
- public void testBasicAdd() throws Throwable {
- setupBasic(10);
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+ getInstrumentation().waitForIdleSync();
+ assertEquals("on notify data set changed, predictive animations should not run",
+ layoutCount + 1, mLayoutManager.mTotalLayoutCount);
mLayoutManager.expectLayouts(2);
- setExpectedItemCounts(10, 13);
- mTestAdapter.addAndNotify(2, 3);
+ mTestAdapter.addAndNotify(4, 2);
+ // make sure animations recover
mLayoutManager.waitForLayout(2);
}
- public void testDeleteVisibleAndInvisible() throws Throwable {
- setupBasic(11, 3, 5); //layout items 3 4 5 6 7
+ public void testStableIdNotifyDataSetChanged() throws Throwable {
+ final int itemCount = 20;
+ List<Item> initialSet = new ArrayList<Item>();
+ final TestAdapter adapter = new TestAdapter(itemCount) {
+ @Override
+ public long getItemId(int position) {
+ return mItems.get(position).mId;
+ }
+ };
+ adapter.setHasStableIds(true);
+ initialSet.addAll(adapter.mItems);
+ positionStatesTest(itemCount, 5, 5, adapter, new AdapterOps() {
+ @Override
+ void onRun(TestAdapter testAdapter) throws Throwable {
+ Item item5 = adapter.mItems.get(5);
+ Item item6 = adapter.mItems.get(6);
+ item5.mAdapterIndex = 6;
+ item6.mAdapterIndex = 5;
+ adapter.mItems.remove(5);
+ adapter.mItems.add(6, item5);
+ adapter.dispatchDataSetChanged();
+ //hacky, we support only 1 layout pass
+ mLayoutManager.layoutLatch.countDown();
+ }
+ }, PositionConstraint.scrap(6, -1, 5), PositionConstraint.scrap(5, -1, 6),
+ PositionConstraint.scrap(7, -1, 7), PositionConstraint.scrap(8, -1, 8),
+ PositionConstraint.scrap(9, -1, 9));
+ // now mix items.
+ }
+
+
+ public void testGetItemForDeletedView() throws Throwable {
+ getItemForDeletedViewTest(false);
+ getItemForDeletedViewTest(true);
+ }
+
+ public void getItemForDeletedViewTest(boolean stableIds) throws Throwable {
+ final Set<Integer> itemViewTypeQueries = new HashSet<Integer>();
+ final Set<Integer> itemIdQueries = new HashSet<Integer>();
+ TestAdapter adapter = new TestAdapter(10) {
+ @Override
+ public int getItemViewType(int position) {
+ itemViewTypeQueries.add(position);
+ return super.getItemViewType(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ itemIdQueries.add(position);
+ return mItems.get(position).mId;
+ }
+ };
+ adapter.setHasStableIds(stableIds);
+ setupBasic(10, 0, 10, adapter);
+ assertEquals("getItemViewType for all items should be called", 10,
+ itemViewTypeQueries.size());
+ if (adapter.hasStableIds()) {
+ assertEquals("getItemId should be called when adapter has stable ids", 10,
+ itemIdQueries.size());
+ } else {
+ assertEquals("getItemId should not be called when adapter does not have stable ids", 0,
+ itemIdQueries.size());
+ }
+ itemViewTypeQueries.clear();
+ itemIdQueries.clear();
mLayoutManager.expectLayouts(2);
- setLayoutRange(3, 6); //layout previously invisible child 10 from end of the list
- setExpectedItemCounts(9, 8);
- mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9
+ // delete last two
+ final int deleteStart = 8;
+ final int deleteCount = adapter.getItemCount() - deleteStart;
+ adapter.deleteAndNotify(deleteStart, deleteCount);
mLayoutManager.waitForLayout(2);
+ for (int i = 0; i < deleteStart; i++) {
+ assertTrue("getItemViewType for existing item " + i + " should be called",
+ itemViewTypeQueries.contains(i));
+ if (adapter.hasStableIds()) {
+ assertTrue("getItemId for existing item " + i
+ + " should be called when adapter has stable ids",
+ itemIdQueries.contains(i));
+ }
+ }
+ for (int i = deleteStart; i < deleteStart + deleteCount; i++) {
+ assertFalse("getItemViewType for deleted item " + i + " SHOULD NOT be called",
+ itemViewTypeQueries.contains(i));
+ if (adapter.hasStableIds()) {
+ assertFalse("getItemId for deleted item " + i + " SHOULD NOT be called",
+ itemIdQueries.contains(i));
+ }
+ }
}
- private void setLayoutRange(int start, int count) {
- mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start;
- mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count;
- }
-
- private void setExpectedItemCounts(int preLayout, int postLayout) {
- mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout);
- }
-
- public void testDeleteInvisible() throws Throwable {
- setupBasic(10, 1, 7);
+ public void testDeleteInvisibleMultiStep() throws Throwable {
+ setupBasic(1000, 1, 7);
+ mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
+ mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
mLayoutManager.expectLayouts(1);
- mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8);
- mTestAdapter.deleteAndNotify(0, 1);// delete item id 0
- mTestAdapter.deleteAndNotify(7, 1);// delete item id 8
+ // try to trigger race conditions
+ int targetItemCount = mTestAdapter.getItemCount();
+ for (int i = 0; i < 100; i++) {
+ mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});
+ targetItemCount -= 2;
+ }
+ // wait until main thread runnables are consumed
+ while (targetItemCount != mTestAdapter.getItemCount()) {
+ Thread.sleep(100);
+ }
+ mLayoutManager.waitForLayout(2);
+ }
+
+ public void testAddManyMultiStep() throws Throwable {
+ setupBasic(10, 1, 7);
+ mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
+ mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
+ mLayoutManager.expectLayouts(1);
+ // try to trigger race conditions
+ int targetItemCount = mTestAdapter.getItemCount();
+ for (int i = 0; i < 100; i++) {
+ mTestAdapter.addAndNotify(0, 1);
+ mTestAdapter.addAndNotify(7, 1);
+ targetItemCount += 2;
+ }
+ // wait until main thread runnables are consumed
+ while (targetItemCount != mTestAdapter.getItemCount()) {
+ Thread.sleep(100);
+ }
mLayoutManager.waitForLayout(2);
}
@@ -155,16 +273,322 @@
}
+ public void testAdapterChangeDuringScrolling() throws Throwable {
+ setupBasic(10);
+ final AtomicInteger onLayoutItemCount = new AtomicInteger(0);
+ final AtomicInteger onScrollItemCount = new AtomicInteger(0);
+
+ mLayoutManager.setOnLayoutCallbacks(new OnLayoutCallbacks() {
+ @Override
+ void onLayoutChildren(RecyclerView.Recycler recycler,
+ AnimationLayoutManager lm, RecyclerView.State state) {
+ onLayoutItemCount.set(state.getItemCount());
+ super.onLayoutChildren(recycler, lm, state);
+ }
+
+ @Override
+ public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
+ onScrollItemCount.set(state.getItemCount());
+ super.onScroll(dx, recycler, state);
+ }
+ });
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTestAdapter.mItems.remove(5);
+ mTestAdapter.notifyItemRangeRemoved(5, 1);
+ mRecyclerView.scrollBy(0, 100);
+ assertTrue("scrolling while there are pending adapter updates should "
+ + "trigger a layout", mLayoutManager.mOnLayoutCallbacks.mLayoutCount > 0);
+ assertEquals("scroll by should be called w/ updated adapter count",
+ mTestAdapter.mItems.size(), onScrollItemCount.get());
+
+ }
+ });
+ }
+
+ public void testAddInvisibleAndVisible() throws Throwable {
+ setupBasic(10, 1, 7);
+ mLayoutManager.expectLayouts(2);
+ mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
+ mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{7, 1});// add a new item 0 // invisible
+ mLayoutManager.waitForLayout(2);
+ }
+
+ public void testAddInvisible() throws Throwable {
+ setupBasic(10, 1, 7);
+ mLayoutManager.expectLayouts(1);
+ mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(10, 12);
+ mTestAdapter.addAndNotify(new int[]{0, 1}, new int[]{8, 1});// add a new item 0
+ mLayoutManager.waitForLayout(2);
+ }
+
+ public void testBasicAdd() throws Throwable {
+ setupBasic(10);
+ mLayoutManager.expectLayouts(2);
+ setExpectedItemCounts(10, 13);
+ mTestAdapter.addAndNotify(2, 3);
+ mLayoutManager.waitForLayout(2);
+ }
+
+ public TestRecyclerView getTestRecyclerView() {
+ return (TestRecyclerView) mRecyclerView;
+ }
+
+ public void testRemoveScrapInvalidate() throws Throwable {
+ setupBasic(10);
+ TestRecyclerView testRecyclerView = getTestRecyclerView();
+ mLayoutManager.expectLayouts(1);
+ testRecyclerView.expectDraw(1);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mTestAdapter.mItems.clear();
+ mTestAdapter.notifyDataSetChanged();
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+ testRecyclerView.waitForDraw(2);
+ }
+
+ public void testDeleteVisibleAndInvisible() throws Throwable {
+ setupBasic(11, 3, 5); //layout items 3 4 5 6 7
+ mLayoutManager.expectLayouts(2);
+ setLayoutRange(3, 5); //layout previously invisible child 10 from end of the list
+ setExpectedItemCounts(9, 8);
+ mTestAdapter.deleteAndNotify(new int[]{4, 1}, new int[]{7, 2});// delete items 4, 8, 9
+ mLayoutManager.waitForLayout(2);
+ }
+
+ public void testFindPositionOffset() throws Throwable {
+ setupBasic(10);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ // [0,1,2,3,4]
+ // delete 1
+ mTestAdapter.notifyItemRangeRemoved(1, 1);
+ // delete 3
+ mTestAdapter.notifyItemRangeRemoved(2, 1);
+ mAdapterHelper.preProcess();
+ // [0,2,4]
+ assertEquals("offset check", 0, mAdapterHelper.findPositionOffset(0));
+ assertEquals("offset check", 1, mAdapterHelper.findPositionOffset(2));
+ assertEquals("offset check", 2, mAdapterHelper.findPositionOffset(4));
+
+ }
+ });
+ }
+
+ private void setLayoutRange(int start, int count) {
+ mLayoutManager.mOnLayoutCallbacks.mLayoutMin = start;
+ mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = count;
+ }
+
+ private void setExpectedItemCounts(int preLayout, int postLayout) {
+ mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(preLayout, postLayout);
+ }
+
+ public void testDeleteInvisible() throws Throwable {
+ setupBasic(10, 1, 7);
+ mLayoutManager.mOnLayoutCallbacks.mLayoutMin = 1;
+ mLayoutManager.mOnLayoutCallbacks.mLayoutItemCount = 7;
+ mLayoutManager.expectLayouts(1);
+ mLayoutManager.mOnLayoutCallbacks.setExpectedItemCounts(8, 8);
+ mTestAdapter.deleteAndNotify(new int[]{0, 1}, new int[]{7, 1});// delete item id 0,8
+ mLayoutManager.waitForLayout(2);
+ }
+
+ private CollectPositionResult findByPos(RecyclerView recyclerView,
+ RecyclerView.Recycler recycler, RecyclerView.State state, int position) {
+ View view = recycler.getViewForPosition(position, true);
+ RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(view);
+ if (vh.wasReturnedFromScrap()) {
+ vh.clearReturnedFromScrapFlag(); //keep data consistent.
+ return CollectPositionResult.fromScrap(vh);
+ } else {
+ return CollectPositionResult.fromAdapter(vh);
+ }
+ }
+
+ public Map<Integer, CollectPositionResult> collectPositions(RecyclerView recyclerView,
+ RecyclerView.Recycler recycler, RecyclerView.State state, int... positions) {
+ Map<Integer, CollectPositionResult> positionToAdapterMapping
+ = new HashMap<Integer, CollectPositionResult>();
+ for (int position : positions) {
+ if (position < 0) {
+ continue;
+ }
+ positionToAdapterMapping.put(position,
+ findByPos(recyclerView, recycler, state, position));
+ }
+ return positionToAdapterMapping;
+ }
+
+ public void testAddDelete2() throws Throwable {
+ positionStatesTest(5, 0, 5, new AdapterOps() {
+ // 0 1 2 3 4
+ // 0 1 2 a b 3 4
+ // 0 1 b 3 4
+ // pre: 0 1 2 3 4
+ // pre w/ adap: 0 1 2 b 3 4
+ @Override
+ void onRun(TestAdapter adapter) throws Throwable {
+ adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{2, -2});
+ }
+ }, PositionConstraint.scrap(2, 2, -1), PositionConstraint.scrap(1, 1, 1),
+ PositionConstraint.scrap(3, 3, 3)
+ );
+ }
+
+ public void testAddDelete1() throws Throwable {
+ positionStatesTest(5, 0, 5, new AdapterOps() {
+ // 0 1 2 3 4
+ // 0 1 2 a b 3 4
+ // 0 2 a b 3 4
+ // 0 c d 2 a b 3 4
+ // 0 c d 2 a 4
+ // c d 2 a 4
+ // pre: 0 1 2 3 4
+ @Override
+ void onRun(TestAdapter adapter) throws Throwable {
+ adapter.addDeleteAndNotify(new int[]{3, 2}, new int[]{1, -1},
+ new int[]{1, 2}, new int[]{5, -2}, new int[]{0, -1});
+ }
+ }, PositionConstraint.scrap(0, 0, -1), PositionConstraint.scrap(1, 1, -1),
+ PositionConstraint.scrap(2, 2, 2), PositionConstraint.scrap(3, 3, -1),
+ PositionConstraint.scrap(4, 4, 4), PositionConstraint.adapter(0),
+ PositionConstraint.adapter(1), PositionConstraint.adapter(3)
+ );
+ }
+
+ public void testAddSameIndexTwice() throws Throwable {
+ positionStatesTest(12, 2, 7, new AdapterOps() {
+ @Override
+ void onRun(TestAdapter adapter) throws Throwable {
+ adapter.addAndNotify(new int[]{1, 2}, new int[]{5, 1}, new int[]{5, 1},
+ new int[]{11, 1});
+ }
+ }, PositionConstraint.adapterScrap(0, 0), PositionConstraint.adapterScrap(1, 3),
+ PositionConstraint.scrap(2, 2, 4), PositionConstraint.scrap(3, 3, 7),
+ PositionConstraint.scrap(4, 4, 8), PositionConstraint.scrap(7, 7, 12),
+ PositionConstraint.scrap(8, 8, 13)
+ );
+ }
+
+ public void testDeleteTwice() throws Throwable {
+ positionStatesTest(12, 2, 7, new AdapterOps() {
+ @Override
+ void onRun(TestAdapter adapter) throws Throwable {
+ adapter.deleteAndNotify(new int[]{0, 1}, new int[]{1, 1}, new int[]{7, 1},
+ new int[]{0, 1});// delete item ids 0,2,9,1
+ }
+ }, PositionConstraint.scrap(2, 0, -1), PositionConstraint.scrap(3, 1, 0),
+ PositionConstraint.scrap(4, 2, 1), PositionConstraint.scrap(5, 3, 2),
+ PositionConstraint.scrap(6, 4, 3), PositionConstraint.scrap(8, 6, 5),
+ PositionConstraint.adapterScrap(7, 6), PositionConstraint.adapterScrap(8, 7)
+ );
+ }
+
+
+ public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
+ int firstLayoutItemCount, AdapterOps adapterChanges,
+ final PositionConstraint... constraints) throws Throwable {
+ positionStatesTest(itemCount, firstLayoutStartIndex, firstLayoutItemCount, null,
+ adapterChanges, constraints);
+ }
+ public void positionStatesTest(int itemCount, int firstLayoutStartIndex,
+ int firstLayoutItemCount,TestAdapter adapter, AdapterOps adapterChanges,
+ final PositionConstraint... constraints) throws Throwable {
+ setupBasic(itemCount, firstLayoutStartIndex, firstLayoutItemCount, adapter);
+ mLayoutManager.expectLayouts(2);
+ mLayoutManager.mOnLayoutCallbacks = new OnLayoutCallbacks() {
+ @Override
+ void beforePreLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
+ RecyclerView.State state) {
+ super.beforePreLayout(recycler, lm, state);
+ //harmless
+ lm.detachAndScrapAttachedViews(recycler);
+ final int[] ids = new int[constraints.length];
+ for (int i = 0; i < constraints.length; i++) {
+ ids[i] = constraints[i].mPreLayoutPos;
+ }
+ Map<Integer, CollectPositionResult> positions
+ = collectPositions(lm.mRecyclerView, recycler, state, ids);
+ for (PositionConstraint constraint : constraints) {
+ if (constraint.mPreLayoutPos != -1) {
+ constraint.validate(state, positions.get(constraint.mPreLayoutPos),
+ lm.getLog());
+ }
+ }
+ }
+
+ @Override
+ void beforePostLayout(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
+ RecyclerView.State state) {
+ super.beforePostLayout(recycler, lm, state);
+ lm.detachAndScrapAttachedViews(recycler);
+ final int[] ids = new int[constraints.length];
+ for (int i = 0; i < constraints.length; i++) {
+ ids[i] = constraints[i].mPostLayoutPos;
+ }
+ Map<Integer, CollectPositionResult> positions
+ = collectPositions(lm.mRecyclerView, recycler, state, ids);
+ for (PositionConstraint constraint : constraints) {
+ if (constraint.mPostLayoutPos >= 0) {
+ constraint.validate(state, positions.get(constraint.mPostLayoutPos),
+ lm.getLog());
+ }
+ }
+ }
+ };
+ adapterChanges.run(mTestAdapter);
+ mLayoutManager.waitForLayout(2);
+ checkForMainThreadException();
+ for (PositionConstraint constraint : constraints) {
+ constraint.assertValidate();
+ }
+ }
+
class AnimationLayoutManager extends TestLayoutManager {
+ private int mTotalLayoutCount = 0;
+ private String log;
+
OnLayoutCallbacks mOnLayoutCallbacks = new OnLayoutCallbacks() {
};
+
+
@Override
- public boolean supportsItemAnimations() {
+ public boolean supportsPredictiveItemAnimations() {
return true;
}
+ public String getLog() {
+ return log;
+ }
+
+ private String prepareLog(RecyclerView.Recycler recycler, RecyclerView.State state, boolean done) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("is pre layout:").append(state.isPreLayout()).append(", done:").append(done);
+ builder.append("\nViewHolders:\n");
+ for (RecyclerView.ViewHolder vh : ((TestRecyclerView)mRecyclerView).collectViewHolders()) {
+ builder.append(vh).append("\n");
+ }
+ builder.append("scrap:\n");
+ for (RecyclerView.ViewHolder vh : recycler.getScrapList()) {
+ builder.append(vh).append("\n");
+ }
+
+ if (state.isPreLayout() && !done) {
+ log = "\n" + builder.toString();
+ } else {
+ log += "\n" + builder.toString();
+ }
+ return log;
+ }
+
@Override
public void expectLayouts(int count) {
super.expectLayouts(count);
@@ -179,12 +603,61 @@
public final void onLayoutChildren(RecyclerView.Recycler recycler,
RecyclerView.State state) {
try {
+ mTotalLayoutCount++;
+ prepareLog(recycler, state, false);
+ if (state.isPreLayout()) {
+ validateOldPositions(recycler, state);
+ } else {
+ validateClearedOldPositions(recycler, state);
+ }
mOnLayoutCallbacks.onLayoutChildren(recycler, this, state);
+ prepareLog(recycler, state, true);
} finally {
layoutLatch.countDown();
}
}
+ private void validateClearedOldPositions(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ if (getTestRecyclerView() == null) {
+ return;
+ }
+ for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) {
+ assertEquals("there should NOT be an old position in post layout",
+ RecyclerView.NO_POSITION, viewHolder.mOldPosition);
+ assertEquals("there should NOT be a pre layout position in post layout",
+ RecyclerView.NO_POSITION, viewHolder.mPreLayoutPosition);
+ }
+ }
+
+ private void validateOldPositions(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ if (getTestRecyclerView() == null) {
+ return;
+ }
+ for (RecyclerView.ViewHolder viewHolder : getTestRecyclerView().collectViewHolders()) {
+ if (!viewHolder.isRemoved() && !viewHolder.isInvalid()) {
+ assertTrue("there should be an old position in pre-layout",
+ viewHolder.mOldPosition != RecyclerView.NO_POSITION);
+ }
+ }
+ }
+
+ public int getTotalLayoutCount() {
+ return mTotalLayoutCount;
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return true;
+ }
+
+ @Override
+ public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ mOnLayoutCallbacks.onScroll(dy, recycler, state);
+ return super.scrollVerticallyBy(dy, recycler, state);
+ }
public void onPostDispatchLayout() {
mOnLayoutCallbacks.postDispatchLayout();
@@ -207,21 +680,21 @@
int expectedPostLayoutItemCount = -1;
- private int mLayoutCount;
-
int mDeletedViewCount;
+ int mLayoutCount = 0;
+
void setExpectedItemCounts(int preLayout, int postLayout) {
expectedPreLayoutItemCount = preLayout;
expectedPostLayoutItemCount = postLayout;
}
void reset() {
- mLayoutCount = 0;
mLayoutMin = Integer.MIN_VALUE;
mLayoutItemCount = Integer.MAX_VALUE;
expectedPreLayoutItemCount = -1;
expectedPostLayoutItemCount = -1;
+ mLayoutCount = 0;
}
void beforePreLayout(RecyclerView.Recycler recycler,
@@ -246,22 +719,20 @@
== Integer.MAX_VALUE ? state.getItemCount() : mLayoutItemCount;
lm.layoutRange(recycler, start, start + count);
assertEquals("correct # of children should be laid out",
- count - (inPreLayout() ? mDeletedViewCount : 0), lm.getChildCount());
- if (!inPreLayout()) { // may not be the correct check
- lm.assertVisibleItemPositions();
- }
+ count, lm.getChildCount());
+ lm.assertVisibleItemPositions();
}
void onLayoutChildren(RecyclerView.Recycler recycler, AnimationLayoutManager lm,
RecyclerView.State state) {
- if (mLayoutCount == 0) {
+ if (state.isPreLayout()) {
if (expectedPreLayoutItemCount != -1) {
assertEquals("on pre layout, state should return abstracted adapter size",
expectedPreLayoutItemCount, state.getItemCount());
}
beforePreLayout(recycler, lm, state);
- } else if (mLayoutCount == 1) {
+ } else {
if (expectedPostLayoutItemCount != -1) {
assertEquals("on post layout, state should return real adapter size",
expectedPostLayoutItemCount, state.getItemCount());
@@ -269,9 +740,9 @@
beforePostLayout(recycler, lm, state);
}
doLayout(recycler, lm, state);
- if (mLayoutCount == 0) {
+ if (state.isPreLayout()) {
afterPreLayout(recycler, lm, state);
- } else if (mLayoutCount == 1) {
+ } else {
afterPostLayout(recycler, lm, state);
}
mLayoutCount++;
@@ -292,13 +763,15 @@
void postDispatchLayout() {
}
- boolean inPreLayout() {
- return mLayoutCount == 0;
+ public void onScroll(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
+
}
}
class TestRecyclerView extends RecyclerView {
+ CountDownLatch drawLatch;
+
public TestRecyclerView(Context context) {
super(context);
}
@@ -312,6 +785,125 @@
}
@Override
+ void initAdapterManager() {
+ super.initAdapterManager();
+ mAdapterHelper.mOnItemProcessedCallback = new Runnable() {
+ @Override
+ public void run() {
+ validatePostUpdateOp();
+ }
+ };
+ }
+
+ public void expectDraw(int count) {
+ drawLatch = new CountDownLatch(count);
+ }
+
+ public void waitForDraw(long timeout) throws Throwable {
+ drawLatch.await(timeout * (DEBUG ? 100 : 1), TimeUnit.SECONDS);
+ assertEquals("all expected draws should happen at the expected time frame",
+ 0, drawLatch.getCount());
+ }
+
+ List<ViewHolder> collectViewHolders() {
+ List<ViewHolder> holders = new ArrayList<ViewHolder>();
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ ViewHolder holder = getChildViewHolderInt(getChildAt(i));
+ if (holder != null) {
+ holders.add(holder);
+ }
+ }
+ return holders;
+ }
+
+
+ private void validateViewHolderPositions() {
+ final Set<Integer> existingOffsets = new HashSet<Integer>();
+ int childCount = getChildCount();
+ StringBuilder log = new StringBuilder();
+ for (int i = 0; i < childCount; i++) {
+ ViewHolder vh = getChildViewHolderInt(getChildAt(i));
+ TestViewHolder tvh = (TestViewHolder) vh;
+ log.append(tvh.mBindedItem).append(vh)
+ .append(" hidden:")
+ .append(mChildHelper.mHiddenViews.contains(vh.itemView))
+ .append("\n");
+ }
+ for (int i = 0; i < childCount; i++) {
+ ViewHolder vh = getChildViewHolderInt(getChildAt(i));
+ if (vh.isInvalid()) {
+ continue;
+ }
+ if (vh.getPosition() < 0) {
+ LayoutManager lm = getLayoutManager();
+ for (int j = 0; j < lm.getChildCount(); j ++) {
+ assertNotSame("removed view holder should not be in LM's child list",
+ vh.itemView, lm.getChildAt(j));
+ }
+ } else if (!mChildHelper.mHiddenViews.contains(vh.itemView)) {
+ if (!existingOffsets.add(vh.getPosition())) {
+ throw new IllegalStateException("view holder position conflict for "
+ + "existing views " + vh + "\n" + log);
+ }
+ }
+ }
+ }
+
+ void validatePostUpdateOp() {
+ try {
+ validateViewHolderPositions();
+ if (super.mState.isPreLayout()) {
+ validatePreLayoutSequence((AnimationLayoutManager) getLayoutManager());
+ }
+ validateAdapterPosition((AnimationLayoutManager) getLayoutManager());
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ }
+ }
+
+
+
+ private void validateAdapterPosition(AnimationLayoutManager lm) {
+ for (ViewHolder vh : collectViewHolders()) {
+ if (!vh.isRemoved() && vh.mPreLayoutPosition >= 0) {
+ assertEquals("adapter position calculations should match view holder "
+ + "pre layout:" + mState.isPreLayout()
+ + " positions\n" + vh + "\n" + lm.getLog(),
+ mAdapterHelper.findPositionOffset(vh.mPreLayoutPosition), vh.mPosition);
+ }
+ }
+ }
+
+ // ensures pre layout positions are continuous block. This is not necessarily a case
+ // but valid in test RV
+ private void validatePreLayoutSequence(AnimationLayoutManager lm) {
+ Set<Integer> preLayoutPositions = new HashSet<Integer>();
+ for (ViewHolder vh : collectViewHolders()) {
+ assertTrue("pre layout positions should be distinct " + lm.getLog(),
+ preLayoutPositions.add(vh.mPreLayoutPosition));
+ }
+ int minPos = Integer.MAX_VALUE;
+ for (Integer pos : preLayoutPositions) {
+ if (pos < minPos) {
+ minPos = pos;
+ }
+ }
+ for (int i = 1; i < preLayoutPositions.size(); i++) {
+ assertNotNull("next position should exist " + lm.getLog(),
+ preLayoutPositions.contains(minPos + i));
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ super.dispatchDraw(canvas);
+ if (drawLatch != null) {
+ drawLatch.countDown();
+ }
+ }
+
+ @Override
void dispatchLayout() {
try {
super.dispatchLayout();
@@ -324,17 +916,132 @@
}
- private void postExceptionToInstrumentation(Throwable t) {
- if (DEBUG) {
- Log.e(TAG, "captured exception on main thread", t);
+
+ }
+
+ abstract class AdapterOps {
+
+ final public void run(TestAdapter adapter) throws Throwable {
+ onRun(adapter);
+ }
+
+ abstract void onRun(TestAdapter testAdapter) throws Throwable;
+ }
+
+ static class CollectPositionResult {
+
+ // true if found in scrap
+ public RecyclerView.ViewHolder scrapResult;
+
+ public RecyclerView.ViewHolder adapterResult;
+
+ static CollectPositionResult fromScrap(RecyclerView.ViewHolder viewHolder) {
+ CollectPositionResult cpr = new CollectPositionResult();
+ cpr.scrapResult = viewHolder;
+ return cpr;
+ }
+
+ static CollectPositionResult fromAdapter(RecyclerView.ViewHolder viewHolder) {
+ CollectPositionResult cpr = new CollectPositionResult();
+ cpr.adapterResult = viewHolder;
+ return cpr;
+ }
+ }
+
+ static class PositionConstraint {
+
+ public static enum Type {
+ scrap,
+ adapter,
+ adapterScrap /*first pass adapter, second pass scrap*/
+ }
+
+ Type mType;
+
+ int mOldPos; // if VH
+
+ int mPreLayoutPos;
+
+ int mPostLayoutPos;
+
+ int mValidateCount = 0;
+
+ public static PositionConstraint scrap(int oldPos, int preLayoutPos, int postLayoutPos) {
+ PositionConstraint constraint = new PositionConstraint();
+ constraint.mType = Type.scrap;
+ constraint.mOldPos = oldPos;
+ constraint.mPreLayoutPos = preLayoutPos;
+ constraint.mPostLayoutPos = postLayoutPos;
+ return constraint;
+ }
+
+ public static PositionConstraint adapterScrap(int preLayoutPos, int position) {
+ PositionConstraint constraint = new PositionConstraint();
+ constraint.mType = Type.adapterScrap;
+ constraint.mOldPos = RecyclerView.NO_POSITION;
+ constraint.mPreLayoutPos = preLayoutPos;
+ constraint.mPostLayoutPos = position;// adapter pos does not change
+ return constraint;
+ }
+
+ public static PositionConstraint adapter(int position) {
+ PositionConstraint constraint = new PositionConstraint();
+ constraint.mType = Type.adapter;
+ constraint.mPreLayoutPos = RecyclerView.NO_POSITION;
+ constraint.mOldPos = RecyclerView.NO_POSITION;
+ constraint.mPostLayoutPos = position;// adapter pos does not change
+ return constraint;
+ }
+
+ public void assertValidate() {
+ int expectedValidate = 0;
+ if (mPreLayoutPos >= 0) {
+ expectedValidate ++;
}
- mainThreadException = t;
- if (mLayoutManager instanceof TestLayoutManager) {
- TestLayoutManager lm = mLayoutManager;
- // finish all layouts so that we get the correct exception
- while (lm.layoutLatch.getCount() > 0) {
- lm.layoutLatch.countDown();
+ if (mPostLayoutPos >= 0) {
+ expectedValidate ++;
+ }
+ assertEquals("should run all validates", expectedValidate, mValidateCount);
+ }
+
+ @Override
+ public String toString() {
+ return "Cons{" +
+ "t=" + mType.name() +
+ ", old=" + mOldPos +
+ ", pre=" + mPreLayoutPos +
+ ", post=" + mPostLayoutPos +
+ '}';
+ }
+
+ public void validate(RecyclerView.State state, CollectPositionResult result, String log) {
+ mValidateCount ++;
+ assertNotNull(this + ": result should not be null\n" + log, result);
+ RecyclerView.ViewHolder viewHolder;
+ if (mType == Type.scrap || (mType == Type.adapterScrap && !state.isPreLayout())) {
+ assertNotNull(this + ": result should come from scrap\n" + log, result.scrapResult);
+ viewHolder = result.scrapResult;
+ } else {
+ assertNotNull(this + ": result should come from adapter\n" + log,
+ result.adapterResult);
+ assertEquals(this + ": old position should be none when it came from adapter\n" + log,
+ RecyclerView.NO_POSITION, result.adapterResult.getOldPosition());
+ viewHolder = result.adapterResult;
+ }
+ if (state.isPreLayout()) {
+ assertEquals(this + ": pre-layout position should match\n" + log, mPreLayoutPos,
+ viewHolder.mPreLayoutPosition == -1 ? viewHolder.mPosition :
+ viewHolder.mPreLayoutPosition);
+ assertEquals(this + ": pre-layout getPosition should match\n" + log, mPreLayoutPos,
+ viewHolder.getPosition());
+ if (mType == Type.scrap) {
+ assertEquals(this + ": old position should match\n" + log, mOldPos,
+ result.scrapResult.getOldPosition());
}
+ } else if (mType == Type.adapter || mType == Type.adapterScrap || !result.scrapResult
+ .isRemoved()) {
+ assertEquals(this + ": post-layout position should match\n" + log + "\n\n"
+ + viewHolder, mPostLayoutPos, viewHolder.getPosition());
}
}
}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
index 2d076ce..30df96e 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewLayoutTest.java
@@ -17,12 +17,734 @@
package android.support.v7.widget;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class RecyclerViewLayoutTest extends BaseRecyclerViewInstrumentationTest {
+ private static final boolean DEBUG = false;
+
+ public RecyclerViewLayoutTest() {
+ super(DEBUG);
+ }
+
+ public void testInvalidateDecorOffsets() throws Throwable {
+ final TestAdapter adapter = new TestAdapter(10);
+ adapter.setHasStableIds(true);
+ final RecyclerView recyclerView = new RecyclerView(getActivity());
+ recyclerView.setAdapter(adapter);
+
+ final Map<Long, Boolean> changes = new HashMap<Long, Boolean>();
+
+ TestLayoutManager testLayoutManager = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ try {
+ if (changes.size() > 0) {
+ // test
+ for (int i = 0; i < getChildCount(); i ++) {
+ View child = getChildAt(i);
+ RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)
+ child.getLayoutParams();
+ RecyclerView.ViewHolder vh = lp.mViewHolder;
+ if (!changes.containsKey(vh.getItemId())) {
+ continue; //nothing to test
+ }
+ assertEquals(
+ "Decord insets validation for VH should have expected value.",
+ changes.get(vh.getItemId()).booleanValue(),
+ lp.mInsetsDirty);
+ }
+ }
+ detachAndScrapAttachedViews(recycler);
+ layoutRange(recycler, 0, state.getItemCount());
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ } finally {
+ layoutLatch.countDown();
+ }
+ }
+
+ @Override
+ public boolean supportsPredictiveItemAnimations() {
+ return false;
+ }
+ };
+ recyclerView.setLayoutManager(testLayoutManager);
+ testLayoutManager.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ testLayoutManager.waitForLayout(2);
+ int itemAddedTo = 5;
+ for (int i = 0; i < itemAddedTo; i++) {
+ changes.put(mRecyclerView.findViewHolderForPosition(i).getItemId(), false);
+ }
+ for (int i = itemAddedTo; i < mRecyclerView.getChildCount(); i++) {
+ changes.put(mRecyclerView.findViewHolderForPosition(i).getItemId(), true);
+ }
+ testLayoutManager.expectLayouts(1);
+ adapter.addAndNotify(5, 1);
+ testLayoutManager.waitForLayout(2);
+ checkForMainThreadException();
+
+ changes.clear();
+ int[] changedItems = new int[]{3, 5, 6};
+ for (int i = 0; i < adapter.getItemCount(); i ++) {
+ changes.put(mRecyclerView.findViewHolderForPosition(i).getItemId(), false);
+ }
+ for (int i = 0; i < changedItems.length; i ++) {
+ changes.put(mRecyclerView.findViewHolderForPosition(changedItems[i]).getItemId(), true);
+ }
+ testLayoutManager.expectLayouts(1);
+ adapter.changePositionsAndNotify(changedItems);
+ testLayoutManager.waitForLayout(2);
+ checkForMainThreadException();
+
+ for (int i = 0; i < adapter.getItemCount(); i ++) {
+ changes.put(mRecyclerView.findViewHolderForPosition(i).getItemId(), true);
+ }
+ testLayoutManager.expectLayouts(1);
+ adapter.dispatchDataSetChanged();
+ testLayoutManager.waitForLayout(2);
+ checkForMainThreadException();
+ }
+
+ public void testMovingViaStableIds() throws Throwable {
+ stableIdsMoveTest(true);
+ removeRecyclerView();
+ stableIdsMoveTest(false);
+ removeRecyclerView();
+ }
+
+ public void stableIdsMoveTest(final boolean supportsPredictive) throws Throwable {
+ final TestAdapter testAdapter = new TestAdapter(10);
+ testAdapter.setHasStableIds(true);
+ final AtomicBoolean test = new AtomicBoolean(false);
+ final int movedViewFromIndex = 3;
+ final int movedViewToIndex = 6;
+ final View[] movedView = new View[1];
+ TestLayoutManager lm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ detachAndScrapAttachedViews(recycler);
+ try {
+ if (test.get()) {
+ if (state.isPreLayout()) {
+ View view = recycler.getViewForPosition(movedViewFromIndex, true);
+ assertSame("In pre layout, should be able to get moved view w/ old "
+ + "position", movedView[0], view);
+ RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view);
+ assertTrue("it should come from scrap", holder.wasReturnedFromScrap());
+ // clear scrap flag
+ holder.clearReturnedFromScrapFlag();
+ } else {
+ View view = recycler.getViewForPosition(movedViewToIndex, true);
+ assertSame("In post layout, should be able to get moved view w/ new "
+ + "position", movedView[0], view);
+ RecyclerView.ViewHolder holder = mRecyclerView.getChildViewHolder(view);
+ assertTrue("it should come from scrap", holder.wasReturnedFromScrap());
+ // clear scrap flag
+ holder.clearReturnedFromScrapFlag();
+ }
+ }
+ layoutRange(recycler, 0, state.getItemCount());
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ } finally {
+ layoutLatch.countDown();
+ }
+
+
+ }
+
+ @Override
+ public boolean supportsPredictiveItemAnimations() {
+ return supportsPredictive;
+ }
+ };
+ RecyclerView recyclerView = new RecyclerView(this.getActivity());
+ recyclerView.setAdapter(testAdapter);
+ recyclerView.setLayoutManager(lm);
+ lm.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ lm.waitForLayout(1);
+
+ movedView[0] = recyclerView.getChildAt(movedViewFromIndex);
+ test.set(true);
+ lm.expectLayouts(supportsPredictive ? 2 : 1);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Item item = testAdapter.mItems.remove(movedViewFromIndex);
+ testAdapter.mItems.add(movedViewToIndex, item);
+ testAdapter.notifyItemRemoved(movedViewFromIndex);
+ testAdapter.notifyItemInserted(movedViewToIndex);
+ }
+ });
+ lm.waitForLayout(2);
+ checkForMainThreadException();
+ }
+
+ public void testAdapterChangeDuringLayout() throws Throwable {
+ adapterChangeInMainThreadTest("notifyDataSetChanged", new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getAdapter().notifyDataSetChanged();
+ }
+ });
+
+ adapterChangeInMainThreadTest("notifyItemChanged", new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getAdapter().notifyItemChanged(2);
+ }
+ });
+
+ adapterChangeInMainThreadTest("notifyItemInserted", new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getAdapter().notifyItemInserted(2);
+ }
+ });
+ adapterChangeInMainThreadTest("notifyItemRemoved", new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getAdapter().notifyItemRemoved(2);
+ }
+ });
+ }
+
+ public void adapterChangeInMainThreadTest(String msg,
+ final Runnable onLayoutRunnable) throws Throwable {
+ final AtomicBoolean doneFirstLayout = new AtomicBoolean(false);
+ TestAdapter testAdapter = new TestAdapter(10);
+ TestLayoutManager lm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ super.onLayoutChildren(recycler, state);
+ try {
+ layoutRange(recycler, 0, state.getItemCount());
+ if (doneFirstLayout.get()) {
+ onLayoutRunnable.run();
+ }
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ } finally {
+ layoutLatch.countDown();
+ }
+
+ }
+ };
+ RecyclerView recyclerView = new RecyclerView(getActivity());
+ recyclerView.setLayoutManager(lm);
+ recyclerView.setAdapter(testAdapter);
+ lm.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ lm.waitForLayout(2);
+ doneFirstLayout.set(true);
+ lm.expectLayouts(1);
+ requestLayoutOnUIThread(recyclerView);
+ lm.waitForLayout(2);
+ removeRecyclerView();
+ assertTrue("Invalid data updates should be caught:" + msg,
+ mainThreadException instanceof IllegalStateException);
+ }
+
+ public void testAdapterChangeDuringScroll() throws Throwable {
+ for (int orientation : new int[]{OrientationHelper.HORIZONTAL,
+ OrientationHelper.VERTICAL}) {
+ adapterChangeDuringScrollTest("notifyDataSetChanged", orientation,
+ new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getAdapter().notifyDataSetChanged();
+ }
+ });
+ adapterChangeDuringScrollTest("notifyItemChanged", orientation, new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getAdapter().notifyItemChanged(2);
+ }
+ });
+
+ adapterChangeDuringScrollTest("notifyItemInserted", orientation, new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getAdapter().notifyItemInserted(2);
+ }
+ });
+ adapterChangeDuringScrollTest("notifyItemRemoved", orientation, new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getAdapter().notifyItemRemoved(2);
+ }
+ });
+ }
+ }
+
+ public void adapterChangeDuringScrollTest(String msg, final int orientation,
+ final Runnable onScrollRunnable) throws Throwable {
+ TestAdapter testAdapter = new TestAdapter(100);
+ TestLayoutManager lm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ super.onLayoutChildren(recycler, state);
+ try {
+ layoutRange(recycler, 0, 10);
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ } finally {
+ layoutLatch.countDown();
+ }
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return orientation == OrientationHelper.VERTICAL;
+ }
+
+ @Override
+ public boolean canScrollHorizontally() {
+ return orientation == OrientationHelper.HORIZONTAL;
+ }
+
+ public int mockScroll() {
+ try {
+ onScrollRunnable.run();
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ } finally {
+ layoutLatch.countDown();
+ }
+ return 0;
+ }
+
+ @Override
+ public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ return mockScroll();
+ }
+
+ @Override
+ public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ return mockScroll();
+ }
+ };
+ RecyclerView recyclerView = new RecyclerView(getActivity());
+ recyclerView.setLayoutManager(lm);
+ recyclerView.setAdapter(testAdapter);
+ lm.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ lm.waitForLayout(2);
+ lm.expectLayouts(1);
+ scrollBy(200);
+ lm.waitForLayout(2);
+ removeRecyclerView();
+ assertTrue("Invalid data updates should be caught:" + msg,
+ mainThreadException instanceof IllegalStateException);
+ }
+
+ public void testRecycleOnDetach() throws Throwable {
+ final RecyclerView recyclerView = new RecyclerView(getActivity());
+ final TestAdapter testAdapter = new TestAdapter(10);
+ final AtomicBoolean didRunOnDetach = new AtomicBoolean(false);
+ final TestLayoutManager lm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ super.onLayoutChildren(recycler, state);
+ layoutRange(recycler, 0, state.getItemCount() - 1);
+ layoutLatch.countDown();
+ }
+
+ @Override
+ public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
+ super.onDetachedFromWindow(view, recycler);
+ didRunOnDetach.set(true);
+ removeAndRecycleAllViews(recycler);
+ }
+ };
+ recyclerView.setAdapter(testAdapter);
+ recyclerView.setLayoutManager(lm);
+ lm.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ lm.waitForLayout(2);
+ removeRecyclerView();
+ assertTrue("When recycler view is removed, detach should run", didRunOnDetach.get());
+ assertEquals("All children should be recycled", recyclerView.getChildCount(), 0);
+ }
+
+ public void testUpdatesWhileDetached() throws Throwable {
+ final RecyclerView recyclerView = new RecyclerView(getActivity());
+ final int initialAdapterSize = 20;
+ final TestAdapter adapter = new TestAdapter(initialAdapterSize);
+ final AtomicInteger layoutCount = new AtomicInteger(0);
+ TestLayoutManager lm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ super.onLayoutChildren(recycler, state);
+ layoutRange(recycler, 0, 5);
+ layoutCount.incrementAndGet();
+ layoutLatch.countDown();
+ }
+ };
+ recyclerView.setAdapter(adapter);
+ recyclerView.setLayoutManager(lm);
+ recyclerView.setHasFixedSize(true);
+ lm.expectLayouts(1);
+ adapter.addAndNotify(4, 5);
+ lm.assertNoLayout("When RV is not attached, layout should not happen", 1);
+ }
+
+ public void testUpdatesAfterDetach() throws Throwable {
+ final RecyclerView recyclerView = new RecyclerView(getActivity());
+ final int initialAdapterSize = 20;
+ final TestAdapter adapter = new TestAdapter(initialAdapterSize);
+ final AtomicInteger layoutCount = new AtomicInteger(0);
+ TestLayoutManager lm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ super.onLayoutChildren(recycler, state);
+ layoutRange(recycler, 0, 5);
+ layoutCount.incrementAndGet();
+ layoutLatch.countDown();
+ }
+ };
+ recyclerView.setAdapter(adapter);
+ recyclerView.setLayoutManager(lm);
+ lm.expectLayouts(1);
+ recyclerView.setHasFixedSize(true);
+ setRecyclerView(recyclerView);
+ lm.waitForLayout(2);
+ lm.expectLayouts(1);
+ final int prevLayoutCount = layoutCount.get();
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ adapter.addAndNotify(4, 5);
+ removeRecyclerView();
+ } catch (Throwable throwable) {
+ throwable.printStackTrace();
+ }
+ }
+ });
+
+ lm.assertNoLayout("When RV is not attached, layout should not happen", 1);
+ assertEquals("No extra layout should happen when detached", prevLayoutCount,
+ layoutCount.get());
+ }
+
+ public void testNotifyDataSetChangedWithStableIds() throws Throwable {
+ final int defaultViewType = 1;
+ final Map<Item, Integer> viewTypeMap = new HashMap<Item, Integer>();
+ final Map<Integer, Integer> oldPositionToNewPositionMapping =
+ new HashMap<Integer, Integer>();
+ final TestAdapter adapter = new TestAdapter(100) {
+ @Override
+ public int getItemViewType(int position) {
+ Integer type = viewTypeMap.get(mItems.get(position));
+ return type == null ? defaultViewType : type;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mItems.get(position).mId;
+ }
+ };
+ adapter.setHasStableIds(true);
+ final ArrayList<Item> previousItems = new ArrayList<Item>();
+ previousItems.addAll(adapter.mItems);
+
+ final AtomicInteger layoutStart = new AtomicInteger(50);
+ final AtomicBoolean validate = new AtomicBoolean(false);
+ final int childCount = 10;
+ final TestLayoutManager lm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ try {
+ super.onLayoutChildren(recycler, state);
+ if (validate.get()) {
+ assertEquals("Cached views should be kept", 5, recycler
+ .mCachedViews.size());
+ for (RecyclerView.ViewHolder vh : recycler.mCachedViews) {
+ TestViewHolder tvh = (TestViewHolder) vh;
+ assertTrue("view holder should be marked for update",
+ tvh.needsUpdate());
+ assertTrue("view holder should be marked as invalid", tvh.isInvalid());
+ }
+ }
+ detachAndScrapAttachedViews(recycler);
+ if (validate.get()) {
+ assertEquals("cache size should stay the same", 5,
+ recycler.mCachedViews.size());
+ assertEquals("all views should be scrapped", childCount,
+ recycler.getScrapList().size());
+ for (RecyclerView.ViewHolder vh : recycler.getScrapList()) {
+ // TODO create test case for type change
+ TestViewHolder tvh = (TestViewHolder) vh;
+ assertTrue("view holder should be marked for update",
+ tvh.needsUpdate());
+ assertTrue("view holder should be marked as invalid", tvh.isInvalid());
+ }
+ }
+ layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount);
+ if (validate.get()) {
+ for (int i = 0; i < getChildCount(); i++) {
+ View view = getChildAt(i);
+ TestViewHolder tvh = (TestViewHolder) mRecyclerView
+ .getChildViewHolder(view);
+ final int oldPos = previousItems.indexOf(tvh.mBindedItem);
+ assertEquals("view holder's position should be correct",
+ oldPositionToNewPositionMapping.get(oldPos).intValue(),
+ tvh.getPosition());
+ ;
+ }
+ }
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ } finally {
+ layoutLatch.countDown();
+ }
+ }
+ };
+ final RecyclerView recyclerView = new RecyclerView(getActivity());
+ recyclerView.setItemAnimator(null);
+ recyclerView.setAdapter(adapter);
+ recyclerView.setLayoutManager(lm);
+ recyclerView.setItemViewCacheSize(10);
+ lm.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ lm.waitForLayout(2);
+ checkForMainThreadException();
+ getInstrumentation().waitForIdleSync();
+ layoutStart.set(layoutStart.get() + 5);//55
+ lm.expectLayouts(1);
+ requestLayoutOnUIThread(recyclerView);
+ lm.waitForLayout(2);
+ validate.set(true);
+ lm.expectLayouts(1);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ adapter.moveItems(false,
+ new int[]{50, 56}, new int[]{51, 1}, new int[]{52, 2},
+ new int[]{53, 54}, new int[]{60, 61}, new int[]{62, 64},
+ new int[]{75, 58});
+ for (int i = 0; i < previousItems.size(); i++) {
+ Item item = previousItems.get(i);
+ oldPositionToNewPositionMapping.put(i, adapter.mItems.indexOf(item));
+ }
+ adapter.dispatchDataSetChanged();
+ } catch (Throwable throwable) {
+ postExceptionToInstrumentation(throwable);
+ }
+ }
+ });
+ lm.waitForLayout(2);
+ checkForMainThreadException();
+ }
+
+ public void testFindViewById() throws Throwable {
+ findViewByIdTest(false);
+ removeRecyclerView();
+ findViewByIdTest(true);
+ }
+
+ public void findViewByIdTest(final boolean supportPredictive) throws Throwable {
+ final RecyclerView recyclerView = new RecyclerView(getActivity());
+ final int initialAdapterSize = 20;
+ final TestAdapter adapter = new TestAdapter(initialAdapterSize);
+ final int deleteStart = 6;
+ final int deleteCount = 5;
+ recyclerView.setAdapter(adapter);
+ final AtomicBoolean assertPositions = new AtomicBoolean(false);
+ TestLayoutManager lm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ super.onLayoutChildren(recycler, state);
+ if (assertPositions.get()) {
+ if (state.isPreLayout()) {
+ for (int i = 0; i < deleteStart; i++) {
+ View view = findViewByPosition(i);
+ assertNotNull("find view by position for existing items should work "
+ + "fine", view);
+ assertFalse("view should not be marked as removed",
+ ((RecyclerView.LayoutParams) view.getLayoutParams())
+ .isItemRemoved());
+ }
+ for (int i = 0; i < deleteCount; i++) {
+ View view = findViewByPosition(i + deleteStart);
+ assertNotNull("find view by position should work fine for removed "
+ + "views in pre-layout", view);
+ assertTrue("view should be marked as removed",
+ ((RecyclerView.LayoutParams) view.getLayoutParams())
+ .isItemRemoved());
+ }
+ for (int i = deleteStart + deleteCount; i < 20; i++) {
+ View view = findViewByPosition(i);
+ assertNotNull(view);
+ assertFalse("view should not be marked as removed",
+ ((RecyclerView.LayoutParams) view.getLayoutParams())
+ .isItemRemoved());
+ }
+ } else {
+ for (int i = 0; i < initialAdapterSize - deleteCount; i++) {
+ View view = findViewByPosition(i);
+ assertNotNull("find view by position for existing item " + i +
+ " should work fine. child count:" + getChildCount(), view);
+ TestViewHolder viewHolder =
+ (TestViewHolder) mRecyclerView.getChildViewHolder(view);
+ assertSame("should be the correct item " + viewHolder
+ , viewHolder.mBindedItem,
+ adapter.mItems.get(viewHolder.mPosition));
+ assertFalse("view should not be marked as removed",
+ ((RecyclerView.LayoutParams) view.getLayoutParams())
+ .isItemRemoved());
+ }
+ }
+ }
+ detachAndScrapAttachedViews(recycler);
+ layoutRange(recycler, state.getItemCount() - 1, -1);
+ layoutLatch.countDown();
+ }
+
+ @Override
+ public boolean supportsPredictiveItemAnimations() {
+ return supportPredictive;
+ }
+ };
+ recyclerView.setLayoutManager(lm);
+ lm.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ lm.waitForLayout(2);
+ getInstrumentation().waitForIdleSync();
+
+ assertPositions.set(true);
+ lm.expectLayouts(supportPredictive ? 2 : 1);
+ adapter.deleteAndNotify(new int[]{deleteStart, deleteCount - 1}, new int[]{deleteStart, 1});
+ lm.waitForLayout(2);
+ }
+
+ public void testTypeForCache() throws Throwable {
+ final AtomicInteger viewType = new AtomicInteger(1);
+ final TestAdapter adapter = new TestAdapter(100) {
+ @Override
+ public int getItemViewType(int position) {
+ return viewType.get();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mItems.get(position).mId;
+ }
+ };
+ adapter.setHasStableIds(true);
+ final AtomicInteger layoutStart = new AtomicInteger(2);
+ final int childCount = 10;
+ final TestLayoutManager lm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ super.onLayoutChildren(recycler, state);
+ detachAndScrapAttachedViews(recycler);
+ layoutRange(recycler, layoutStart.get(), layoutStart.get() + childCount);
+ layoutLatch.countDown();
+ }
+ };
+ final RecyclerView recyclerView = new RecyclerView(getActivity());
+ recyclerView.setItemAnimator(null);
+ recyclerView.setAdapter(adapter);
+ recyclerView.setLayoutManager(lm);
+ recyclerView.setItemViewCacheSize(10);
+ lm.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ lm.waitForLayout(2);
+ getInstrumentation().waitForIdleSync();
+ layoutStart.set(4); // trigger a cache for 3,4
+ lm.expectLayouts(1);
+ requestLayoutOnUIThread(recyclerView);
+ lm.waitForLayout(2);
+ //
+ viewType.incrementAndGet();
+ layoutStart.set(2); // go back to bring views from cache
+ lm.expectLayouts(1);
+ adapter.mItems.remove(1);
+ adapter.dispatchDataSetChanged();
+ lm.waitForLayout(2);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 2; i < 4; i++) {
+ RecyclerView.ViewHolder vh = recyclerView.findViewHolderForPosition(i);
+ assertEquals("View holder's type should match latest type", viewType.get(),
+ vh.getItemViewType());
+ }
+ }
+ });
+ }
+
+ public void testTypeForExistingViews() throws Throwable {
+ final AtomicInteger viewType = new AtomicInteger(1);
+ final int invalidatedCount = 2;
+ final int layoutStart = 2;
+ final TestAdapter adapter = new TestAdapter(100) {
+ @Override
+ public int getItemViewType(int position) {
+ return viewType.get();
+ }
+
+ @Override
+ public void onBindViewHolder(TestViewHolder holder,
+ int position) {
+ super.onBindViewHolder(holder, position);
+ if (position >= layoutStart && position < invalidatedCount + layoutStart) {
+ try {
+ assertEquals("holder type should match current view type at position " +
+ position, viewType.get(), holder.getItemViewType());
+ } catch (Throwable t) {
+ postExceptionToInstrumentation(t);
+ }
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mItems.get(position).mId;
+ }
+ };
+ adapter.setHasStableIds(true);
+
+ final int childCount = 10;
+ final TestLayoutManager lm = new TestLayoutManager() {
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ super.onLayoutChildren(recycler, state);
+ detachAndScrapAttachedViews(recycler);
+ layoutRange(recycler, layoutStart, layoutStart + childCount);
+ layoutLatch.countDown();
+ }
+ };
+ final RecyclerView recyclerView = new RecyclerView(getActivity());
+ recyclerView.setAdapter(adapter);
+ recyclerView.setLayoutManager(lm);
+ lm.expectLayouts(1);
+ setRecyclerView(recyclerView);
+ lm.waitForLayout(2);
+ getInstrumentation().waitForIdleSync();
+ viewType.incrementAndGet();
+ lm.expectLayouts(1);
+ adapter.changeAndNotify(layoutStart, invalidatedCount);
+ lm.waitForLayout(2);
+ checkForMainThreadException();
+ }
+
+
public void testState() throws Throwable {
final TestAdapter adapter = new TestAdapter(10);
final RecyclerView recyclerView = new RecyclerView(getActivity());
@@ -33,6 +755,7 @@
TestLayoutManager testLayoutManager = new TestLayoutManager() {
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ detachAndScrapAttachedViews(recycler);
layoutRange(recycler, 0, state.getItemCount());
itemCount.set(state.getItemCount());
structureChanged.set(state.didStructureChange());
@@ -47,7 +770,6 @@
getActivity().mContainer.addView(recyclerView);
}
});
- Thread.sleep(5000);
testLayoutManager.waitForLayout(2, TimeUnit.SECONDS);
assertEquals("item count in state should be correct", adapter.getItemCount()
@@ -62,20 +784,20 @@
recyclerView.requestLayout();
}
});
- testLayoutManager.waitForLayout(1, TimeUnit.SECONDS);
+ testLayoutManager.waitForLayout(2);
assertEquals("in second layout,structure changed should be false", false,
structureChanged.get());
- testLayoutManager.expectLayouts(recyclerView.getItemAnimator() == null ? 1 : 2); //
+ testLayoutManager.expectLayouts(1); //
adapter.deleteAndNotify(3, 2);
- testLayoutManager.waitForLayout(3, TimeUnit.SECONDS);
+ testLayoutManager.waitForLayout(2);
assertEquals("when items are removed, item count in state should be updated",
adapter.getItemCount(),
itemCount.get());
assertEquals("structure changed should be true when items are removed", true,
structureChanged.get());
- testLayoutManager.expectLayouts(recyclerView.getItemAnimator() == null ? 1 : 2);
+ testLayoutManager.expectLayouts(1);
adapter.addAndNotify(2, 5);
- testLayoutManager.waitForLayout(3, TimeUnit.SECONDS);
+ testLayoutManager.waitForLayout(2);
assertEquals("when items are added, item count in state should be updated",
adapter.getItemCount(),
@@ -85,4 +807,4 @@
}
-}
\ No newline at end of file
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java
new file mode 100644
index 0000000..7509936
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/StaggeredGridLayoutManagerTest.java
@@ -0,0 +1,1132 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.support.v7.widget;
+
+
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static android.support.v7.widget.LayoutState.*;
+import static android.support.v7.widget.StaggeredGridLayoutManager.*;
+
+public class StaggeredGridLayoutManagerTest extends BaseRecyclerViewInstrumentationTest {
+
+ private static final boolean DEBUG = false;
+
+ private static final String TAG = "StaggeredGridLayoutManagerTest";
+
+ WrappedLayoutManager mLayoutManager;
+
+ GridTestAdapter mAdapter;
+
+ RecyclerView mRecyclerView;
+
+ final List<Config> mBaseVariations = new ArrayList<Config>();
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ for (int orientation : new int[]{VERTICAL, HORIZONTAL}) {
+ for (boolean reverseLayout : new boolean[]{false, true}) {
+ for (int spanCount : new int[]{1, 3}) {
+ for (int gapStrategy : new int[]{GAP_HANDLING_NONE,
+ GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}) {
+ mBaseVariations.add(new Config(orientation, reverseLayout, spanCount,
+ gapStrategy));
+ }
+ }
+ }
+ }
+ }
+
+ void setupByConfig(Config config) throws Throwable {
+ mAdapter = new GridTestAdapter(config.mItemCount, config.mOrientation);
+ mRecyclerView = new RecyclerView(getActivity());
+ mRecyclerView.setAdapter(mAdapter);
+ mRecyclerView.setHasFixedSize(true);
+ mLayoutManager = new WrappedLayoutManager(config.mSpanCount,
+ config.mOrientation);
+ mLayoutManager.setGapStrategy(config.mGapStrategy);
+ mLayoutManager.setReverseLayout(config.mReverseLayout);
+ mRecyclerView.setLayoutManager(mLayoutManager);
+ }
+
+ LayoutParams getLp(View view) {
+ return (LayoutParams) view.getLayoutParams();
+ }
+
+ public void testInnerGapHandling() throws Throwable {
+ innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_NONE);
+ innerGapHandlingTest(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
+ }
+
+ public void innerGapHandlingTest(int strategy) throws Throwable {
+ Config config = new Config().spanCount(3).itemCount(500);
+ setupByConfig(config);
+ mLayoutManager.setGapStrategy(strategy);
+ mAdapter.mFullSpanItems.add(100);
+ mAdapter.mFullSpanItems.add(104);
+ mAdapter.mViewsHaveEqualSize = true;
+ waitFirstLayout();
+ mLayoutManager.expectLayouts(1);
+ scrollToPosition(400);
+ mLayoutManager.waitForLayout(2);
+ mLayoutManager.expectLayouts(2);
+ mAdapter.addAndNotify(101, 1);
+ mLayoutManager.waitForLayout(2);
+ if (strategy == GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
+ mLayoutManager.expectLayouts(1);
+ }
+
+ // state
+ // now smooth scroll to 99 to trigger a layout around 100
+ smoothScrollToPosition(99);
+ switch (strategy) {
+ case GAP_HANDLING_NONE:
+ assertSpans("gap handling:" + Config.gapStrategyName(strategy), new int[]{100, 0},
+ new int[]{101, 2}, new int[]{102, 0}, new int[]{103, 1}, new int[]{104, 2},
+ new int[]{105, 0});
+
+ // should be able to detect the gap
+ View gapView = mLayoutManager.hasGapsToFix(0, mLayoutManager.getChildCount());
+ assertSame("gap should be detected", mLayoutManager.findViewByPosition(101),
+ gapView);
+ break;
+ case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
+ mLayoutManager.waitForLayout(2);
+ assertSpans("swap items between spans", new int[]{100, 0}, new int[]{101, 0},
+ new int[]{102, 1}, new int[]{103, 2}, new int[]{104, 0}, new int[]{105, 0});
+ break;
+ }
+
+ }
+
+ public void testFullSizeSpans() throws Throwable {
+ Config config = new Config().spanCount(5).itemCount(30);
+ setupByConfig(config);
+ mAdapter.mFullSpanItems.add(3);
+ waitFirstLayout();
+ assertSpans("Testing full size span", new int[]{0, 0}, new int[]{1, 1}, new int[]{2, 2},
+ new int[]{3, 0}, new int[]{4, 0}, new int[]{5, 1}, new int[]{6, 2},
+ new int[]{7, 3}, new int[]{8, 4});
+ }
+
+ void assertSpans(String msg, int[]... childSpanTuples) {
+ for (int i = 0; i < childSpanTuples.length; i++) {
+ assertSpan(msg, childSpanTuples[i][0], childSpanTuples[i][1]);
+ }
+ }
+
+ void assertSpan(String msg, int childPosition, int expectedSpan) {
+ View view = mLayoutManager.findViewByPosition(childPosition);
+ assertNotNull(msg + "view at position " + childPosition + " should exists", view);
+ assertEquals(msg + "[child:" + childPosition + "]", expectedSpan,
+ getLp(view).mSpan.mIndex);
+ }
+
+ public void testSpanReassignmentsOnItemChange() throws Throwable {
+ Config config = new Config().spanCount(5);
+ setupByConfig(config);
+ waitFirstLayout();
+ smoothScrollToPosition(mAdapter.getItemCount() / 2);
+ final int changePosition = mAdapter.getItemCount() / 4;
+ int[] prevAssignments = mLayoutManager.mLazySpanLookup.mData.clone();
+ mLayoutManager.expectLayouts(1);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mAdapter.notifyItemChanged(changePosition);
+ }
+ });
+ mLayoutManager.assertNoLayout("no layout should happen when an invisible child is updated",
+ 1);
+ // item change should not affect span assignments
+ assertSpanAssignmentEquality("item change should not affect span assignments ",
+ prevAssignments, mLayoutManager.mLazySpanLookup.mData, 0, prevAssignments.length);
+
+ // delete an item before visible area
+ int deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(0)) - 2;
+ Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
+ if (DEBUG) {
+ Log.d(TAG, "before:");
+ for (Map.Entry<Item, Rect> entry : before.entrySet()) {
+ Log.d(TAG, entry.getKey().mAdapterIndex + ":" + entry.getValue());
+ }
+ }
+ mLayoutManager.expectLayouts(1);
+ // TODO move these bounds to edge case once animation changes are in.
+ mAdapter.deleteAndNotify(deletedPosition, 1);
+ mLayoutManager.waitForLayout(2);
+ assertRectSetsEqual(config + " when an item towards the head of the list is deleted, it "
+ + "should not affect the layout if it is not visible", before,
+ mLayoutManager.collectChildCoordinates());
+ deletedPosition = mLayoutManager.getPosition(mLayoutManager.getChildAt(2));
+ mLayoutManager.expectLayouts(1);
+ mAdapter.deleteAndNotify(deletedPosition, 1);
+ mLayoutManager.waitForLayout(2);
+ assertRectSetsNotEqual(config + " when a visible item is deleted, it should affect the "
+ + "layout", before, mLayoutManager.collectChildCoordinates());
+ }
+
+ void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start, int end) {
+ for (int i = start; i < end; i++) {
+ assertEquals(msg + " ind:" + i, set1[i], set2[i]);
+ }
+ }
+
+ void assertSpanAssignmentEquality(String msg, int[] set1, int[] set2, int start1, int start2,
+ int length) {
+ for (int i = 0; i < length; i++) {
+ assertEquals(msg + " ind1:" + (start1 + i) + ", ind2:" + (start2 + i), set1[start1 + i],
+ set2[start2 + i]);
+ }
+ }
+
+ public void testViewSnapping() throws Throwable {
+ for (Config config : mBaseVariations) {
+ viewSnapTest(config.itemCount(config.mSpanCount + 1));
+ removeRecyclerView();
+ }
+ }
+
+ public void viewSnapTest(Config config) throws Throwable {
+ setupByConfig(config);
+ waitFirstLayout();
+ // run these tests twice. once initial layout, once after scroll
+ String logSuffix = "";
+ for (int i = 0; i < 2; i ++) {
+ Map<Item, Rect> itemRectMap = mLayoutManager.collectChildCoordinates();
+ Rect recyclerViewBounds = getDecoratedRecyclerViewBounds();
+ Rect usedLayoutBounds = new Rect();
+ for (Rect rect : itemRectMap.values()) {
+ usedLayoutBounds.union(rect);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "testing view snapping (" + logSuffix + ") for config " + config);
+ }
+ if (config.mOrientation == VERTICAL) {
+ assertEquals(config + " there should be no gap on left" + logSuffix,
+ usedLayoutBounds.left, recyclerViewBounds.left);
+ assertEquals(config + " there should be no gap on right" + logSuffix,
+ usedLayoutBounds.right, recyclerViewBounds.right);
+ if (config.mReverseLayout) {
+ assertEquals(config + " there should be no gap on bottom" + logSuffix,
+ usedLayoutBounds.bottom, recyclerViewBounds.bottom);
+ assertTrue(config + " there should be some gap on top" + logSuffix,
+ usedLayoutBounds.top > recyclerViewBounds.top);
+ } else {
+ assertEquals(config + " there should be no gap on top" + logSuffix,
+ usedLayoutBounds.top, recyclerViewBounds.top);
+ assertTrue(config + " there should be some gap at the bottom" + logSuffix,
+ usedLayoutBounds.bottom < recyclerViewBounds.bottom);
+ }
+ } else {
+ assertEquals(config + " there should be no gap on top" + logSuffix,
+ usedLayoutBounds.top, recyclerViewBounds.top);
+ assertEquals(config + " there should be no gap at the bottom" + logSuffix,
+ usedLayoutBounds.bottom, recyclerViewBounds.bottom);
+ if (config.mReverseLayout) {
+ assertEquals(config + " there should be no on right" + logSuffix,
+ usedLayoutBounds.right, recyclerViewBounds.right);
+ assertTrue(config + " there should be some gap on left" + logSuffix,
+ usedLayoutBounds.left > recyclerViewBounds.left);
+ } else {
+ assertEquals(config + " there should be no gap on left" + logSuffix,
+ usedLayoutBounds.left, recyclerViewBounds.left);
+ assertTrue(config + " there should be some gap on right" + logSuffix,
+ usedLayoutBounds.right < recyclerViewBounds.right);
+ }
+ }
+ final int scroll = config.mReverseLayout ? -500 : 500;
+ scrollBy(scroll);
+ logSuffix = " scrolled " + scroll;
+ }
+
+ }
+
+ public void testSpanCountChangeOnRestoreSavedState() throws Throwable {
+ Config config = new Config(HORIZONTAL, true, 5,
+ GAP_HANDLING_NONE);
+ setupByConfig(config);
+ waitFirstLayout();
+
+ int beforeChildCount = mLayoutManager.getChildCount();
+ Parcelable savedState = mRecyclerView.onSaveInstanceState();
+ // we append a suffix to the parcelable to test out of bounds
+ String parcelSuffix = UUID.randomUUID().toString();
+ Parcel parcel = Parcel.obtain();
+ savedState.writeToParcel(parcel, 0);
+ parcel.writeString(parcelSuffix);
+ removeRecyclerView();
+ // reset for reading
+ parcel.setDataPosition(0);
+ // re-create
+ savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
+ removeRecyclerView();
+
+ RecyclerView restored = new RecyclerView(getActivity());
+ mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
+ restored.setLayoutManager(mLayoutManager);
+ // use the same adapter for Rect matching
+ restored.setAdapter(mAdapter);
+ restored.onRestoreInstanceState(savedState);
+ mLayoutManager.setSpanCount(1);
+ mLayoutManager.expectLayouts(1);
+ setRecyclerView(restored);
+ mLayoutManager.waitForLayout(2);
+ assertEquals("on saved state, reverse layout should be preserved",
+ config.mReverseLayout, mLayoutManager.getReverseLayout());
+ assertEquals("on saved state, orientation should be preserved",
+ config.mOrientation, mLayoutManager.getOrientation());
+ assertEquals("after setting new span cound, layout manager should keep new value",
+ 1, mLayoutManager.getSpanCount());
+ assertEquals("on saved state, gap strategy should be preserved",
+ config.mGapStrategy, mLayoutManager.getGapStrategy());
+ assertTrue("when span count is dramatically changed after restore, # of child views "
+ + "should change", beforeChildCount > mLayoutManager.getChildCount());
+ // make sure LLM can layout all children. is some span info is leaked, this would crash
+ smoothScrollToPosition(mAdapter.getItemCount() - 1);
+ }
+
+ public void testSavedState() throws Throwable {
+ PostLayoutRunnable[] postLayoutOptions = new PostLayoutRunnable[]{
+ new PostLayoutRunnable() {
+ @Override
+ public void run() throws Throwable {
+ // do nothing
+ }
+
+ @Override
+ public String describe() {
+ return "doing nothing";
+ }
+ },
+ new PostLayoutRunnable() {
+ @Override
+ public void run() throws Throwable {
+ mLayoutManager.expectLayouts(1);
+ scrollToPosition(mAdapter.getItemCount() * 3 / 4);
+ mLayoutManager.waitForLayout(2);
+ }
+
+ @Override
+ public String describe() {
+ return "scroll to position";
+ }
+ },
+ new PostLayoutRunnable() {
+ @Override
+ public void run() throws Throwable {
+ mLayoutManager.expectLayouts(1);
+ scrollToPositionWithOffset(mAdapter.getItemCount() * 1 / 3,
+ 50);
+ mLayoutManager.waitForLayout(2);
+ }
+
+ @Override
+ public String describe() {
+ return "scroll to position with positive offset";
+ }
+ },
+ new PostLayoutRunnable() {
+ @Override
+ public void run() throws Throwable {
+ mLayoutManager.expectLayouts(1);
+ scrollToPositionWithOffset(mAdapter.getItemCount() * 2 / 3,
+ -50);
+ mLayoutManager.waitForLayout(2);
+ }
+
+ @Override
+ public String describe() {
+ return "scroll to position with negative offset";
+ }
+ }
+ };
+ boolean[] waitForLayoutOptions = new boolean[]{false, true};
+ for (Config config : mBaseVariations) {
+ for (PostLayoutRunnable runnable : postLayoutOptions) {
+ for (boolean waitForLayout : waitForLayoutOptions) {
+ savedStateTest(config, waitForLayout, runnable);
+ removeRecyclerView();
+ }
+ }
+ }
+ }
+
+ public void savedStateTest(Config config, boolean waitForLayout,
+ PostLayoutRunnable postLayoutOperations)
+ throws Throwable {
+ if (DEBUG) {
+ Log.d(TAG, "testing saved state with wait for layout = " + waitForLayout + " config "
+ + config + " post layout action " + postLayoutOperations.describe());
+ }
+ setupByConfig(config);
+ if (waitForLayout) {
+ waitFirstLayout();
+ postLayoutOperations.run();
+ }
+ Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
+ Parcelable savedState = mRecyclerView.onSaveInstanceState();
+ // we append a suffix to the parcelable to test out of bounds
+ String parcelSuffix = UUID.randomUUID().toString();
+ Parcel parcel = Parcel.obtain();
+ savedState.writeToParcel(parcel, 0);
+ parcel.writeString(parcelSuffix);
+ removeRecyclerView();
+ // reset for reading
+ parcel.setDataPosition(0);
+ // re-create
+ savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
+ removeRecyclerView();
+
+ RecyclerView restored = new RecyclerView(getActivity());
+ mLayoutManager = new WrappedLayoutManager(config.mSpanCount, config.mOrientation);
+ restored.setLayoutManager(mLayoutManager);
+ // use the same adapter for Rect matching
+ restored.setAdapter(mAdapter);
+ restored.onRestoreInstanceState(savedState);
+ assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
+ parcel.readString());
+ mLayoutManager.expectLayouts(1);
+ setRecyclerView(restored);
+ mLayoutManager.waitForLayout(2);
+ assertEquals(config + " on saved state, reverse layout should be preserved",
+ config.mReverseLayout, mLayoutManager.getReverseLayout());
+ assertEquals(config + " on saved state, orientation should be preserved",
+ config.mOrientation, mLayoutManager.getOrientation());
+ assertEquals(config + " on saved state, span count should be preserved",
+ config.mSpanCount, mLayoutManager.getSpanCount());
+ assertEquals(config + " on saved state, gap strategy should be preserved",
+ config.mGapStrategy, mLayoutManager.getGapStrategy());
+ if (waitForLayout) {
+ assertRectSetsEqual(config + "\npost layout op:" + postLayoutOperations.describe()
+ + ": on restore, previous view positions should be preserved",
+ before, mLayoutManager.collectChildCoordinates()
+ );
+ }
+ // TODO add tests for changing values after restore before layout
+ }
+
+ public void testScrollToPositionWithOffset() throws Throwable {
+ for (Config config : mBaseVariations) {
+ scrollToPositionWithOffsetTest(config);
+ removeRecyclerView();
+ }
+ }
+
+ public void scrollToPositionWithOffsetTest(Config config) throws Throwable {
+ setupByConfig(config);
+ waitFirstLayout();
+ OrientationHelper orientationHelper = OrientationHelper
+ .createOrientationHelper(mLayoutManager, config.mOrientation);
+ Rect layoutBounds = getDecoratedRecyclerViewBounds();
+ // try scrolling towards head, should not affect anything
+ Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
+ scrollToPositionWithOffset(0, 20);
+ assertRectSetsEqual(config + " trying to over scroll with offset should be no-op",
+ before, mLayoutManager.collectChildCoordinates());
+ // try offsetting some visible children
+ int testCount = 10;
+ while (testCount-- > 0) {
+ // get middle child
+ final View child = mLayoutManager.getChildAt(mLayoutManager.getChildCount() / 2);
+ final int position = mRecyclerView.getChildPosition(child);
+ final int startOffset = config.mReverseLayout ?
+ orientationHelper.getEndAfterPadding() - orientationHelper
+ .getDecoratedEnd(child)
+ : orientationHelper.getDecoratedStart(child) - orientationHelper
+ .getStartAfterPadding();
+ final int scrollOffset = startOffset / 2;
+ mLayoutManager.expectLayouts(1);
+ scrollToPositionWithOffset(position, scrollOffset);
+ mLayoutManager.waitForLayout(2);
+ final int finalOffset = config.mReverseLayout ?
+ orientationHelper.getEndAfterPadding() - orientationHelper
+ .getDecoratedEnd(child)
+ : orientationHelper.getDecoratedStart(child) - orientationHelper
+ .getStartAfterPadding();
+ assertEquals(config + " scroll with offset on a visible child should work fine",
+ scrollOffset, finalOffset);
+ }
+
+ // try scrolling to invisible children
+ testCount = 10;
+ // we test above and below, one by one
+ int offsetMultiplier = -1;
+ while (testCount-- > 0) {
+ final TargetTuple target = findInvisibleTarget(config);
+ mLayoutManager.expectLayouts(1);
+ final int offset = offsetMultiplier
+ * orientationHelper.getDecoratedMeasurement(mLayoutManager.getChildAt(0)) / 3;
+ scrollToPositionWithOffset(target.mPosition, offset);
+ mLayoutManager.waitForLayout(2);
+ final View child = mLayoutManager.findViewByPosition(target.mPosition);
+ assertNotNull(config + " scrolling to a mPosition with offset " + offset
+ + " should layout it", child);
+ final Rect bounds = mLayoutManager.getViewBounds(child);
+ if (DEBUG) {
+ Log.d(TAG, config + " post scroll to invisible mPosition " + bounds + " in "
+ + layoutBounds + " with offset " + offset);
+ }
+
+ if (config.mReverseLayout) {
+ assertEquals(config + " when scrolling with offset to an invisible in reverse "
+ + "layout, its end should align with recycler view's end - offset",
+ orientationHelper.getEndAfterPadding() - offset,
+ orientationHelper.getDecoratedEnd(child)
+ );
+ } else {
+ assertEquals(config + " when scrolling with offset to an invisible child in normal"
+ + " layout its start should align with recycler view's start + "
+ + "offset",
+ orientationHelper.getStartAfterPadding() + offset,
+ orientationHelper.getDecoratedStart(child)
+ );
+ }
+ offsetMultiplier *= -1;
+ }
+ }
+
+ public void testScrollToPosition() throws Throwable {
+ for (Config config : mBaseVariations) {
+ scrollToPositionTest(config);
+ removeRecyclerView();
+ }
+ }
+
+ public Rect getDecoratedRecyclerViewBounds() {
+ return new Rect(
+ mRecyclerView.getPaddingLeft(),
+ mRecyclerView.getPaddingTop(),
+ mRecyclerView.getPaddingLeft() + mRecyclerView.getWidth(),
+ mRecyclerView.getPaddingTop() + mRecyclerView.getHeight()
+ );
+ }
+
+ private TargetTuple findInvisibleTarget(Config config) {
+ int minPosition = Integer.MAX_VALUE, maxPosition = Integer.MIN_VALUE;
+ for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
+ View child = mLayoutManager.getChildAt(i);
+ int position = mRecyclerView.getChildPosition(child);
+ if (position < minPosition) {
+ minPosition = position;
+ }
+ if (position > maxPosition) {
+ maxPosition = position;
+ }
+ }
+ final int tailTarget = maxPosition + (mAdapter.getItemCount() - maxPosition) / 2;
+ final int headTarget = minPosition / 2;
+ final int target;
+ // where will the child come from ?
+ final int itemLayoutDirection;
+ if (Math.abs(tailTarget - maxPosition) > Math.abs(headTarget - minPosition)) {
+ target = tailTarget;
+ itemLayoutDirection = config.mReverseLayout ? LAYOUT_START : LAYOUT_END;
+ } else {
+ target = headTarget;
+ itemLayoutDirection = config.mReverseLayout ? LAYOUT_END : LAYOUT_START;
+ }
+ if (DEBUG) {
+ Log.d(TAG,
+ config + " target:" + target + " min:" + minPosition + ", max:" + maxPosition);
+ }
+ return new TargetTuple(target, itemLayoutDirection);
+ }
+
+ public void scrollToPositionTest(Config config) throws Throwable {
+ setupByConfig(config);
+ waitFirstLayout();
+ OrientationHelper orientationHelper = OrientationHelper
+ .createOrientationHelper(mLayoutManager, config.mOrientation);
+ Rect layoutBounds = getDecoratedRecyclerViewBounds();
+ for (int i = 0; i < mLayoutManager.getChildCount(); i++) {
+ View view = mLayoutManager.getChildAt(i);
+ Rect bounds = mLayoutManager.getViewBounds(view);
+ if (layoutBounds.contains(bounds)) {
+ Map<Item, Rect> initialBounds = mLayoutManager.collectChildCoordinates();
+ final int position = mRecyclerView.getChildPosition(view);
+ LayoutParams layoutParams
+ = (LayoutParams) (view.getLayoutParams());
+ TestViewHolder vh = (TestViewHolder) layoutParams.mViewHolder;
+ assertEquals("recycler view mPosition should match adapter mPosition", position,
+ vh.mBindedItem.mAdapterIndex);
+ if (DEBUG) {
+ Log.d(TAG, "testing scroll to visible mPosition at " + position
+ + " " + bounds + " inside " + layoutBounds);
+ }
+ mLayoutManager.expectLayouts(1);
+ scrollToPosition(position);
+ mLayoutManager.waitForLayout(2);
+ if (DEBUG) {
+ view = mLayoutManager.findViewByPosition(position);
+ Rect newBounds = mLayoutManager.getViewBounds(view);
+ Log.d(TAG, "after scrolling to visible mPosition " +
+ bounds + " equals " + newBounds);
+ }
+
+ assertRectSetsEqual(
+ config + "scroll to mPosition on fully visible child should be no-op",
+ initialBounds, mLayoutManager.collectChildCoordinates());
+ } else {
+ final int position = mRecyclerView.getChildPosition(view);
+ if (DEBUG) {
+ Log.d(TAG,
+ "child(" + position + ") not fully visible " + bounds + " not inside "
+ + layoutBounds
+ + mRecyclerView.getChildPosition(view)
+ );
+ }
+ mLayoutManager.expectLayouts(1);
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mLayoutManager.scrollToPosition(position);
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+ view = mLayoutManager.findViewByPosition(position);
+ bounds = mLayoutManager.getViewBounds(view);
+ if (DEBUG) {
+ Log.d(TAG, "after scroll to partially visible child " + bounds + " in "
+ + layoutBounds);
+ }
+ assertTrue(config
+ + " after scrolling to a partially visible child, it should become fully "
+ + " visible. " + bounds + " not inside " + layoutBounds,
+ layoutBounds.contains(bounds)
+ );
+ assertTrue(config + " when scrolling to a partially visible item, one of its edges "
+ + "should be on the boundaries", orientationHelper.getStartAfterPadding() ==
+ orientationHelper.getDecoratedStart(view)
+ || orientationHelper.getEndAfterPadding() ==
+ orientationHelper.getDecoratedEnd(view));
+ }
+ }
+
+ // try scrolling to invisible children
+ int testCount = 10;
+ while (testCount-- > 0) {
+ final TargetTuple target = findInvisibleTarget(config);
+ mLayoutManager.expectLayouts(1);
+ scrollToPosition(target.mPosition);
+ mLayoutManager.waitForLayout(2);
+ final View child = mLayoutManager.findViewByPosition(target.mPosition);
+ assertNotNull(config + " scrolling to a mPosition should lay it out", child);
+ final Rect bounds = mLayoutManager.getViewBounds(child);
+ if (DEBUG) {
+ Log.d(TAG, config + " post scroll to invisible mPosition " + bounds + " in "
+ + layoutBounds);
+ }
+ assertTrue(config + " scrolling to a mPosition should make it fully visible",
+ layoutBounds.contains(bounds));
+ if (target.mLayoutDirection == LAYOUT_START) {
+ assertEquals(
+ config + " when scrolling to an invisible child above, its start should"
+ + " align with recycler view's start",
+ orientationHelper.getStartAfterPadding(),
+ orientationHelper.getDecoratedStart(child)
+ );
+ } else {
+ assertEquals(config + " when scrolling to an invisible child below, its end "
+ + "should align with recycler view's end",
+ orientationHelper.getEndAfterPadding(),
+ orientationHelper.getDecoratedEnd(child)
+ );
+ }
+ }
+ }
+
+ private void scrollToPositionWithOffset(final int position, final int offset) throws Throwable {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mLayoutManager.scrollToPositionWithOffset(position, offset);
+ }
+ });
+ }
+
+ public void testLayoutOrder() throws Throwable {
+ for (Config config : mBaseVariations) {
+ layoutOrderTest(config);
+ removeRecyclerView();
+ }
+ }
+
+ public void layoutOrderTest(Config config) throws Throwable {
+ setupByConfig(config);
+ assertViewPositions(config);
+ }
+
+ void assertViewPositions(Config config) {
+ ArrayList<ArrayList<View>> viewsBySpan = mLayoutManager.collectChildrenBySpan();
+ OrientationHelper orientationHelper = OrientationHelper
+ .createOrientationHelper(mLayoutManager, config.mOrientation);
+ for (ArrayList<View> span : viewsBySpan) {
+ // validate all children's order. first child should have min start mPosition
+ final int count = span.size();
+ for (int i = 0, j = 1; j < count; i++, j++) {
+ View prev = span.get(i);
+ View next = span.get(j);
+ assertTrue(config + " prev item should be above next item",
+ orientationHelper.getDecoratedEnd(prev) <= orientationHelper
+ .getDecoratedStart(next)
+ );
+
+ }
+ }
+ }
+
+ public void testScrollBy() throws Throwable {
+ for (Config config : mBaseVariations) {
+ scrollByTest(config);
+ removeRecyclerView();
+ }
+ }
+
+ void waitFirstLayout() throws Throwable {
+ mLayoutManager.expectLayouts(1);
+ setRecyclerView(mRecyclerView);
+ mLayoutManager.waitForLayout(2);
+ }
+
+ public void scrollByTest(Config config) throws Throwable {
+ setupByConfig(config);
+ waitFirstLayout();
+ // try invalid scroll. should not happen
+ final View first = mLayoutManager.getChildAt(0);
+ OrientationHelper primaryOrientation = OrientationHelper
+ .createOrientationHelper(mLayoutManager, config.mOrientation);
+ int scrollDist;
+ if (config.mReverseLayout) {
+ scrollDist = primaryOrientation.getDecoratedMeasurement(first) / 2;
+ } else {
+ scrollDist = -primaryOrientation.getDecoratedMeasurement(first) / 2;
+ }
+ Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
+ scrollBy(scrollDist);
+ Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
+ assertRectSetsEqual(
+ config + " if there are no more items, scroll should not happen (dt:" + scrollDist
+ + ")",
+ before, after
+ );
+
+ scrollDist = -scrollDist * 3;
+ before = mLayoutManager.collectChildCoordinates();
+ scrollBy(scrollDist);
+ after = mLayoutManager.collectChildCoordinates();
+ int layoutStart = primaryOrientation.getStartAfterPadding();
+ int layoutEnd = primaryOrientation.getEndAfterPadding();
+ for (Map.Entry<Item, Rect> entry : before.entrySet()) {
+ Rect afterRect = after.get(entry.getKey());
+ // offset rect
+ if (config.mOrientation == VERTICAL) {
+ entry.getValue().offset(0, -scrollDist);
+ } else {
+ entry.getValue().offset(-scrollDist, 0);
+ }
+ if (afterRect == null || afterRect.isEmpty()) {
+ // assert item is out of bounds
+ int start, end;
+ if (config.mOrientation == VERTICAL) {
+ start = entry.getValue().top;
+ end = entry.getValue().bottom;
+ } else {
+ start = entry.getValue().left;
+ end = entry.getValue().right;
+ }
+ assertTrue(
+ config + " if item is missing after relayout, it should be out of bounds."
+ + "item start: " + start + ", end:" + end + " layout start:"
+ + layoutStart +
+ ", layout end:" + layoutEnd,
+ start <= layoutStart && end <= layoutEnd ||
+ start >= layoutEnd && end >= layoutEnd
+ );
+ } else {
+ assertEquals(config + " Item should be laid out at the scroll offset coordinates",
+ entry.getValue(),
+ afterRect);
+ }
+ }
+ assertViewPositions(config);
+ }
+
+ public void testConsistentRelayout() throws Throwable {
+ for (Config config : mBaseVariations) {
+ for (boolean firstChildMultiSpan : new boolean[]{false, true}) {
+ consistentRelayoutTest(config, firstChildMultiSpan);
+ }
+ removeRecyclerView();
+ }
+ }
+
+ public void consistentRelayoutTest(Config config, boolean firstChildMultiSpan)
+ throws Throwable {
+ setupByConfig(config);
+ if (firstChildMultiSpan) {
+ mAdapter.mFullSpanItems.add(0);
+ }
+ waitFirstLayout();
+ // record all child positions
+ Map<Item, Rect> before = mLayoutManager.collectChildCoordinates();
+ requestLayoutOnUIThread(mRecyclerView);
+ Map<Item, Rect> after = mLayoutManager.collectChildCoordinates();
+ assertRectSetsEqual(
+ config + " simple re-layout, firstChildMultiSpan:" + firstChildMultiSpan, before,
+ after);
+ // scroll some to create inconsistency
+ View firstChild = mLayoutManager.getChildAt(0);
+ final int firstChildStartBeforeScroll = mLayoutManager.mPrimaryOrientation
+ .getDecoratedStart(firstChild);
+ int distance = mLayoutManager.mPrimaryOrientation.getDecoratedMeasurement(firstChild) / 2;
+ if (config.mReverseLayout) {
+ distance *= -1;
+ }
+ scrollBy(distance);
+ waitForMainThread(2);
+ assertTrue("scroll by should move children", firstChildStartBeforeScroll !=
+ mLayoutManager.mPrimaryOrientation.getDecoratedStart(firstChild));
+ before = mLayoutManager.collectChildCoordinates();
+ mLayoutManager.expectLayouts(1);
+ requestLayoutOnUIThread(mRecyclerView);
+ mLayoutManager.waitForLayout(2);
+ after = mLayoutManager.collectChildCoordinates();
+ assertRectSetsEqual(config + " simple re-layout after scroll", before, after);
+ }
+
+ /**
+ * enqueues an empty runnable to main thread so that we can be assured it did run
+ *
+ * @param count Number of times to run
+ */
+ private void waitForMainThread(int count) throws Throwable {
+ final AtomicInteger i = new AtomicInteger(count);
+ while (i.get() > 0) {
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ i.decrementAndGet();
+ }
+ });
+ }
+ }
+
+ public void assertRectSetsNotEqual(String message, Map<Item, Rect> before,
+ Map<Item, Rect> after) {
+ Throwable throwable = null;
+ try {
+ assertRectSetsEqual("NOT " + message, before, after);
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertNotNull(message + " two layout should be different", throwable);
+ }
+
+ public void assertRectSetsEqual(String message, Map<Item, Rect> before, Map<Item, Rect> after) {
+ StringBuilder log = new StringBuilder();
+ if (DEBUG) {
+ log.append("checking rectangle equality.\n");
+ log.append("before:");
+ for (Map.Entry<Item, Rect> entry : before.entrySet()) {
+ log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
+ .append(entry.getValue());
+ }
+ log.append("\nafter:");
+ for (Map.Entry<Item, Rect> entry : after.entrySet()) {
+ log.append("\n").append(entry.getKey().mAdapterIndex).append(":")
+ .append(entry.getValue());
+ }
+ message += "\n\n" + log.toString();
+ }
+ assertEquals(message + ": item counts should be equal", before.size()
+ , after.size());
+ for (Map.Entry<Item, Rect> entry : before.entrySet()) {
+ Rect afterRect = after.get(entry.getKey());
+ assertNotNull(message + ": Same item should be visible after simple re-layout",
+ afterRect);
+ assertEquals(message + ": Item should be laid out at the same coordinates",
+ entry.getValue(),
+ afterRect);
+ }
+ }
+
+ // test layout params assignment
+
+ class WrappedLayoutManager extends StaggeredGridLayoutManager {
+
+ CountDownLatch layoutLatch;
+
+ public void expectLayouts(int count) {
+ layoutLatch = new CountDownLatch(count);
+ }
+
+ public void waitForLayout(long timeout) throws InterruptedException {
+ waitForLayout(timeout * (DEBUG ? 1000 : 1), TimeUnit.SECONDS);
+ }
+
+ public void waitForLayout(long timeout, TimeUnit timeUnit) throws InterruptedException {
+ layoutLatch.await(timeout, timeUnit);
+ assertEquals("all expected layouts should be executed at the expected time",
+ 0, layoutLatch.getCount());
+ }
+
+ public void assertNoLayout(String msg, long timeout) throws Throwable {
+ layoutLatch.await(timeout, TimeUnit.SECONDS);
+ assertFalse(msg, layoutLatch.getCount() == 0);
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
+ super.onLayoutChildren(recycler, state);
+ layoutLatch.countDown();
+ }
+
+ public WrappedLayoutManager(int spanCount, int orientation) {
+ super(spanCount, orientation);
+ }
+
+ ArrayList<ArrayList<View>> collectChildrenBySpan() {
+ ArrayList<ArrayList<View>> viewsBySpan = new ArrayList<ArrayList<View>>();
+ for (int i = 0; i < getSpanCount(); i++) {
+ viewsBySpan.add(new ArrayList<View>());
+ }
+ for (int i = 0; i < getChildCount(); i++) {
+ View view = getChildAt(i);
+ LayoutParams lp
+ = (LayoutParams) view
+ .getLayoutParams();
+ viewsBySpan.get(lp.mSpan.mIndex).add(view);
+ }
+ return viewsBySpan;
+ }
+
+ Rect getViewBounds(View view) {
+ if (getOrientation() == HORIZONTAL) {
+ return new Rect(
+ mPrimaryOrientation.getDecoratedStart(view),
+ mSecondaryOrientation.getDecoratedStart(view),
+ mPrimaryOrientation.getDecoratedEnd(view),
+ mSecondaryOrientation.getDecoratedEnd(view));
+ } else {
+ return new Rect(
+ mSecondaryOrientation.getDecoratedStart(view),
+ mPrimaryOrientation.getDecoratedStart(view),
+ mSecondaryOrientation.getDecoratedEnd(view),
+ mPrimaryOrientation.getDecoratedEnd(view));
+ }
+
+ }
+
+ Map<Item, Rect> collectChildCoordinates() throws Throwable {
+ final Map<Item, Rect> items = new LinkedHashMap<Item, Rect>();
+ runTestOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ // do it if and only if child is visible
+ if (child.getRight() < 0 || child.getBottom() < 0 ||
+ child.getLeft() >= getWidth() || child.getTop() >= getHeight()) {
+ // invisible children may be drawn in cases like scrolling so we should
+ // ignore them
+ continue;
+ }
+ LayoutParams lp = (LayoutParams) child
+ .getLayoutParams();
+ TestViewHolder vh = (TestViewHolder) lp.mViewHolder;
+ items.put(vh.mBindedItem, getViewBounds(child));
+ }
+ }
+ });
+ return items;
+ }
+ }
+
+ class GridTestAdapter extends TestAdapter {
+
+ int mOrientation;
+
+ // original ids of items that should be full span
+ HashSet<Integer> mFullSpanItems = new HashSet<Integer>();
+
+ private boolean mViewsHaveEqualSize = false; // size in the scrollable direction
+
+ GridTestAdapter(int count, int orientation) {
+ super(count);
+ mOrientation = orientation;
+ }
+
+ @Override
+ public void offsetOriginalIndices(int start, int offset) {
+ if (mFullSpanItems.size() > 0) {
+ HashSet<Integer> old = mFullSpanItems;
+ mFullSpanItems = new HashSet<Integer>();
+ for (Integer i : old) {
+ if (i < start) {
+ mFullSpanItems.add(i);
+ } else if (offset > 0 || (start + Math.abs(offset)) <= i) {
+ mFullSpanItems.add(i + offset);
+ } else if (DEBUG) {
+ Log.d(TAG, "removed full span item " + i);
+ }
+ }
+ }
+ super.offsetOriginalIndices(start, offset);
+ }
+
+ @Override
+ public void onBindViewHolder(TestViewHolder holder,
+ int position) {
+ super.onBindViewHolder(holder, position);
+ Item item = mItems.get(position);
+ final int minSize = mViewsHaveEqualSize ? 200 : 200 + 20 * (position % 10);
+ if (mOrientation == OrientationHelper.HORIZONTAL) {
+ holder.itemView.setMinimumWidth(minSize);
+ } else {
+ holder.itemView.setMinimumHeight(minSize);
+ }
+ RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) holder.itemView
+ .getLayoutParams();
+ if (lp instanceof LayoutParams) {
+ ((LayoutParams) lp).setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
+ } else {
+ LayoutParams slp = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ holder.itemView.setLayoutParams(slp);
+ slp.setFullSpan(mFullSpanItems.contains(item.mAdapterIndex));
+ lp = slp;
+ }
+ lp.topMargin = 3;
+ lp.leftMargin = 5;
+ lp.rightMargin = 7;
+ lp.bottomMargin = 9;
+ }
+ }
+
+ static class Config {
+
+ private static final int DEFAULT_ITEM_COUNT = 300;
+
+ int mOrientation = OrientationHelper.VERTICAL;
+
+ boolean mReverseLayout = false;
+
+ int mSpanCount = 3;
+
+ int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
+
+ int mItemCount = DEFAULT_ITEM_COUNT;
+
+ OrientationHelper mOrientationHelper;
+
+ Config(int orientation, boolean reverseLayout, int spanCount, int gapStrategy) {
+ mOrientation = orientation;
+ mReverseLayout = reverseLayout;
+ mSpanCount = spanCount;
+ mGapStrategy = gapStrategy;
+ }
+
+ public Config() {
+
+ }
+
+ Config orientation(int orientation) {
+ mOrientation = orientation;
+ return this;
+ }
+
+ Config reverseLayout(boolean reverseLayout) {
+ mReverseLayout = reverseLayout;
+ return this;
+ }
+
+ Config spanCount(int spanCount) {
+ mSpanCount = spanCount;
+ return this;
+ }
+
+ Config gapStrategy(int gapStrategy) {
+ mGapStrategy = gapStrategy;
+ return this;
+ }
+
+ public Config itemCount(int itemCount) {
+ mItemCount = itemCount;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "[CONFIG:" +
+ " span:" + mSpanCount + "," +
+ " orientation:" + (mOrientation == HORIZONTAL ? "horz," : "vert,") +
+ " reverse:" + (mReverseLayout ? "T" : "F") +
+ " gap strategy: " + gapStrategyName(mGapStrategy);
+ }
+
+ private static String gapStrategyName(int gapStrategy) {
+ switch (gapStrategy) {
+ case GAP_HANDLING_NONE:
+ return "none";
+ case GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS:
+ return "move spans";
+ case GAP_HANDLING_LAZY:
+ return "lazy";
+ }
+ return "gap strategy: unknown";
+ }
+ }
+
+ private static class TargetTuple {
+
+ final int mPosition;
+
+ final int mLayoutDirection;
+
+ TargetTuple(int position, int layoutDirection) {
+ this.mPosition = position;
+ this.mLayoutDirection = layoutDirection;
+ }
+ }
+
+ private interface PostLayoutRunnable {
+
+ void run() throws Throwable;
+
+ String describe();
+ }
+
+}
diff --git a/v8/renderscript/java/src/android/support/v8/renderscript/ElementThunker.java b/v8/renderscript/java/src/android/support/v8/renderscript/ElementThunker.java
index fc986b3..9b820e2 100644
--- a/v8/renderscript/java/src/android/support/v8/renderscript/ElementThunker.java
+++ b/v8/renderscript/java/src/android/support/v8/renderscript/ElementThunker.java
@@ -57,10 +57,6 @@
return android.renderscript.Element.DataKind.PIXEL_RGB;
case PIXEL_RGBA:
return android.renderscript.Element.DataKind.PIXEL_RGBA;
- case PIXEL_DEPTH:
- return android.renderscript.Element.DataKind.PIXEL_DEPTH;
- case PIXEL_YUV:
- return android.renderscript.Element.DataKind.PIXEL_YUV;
}
return null;
}