am f9fd9749: am 26d0f7a7: Merge "Remove unused LOCAL_LDLIBS."
* commit 'f9fd97499795cd47473f0344e00db9c9837eea36':
Remove unused LOCAL_LDLIBS.
diff --git a/CleanSpec.mk b/CleanSpec.mk
index ea4358f..317bb03 100644
--- a/CleanSpec.mk
+++ b/CleanSpec.mk
@@ -48,6 +48,8 @@
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
+$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/android-support-v*)
# ************************************************
# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
diff --git a/annotations/src/android/support/annotation/IntDef.java b/annotations/src/android/support/annotation/IntDef.java
index 3232ff2..ce49b6e 100644
--- a/annotations/src/android/support/annotation/IntDef.java
+++ b/annotations/src/android/support/annotation/IntDef.java
@@ -29,7 +29,7 @@
/**
* Denotes that the annotated element of integer type, represents
* a logical type and that its value should be one of the explicitly
- * named constants. If the {@link #flag()} attribute is set to true,
+ * named constants. If the IntDef#flag() attribute is set to true,
* multiple constants can be combined.
* <p>
* Example:
diff --git a/build.gradle b/build.gradle
index a95df22..ae2f294 100644
--- a/build.gradle
+++ b/build.gradle
@@ -5,12 +5,12 @@
maven { url '../../prebuilts/tools/common/m2/internal' }
}
dependencies {
- classpath 'com.android.tools.build:gradle:0.9.+'
+ classpath 'com.android.tools.build:gradle:0.10.0'
}
}
-ext.supportVersion = '19.1.0'
-ext.extraVersion = 5
+ext.supportVersion = '19.2.0'
+ext.extraVersion = 6
ext.supportRepoOut = ''
/*
diff --git a/settings.gradle b/settings.gradle
index 83498d3..2104257 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,6 @@
+include ':support-annotations'
+project(':support-annotations').projectDir = new File(rootDir, 'annotations')
+
include ':support-v4'
project(':support-v4').projectDir = new File(rootDir, 'v4')
@@ -10,8 +13,8 @@
include ':support-mediarouter-v7'
project(':support-mediarouter-v7').projectDir = new File(rootDir, 'v7/mediarouter')
+include ':support-recyclerview-v7'
+project(':support-recyclerview-v7').projectDir = new File(rootDir, 'v7/recyclerview')
+
include ':support-v13'
project(':support-v13').projectDir = new File(rootDir, 'v13')
-
-include ':support-annotations'
-project(':support-annotations').projectDir = new File(rootDir, 'annotations')
diff --git a/v17/leanback/.classpath b/v17/leanback/.classpath
new file mode 100644
index 0000000..a4763d1
--- /dev/null
+++ b/v17/leanback/.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/v17/leanback/.gitignore b/v17/leanback/.gitignore
new file mode 100644
index 0000000..c3c25e0
--- /dev/null
+++ b/v17/leanback/.gitignore
@@ -0,0 +1,4 @@
+.settings
+bin
+libs
+gen
diff --git a/v17/leanback/.project b/v17/leanback/.project
new file mode 100644
index 0000000..9d191cd
--- /dev/null
+++ b/v17/leanback/.project
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>android-support-v17-leanback</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/Android.mk b/v17/leanback/Android.mk
new file mode 100644
index 0000000..01a8ae3
--- /dev/null
+++ b/v17/leanback/Android.mk
@@ -0,0 +1,144 @@
+# 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-v17-leanback-res
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, dummy)
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_AAPT_FLAGS := \
+ --auto-add-overlay
+LOCAL_JAR_EXCLUDE_FILES := none
+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
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# -----------------------------------------------------------------------
+
+# 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_SRC_FILES := $(call all-java-files-under, jbmr2)
+LOCAL_JAVA_LIBRARIES := android-support-v17-leanback-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-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_JAVA_LIBRARIES := \
+ android-support-v4 \
+ android-support-v7-recyclerview \
+ android-support-v17-leanback-res
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+
+# ===========================================================
+# Common Droiddoc vars
+leanback.docs.src_files := \
+ $(call all-java-files-under, src) \
+ $(call all-html-files-under, src)
+leanback.docs.java_libraries := \
+ android-support-v4 \
+ android-support-v7-recyclerview \
+ android-support-v17-leanback-res \
+ android-support-v17-leanback
+
+# Documentation
+# ===========================================================
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := android-support-v17-leanback
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+LOCAL_MODULE_TAGS := optional
+
+intermediates.COMMON := $(call intermediates-dir-for,$(LOCAL_MODULE_CLASS),android-support-v17-leanback,,COMMON)
+
+LOCAL_SRC_FILES := $(leanback.docs.src_files)
+LOCAL_ADDITONAL_JAVA_DIR := $(intermediates.COMMON)/src
+
+LOCAL_SDK_VERSION := 19
+LOCAL_IS_HOST_MODULE := false
+LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR := build/tools/droiddoc/templates-sdk
+
+LOCAL_JAVA_LIBRARIES := $(leanback.docs.java_libraries)
+
+LOCAL_DROIDDOC_OPTIONS := \
+ -offlinemode \
+ -hdf android.whichdoc offline \
+ -federate Android http://developer.android.com \
+ -federationapi Android prebuilts/sdk/api/17.txt \
+ -hide 113
+
+include $(BUILD_DROIDDOC)
+
+# Stub source files
+# ===========================================================
+
+leanback_internal_api_file := $(TARGET_OUT_COMMON_INTERMEDIATES)/PACKAGING/android-support-v17-leanback_api.txt
+leanback.docs.stubpackages := android.support.v17.leanback:android.support.v17.leanback.app:android.support.v17.leanback.database:android.support.v17.leanback.widget
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := android-support-v17-leanback-stubs
+LOCAL_MODULE_CLASS := JAVA_LIBRARIES
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_SRC_FILES := $(leanback.docs.src_files)
+LOCAL_JAVA_LIBRARIES := $(leanback.docs.java_libraries)
+
+LOCAL_DROIDDOC_CUSTOM_TEMPLATE_DIR := build/tools/droiddoc/templates-sdk
+LOCAL_UNINSTALLABLE_MODULE := true
+
+LOCAL_DROIDDOC_OPTIONS := \
+ -stubs $(TARGET_OUT_COMMON_INTERMEDIATES)/JAVA_LIBRARIES/android-support-v17-leanback-stubs_intermediates/src \
+ -stubpackages $(leanback.docs.stubpackages) \
+ -api $(leanback_internal_api_file) \
+ -hide 113 \
+ -nodocs
+
+include $(BUILD_DROIDDOC)
+leanback_stubs_stamp := $(full_target)
+$(leanback_internal_api_file) : $(full_target)
+
+# Cleanup temp vars
+# ===========================================================
+leanback.docs.src_files :=
+leanback.docs.java_libraries :=
+intermediates.COMMON :=
+leanback_internal_api_file :=
+leanback_stubs_stamp :=
+leanback.docs.stubpackages :=
diff --git a/v17/leanback/AndroidManifest.xml b/v17/leanback/AndroidManifest.xml
new file mode 100644
index 0000000..20ef094
--- /dev/null
+++ b/v17/leanback/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.v17.leanback">
+ <uses-sdk android:minSdkVersion="17"/>
+ <application />
+</manifest>
diff --git a/v17/leanback/README.txt b/v17/leanback/README.txt
new file mode 100644
index 0000000..f3dbe92
--- /dev/null
+++ b/v17/leanback/README.txt
@@ -0,0 +1 @@
+Library Project including Leanback framework support.
diff --git a/v17/leanback/build.gradle b/v17/leanback/build.gradle
new file mode 100644
index 0000000..89d5f3b
--- /dev/null
+++ b/v17/leanback/build.gradle
@@ -0,0 +1,28 @@
+apply plugin: 'android-library'
+
+archivesBaseName = 'support-leanback-v17'
+
+dependencies {
+ compile project(':support-v4')
+ compile project(':support-recyclerview-v7')
+}
+
+android {
+ compileSdkVersion 'current'
+ buildToolsVersion "19.0.1"
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ main.java.srcDirs = [
+ 'src',
+ 'kitkat',
+ 'jbmr2'
+ ]
+ main.res.srcDir 'res'
+ }
+
+ lintOptions {
+ // TODO: fix errors and reenable.
+ abortOnError false
+ }
+}
\ No newline at end of file
diff --git a/v17/leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.java b/v17/leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.java
new file mode 100644
index 0000000..ad53425
--- /dev/null
+++ b/v17/leanback/jbmr2/android/support/v17/leanback/widget/ShadowHelperJbmr2.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;
+
+import android.support.v17.leanback.R;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+class ShadowHelperJbmr2 {
+
+ static class ShadowImpl {
+ View mNormalShadow;
+ View mFocusShadow;
+ }
+
+ /* prepare parent for allowing shadows of a child */
+ public static void prepareParent(ViewGroup parent) {
+ parent.setLayoutMode(ViewGroup.LAYOUT_MODE_OPTICAL_BOUNDS);
+ }
+
+ /* add shadows and return a implementation detail object */
+ public static Object addShadow(ViewGroup shadowContainer) {
+ shadowContainer.setLayoutMode(ViewGroup.LAYOUT_MODE_OPTICAL_BOUNDS);
+ LayoutInflater inflater = LayoutInflater.from(shadowContainer.getContext());
+ inflater.inflate(R.layout.lb_shadow, shadowContainer, true);
+ ShadowImpl impl = new ShadowImpl();
+ impl.mNormalShadow = shadowContainer.findViewById(R.id.lb_shadow_normal);
+ impl.mFocusShadow = shadowContainer.findViewById(R.id.lb_shadow_focused);
+ return impl;
+ }
+
+ /* set shadow focus level 0 for unfocused 1 for fully focused */
+ public static void setShadowFocusLevel(Object impl, float level) {
+ ShadowImpl shadowImpl = (ShadowImpl) impl;
+ shadowImpl.mNormalShadow.setAlpha(1 - level);
+ shadowImpl.mFocusShadow.setAlpha(level);
+ }
+}
diff --git a/v17/leanback/kitkat/android/support/v17/leanback/app/ChangeBoundsKitKat.java b/v17/leanback/kitkat/android/support/v17/leanback/app/ChangeBoundsKitKat.java
new file mode 100644
index 0000000..ce8c2de
--- /dev/null
+++ b/v17/leanback/kitkat/android/support/v17/leanback/app/ChangeBoundsKitKat.java
@@ -0,0 +1,135 @@
+/*
+ * 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.ObjectAnimator;
+import android.animation.RectEvaluator;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.view.View;
+import android.view.ViewGroup;
+import android.transition.ChangeBounds;
+import android.transition.Transition;
+import android.transition.TransitionValues;
+
+import java.util.Map;
+
+/**
+ * This is a replacement of android.transition.ChangeBounds that treat reparent
+ * views slightly differently: see "PATCH" in the code.
+ */
+class ChangeBoundsKitKat extends ChangeBounds {
+
+ private static final String PROPNAME_PARENT = "android:changeBounds:parent";
+ private static final String PROPNAME_WINDOW_X = "android:changeBounds:windowX";
+ private static final String PROPNAME_WINDOW_Y = "android:changeBounds:windowY";
+
+ int[] tempLocation = new int[2];
+ boolean mReparent = false;
+ private static final String LOG_TAG = "ChangeBoundsKitKat";
+
+ private static RectEvaluator sRectEvaluator = new RectEvaluator();
+
+ @Override
+ public void setReparent(boolean reparent) {
+ super.setReparent(reparent);
+ mReparent = reparent;
+ }
+
+ @Override
+ public Animator createAnimator(final ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ if (startValues == null || endValues == null) {
+ return null;
+ }
+ Map<String, Object> startParentVals = startValues.values;
+ Map<String, Object> endParentVals = endValues.values;
+ ViewGroup startParent = (ViewGroup) startParentVals.get(PROPNAME_PARENT);
+ ViewGroup endParent = (ViewGroup) endParentVals.get(PROPNAME_PARENT);
+ if (startParent == null || endParent == null) {
+ return null;
+ }
+ final View view = endValues.view;
+ boolean parentsEqual = (startParent == endParent) ||
+ (startParent.getId() == endParent.getId());
+ // TODO: Might want reparenting to be separate/subclass transition, or at least
+ // triggered by a property on ChangeBounds. Otherwise, we're forcing the requirement that
+ // all parents in layouts have IDs to avoid layout-inflation resulting in a side-effect
+ // of reparenting the views.
+ if (!mReparent || parentsEqual) {
+ return super.createAnimator(sceneRoot, startValues, endValues);
+ } else {
+ int startX = (Integer) startValues.values.get(PROPNAME_WINDOW_X);
+ int startY = (Integer) startValues.values.get(PROPNAME_WINDOW_Y);
+ int endX = (Integer) endValues.values.get(PROPNAME_WINDOW_X);
+ int endY = (Integer) endValues.values.get(PROPNAME_WINDOW_Y);
+ // TODO: also handle size changes: check bounds and animate size changes
+ if (startX != endX || startY != endY) {
+ sceneRoot.getLocationInWindow(tempLocation);
+ Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ view.draw(canvas);
+ final BitmapDrawable drawable = new BitmapDrawable(bitmap);
+ view.setVisibility(View.INVISIBLE);
+ sceneRoot.getOverlay().add(drawable);
+ Rect startBounds1 = new Rect(startX - tempLocation[0], startY - tempLocation[1],
+ startX - tempLocation[0] + view.getWidth(),
+ startY - tempLocation[1] + view.getHeight());
+ // PATCH : initialize the startBounds immediately so that the bitmap
+ // will show up immediately without waiting start delay.
+ drawable.setBounds(startBounds1);
+ Rect endBounds1 = new Rect(endX - tempLocation[0], endY - tempLocation[1],
+ endX - tempLocation[0] + view.getWidth(),
+ endY - tempLocation[1] + view.getHeight());
+ ObjectAnimator anim = ObjectAnimator.ofObject(drawable, "bounds",
+ sRectEvaluator, startBounds1, endBounds1);
+ // PATCH : switch back to view when whole transition finishes.
+ TransitionListener transitionListener = new TransitionListener() {
+ @Override
+ public void onTransitionCancel(Transition transition) {
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ sceneRoot.getOverlay().remove(drawable);
+ view.setVisibility(View.VISIBLE);
+ removeListener(this);
+ }
+
+ @Override
+ public void onTransitionPause(Transition transition) {
+ }
+
+ @Override
+ public void onTransitionResume(Transition transition) {
+ }
+
+ @Override
+ public void onTransitionStart(Transition transition) {
+ }
+ };
+ addListener(transitionListener);
+ return anim;
+ }
+ }
+ return null;
+ }
+}
\ 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
new file mode 100644
index 0000000..4ad20b0
--- /dev/null
+++ b/v17/leanback/kitkat/android/support/v17/leanback/app/TransitionHelperKitkat.java
@@ -0,0 +1,201 @@
+/*
+ * 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.content.Context;
+import android.transition.AutoTransition;
+import android.transition.ChangeBounds;
+import android.transition.Fade;
+import android.transition.Scene;
+import android.transition.Transition;
+import android.transition.TransitionManager;
+import android.transition.TransitionSet;
+import android.transition.TransitionValues;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+class TransitionHelperKitkat {
+
+ private final Context mContext;
+
+ TransitionHelperKitkat(Context context) {
+ mContext = context;
+ }
+
+ Object createScene(ViewGroup sceneRoot, Runnable enterAction) {
+ Scene scene = new Scene(sceneRoot);
+ scene.setEnterAction(enterAction);
+ return scene;
+ }
+
+ Object createTransitionSet(boolean sequential) {
+ TransitionSet set = new TransitionSet();
+ set.setOrdering(sequential ? TransitionSet.ORDERING_SEQUENTIAL :
+ TransitionSet.ORDERING_TOGETHER);
+ return set;
+ }
+
+ void addTransition(Object transitionSet, Object transition) {
+ ((TransitionSet) transitionSet).addTransition((Transition) transition);
+ }
+
+ Object createAutoTransition() {
+ return new AutoTransition();
+ }
+
+ Object createFadeTransition(int fadingMode) {
+ Fade fade = new Fade(fadingMode);
+ return fade;
+ }
+
+ /**
+ * change bounds that support customized start delay.
+ */
+ static class CustomChangeBounds extends ChangeBoundsKitKat {
+
+ int mDefaultStartDelay;
+ // View -> delay
+ final HashMap<View, Integer> mViewStartDelays = new HashMap<View, Integer>();
+ // id -> delay
+ final SparseIntArray mIdStartDelays = new SparseIntArray();
+ // Class.getName() -> delay
+ final HashMap<String, Integer> mClassStartDelays = new HashMap<String, Integer>();
+
+ private int getDelay(View view) {
+ Integer delay = mViewStartDelays.get(view);
+ if (delay != null) {
+ return delay;
+ }
+ int idStartDelay = mIdStartDelays.get(view.getId(), -1);
+ if (idStartDelay != -1) {
+ return idStartDelay;
+ }
+ delay = mClassStartDelays.get(view.getClass().getName());
+ if (delay != null) {
+ return delay;
+ }
+ return mDefaultStartDelay;
+ }
+
+ @Override
+ public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
+ TransitionValues endValues) {
+ Animator animator = super.createAnimator(sceneRoot, startValues, endValues);
+ if (animator != null && endValues != null && endValues.view != null) {
+ animator.setStartDelay(getDelay(endValues.view));
+ }
+ return animator;
+ }
+
+ public void setStartDelay(View view, int startDelay) {
+ mViewStartDelays.put(view, startDelay);
+ }
+
+ public void setStartDelay(int viewId, int startDelay) {
+ mIdStartDelays.put(viewId, startDelay);
+ }
+
+ public void setStartDelay(String className, int startDelay) {
+ mClassStartDelays.put(className, startDelay);
+ }
+
+ public void setDefaultStartDelay(int startDelay) {
+ mDefaultStartDelay = startDelay;
+ }
+ }
+
+ Object createChangeBounds(boolean reparent) {
+ CustomChangeBounds changeBounds = new CustomChangeBounds();
+ changeBounds.setReparent(reparent);
+ return changeBounds;
+ }
+
+ void setChangeBoundsStartDelay(Object changeBounds, int viewId, int startDelay) {
+ ((CustomChangeBounds) changeBounds).setStartDelay(viewId, startDelay);
+ }
+
+ void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay) {
+ ((CustomChangeBounds) changeBounds).setStartDelay(view, startDelay);
+ }
+
+ void setChangeBoundsStartDelay(Object changeBounds, String className, int startDelay) {
+ ((CustomChangeBounds) changeBounds).setStartDelay(className, startDelay);
+ }
+
+ void setChangeBoundsDefaultStartDelay(Object changeBounds, int startDelay) {
+ ((CustomChangeBounds) changeBounds).setDefaultStartDelay(startDelay);
+ }
+
+ void exclude(Object transition, int targetId, boolean exclude) {
+ ((Transition) transition).excludeTarget(targetId, exclude);
+ }
+
+ void exclude(Object transition, View targetView, boolean exclude) {
+ ((Transition) transition).excludeTarget(targetView, exclude);
+ }
+
+ void excludeChildren(Object transition, int targetId, boolean exclude) {
+ ((Transition) transition).excludeChildren(targetId, exclude);
+ }
+
+ void excludeChildren(Object transition, View targetView, boolean exclude) {
+ ((Transition) transition).excludeChildren(targetView, exclude);
+ }
+
+ void include(Object transition, int targetId) {
+ ((Transition) transition).addTarget(targetId);
+ }
+
+ void include(Object transition, View targetView) {
+ ((Transition) transition).addTarget(targetView);
+ }
+
+ public void setTransitionCompleteListener(Object transition, Runnable listener) {
+ Transition t = (Transition) transition;
+ final Runnable completeListener = listener;
+ t.addListener(new Transition.TransitionListener() {
+
+ @Override
+ public void onTransitionStart(Transition transition) {
+ }
+
+ @Override
+ public void onTransitionResume(Transition transition) {
+ }
+
+ @Override
+ public void onTransitionPause(Transition transition) {
+ }
+
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ completeListener.run();
+ }
+
+ @Override
+ public void onTransitionCancel(Transition transition) {
+ }
+ });
+ }
+
+ void runTransition(Object scene, Object transition) {
+ TransitionManager.go((Scene) scene, (Transition) transition);
+ }
+}
diff --git a/v17/leanback/project.properties b/v17/leanback/project.properties
new file mode 100644
index 0000000..91d2b02
--- /dev/null
+++ b/v17/leanback/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-hdpi/ic_action_search.png b/v17/leanback/res/drawable-hdpi/ic_action_search.png
new file mode 100644
index 0000000..a70393b
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/ic_action_search.png
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
new file mode 100644
index 0000000..6777e19
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/ic_circle_g_bg.png
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
new file mode 100644
index 0000000..f556eac
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/ic_circle_shadow.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_action_bg_focused.9.png b/v17/leanback/res/drawable-hdpi/lb_action_bg_focused.9.png
new file mode 100644
index 0000000..112b541
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_card_shadow_focused.9.png b/v17/leanback/res/drawable-hdpi/lb_card_shadow_focused.9.png
new file mode 100644
index 0000000..7c59b7f
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_card_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-hdpi/lb_card_shadow_normal.9.png b/v17/leanback/res/drawable-hdpi/lb_card_shadow_normal.9.png
new file mode 100644
index 0000000..4abb20a
--- /dev/null
+++ b/v17/leanback/res/drawable-hdpi/lb_card_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
new file mode 100644
index 0000000..dea3962
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/ic_action_search.png
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
new file mode 100644
index 0000000..641f096
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/ic_circle_g_bg.png
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
new file mode 100644
index 0000000..f45b76c
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/ic_circle_shadow.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_action_bg_focused.9.png b/v17/leanback/res/drawable-mdpi/lb_action_bg_focused.9.png
new file mode 100644
index 0000000..1d2b041
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_card_shadow_focused.9.png b/v17/leanback/res/drawable-mdpi/lb_card_shadow_focused.9.png
new file mode 100644
index 0000000..39b220c
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_card_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-mdpi/lb_card_shadow_normal.9.png b/v17/leanback/res/drawable-mdpi/lb_card_shadow_normal.9.png
new file mode 100644
index 0000000..b9c3400
--- /dev/null
+++ b/v17/leanback/res/drawable-mdpi/lb_card_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
new file mode 100644
index 0000000..19658e4
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/ic_action_search.png
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
new file mode 100644
index 0000000..acb7c79
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/ic_circle_g_bg.png
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
new file mode 100644
index 0000000..60930f4
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/ic_circle_shadow.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png b/v17/leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png
new file mode 100644
index 0000000..5e7c2be
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png b/v17/leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png
new file mode 100644
index 0000000..599928b
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_card_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png b/v17/leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png
new file mode 100644
index 0000000..e5413a8
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_card_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xhdpi/lb_ic_card_info_text_fade.png b/v17/leanback/res/drawable-xhdpi/lb_ic_card_info_text_fade.png
new file mode 100644
index 0000000..1364a48
--- /dev/null
+++ b/v17/leanback/res/drawable-xhdpi/lb_ic_card_info_text_fade.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
new file mode 100644
index 0000000..a108638
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/ic_action_search.png
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
new file mode 100644
index 0000000..2bd8ab7
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/ic_circle_g_bg.png
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
new file mode 100644
index 0000000..619ce81
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/ic_circle_shadow.9.png
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
new file mode 100644
index 0000000..48f82a4
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_action_bg_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_card_shadow_focused.9.png b/v17/leanback/res/drawable-xxhdpi/lb_card_shadow_focused.9.png
new file mode 100644
index 0000000..125bf12
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_card_shadow_focused.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable-xxhdpi/lb_card_shadow_normal.9.png b/v17/leanback/res/drawable-xxhdpi/lb_card_shadow_normal.9.png
new file mode 100644
index 0000000..887d24f
--- /dev/null
+++ b/v17/leanback/res/drawable-xxhdpi/lb_card_shadow_normal.9.png
Binary files differ
diff --git a/v17/leanback/res/drawable/lb_action_bg.xml b/v17/leanback/res/drawable/lb_action_bg.xml
new file mode 100644
index 0000000..76fbd8f
--- /dev/null
+++ b/v17/leanback/res/drawable/lb_action_bg.xml
@@ -0,0 +1,22 @@
+<?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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="2dp" />
+ <solid android:color="@color/lb_action_bg_color" />
+</shape>
diff --git a/v17/leanback/res/drawable/lb_background.xml b/v17/leanback/res/drawable/lb_background.xml
new file mode 100644
index 0000000..4732bc1
--- /dev/null
+++ b/v17/leanback/res/drawable/lb_background.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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item android:drawable="@color/lb_grey" android:id="@+id/background_theme"/>
+ <item android:drawable="@color/lb_grey" android:id="@+id/background_color"/>
+ <!-- Replaced at runtime with image to fade out -->
+ <item android:drawable="@color/lb_grey" android:id="@+id/background_imageout"/>
+ <!-- Replaced at runtime with image to fade in -->
+ <item android:drawable="@color/lb_grey" android:id="@+id/background_imagein"/>
+ <item android:drawable="@color/lb_background_protection" android:id="@+id/background_dim" />
+</layer-list>
diff --git a/v17/leanback/res/drawable/lb_transition_action_bg.xml b/v17/leanback/res/drawable/lb_transition_action_bg.xml
new file mode 100644
index 0000000..36688bc
--- /dev/null
+++ b/v17/leanback/res/drawable/lb_transition_action_bg.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+
+<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>
diff --git a/v17/leanback/res/layout/lb_action_1_line.xml b/v17/leanback/res/layout/lb_action_1_line.xml
new file mode 100644
index 0000000..2374cf3
--- /dev/null
+++ b/v17/leanback/res/layout/lb_action_1_line.xml
@@ -0,0 +1,31 @@
+<?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.
+-->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/lb_action_text"
+ 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"
+ 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
new file mode 100644
index 0000000..15cff81
--- /dev/null
+++ b/v17/leanback/res/layout/lb_action_2_lines.xml
@@ -0,0 +1,46 @@
+<?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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ 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
diff --git a/v17/leanback/res/layout/lb_background_window.xml b/v17/leanback/res/layout/lb_background_window.xml
new file mode 100644
index 0000000..73c84d2
--- /dev/null
+++ b/v17/leanback/res/layout/lb_background_window.xml
@@ -0,0 +1,21 @@
+<!--?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:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ />
diff --git a/v17/leanback/res/layout/lb_browse_fragment.xml b/v17/leanback/res/layout/lb_browse_fragment.xml
new file mode 100644
index 0000000..0315daa
--- /dev/null
+++ b/v17/leanback/res/layout/lb_browse_fragment.xml
@@ -0,0 +1,41 @@
+<?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/browse_dummy"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <!-- BrowseFrameLayout serves as root of transition and manages switch between
+ left and right-->
+ <android.support.v17.leanback.app.BrowseFrameLayout
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ 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_height="match_parent" />
+ <FrameLayout
+ android:id="@+id/browse_headers_dock"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent" />
+ </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
new file mode 100644
index 0000000..77bd35c
--- /dev/null
+++ b/v17/leanback/res/layout/lb_browse_title.xml
@@ -0,0 +1,56 @@
+<?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.
+-->
+<LinearLayout 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">
+
+ <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_card_color_overlay.xml b/v17/leanback/res/layout/lb_card_color_overlay.xml
new file mode 100644
index 0000000..45a40e1
--- /dev/null
+++ b/v17/leanback/res/layout/lb_card_color_overlay.xml
@@ -0,0 +1,21 @@
+<?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.
+-->
+
+<View
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/v17/leanback/res/layout/lb_details_description.xml b/v17/leanback/res/layout/lb_details_description.xml
new file mode 100644
index 0000000..1cd26cd
--- /dev/null
+++ b/v17/leanback/res/layout/lb_details_description.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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ >
+
+ <TextView
+ android:id="@+id/lb_details_description_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?attr/detailsDescriptionTitleStyle"
+ />
+
+ <TextView
+ android:id="@+id/lb_details_description_subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?attr/detailsDescriptionSubtitleStyle"
+ />
+
+ <TextView
+ android:id="@+id/lb_details_description_body"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?attr/detailsDescriptionBodyStyle"
+ />
+</LinearLayout>
diff --git a/v17/leanback/res/layout/lb_details_fragment.xml b/v17/leanback/res/layout/lb_details_fragment.xml
new file mode 100644
index 0000000..92cf4b4
--- /dev/null
+++ b/v17/leanback/res/layout/lb_details_fragment.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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/dummy"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ <FrameLayout
+ android:id="@+id/fragment_dock"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent" />
+</FrameLayout>
diff --git a/v17/leanback/res/layout/lb_details_overview.xml b/v17/leanback/res/layout/lb_details_overview.xml
new file mode 100644
index 0000000..ea8220e
--- /dev/null
+++ b/v17/leanback/res/layout/lb_details_overview.xml
@@ -0,0 +1,59 @@
+<?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.
+-->
+
+<LinearLayout 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" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ 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" />
+
+ <FrameLayout
+ android:id="@+id/details_overview_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="@dimen/lb_details_overview_description_margin_left"
+ android:layout_marginRight="@dimen/lb_details_overview_description_margin_right"
+ android:gravity="top" />
+ </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
diff --git a/v17/leanback/res/layout/lb_headers_fragment.xml b/v17/leanback/res/layout/lb_headers_fragment.xml
new file mode 100644
index 0000000..c72cd06
--- /dev/null
+++ b/v17/leanback/res/layout/lb_headers_fragment.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+<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/browse_headers"
+ android:layout_width="match_parent"
+ 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
new file mode 100644
index 0000000..0a14ff7
--- /dev/null
+++ b/v17/leanback/res/layout/lb_image_card_view.xml
@@ -0,0 +1,85 @@
+<?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"
+ xmlns:lb="http://schemas.android.com/apk/res-auto">
+
+ <ImageView
+ android:id="@+id/main_image"
+ lb:layout_viewType="main"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="centerCrop"
+ android:contentDescription="@null" />
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ lb:layout_viewType="info" >
+ <RelativeLayout
+ android:id="@+id/info_field"
+ android:background="@color/lb_basic_card_info_bg_color"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/lb_basic_card_info_height"
+ android:padding="@dimen/lb_basic_card_info_padding"
+ android:layout_centerHorizontal="true" >
+ <TextView
+ android:id="@+id/title_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_marginTop="@dimen/lb_basic_card_info_text_margin"
+ android:layout_marginLeft="@dimen/lb_basic_card_info_text_margin"
+ android:maxLines="1"
+ android:fontFamily="sans-serif-condensed"
+ android:textColor="@color/lb_basic_card_title_text_color"
+ android:textSize="@dimen/lb_basic_card_title_text_size"
+ android:ellipsize="end" />
+ <TextView
+ android:id="@+id/content_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentBottom="true"
+ android:layout_marginLeft="@dimen/lb_basic_card_info_text_margin"
+ android:layout_marginBottom="@dimen/lb_basic_card_info_text_margin"
+ android:maxLines="1"
+ android:fontFamily="sans-serif-condensed"
+ android:textColor="@color/lb_basic_card_content_text_color"
+ android:textSize="@dimen/lb_basic_card_content_text_size"
+ android:ellipsize="none" />
+ <ImageView
+ android:id="@+id/extra_badge"
+ android:layout_width="@dimen/lb_basic_card_info_badge_size"
+ android:layout_height="@dimen/lb_basic_card_info_badge_size"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:scaleType="fitCenter"
+ android:background="@color/lb_basic_card_info_bg_color"
+ android:contentDescription="@null" />
+ <ImageView
+ android:id="@+id/fade_mask"
+ android:src="@drawable/lb_ic_card_info_text_fade"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/lb_basic_card_info_badge_size"
+ android:layout_alignParentBottom="true"
+ android:layout_toStartOf="@id/extra_badge"
+ android:scaleType="fitCenter"
+ android:contentDescription="@null" />
+ </RelativeLayout>
+ </FrameLayout>
+</merge>
diff --git a/v17/leanback/res/layout/lb_list_row.xml b/v17/leanback/res/layout/lb_list_row.xml
new file mode 100644
index 0000000..a432518
--- /dev/null
+++ b/v17/leanback/res/layout/lb_list_row.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<android.support.v17.leanback.widget.HorizontalGridView
+ 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"
+ style="?attr/rowHorizontalGridStyle" />
diff --git a/v17/leanback/res/layout/lb_list_row_hovercard.xml b/v17/leanback/res/layout/lb_list_row_hovercard.xml
new file mode 100644
index 0000000..a001dc9
--- /dev/null
+++ b/v17/leanback/res/layout/lb_list_row_hovercard.xml
@@ -0,0 +1,32 @@
+<?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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/hovercard_panel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?attr/rowHoverCardTitleStyle" />
+ <TextView
+ android:id="@+id/description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?attr/rowHoverCardDescriptionStyle" />
+</LinearLayout>
\ No newline at end of file
diff --git a/v17/leanback/res/layout/lb_row_container.xml b/v17/leanback/res/layout/lb_row_container.xml
new file mode 100644
index 0000000..0f5dd5f
--- /dev/null
+++ b/v17/leanback/res/layout/lb_row_container.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.
+-->
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+<LinearLayout
+ android:id="@+id/lb_row_container_header_dock"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingLeft="?attr/browsePaddingLeft"
+ android:clipToPadding="false">
+</LinearLayout>
+</merge>
\ No newline at end of file
diff --git a/v17/leanback/res/layout/lb_rows_fragment.xml b/v17/leanback/res/layout/lb_rows_fragment.xml
new file mode 100644
index 0000000..5b147c5
--- /dev/null
+++ b/v17/leanback/res/layout/lb_rows_fragment.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+<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
new file mode 100644
index 0000000..8e1a1b7
--- /dev/null
+++ b/v17/leanback/res/layout/lb_search_bar.xml
@@ -0,0 +1,65 @@
+<?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.
+-->
+<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" >
+
+ <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" >
+
+
+ <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>
+ </RelativeLayout>
+</android.support.v17.leanback.widget.SearchBar>
diff --git a/v17/leanback/res/layout/lb_search_fragment.xml b/v17/leanback/res/layout/lb_search_fragment.xml
new file mode 100644
index 0000000..985ab21
--- /dev/null
+++ b/v17/leanback/res/layout/lb_search_fragment.xml
@@ -0,0 +1,27 @@
+<?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/lb_search_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/lb_search_bar_padding_top">
+ <FrameLayout
+ android:id="@+id/lb_results_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"/>
+ <include layout="@layout/lb_search_bar" />
+</FrameLayout>
diff --git a/v17/leanback/res/layout/lb_search_orb.xml b/v17/leanback/res/layout/lb_search_orb.xml
new file mode 100644
index 0000000..c6bf690
--- /dev/null
+++ b/v17/leanback/res/layout/lb_search_orb.xml
@@ -0,0 +1,58 @@
+<?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.
+-->
+<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" />
+
+ <FrameLayout
+ 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" >
+
+ <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="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
diff --git a/v17/leanback/res/layout/lb_shadow.xml b/v17/leanback/res/layout/lb_shadow.xml
new file mode 100644
index 0000000..b0aa0b1
--- /dev/null
+++ b/v17/leanback/res/layout/lb_shadow.xml
@@ -0,0 +1,31 @@
+<?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_shadow_normal"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/lb_card_shadow_normal" />
+ <View
+ android:id="@+id/lb_shadow_focused"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/lb_card_shadow_focused"
+ android:alpha="0" />
+
+</merge>
\ No newline at end of file
diff --git a/v17/leanback/res/layout/lb_vertical_grid.xml b/v17/leanback/res/layout/lb_vertical_grid.xml
new file mode 100644
index 0000000..5dfe0c9
--- /dev/null
+++ b/v17/leanback/res/layout/lb_vertical_grid.xml
@@ -0,0 +1,23 @@
+<?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.
+-->
+
+<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_height="match_parent"
+ 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
new file mode 100644
index 0000000..902c483
--- /dev/null
+++ b/v17/leanback/res/layout/lb_vertical_grid_fragment.xml
@@ -0,0 +1,30 @@
+<?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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include layout="@layout/lb_browse_title" />
+
+ <FrameLayout
+ android:id="@+id/browse_grid_dock"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/v17/leanback/res/values/attrs.xml b/v17/leanback/res/values/attrs.xml
new file mode 100644
index 0000000..78456c6
--- /dev/null
+++ b/v17/leanback/res/values/attrs.xml
@@ -0,0 +1,164 @@
+<?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>
+ <declare-styleable name="lbBaseGridView">
+ <!-- Allow DPAD key to navigate out at the front of the View (where position = 0),
+ default is false -->
+ <attr name="focusOutFront" format="boolean" />
+ <!-- Allow DPAD key to navigate out at the end of the view, default is false -->
+ <attr name="focusOutEnd" format="boolean" />
+ <!-- Defining margin between two items horizontally -->
+ <attr name="horizontalMargin" format="dimension" />
+ <!-- Defining margin between two items vertically -->
+ <attr name="verticalMargin" format="dimension" />
+ </declare-styleable>
+
+ <declare-styleable name="lbHorizontalGridView">
+ <!-- Defining height of each row of HorizontalGridView -->
+ <attr name="rowHeight" format="dimension" />
+ <!-- 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" />
+ <!-- Defining number of columns -->
+ <attr name="numberOfColumns" format="integer" />
+ </declare-styleable>
+
+ <declare-styleable name="lbBaseCardView">
+ <!-- Defines the type of the card layout -->
+ <attr name="cardType" format="enum">
+ <!-- A simple card layout with a single layout region. -->
+ <enum name="mainOnly" value="0" />
+ <!-- A card layout with two layout regions: a main area which is
+ always visible, and an info region that appears over the lower
+ area of the main region. -->
+ <enum name="infoOver" value="1" />
+ <!-- A card layout with two layout regions: a main area which is
+ always visible, and an info region that appears below the main
+ region. -->
+ <enum name="infoUnder" value="2" />
+ <!-- A card layout with three layout regions: a main area which is
+ always visible, an info region that appears below the main
+ region, and an extra region that appears below the info region
+ after a small delay. -->
+ <enum name="infoUnderWithExtra" value="3" />
+ </attr>
+ <!-- Defines when the info region of a card layout is displayed. -->
+ <attr name="infoVisibility" format="enum">
+ <!-- Always display the info region. -->
+ <enum name="always" value="0"/>
+ <!-- Display the info region only when activated. -->
+ <enum name="activated" value="1"/>
+ <!-- Display the info region only when selected. -->
+ <enum name="selected" value="2"/>
+ </attr>
+ <!-- Defines when the extra region of a card layout is displayed.
+ Depends on infoVisibility, meaning the extra region never displays
+ if the info region is not displayed as well. -->
+ <attr name="extraVisibility" format="enum">
+ <!-- Always display the extra region. -->
+ <enum name="always" value="0"/>
+ <!-- Display the extra region only when activated. -->
+ <enum name="activated" value="1"/>
+ <!-- Display the extra region only when selected. -->
+ <enum name="selected" value="2"/>
+ </attr>
+ <!-- Defines the delay in milliseconds before the selection animation
+ runs for a card layout. -->
+ <attr name="selectedAnimationDelay" format="integer" />
+ <!-- Defines the duration in milliseconds of the selection animation for
+ a card layout. -->
+ <attr name="selectedAnimationDuration" format="integer" />
+ <!-- Defines the duration in milliseconds of the activated animation for
+ a card layout. -->
+ <attr name="activatedAnimationDuration" format="integer" />
+ </declare-styleable>
+
+ <!-- This is the basic set of layout attributes for elements within a card
+ layout. These attributes are specified with the rest of an elements's
+ normal attributes. -->
+ <declare-styleable name="lbBaseCardView_Layout">
+ <!-- The card layout region defined by this element. At most one of
+ element of each type should be specified as an immediate child of
+ the card layout. -->
+ <attr name="layout_viewType" format="enum">
+ <!-- The main region of the card. -->
+ <enum name="main" value="0"/>
+ <!-- The info region of the card. -->
+ <enum name="info" value="1"/>
+ <!-- The extra region of the card. -->
+ <enum name="extra" value="2"/>
+ </attr>
+ </declare-styleable>
+
+ <declare-styleable name="LeanbackTheme">
+
+ <!-- left padding of BrowseFragment, RowsFragment, DetailsFragment -->
+ <attr name="browsePaddingLeft" format="dimension" />
+ <!-- right padding of BrowseFragment, RowsFragment, DetailsFragment -->
+ <attr name="browsePaddingRight" format="dimension" />
+ <!-- top padding of BrowseFragment -->
+ <attr name="browsePaddingTop" format="dimension" />
+ <!-- bottom padding of BrowseFragment -->
+ <attr name="browsePaddingBottom" format="dimension" />
+ <!-- start margin of RowsFragment inside BrowseFragment when HeadersFragment is visible -->
+ <attr name="browseRowsMarginStart" format="dimension" />
+ <!-- top margin of RowsFragment inside BrowseFragment when BrowseFragment title is visible -->
+ <attr name="browseRowsMarginTop" format="dimension" />
+ <!-- fading edge length of start of browse row when HeadersFragment is visible -->
+ <attr name="browseRowsFadingEdgeLength" format="dimension" />
+
+ <!-- BrowseFragment Title text style -->
+ <attr name="browseTitleTextStyle" format="reference" />
+
+ <!-- BrowseFragment Title icon style -->
+ <attr name="browseTitleIconStyle" format="reference" />
+
+ <!-- vertical grid style inside HeadersFragment -->
+ <attr name="headersVerticalGridStyle" format="reference" />
+
+ <!-- vertical grid style inside RowsFragment -->
+ <attr name="rowsVerticalGridStyle" format="reference" />
+
+ <!-- horizontal grid style inside a row -->
+ <attr name="rowHorizontalGridStyle" format="reference" />
+ <!-- header style inside a row -->
+ <attr name="rowHeaderStyle" format="reference" />
+
+ <!-- hover card title style -->
+ <attr name="rowHoverCardTitleStyle" format="reference" />
+ <!-- hover card description style -->
+ <attr name="rowHoverCardDescriptionStyle" format="reference" />
+
+ <!-- CardView styles -->
+ <attr name="baseCardViewStyle" format="reference" />
+ <attr name="imageCardViewStyle" format="reference" />
+
+ <!-- for details overviews -->
+ <attr name="detailsDescriptionTitleStyle" format="reference" />
+ <attr name="detailsDescriptionSubtitleStyle" format="reference" />
+ <attr name="detailsDescriptionBodyStyle" format="reference" />
+
+ <!-- style for a vertical grid of items -->
+ <attr name="itemsVerticalGridStyle" format="reference" />
+
+ </declare-styleable>
+</resources>
diff --git a/v17/leanback/res/values/colors.xml b/v17/leanback/res/values/colors.xml
new file mode 100644
index 0000000..0eeb825
--- /dev/null
+++ b/v17/leanback/res/values/colors.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 xmlns:android="http://schemas.android.com/apk/res/android">
+ <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_list_item_unselected_text_color">#FFF1F1F1</color>
+ <color name="lb_background_protection">#A0333333</color>
+
+ <color name="lb_view_dim_mask_color">#000000</color>
+ <item name="lb_view_dimmed_level" type="dimen">60%</item>
+
+ <color name="lb_details_description_color">#EEEEEE</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_basic_card_bg_color">#FF1B1B1B</color>
+ <color name="lb_basic_card_info_bg_color">#FF1B1B1B</color>
+ <color name="lb_basic_card_title_text_color">#FFEEEEEE</color>
+ <color name="lb_basic_card_content_text_color">#FFEEEEEE</color>
+</resources>
diff --git a/v17/leanback/res/values/dimens.xml b/v17/leanback/res/values/dimens.xml
new file mode 100644
index 0000000..61792fa
--- /dev/null
+++ b/v17/leanback/res/values/dimens.xml
@@ -0,0 +1,117 @@
+<?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>
+ <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_fading_edge">16dp</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>
+
+ <integer name="lb_browse_headers_transition_delay">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>
+
+ <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>
+ <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>
+
+ <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>
+ <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_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_body_text_size">14sp</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>
+
+ <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_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>
+
+ <!-- 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_items_layout_margin_top">27dp</dimen>
+ <dimen name="lb_search_bar_items_width">660dp</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>
+
+ <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>
+
+ <!-- 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_info_badge_size">16dp</dimen>
+</resources>
diff --git a/v17/leanback/res/values/ids.xml b/v17/leanback/res/values/ids.xml
new file mode 100644
index 0000000..4010b2c
--- /dev/null
+++ b/v17/leanback/res/values/ids.xml
@@ -0,0 +1,19 @@
+<?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>
+ <item type="id" name="lb_focus_animator" />
+ </resources>
\ No newline at end of file
diff --git a/v17/leanback/res/values/integers.xml b/v17/leanback/res/values/integers.xml
new file mode 100644
index 0000000..4fbe83f
--- /dev/null
+++ b/v17/leanback/res/values/integers.xml
@@ -0,0 +1,20 @@
+<?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>
+ <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>
+</resources>
diff --git a/v17/leanback/res/values/strings.xml b/v17/leanback/res/values/strings.xml
new file mode 100644
index 0000000..333c06a
--- /dev/null
+++ b/v17/leanback/res/values/strings.xml
@@ -0,0 +1,21 @@
+<?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>
+ <string name="orb_search_label">Search</string>
+ <string name="orb_search_action">Search Action</string>
+ <string name="lb_search_bar_hint">Search</string>
+</resources>
\ No newline at end of file
diff --git a/v17/leanback/res/values/styles.xml b/v17/leanback/res/values/styles.xml
new file mode 100644
index 0000000..955e9fe
--- /dev/null
+++ b/v17/leanback/res/values/styles.xml
@@ -0,0 +1,196 @@
+<?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">
+ <style name="TextAppearance.Leanback" parent="android:TextAppearance.Holo">
+ <!-- Any text appearance overrides go here. -->
+ <item name="android:fontFamily">sans-serif-condensed</item>
+ </style>
+
+ <style name="TextAppearance.Leanback.Title" parent="TextAppearance.Leanback">
+ <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>
+ </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>
+
+ <style name="TextAppearance.Leanback.SearchTextEdit" parent="TextAppearance.Leanback">
+ <item name="android:textSize">@dimen/lb_search_bar_text_size</item>
+ </style>
+
+ <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>
+ </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>
+ </style>
+
+ <style name="TextAppearance.Leanback.DetailsDescriptionBody">
+ <item name="android:textSize">@dimen/lb_details_description_body_text_size</item>
+ <item name="android:textColor">@color/lb_details_description_color</item>
+ <item name="android:fontFamily">sans-serif</item>
+ </style>
+
+ <style name="Widget.Leanback" parent="android:Widget.Holo" />
+
+ <style name="Widget.Leanback.BaseCardViewStyle" />
+
+ <style name="Widget.Leanback.ImageCardViewStyle" parent="Widget.Leanback.BaseCardViewStyle">
+ <item name="cardType">infoUnder</item>
+ <item name="infoVisibility">activated</item>
+ <item name="android:background">@color/lb_basic_card_bg_color</item>
+ </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:textAppearance">@style/TextAppearance.Leanback.Title</item>
+ </style>
+
+ <style name="Widget.Leanback.Title.Icon">
+ <item name="android:scaleType">centerInside</item>
+ </style>
+
+ <!-- HeadersFragment (fast lane) -->
+ <style name="Widget.Leanback.Headers" />
+
+ <!-- RowsFragment -->
+ <style name="Widget.Leanback.Rows" >
+ </style>
+
+ <!-- row view -->
+ <style name="Widget.Leanback.Row" >
+ </style>
+
+ <style name="Widget.Leanback.GridItems" />
+
+ <style name="Widget.Leanback.Headers.VerticalGridView" >
+ <item name="android:paddingLeft">?attr/browsePaddingLeft</item>
+ <item name="android:clipToPadding">false</item>
+ <item name="focusOutFront">true</item>
+ <item name="focusOutEnd">true</item>
+ <item name="verticalMargin">@dimen/lb_browse_headers_vertical_margin</item>
+ <item name="android:focusable">true</item>
+ <item name="android:focusableInTouchMode">true</item>
+ </style>
+
+ <style name="Widget.Leanback.Header" >
+ <item name="android:gravity">center_vertical</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>
+ </style>
+
+ <style name="Widget.Leanback.Rows.VerticalGridView" >
+ <item name="android:paddingBottom">?attr/browsePaddingBottom</item>
+ <item name="android:clipToPadding">false</item>
+ <item name="focusOutFront">true</item>
+ <item name="focusOutEnd">true</item>
+ <item name="android:focusable">true</item>
+ <item name="android:focusableInTouchMode">true</item>
+ </style>
+
+ <style name="Widget.Leanback.Row.HorizontalGridView">
+ <item name="android:clipToPadding">false</item>
+ <item name="android:focusable">true</item>
+ <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="focusOutFront">true</item>
+ </style>
+
+ <style name="Widget.Leanback.GridItems.VerticalGridView">
+ <item name="android:clipToPadding">false</item>
+ <item name="android:focusable">true</item>
+ <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="focusOutFront">true</item>
+ </style>
+
+ <style name="Widget.Leanback.Row.Header">
+ <item name="android:minHeight">@dimen/lb_browse_row_title_height</item>
+ <item name="android:textAppearance">@style/TextAppearance.Leanback.Row.Header</item>
+ </style>
+
+ <style name="TextAppearance.Leanback.Row.HoverCardTitle" parent="TextAppearance.Leanback">
+ <item name="android:textSize">@dimen/lb_browse_row_hovercard_title_font_size</item>
+ </style>
+
+ <style name="TextAppearance.Leanback.Row.HoverCardDescription" parent="TextAppearance.Leanback">
+ <item name="android:textSize">@dimen/lb_browse_row_hovercard_description_font_size</item>
+ </style>
+
+ <style name="Widget.Leanback.Row.HoverCardTitle" >
+ <item name="android:textAppearance">@style/TextAppearance.Leanback.Row.HoverCardTitle</item>
+ <item name="android:maxWidth">@dimen/lb_browse_row_hovercard_max_width</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:ellipsize">end</item>
+ </style>
+
+ <style name="Widget.Leanback.Row.HoverCardDescription" >
+ <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>
+ </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>
+ </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>
+ </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>
+ </style>
+</resources>
diff --git a/v17/leanback/res/values/themes.xml b/v17/leanback/res/values/themes.xml
new file mode 100644
index 0000000..b683b28
--- /dev/null
+++ b/v17/leanback/res/values/themes.xml
@@ -0,0 +1,51 @@
+<?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="Theme.Leanback" parent="android:Theme.Holo.NoActionBar">
+
+ <item name="android:windowOverscan">true</item>
+
+ <item name="baseCardViewStyle">@style/Widget.Leanback.BaseCardViewStyle</item>
+ <item name="imageCardViewStyle">@style/Widget.Leanback.ImageCardViewStyle</item>
+
+ <item name="browsePaddingLeft">@dimen/lb_browse_padding_left</item>
+ <item name="browsePaddingRight">@dimen/lb_browse_padding_right</item>
+ <item name="browsePaddingTop">@dimen/lb_browse_padding_top</item>
+ <item name="browsePaddingBottom">@dimen/lb_browse_padding_bottom</item>
+ <item name="browseRowsMarginStart">@dimen/lb_browse_rows_margin_start</item>
+ <item name="browseRowsMarginTop">@dimen/lb_browse_rows_margin_top</item>
+ <item name="browseRowsFadingEdgeLength">@dimen/lb_browse_rows_fading_edge</item>
+
+ <item name="headersVerticalGridStyle">@style/Widget.Leanback.Headers.VerticalGridView</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="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="detailsDescriptionTitleStyle">@style/Widget.Leanback.DetailsDescriptionTitleStyle</item>
+ <item name="detailsDescriptionSubtitleStyle">@style/Widget.Leanback.DetailsDescriptionSubtitleStyle</item>
+ <item name="detailsDescriptionBodyStyle">@style/Widget.Leanback.DetailsDescriptionBodyStyle</item>
+ </style>
+
+</resources>
diff --git a/v17/leanback/src/.readme b/v17/leanback/src/.readme
new file mode 100644
index 0000000..4bcebad
--- /dev/null
+++ b/v17/leanback/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/v17/leanback/src/android/support/v17/leanback/app/BackgroundFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BackgroundFragment.java
new file mode 100644
index 0000000..334dd9d
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/BackgroundFragment.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.app;
+
+import android.app.Fragment;
+
+/**
+ * Fragment used by the background manager.
+ * @hide
+ */
+public final class BackgroundFragment extends Fragment {
+ private BackgroundManager mBackgroundManager;
+
+ void setBackgroundManager(BackgroundManager backgroundManager) {
+ mBackgroundManager = backgroundManager;
+ }
+
+ BackgroundManager getBackgroundManager() {
+ return mBackgroundManager;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // mBackgroundManager might be null:
+ // if BackgroundFragment is just restored by FragmentManager,
+ // and user does not call BackgroundManager.getInstance() yet.
+ if (mBackgroundManager != null) {
+ mBackgroundManager.onActivityResume();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ // mBackgroundManager might be null:
+ // if BackgroundFragment is just restored by FragmentManager,
+ // and user does not call BackgroundManager.getInstance() yet.
+ if (mBackgroundManager != null) {
+ mBackgroundManager.detach();
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java b/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
new file mode 100644
index 0000000..8b56775
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/BackgroundManager.java
@@ -0,0 +1,724 @@
+/*
+ * 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.R;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.drawable.BitmapDrawable;
+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.LinearInterpolator;
+
+/**
+ * Supports background image continuity between multiple Activities.
+ *
+ * <p>An Activity should instantiate a BackgroundManager and {@link #attach}
+ * to the Activity's window. When the Activity is started, the background is
+ * initialized to the current background values stored in a continuity service.
+ * The background continuity service is updated as the background is updated.
+ *
+ * <p>At some point, for example when it is stopped, the Activity may release
+ * its background state.
+ *
+ * <p>When an Activity is resumed, if the BackgroundManager has not been
+ * released, the continuity service is updated from the BackgroundManager state.
+ * If the BackgroundManager was released, the BackgroundManager inherits the
+ * current state from the continuity service.
+ *
+ * <p>When the last Activity is destroyed, the background state is reset.
+ *
+ * <p>Backgrounds consist of several layers, from back to front:
+ * <ul>
+ * <li>the background Drawable of the theme</li>
+ * <li>a solid color (set via {@link #setColor})</li>
+ * <li>two Drawables, previous and current (set via {@link #setBitmap} or
+ * {@link #setDrawable}), which may be in transition</li>
+ * </ul>
+ *
+ * <p>BackgroundManager holds references to potentially large bitmap Drawables.
+ * Call {@link #release} to release these references when the Activity is not
+ * visible.
+ */
+// TODO: support for multiple app processes requires a proper android service
+// instead of the shared memory "service" implemented here. Such a service could
+// support continuity between fragments of different applications if desired.
+public final class BackgroundManager {
+ private static final String TAG = "BackgroundManager";
+ private static final boolean DEBUG = false;
+
+ 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;
+
+ /**
+ * Using a separate window for backgrounds can improve graphics performance by
+ * leveraging hardware display layers.
+ * TODO: support a leanback configuration option.
+ */
+ 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();
+
+ private Context mContext;
+ private Handler mHandler;
+ private Window mWindow;
+ private WindowManager mWindowManager;
+ private View mBgView;
+ private BackgroundContinuityService mService;
+ private int mThemeDrawableResourceId;
+
+ private int mHeightPx;
+ private int mWidthPx;
+ private Drawable mBackgroundDrawable;
+ private int mBackgroundColor;
+ private boolean mAttached;
+
+ private class DrawableWrapper {
+ protected int mAlpha;
+ protected Drawable mDrawable;
+ protected ObjectAnimator mAnimator;
+ protected boolean mAnimationPending;
+
+ public DrawableWrapper(Drawable drawable) {
+ mDrawable = drawable;
+ setAlpha(FULL_ALPHA);
+ }
+
+ public Drawable getDrawable() {
+ return mDrawable;
+ }
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ mDrawable.setAlpha(alpha);
+ }
+ public int getAlpha() {
+ return mAlpha;
+ }
+ public void setColor(int color) {
+ ((ColorDrawable) mDrawable).setColor(color);
+ }
+ public void fadeIn(int durationMs, int delayMs) {
+ fade(durationMs, delayMs, FULL_ALPHA);
+ }
+ public void fadeOut(int durationMs) {
+ fade(durationMs, 0, 0);
+ }
+ public void fade(int durationMs, int delayMs, int alpha) {
+ if (mAnimator != null && mAnimator.isStarted()) {
+ mAnimator.cancel();
+ }
+ mAnimator = ObjectAnimator.ofInt(this, "alpha", alpha);
+ mAnimator.setInterpolator(new LinearInterpolator());
+ mAnimator.setDuration(durationMs);
+ mAnimator.setStartDelay(delayMs);
+ mAnimationPending = true;
+ }
+ public boolean isAnimationPending() {
+ return mAnimationPending;
+ }
+ public boolean isAnimationStarted() {
+ return mAnimator != null && mAnimator.isStarted();
+ }
+ public void startAnimation() {
+ mAnimator.start();
+ mAnimationPending = false;
+ }
+ }
+
+ private LayerDrawable mLayerDrawable;
+ private DrawableWrapper mLayerWrapper;
+ private DrawableWrapper mImageInWrapper;
+ private DrawableWrapper mImageOutWrapper;
+ private DrawableWrapper mColorWrapper;
+ private DrawableWrapper mDimWrapper;
+
+ private Drawable mThemeDrawable;
+ private ChangeBackgroundRunnable mChangeRunnable;
+
+ /**
+ * Shared memory continuity service.
+ */
+ private static class BackgroundContinuityService {
+ private static final String TAG = "BackgroundContinuityService";
+ private static boolean DEBUG = BackgroundManager.DEBUG;
+
+ private static BackgroundContinuityService sService = new BackgroundContinuityService();
+
+ private int mColor;
+ private Drawable mDrawable;
+ private int mCount;
+
+ private BackgroundContinuityService() {
+ reset();
+ }
+
+ private void reset() {
+ mColor = Color.TRANSPARENT;
+ mDrawable = null;
+ }
+
+ public static BackgroundContinuityService getInstance() {
+ final int count = sService.mCount++;
+ if (DEBUG) Log.v(TAG, "Returning instance with new count " + count);
+ return sService;
+ }
+
+ public void unref() {
+ if (mCount <= 0) throw new IllegalStateException("Can't unref, count " + mCount);
+ if (--mCount == 0) {
+ if (DEBUG) Log.v(TAG, "mCount is zero, resetting");
+ reset();
+ }
+ }
+ public int getColor() {
+ return mColor;
+ }
+ public Drawable getDrawable() {
+ return mDrawable;
+ }
+ public void setColor(int color) {
+ mColor = color;
+ }
+ public void setDrawable(Drawable drawable) {
+ mDrawable = drawable;
+ }
+ }
+
+ private Drawable getThemeDrawable() {
+ Drawable drawable = null;
+ if (mThemeDrawableResourceId != -1) {
+ drawable = mContext.getResources().getDrawable(mThemeDrawableResourceId);
+ }
+ if (drawable == null) {
+ drawable = createEmptyDrawable();
+ }
+ return drawable;
+ }
+
+ /**
+ * Get the BackgroundManager associated with the Activity.
+ * <p>
+ * The BackgroundManager will be created on-demand for each individual
+ * Activity. Subsequent calls will return the same BackgroundManager created
+ * for this Activity.
+ */
+ public static BackgroundManager getInstance(Activity activity) {
+ BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager()
+ .findFragmentByTag(FRAGMENT_TAG);
+ if (fragment != null) {
+ BackgroundManager manager = fragment.getBackgroundManager();
+ if (manager != null) {
+ return manager;
+ }
+ // manager is null: this is a fragment restored by FragmentManager,
+ // fall through to create a BackgroundManager attach to it.
+ }
+ 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) {
+ mContext = activity;
+ mService = BackgroundContinuityService.getInstance();
+ mHeightPx = mContext.getResources().getDisplayMetrics().heightPixels;
+ mWidthPx = mContext.getResources().getDisplayMetrics().widthPixels;
+ mHandler = new Handler();
+
+ TypedArray ta = activity.getTheme().obtainStyledAttributes(new int[] {
+ android.R.attr.windowBackground });
+ mThemeDrawableResourceId = ta.getResourceId(0, -1);
+ if (mThemeDrawableResourceId < 0) {
+ if (DEBUG) Log.v(TAG, "BackgroundManager no window background resource!");
+ }
+ ta.recycle();
+
+ createFragment(activity);
+ }
+
+ private void createFragment(Activity activity) {
+ // Use a fragment to ensure the background manager gets detached properly.
+ BackgroundFragment fragment = (BackgroundFragment) activity.getFragmentManager()
+ .findFragmentByTag(FRAGMENT_TAG);
+ if (fragment == null) {
+ fragment = new BackgroundFragment();
+ activity.getFragmentManager().beginTransaction().add(fragment, FRAGMENT_TAG).commit();
+ } else {
+ if (fragment.getBackgroundManager() != null) {
+ throw new IllegalStateException("Created duplicated BackgroundManager for same " +
+ "activity, please use getInstance() instead");
+ }
+ }
+ fragment.setBackgroundManager(this);
+ }
+
+ /**
+ * Synchronizes state when the owning Activity is resumed.
+ */
+ void onActivityResume() {
+ if (mService == null) {
+ return;
+ }
+ if (mLayerDrawable == null) {
+ if (DEBUG) Log.v(TAG, "onActivityResume: released state, syncing with service");
+ syncWithService();
+ } else {
+ if (DEBUG) Log.v(TAG, "onActivityResume: updating service color "
+ + mBackgroundColor + " drawable " + mBackgroundDrawable);
+ mService.setColor(mBackgroundColor);
+ mService.setDrawable(mBackgroundDrawable);
+ }
+ }
+
+ private void syncWithService() {
+ int color = mService.getColor();
+ Drawable drawable = mService.getDrawable();
+
+ 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;
+
+ updateImmediate();
+ }
+
+ private void lazyInit() {
+ if (mLayerDrawable != null) {
+ return;
+ }
+
+ mLayerDrawable = (LayerDrawable) mContext.getResources().getDrawable(
+ R.drawable.lb_background);
+ mBgView.setBackground(mLayerDrawable);
+
+ mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable());
+
+ mDimWrapper = new DrawableWrapper(
+ mLayerDrawable.findDrawableByLayerId(R.id.background_dim));
+
+ mLayerWrapper = new DrawableWrapper(mLayerDrawable);
+
+ mColorWrapper = new DrawableWrapper(
+ mLayerDrawable.findDrawableByLayerId(R.id.background_color));
+ }
+
+ /**
+ * Make the background visible on the given Window.
+ */
+ public void attach(Window window) {
+ if (USE_SEPARATE_WINDOW) {
+ attachBehindWindow(window);
+ } else {
+ attachToView(window.getDecorView());
+ }
+ }
+
+ private void attachBehindWindow(Window window) {
+ if (DEBUG) Log.v(TAG, "attachBehindWindow " + window);
+ mWindow = window;
+ mWindowManager = window.getWindowManager();
+
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams(
+ // Media window sits behind the main application window
+ WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA,
+ // Avoid default to software format RGBA
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ android.graphics.PixelFormat.TRANSLUCENT);
+ params.setTitle(WINDOW_NAME);
+ params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+ params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+
+ View backgroundView = LayoutInflater.from(mContext).inflate(
+ R.layout.lb_background_window, null);
+ mWindowManager.addView(backgroundView, params);
+
+ attachToView(backgroundView);
+ }
+
+ private void attachToView(View sceneRoot) {
+ mBgView = sceneRoot;
+ mAttached = true;
+ syncWithService();
+ }
+
+ /**
+ * Release references to Drawables and put the BackgroundManager into the
+ * detached state. Called when the associated Activity is destroyed.
+ * @hide
+ */
+ void detach() {
+ if (DEBUG) Log.v(TAG, "detach");
+ release();
+
+ if (mWindowManager != null && mBgView != null) {
+ mWindowManager.removeViewImmediate(mBgView);
+ }
+
+ mWindowManager = null;
+ mWindow = null;
+ mBgView = null;
+ mAttached = false;
+
+ if (mService != null) {
+ mService.unref();
+ mService = null;
+ }
+ }
+
+ /**
+ * Release references to Drawables. Typically called to reduce memory
+ * overhead when not visible.
+ * <p>
+ * When an Activity is resumed, if the BackgroundManager has not been
+ * released, the continuity service is updated from the BackgroundManager
+ * state. If the BackgroundManager was released, the BackgroundManager
+ * inherits the current state from the continuity service.
+ */
+ public void release() {
+ if (DEBUG) Log.v(TAG, "release");
+ if (mLayerDrawable != null) {
+ mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
+ mLayerDrawable.setDrawableByLayerId(R.id.background_imageout, createEmptyDrawable());
+ mLayerDrawable = null;
+ }
+ mLayerWrapper = null;
+ mImageInWrapper = null;
+ mImageOutWrapper = null;
+ mColorWrapper = null;
+ mDimWrapper = null;
+ mThemeDrawable = null;
+ if (mChangeRunnable != null) {
+ mChangeRunnable.cancel();
+ mChangeRunnable = null;
+ }
+ releaseBackgroundBitmap();
+ }
+
+ private void releaseBackgroundBitmap() {
+ mBackgroundDrawable = null;
+ }
+
+ private void updateImmediate() {
+ lazyInit();
+
+ mColorWrapper.setColor(mBackgroundColor);
+ if (mDimWrapper != null) {
+ mDimWrapper.setAlpha(mBackgroundColor == Color.TRANSPARENT ? 0 : DIM_ALPHA_ON_SOLID);
+ }
+ showWallpaper(mBackgroundColor == Color.TRANSPARENT);
+
+ mThemeDrawable = getThemeDrawable();
+ mLayerDrawable.setDrawableByLayerId(R.id.background_theme, mThemeDrawable);
+
+ if (mBackgroundDrawable == null) {
+ mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
+ } else {
+ if (DEBUG) Log.v(TAG, "Background drawable is available");
+ mImageInWrapper = new DrawableWrapper(mBackgroundDrawable);
+ mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, mBackgroundDrawable);
+ if (mDimWrapper != null) {
+ mDimWrapper.setAlpha(FULL_ALPHA);
+ }
+ }
+ }
+
+ /**
+ * Set the background to the given color. The timing for when this becomes
+ * visible in the app is undefined and may take place after a small delay.
+ */
+ public void setColor(int color) {
+ if (DEBUG) Log.v(TAG, "setColor " + Integer.toHexString(color));
+
+ mBackgroundColor = color;
+ mService.setColor(mBackgroundColor);
+
+ if (mColorWrapper != null) {
+ mColorWrapper.setColor(mBackgroundColor);
+ }
+ }
+
+ /**
+ * Set the given drawable into the background. The provided Drawable will be
+ * used unmodified as the background, without any scaling or cropping
+ * applied to it. The timing for when this becomes visible in the app is
+ * undefined and may take place after a small delay.
+ */
+ public void setDrawable(Drawable drawable) {
+ if (DEBUG) Log.v(TAG, "setBackgroundDrawable " + drawable);
+ setDrawableInternal(drawable);
+ }
+
+ private void setDrawableInternal(Drawable drawable) {
+ if (!mAttached) {
+ throw new IllegalStateException("Must attach before setting background drawable");
+ }
+
+ if (mChangeRunnable != null) {
+ mChangeRunnable.cancel();
+ }
+ mChangeRunnable = new ChangeBackgroundRunnable(drawable);
+
+ mHandler.postDelayed(mChangeRunnable, CHANGE_BG_DELAY_MS);
+ }
+
+ /**
+ * Set the given bitmap into the background. When using setBitmap to set the
+ * background, the provided bitmap will be scaled and cropped to correctly
+ * fit within the dimensions of the view. The timing for when this becomes
+ * visible in the app is undefined and may take place after a small delay.
+ */
+ public void setBitmap(Bitmap bitmap) {
+ if (DEBUG) {
+ Log.v(TAG, "setBitmap " + bitmap);
+ }
+
+ if (bitmap == null) {
+ setDrawableInternal(null);
+ return;
+ }
+
+ if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
+ if (DEBUG) {
+ Log.v(TAG, "invalid bitmap width or height");
+ }
+ return;
+ }
+
+ if (mBackgroundDrawable instanceof BitmapDrawable &&
+ ((BitmapDrawable) mBackgroundDrawable).getBitmap() == bitmap) {
+ if (DEBUG) {
+ Log.v(TAG, "same bitmap detected");
+ }
+ mService.setDrawable(mBackgroundDrawable);
+ return;
+ }
+
+ if (SCALE_BITMAPS_TO_FIT &&
+ (bitmap.getWidth() != mWidthPx || bitmap.getHeight() != mHeightPx)) {
+ // Scale proportionately to fit width and height.
+
+ Matrix matrix = new Matrix();
+
+ int dwidth = bitmap.getWidth();
+ int dheight = bitmap.getHeight();
+ float scale;
+ int dx;
+
+ if (DEBUG) {
+ Log.v(TAG, "original image size " + dwidth + "x" + dheight);
+ }
+
+ 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);
+
+ bitmap = Bitmap.createBitmap(bitmap, dx, 0, subX, subY, matrix, true);
+ if (DEBUG) {
+ Log.v(TAG, "new image size " + bitmap.getWidth() + "x" + bitmap.getHeight());
+ }
+ }
+
+ BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
+
+ setDrawableInternal(bitmapDrawable);
+ }
+
+ private void applyBackgroundChanges() {
+ if (!mAttached || mLayerWrapper == null) {
+ return;
+ }
+
+ if (DEBUG) Log.v(TAG, "applyBackgroundChanges drawable " + mBackgroundDrawable);
+
+ int dimAlpha = 0;
+
+ if (mImageOutWrapper != null && mImageOutWrapper.isAnimationPending()) {
+ if (DEBUG) Log.v(TAG, "mImageOutWrapper animation starting");
+ mImageOutWrapper.startAnimation();
+ mImageOutWrapper = null;
+ dimAlpha = DIM_ALPHA_ON_SOLID;
+ }
+
+ if (mImageInWrapper == null && mBackgroundDrawable != null) {
+ if (DEBUG) Log.v(TAG, "creating new imagein drawable");
+ mImageInWrapper = new DrawableWrapper(mBackgroundDrawable);
+ 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();
+ 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.startAnimation();
+ }
+ }
+
+ /**
+ * Returns the current background color.
+ */
+ public final int getColor() {
+ return mBackgroundColor;
+ }
+
+ /**
+ * Returns the current background {@link Drawable}.
+ */
+ public Drawable getDrawable() {
+ return mBackgroundDrawable;
+ }
+
+ /**
+ * Task which changes the background.
+ */
+ class ChangeBackgroundRunnable implements Runnable {
+ private Drawable mDrawable;
+ private boolean mCancel;
+
+ ChangeBackgroundRunnable(Drawable drawable) {
+ mDrawable = drawable;
+ }
+
+ public void cancel() {
+ mCancel = true;
+ }
+
+ @Override
+ public void run() {
+ if (!mCancel) {
+ runTask();
+ }
+ }
+
+ 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) {
+ return;
+ }
+
+ releaseBackgroundBitmap();
+
+ if (mImageInWrapper != null) {
+ mImageOutWrapper = new DrawableWrapper(mImageInWrapper.getDrawable());
+ mImageOutWrapper.setAlpha(mImageInWrapper.getAlpha());
+ mImageOutWrapper.fadeOut(FADE_DURATION_QUICK);
+
+ // Order is important! Setting a drawable "removes" the
+ // previous one from the view
+ mLayerDrawable.setDrawableByLayerId(R.id.background_imagein, createEmptyDrawable());
+ mLayerDrawable.setDrawableByLayerId(R.id.background_imageout,
+ mImageOutWrapper.getDrawable());
+ mImageInWrapper.setAlpha(0);
+ mImageInWrapper = null;
+ }
+
+ mBackgroundDrawable = mDrawable;
+ mService.setDrawable(mBackgroundDrawable);
+
+ applyBackgroundChanges();
+ }
+ }
+
+ private Drawable createEmptyDrawable() {
+ Bitmap bitmap = null;
+ return new BitmapDrawable(mContext.getResources(), bitmap);
+ }
+
+ private void showWallpaper(boolean show) {
+ if (mWindow == null) {
+ return;
+ }
+
+ WindowManager.LayoutParams layoutParams = mWindow.getAttributes();
+ if (show) {
+ if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) {
+ return;
+ }
+ if (DEBUG) Log.v(TAG, "showing wallpaper");
+ layoutParams.flags |= WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+ } else {
+ if ((layoutParams.flags & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) == 0) {
+ return;
+ }
+ if (DEBUG) Log.v(TAG, "hiding wallpaper");
+ layoutParams.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+ }
+
+ mWindow.setAttributes(layoutParams);
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
new file mode 100644
index 0000000..2be3e54
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/BaseRowFragment.java
@@ -0,0 +1,147 @@
+/*
+ * 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.app.Fragment;
+import android.os.Bundle;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.PresenterSelector;
+import android.support.v17.leanback.widget.ItemBridgeAdapter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.ListRow;
+import android.support.v17.leanback.widget.OnChildSelectedListener;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An internal base class for a fragment containing a list of rows.
+ */
+abstract class BaseRowFragment extends Fragment {
+ private ObjectAdapter mAdapter;
+ private VerticalGridView mVerticalGridView;
+ private PresenterSelector mPresenterSelector;
+ private ItemBridgeAdapter mBridgeAdapter;
+ private int mSelectedPosition = -1;
+
+ abstract protected int getLayoutResourceId();
+
+ private final OnChildSelectedListener mRowSelectedListener = new OnChildSelectedListener() {
+ @Override
+ public void onChildSelected(ViewGroup parent, View view, int position, long id) {
+ onRowSelected(parent, view, position, id);
+ }
+ };
+
+ protected void onRowSelected(ViewGroup parent, View view, int position, long id) {
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ mVerticalGridView = (VerticalGridView) inflater.inflate(getLayoutResourceId(), container, false);
+ return mVerticalGridView;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ if (mBridgeAdapter != null) {
+ mVerticalGridView.setAdapter(mBridgeAdapter);
+ if (mSelectedPosition != -1) {
+ mVerticalGridView.setSelectedPosition(mSelectedPosition);
+ }
+ }
+ mVerticalGridView.setOnChildSelectedListener(mRowSelectedListener);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mVerticalGridView = null;
+ }
+
+ /**
+ * Set the presenter selector used to create and bind views.
+ */
+ public final void setPresenterSelector(PresenterSelector presenterSelector) {
+ mPresenterSelector = presenterSelector;
+ updateAdapter();
+ }
+
+ /**
+ * Get the presenter selector used to create and bind views.
+ */
+ public final PresenterSelector getPresenterSelector() {
+ return mPresenterSelector;
+ }
+
+ /**
+ * Sets the adapter for the fragment.
+ */
+ public final void setAdapter(ObjectAdapter rowsAdapter) {
+ mAdapter = rowsAdapter;
+ updateAdapter();
+ }
+
+ /**
+ * Returns the list of rows.
+ */
+ public final ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Returns the bridge adapter.
+ */
+ protected final ItemBridgeAdapter getBridgeAdapter() {
+ return mBridgeAdapter;
+ }
+
+ /**
+ * Set the selected item position.
+ */
+ public void setSelectedPosition(int position) {
+ mSelectedPosition = position;
+ if(mVerticalGridView != null && mVerticalGridView.getAdapter() != null) {
+ mVerticalGridView.setSelectedPositionSmooth(position);
+ }
+ }
+
+ final VerticalGridView getVerticalGridView() {
+ return mVerticalGridView;
+ }
+
+ protected void updateAdapter() {
+ mBridgeAdapter = null;
+
+ if (mAdapter != null) {
+ // If presenter selector is null, adapter ps will be used
+ mBridgeAdapter = new ItemBridgeAdapter(mAdapter, mPresenterSelector);
+ }
+ if (mVerticalGridView != null) {
+ mVerticalGridView.setAdapter(mBridgeAdapter);
+ if (mBridgeAdapter != null && mSelectedPosition != -1) {
+ mVerticalGridView.setSelectedPosition(mSelectedPosition);
+ }
+ }
+ }
+
+ protected Object getItem(Row row, int position) {
+ if (row instanceof ListRow) {
+ return ((ListRow) row).getAdapter().get(position);
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
new file mode 100644
index 0000000..0df07f4
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseFragment.java
@@ -0,0 +1,660 @@
+/*
+ * 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.R;
+import android.support.v17.leanback.widget.HorizontalGridView;
+import android.support.v17.leanback.widget.Presenter;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.support.v17.leanback.widget.Row;
+import android.support.v17.leanback.widget.ObjectAdapter;
+import android.support.v17.leanback.widget.OnItemSelectedListener;
+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.Fragment;
+import android.content.res.TypedArray;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+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.drawable.Drawable;
+
+import java.util.ArrayList;
+
+import static android.support.v7.widget.RecyclerView.NO_POSITION;
+
+/**
+ * Wrapper fragment for leanback browse screens. Composed of a
+ * RowsFragment and a HeadersFragment.
+ *
+ */
+public class BrowseFragment extends Fragment {
+ private static final String TAG = "BrowseFragment";
+ private static boolean DEBUG = false;
+
+ /** The fastlane navigation panel is enabled and shown by default. */
+ public static final int HEADERS_ENABLED = 1;
+
+ /** The fastlane navigation panel is enabled and hidden by default. */
+ public static final int HEADERS_HIDDEN = 2;
+
+ /** The fastlane navigation panel is disabled and will never be shown. */
+ public static final int HEADERS_DISABLED = 3;
+
+ private RowsFragment mRowsFragment;
+ private HeadersFragment mHeadersFragment;
+
+ private ObjectAdapter mAdapter;
+
+ private Params mParams;
+ private BrowseFrameLayout mBrowseFrame;
+ private ImageView mBadgeView;
+ private TextView mTitleView;
+ private ViewGroup mBrowseTitle;
+ private SearchOrbView mSearchOrbView;
+ private boolean mShowingTitle = true;
+ private boolean mShowingHeaders = true;
+ private boolean mCanShowHeaders = true;
+ private int mContainerListMarginLeft;
+ private int mContainerListAlignTop;
+ private TransitionHelper mTransitionHelper;
+ private OnItemSelectedListener mExternalOnItemSelectedListener;
+ private OnClickListener mExternalOnSearchClickedListener;
+ private OnItemClickedListener mOnItemClickedListener;
+ private int mSelectedPosition = -1;
+
+ // transition related:
+ private static int sReparentHeaderId = View.generateViewId();
+ private Object mSceneWithTitle;
+ private Object mSceneWithoutTitle;
+ private Object mSceneWithHeaders;
+ private Object mSceneWithoutHeaders;
+ private Object mTitleTransition;
+ private Object mHeadersTransition;
+ private int mHeadersTransitionStartDelay;
+
+ private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title";
+ private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge";
+ private static final String ARG_HEADERS_STATE =
+ BrowseFragment.class.getCanonicalName() + ".headersState";
+
+ /**
+ * @param args Bundle to use for the arguments, if null a new Bundle will be created.
+ */
+ 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) {
+ 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.
+ */
+ public void setBrowseParams(Params params) {
+ mParams = params;
+ setBadgeDrawable(mParams.mBadgeDrawable);
+ setTitle(mParams.mTitle);
+ setHeadersState(mParams.mHeadersState);
+ }
+
+ /**
+ * Returns browse parameters.
+ */
+ public Params getBrowseParams() {
+ return mParams;
+ }
+
+ /**
+ * Sets the list of rows for the fragment.
+ */
+ public void setAdapter(ObjectAdapter adapter) {
+ mAdapter = adapter;
+ if (mRowsFragment != null) {
+ mRowsFragment.setAdapter(adapter);
+ mHeadersFragment.setAdapter(adapter);
+ }
+ }
+
+ /**
+ * Returns the list of rows.
+ */
+ public ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mExternalOnItemSelectedListener = listener;
+ }
+
+ /**
+ * 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.
+ */
+ public void setOnItemClickedListener(OnItemClickedListener listener) {
+ mOnItemClickedListener = listener;
+ if (mRowsFragment != null) {
+ mRowsFragment.setOnItemClickedListener(listener);
+ }
+ }
+
+ /**
+ * Returns the item Clicked listener.
+ */
+ 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.
+ */
+ public void setOnSearchClickedListener(View.OnClickListener listener) {
+ mExternalOnSearchClickedListener = listener;
+ if (mSearchOrbView != null) {
+ mSearchOrbView.setOnOrbClickedListener(listener);
+ }
+ }
+
+ private void onHeadersTransitionStart(boolean withHeaders) {
+ mRowsFragment.getVerticalGridView().setAnimateChildLayout(false);
+ mRowsFragment.getVerticalGridView().setFocusSearchDisabled(true);
+ mHeadersFragment.getVerticalGridView().setFocusSearchDisabled(true);
+ createHeadersTransition(withHeaders);
+ }
+
+ private boolean isVerticalScrolling() {
+ // don't run transition
+ return mHeadersFragment.getVerticalGridView().getScrollState()
+ != HorizontalGridView.SCROLL_STATE_IDLE
+ || mRowsFragment.getVerticalGridView().getScrollState()
+ != HorizontalGridView.SCROLL_STATE_IDLE;
+ }
+
+ private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener =
+ new BrowseFrameLayout.OnFocusSearchListener() {
+ @Override
+ public View onFocusSearch(View focused, int direction) {
+ // If fastlane is disabled, just return null.
+ if (!mCanShowHeaders) return null;
+
+ // if fast lane is running transition, focus stays
+ if (mHeadersTransition != null) return focused;
+ if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction);
+ if (direction == View.FOCUS_LEFT) {
+ if (isVerticalScrolling() || mShowingHeaders) {
+ return focused;
+ }
+ return mHeadersFragment.getVerticalGridView();
+ } else if (direction == View.FOCUS_RIGHT) {
+ if (isVerticalScrolling() || !mShowingHeaders) {
+ return focused;
+ }
+ return mRowsFragment.getVerticalGridView();
+ } else if (focused == mSearchOrbView && direction == View.FOCUS_DOWN) {
+ return mShowingHeaders ? mHeadersFragment.getVerticalGridView() :
+ mRowsFragment.getVerticalGridView();
+
+ } else if (focused != mSearchOrbView && mSearchOrbView.getVisibility() == View.VISIBLE
+ && direction == View.FOCUS_UP) {
+ return mSearchOrbView;
+
+ } else {
+ return null;
+ }
+ }
+ };
+
+ private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener =
+ new BrowseFrameLayout.OnChildFocusListener() {
+ @Override
+ public void onRequestChildFocus(View child, View focused) {
+ int childId = child.getId();
+ if (mHeadersTransition != null) return;
+ if (childId == R.id.browse_container_dock && mShowingHeaders) {
+ mShowingHeaders = false;
+ onHeadersTransitionStart(false);
+ mTransitionHelper.runTransition(mSceneWithoutHeaders, mHeadersTransition);
+ } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) {
+ mShowingHeaders = true;
+ //mHeadersFragment.getView().setAlpha(1f);
+ onHeadersTransitionStart(true);
+ mTransitionHelper.runTransition(mSceneWithHeaders, mHeadersTransition);
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme);
+ mContainerListMarginLeft = (int) ta.getDimension(
+ R.styleable.LeanbackTheme_browseRowsMarginStart, 0);
+ mContainerListAlignTop = (int) ta.getDimension(
+ R.styleable.LeanbackTheme_browseRowsMarginTop, 0);
+ ta.recycle();
+ mHeadersTransitionStartDelay = getResources()
+ .getInteger(R.integer.lb_browse_headers_transition_delay);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) {
+ mRowsFragment = new RowsFragment();
+ mHeadersFragment = new HeadersFragment();
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.browse_headers_dock, mHeadersFragment)
+ .replace(R.id.browse_container_dock, mRowsFragment).commit();
+ } else {
+ mHeadersFragment = (HeadersFragment) getChildFragmentManager()
+ .findFragmentById(R.id.browse_headers_dock);
+ mRowsFragment = (RowsFragment) getChildFragmentManager()
+ .findFragmentById(R.id.browse_container_dock);
+ }
+ mRowsFragment.setAdapter(mAdapter);
+ mHeadersFragment.setAdapter(mAdapter);
+
+ mRowsFragment.setOnItemSelectedListener(mRowSelectedListener);
+ mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener);
+ mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener);
+ mRowsFragment.setOnItemClickedListener(mOnItemClickedListener);
+
+ View root = inflater.inflate(R.layout.lb_browse_fragment, container, false);
+
+ mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame);
+ 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);
+ if (mExternalOnSearchClickedListener != null) {
+ mSearchOrbView.setOnOrbClickedListener(mExternalOnSearchClickedListener);
+ }
+
+ readArguments(getArguments());
+ if (mParams != null) {
+ setBadgeDrawable(mParams.mBadgeDrawable);
+ setTitle(mParams.mTitle);
+ setHeadersState(mParams.mHeadersState);
+ }
+
+ mTransitionHelper = new TransitionHelper(getActivity());
+ mSceneWithTitle = mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ @Override
+ public void run() {
+ showTitle(true);
+ }
+ });
+ mSceneWithoutTitle = mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ @Override
+ public void run() {
+ showTitle(false);
+ }
+ });
+ mSceneWithHeaders = mTransitionHelper.createScene(mBrowseFrame, new Runnable() {
+ @Override
+ public void run() {
+ showHeaders(true);
+ }
+ });
+ mSceneWithoutHeaders = mTransitionHelper.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);
+
+ 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>();
+
+ 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);
+ }
+
+ 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() {
+ @Override
+ public void run() {
+ 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()) {
+ rowsGridView.requestFocus();
+ }
+ }
+ VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView();
+ if (headerGridView != null) {
+ headerGridView.setFocusSearchDisabled(false);
+ headerGridView.invalidate();
+ if (mShowingHeaders && !headerGridView.hasFocus()) {
+ headerGridView.requestFocus();
+ }
+ }
+ }
+ });
+ }
+
+ private void showTitle(boolean show) {
+ mBrowseTitle.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
+ private void showHeaders(boolean show) {
+ if (DEBUG) Log.v(TAG, "showHeaders " + show);
+ mHeadersFragment.setHeadersVisiblity(show);
+
+ View containerList = mRowsFragment.getView();
+ MarginLayoutParams lp;
+ lp = (MarginLayoutParams) containerList.getLayoutParams();
+ lp.leftMargin = show ? mContainerListMarginLeft : 0;
+ containerList.setLayoutParams(lp);
+ mRowsFragment.setExpand(!show);
+ }
+
+ private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener =
+ new HeadersFragment.OnHeaderClickedListener() {
+ @Override
+ public void onHeaderClicked() {
+ if (!mCanShowHeaders || !mShowingHeaders) return;
+
+ if (mHeadersTransition != null) {
+ return;
+ }
+ mShowingHeaders = false;
+ onHeadersTransitionStart(false);
+ mTransitionHelper.runTransition(mSceneWithoutHeaders, mHeadersTransition);
+ mRowsFragment.getVerticalGridView().requestFocus();
+ }
+ };
+
+ private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(Object item, Row row) {
+ int position = mRowsFragment.getVerticalGridView().getSelectedPosition();
+ if (DEBUG) Log.v(TAG, "row selected position " + position);
+ onRowSelected(position);
+ if (mExternalOnItemSelectedListener != null) {
+ mExternalOnItemSelectedListener.onItemSelected(item, row);
+ }
+ }
+ };
+
+ private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(Object item, Row row) {
+ int position = mHeadersFragment.getVerticalGridView().getSelectedPosition();
+ if (DEBUG) Log.v(TAG, "header selected position " + position);
+ onRowSelected(position);
+ }
+ };
+
+ private void onRowSelected(int position) {
+ if (position != mSelectedPosition) {
+ mSetSelectionRunnable.mPosition = position;
+ mBrowseFrame.getHandler().post(mSetSelectionRunnable);
+
+ if (position == 0) {
+ if (!mShowingTitle) {
+ mTransitionHelper.runTransition(mSceneWithTitle, mTitleTransition);
+ mShowingTitle = true;
+ }
+ } else if (mShowingTitle) {
+ mTransitionHelper.runTransition(mSceneWithoutTitle, mTitleTransition);
+ mShowingTitle = false;
+ }
+ }
+ }
+
+ private class SetSelectionRunnable implements Runnable {
+ int mPosition;
+ @Override
+ public void run() {
+ setSelection(mPosition);
+ }
+ }
+
+ private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable();
+
+ private void setSelection(int position) {
+ if (position != NO_POSITION) {
+ mRowsFragment.setSelectedPosition(position);
+ mHeadersFragment.setSelectedPosition(position);
+ }
+ 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();
+ if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) {
+ mHeadersFragment.getView().requestFocus();
+ } else if ((!mCanShowHeaders || !mShowingHeaders)
+ && mRowsFragment.getView() != null) {
+ mRowsFragment.getView().requestFocus();
+ }
+ showHeaders(mCanShowHeaders && mShowingHeaders);
+ }
+
+ private void readArguments(Bundle args) {
+ if (args == null) {
+ return;
+ }
+ 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);
+ }
+ }
+
+ private void setTitle(String title) {
+ if (mTitleView != null) {
+ mTitleView.setText(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;
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseFrameLayout.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseFrameLayout.java
new file mode 100644
index 0000000..4f04b05
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseFrameLayout.java
@@ -0,0 +1,77 @@
+/*
+ * 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.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * Top level implementation viewgroup for browse to manage transitions between
+ * browse sub fragments.
+ *
+ */
+class BrowseFrameLayout extends FrameLayout {
+
+ public interface OnFocusSearchListener {
+ public View onFocusSearch(View focused, int direction);
+ }
+
+ public interface OnChildFocusListener {
+ public void onRequestChildFocus(View child, View focused);
+ }
+
+ public BrowseFrameLayout(Context context) {
+ this(context, null, 0);
+ }
+
+ public BrowseFrameLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BrowseFrameLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ private OnFocusSearchListener mListener;
+ private OnChildFocusListener mOnChildFocusListener;
+
+ public void setOnFocusSearchListener(OnFocusSearchListener listener) {
+ mListener = listener;
+ }
+
+ public void setOnChildFocusListener(OnChildFocusListener listener) {
+ mOnChildFocusListener = listener;
+ }
+
+ @Override
+ public View focusSearch(View focused, int direction) {
+ if (mListener != null) {
+ View view = mListener.onFocusSearch(focused, direction);
+ if (view != null) {
+ return view;
+ }
+ }
+ return super.focusSearch(focused, direction);
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ super.requestChildFocus(child, focused);
+ if (mOnChildFocusListener != null) {
+ mOnChildFocusListener.onRequestChildFocus(child, focused);
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/BrowseRowsFrameLayout.java b/v17/leanback/src/android/support/v17/leanback/app/BrowseRowsFrameLayout.java
new file mode 100644
index 0000000..3f10a63
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/BrowseRowsFrameLayout.java
@@ -0,0 +1,53 @@
+/*
+ * 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.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * Customized FrameLayout excludes margin of child from calculating the child size.
+ * So we can change left margin of rows while keep the width of rows unchanged without
+ * using hardcoded DIPS.
+ */
+class BrowseRowsFrameLayout extends FrameLayout {
+
+ public BrowseRowsFrameLayout(Context context) {
+ this(context ,null);
+ }
+
+ public BrowseRowsFrameLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public BrowseRowsFrameLayout(Context context, AttributeSet attrs,
+ int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void measureChildWithMargins(View child,
+ int parentWidthMeasureSpec, int widthUsed,
+ int parentHeightMeasureSpec, int heightUsed) {
+ final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+ final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+ getPaddingLeft() + getPaddingRight() + widthUsed, lp.width);
+ final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+ getPaddingTop() + getPaddingBottom() + heightUsed, lp.height);
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
new file mode 100644
index 0000000..9f426d3
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/DetailsFragment.java
@@ -0,0 +1,144 @@
+/*
+ * 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.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.Row;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.util.Log;
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Wrapper fragment for leanback details screens.
+ */
+public class DetailsFragment extends Fragment {
+ private static final String TAG = "DetailsFragment";
+ private static boolean DEBUG = false;
+
+ private RowsFragment mRowsFragment;
+
+ private ObjectAdapter mAdapter;
+ private int mContainerListAlignTop;
+ private OnItemSelectedListener mExternalOnItemSelectedListener;
+ private OnItemClickedListener mOnItemClickedListener;
+ private int mSelectedPosition = -1;
+
+ /**
+ * Sets the list of rows for the fragment.
+ */
+ public void setAdapter(ObjectAdapter adapter) {
+ mAdapter = adapter;
+ if (mRowsFragment != null) {
+ mRowsFragment.setAdapter(adapter);
+ }
+ }
+
+ /**
+ * Returns the list of rows.
+ */
+ public ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mExternalOnItemSelectedListener = listener;
+ }
+
+ /**
+ * Sets an item Clicked listener.
+ */
+ public void setOnItemClickedListener(OnItemClickedListener listener) {
+ mOnItemClickedListener = listener;
+ if (mRowsFragment != null) {
+ mRowsFragment.setOnItemClickedListener(listener);
+ }
+ }
+
+ /**
+ * Returns the item Clicked listener.
+ */
+ public OnItemClickedListener getOnItemClickedListener() {
+ return mOnItemClickedListener;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mContainerListAlignTop =
+ getResources().getDimensionPixelSize(R.dimen.lb_details_rows_align_top);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.lb_details_fragment, container, false);
+ mRowsFragment = (RowsFragment) getChildFragmentManager().findFragmentById(
+ R.id.fragment_dock);
+ if (mRowsFragment == null) {
+ mRowsFragment = new RowsFragment();
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.fragment_dock, mRowsFragment).commit();
+ }
+ mRowsFragment.setAdapter(mAdapter);
+ mRowsFragment.setOnItemSelectedListener(mRowSelectedListener);
+ mRowsFragment.setOnItemClickedListener(mOnItemClickedListener);
+ return view;
+ }
+
+ private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(Object item, Row row) {
+ if (mExternalOnItemSelectedListener != null) {
+ mExternalOnItemSelectedListener.onItemSelected(item, row);
+ }
+ }
+ };
+
+ private void setVerticalGridViewLayout(VerticalGridView listview) {
+ // align the top edge of item to a fixed position
+ listview.setItemAlignmentOffset(0);
+ listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ listview.setWindowAlignmentOffset(mContainerListAlignTop);
+ 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
+ * DetailsFragment.
+ */
+ private void setupChildFragmentLayout() {
+ VerticalGridView containerList = mRowsFragment.getVerticalGridView();
+ setVerticalGridViewLayout(containerList);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ setupChildFragmentLayout();
+ mRowsFragment.getView().requestFocus();
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java b/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
new file mode 100644
index 0000000..7cb5ce1
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/HeadersFragment.java
@@ -0,0 +1,163 @@
+/*
+ * 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.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.VerticalGridView;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.List;
+
+/**
+ * An internal fragment containing a list of row headers.
+ */
+public class HeadersFragment extends BaseRowFragment {
+
+ interface OnHeaderClickedListener {
+ void onHeaderClicked();
+ }
+
+ private OnItemSelectedListener mOnItemSelectedListener;
+ private OnHeaderClickedListener mOnHeaderClickedListener;
+ private boolean mShow = true;
+
+ private static final Presenter sHeaderPresenter = new RowHeaderPresenter();
+
+ public HeadersFragment() {
+ setPresenterSelector(new PresenterSelector() {
+ @Override
+ public Presenter getPresenter(Object item) {
+ return sHeaderPresenter;
+ }
+ });
+ }
+
+ public void setOnHeaderClickedListener(OnHeaderClickedListener listener) {
+ mOnHeaderClickedListener = listener;
+ }
+
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ }
+
+ @Override
+ protected void onRowSelected(ViewGroup parent, View view, int position, long id) {
+ if (mOnItemSelectedListener != null) {
+ if (position >= 0) {
+ Row row = (Row) getAdapter().get(position);
+ mOnItemSelectedListener.onItemSelected(null, row);
+ }
+ }
+ }
+
+ private final ItemBridgeAdapter.AdapterListener mAdapterListener =
+ new ItemBridgeAdapter.AdapterListener() {
+ @Override
+ public void onCreate(ItemBridgeAdapter.ViewHolder viewHolder) {
+ View headerView = viewHolder.getViewHolder().view;
+ headerView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mOnHeaderClickedListener != null) {
+ mOnHeaderClickedListener.onHeaderClicked();
+ }
+ }
+ });
+ headerView.setFocusable(true);
+ headerView.setFocusableInTouchMode(true);
+ }
+
+ @Override
+ public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
+ View headerView = viewHolder.getViewHolder().view;
+ headerView.setVisibility(mShow ? View.VISIBLE : View.INVISIBLE);
+ }
+ };
+
+ @Override
+ protected int getLayoutResourceId() {
+ return R.layout.lb_headers_fragment;
+ }
+
+ @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());
+ }
+ }
+
+ void setHeadersVisiblity(boolean show) {
+ mShow = show;
+ 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);
+ }
+ }
+ }
+
+ @Override
+ protected void updateAdapter() {
+ super.updateAdapter();
+ ItemBridgeAdapter adapter = getBridgeAdapter();
+ if (adapter != null) {
+ adapter.setAdapterListener(mAdapterListener);
+ }
+ if (adapter != null && getVerticalGridView() != null) {
+ FocusHighlightHelper.setupHeaderItemFocusHighlight(getVerticalGridView());
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
new file mode 100644
index 0000000..a97c5d8
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/RowsFragment.java
@@ -0,0 +1,326 @@
+/*
+ * 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.TimeAnimator;
+import android.animation.TimeAnimator.TimeListener;
+import android.graphics.Canvas;
+import android.os.Bundle;
+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.RowPresenter.ViewHolder;
+import android.support.v17.leanback.widget.VerticalGridView;
+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.Presenter;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import java.util.List;
+
+/**
+ * An ordered set of rows of leanback widgets.
+ */
+public class RowsFragment extends BaseRowFragment {
+
+ /**
+ * Internal helper class that manages row select animation and apply a default
+ * dim to each row.
+ */
+ final class RowViewHolderExtra implements TimeListener {
+ final RowPresenter mRowPresenter;
+ final Presenter.ViewHolder mRowViewHolder;
+
+ final TimeAnimator mSelectAnimator = new TimeAnimator();
+ final ColorOverlayDimmer mColorDimmer;
+ int mSelectAnimatorDurationInUse;
+ Interpolator mSelectAnimatorInterpolatorInUse;
+ float mSelectLevelAnimStart;
+ float mSelectLevelAnimDelta;
+
+ RowViewHolderExtra(ItemBridgeAdapter.ViewHolder ibvh) {
+ mRowPresenter = (RowPresenter) ibvh.getPresenter();
+ mRowViewHolder = ibvh.getViewHolder();
+ mSelectAnimator.setTimeListener(this);
+ if (mRowPresenter.getSelectEffectEnabled()
+ && mRowPresenter.isUsingDefaultSelectEffect()) {
+ mColorDimmer = ColorOverlayDimmer.createDefault(ibvh.itemView.getContext());
+ } else {
+ mColorDimmer = null;
+ }
+ }
+
+ @Override
+ public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
+ float fraction;
+ if (totalTime >= mSelectAnimatorDurationInUse) {
+ fraction = 1;
+ mSelectAnimator.end();
+ } else {
+ fraction = (float) (totalTime / (double) mSelectAnimatorDurationInUse);
+ }
+ if (mSelectAnimatorInterpolatorInUse != null) {
+ fraction = mSelectAnimatorInterpolatorInUse.getInterpolation(fraction);
+ }
+ float level = mSelectLevelAnimStart + fraction * mSelectLevelAnimDelta;
+ if (mColorDimmer != null) {
+ mColorDimmer.setActiveLevel(level);
+ }
+ mRowPresenter.setSelectLevel(mRowViewHolder, level);
+ }
+
+ void animateSelect(boolean select, boolean immediate) {
+ endAnimation();
+ final float end = select ? 1 : 0;
+ if (immediate) {
+ mRowPresenter.setSelectLevel(mRowViewHolder, end);
+ if (mColorDimmer != null) {
+ mColorDimmer.setActiveLevel(end);
+ }
+ } else if (mRowPresenter.getSelectLevel(mRowViewHolder) != end) {
+ mSelectAnimatorDurationInUse = mSelectAnimatorDuration;
+ mSelectAnimatorInterpolatorInUse = mSelectAnimatorInterpolator;
+ mSelectLevelAnimStart = mRowPresenter.getSelectLevel(mRowViewHolder);
+ mSelectLevelAnimDelta = end - mSelectLevelAnimStart;
+ mSelectAnimator.start();
+ }
+ }
+
+ void endAnimation() {
+ mSelectAnimator.end();
+ }
+
+ void drawDimForSelection(Canvas c) {
+ if (mColorDimmer != null) {
+ mColorDimmer.drawColorOverlay(c, mRowViewHolder.view, false);
+ }
+ }
+ }
+
+ private static final String TAG = "RowsFragment";
+ private static final boolean DEBUG = false;
+
+ private ItemBridgeAdapter.ViewHolder mSelectedViewHolder;
+ private boolean mExpand = true;
+ private boolean mViewsCreated;
+
+ private OnItemSelectedListener mOnItemSelectedListener;
+ private OnItemClickedListener mOnItemClickedListener;
+
+ // Select animation and interpolator are not intended to exposed at this moment.
+ // They might be synced with vertical scroll animation later.
+ int mSelectAnimatorDuration;
+ Interpolator mSelectAnimatorInterpolator = new DecelerateInterpolator(2);
+
+ /**
+ * 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.
+ */
+ public void setOnItemClickedListener(OnItemClickedListener listener) {
+ mOnItemClickedListener = listener;
+ if (mViewsCreated) {
+ throw new IllegalStateException(
+ "Item clicked listener must be set before views are created");
+ }
+ }
+
+ /**
+ * Returns the item clicked listener.
+ */
+ public OnItemClickedListener getOnItemClickedListener() {
+ return mOnItemClickedListener;
+ }
+
+ /**
+ * 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++) {
+ View view = listView.getChildAt(i);
+ ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) listView.getChildViewHolder(view);
+ setRowViewExpanded(vh, mExpand);
+ }
+ }
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = 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);
+ setOnItemSelectedListener(vh, mOnItemSelectedListener);
+ }
+ }
+ }
+
+ @Override
+ protected void onRowSelected(ViewGroup parent, View view, int position, long id) {
+ VerticalGridView listView = getVerticalGridView();
+ if (listView == null) {
+ return;
+ }
+ ItemBridgeAdapter.ViewHolder vh = (view == null) ? null :
+ (ItemBridgeAdapter.ViewHolder) listView.getChildViewHolder(view);
+
+ if (mSelectedViewHolder != vh) {
+ if (DEBUG) Log.v(TAG, "new row selected position " + position + " view " + view);
+
+ if (mSelectedViewHolder != null) {
+ setRowViewSelected(mSelectedViewHolder, false, false);
+ }
+ mSelectedViewHolder = vh;
+ if (mSelectedViewHolder != null) {
+ setRowViewSelected(mSelectedViewHolder, true, false);
+ }
+ }
+ }
+
+ @Override
+ protected int getLayoutResourceId() {
+ return R.layout.lb_rows_fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mSelectAnimatorDuration = getResources().getInteger(R.integer.lb_browse_rows_anim_duration);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ if (DEBUG) Log.v(TAG, "onViewCreated");
+ super.onViewCreated(view, savedInstanceState);
+ getVerticalGridView().setItemAlignmentViewId(R.id.row_content);
+ getVerticalGridView().addItemDecoration(mItemDecoration);
+ }
+
+ private RecyclerView.ItemDecoration mItemDecoration = new RecyclerView.ItemDecoration() {
+ @Override
+ public void onDrawOver(Canvas c, RecyclerView parent) {
+ final int count = parent.getChildCount();
+ for (int i = 0; i < count; i++) {
+ ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder)
+ parent.getViewHolderForChildAt(i);
+ RowViewHolderExtra extra = (RowViewHolderExtra) ibvh.getExtraObject();
+ extra.drawDimForSelection(c);
+ }
+ }
+ };
+
+ private static void setRowViewExpanded(ItemBridgeAdapter.ViewHolder vh, boolean expanded) {
+ ((RowPresenter) vh.getPresenter()).setRowViewExpanded(vh.getViewHolder(), expanded);
+ }
+
+ private static void setRowViewSelected(ItemBridgeAdapter.ViewHolder vh, boolean selected,
+ boolean immediate) {
+ RowViewHolderExtra extra = (RowViewHolderExtra) vh.getExtraObject();
+ extra.animateSelect(selected, immediate);
+ ((RowPresenter) vh.getPresenter()).setRowViewSelected(vh.getViewHolder(), selected);
+ }
+
+ private static void setOnItemSelectedListener(ItemBridgeAdapter.ViewHolder vh,
+ OnItemSelectedListener listener) {
+ ((RowPresenter) vh.getPresenter()).setOnItemSelectedListener(listener);
+ }
+
+ private final ItemBridgeAdapter.AdapterListener mBridgeAdapterListener =
+ new ItemBridgeAdapter.AdapterListener() {
+ @Override
+ public void onAddPresenter(Presenter presenter) {
+ ((RowPresenter) presenter).setOnItemClickedListener(mOnItemClickedListener);
+ }
+ @Override
+ public void onCreate(ItemBridgeAdapter.ViewHolder vh) {
+ Presenter rowPresenter = vh.getPresenter();
+ VerticalGridView listView = getVerticalGridView();
+ if (listView != null && ((RowPresenter) vh.getPresenter()).canDrawOutOfBounds()) {
+ listView.setClipChildren(false);
+ }
+ mViewsCreated = true;
+ vh.setExtraObject(new RowViewHolderExtra(vh));
+ // selected state is initialized to false, then driven by grid view onChildSelected
+ // events. When there is rebind, grid view fires onChildSelected event properly.
+ // So we don't need do anything special later in onBind or onAttachedToWindow.
+ setRowViewSelected(vh, false, true);
+ }
+ @Override
+ public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) {
+ if (DEBUG) Log.v(TAG, "onAttachToWindow");
+ // All views share the same mExpand value. When we attach a view to grid view,
+ // we should make sure it pick up the latest mExpand value we set early on other
+ // attached views. For no-structure-change update, the view is rebound to new data,
+ // but again it should use the unchanged mExpand value, so we don't need do any
+ // thing in onBind.
+ setRowViewExpanded(vh, mExpand);
+ setOnItemSelectedListener(vh, mOnItemSelectedListener);
+ }
+ @Override
+ public void onUnbind(ItemBridgeAdapter.ViewHolder vh) {
+ RowViewHolderExtra extra = (RowViewHolderExtra) vh.getExtraObject();
+ extra.endAnimation();
+ }
+ };
+
+ @Override
+ protected void updateAdapter() {
+ super.updateAdapter();
+ mSelectedViewHolder = null;
+ mViewsCreated = false;
+
+ ItemBridgeAdapter adapter = getBridgeAdapter();
+ if (adapter != null) {
+ adapter.setAdapterListener(mBridgeAdapterListener);
+ }
+ }
+
+ 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
new file mode 100644
index 0000000..9720634
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
@@ -0,0 +1,261 @@
+/*
+ * 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.app.Fragment;
+import android.os.Bundle;
+import android.os.Handler;
+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.Row;
+import android.support.v17.leanback.widget.SearchBar;
+import android.support.v17.leanback.widget.VerticalGridView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.support.v17.leanback.R;
+
+/**
+ * A fragment to handle searches
+ */
+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";
+
+ /**
+ * Search API exposed to application
+ */
+ public static interface SearchResultProvider {
+ /**
+ * <p>Method invoked some time prior to the first call to onQueryTextChange to retrieve
+ * an ObjectAdapter that will contain the results to future updates of the search query.</p>
+ *
+ * <p>As results are retrieved, the application should use the data set notification methods
+ * on the ObjectAdapter to instruct the SearchFragment to update the results.</p>
+ *
+ * @return ObjectAdapter The result object adapter.
+ */
+ public ObjectAdapter getResultsAdapter();
+
+ /**
+ * <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>
+ *
+ * @param newQuery The current search query.
+ * @return whether the results changed or not.
+ */
+ public boolean onQueryTextChange(String newQuery);
+
+ /**
+ * 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
+ */
+ public boolean onQueryTextSubmit(String query);
+ }
+
+ private RowsFragment mRowsFragment;
+ private final Handler mHandler = new Handler();
+
+ private SearchBar mSearchBar;
+ private SearchResultProvider mProvider;
+ private String mPendingQuery = null;
+
+ private OnItemSelectedListener mOnItemSelectedListener;
+ private OnItemClickedListener mOnItemClickedListener;
+ private ObjectAdapter mResultAdapter;
+
+ /**
+ * @param args Bundle to use for the arguments, if null a new Bundle will be created.
+ */
+ public static Bundle createArgs(Bundle args, String query) {
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putString(ARG_QUERY, query);
+ return args;
+ }
+
+ /**
+ * Create a search fragment with a given search query to start with
+ *
+ * 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
+ */
+ public static SearchFragment newInstance(String query) {
+ SearchFragment fragment = new SearchFragment();
+ Bundle args = createArgs(null, query);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.lb_search_fragment, container, false);
+
+ FrameLayout searchFrame = (FrameLayout) root.findViewById(R.id.lb_search_frame);
+ mSearchBar = (SearchBar) searchFrame.findViewById(R.id.lb_search_bar);
+ mSearchBar.setSearchBarListener(new SearchBar.SearchBarListener() {
+ @Override
+ public void onSearchQueryChange(String query) {
+ if (DEBUG) Log.v(TAG, String.format("onSearchQueryChange %s", query));
+ if (null != mProvider) {
+ retrieveResults(query);
+ } else {
+ mPendingQuery = query;
+ }
+ }
+
+ @Override
+ public void onSearchQuerySubmit(String query) {
+ if (DEBUG) Log.v(TAG, String.format("onSearchQuerySubmit %s", query));
+ mRowsFragment.setSelectedPosition(0);
+ mRowsFragment.getVerticalGridView().requestFocus();
+ if (null != mProvider) {
+ mProvider.onQueryTextSubmit(query);
+ }
+ }
+
+ @Override
+ public void onKeyboardDismiss(String query) {
+ mRowsFragment.setSelectedPosition(0);
+ mRowsFragment.getVerticalGridView().requestFocus();
+ }
+ });
+
+ Bundle args = getArguments();
+ if (null != args) {
+ String query = args.getString(ARG_QUERY, "");
+ mSearchBar.setSearchQuery(query);
+ }
+
+ // Inject the RowsFragment in the results container
+ if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) {
+ mRowsFragment = new RowsFragment();
+ getChildFragmentManager().beginTransaction()
+ .replace(R.id.lb_results_frame, mRowsFragment).commit();
+ } else {
+ mRowsFragment = (RowsFragment) getChildFragmentManager()
+ .findFragmentById(R.id.browse_container_dock);
+ }
+ mRowsFragment.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(Object item, 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);
+ }
+ }
+ });
+ mRowsFragment.setOnItemClickedListener(new OnItemClickedListener() {
+ @Override
+ public void onItemClicked(Object item, Row row) {
+ if (null != mOnItemClickedListener) {
+ mOnItemClickedListener.onItemClicked(item, row);
+ }
+ }
+ });
+ mRowsFragment.setExpand(true);
+ if (null != mProvider) {
+ onSetSearchResultProvider();
+ }
+ return root;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ VerticalGridView list = mRowsFragment.getVerticalGridView();
+ int mContainerListAlignTop =
+ getResources().getDimensionPixelSize(R.dimen.lb_search_browse_rows_align_top);
+ list.setItemAlignmentOffset(0);
+ list.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED);
+ list.setWindowAlignmentOffset(mContainerListAlignTop);
+ list.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED);
+ list.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE);
+ }
+
+ /**
+ * Set the search provider, which is responsible for returning items given
+ * a search term
+ *
+ * @param searchResultProvider the search provider
+ */
+ public void setSearchResultProvider(SearchResultProvider searchResultProvider) {
+ mProvider = searchResultProvider;
+ onSetSearchResultProvider();
+ }
+
+ /**
+ * Sets an item selection listener.
+ * @param listener the item selection listener
+ */
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ }
+
+ /**
+ * Sets an item clicked listener.
+ */
+ public void setOnItemClickedListener(OnItemClickedListener listener) {
+ mOnItemClickedListener = listener;
+ }
+
+ private void retrieveResults(String searchQuery) {
+ if (DEBUG) Log.v(TAG, String.format("retrieveResults %s", searchQuery));
+ mProvider.onQueryTextChange(searchQuery);
+ }
+
+ private void onSetSearchResultProvider() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ // Retrieve the result adapter
+ mResultAdapter = mProvider.getResultsAdapter();
+ if (null != mRowsFragment) {
+ mRowsFragment.setAdapter(mResultAdapter);
+ executePendingQuery();
+ }
+ }
+ });
+ }
+
+ private void executePendingQuery() {
+ if (null != mPendingQuery && null != mResultAdapter) {
+ String query = mPendingQuery;
+ mPendingQuery = null;
+ retrieveResults(query);
+ }
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/TransitionHelper.java b/v17/leanback/src/android/support/v17/leanback/app/TransitionHelper.java
new file mode 100644
index 0000000..78c2766
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/TransitionHelper.java
@@ -0,0 +1,373 @@
+/*
+ * 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.content.Context;
+import android.os.Build;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Helper for view transitions.
+ */
+final class TransitionHelper {
+
+ public static final int FADE_IN = 0x1;
+ public static final int FADE_OUT = 0x2;
+
+ TransitionHelperVersionImpl mImpl;
+
+ /**
+ * Gets whether the system supports Transition animations.
+ *
+ * @return True if Transition animations are supported.
+ */
+ public static boolean systemSupportsTransitions() {
+ if (Build.VERSION.SDK_INT >= 19) {
+ // Supported on Android 4.4 or later.
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Interface implemented by classes that support Transition animations.
+ */
+ static interface TransitionHelperVersionImpl {
+
+ public Object createScene(ViewGroup sceneRoot, Runnable r);
+
+ public Object createAutoTransition();
+
+ public Object createFadeTransition(int fadingMode);
+
+ public Object createChangeBounds(boolean reparent);
+
+ public void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay);
+
+ public void setChangeBoundsStartDelay(Object changeBounds, int viewId, int startDelay);
+
+ public void setChangeBoundsStartDelay(Object changeBounds, String className,
+ int startDelay);
+
+ public void setChangeBoundsDefaultStartDelay(Object changeBounds, int startDelay);
+
+ public Object createTransitionSet(boolean sequential);
+
+ public void addTransition(Object transitionSet, Object transition);
+
+ public void setTransitionCompleteListener(Object transition, Runnable listener);
+
+ public void runTransition(Object scene, Object transition);
+
+ public void exclude(Object transition, int targetId, boolean exclude);
+
+ public void exclude(Object transition, View targetView, boolean exclude);
+
+ public void excludeChildren(Object transition, int targetId, boolean exclude);
+
+ public void excludeChildren(Object transition, View target, boolean exclude);
+
+ public void include(Object transition, int targetId);
+
+ public void include(Object transition, View targetView);
+
+ }
+
+ /**
+ * Interface used when we do not support Transition animations.
+ */
+ private static final class TransitionHelperStubImpl implements TransitionHelperVersionImpl {
+
+ private static class TransitionStub {
+ Runnable mCompleteListener;
+ }
+
+ @Override
+ public Object createScene(ViewGroup sceneRoot, Runnable r) {
+ return r;
+ }
+
+ @Override
+ public Object createAutoTransition() {
+ return new TransitionStub();
+ }
+
+ @Override
+ public Object createFadeTransition(int fadingMode) {
+ return new TransitionStub();
+ }
+
+ @Override
+ public Object createChangeBounds(boolean reparent) {
+ return new TransitionStub();
+ }
+
+ @Override
+ public void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay) {
+ }
+
+ @Override
+ public void setChangeBoundsStartDelay(Object changeBounds, int viewId, int startDelay) {
+ }
+
+ @Override
+ public void setChangeBoundsStartDelay(Object changeBounds, String className,
+ int startDelay) {
+ }
+
+ @Override
+ public void setChangeBoundsDefaultStartDelay(Object changeBounds, int startDelay) {
+ }
+
+ @Override
+ public Object createTransitionSet(boolean sequential) {
+ return new TransitionStub();
+ }
+
+ @Override
+ public void addTransition(Object transitionSet, Object transition) {
+ }
+
+ @Override
+ public void exclude(Object transition, int targetId, boolean exclude) {
+ }
+
+ @Override
+ public void exclude(Object transition, View targetView, boolean exclude) {
+ }
+
+ @Override
+ public void excludeChildren(Object transition, int targetId, boolean exclude) {
+ }
+
+ @Override
+ public void excludeChildren(Object transition, View targetView, boolean exclude) {
+ }
+
+ @Override
+ public void include(Object transition, int targetId) {
+ }
+
+ @Override
+ public void include(Object transition, View targetView) {
+ }
+
+ @Override
+ public void setTransitionCompleteListener(Object transition, Runnable listener) {
+ ((TransitionStub) transition).mCompleteListener = listener;
+ }
+
+ @Override
+ public void runTransition(Object scene, Object transition) {
+ Runnable r = ((Runnable) scene);
+ if (r != null) {
+ r.run();
+ }
+ TransitionStub transitionStub = (TransitionStub) transition;
+ if (transitionStub != null && transitionStub.mCompleteListener != null) {
+ transitionStub.mCompleteListener.run();
+ }
+ }
+ }
+
+ /**
+ * Implementation used on KitKat (and above).
+ */
+ private static final class TransitionHelperKitkatImpl implements TransitionHelperVersionImpl {
+ private final TransitionHelperKitkat mTransitionHelper;
+
+ TransitionHelperKitkatImpl(Context context) {
+ mTransitionHelper = new TransitionHelperKitkat(context);
+ }
+
+ @Override
+ public Object createScene(ViewGroup sceneRoot, Runnable r) {
+ return mTransitionHelper.createScene(sceneRoot, r);
+ }
+
+ @Override
+ public Object createAutoTransition() {
+ return mTransitionHelper.createAutoTransition();
+ }
+
+ @Override
+ public Object createFadeTransition(int fadingMode) {
+ return mTransitionHelper.createFadeTransition(fadingMode);
+ }
+
+ @Override
+ public Object createChangeBounds(boolean reparent) {
+ return mTransitionHelper.createChangeBounds(reparent);
+ }
+
+ @Override
+ public void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay) {
+ mTransitionHelper.setChangeBoundsStartDelay(changeBounds, view, startDelay);
+ }
+
+ @Override
+ public void setChangeBoundsStartDelay(Object changeBounds, int viewId, int startDelay) {
+ mTransitionHelper.setChangeBoundsStartDelay(changeBounds, viewId, startDelay);
+ }
+
+ @Override
+ public void setChangeBoundsStartDelay(Object changeBounds, String className,
+ int startDelay) {
+ mTransitionHelper.setChangeBoundsStartDelay(changeBounds, className, startDelay);
+ }
+
+ @Override
+ public void setChangeBoundsDefaultStartDelay(Object changeBounds, int startDelay) {
+ mTransitionHelper.setChangeBoundsDefaultStartDelay(changeBounds, startDelay);
+ }
+
+ @Override
+ public Object createTransitionSet(boolean sequential) {
+ return mTransitionHelper.createTransitionSet(sequential);
+ }
+
+ @Override
+ public void addTransition(Object transitionSet, Object transition) {
+ mTransitionHelper.addTransition(transitionSet, transition);
+ }
+
+ @Override
+ public void exclude(Object transition, int targetId, boolean exclude) {
+ mTransitionHelper.exclude(transition, targetId, exclude);
+ }
+
+ @Override
+ public void exclude(Object transition, View targetView, boolean exclude) {
+ mTransitionHelper.exclude(transition, targetView, exclude);
+ }
+
+ @Override
+ public void excludeChildren(Object transition, int targetId, boolean exclude) {
+ mTransitionHelper.excludeChildren(transition, targetId, exclude);
+ }
+
+ @Override
+ public void excludeChildren(Object transition, View targetView, boolean exclude) {
+ mTransitionHelper.excludeChildren(transition, targetView, exclude);
+ }
+
+ @Override
+ public void include(Object transition, int targetId) {
+ mTransitionHelper.include(transition, targetId);
+ }
+
+ @Override
+ public void include(Object transition, View targetView) {
+ mTransitionHelper.include(transition, targetView);
+ }
+
+ @Override
+ public void setTransitionCompleteListener(Object transition, Runnable listener) {
+ mTransitionHelper.setTransitionCompleteListener(transition, listener);
+ }
+
+ @Override
+ public void runTransition(Object scene, Object transition) {
+ mTransitionHelper.runTransition(scene, transition);
+ }
+ }
+
+ /**
+ * Returns the TransitionHelper that can be used to perform Transition
+ * animations.
+ *
+ * @param context A context for accessing system resources.
+ */
+ public TransitionHelper(Context context) {
+ if (systemSupportsTransitions()) {
+ mImpl = new TransitionHelperKitkatImpl(context);
+ } else {
+ mImpl = new TransitionHelperStubImpl();
+ }
+ }
+
+ public Object createScene(ViewGroup sceneRoot, Runnable r) {
+ return mImpl.createScene(sceneRoot, r);
+ }
+
+ public Object createChangeBounds(boolean reparent) {
+ return mImpl.createChangeBounds(reparent);
+ }
+
+ public void setChangeBoundsStartDelay(Object changeBounds, View view, int startDelay) {
+ mImpl.setChangeBoundsStartDelay(changeBounds, view, startDelay);
+ }
+
+ public void setChangeBoundsStartDelay(Object changeBounds, int viewId, int startDelay) {
+ mImpl.setChangeBoundsStartDelay(changeBounds, viewId, startDelay);
+ }
+
+ public void setChangeBoundsStartDelay(Object changeBounds, String className, int startDelay) {
+ mImpl.setChangeBoundsStartDelay(changeBounds, className, startDelay);
+ }
+
+ public void setChangeBoundsDefaultStartDelay(Object changeBounds, int startDelay) {
+ mImpl.setChangeBoundsDefaultStartDelay(changeBounds, startDelay);
+ }
+
+ public Object createTransitionSet(boolean sequential) {
+ return mImpl.createTransitionSet(sequential);
+ }
+
+ public void addTransition(Object transitionSet, Object transition) {
+ mImpl.addTransition(transitionSet, transition);
+ }
+
+ public void exclude(Object transition, int targetId, boolean exclude) {
+ mImpl.exclude(transition, targetId, exclude);
+ }
+
+ public void exclude(Object transition, View targetView, boolean exclude) {
+ mImpl.exclude(transition, targetView, exclude);
+ }
+
+ public void excludeChildren(Object transition, int targetId, boolean exclude) {
+ mImpl.excludeChildren(transition, targetId, exclude);
+ }
+
+ public void excludeChildren(Object transition, View targetView, boolean exclude) {
+ mImpl.excludeChildren(transition, targetView, exclude);
+ }
+
+ public void include(Object transition, int targetId) {
+ mImpl.include(transition, targetId);
+ }
+
+ public void include(Object transition, View targetView) {
+ mImpl.include(transition, targetView);
+ }
+
+ public Object createAutoTransition() {
+ return mImpl.createAutoTransition();
+ }
+
+ public Object createFadeTransition(int fadeMode) {
+ return mImpl.createFadeTransition(fadeMode);
+ }
+
+ public void setTransitionCompleteListener(Object transition, Runnable listener) {
+ mImpl.setTransitionCompleteListener(transition, listener);
+ }
+
+ public void runTransition(Object scene, Object transition) {
+ mImpl.runTransition(scene, transition);
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
new file mode 100644
index 0000000..ff4cddf
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/app/VerticalGridFragment.java
@@ -0,0 +1,271 @@
+/*
+ * 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.R;
+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.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ * Leanback fragment for a vertical grid.
+ *
+ * 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 ObjectAdapter mAdapter;
+ private VerticalGridPresenter mGridPresenter;
+ private VerticalGridPresenter.ViewHolder mGridViewHolder;
+ private OnItemSelectedListener mOnItemSelectedListener;
+ private OnItemClickedListener mOnItemClickedListener;
+ private View.OnClickListener mExternalOnSearchClickedListener;
+ private int mSelectedPosition = -1;
+
+ private ImageView mBadgeView;
+ private TextView mTitleView;
+ private ViewGroup mBrowseTitle;
+ private SearchOrbView mSearchOrbView;
+
+ public static class Params {
+ private String mTitle;
+ private Drawable mBadgeDrawable;
+
+ /**
+ * 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;
+ }
+ }
+
+ /**
+ * Set fragment parameters.
+ */
+ public void setParams(Params params) {
+ mParams = params;
+ setBadgeDrawable(mParams.mBadgeDrawable);
+ setTitle(mParams.mTitle);
+ }
+
+ /**
+ * Returns fragment parameters.
+ */
+ public Params getParams() {
+ return mParams;
+ }
+
+ /**
+ * Set 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);
+ }
+ if (mOnItemClickedListener != null) {
+ mGridPresenter.setOnItemClickedListener(mOnItemClickedListener);
+ }
+ }
+
+ /**
+ * Returns the grid presenter.
+ */
+ public VerticalGridPresenter getGridPresenter() {
+ return mGridPresenter;
+ }
+
+ /**
+ * Sets the object adapter for the fragment.
+ */
+ public void setAdapter(ObjectAdapter adapter) {
+ mAdapter = adapter;
+ updateAdapter();
+ }
+
+ /**
+ * Returns the object adapter.
+ */
+ public ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Sets an item selection listener.
+ */
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ if (mGridPresenter != null) {
+ mGridPresenter.setOnItemSelectedListener(mOnItemSelectedListener);
+ }
+ }
+
+ // TODO: getitemselectedlistener?
+
+ /**
+ * Sets an item clicked listener.
+ */
+ public void setOnItemClickedListener(OnItemClickedListener listener) {
+ mOnItemClickedListener = listener;
+ if (mGridPresenter != null) {
+ mGridPresenter.setOnItemClickedListener(mOnItemClickedListener);
+ }
+ }
+
+ /**
+ * Returns the item clicked listener.
+ */
+ 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.
+ */
+ 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);
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = 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);
+ if (mExternalOnSearchClickedListener != null) {
+ mSearchOrbView.setOnOrbClickedListener(mExternalOnSearchClickedListener);
+ }
+
+ if (mParams != null) {
+ setBadgeDrawable(mParams.mBadgeDrawable);
+ setTitle(mParams.mTitle);
+ }
+
+ return root;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ ViewGroup gridDock = (ViewGroup) view.findViewById(R.id.browse_grid_dock);
+ mGridViewHolder = mGridPresenter.onCreateViewHolder(gridDock);
+ gridDock.addView(mGridViewHolder.view);
+
+ updateAdapter();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mGridViewHolder.getGridView().requestFocus();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mGridViewHolder = null;
+ }
+
+ /**
+ * Sets the selected item position.
+ */
+ public void setSelectedPosition(int position) {
+ mSelectedPosition = position;
+ if(mGridViewHolder != null && mGridViewHolder.getGridView().getAdapter() != null) {
+ mGridViewHolder.getGridView().setSelectedPositionSmooth(position);
+ }
+ }
+
+ private void updateAdapter() {
+ if (mGridViewHolder != null) {
+ mGridPresenter.onBindViewHolder(mGridViewHolder, mAdapter);
+ if (mSelectedPosition != -1) {
+ mGridViewHolder.getGridView().setSelectedPosition(mSelectedPosition);
+ }
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/database/CursorMapper.java b/v17/leanback/src/android/support/v17/leanback/database/CursorMapper.java
new file mode 100644
index 0000000..20b4a36
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/database/CursorMapper.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.database;
+
+import android.database.Cursor;
+
+/**
+ * Abstract class used to convert the current {@link Cursor} row to a single
+ * object.
+ */
+public abstract class CursorMapper {
+
+ private Cursor mCursor;
+
+ /**
+ * Called once when the associated {@link Cursor} is changed. A subclass
+ * should bind column indexes to column names in this method. This method is
+ * not intended to be called outside of CursorMapper.
+ */
+ protected abstract void bindColumns(Cursor cursor);
+
+ /**
+ * A subclass should implement this method to create a single object using
+ * binding information. This method is not intended to be called
+ * outside of CursorMapper.
+ */
+ protected abstract Object bind(Cursor cursor);
+
+ /**
+ * Convert a {@link Cursor} at its current position to an Object.
+ */
+ public Object convert(Cursor cursor) {
+ if (cursor != mCursor) {
+ mCursor = cursor;
+ bindColumns(mCursor);
+ }
+ return bind(mCursor);
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java b/v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java
new file mode 100644
index 0000000..872d282
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterCache.java
@@ -0,0 +1,76 @@
+/*
+ * 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.graphics;
+
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.util.SparseArray;
+
+/**
+ * Cache of {@link ColorFilter}s for a given color at different alpha levels.
+ */
+public final class ColorFilterCache {
+
+ private static final SparseArray<ColorFilterCache> sColorToFiltersMap =
+ new SparseArray<ColorFilterCache>();
+
+ private final PorterDuffColorFilter[] mFilters = new PorterDuffColorFilter[0x100];
+
+ /**
+ * Get a ColorDimmer for a given color. Only the RGB values are used; the
+ * alpha channel is ignored in color. Subsequent calls to this method
+ * with the same color value will return the same cache.
+ *
+ * @param color The color to use for the color filters.
+ * @return A cache of ColorFilters at different alpha levels for the color.
+ */
+ public static ColorFilterCache getColorFilterCache(int color) {
+ final int r = Color.red(color);
+ final int g = Color.green(color);
+ final int b = Color.blue(color);
+ color = Color.rgb(r, g, b);
+ ColorFilterCache filters = sColorToFiltersMap.get(color);
+ if (filters == null) {
+ filters = new ColorFilterCache(r, g, b);
+ sColorToFiltersMap.put(color, filters);
+ }
+ return filters;
+ }
+
+ private ColorFilterCache(int r, int g, int b) {
+ // Pre cache all 256 filter levels
+ for (int i = 0x00; i <= 0xFF; i++) {
+ int color = Color.argb(i, r, g, b);
+ mFilters[i] = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+ }
+ }
+
+ /**
+ * Returns a ColorFilter for a given alpha level between 0 and 1.0.
+ *
+ * @param level The alpha level the filter should apply.
+ * @return A ColorFilter at the alpha level for the color represented by the
+ * cache.
+ */
+ public ColorFilter getFilterForLevel(float level) {
+ if (level >= 0 && level <= 1.0) {
+ int filterIndex = (int) (0xFF * level);
+ return mFilters[filterIndex];
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java b/v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java
new file mode 100644
index 0000000..d64a098
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/graphics/ColorFilterDimmer.java
@@ -0,0 +1,125 @@
+/*
+ * 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.graphics;
+
+import android.content.Context;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.view.View;
+import android.support.v17.leanback.R;
+
+/**
+ * Helper class for applying a dim level to a View. The ColorFilterDimmer
+ * uses a ColorFilter in a Paint object to dim the view according to the
+ * currently active level.
+ */
+public final class ColorFilterDimmer {
+
+ private final ColorFilterCache mColorDimmer;
+
+ private final float mActiveLevel;
+ private final float mDimmedLevel;
+
+ private final Paint mPaint;
+ private ColorFilter mFilter;
+
+ /**
+ * Creates a default ColorFilterDimmer. Uses the default color and level for
+ * the dimmer.
+ *
+ * @param context A Context used to retrieve Resources.
+ * @return A ColorFilterDimmer with the default dim color and levels.
+ */
+ public static ColorFilterDimmer createDefault(Context context) {
+ return new ColorFilterDimmer(ColorFilterCache.getColorFilterCache(
+ context.getResources().getColor(R.color.lb_view_dim_mask_color)),
+ 0, context.getResources().getFraction(R.dimen.lb_view_dimmed_level, 1, 1));
+ }
+
+ /**
+ * Creates a ColorFilterDimmer for the given color and levels..
+ *
+ * @param dimmer The ColorFilterCache for dim color.
+ * @param activeLevel The level of dimming when the View is in its active
+ * state. Must be a float value between 0.0 and 1.0.
+ * @param dimmedLevel The level of dimming when the View is in its dimmed
+ * state. Must be a float value between 0.0 and 1.0.
+ */
+ public static ColorFilterDimmer create(ColorFilterCache dimmer,
+ float activeLevel, float dimmedLevel) {
+ return new ColorFilterDimmer(dimmer, activeLevel, dimmedLevel);
+ }
+
+ private ColorFilterDimmer(ColorFilterCache dimmer, float activeLevel, float dimmedLevel) {
+ mColorDimmer = dimmer;
+ if (activeLevel > 1.0f) activeLevel = 1.0f;
+ if (activeLevel < 0.0f) activeLevel = 0.0f;
+ if (dimmedLevel > 1.0f) dimmedLevel = 1.0f;
+ if (dimmedLevel < 0.0f) dimmedLevel = 0.0f;
+ mActiveLevel = activeLevel;
+ mDimmedLevel = dimmedLevel;
+ mPaint = new Paint();
+ }
+
+ /**
+ * Apply current the ColorFilter to a View. This method will set the
+ * hardware layer of the view when applying a filter, and remove it when not
+ * applying a filter.
+ *
+ * @param view The View to apply the ColorFilter to.
+ */
+ public void applyFilterToView(View view) {
+ if (mFilter != null) {
+ view.setLayerType(View.LAYER_TYPE_HARDWARE, mPaint);
+ } else {
+ view.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ // FIXME: Current framework has bug that not triggering invalidate when change layer
+ // paint. Will add conditional sdk version check once bug is fixed in released
+ // framework.
+ view.invalidate();
+ }
+
+ /**
+ * Sets the active level of the dimmer. Updates the ColorFilter based on the
+ * level.
+ *
+ * @param level A float between 0 (fully dim) and 1 (fully active).
+ */
+ public void setActiveLevel(float level) {
+ if (level < 0.0f) level = 0.0f;
+ if (level > 1.0f) level = 1.0f;
+ mFilter = mColorDimmer.getFilterForLevel(
+ mDimmedLevel + level * (mActiveLevel - mDimmedLevel));
+ mPaint.setColorFilter(mFilter);
+ }
+
+ /**
+ * Gets the ColorFilter set to the current dim level.
+ *
+ * @return The current ColorFilter.
+ */
+ public ColorFilter getColorFilter() {
+ return mFilter;
+ }
+
+ /**
+ * Gets the Paint object set to the current dim level.
+ *
+ * @return The current Paint object.
+ */
+ public Paint getPaint() {
+ return mPaint;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java b/v17/leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java
new file mode 100644
index 0000000..17a185b
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/graphics/ColorOverlayDimmer.java
@@ -0,0 +1,157 @@
+/*
+ * 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.graphics;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.support.v17.leanback.R;
+import android.view.View;
+
+/**
+ * Helper class for assigning a dim color to Paint. It holds the alpha value for
+ * the current active level.
+ */
+public final class ColorOverlayDimmer {
+
+ private final float mActiveLevel;
+ private final float mDimmedLevel;
+
+ private final Paint mPaint;
+
+ private int mAlpha;
+ private float mAlphaFloat;
+
+ /**
+ * Creates a default ColorOverlayDimmer.
+ */
+ public static ColorOverlayDimmer createDefault(Context context) {
+ return new ColorOverlayDimmer(
+ context.getResources().getColor(R.color.lb_view_dim_mask_color), 0,
+ context.getResources().getFraction(R.dimen.lb_view_dimmed_level, 1, 1));
+ }
+
+ /**
+ * Creates a ColorOverlayDimmer for the given color and levels.
+ *
+ * @param dimColor The color for fully dimmed. Only the RGB values are
+ * used; the alpha channel is ignored.
+ * @param activeLevel The level of dimming when the View is in its active
+ * state. Must be a float value between 0.0 and 1.0.
+ * @param dimmedLevel The level of dimming when the View is in its dimmed
+ * state. Must be a float value between 0.0 and 1.0.
+ */
+ public static ColorOverlayDimmer createColorOverlayDimmer(int dimColor, float activeLevel,
+ float dimmedLevel) {
+ return new ColorOverlayDimmer(dimColor, activeLevel, dimmedLevel);
+ }
+
+ private ColorOverlayDimmer(int dimColor, float activeLevel, float dimmedLevel) {
+ if (activeLevel > 1.0f) activeLevel = 1.0f;
+ if (activeLevel < 0.0f) activeLevel = 0.0f;
+ if (dimmedLevel > 1.0f) dimmedLevel = 1.0f;
+ if (dimmedLevel < 0.0f) dimmedLevel = 0.0f;
+ mPaint = new Paint();
+ dimColor = Color.rgb(Color.red(dimColor), Color.green(dimColor), Color.blue(dimColor));
+ mPaint.setColor(dimColor);
+ mActiveLevel = activeLevel;
+ mDimmedLevel = dimmedLevel;
+ setActiveLevel(1);
+ }
+
+ /**
+ * Sets the active level of the dimmer. Updates the alpha value based on the
+ * level.
+ *
+ * @param level A float between 0 (fully dim) and 1 (fully active).
+ */
+ public void setActiveLevel(float level) {
+ mAlphaFloat = (mDimmedLevel + level * (mActiveLevel - mDimmedLevel));
+ mAlpha = (int) (255 * mAlphaFloat);
+ mPaint.setAlpha(mAlpha);
+ }
+
+ /**
+ * Returns whether the dimmer needs to draw.
+ */
+ public boolean needsDraw() {
+ return mAlpha != 0;
+ }
+
+ /**
+ * Returns the alpha value for the dimmer.
+ */
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ /**
+ * Returns the float value between 0 and 1 corresponding to alpha between
+ * 0 and 255.
+ */
+ public float getAlphaFloat() {
+ return mAlphaFloat;
+ }
+
+ /**
+ * Returns the Paint object set to the current alpha value.
+ */
+ public Paint getPaint() {
+ return mPaint;
+ }
+
+ /**
+ * Change the RGB of the color according to current dim level. Maintains the
+ * alpha value of the color.
+ *
+ * @param color The color to apply the dim level to.
+ * @return A color with the RGB values adjusted by the alpha of the current
+ * dim level.
+ */
+ public int applyToColor(int color) {
+ float f = 1 - mAlphaFloat;
+ return Color.argb(Color.alpha(color),
+ (int)(Color.red(color) * f),
+ (int)(Color.green(color) * f),
+ (int)(Color.blue(color) * f));
+ }
+
+ /**
+ * Draw a dim color overlay on top of a child View inside the canvas of
+ * the parent View.
+ *
+ * @param c Canvas of the parent View.
+ * @param v A child of the parent View.
+ * @param includePadding Set to true to draw overlay on padding area of the
+ * View.
+ */
+ public void drawColorOverlay(Canvas c, View v, boolean includePadding) {
+ c.save();
+ float dx = v.getLeft() + v.getTranslationX();
+ float dy = v.getTop() + v.getTranslationY();
+ c.translate(dx, dy);
+ c.concat(v.getMatrix());
+ c.translate(-dx, -dy);
+ if (includePadding) {
+ c.drawRect(v.getLeft(), v.getTop(), v.getRight(), v.getBottom(), mPaint);
+ } else {
+ c.drawRect(v.getLeft() + v.getPaddingLeft(),
+ v.getTop() + v.getPaddingTop(),
+ v.getRight() - v.getPaddingRight(),
+ v.getBottom() - v.getPaddingBottom(), mPaint);
+ }
+ c.restore();
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
new file mode 100644
index 0000000..c42f3e0
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/AbstractDetailsDescriptionPresenter.java
@@ -0,0 +1,139 @@
+/*
+ * 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.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
+import android.support.v17.leanback.R;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/**
+ * An abstract {@link Presenter} for rendering a detailed description of an
+ * item. Typically this Presenter will be used in a DetailsOveriewRowPresenter.
+ *
+ * <p>Subclasses will override {@link #onBindDescription} to implement the data
+ * binding for this Presenter.
+ */
+public abstract class AbstractDetailsDescriptionPresenter extends Presenter {
+
+ public static class ViewHolder extends Presenter.ViewHolder {
+ private final TextView mTitle;
+ private final TextView mSubtitle;
+ private final TextView mBody;
+ private final int mUnderTitleSpacing;
+ private final int mUnderSubtitleSpacing;
+
+ 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;
+ }
+
+ public TextView getTitle() {
+ return mTitle;
+ }
+
+ public TextView getSubtitle() {
+ return mSubtitle;
+ }
+
+ public TextView getBody() {
+ return mBody;
+ }
+
+ private FontMetricsInt getFontMetricsInt(TextView textView) {
+ Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ paint.setTextSize(textView.getTextSize());
+ paint.setTypeface(textView.getTypeface());
+ return paint.getFontMetricsInt();
+ }
+ }
+
+ @Override
+ public final ViewHolder onCreateViewHolder(ViewGroup parent) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.lb_details_description, parent, false);
+ return new ViewHolder(v);
+ }
+
+ @Override
+ public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ ViewHolder vh = (ViewHolder) viewHolder;
+ DetailsOverviewRow row = (DetailsOverviewRow) item;
+ onBindDescription(vh, row.getItem());
+
+ boolean hasTitle = true;
+ if (TextUtils.isEmpty(vh.mTitle.getText())) {
+ vh.mTitle.setVisibility(View.GONE);
+ hasTitle = false;
+ } else {
+ vh.mTitle.setVisibility(View.VISIBLE);
+ }
+
+ boolean hasSubtitle = true;
+ if (TextUtils.isEmpty(vh.mSubtitle.getText())) {
+ vh.mSubtitle.setVisibility(View.GONE);
+ hasSubtitle = false;
+ } else {
+ vh.mSubtitle.setVisibility(View.VISIBLE);
+ if (hasTitle) {
+ setTopMargin(vh.mSubtitle, vh.mUnderTitleSpacing);
+ } else {
+ setTopMargin(vh.mSubtitle, 0);
+ }
+ }
+
+ if (TextUtils.isEmpty(vh.mBody.getText())) {
+ vh.mBody.setVisibility(View.GONE);
+ } else {
+ vh.mBody.setVisibility(View.VISIBLE);
+ if (hasSubtitle) {
+ setTopMargin(vh.mBody, vh.mUnderSubtitleSpacing);
+ } else if (hasTitle) {
+ setTopMargin(vh.mBody, vh.mUnderTitleSpacing);
+ } else {
+ setTopMargin(vh.mBody, 0);
+ }
+ }
+ }
+
+ /**
+ * Binds the data from the item referenced in the DetailsOverviewRow to the
+ * ViewHolder.
+ *
+ * @param vh The ViewHolder for this details description view.
+ * @param item The item from the DetailsOverviewRow being presented.
+ */
+ protected abstract void onBindDescription(ViewHolder vh, Object item);
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {}
+
+ private 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/widget/Action.java b/v17/leanback/src/android/support/v17/leanback/widget/Action.java
new file mode 100644
index 0000000..31d0fb5
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/Action.java
@@ -0,0 +1,124 @@
+/*
+ * 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.graphics.drawable.Drawable;
+import android.text.TextUtils;
+
+import static android.support.v17.leanback.widget.ObjectAdapter.NO_ID;
+
+/**
+ * An action that can be shown on a details page. It contains one or two lines
+ * of text and an optional image.
+ */
+public class Action {
+
+ private long mId = NO_ID;
+ private Drawable mIcon;
+ private CharSequence mLabel1;
+ private CharSequence mLabel2;
+
+ public Action(long id) {
+ this(id, "");
+ }
+
+ public Action(long id, CharSequence label) {
+ this(id, label, null);
+ }
+
+ public Action(long id, CharSequence label1, CharSequence label2) {
+ this(id, label1, label2, null);
+ }
+
+ public Action(long id, CharSequence label1, CharSequence label2, Drawable icon) {
+ setId(id);
+ setLabel1(label1);
+ setLabel2(label2);
+ setIcon(icon);
+ }
+
+ /**
+ * Set id for this action.
+ */
+ public final void setId(long id) {
+ mId = id;
+ }
+
+ /**
+ * Returns the id for this action.
+ */
+ public final long getId() {
+ return mId;
+ }
+
+ /**
+ * Set the first line label for this action.
+ */
+ public final void setLabel1(CharSequence label) {
+ mLabel1 = label;
+ }
+
+ /**
+ * Returns the first line label for this action.
+ */
+ public final CharSequence getLabel1() {
+ return mLabel1;
+ }
+
+ /**
+ * Set the second line label for this action.
+ */
+ public final void setLabel2(CharSequence label) {
+ mLabel2 = label;
+ }
+
+ /**
+ * Returns the second line label for this action.
+ */
+ public final CharSequence getLabel2() {
+ return mLabel2;
+ }
+
+ /**
+ * Set the icon drawable for this action.
+ */
+ public final void setIcon(Drawable icon) {
+ mIcon = icon;
+ }
+
+ /**
+ * Returns the icon drawable for this action.
+ */
+ public final Drawable getIcon() {
+ return mIcon;
+ }
+
+ @Override
+ public String toString(){
+ StringBuilder sb = new StringBuilder();
+ if (!TextUtils.isEmpty(mLabel1)) {
+ sb.append(mLabel1);
+ }
+ if (!TextUtils.isEmpty(mLabel2)) {
+ if (!TextUtils.isEmpty(mLabel1)) {
+ sb.append(" ");
+ }
+ sb.append(mLabel2);
+ }
+ if (mIcon != null && sb.length() == 0) {
+ sb.append("(action icon)");
+ }
+ return sb.toString();
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java b/v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
new file mode 100644
index 0000000..7698872
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ActionPresenterSelector.java
@@ -0,0 +1,146 @@
+/*
+ * 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.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+class ActionPresenterSelector extends PresenterSelector {
+
+ private final Presenter mOneLineActionPresenter = new OneLineActionPresenter();
+ private final Presenter mTwoLineActionPresenter = new TwoLineActionPresenter();
+ private OnActionClickedListener mOnActionClickedListener;
+
+ @Override
+ public Presenter getPresenter(Object item) {
+ Action action = (Action) item;
+ if (TextUtils.isEmpty(action.getLabel2())) {
+ return mOneLineActionPresenter;
+ } else {
+ return mTwoLineActionPresenter;
+ }
+ }
+
+ public final void setOnActionClickedListener(OnActionClickedListener listener) {
+ mOnActionClickedListener = listener;
+ }
+
+ public final OnActionClickedListener getOnActionClickedListener() {
+ return mOnActionClickedListener;
+ }
+
+ static class ActionViewHolder extends Presenter.ViewHolder {
+ Action mAction;
+ ImageView mIconView;
+ TextView mLabel;
+
+ public ActionViewHolder(View view) {
+ super(view);
+ mIconView = (ImageView) view.findViewById(R.id.lb_action_icon);
+ mLabel = (TextView) view.findViewById(R.id.lb_action_text);
+ }
+ }
+
+ class OneLineActionPresenter extends Presenter {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.lb_action_1_line, parent, false);
+ final ActionViewHolder vh = new ActionViewHolder(v);
+ v.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (ActionPresenterSelector.this.mOnActionClickedListener != null &&
+ vh.mAction != null) {
+ ActionPresenterSelector.this.mOnActionClickedListener.onActionClicked(vh.mAction);
+ }
+ }
+ });
+ return vh;
+ }
+
+ @Override
+ public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ Action action = (Action) item;
+ ActionViewHolder vh = (ActionViewHolder) viewHolder;
+ vh.mAction = action;
+ vh.mLabel.setText(action.getLabel1());
+ }
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
+ ((ActionViewHolder) viewHolder).mAction = null;
+ }
+ }
+
+ class TwoLineActionPresenter extends Presenter {
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.lb_action_2_lines, parent, false);
+ final ActionViewHolder vh = new ActionViewHolder(v);
+ v.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (ActionPresenterSelector.this.mOnActionClickedListener != null &&
+ vh.mAction != null) {
+ ActionPresenterSelector.this.mOnActionClickedListener.onActionClicked(vh.mAction);
+ }
+ }
+ });
+ return vh;
+ }
+
+ @Override
+ public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ Action action = (Action) item;
+ 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());
+ } else {
+ vh.view.setPadding(horizontalPadding, 0, horizontalPadding, 0);
+ vh.mIconView.setVisibility(View.GONE);
+ }
+ CharSequence line1 = action.getLabel1();
+ CharSequence line2 = action.getLabel2();
+ if (TextUtils.isEmpty(line1)) {
+ vh.mLabel.setText(line2);
+ } else if (TextUtils.isEmpty(line2)) {
+ vh.mLabel.setText(line1);
+ } else {
+ vh.mLabel.setText(line1 + "\n" + line2);
+ }
+ }
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
+ ActionViewHolder vh = (ActionViewHolder) viewHolder;
+ vh.mIconView.setVisibility(View.GONE);
+ 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
new file mode 100644
index 0000000..74bb038
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ArrayObjectAdapter.java
@@ -0,0 +1,122 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Adapter implemented with an {@link ArrayList}.
+ */
+public class ArrayObjectAdapter extends ObjectAdapter {
+
+ private ArrayList<Object> mItems = new ArrayList<Object>();
+
+ public ArrayObjectAdapter(PresenterSelector presenterSelector) {
+ super(presenterSelector);
+ }
+
+ public ArrayObjectAdapter(Presenter presenter) {
+ super(presenter);
+ }
+
+ public ArrayObjectAdapter() {
+ super();
+ }
+
+ @Override
+ public int size() {
+ return mItems.size();
+ }
+
+ @Override
+ public Object get(int index) {
+ return mItems.get(index);
+ }
+
+ /**
+ * Adds an item to the end of the list.
+ *
+ * @param item The item to add to the end of the list.
+ */
+ public void add(Object item) {
+ add(mItems.size(), item);
+ }
+
+ /**
+ * Inserts an item into this list at the specified index.
+ *
+ * @param index The index at which the item should be inserted.
+ * @param item The item to insert into the list.
+ */
+ public void add(int index, Object item) {
+ mItems.add(index, item);
+ notifyItemRangeInserted(index, 1);
+ }
+
+ /**
+ * Adds the objects in the given collection to the list, starting at the
+ * given index.
+ *
+ * @param index The index at which the items should be inserted.
+ * @param items A {@link Collection} of items to insert.
+ */
+ public void addAll(int index, Collection items) {
+ int itemsCount = items.size();
+ mItems.addAll(index, items);
+ notifyItemRangeInserted(index, itemsCount);
+ }
+
+ /**
+ * Removes the first occurrence of the given item from the list.
+ *
+ * @param item The item to remove from the list.
+ * @return True if the item was found and thus removed from the list.
+ */
+ public boolean remove(Object item) {
+ int index = mItems.indexOf(item);
+ if (index >= 0) {
+ mItems.remove(index);
+ notifyItemRangeRemoved(index, 1);
+ }
+ return index >= 0;
+ }
+
+ /**
+ * Removes a range of items from the list. 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.
+ * @param count The number of items to remove.
+ * @return The number of items removed.
+ */
+ public int removeItems(int position, int count) {
+ int itemsToRemove = Math.min(count, mItems.size() - position);
+
+ for (int i = 0; i < itemsToRemove; i++) {
+ mItems.remove(position);
+ }
+ notifyItemRangeRemoved(position, itemsToRemove);
+ return itemsToRemove;
+ }
+
+ /**
+ * Removes all items from this list, leaving it empty.
+ */
+ public void clear() {
+ int itemCount = mItems.size();
+ mItems.clear();
+ notifyItemRangeRemoved(0, itemCount);
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java b/v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java
new file mode 100644
index 0000000..20360f7
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/BaseCardView.java
@@ -0,0 +1,891 @@
+/*
+ * 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.content.res.TypedArray;
+import android.support.v17.leanback.R;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Transformation;
+
+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()}.
+ * <p>
+ * See {@link BaseCardView.LayoutParams BaseCardView.LayoutParams} for
+ * layout attributes.
+ * </p>
+ */
+public class BaseCardView extends ViewGroup {
+ private static final String TAG = "BaseCardView";
+ private static final boolean DEBUG = false;
+
+ /**
+ * A simple card type with a single layout area. This card type does not
+ * change its layout or size as it transitions between
+ * Activated/Not-Activated or Selected/Unselected states.
+ *
+ * @see #getCardType()
+ */
+ 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.
+ *
+ * @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.
+ *
+ * @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
+ * the card height.
+ *
+ * @see #getCardType()
+ */
+ public static final int CARD_TYPE_INFO_UNDER_WITH_EXTRA = 3;
+
+ /**
+ * Indicates that a card region is always visible.
+ */
+ public static final int CARD_REGION_VISIBLE_ALWAYS = 0;
+
+ /**
+ * Indicates that a card region is visible when the card is activated.
+ */
+ public static final int CARD_REGION_VISIBLE_ACTIVATED = 1;
+
+ /**
+ * Indicates that a card region is visible when the card is selected.
+ */
+ public static final int CARD_REGION_VISIBLE_SELECTED = 2;
+
+ private static final int CARD_TYPE_INVALID = 4;
+
+ private int mCardType;
+ private int mInfoVisibility;
+ private int mExtraVisibility;
+
+ private ArrayList<View> mMainViewList;
+ private ArrayList<View> mInfoViewList;
+ private ArrayList<View> mExtraViewList;
+
+ private int mMeasuredWidth;
+ private int mMeasuredHeight;
+ private boolean mDelaySelectedAnim;
+ private int mSelectedAnimationDelay;
+ private final int mActivatedAnimDuration;
+ private final int mSelectedAnimDuration;
+
+ private float mInfoOffset;
+ private float mInfoVisFraction;
+ private float mInfoAlpha = 1.0f;
+ private Animation mAnim;
+
+ private final Runnable mAnimationTrigger = new Runnable() {
+ @Override
+ public void run() {
+ animateInfoOffset(true);
+ }
+ };
+
+ public BaseCardView(Context context) {
+ this(context, null);
+ }
+
+ public BaseCardView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.baseCardViewStyle);
+ }
+
+ public BaseCardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseCardView, defStyle, 0);
+
+ try {
+ mCardType = a.getInteger(R.styleable.lbBaseCardView_cardType, CARD_TYPE_MAIN_ONLY);
+ mInfoVisibility = a.getInteger(R.styleable.lbBaseCardView_infoVisibility,
+ CARD_REGION_VISIBLE_ACTIVATED);
+ mExtraVisibility = a.getInteger(R.styleable.lbBaseCardView_extraVisibility,
+ CARD_REGION_VISIBLE_SELECTED);
+ // Extra region should never show before info region.
+ if (mExtraVisibility < mInfoVisibility) {
+ mExtraVisibility = mInfoVisibility;
+ }
+
+ mSelectedAnimationDelay = a.getInteger(
+ R.styleable.lbBaseCardView_selectedAnimationDelay,
+ getResources().getInteger(R.integer.lb_card_selected_animation_delay));
+
+ mSelectedAnimDuration = a.getInteger(
+ R.styleable.lbBaseCardView_selectedAnimationDuration,
+ getResources().getInteger(R.integer.lb_card_selected_animation_duration));
+
+ mActivatedAnimDuration =
+ a.getInteger(R.styleable.lbBaseCardView_activatedAnimationDuration,
+ getResources().getInteger(R.integer.lb_card_activated_animation_duration));
+ } finally {
+ a.recycle();
+ }
+
+ mDelaySelectedAnim = true;
+
+ mMainViewList = new ArrayList<View>();
+ mInfoViewList = new ArrayList<View>();
+ mExtraViewList = new ArrayList<View>();
+
+ mInfoOffset = 0.0f;
+ mInfoVisFraction = 0.0f;
+ }
+
+ /**
+ * Sets a flag indicating if the Selected animation (if the selected card
+ * type implements one) should run immediately after the card is selected,
+ * or if it should be delayed. The default behavior is to delay this
+ * animation. This is a one-shot override. If set to false, after the card
+ * is selected and the selected animation is triggered, this flag is
+ * automatically reset to true. This is useful when you want to change the
+ * default behavior, and have the selected animation run immediately. One
+ * such case could be when focus moves from one row to the other, when
+ * instead of delaying the selected animation until the user pauses on a
+ * card, it may be desirable to trigger the animation for that card
+ * immediately.
+ *
+ * @param delay True (default) if the selected animation should be delayed
+ * after the card is selected, or false if the animation should
+ * run immediately the next time the card is Selected.
+ */
+ public void setSelectedAnimationDelayed(boolean delay) {
+ mDelaySelectedAnim = delay;
+ }
+
+ /**
+ * Returns a boolean indicating if the selected animation will run
+ * immediately or be delayed the next time the card is Selected.
+ *
+ * @return true if this card is set to delay the selected animation the next
+ * time it is selected, or false if the selected animation will run
+ * immediately the next time the card is selected.
+ */
+ public boolean isSelectedAnimationDelayed() {
+ return mDelaySelectedAnim;
+ }
+
+ /**
+ * Sets the type of this Card.
+ *
+ * @param type The desired card type.
+ */
+ public void setCardType(int type) {
+ if (mCardType != type) {
+ if (type >= CARD_TYPE_MAIN_ONLY && type < CARD_TYPE_INVALID) {
+ // Valid card type
+ mCardType = type;
+ } else {
+ Log.e(TAG, "Invalid card type specified: " + type +
+ ". Defaulting to type CARD_TYPE_MAIN_ONLY.");
+ mCardType = CARD_TYPE_MAIN_ONLY;
+ }
+ requestLayout();
+ }
+ }
+
+ /**
+ * Returns the type of this Card.
+ *
+ * @return The type of this card.
+ */
+ public int getCardType() {
+ return mCardType;
+ }
+
+ public void setInfoVisibility(int visibility) {
+ if (mInfoVisibility != visibility) {
+ mInfoVisibility = visibility;
+ if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED && isSelected()) {
+ mInfoVisFraction = 1.0f;
+ } else {
+ mInfoVisFraction = 0.0f;
+ }
+ requestLayout();
+ }
+ }
+
+ public int getInfoVisibility() {
+ return mInfoVisibility;
+ }
+
+ public void setExtraVisibility(int visibility) {
+ if (mExtraVisibility != visibility) {
+ mExtraVisibility = visibility;
+ requestLayout();
+ }
+ }
+
+ public int getExtraVisibility() {
+ return mExtraVisibility;
+ }
+
+ /**
+ * Sets the Activated state of this Card. This can trigger changes in the
+ * card layout, resulting in views to become visible or hidden. A card is
+ * normally set to Activated state when its parent container (like a Row)
+ * receives focus, and then activates all of its children.
+ *
+ * @param activated True if the card is ACTIVE, or false if INACTIVE.
+ * @see #isActivated()
+ */
+ @Override
+ public void setActivated(boolean activated) {
+ if (activated != isActivated()) {
+ super.setActivated(activated);
+ applyActiveState(isActivated());
+ }
+ }
+
+ /**
+ * Sets the Selected state of this Card. This can trigger changes in the
+ * card layout, resulting in views to become visible or hidden. A card is
+ * normally set to Selected state when it receives input focus.
+ *
+ * @param selected True if the card is Selected, or false otherwise.
+ * @see #isSelected()
+ */
+ @Override
+ public void setSelected(boolean selected) {
+ if (selected != isSelected()) {
+ super.setSelected(selected);
+ applySelectedState(isSelected());
+ }
+ }
+
+ @Override
+ public boolean shouldDelayChildPressedState() {
+ return false;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ mMeasuredWidth = 0;
+ mMeasuredHeight = 0;
+ int state = 0;
+ int mainHeight = 0;
+ int infoHeight = 0;
+ int extraHeight = 0;
+
+ findChildrenViews();
+
+ final int unspecifiedSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ // MAIN is always present
+ for (int i = 0; i < mMainViewList.size(); i++) {
+ View mainView = mMainViewList.get(i);
+ if (mainView.getVisibility() != View.GONE) {
+ measureChild(mainView, unspecifiedSpec, unspecifiedSpec);
+ mMeasuredWidth = Math.max(mMeasuredWidth, mainView.getMeasuredWidth());
+ mainHeight += mainView.getMeasuredHeight();
+ state = View.combineMeasuredStates(state, mainView.getMeasuredState());
+ }
+ }
+
+
+ // The MAIN area determines the card width
+ int cardWidthMeasureSpec = MeasureSpec.makeMeasureSpec(mMeasuredWidth, MeasureSpec.EXACTLY);
+
+ if (hasInfoRegion()) {
+ for (int i = 0; i < mInfoViewList.size(); i++) {
+ View infoView = mInfoViewList.get(i);
+ if (infoView.getVisibility() != View.GONE) {
+ measureChild(infoView, cardWidthMeasureSpec, unspecifiedSpec);
+ if (mCardType != CARD_TYPE_INFO_OVER) {
+ infoHeight += infoView.getMeasuredHeight();
+ }
+ state = View.combineMeasuredStates(state, infoView.getMeasuredState());
+ }
+ }
+
+ if (hasExtraRegion()) {
+ for (int i = 0; i < mExtraViewList.size(); i++) {
+ View extraView = mExtraViewList.get(i);
+ if (extraView.getVisibility() != View.GONE) {
+ measureChild(extraView, cardWidthMeasureSpec, unspecifiedSpec);
+ extraHeight += extraView.getMeasuredHeight();
+ state = View.combineMeasuredStates(state, extraView.getMeasuredState());
+ }
+ }
+ }
+ }
+
+ boolean infoAnimating = hasInfoRegion() && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED;
+ mMeasuredHeight = (int) (mainHeight +
+ (infoAnimating ? (infoHeight * mInfoVisFraction) : infoHeight)
+ + extraHeight - (infoAnimating ? 0 : mInfoOffset));
+
+ // Report our final dimensions.
+ setMeasuredDimension(View.resolveSizeAndState(mMeasuredWidth + getPaddingLeft() +
+ getPaddingRight(), widthMeasureSpec, state),
+ View.resolveSizeAndState(mMeasuredHeight + getPaddingTop() + getPaddingBottom(),
+ heightMeasureSpec, state << View.MEASURED_HEIGHT_STATE_SHIFT));
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ float currBottom = getPaddingTop();
+
+ // MAIN is always present
+ for (int i = 0; i < mMainViewList.size(); i++) {
+ View mainView = mMainViewList.get(i);
+ if (mainView.getVisibility() != View.GONE) {
+ mainView.layout(getPaddingLeft(),
+ (int) currBottom,
+ mMeasuredWidth + getPaddingLeft(),
+ (int) (currBottom + mainView.getMeasuredHeight()));
+ currBottom += mainView.getMeasuredHeight();
+ }
+ }
+
+ if (hasInfoRegion()) {
+ float infoHeight = 0f;
+ for (int i = 0; i < mInfoViewList.size(); i++) {
+ infoHeight += mInfoViewList.get(i).getMeasuredHeight();
+ }
+
+ if (mCardType == CARD_TYPE_INFO_OVER) {
+ // retract currBottom to overlap the info views on top of main
+ currBottom -= infoHeight;
+ if (currBottom < 0) {
+ currBottom = 0;
+ }
+ } else if (mCardType == CARD_TYPE_INFO_UNDER) {
+ if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
+ infoHeight = infoHeight * mInfoVisFraction;
+ }
+ } else {
+ currBottom -= mInfoOffset;
+ }
+
+ for (int i = 0; i < mInfoViewList.size(); i++) {
+ View infoView = mInfoViewList.get(i);
+ if (infoView.getVisibility() != View.GONE) {
+ int viewHeight = infoView.getMeasuredHeight();
+ if (viewHeight > infoHeight) {
+ viewHeight = (int) infoHeight;
+ }
+ infoView.layout(getPaddingLeft(),
+ (int) currBottom,
+ mMeasuredWidth + getPaddingLeft(),
+ (int) (currBottom + viewHeight));
+ currBottom += viewHeight;
+ infoHeight -= viewHeight;
+ if (infoHeight <= 0) {
+ break;
+ }
+ }
+ }
+
+ if (hasExtraRegion()) {
+ for (int i = 0; i < mExtraViewList.size(); i++) {
+ View extraView = mExtraViewList.get(i);
+ if (extraView.getVisibility() != View.GONE) {
+ extraView.layout(getPaddingLeft(),
+ (int) currBottom,
+ mMeasuredWidth + getPaddingLeft(),
+ (int) (currBottom + extraView.getMeasuredHeight()));
+ currBottom += extraView.getMeasuredHeight();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ removeCallbacks(mAnimationTrigger);
+ cancelAnimations();
+ mInfoOffset = 0.0f;
+ mInfoVisFraction = 0.0f;
+ }
+
+ private boolean hasInfoRegion() {
+ return mCardType != CARD_TYPE_MAIN_ONLY;
+ }
+
+ private boolean hasExtraRegion() {
+ return mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA;
+ }
+
+ private boolean isRegionVisible(int regionVisibility) {
+ switch (regionVisibility) {
+ case CARD_REGION_VISIBLE_ALWAYS:
+ return true;
+ case CARD_REGION_VISIBLE_ACTIVATED:
+ return isActivated();
+ case CARD_REGION_VISIBLE_SELECTED:
+ return isActivated() && isSelected();
+ default:
+ if (DEBUG) Log.e(TAG, "invalid region visibility state: " + regionVisibility);
+ return false;
+ }
+ }
+
+ private void findChildrenViews() {
+ mMainViewList.clear();
+ mInfoViewList.clear();
+ mExtraViewList.clear();
+
+ final int count = getChildCount();
+
+ boolean infoVisible = isRegionVisible(mInfoVisibility);
+ boolean extraVisible = hasExtraRegion() && mInfoOffset > 0f;
+
+ if (mCardType == CARD_TYPE_INFO_UNDER && mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
+ infoVisible = infoVisible && mInfoVisFraction > 0f;
+ }
+
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+
+ if (child == null) {
+ continue;
+ }
+
+ BaseCardView.LayoutParams lp = (BaseCardView.LayoutParams) child
+ .getLayoutParams();
+ if (lp.viewType == LayoutParams.VIEW_TYPE_INFO) {
+ mInfoViewList.add(child);
+ child.setVisibility(infoVisible ? View.VISIBLE : View.GONE);
+ } else if (lp.viewType == LayoutParams.VIEW_TYPE_EXTRA) {
+ mExtraViewList.add(child);
+ child.setVisibility(extraVisible ? View.VISIBLE : View.GONE);
+ } else {
+ // Default to MAIN
+ mMainViewList.add(child);
+ child.setVisibility(View.VISIBLE);
+ }
+ }
+
+ }
+
+ private void applyActiveState(boolean active) {
+ if (hasInfoRegion() && mInfoVisibility <= CARD_REGION_VISIBLE_ACTIVATED) {
+ setInfoViewVisibility(active);
+ }
+ if (hasExtraRegion() && mExtraVisibility <= CARD_REGION_VISIBLE_ACTIVATED) {
+ //setExtraVisibility(active);
+ }
+ }
+
+ private void setInfoViewVisibility(boolean visible) {
+ if (mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA) {
+ // Active state changes for card type
+ // CARD_TYPE_INFO_UNDER_WITH_EXTRA
+ if (visible) {
+ for (int i = 0; i < mInfoViewList.size(); i++) {
+ mInfoViewList.get(i).setVisibility(View.VISIBLE);
+ }
+ } else {
+ for (int i = 0; i < mInfoViewList.size(); i++) {
+ mInfoViewList.get(i).setVisibility(View.GONE);
+ }
+ for (int i = 0; i < mExtraViewList.size(); i++) {
+ mExtraViewList.get(i).setVisibility(View.GONE);
+ }
+ mInfoOffset = 0.0f;
+ }
+ } else if (mCardType == CARD_TYPE_INFO_UNDER) {
+ // Active state changes for card type CARD_TYPE_INFO_UNDER
+ if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
+ animateInfoHeight(visible);
+ } else {
+ for (int i = 0; i < mInfoViewList.size(); i++) {
+ mInfoViewList.get(i).setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+ } else if (mCardType == CARD_TYPE_INFO_OVER) {
+ // Active state changes for card type CARD_TYPE_INFO_OVER
+ animateInfoAlpha(visible);
+ }
+ }
+
+ private void applySelectedState(boolean focused) {
+ removeCallbacks(mAnimationTrigger);
+
+ if (mCardType == CARD_TYPE_INFO_UNDER_WITH_EXTRA) {
+ // Focus changes for card type CARD_TYPE_INFO_UNDER_WITH_EXTRA
+ if (focused) {
+ if (!mDelaySelectedAnim) {
+ post(mAnimationTrigger);
+ mDelaySelectedAnim = true;
+ } else {
+ postDelayed(mAnimationTrigger, mSelectedAnimationDelay);
+ }
+ } else {
+ animateInfoOffset(false);
+ }
+ } else if (mInfoVisibility == CARD_REGION_VISIBLE_SELECTED) {
+ setInfoViewVisibility(focused);
+ }
+ }
+
+ private void cancelAnimations() {
+ if (mAnim != null) {
+ mAnim.cancel();
+ mAnim = null;
+ }
+ }
+
+ // This animation changes the Y offset of the info and extra views,
+ // so that they animate UP to make the extra info area visible when a
+ // card is selected.
+ private void animateInfoOffset(boolean shown) {
+ cancelAnimations();
+
+ int extraHeight = 0;
+ if (shown) {
+ int widthSpec = MeasureSpec.makeMeasureSpec(mMeasuredWidth, MeasureSpec.EXACTLY);
+ int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+
+ for (int i = 0; i < mExtraViewList.size(); i++) {
+ View extraView = mExtraViewList.get(i);
+ extraView.setVisibility(View.VISIBLE);
+ extraView.measure(widthSpec, heightSpec);
+ extraHeight = Math.max(extraHeight, extraView.getMeasuredHeight());
+ }
+ }
+
+ mAnim = new InfoOffsetAnimation(mInfoOffset, shown ? extraHeight : 0);
+ mAnim.setDuration(mSelectedAnimDuration);
+ mAnim.setInterpolator(new AccelerateDecelerateInterpolator());
+ mAnim.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ if (mInfoOffset == 0f) {
+ for (int i = 0; i < mExtraViewList.size(); i++) {
+ mExtraViewList.get(i).setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ });
+ startAnimation(mAnim);
+ }
+
+ // This animation changes the visible height of the info views,
+ // so that they animate in and out of view.
+ private void animateInfoHeight(boolean shown) {
+ cancelAnimations();
+
+ int extraHeight = 0;
+ if (shown) {
+ int widthSpec = MeasureSpec.makeMeasureSpec(mMeasuredWidth, MeasureSpec.EXACTLY);
+ int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+
+ for (int i = 0; i < mExtraViewList.size(); i++) {
+ View extraView = mExtraViewList.get(i);
+ extraView.setVisibility(View.VISIBLE);
+ extraView.measure(widthSpec, heightSpec);
+ extraHeight = Math.max(extraHeight, extraView.getMeasuredHeight());
+ }
+ }
+
+ mAnim = new InfoHeightAnimation(mInfoVisFraction, shown ? 1.0f : 0f);
+ mAnim.setDuration(mSelectedAnimDuration);
+ mAnim.setInterpolator(new AccelerateDecelerateInterpolator());
+ mAnim.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ if (mInfoOffset == 0f) {
+ for (int i = 0; i < mExtraViewList.size(); i++) {
+ mExtraViewList.get(i).setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ });
+ startAnimation(mAnim);
+ }
+
+ // This animation changes the alpha of the info views, so they animate in
+ // and out. It's meant to be used when the info views are overlaid on top of
+ // the main view area. It gets triggered by a change in the Active state of
+ // the card.
+ private void animateInfoAlpha(boolean shown) {
+ cancelAnimations();
+
+ if (shown) {
+ for (int i = 0; i < mInfoViewList.size(); i++) {
+ mInfoViewList.get(i).setVisibility(View.VISIBLE);
+ }
+ }
+
+ mAnim = new InfoAlphaAnimation(mInfoAlpha, shown ? 1.0f : 0.0f);
+ mAnim.setDuration(mActivatedAnimDuration);
+ mAnim.setInterpolator(new DecelerateInterpolator());
+ mAnim.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ if (mInfoAlpha == 0.0) {
+ for (int i = 0; i < mInfoViewList.size(); i++) {
+ mInfoViewList.get(i).setVisibility(View.GONE);
+ }
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ });
+ startAnimation(mAnim);
+ }
+
+ @Override
+ public LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new BaseCardView.LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new BaseCardView.LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (lp instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) lp);
+ } else {
+ return new LayoutParams(lp);
+ }
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof BaseCardView.LayoutParams;
+ }
+
+ /**
+ * Per-child layout information associated with BaseCardView.
+ */
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ public static final int VIEW_TYPE_MAIN = 0;
+ public static final int VIEW_TYPE_INFO = 1;
+ public static final int VIEW_TYPE_EXTRA = 2;
+
+ /**
+ * Card component type for the view associated with these LayoutParams.
+ */
+ @ViewDebug.ExportedProperty(category = "layout", mapping = {
+ @ViewDebug.IntToString(from = VIEW_TYPE_MAIN, to = "MAIN"),
+ @ViewDebug.IntToString(from = VIEW_TYPE_INFO, to = "INFO"),
+ @ViewDebug.IntToString(from = VIEW_TYPE_EXTRA, to = "EXTRA")
+ })
+ public int viewType = VIEW_TYPE_MAIN;
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.lbBaseCardView_Layout);
+
+ viewType = a.getInt(
+ R.styleable.lbBaseCardView_Layout_layout_viewType, VIEW_TYPE_MAIN);
+
+ a.recycle();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public LayoutParams(ViewGroup.LayoutParams p) {
+ super(p);
+ }
+
+ /**
+ * Copy constructor. Clones the width, height, and View Type of the
+ * source.
+ *
+ * @param source The layout params to copy from.
+ */
+ public LayoutParams(LayoutParams source) {
+ super(source);
+
+ this.viewType = source.viewType;
+ }
+ }
+
+ // Helper animation class used in the animation of the info and extra
+ // fields vertically within the card
+ private class InfoOffsetAnimation extends Animation {
+ private float mStartValue;
+ private float mDelta;
+
+ public InfoOffsetAnimation(float start, float end) {
+ mStartValue = start;
+ mDelta = end - start;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mInfoOffset = mStartValue + (interpolatedTime * mDelta);
+ requestLayout();
+ }
+ }
+
+ // Helper animation class used in the animation of the visible height
+ // for the info fields.
+ private class InfoHeightAnimation extends Animation {
+ private float mStartValue;
+ private float mDelta;
+
+ public InfoHeightAnimation(float start, float end) {
+ mStartValue = start;
+ mDelta = end - start;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mInfoVisFraction = mStartValue + (interpolatedTime * mDelta);
+ requestLayout();
+ }
+ }
+
+ // Helper animation class used to animate the alpha for the info views
+ // when they are fading in or out of view.
+ private class InfoAlphaAnimation extends Animation {
+ private float mStartValue;
+ private float mDelta;
+
+ public InfoAlphaAnimation(float start, float end) {
+ mStartValue = start;
+ mDelta = end - start;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mInfoAlpha = mStartValue + (interpolatedTime * mDelta);
+ for (int i = 0; i < mInfoViewList.size(); i++) {
+ mInfoViewList.get(i).setAlpha(mInfoAlpha);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (DEBUG) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName()).append(" : ");
+ sb.append("cardType=");
+ switch(mCardType) {
+ case CARD_TYPE_MAIN_ONLY:
+ sb.append("MAIN_ONLY");
+ break;
+ case CARD_TYPE_INFO_OVER:
+ sb.append("INFO_OVER");
+ break;
+ case CARD_TYPE_INFO_UNDER:
+ sb.append("INFO_UNDER");
+ break;
+ case CARD_TYPE_INFO_UNDER_WITH_EXTRA:
+ sb.append("INFO_UNDER_WITH_EXTRA");
+ break;
+ default:
+ sb.append("INVALID");
+ break;
+ }
+ sb.append(" : ");
+ sb.append(mMainViewList.size()).append(" main views, ");
+ sb.append(mInfoViewList.size()).append(" info views, ");
+ sb.append(mExtraViewList.size()).append(" extra views : ");
+ sb.append("infoVisibility=").append(mInfoVisibility).append(" ");
+ sb.append("extraVisibility=").append(mExtraVisibility).append(" ");
+ sb.append("isActivated=").append(isActivated());
+ sb.append(" : ");
+ sb.append("isSelected=").append(isSelected());
+ return sb.toString();
+ } else {
+ return super.toString();
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
new file mode 100644
index 0000000..854f5de
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/BaseGridView.java
@@ -0,0 +1,470 @@
+/*
+ * 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.content.res.TypedArray;
+import android.graphics.Rect;
+import android.support.v17.leanback.R;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+
+/**
+ * Base class for vertically and horizontally scrolling lists. The items come
+ * from the {@link RecyclerView.Adapter} associated with this view.
+ * @hide
+ */
+abstract class BaseGridView extends RecyclerView {
+
+ /**
+ * Always keep focused item at a aligned position. Developer can use
+ * WINDOW_ALIGN_XXX and ITEM_ALIGN_XXX to define how focused item is aligned.
+ * In this mode, the last focused position will be remembered and restored when focus
+ * is back to the view.
+ */
+ public final static int FOCUS_SCROLL_ALIGNED = 0;
+
+ /**
+ * Scroll to make the focused item inside client area.
+ */
+ public final static int FOCUS_SCROLL_ITEM = 1;
+
+ /**
+ * Scroll a page of items when focusing to item outside the client area.
+ * The page size matches the client area size of RecyclerView.
+ */
+ public final static int FOCUS_SCROLL_PAGE = 2;
+
+ /**
+ * The first item is aligned with the low edge of the viewport. When
+ * navigating away from the first item, the focus maintains a middle
+ * location.
+ * <p>
+ * The middle location is calculated by "windowAlignOffset" and
+ * "windowAlignOffsetPercent"; if neither of these two is defined, the
+ * default value is 1/2 of the size.
+ */
+ public final static int WINDOW_ALIGN_LOW_EDGE = 1;
+
+ /**
+ * The last item is aligned with the high edge of the viewport when
+ * navigating to the end of list. When navigating away from the end, the
+ * focus maintains a middle location.
+ * <p>
+ * The middle location is calculated by "windowAlignOffset" and
+ * "windowAlignOffsetPercent"; if neither of these two is defined, the
+ * default value is 1/2 of the size.
+ */
+ public final static int WINDOW_ALIGN_HIGH_EDGE = 1 << 1;
+
+ /**
+ * The first item and last item are aligned with the two edges of the
+ * viewport. When navigating in the middle of list, the focus maintains a
+ * middle location.
+ * <p>
+ * The middle location is calculated by "windowAlignOffset" and
+ * "windowAlignOffsetPercent"; if neither of these two is defined, the
+ * default value is 1/2 of the size.
+ */
+ public final static int WINDOW_ALIGN_BOTH_EDGE =
+ WINDOW_ALIGN_LOW_EDGE | WINDOW_ALIGN_HIGH_EDGE;
+
+ /**
+ * The focused item always stays in a middle location.
+ * <p>
+ * The middle location is calculated by "windowAlignOffset" and
+ * "windowAlignOffsetPercent"; if neither of these two is defined, the
+ * default value is 1/2 of the size.
+ */
+ public final static int WINDOW_ALIGN_NO_EDGE = 0;
+
+ /**
+ * Value indicates that percent is not used.
+ */
+ public final static float WINDOW_ALIGN_OFFSET_PERCENT_DISABLED = -1;
+
+ /**
+ * Value indicates that percent is not used.
+ */
+ public final static float ITEM_ALIGN_OFFSET_PERCENT_DISABLED = -1;
+
+ protected final GridLayoutManager mLayoutManager;
+
+ public BaseGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mLayoutManager = new GridLayoutManager(this);
+ setLayoutManager(mLayoutManager);
+ setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ setHasFixedSize(true);
+ setChildrenDrawingOrderEnabled(true);
+ }
+
+ protected void initBaseGridViewAttributes(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseGridView);
+ boolean throughFront = a.getBoolean(R.styleable.lbBaseGridView_focusOutFront, false);
+ boolean throughEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutEnd, false);
+ mLayoutManager.setFocusOutAllowed(throughFront, throughEnd);
+ mLayoutManager.setVerticalMargin(
+ a.getDimensionPixelSize(R.styleable.lbBaseGridView_verticalMargin, 0));
+ mLayoutManager.setHorizontalMargin(
+ a.getDimensionPixelSize(R.styleable.lbBaseGridView_horizontalMargin, 0));
+ a.recycle();
+ }
+
+ /**
+ * Set the strategy used to scroll in response to item focus changing:
+ * <ul>
+ * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
+ * <li>{@link #FOCUS_SCROLL_ITEM}</li>
+ * <li>{@link #FOCUS_SCROLL_PAGE}</li>
+ * </ul>
+ */
+ public void setFocusScrollStrategy(int scrollStrategy) {
+ if (scrollStrategy != FOCUS_SCROLL_ALIGNED && scrollStrategy != FOCUS_SCROLL_ITEM
+ && scrollStrategy != FOCUS_SCROLL_PAGE) {
+ throw new IllegalArgumentException("Invalid scrollStrategy");
+ }
+ mLayoutManager.setFocusScrollStrategy(scrollStrategy);
+ requestLayout();
+ }
+
+ /**
+ * Returns the strategy used to scroll in response to item focus changing.
+ * <ul>
+ * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li>
+ * <li>{@link #FOCUS_SCROLL_ITEM}</li>
+ * <li>{@link #FOCUS_SCROLL_PAGE}</li>
+ * </ul>
+ */
+ public int getFocusScrollStrategy() {
+ return mLayoutManager.getFocusScrollStrategy();
+ }
+
+ /**
+ * Set how the focused item gets aligned in the view.
+ *
+ * @param windowAlignment {@link #WINDOW_ALIGN_BOTH_EDGE},
+ * {@link #WINDOW_ALIGN_LOW_EDGE}, {@link #WINDOW_ALIGN_HIGH_EDGE} or
+ * {@link #WINDOW_ALIGN_NO_EDGE}.
+ */
+ public void setWindowAlignment(int windowAlignment) {
+ mLayoutManager.setWindowAlignment(windowAlignment);
+ requestLayout();
+ }
+
+ /**
+ * Get how the focused item gets aligned in the view.
+ *
+ * @return {@link #WINDOW_ALIGN_BOTH_EDGE}, {@link #WINDOW_ALIGN_LOW_EDGE},
+ * {@link #WINDOW_ALIGN_HIGH_EDGE} or {@link #WINDOW_ALIGN_NO_EDGE}.
+ */
+ public int getWindowAlignment() {
+ return mLayoutManager.getWindowAlignment();
+ }
+
+ /**
+ * Set the absolute offset in pixels for window alignment.
+ *
+ * @param offset The number of pixels to offset. Can be negative for
+ * alignment from the high edge, or positive for alignment from the
+ * low edge.
+ */
+ public void setWindowAlignmentOffset(int offset) {
+ mLayoutManager.setWindowAlignmentOffset(offset);
+ requestLayout();
+ }
+
+ /**
+ * Get the absolute offset in pixels for window alignment.
+ *
+ * @return The number of pixels to offset. Will be negative for alignment
+ * from the high edge, or positive for alignment from the low edge.
+ * Default value is 0.
+ */
+ public int getWindowAlignmentOffset() {
+ return mLayoutManager.getWindowAlignmentOffset();
+ }
+
+ /**
+ * Set offset percent for window alignment in addition to {@link
+ * #getWindowAlignmentOffset()}.
+ *
+ * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
+ * width from low edge. Use
+ * {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
+ */
+ public void setWindowAlignmentOffsetPercent(float offsetPercent) {
+ mLayoutManager.setWindowAlignmentOffsetPercent(offsetPercent);
+ requestLayout();
+ }
+
+ /**
+ * Get offset percent for window alignment in addition to
+ * {@link #getWindowAlignmentOffset()}.
+ *
+ * @return Percentage to offset. E.g., 40 means 40% of the width from the
+ * low edge, or {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} if
+ * disabled. Default value is 50.
+ */
+ public float getWindowAlignmentOffsetPercent() {
+ return mLayoutManager.getWindowAlignmentOffsetPercent();
+ }
+
+ /**
+ * Set the absolute offset in pixels for item alignment.
+ *
+ * @param offset The number of pixels to offset. Can be negative for
+ * alignment from the high edge, or positive for alignment from the
+ * low edge.
+ */
+ public void setItemAlignmentOffset(int offset) {
+ mLayoutManager.setItemAlignmentOffset(offset);
+ requestLayout();
+ }
+
+ /**
+ * Get the absolute offset in pixels for item alignment.
+ *
+ * @return The number of pixels to offset. Will be negative for alignment
+ * from the high edge, or positive for alignment from the low edge.
+ * Default value is 0.
+ */
+ public int getItemAlignmentOffset() {
+ return mLayoutManager.getItemAlignmentOffset();
+ }
+
+ /**
+ * Set offset percent for item alignment in addition to {@link
+ * #getItemAlignmentOffset()}.
+ *
+ * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the
+ * width from the low edge. Use
+ * {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} to disable.
+ */
+ public void setItemAlignmentOffsetPercent(float offsetPercent) {
+ mLayoutManager.setItemAlignmentOffsetPercent(offsetPercent);
+ requestLayout();
+ }
+
+ /**
+ * Get offset percent for item alignment in addition to {@link
+ * #getItemAlignmentOffset()}.
+ *
+ * @return Percentage to offset. E.g., 40 means 40% of the width from the
+ * low edge, or {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} if
+ * disabled. Default value is 50.
+ */
+ public float getItemAlignmentOffsetPercent() {
+ return mLayoutManager.getItemAlignmentOffsetPercent();
+ }
+
+ /**
+ * Set the id of the view to align with. Use zero (default) for the item
+ * view itself.
+ */
+ public void setItemAlignmentViewId(int viewId) {
+ mLayoutManager.setItemAlignmentViewId(viewId);
+ }
+
+ /**
+ * Get the id of the view to align with, or zero for the item view itself.
+ */
+ public int getItemAlignmentViewId() {
+ return mLayoutManager.getItemAlignmentViewId();
+ }
+
+ /**
+ * Set the margin in pixels between two child items.
+ */
+ public void setItemMargin(int margin) {
+ mLayoutManager.setItemMargin(margin);
+ requestLayout();
+ }
+
+ /**
+ * Set the margin in pixels between two child items vertically.
+ */
+ public void setVerticalMargin(int margin) {
+ mLayoutManager.setVerticalMargin(margin);
+ requestLayout();
+ }
+
+ /**
+ * Get the margin in pixels between two child items vertically.
+ */
+ public int getVerticalMargin() {
+ return mLayoutManager.getVerticalMargin();
+ }
+
+ /**
+ * Set the margin in pixels between two child items horizontally.
+ */
+ public void setHorizontalMargin(int margin) {
+ mLayoutManager.setHorizontalMargin(margin);
+ requestLayout();
+ }
+
+ /**
+ * Get the margin in pixels between two child items horizontally.
+ */
+ public int getHorizontalMargin() {
+ return mLayoutManager.getHorizontalMargin();
+ }
+
+ /**
+ * Register a callback to be invoked when an item in BaseGridView has
+ * been selected.
+ */
+ public void setOnChildSelectedListener(OnChildSelectedListener listener) {
+ mLayoutManager.setOnChildSelectedListener(listener);
+ }
+
+ /**
+ * Change the selected item immediately without animation.
+ */
+ public void setSelectedPosition(int position) {
+ mLayoutManager.setSelection(this, position);
+ }
+
+ /**
+ * Change the selected item and run an animation to scroll to the target
+ * position.
+ */
+ public void setSelectedPositionSmooth(int position) {
+ mLayoutManager.setSelectionSmooth(this, position);
+ }
+
+ /**
+ * Get the selected item position.
+ */
+ public int getSelectedPosition() {
+ return mLayoutManager.getSelection();
+ }
+
+ /**
+ * Set if an animation should run when a child changes size or when adding
+ * or removing a child.
+ * <p><i>Unstable API, might change later.</i>
+ */
+ public void setAnimateChildLayout(boolean animateChildLayout) {
+ mLayoutManager.setAnimateChildLayout(animateChildLayout);
+ }
+
+ /**
+ * Return true if an animation will run when a child changes size or when
+ * adding or removing a child.
+ * <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();
+ }
+
+ /**
+ * Describes how the child views are positioned. Defaults to
+ * GRAVITY_TOP|GRAVITY_LEFT.
+ *
+ * @param gravity See {@link android.view.Gravity}
+ */
+ public void setGravity(int gravity) {
+ mLayoutManager.setGravity(gravity);
+ requestLayout();
+ }
+
+ @Override
+ public void setDescendantFocusability (int focusability) {
+ // enforce FOCUS_AFTER_DESCENDANTS
+ super.setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+ }
+
+ @Override
+ public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
+ return mLayoutManager.gridOnRequestFocusInDescendants(this, direction,
+ previouslyFocusedRect);
+ }
+
+ /**
+ * Get the x/y offsets to final position from current position if the view
+ * is selected.
+ *
+ * @param view The view to get offsets.
+ * @param offsets offsets[0] holds offset of X, offsets[1] holds offset of
+ * Y.
+ */
+ public void getViewSelectedOffsets(View view, int[] offsets) {
+ mLayoutManager.getViewSelectedOffsets(view, offsets);
+ }
+
+ @Override
+ public int getChildDrawingOrder(int childCount, int i) {
+ return mLayoutManager.getChildDrawingOrder(this, childCount, i);
+ }
+
+ final boolean isChildrenDrawingOrderEnabledInternal() {
+ return isChildrenDrawingOrderEnabled();
+ }
+
+ /**
+ * Disable or enable focus search.
+ */
+ public final void setFocusSearchDisabled(boolean disabled) {
+ mLayoutManager.setFocusSearchDisabled(disabled);
+ }
+
+ /**
+ * Return true if focus search is disabled.
+ */
+ public final boolean isFocusSearchDisabled() {
+ return mLayoutManager.isFocusSearchDisabled();
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java b/v17/leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java
new file mode 100644
index 0000000..64d2270
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ClassPresenterSelector.java
@@ -0,0 +1,42 @@
+/*
+ * 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 java.util.HashMap;
+
+/**
+ * A ClassPresenterSelector selects a {@link Presenter} based on the item's
+ * Java class.
+ */
+public final class ClassPresenterSelector extends PresenterSelector {
+
+ private final HashMap<Class<?>, Presenter> mClassMap = new HashMap<Class<?>, Presenter>();
+
+ public void addClassPresenter(Class<?> cls, Presenter presenter) {
+ mClassMap.put(cls, presenter);
+ }
+
+ @Override
+ public Presenter getPresenter(Object item) {
+ Class<?> cls = item.getClass();
+ Presenter presenter = null;
+
+ do {
+ presenter = mClassMap.get(cls);
+ cls = cls.getSuperclass();
+ } while (presenter == null && cls != null);
+
+ return presenter;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
new file mode 100644
index 0000000..1968822
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/CursorObjectAdapter.java
@@ -0,0 +1,176 @@
+/*
+ * 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.database.Cursor;
+import android.support.v17.leanback.database.CursorMapper;
+import android.util.LruCache;
+
+/**
+ * Adapter implemented with a {@link Cursor}.
+ */
+public class CursorObjectAdapter extends ObjectAdapter {
+ private static final int CACHE_SIZE = 100;
+ private Cursor mCursor;
+ private CursorMapper mMapper;
+ private final LruCache<Integer, Object> mItemCache = new LruCache<Integer, Object>(CACHE_SIZE);
+
+ public CursorObjectAdapter(PresenterSelector presenterSelector) {
+ super(presenterSelector);
+ }
+
+ public CursorObjectAdapter(Presenter presenter) {
+ super(presenter);
+ }
+
+ public CursorObjectAdapter() {
+ super();
+ }
+
+ /**
+ * Change the underlying cursor to a new cursor. If there is
+ * an existing cursor it will be closed.
+ *
+ * @param cursor The new cursor to be used.
+ */
+ public void changeCursor(Cursor cursor) {
+ if (cursor == mCursor) {
+ return;
+ }
+ if (mCursor != null) {
+ mCursor.close();
+ }
+ mCursor = cursor;
+ mItemCache.trimToSize(0);
+ onCursorChanged();
+ }
+
+ /**
+ * Swap in a new Cursor, returning the old Cursor. Unlike changeCursor(Cursor),
+ * the returned old Cursor is not closed.
+ *
+ * @param cursor The new cursor to be used.
+ */
+ public Cursor swapCursor(Cursor cursor) {
+ if (cursor == mCursor) {
+ return mCursor;
+ }
+ Cursor oldCursor = mCursor;
+ mCursor = cursor;
+ mItemCache.trimToSize(0);
+ onCursorChanged();
+ return oldCursor;
+ }
+
+ /**
+ * Called whenever the cursor changes.
+ */
+ protected void onCursorChanged() {
+ notifyChanged();
+ }
+
+ /**
+ * Gets the {@link Cursor} backing the adapter.
+ */
+ public final Cursor getCursor() {
+ return mCursor;
+ }
+
+ /**
+ * Sets the {@link CursorMapper} used to convert {@link Cursor} rows into
+ * Objects.
+ */
+ public final void setMapper(CursorMapper mapper) {
+ boolean changed = mMapper != mapper;
+ mMapper = mapper;
+
+ if (changed) {
+ onMapperChanged();
+ }
+ }
+
+ /**
+ * Called when {@link #setMapper(CursorMapper)} is called and a different
+ * mapper is provided.
+ */
+ protected void onMapperChanged() {
+ }
+
+ /**
+ * Gets the {@link CursorMapper} used to convert {@link Cursor} rows into
+ * Objects.
+ */
+ public final CursorMapper getMapper() {
+ return mMapper;
+ }
+
+ @Override
+ public int size() {
+ if (mCursor == null) {
+ return 0;
+ }
+ return mCursor.getCount();
+ }
+
+ @Override
+ public Object get(int index) {
+ if (mCursor == null) {
+ return null;
+ }
+ if (!mCursor.moveToPosition(index)) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ Object item = mItemCache.get(index);
+ if (item != null) {
+ return item;
+ }
+ item = mMapper.convert(mCursor);
+ mItemCache.put(index, item);
+ return item;
+ }
+
+ /**
+ * Closes this adapter, closing the backing {@link Cursor} as well.
+ */
+ public void close() {
+ if (mCursor != null) {
+ mCursor.close();
+ mCursor = null;
+ }
+ }
+
+ /**
+ * Checks whether the adapter, and hence the backing {@link Cursor}, is closed.
+ */
+ public boolean isClosed() {
+ return mCursor == null || mCursor.isClosed();
+ }
+
+ /**
+ * Remove an item from the cache. This will force the item to be re-read
+ * from the data source the next time (@link #get(int)} is called.
+ */
+ protected final void invalidateCache(int index) {
+ mItemCache.remove(index);
+ }
+
+ /**
+ * Remove {@code count} items starting at {@code index}.
+ */
+ protected final void invalidateCache(int index, int count) {
+ for (int limit = count + index; index < limit; index++) {
+ invalidateCache(index);
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java b/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
new file mode 100644
index 0000000..e5dbfaf
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRow.java
@@ -0,0 +1,126 @@
+/*
+ * 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.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+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
+ * the item.
+ */
+public class DetailsOverviewRow extends Row {
+
+ private Object mItem;
+ private Drawable mImageDrawable;
+ private ArrayList<Action> mActions = new ArrayList<Action>();
+
+ /**
+ * Constructor.
+ *
+ * @param item The main item for the details page.
+ */
+ public DetailsOverviewRow(Object item) {
+ super(null);
+ mItem = item;
+ verify();
+ }
+
+ /**
+ * Gets the main item for the details page.
+ */
+ public final Object getItem() {
+ return mItem;
+ }
+
+ /**
+ * Sets a drawable as the image of this details overview.
+ *
+ * @param drawable The drawable to set.
+ */
+ public final void setImageDrawable(Drawable drawable) {
+ mImageDrawable = drawable;
+ }
+
+ /**
+ * Sets a Bitmap as the image of this details overview.
+ *
+ * @param context The context to retrieve display metrics from.
+ * @param bm The bitmap to set.
+ */
+ public final void setImageBitmap(Context context, Bitmap bm) {
+ mImageDrawable = new BitmapDrawable(context.getResources(), bm);
+ }
+
+ /**
+ * Gets the image drawable of this details overview.
+ *
+ * @return The overview's image drawable, or null if no drawable has been
+ * assigned.
+ */
+ public final Drawable getImageDrawable() {
+ return mImageDrawable;
+ }
+
+ /**
+ * Add an action to the overview.
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @return An unmodifiable view of the list of actions.
+ */
+ public final List<Action> getActions() {
+ return Collections.unmodifiableList(mActions);
+ }
+
+ private void verify() {
+ if (mItem == null) {
+ throw new IllegalArgumentException("Object cannot be null");
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
new file mode 100644
index 0000000..3504581
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/DetailsOverviewRowPresenter.java
@@ -0,0 +1,127 @@
+/*
+ * 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.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import java.util.Collection;
+
+/**
+ * DetailsOverviewRowPresenter renders {@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
+ * DetailsFragment}.
+ *
+ * <p>The detailed description is rendered using a {@link Presenter}.
+ */
+public class DetailsOverviewRowPresenter extends RowPresenter {
+
+ private static final String TAG = "DetailsOverviewRowPresenter";
+ private static final boolean DEBUG = false;
+
+ public static class ViewHolder extends RowPresenter.ViewHolder {
+ final ImageView mImageView;
+ final FrameLayout mDetailsDescriptionFrame;
+ final HorizontalGridView mActionsRow;
+ Presenter.ViewHolder mDetailsDescriptionViewHolder;
+
+ public ViewHolder(View rootView) {
+ super(rootView);
+ mImageView = (ImageView) rootView.findViewById(R.id.details_overview_image);
+ mDetailsDescriptionFrame =
+ (FrameLayout) rootView.findViewById(R.id.details_overview_description);
+ mActionsRow =
+ (HorizontalGridView) rootView.findViewById(R.id.details_overview_actions);
+ }
+ }
+
+ private final Presenter mDetailsPresenter;
+ private final ActionPresenterSelector mActionPresenterSelector;
+ private final ItemBridgeAdapter mActionBridgeAdapter;
+
+ /**
+ * Constructor that uses the given {@link Presenter} to render the detailed
+ * description for the row.
+ */
+ public DetailsOverviewRowPresenter(Presenter detailsPresenter) {
+ setSelectEffectEnabled(false);
+ mDetailsPresenter = detailsPresenter;
+ mActionPresenterSelector = new ActionPresenterSelector();
+ mActionBridgeAdapter = new ItemBridgeAdapter();
+ FocusHighlightHelper.setupActionItemFocusHighlight(mActionBridgeAdapter);
+ }
+
+ /**
+ * Set the listener for action click events.
+ */
+ public void setOnActionClickedListener(OnActionClickedListener listener) {
+ mActionPresenterSelector.setOnActionClickedListener(listener);
+ }
+
+ /**
+ * Get the listener for action click events.
+ */
+ public OnActionClickedListener getOnActionClickedListener() {
+ return mActionPresenterSelector.getOnActionClickedListener();
+ }
+
+ @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);
+
+ return vh;
+ }
+
+ @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());
+ }
+ if (vh.mDetailsDescriptionViewHolder == null) {
+ }
+ 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);
+ }
+
+ @Override
+ protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
+ super.onUnbindRowViewHolder(holder);
+
+ ViewHolder vh = (ViewHolder) holder;
+ if (vh.mDetailsDescriptionViewHolder != null) {
+ mDetailsPresenter.onUnbindViewHolder(vh.mDetailsDescriptionViewHolder);
+ }
+
+ vh.mActionsRow.setAdapter(null);
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlight.java b/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlight.java
new file mode 100644
index 0000000..2171992
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlight.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.v17.leanback.widget;
+
+import android.view.View;
+
+/**
+ * Interface for highlighting the item that has focus.
+ *
+ */
+public interface FocusHighlight {
+ /**
+ * No zoom factor.
+ */
+ public static final int ZOOM_FACTOR_NONE = 0;
+
+ /**
+ * A small zoom factor, recommended for large item views.
+ */
+ public static final int ZOOM_FACTOR_SMALL = 1;
+
+ /**
+ * A medium zoom factor, recommended for medium sized item views.
+ */
+ public static final int ZOOM_FACTOR_MEDIUM = 2;
+
+ /**
+ * A large zoom factor, recommended for small item views.
+ */
+ public static final int ZOOM_FACTOR_LARGE = 3;
+
+ /**
+ * Called when an item gains or loses focus.
+ * @hide
+ *
+ * @param view The view whose focus is changing.
+ * @param hasFocus True if focus is gained; false otherwise.
+ */
+ void onItemFocused(View view, boolean hasFocus);
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java b/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
new file mode 100644
index 0000000..d027ede
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/FocusHighlightHelper.java
@@ -0,0 +1,258 @@
+/*
+ * 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.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.
+ */
+public class FocusHighlightHelper {
+
+ static class FocusAnimator implements TimeAnimator.TimeListener {
+ private final View mView;
+ private final int mDuration;
+ private final ShadowOverlayContainer mWrapper;
+ private final float mScaleDiff;
+ private float mFocusLevel = 0f;
+ private float mFocusLevelStart;
+ private float mFocusLevelDelta;
+ private final TimeAnimator mAnimator = new TimeAnimator();
+ private final Interpolator mInterpolator = new AccelerateDecelerateInterpolator();
+
+ void animateFocus(boolean select, boolean immediate) {
+ endAnimation();
+ final float end = select ? 1 : 0;
+ if (immediate) {
+ setFocusLevel(end);
+ } else if (mFocusLevel != end) {
+ mFocusLevelStart = mFocusLevel;
+ mFocusLevelDelta = end - mFocusLevelStart;
+ mAnimator.start();
+ }
+ }
+
+ FocusAnimator(View view, float scale, int duration) {
+ mView = view;
+ mDuration = duration;
+ mScaleDiff = scale - 1f;
+ if (view instanceof ShadowOverlayContainer) {
+ mWrapper = (ShadowOverlayContainer) view;
+ } else {
+ mWrapper = null;
+ }
+ mAnimator.setTimeListener(this);
+ }
+
+ void setFocusLevel(float level) {
+ mFocusLevel = level;
+ float scale = 1f + mScaleDiff * level;
+ mView.setScaleX(scale);
+ mView.setScaleY(scale);
+ if (mWrapper != null) {
+ mWrapper.setShadowFocusLevel(level);
+ }
+ }
+
+ float getFocusLevel() {
+ return mFocusLevel;
+ }
+
+ void endAnimation() {
+ mAnimator.end();
+ }
+
+ @Override
+ public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
+ float fraction;
+ if (totalTime >= mDuration) {
+ fraction = 1;
+ mAnimator.end();
+ } else {
+ fraction = (float) (totalTime / (double) mDuration);
+ }
+ if (mInterpolator != null) {
+ fraction = mInterpolator.getInterpolation(fraction);
+ }
+ setFocusLevel(mFocusLevelStart + fraction * mFocusLevelDelta);
+ }
+ }
+
+ static class BrowseItemFocusHighlight implements FocusHighlight {
+ private static final int DURATION_MS = 150;
+
+ private static float[] sScaleFactor = new float[4];
+
+ private int mScaleIndex;
+
+ BrowseItemFocusHighlight(int zoomIndex) {
+ mScaleIndex = (zoomIndex >= 0 && zoomIndex < sScaleFactor.length) ?
+ zoomIndex : ZOOM_FACTOR_MEDIUM;
+ }
+
+ private static void lazyInit(Resources resources) {
+ if (sScaleFactor[ZOOM_FACTOR_NONE] == 0f) {
+ sScaleFactor[ZOOM_FACTOR_NONE] = 1f;
+ sScaleFactor[ZOOM_FACTOR_SMALL] =
+ resources.getFraction(R.fraction.lb_focus_zoom_factor_small, 1, 1);
+ sScaleFactor[ZOOM_FACTOR_MEDIUM] =
+ resources.getFraction(R.fraction.lb_focus_zoom_factor_medium, 1, 1);
+ sScaleFactor[ZOOM_FACTOR_LARGE] =
+ resources.getFraction(R.fraction.lb_focus_zoom_factor_large, 1, 1);
+ }
+ }
+
+ private float getScale(View view) {
+ lazyInit(view.getResources());
+ return sScaleFactor[mScaleIndex];
+ }
+
+ private void viewFocused(View view, boolean hasFocus) {
+ view.setSelected(hasFocus);
+ FocusAnimator animator = (FocusAnimator) view.getTag(R.id.lb_focus_animator);
+ if (animator == null) {
+ animator = new FocusAnimator(view, getScale(view), DURATION_MS);
+ view.setTag(R.id.lb_focus_animator, animator);
+ }
+ animator.animateFocus(hasFocus, false);
+ }
+
+ @Override
+ public void onItemFocused(View view, boolean hasFocus) {
+ viewFocused(view, hasFocus);
+ }
+ }
+
+ 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.
+ */
+ public static void setupBrowseItemFocusHighlight(ItemBridgeAdapter adapter, int zoomIndex) {
+ adapter.setFocusHighlight(new BrowseItemFocusHighlight(zoomIndex));
+ }
+
+ /**
+ * Setup the focus highlight behavior of a focused item in header list.
+ * @param gridView the header list.
+ */
+ public static void setupHeaderItemFocusHighlight(VerticalGridView gridView) {
+ if (gridView.getAdapter() instanceof ItemBridgeAdapter) {
+ ((ItemBridgeAdapter) gridView.getAdapter())
+ .setFocusHighlight(new HeaderItemFocusHighlight(gridView));
+ }
+ }
+
+ /**
+ * 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;
+ private static int sDuration;
+ private BaseGridView mGridView;
+
+ HeaderItemFocusHighlight(BaseGridView gridView) {
+ mGridView = gridView;
+ lazyInit(gridView.getContext().getResources());
+ }
+
+ private static void lazyInit(Resources res) {
+ if (!sInitialized) {
+ sSelectScale =
+ Float.parseFloat(res.getString(R.dimen.lb_browse_header_select_scale));
+ sDuration =
+ Integer.parseInt(res.getString(R.dimen.lb_browse_header_select_duration));
+ sInitialized = true;
+ }
+ }
+
+ class HeaderFocusAnimator extends FocusAnimator {
+
+ ItemBridgeAdapter.ViewHolder mViewHolder;
+ HeaderFocusAnimator(View view, float scale, int duration) {
+ super(view, scale, duration);
+ mViewHolder = (ItemBridgeAdapter.ViewHolder) mGridView.getChildViewHolder(view);
+ }
+
+ @Override
+ void setFocusLevel(float level) {
+ Presenter presenter = mViewHolder.getPresenter();
+ if (presenter instanceof RowHeaderPresenter) {
+ ((RowHeaderPresenter) presenter).setSelectLevel(
+ ((RowHeaderPresenter.ViewHolder) mViewHolder.getViewHolder()), level);
+ }
+ super.setFocusLevel(level);
+ }
+
+ }
+
+ private void viewFocused(View view, boolean hasFocus) {
+ view.setSelected(hasFocus);
+ FocusAnimator animator = (FocusAnimator) view.getTag(R.id.lb_focus_animator);
+ if (animator == null) {
+ animator = new HeaderFocusAnimator(view, sSelectScale, sDuration);
+ view.setTag(R.id.lb_focus_animator, animator);
+ }
+ animator.animateFocus(hasFocus, false);
+ }
+
+ @Override
+ public void onItemFocused(View view, boolean hasFocus) {
+ 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
new file mode 100644
index 0000000..0ad0669
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -0,0 +1,2016 @@
+/*
+ * 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.animation.TimeAnimator;
+import android.content.Context;
+import android.graphics.Rect;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.RecyclerView.Adapter;
+import android.support.v7.widget.RecyclerView.Recycler;
+
+import static android.support.v7.widget.RecyclerView.NO_ID;
+import static android.support.v7.widget.RecyclerView.NO_POSITION;
+import static android.support.v7.widget.RecyclerView.HORIZONTAL;
+import static android.support.v7.widget.RecyclerView.VERTICAL;
+
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.FocusFinder;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewParent;
+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;
+import java.util.ArrayList;
+import java.util.List;
+
+final class GridLayoutManager extends RecyclerView.LayoutManager {
+
+ /*
+ * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}.
+ * The class currently does three internal jobs:
+ * - Saves optical bounds insets.
+ * - Caches focus align view center.
+ * - Manages child view layout animation.
+ */
+ static class LayoutParams extends RecyclerView.LayoutParams {
+
+ // The view is saved only during animation.
+ private View mView;
+
+ // For placement
+ private int mLeftInset;
+ private int mTopInset;
+ private int mRighInset;
+ private int mBottomInset;
+
+ // For alignment
+ 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);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(RecyclerView.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+ }
+
+ void onViewAttached() {
+ endAnimate();
+ mFirstAttached = true;
+ }
+
+ void onViewDetached() {
+ endAnimate();
+ }
+
+ int getAlignX() {
+ return mAlignX;
+ }
+
+ int getAlignY() {
+ return mAlignY;
+ }
+
+ int getOpticalLeft(View view) {
+ return view.getLeft() + mLeftInset;
+ }
+
+ int getOpticalTop(View view) {
+ return view.getTop() + mTopInset;
+ }
+
+ int getOpticalRight(View view) {
+ return view.getRight() - mRighInset;
+ }
+
+ int getOpticalBottom(View view) {
+ return view.getBottom() - mBottomInset;
+ }
+
+ int getOpticalWidth(View view) {
+ return view.getWidth() - mLeftInset - mRighInset;
+ }
+
+ int getOpticalHeight(View view) {
+ return view.getHeight() - mTopInset - mBottomInset;
+ }
+
+ void setAlignX(int alignX) {
+ mAlignX = alignX;
+ }
+
+ void setAlignY(int alignY) {
+ mAlignY = alignY;
+ }
+
+ void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) {
+ mLeftInset = leftInset;
+ mTopInset = topInset;
+ mRighInset = rightInset;
+ 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) {
+ // TODO: we only need invalidate parent if it has ItemDecoration
+ ((RecyclerView) parent).invalidate();
+ }
+ }
+ }
+
+ 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();
+ }
+
+ private final BaseGridView mBaseGridView;
+
+ /**
+ * The orientation of a "row".
+ */
+ private int mOrientation = HORIZONTAL;
+
+ private RecyclerView.Adapter mAdapter;
+ private RecyclerView.Recycler mRecycler;
+
+ private boolean mInLayout = false;
+
+ private OnChildSelectedListener mChildSelectedListener = null;
+
+ /**
+ * The focused position, it's not the currently visually aligned position
+ * but it is the final position that we intend to focus on. If there are
+ * multiple setSelection() called, mFocusPosition saves last value.
+ */
+ private int mFocusPosition = NO_POSITION;
+
+ /**
+ * Force a full layout under certain situations.
+ */
+ private boolean mForceFullLayout;
+
+ /**
+ * 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.
+ */
+ private int mItemLengthSecondaryRequested;
+ /**
+ * 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;
+
+ /**
+ * Margin between items.
+ */
+ private int mHorizontalMargin;
+ /**
+ * Margin between items vertically.
+ */
+ private int mVerticalMargin;
+ /**
+ * Margin in main direction.
+ */
+ private int mMarginPrimary;
+ /**
+ * Margin in second direction.
+ */
+ private int mMarginSecondary;
+ /**
+ * How to position child in secondary direction.
+ */
+ private int mGravity = Gravity.LEFT | Gravity.TOP;
+ /**
+ * The number of rows in the grid.
+ */
+ private int mNumRows;
+ /**
+ * Number of rows requested, can be 0 to be determined by parent size and
+ * rowHeight.
+ */
+ private int mNumRowsRequested = 1;
+
+ /**
+ * Tracking start/end position of each row for visible items.
+ */
+ private StaggeredGrid.Row[] mRows;
+
+ /**
+ * Saves grid information of each view.
+ */
+ private StaggeredGrid mGrid;
+ /**
+ * Position of first item (included) that has attached views.
+ */
+ private int mFirstVisiblePos;
+ /**
+ * Position of last item (included) that has attached views.
+ */
+ private int mLastVisiblePos;
+
+ /**
+ * Focus Scroll strategy.
+ */
+ private int mFocusScrollStrategy = BaseGridView.FOCUS_SCROLL_ALIGNED;
+ /**
+ * Defines how item view is aligned in the window.
+ */
+ private final WindowAlignment mWindowAlignment = new WindowAlignment();
+
+ /**
+ * Defines how item view is aligned.
+ */
+ private final ItemAlignment mItemAlignment = new ItemAlignment();
+
+ /**
+ * Dimensions of the view, width or height depending on orientation.
+ */
+ private int mSizePrimary;
+
+ /**
+ * Allow DPAD key to navigate out at the front of the View (where position = 0),
+ * default is false.
+ */
+ private boolean mFocusOutFront;
+
+ /**
+ * Allow DPAD key to navigate out at the end of the view, default is false.
+ */
+ private boolean mFocusOutEnd;
+
+ /**
+ * True if focus search is disabled.
+ */
+ private boolean mFocusSearchDisabled;
+
+ /**
+ * Animate layout changes from a child resizing or adding/removing a child.
+ */
+ private boolean mAnimateChildLayout = true;
+
+ /**
+ * Interpolator used to animate layout of children.
+ */
+ private Interpolator mAnimateLayoutChildInterpolator = sDefaultAnimationChildLayoutInterpolator;
+
+ /**
+ * Duration used to animate layout of children.
+ */
+ private long mAnimateLayoutChildDuration = DEFAULT_CHILD_ANIMATION_DURATION_MS;
+
+ public GridLayoutManager(BaseGridView baseGridView) {
+ mBaseGridView = baseGridView;
+ }
+
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL && orientation != VERTICAL) {
+ if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation);
+ return;
+ }
+
+ mOrientation = orientation;
+ mWindowAlignment.setOrientation(orientation);
+ mItemAlignment.setOrientation(orientation);
+ mForceFullLayout = true;
+ }
+
+ public int getFocusScrollStrategy() {
+ return mFocusScrollStrategy;
+ }
+
+ public void setFocusScrollStrategy(int focusScrollStrategy) {
+ mFocusScrollStrategy = focusScrollStrategy;
+ }
+
+ public void setWindowAlignment(int windowAlignment) {
+ mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment);
+ }
+
+ public int getWindowAlignment() {
+ return mWindowAlignment.mainAxis().getWindowAlignment();
+ }
+
+ public void setWindowAlignmentOffset(int alignmentOffset) {
+ mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset);
+ }
+
+ public int getWindowAlignmentOffset() {
+ return mWindowAlignment.mainAxis().getWindowAlignmentOffset();
+ }
+
+ public void setWindowAlignmentOffsetPercent(float offsetPercent) {
+ mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent);
+ }
+
+ public float getWindowAlignmentOffsetPercent() {
+ return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent();
+ }
+
+ public void setItemAlignmentOffset(int alignmentOffset) {
+ mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset);
+ updateChildAlignments();
+ }
+
+ public int getItemAlignmentOffset() {
+ return mItemAlignment.mainAxis().getItemAlignmentOffset();
+ }
+
+ public void setItemAlignmentOffsetPercent(float offsetPercent) {
+ mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent);
+ updateChildAlignments();
+ }
+
+ public float getItemAlignmentOffsetPercent() {
+ return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent();
+ }
+
+ public void setItemAlignmentViewId(int viewId) {
+ mItemAlignment.mainAxis().setItemAlignmentViewId(viewId);
+ updateChildAlignments();
+ }
+
+ public int getItemAlignmentViewId() {
+ return mItemAlignment.mainAxis().getItemAlignmentViewId();
+ }
+
+ public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) {
+ mFocusOutFront = throughFront;
+ mFocusOutEnd = throughEnd;
+ }
+
+ public void setNumRows(int numRows) {
+ if (numRows < 0) throw new IllegalArgumentException();
+ mNumRowsRequested = numRows;
+ mForceFullLayout = true;
+ }
+
+ public void setRowHeight(int height) {
+ if (height < 0) throw new IllegalArgumentException();
+ mItemLengthSecondaryRequested = height;
+ }
+
+ public void setItemMargin(int margin) {
+ mVerticalMargin = mHorizontalMargin = margin;
+ mMarginPrimary = mMarginSecondary = margin;
+ }
+
+ public void setVerticalMargin(int margin) {
+ if (mOrientation == HORIZONTAL) {
+ mMarginSecondary = mVerticalMargin = margin;
+ } else {
+ mMarginPrimary = mVerticalMargin = margin;
+ }
+ }
+
+ public void setHorizontalMargin(int margin) {
+ if (mOrientation == HORIZONTAL) {
+ mMarginPrimary = mHorizontalMargin = margin;
+ } else {
+ mMarginSecondary = mHorizontalMargin = margin;
+ }
+ }
+
+ public int getVerticalMargin() {
+ return mVerticalMargin;
+ }
+
+ public int getHorizontalMargin() {
+ return mHorizontalMargin;
+ }
+
+ public void setGravity(int gravity) {
+ mGravity = gravity;
+ }
+
+ protected boolean hasDoneFirstLayout() {
+ return mGrid != null;
+ }
+
+ public void setOnChildSelectedListener(OnChildSelectedListener listener) {
+ mChildSelectedListener = listener;
+ }
+
+ private int getPositionByView(View view) {
+ return getPositionByIndex(mBaseGridView.indexOfChild(view));
+ }
+
+ 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;
+ }
+
+ 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);
+ }
+ }
+
+ @Override
+ public boolean canScrollHorizontally() {
+ // We can scroll horizontally if we have horizontal orientation, or if
+ // we are vertical and have more than one column.
+ return mOrientation == HORIZONTAL || mNumRows > 1;
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ // We can scroll vertically if we have vertical orientation, or if we
+ // are horizontal and have more than one row.
+ return mOrientation == VERTICAL || mNumRows > 1;
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateLayoutParams(Context context, AttributeSet attrs) {
+ return new LayoutParams(context, attrs);
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (lp instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) lp);
+ } else if (lp instanceof RecyclerView.LayoutParams) {
+ return new LayoutParams((RecyclerView.LayoutParams) lp);
+ } else if (lp instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) lp);
+ } else {
+ return new LayoutParams(lp);
+ }
+ }
+
+ protected View getViewForPosition(int position) {
+ View v = mRecycler.getViewForPosition(mAdapter, position);
+ if (v != null) {
+ ((LayoutParams) v.getLayoutParams()).onViewAttached();
+ }
+ return v;
+ }
+
+ final int getOpticalLeft(View v) {
+ return ((LayoutParams) v.getLayoutParams()).getOpticalLeft(v);
+ }
+
+ final int getOpticalRight(View v) {
+ return ((LayoutParams) v.getLayoutParams()).getOpticalRight(v);
+ }
+
+ final int getOpticalTop(View v) {
+ return ((LayoutParams) v.getLayoutParams()).getOpticalTop(v);
+ }
+
+ final int getOpticalBottom(View v) {
+ return ((LayoutParams) v.getLayoutParams()).getOpticalBottom(v);
+ }
+
+ private int getViewMin(View v) {
+ return (mOrientation == HORIZONTAL) ? getOpticalLeft(v) : getOpticalTop(v);
+ }
+
+ private int getViewMax(View v) {
+ return (mOrientation == HORIZONTAL) ? getOpticalRight(v) : getOpticalBottom(v);
+ }
+
+ private int getViewCenter(View view) {
+ return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view);
+ }
+
+ private int getViewCenterSecondary(View view) {
+ return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view);
+ }
+
+ private int getViewCenterX(View v) {
+ LayoutParams p = (LayoutParams) v.getLayoutParams();
+ return p.getOpticalLeft(v) + p.getAlignX();
+ }
+
+ private int getViewCenterY(View v) {
+ LayoutParams p = (LayoutParams) v.getLayoutParams();
+ return p.getOpticalTop(v) + p.getAlignY();
+ }
+
+ /**
+ * 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.
+ * @param focusPosition The initial focusPosition that we would like to
+ * focus on.
+ * @return Actual position that can be focused on.
+ */
+ private int init(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
+ int focusPosition) {
+
+ final int newItemCount = adapter.getItemCount();
+
+ if (focusPosition == NO_POSITION && newItemCount > 0) {
+ // if focus position is never set before, initialize it to 0
+ focusPosition = 0;
+ }
+ // If adapter has changed then caches are invalid; otherwise,
+ // we try to maintain each row's position if number of rows keeps the same
+ // and existing mGrid contains the focusPosition.
+ if (mRows != null && mNumRows == mRows.length &&
+ mGrid != null && mGrid.getSize() > 0 && focusPosition >= 0 &&
+ focusPosition >= mGrid.getFirstIndex() &&
+ focusPosition <= mGrid.getLastIndex()) {
+ // strip mGrid to a subset (like a column) that contains focusPosition
+ mGrid.stripDownTo(focusPosition);
+ // make sure that remaining items do not exceed new adapter size
+ int firstIndex = mGrid.getFirstIndex();
+ int lastIndex = mGrid.getLastIndex();
+ if (DEBUG) {
+ Log .v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex " + lastIndex);
+ }
+ for (int i = lastIndex; i >=firstIndex; i--) {
+ if (i >= newItemCount) {
+ mGrid.removeLast();
+ }
+ }
+ if (mGrid.getSize() == 0) {
+ focusPosition = newItemCount - 1;
+ // initialize row start locations
+ for (int i = 0; i < mNumRows; i++) {
+ mRows[i].low = 0;
+ mRows[i].high = 0;
+ }
+ if (DEBUG) Log.v(getTag(), "mGrid zero size");
+ } else {
+ // initialize row start locations
+ for (int i = 0; i < mNumRows; i++) {
+ mRows[i].low = Integer.MAX_VALUE;
+ mRows[i].high = Integer.MIN_VALUE;
+ }
+ firstIndex = mGrid.getFirstIndex();
+ lastIndex = mGrid.getLastIndex();
+ if (focusPosition > lastIndex) {
+ focusPosition = mGrid.getLastIndex();
+ }
+ if (DEBUG) {
+ Log.v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex "
+ + lastIndex + " focusPosition " + focusPosition);
+ }
+ // fill rows with minimal view positions of the subset
+ for (int i = firstIndex; i <= lastIndex; i++) {
+ View v = getViewByPosition(i);
+ if (v == null) {
+ continue;
+ }
+ int row = mGrid.getLocation(i).row;
+ int low = getViewMin(v) + mScrollOffsetPrimary;
+ if (low < mRows[row].low) {
+ 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;
+ }
+ }
+ }
+
+ // Same adapter, we can reuse any attached views
+ detachAndScrapAttachedViews(recycler);
+
+ } else {
+ // otherwise recreate data structure
+ mRows = new StaggeredGrid.Row[mNumRows];
+ for (int i = 0; i < mNumRows; i++) {
+ mRows[i] = new StaggeredGrid.Row();
+ }
+ mGrid = new StaggeredGridDefault();
+ if (newItemCount == 0) {
+ focusPosition = NO_POSITION;
+ } else if (focusPosition >= newItemCount) {
+ focusPosition = newItemCount - 1;
+ }
+
+ // Adapter may have changed so remove all attached views permanently
+ removeAllViews();
+
+ 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();
+
+ return focusPosition;
+ }
+
+ @Override
+ public void onMeasure(int widthSpec, int heightSpec) {
+ int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
+ int measuredSizeSecondary;
+ if (mOrientation == HORIZONTAL) {
+ sizePrimary = MeasureSpec.getSize(widthSpec);
+ sizeSecondary = MeasureSpec.getSize(heightSpec);
+ modeSecondary = MeasureSpec.getMode(heightSpec);
+ paddingSecondary = getPaddingTop() + getPaddingBottom();
+ } else {
+ sizeSecondary = MeasureSpec.getSize(widthSpec);
+ sizePrimary = MeasureSpec.getSize(heightSpec);
+ 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");
+ }
+ }
+ 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
+ * (mNumRows - 1) + paddingSecondary;
+ if (childrenSize < measuredSizeSecondary) {
+ measuredSizeSecondary = childrenSize;
+ }
+ }
+ break;
+ default:
+ throw new IllegalStateException("wrong spec");
+ }
+ if (mOrientation == HORIZONTAL) {
+ setMeasuredDimension(sizePrimary, measuredSizeSecondary);
+ } else {
+ setMeasuredDimension(measuredSizeSecondary, sizePrimary);
+ }
+ if (DEBUG) {
+ Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary +
+ " measuredSizeSecondary " + measuredSizeSecondary +
+ " mItemLengthSecondary " + mItemLengthSecondary +
+ " mNumRows " + mNumRows);
+ }
+ }
+
+ private void measureChild(View child) {
+ final ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+ 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);
+ } else {
+ heightSpec = ViewGroup.getChildMeasureSpec(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, lp.height);
+ widthSpec = ViewGroup.getChildMeasureSpec(MeasureSpec.makeMeasureSpec(
+ mItemLengthSecondary, MeasureSpec.EXACTLY), 0, lp.width);
+ }
+
+ child.measure(widthSpec, heightSpec);
+ }
+
+ private StaggeredGrid.Provider mGridProvider = new StaggeredGrid.Provider() {
+
+ @Override
+ public int getCount() {
+ return mAdapter.getItemCount();
+ }
+
+ @Override
+ public void createItem(int index, int rowIndex, boolean append) {
+ View v = getViewForPosition(index);
+ if (mFirstVisiblePos >= 0) {
+ // when StaggeredGrid append or prepend item, we must guarantee
+ // that sibling item has created views already.
+ if (append && index != mLastVisiblePos + 1) {
+ throw new RuntimeException();
+ } else if (!append && index != mFirstVisiblePos - 1) {
+ throw new RuntimeException();
+ }
+ }
+
+ if (append) {
+ addView(v);
+ } else {
+ addView(v, 0);
+ }
+
+ measureChild(v);
+
+ int length = mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight();
+ int start, end;
+ if (append) {
+ start = mRows[rowIndex].high;
+ if (start != mRows[rowIndex].low) {
+ // if there are existing item in the row, add margin between
+ start += mMarginPrimary;
+ } else {
+ final int lastRow = mRows.length - 1;
+ if (lastRow != rowIndex && mRows[lastRow].high != mRows[lastRow].low) {
+ // if there are existing item in the last row, insert
+ // the new item after the last item of last row.
+ start = mRows[lastRow].high + mMarginPrimary;
+ }
+ }
+ end = start + length;
+ mRows[rowIndex].high = end;
+ } else {
+ end = mRows[rowIndex].low;
+ if (end != mRows[rowIndex].high) {
+ end -= mMarginPrimary;
+ } else if (0 != rowIndex && mRows[0].high != mRows[0].low) {
+ // if there are existing item in the first row, insert
+ // the new item before the first item of first row.
+ end = mRows[0].low - mMarginPrimary;
+ }
+ start = end - length;
+ mRows[rowIndex].low = start;
+ }
+ if (mFirstVisiblePos < 0) {
+ mFirstVisiblePos = mLastVisiblePos = index;
+ } else {
+ if (append) {
+ mLastVisiblePos++;
+ } else {
+ mFirstVisiblePos--;
+ }
+ }
+ int startSecondary = rowIndex * (mItemLengthSecondary + mMarginSecondary);
+ layoutChild(v, start - mScrollOffsetPrimary, end - mScrollOffsetPrimary,
+ startSecondary - mScrollOffsetSecondary);
+ if (DEBUG) {
+ Log.d(getTag(), "addView " + index + " " + v);
+ }
+ updateScrollMin();
+ 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;
+ }
+ final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+ final int horizontalGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if (mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP
+ || mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT) {
+ // do nothing
+ } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM
+ || mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT) {
+ startSecondary += mItemLengthSecondary - measuredSecondary;
+ } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL
+ || mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL) {
+ startSecondary += (mItemLengthSecondary - measuredSecondary) / 2;
+ }
+ int left, top, right, bottom;
+ if (mOrientation == HORIZONTAL) {
+ left = start;
+ top = startSecondary;
+ right = end;
+ bottom = startSecondary + measuredSecondary;
+ } else {
+ top = start;
+ left = startSecondary;
+ bottom = end;
+ right = startSecondary + measuredSecondary;
+ }
+ v.layout(left, top, right, bottom);
+ updateChildOpticalInsets(v, left, top, right, bottom);
+ updateChildAlignments(v);
+ }
+
+ private void updateChildOpticalInsets(View v, int left, int top, int right, int bottom) {
+ LayoutParams p = (LayoutParams) v.getLayoutParams();
+ p.setOpticalInsets(left - v.getLeft(), top - v.getTop(),
+ v.getRight() - right, v.getBottom() - bottom);
+ }
+
+ private void updateChildAlignments(View v) {
+ LayoutParams p = (LayoutParams) v.getLayoutParams();
+ p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
+ p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
+ }
+
+ private void updateChildAlignments() {
+ for (int i = 0, c = getChildCount(); i < c; i++) {
+ updateChildAlignments(getChildAt(i));
+ }
+ }
+
+ private boolean needsAppendVisibleItem() {
+ if (mLastVisiblePos < mFocusPosition) {
+ return true;
+ }
+ int right = mScrollOffsetPrimary + mSizePrimary;
+ for (int i = 0; i < mNumRows; i++) {
+ if (mRows[i].low == mRows[i].high) {
+ if (mRows[i].high < right) {
+ return true;
+ }
+ } else if (mRows[i].high < right - mMarginPrimary) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean needsPrependVisibleItem() {
+ if (mFirstVisiblePos > mFocusPosition) {
+ return true;
+ }
+ for (int i = 0; i < mNumRows; i++) {
+ if (mRows[i].low == mRows[i].high) {
+ if (mRows[i].low > mScrollOffsetPrimary) {
+ return true;
+ }
+ } else if (mRows[i].low - mMarginPrimary > mScrollOffsetPrimary) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Append one column if possible and return true if reach end.
+ private boolean appendOneVisibleItem() {
+ while (true) {
+ if (mLastVisiblePos != NO_POSITION && mLastVisiblePos < mAdapter.getItemCount() -1 &&
+ mLastVisiblePos < mGrid.getLastIndex()) {
+ // append invisible view of saved location till last row
+ final int index = mLastVisiblePos + 1;
+ final int row = mGrid.getLocation(index).row;
+ mGridProvider.createItem(index, row, true);
+ if (row == mNumRows - 1) {
+ return false;
+ }
+ } else if ((mLastVisiblePos == NO_POSITION && mAdapter.getItemCount() > 0) ||
+ (mLastVisiblePos != NO_POSITION &&
+ mLastVisiblePos < mAdapter.getItemCount() - 1)) {
+ mGrid.appendItems(mScrollOffsetPrimary + mSizePrimary);
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ private void appendVisibleItems() {
+ while (needsAppendVisibleItem()) {
+ if (appendOneVisibleItem()) {
+ break;
+ }
+ }
+ }
+
+ // Prepend one column if possible and return true if reach end.
+ private boolean prependOneVisibleItem() {
+ while (true) {
+ if (mFirstVisiblePos > 0) {
+ if (mFirstVisiblePos > mGrid.getFirstIndex()) {
+ // prepend invisible view of saved location till first row
+ final int index = mFirstVisiblePos - 1;
+ final int row = mGrid.getLocation(index).row;
+ mGridProvider.createItem(index, row, false);
+ if (row == 0) {
+ return false;
+ }
+ } else {
+ mGrid.prependItems(mScrollOffsetPrimary);
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+ }
+
+ private void prependVisibleItems() {
+ while (needsPrependVisibleItem()) {
+ if (prependOneVisibleItem()) {
+ break;
+ }
+ }
+ }
+
+ private void removeChildAt(int position) {
+ View v = getViewByPosition(position);
+ if (v != null) {
+ if (DEBUG) {
+ Log.d(getTag(), "removeAndRecycleViewAt " + position);
+ }
+ ((LayoutParams) v.getLayoutParams()).onViewDetached();
+ removeAndRecycleViewAt(getIndexByPosition(position), mRecycler);
+ }
+ }
+
+ private void removeInvisibleViewsAtEnd() {
+ boolean update = false;
+ while(mLastVisiblePos > mFirstVisiblePos && mLastVisiblePos > mFocusPosition) {
+ View view = getViewByPosition(mLastVisiblePos);
+ if (getViewMin(view) > mSizePrimary) {
+ removeChildAt(mLastVisiblePos);
+ mLastVisiblePos--;
+ update = true;
+ } else {
+ break;
+ }
+ }
+ if (update) {
+ updateRowsMinMax();
+ }
+ }
+
+ private void removeInvisibleViewsAtFront() {
+ boolean update = false;
+ while(mLastVisiblePos > mFirstVisiblePos && mFirstVisiblePos < mFocusPosition) {
+ View view = getViewByPosition(mFirstVisiblePos);
+ if (getViewMax(view) < 0) {
+ removeChildAt(mFirstVisiblePos);
+ mFirstVisiblePos++;
+ update = true;
+ } else {
+ break;
+ }
+ }
+ if (update) {
+ updateRowsMinMax();
+ }
+ }
+
+ private void updateRowsMinMax() {
+ if (mFirstVisiblePos < 0) {
+ return;
+ }
+ for (int i = 0; i < mNumRows; i++) {
+ mRows[i].low = Integer.MAX_VALUE;
+ mRows[i].high = Integer.MIN_VALUE;
+ }
+ for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
+ View view = getViewByPosition(i);
+ int row = mGrid.getLocation(i).row;
+ int low = getViewMin(view) + mScrollOffsetPrimary;
+ if (low < mRows[row].low) {
+ mRows[row].low = low;
+ }
+ int high = getViewMax(view) + mScrollOffsetPrimary;
+ if (high > mRows[row].high) {
+ mRows[row].high = high;
+ }
+ }
+ }
+
+ /**
+ * 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();
+
+ List<Integer>[] rows = mGrid.getItemPositionsInRows(mFirstVisiblePos, mLastVisiblePos);
+
+ // 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;
+ for (int j = 0, size = row.size(); j < size; j++) {
+ int position = row.get(j);
+ start = updateChildView(getViewByPosition(position), start, startSecondary);
+ }
+ }
+
+ appendVisibleItems();
+ prependVisibleItems();
+
+ updateRowsMinMax();
+ updateScrollMin();
+ updateScrollMax();
+
+ if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
+ View focusView = getViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition);
+ scrollToView(focusView, false);
+ }
+ }
+
+ // Lays out items based on the current scroll position
+ @Override
+ public void onLayoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
+ boolean structureChanged, RecyclerView.State state) {
+ if (DEBUG) {
+ Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
+ + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
+ + " structureChanged " + structureChanged
+ + " mForceFullLayout " + mForceFullLayout);
+ Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
+ }
+
+ if (mNumRows == 0) {
+ // haven't done measure yet
+ return;
+ }
+ final int itemCount = adapter.getItemCount();
+ if (itemCount < 0) {
+ return;
+ }
+
+ mInLayout = true;
+
+ // Track the old focus view so we can adjust our system scroll position
+ // so that any scroll animations happening now will remain valid.
+ 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;
+ }
+ }
+
+ boolean hasDoneFirstLayout = hasDoneFirstLayout();
+ if (!structureChanged && !mForceFullLayout && hasDoneFirstLayout) {
+ fastRelayout();
+ } else {
+ boolean hadFocus = mBaseGridView.hasFocus();
+
+ int newFocusPosition = init(adapter, recycler, mFocusPosition);
+ if (DEBUG) {
+ Log.v(getTag(), "mFocusPosition " + mFocusPosition + " newFocusPosition "
+ + newFocusPosition);
+ }
+
+ // depending on result of init(), either recreating everything
+ // or try to reuse the row start positions near mFocusPosition
+ if (mGrid.getSize() == 0) {
+ // this is a fresh creating all items, starting from
+ // mFocusPosition with a estimated row index.
+ mGrid.setStart(newFocusPosition, StaggeredGrid.START_DEFAULT);
+
+ // Can't track the old focus view
+ delta = deltaSecondary = 0;
+
+ } else {
+ // mGrid remembers Locations for the column that
+ // contains mFocusePosition and also mRows remembers start
+ // positions of each row.
+ // Manually re-create child views for that column
+ int firstIndex = mGrid.getFirstIndex();
+ int lastIndex = mGrid.getLastIndex();
+ for (int i = firstIndex; i <= lastIndex; i++) {
+ mGridProvider.createItem(i, mGrid.getLocation(i).row, true);
+ }
+ }
+ // add visible views at end until reach the end of window
+ appendVisibleItems();
+ // add visible views at front until reach the start of window
+ prependVisibleItems();
+ // multiple rounds: scrollToView of first round may drag first/last child into
+ // "visible window" and we update scrollMin/scrollMax then run second scrollToView
+ int oldFirstVisible;
+ int oldLastVisible;
+ do {
+ oldFirstVisible = mFirstVisiblePos;
+ oldLastVisible = mLastVisiblePos;
+ View focusView = getViewByPosition(newFocusPosition);
+ // we need force to initialize the child view's position
+ scrollToView(focusView, false);
+ if (focusView != null && hadFocus) {
+ focusView.requestFocus();
+ }
+ appendVisibleItems();
+ prependVisibleItems();
+ removeInvisibleViewsAtFront();
+ removeInvisibleViewsAtEnd();
+ } while (mFirstVisiblePos != oldFirstVisible || mLastVisiblePos != oldLastVisible);
+ }
+ mForceFullLayout = false;
+
+ if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
+ scrollDirectionPrimary(-delta);
+ scrollDirectionSecondary(-deltaSecondary);
+ }
+ appendVisibleItems();
+ prependVisibleItems();
+ removeInvisibleViewsAtFront();
+ removeInvisibleViewsAtEnd();
+
+ if (DEBUG) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ mGrid.debugPrint(pw);
+ Log.d(getTag(), sw.toString());
+ }
+
+ removeAndRecycleScrap(recycler);
+ attemptAnimateLayoutChild();
+
+ if (!hasDoneFirstLayout) {
+ dispatchChildSelected();
+ }
+ mInLayout = false;
+ if (DEBUG) Log.v(getTag(), "layoutChildren end");
+ }
+
+ private void offsetChildrenSecondary(int increment) {
+ final int childCount = getChildCount();
+ if (mOrientation == HORIZONTAL) {
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).offsetTopAndBottom(increment);
+ }
+ } else {
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).offsetLeftAndRight(increment);
+ }
+ }
+ mScrollOffsetSecondary -= increment;
+ }
+
+ private void offsetChildrenPrimary(int increment) {
+ final int childCount = getChildCount();
+ if (mOrientation == VERTICAL) {
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).offsetTopAndBottom(increment);
+ }
+ } else {
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).offsetLeftAndRight(increment);
+ }
+ }
+ mScrollOffsetPrimary -= increment;
+ }
+
+ @Override
+ public int scrollHorizontallyBy(int dx, Adapter adapter, Recycler recycler,
+ RecyclerView.State state) {
+ if (DEBUG) Log.v(TAG, "scrollHorizontallyBy " + dx);
+
+ if (mOrientation == HORIZONTAL) {
+ return scrollDirectionPrimary(dx);
+ } else {
+ return scrollDirectionSecondary(dx);
+ }
+ }
+
+ @Override
+ public int scrollVerticallyBy(int dy, Adapter adapter, Recycler recycler,
+ RecyclerView.State state) {
+ if (DEBUG) Log.v(TAG, "scrollVerticallyBy " + dy);
+ if (mOrientation == VERTICAL) {
+ return scrollDirectionPrimary(dy);
+ } else {
+ return scrollDirectionSecondary(dy);
+ }
+ }
+
+ // scroll in main direction may add/prune views
+ private int scrollDirectionPrimary(int da) {
+ offsetChildrenPrimary(-da);
+ if (mInLayout) {
+ return da;
+ }
+ if (da > 0) {
+ appendVisibleItems();
+ removeInvisibleViewsAtFront();
+ } else if (da < 0) {
+ prependVisibleItems();
+ removeInvisibleViewsAtEnd();
+ }
+ attemptAnimateLayoutChild();
+ mBaseGridView.invalidate();
+ return da;
+ }
+
+ // scroll in second direction will not add/prune views
+ private int scrollDirectionSecondary(int dy) {
+ offsetChildrenSecondary(-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;
+ }
+ }
+ mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
+ if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge);
+ }
+ }
+
+ 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);
+ }
+ }
+
+ private void initScrollController() {
+ mWindowAlignment.horizontal.setSize(getWidth());
+ mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
+ 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);
+ }
+ }
+
+ public void setSelection(RecyclerView parent, int position) {
+ setSelection(parent, position, false);
+ }
+
+ public void setSelectionSmooth(RecyclerView parent, int position) {
+ setSelection(parent, position, true);
+ }
+
+ public int getSelection() {
+ return mFocusPosition;
+ }
+
+ public void setSelection(RecyclerView parent, int position, boolean smooth) {
+ if (mFocusPosition == position) {
+ return;
+ }
+ View view = getViewByPosition(position);
+ if (view != null) {
+ scrollToView(view, smooth);
+ } else {
+ boolean right = position > mFocusPosition;
+ mFocusPosition = position;
+ 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);
+ } else {
+ mForceFullLayout = true;
+ parent.requestLayout();
+ }
+ }
+ }
+
+ @Override
+ public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+ boolean needsLayout = false;
+ if (itemCount != 0) {
+ if (mFirstVisiblePos < 0) {
+ needsLayout = true;
+ } else if (!(positionStart > mLastVisiblePos + 1 ||
+ positionStart + itemCount < mFirstVisiblePos - 1)) {
+ needsLayout = true;
+ }
+ }
+ if (needsLayout) {
+ recyclerView.requestLayout();
+ }
+ }
+
+ @Override
+ public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
+ if (mFocusSearchDisabled) {
+ return true;
+ }
+ if (!mInLayout) {
+ scrollToView(child, true);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
+ boolean immediate) {
+ if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
+ return false;
+ }
+
+ int getScrollOffsetX() {
+ return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
+ }
+
+ int getScrollOffsetY() {
+ return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
+ }
+
+ 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;
+ }
+
+ /**
+ * Scroll to a given child view and change mFocusPosition.
+ */
+ private void scrollToView(View view, boolean smooth) {
+ int newFocusPosition = getPositionByView(view);
+ if (mInLayout || newFocusPosition != mFocusPosition) {
+ mFocusPosition = newFocusPosition;
+ dispatchChildSelected();
+ }
+ if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
+ mBaseGridView.invalidate();
+ }
+ if (view == null) {
+ return;
+ }
+ if (!view.hasFocus() && mBaseGridView.hasFocus()) {
+ // transfer focus to the child if it does not have focus yet (e.g. triggered
+ // 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;
+ }
+ }
+
+ private void scrollItemOrPage(View view, boolean smooth) {
+ int pos = getPositionByView(view);
+ int viewMin = getViewMin(view);
+ int viewMax = getViewMax(view);
+ // we either align "firstView" to left/top padding edge
+ // or align "lastView" to right/bottom padding edge
+ View firstView = null;
+ View lastView = null;
+ int paddingLow = mWindowAlignment.mainAxis().getPaddingLow();
+ int clientSize = mWindowAlignment.mainAxis().getClientSize();
+ final int row = mGrid.getLocation(pos).row;
+ if (viewMin < paddingLow) {
+ // view enters low padding area:
+ firstView = view;
+ if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
+ // scroll one "page" left/top,
+ // align first visible item of the "page" at the low padding edge.
+ while (!prependOneVisibleItem()) {
+ List<Integer> positions =
+ mGrid.getItemPositionsInRows(mFirstVisiblePos, pos)[row];
+ firstView = getViewByPosition(positions.get(0));
+ if (viewMax - getViewMin(firstView) > clientSize) {
+ if (positions.size() > 1) {
+ firstView = getViewByPosition(positions.get(1));
+ }
+ break;
+ }
+ }
+ }
+ } else if (viewMax > clientSize + paddingLow) {
+ // view enters high padding area:
+ if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
+ // scroll whole one page right/bottom, align view at the low padding edge.
+ firstView = view;
+ do {
+ List<Integer> positions =
+ mGrid.getItemPositionsInRows(pos, mLastVisiblePos)[row];
+ lastView = getViewByPosition(positions.get(positions.size() - 1));
+ if (getViewMax(lastView) - viewMin > clientSize) {
+ lastView = null;
+ break;
+ }
+ } while (!appendOneVisibleItem());
+ if (lastView != null) {
+ // however if we reached end, we should align last view.
+ firstView = null;
+ }
+ } else {
+ lastView = view;
+ }
+ }
+ int scrollPrimary = 0;
+ int scrollSecondary = 0;
+ if (firstView != null) {
+ scrollPrimary = getViewMin(firstView) - paddingLow;
+ } else if (lastView != null) {
+ scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize);
+ }
+ View secondaryAlignedView;
+ if (firstView != null) {
+ secondaryAlignedView = firstView;
+ } else if (lastView != null) {
+ secondaryAlignedView = lastView;
+ } else {
+ secondaryAlignedView = view;
+ }
+ int viewCenterSecondary = mScrollOffsetSecondary +
+ getViewCenterSecondary(secondaryAlignedView);
+ mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
+ scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
+ scrollSecondary -= mScrollOffsetSecondary;
+ scrollGrid(scrollPrimary, scrollSecondary, smooth);
+ }
+
+ private void scrollToAlignedPosition(View view, boolean smooth) {
+ int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
+ int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
+ if (DEBUG) {
+ Log.v(getTag(), "scrollAligned smooth=" + smooth + " pos=" + mFocusPosition + " "
+ + viewCenterPrimary +","+viewCenterSecondary + " " + 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);
+ }
+ }
+
+ private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
+ if (mInLayout) {
+ scrollDirectionPrimary(scrollPrimary);
+ scrollDirectionSecondary(scrollSecondary);
+ } else {
+ int scrollX;
+ int scrollY;
+ if (mOrientation == HORIZONTAL) {
+ scrollX = scrollPrimary;
+ scrollY = scrollSecondary;
+ } else {
+ scrollX = scrollSecondary;
+ scrollY = scrollPrimary;
+ }
+ if (smooth) {
+ mBaseGridView.smoothScrollBy(scrollX, scrollY);
+ } else {
+ mBaseGridView.scrollBy(scrollX, scrollY);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+
+ 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;
+ }
+
+ private int findImmediateChildIndex(View view) {
+ while (view != null && view != mBaseGridView) {
+ int index = mBaseGridView.indexOfChild(view);
+ if (index >= 0) {
+ return index;
+ }
+ view = (View) view.getParent();
+ }
+ return NO_POSITION;
+ }
+
+ void setFocusSearchDisabled(boolean disabled) {
+ mFocusSearchDisabled = disabled;
+ }
+
+ boolean isFocusSearchDisabled() {
+ return mFocusSearchDisabled;
+ }
+
+ @Override
+ public View onInterceptFocusSearch(View focused, int direction) {
+ if (mFocusSearchDisabled) {
+ return focused;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean onAddFocusables(RecyclerView recyclerView,
+ ArrayList<View> views, int direction, int focusableMode) {
+ if (mFocusSearchDisabled) {
+ return true;
+ }
+ // If this viewgroup or one of its children currently has focus then we
+ // consider our children for focus searching in main direction on the same row.
+ // If this viewgroup has no focus and using focus align, we want the system
+ // to ignore our children and pass focus to the viewgroup, which will pass
+ // focus on to its children appropriately.
+ // If this viewgroup has no focus and not using focus align, we want to
+ // consider the child that does not overlap with padding area.
+ if (recyclerView.hasFocus()) {
+ final int movement = getMovement(direction);
+ if (movement != PREV_ITEM && movement != NEXT_ITEM) {
+ // Move on secondary direction uses default addFocusables().
+ return false;
+ }
+ final View focused = recyclerView.findFocus();
+ final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
+ // Add focusables of focused item.
+ if (focusedPos != NO_POSITION) {
+ getViewByPosition(focusedPos).addFocusables(views, direction, focusableMode);
+ }
+ final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
+ mGrid.getLocation(focusedPos).row : NO_POSITION;
+ // Add focusables of next neighbor of same row on the focus search direction.
+ if (mGrid != null) {
+ final int focusableCount = views.size();
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ int index = movement == NEXT_ITEM ? i : count - 1 - i;
+ final View child = getChildAt(index);
+ if (child.getVisibility() != View.VISIBLE) {
+ continue;
+ }
+ int position = getPositionByIndex(index);
+ StaggeredGrid.Location loc = mGrid.getLocation(position);
+ if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
+ if (focusedPos == NO_POSITION ||
+ (movement == NEXT_ITEM && position > focusedPos)
+ || (movement == PREV_ITEM && position < focusedPos)) {
+ child.addFocusables(views, direction, focusableMode);
+ if (views.size() > focusableCount) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ } else {
+ if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
+ // adding views not overlapping padding area to avoid scrolling in gaining focus
+ int left = mWindowAlignment.mainAxis().getPaddingLow();
+ int right = mWindowAlignment.mainAxis().getClientSize() + left;
+ int focusableCount = views.size();
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ if (getViewMin(child) >= left && getViewMax(child) <= right) {
+ child.addFocusables(views, direction, focusableMode);
+ }
+ }
+ }
+ // if we cannot find any, then just add all children.
+ if (views.size() == focusableCount) {
+ for (int i = 0, count = getChildCount(); i < count; i++) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ child.addFocusables(views, direction, focusableMode);
+ }
+ }
+ if (views.size() != focusableCount) {
+ return true;
+ }
+ } else {
+ return true;
+ }
+ // if still cannot find any, fall through and add itself
+ }
+ if (recyclerView.isFocusable()) {
+ views.add(recyclerView);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public View onFocusSearchFailed(View focused, int direction, Adapter adapter,
+ Recycler recycler) {
+ if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
+
+ View view = null;
+ int movement = getMovement(direction);
+ final FocusFinder ff = FocusFinder.getInstance();
+ if (movement == NEXT_ITEM) {
+ while (view == null && !appendOneVisibleItem()) {
+ view = ff.findNextFocus(mBaseGridView, focused, direction);
+ }
+ } else if (movement == PREV_ITEM){
+ while (view == null && !prependOneVisibleItem()) {
+ view = ff.findNextFocus(mBaseGridView, focused, direction);
+ }
+ }
+ if (view == null) {
+ // returning the same view to prevent focus lost when scrolling past the end of the list
+ if (movement == PREV_ITEM) {
+ view = mFocusOutFront ? null : focused;
+ } else if (movement == NEXT_ITEM){
+ view = mFocusOutEnd ? null : focused;
+ }
+ }
+ if (DEBUG) Log.v(getTag(), "returning view " + view);
+ return view;
+ }
+
+ boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
+ Rect previouslyFocusedRect) {
+ switch (mFocusScrollStrategy) {
+ case BaseGridView.FOCUS_SCROLL_ALIGNED:
+ default:
+ return gridOnRequestFocusInDescendantsAligned(recyclerView,
+ direction, previouslyFocusedRect);
+ case BaseGridView.FOCUS_SCROLL_PAGE:
+ case BaseGridView.FOCUS_SCROLL_ITEM:
+ return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
+ direction, previouslyFocusedRect);
+ }
+ }
+
+ private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
+ int direction, Rect previouslyFocusedRect) {
+ View view = getViewByPosition(mFocusPosition);
+ if (view != null) {
+ boolean result = view.requestFocus(direction, previouslyFocusedRect);
+ if (!result && DEBUG) {
+ Log.w(getTag(), "failed to request focus on " + view);
+ }
+ return result;
+ }
+ return false;
+ }
+
+ private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
+ int direction, Rect previouslyFocusedRect) {
+ // focus to view not overlapping padding area to avoid scrolling in gaining focus
+ int index;
+ int increment;
+ int end;
+ int count = getChildCount();
+ if ((direction & View.FOCUS_FORWARD) != 0) {
+ index = 0;
+ increment = 1;
+ end = count;
+ } else {
+ index = count - 1;
+ increment = -1;
+ end = -1;
+ }
+ int left = mWindowAlignment.mainAxis().getPaddingLow();
+ int right = mWindowAlignment.mainAxis().getClientSize() + left;
+ for (int i = index; i != end; i += increment) {
+ View child = getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ if (getViewMin(child) >= left && getViewMax(child) <= right) {
+ if (child.requestFocus(direction, previouslyFocusedRect)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private final static int PREV_ITEM = 0;
+ private final static int NEXT_ITEM = 1;
+ private final static int PREV_ROW = 2;
+ private final static int NEXT_ROW = 3;
+
+ private int getMovement(int direction) {
+ int movement = View.FOCUS_LEFT;
+
+ if (mOrientation == HORIZONTAL) {
+ switch(direction) {
+ case View.FOCUS_LEFT:
+ movement = PREV_ITEM;
+ break;
+ case View.FOCUS_RIGHT:
+ movement = NEXT_ITEM;
+ break;
+ case View.FOCUS_UP:
+ movement = PREV_ROW;
+ break;
+ case View.FOCUS_DOWN:
+ movement = NEXT_ROW;
+ break;
+ }
+ } else if (mOrientation == VERTICAL) {
+ switch(direction) {
+ case View.FOCUS_LEFT:
+ movement = PREV_ROW;
+ break;
+ case View.FOCUS_RIGHT:
+ movement = NEXT_ROW;
+ break;
+ case View.FOCUS_UP:
+ movement = PREV_ITEM;
+ break;
+ case View.FOCUS_DOWN:
+ movement = NEXT_ITEM;
+ break;
+ }
+ }
+
+ return movement;
+ }
+
+ int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
+ int focusIndex = getIndexByPosition(mFocusPosition);
+ if (focusIndex == NO_POSITION) {
+ return i;
+ }
+ // 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) {
+ return i;
+ } else if (i < childCount - 1) {
+ return focusIndex + childCount - 1 - i;
+ } else {
+ return focusIndex;
+ }
+ }
+
+ @Override
+ public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
+ RecyclerView.Adapter newAdapter) {
+ mGrid = null;
+ mRows = null;
+ super.onAdapterChanged(oldAdapter, newAdapter);
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/HeaderItem.java b/v17/leanback/src/android/support/v17/leanback/widget/HeaderItem.java
new file mode 100644
index 0000000..a280f4f
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/HeaderItem.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.v17.leanback.widget;
+
+import static android.support.v17.leanback.widget.ObjectAdapter.NO_ID;
+
+/**
+ * A header item is an item that describes metadata of {@link Row}, such as a category
+ * of media items. Developer may override this class to add more information.
+ */
+public class HeaderItem {
+
+ private final long mId;
+ private final String mImageUri;
+ private final String mName;
+
+ /**
+ * Create a header item. All fields are optional.
+ */
+ public HeaderItem(long id, String name, String imageUri) {
+ mId = id;
+ mName = name;
+ mImageUri = imageUri;
+ }
+
+ /**
+ * Create a header item. All fields are optional.
+ */
+ public HeaderItem(String name, String imageUri) {
+ this(NO_ID, name, imageUri);
+ }
+
+ /**
+ * Returns a unique identifier for this item.
+ */
+ public final long getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the name of this header item.
+ */
+ public final String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the icon for this header item.
+ */
+ public final String getImageUri() {
+ return mImageUri;
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
new file mode 100644
index 0000000..2024425
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/HorizontalGridView.java
@@ -0,0 +1,350 @@
+/*
+ * 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.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.support.v17.leanback.R;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * A view that shows items in a horizontal scrolling list. The items come from
+ * the {@link RecyclerView.Adapter} associated with this view.
+ */
+public class HorizontalGridView extends BaseGridView {
+
+ private boolean mFadingLowEdge;
+ private boolean mFadingHighEdge;
+
+ private Paint mTempPaint = new Paint();
+ private Bitmap mTempBitmapLow;
+ private LinearGradient mLowFadeShader;
+ private int mLowFadeShaderLength;
+ private int mLowFadeShaderOffset;
+ private Bitmap mTempBitmapHigh;
+ private LinearGradient mHighFadeShader;
+ private int mHighFadeShaderLength;
+ private int mHighFadeShaderOffset;
+ private Rect mTempRect = new Rect();
+
+ public HorizontalGridView(Context context) {
+ this(context, null);
+ }
+
+ public HorizontalGridView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public HorizontalGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mLayoutManager.setOrientation(RecyclerView.HORIZONTAL);
+ initAttributes(context, attrs);
+ }
+
+ 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));
+ setNumRows(a.getInt(R.styleable.lbHorizontalGridView_numberOfRows, 1));
+ a.recycle();
+ setWillNotDraw(false);
+ mTempPaint = new Paint();
+ mTempPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
+ }
+
+ /**
+ * Set the number of rows.
+ */
+ public void setNumRows(int numRows) {
+ mLayoutManager.setNumRows(numRows);
+ requestLayout();
+ }
+
+ /**
+ * Set the row height.
+ */
+ public void setRowHeight(int height) {
+ mLayoutManager.setRowHeight(height);
+ requestLayout();
+ }
+
+ /**
+ * Set fade out left edge to transparent. Note turn on fading edge is very expensive
+ * that you should turn off when HorizontalGridView is scrolling.
+ */
+ public final void setFadingLeftEdge(boolean fading) {
+ if (mFadingLowEdge != fading) {
+ mFadingLowEdge = fading;
+ if (!mFadingLowEdge) {
+ mTempBitmapLow = null;
+ }
+ invalidate();
+ }
+ }
+
+ /**
+ * Return true if fading left edge.
+ */
+ public final boolean getFadingLeftEdge() {
+ return mFadingLowEdge;
+ }
+
+ /**
+ * Set left edge fading length in pixels.
+ */
+ public final void setFadingLeftEdgeLength(int fadeLength) {
+ if (mLowFadeShaderLength != fadeLength) {
+ mLowFadeShaderLength = fadeLength;
+ if (mLowFadeShaderLength != 0) {
+ mLowFadeShader = new LinearGradient(0, 0, mLowFadeShaderLength, 0,
+ Color.TRANSPARENT, Color.BLACK, Shader.TileMode.CLAMP);
+ } else {
+ mLowFadeShader = null;
+ }
+ invalidate();
+ }
+ }
+
+ /**
+ * Get left edge fading length in pixels.
+ */
+ public final int getFadingLeftEdgeLength() {
+ return mLowFadeShaderLength;
+ }
+
+ /**
+ * Set distance in pixels between fading start position and left padding edge.
+ * The fading start position is positive when start position is inside left padding
+ * area. Default value is 0, means that the fading starts from left padding edge.
+ */
+ public final void setFadingLeftEdgeOffset(int fadeOffset) {
+ if (mLowFadeShaderOffset != fadeOffset) {
+ mLowFadeShaderOffset = fadeOffset;
+ invalidate();
+ }
+ }
+
+ /**
+ * Get distance in pixels between fading start position and left padding edge.
+ * The fading start position is positive when start position is inside left padding
+ * area. Default value is 0, means that the fading starts from left padding edge.
+ */
+ public final int getFadingLeftEdgeOffset() {
+ return mLowFadeShaderOffset;
+ }
+
+ /**
+ * Set fade out right edge to transparent. Note turn on fading edge is very expensive
+ * that you should turn off when HorizontalGridView is scrolling.
+ */
+ public final void setFadingRightEdge(boolean fading) {
+ if (mFadingHighEdge != fading) {
+ mFadingHighEdge = fading;
+ if (!mFadingHighEdge) {
+ mTempBitmapHigh = null;
+ }
+ invalidate();
+ }
+ }
+
+ /**
+ * Return true if fading right edge.
+ */
+ public final boolean getFadingRightEdge() {
+ return mFadingHighEdge;
+ }
+
+ /**
+ * Set right edge fading length in pixels.
+ */
+ public final void setFadingRightEdgeLength(int fadeLength) {
+ if (mHighFadeShaderLength != fadeLength) {
+ mHighFadeShaderLength = fadeLength;
+ if (mHighFadeShaderLength != 0) {
+ mHighFadeShader = new LinearGradient(0, 0, mHighFadeShaderLength, 0,
+ Color.BLACK, Color.TRANSPARENT, Shader.TileMode.CLAMP);
+ } else {
+ mHighFadeShader = null;
+ }
+ invalidate();
+ }
+ }
+
+ /**
+ * Get right edge fading length in pixels.
+ */
+ public final int getFadingRightEdgeLength() {
+ return mHighFadeShaderLength;
+ }
+
+ /**
+ * Get distance in pixels between fading start position and right padding edge.
+ * The fading start position is positive when start position is inside right padding
+ * area. Default value is 0, means that the fading starts from right padding edge.
+ */
+ public final void setFadingRightEdgeOffset(int fadeOffset) {
+ if (mHighFadeShaderOffset != fadeOffset) {
+ mHighFadeShaderOffset = fadeOffset;
+ invalidate();
+ }
+ }
+
+ /**
+ * Set distance in pixels between fading start position and right padding edge.
+ * The fading start position is positive when start position is inside right padding
+ * area. Default value is 0, means that the fading starts from right padding edge.
+ */
+ public final int getFadingRightEdgeOffset() {
+ return mHighFadeShaderOffset;
+ }
+
+ private boolean needsFadingLowEdge() {
+ if (!mFadingLowEdge) {
+ return false;
+ }
+ final int c = getChildCount();
+ for (int i = 0; i < c; i++) {
+ View view = getChildAt(i);
+ if (mLayoutManager.getOpticalLeft(view) <
+ getPaddingLeft() - mLowFadeShaderOffset) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean needsFadingHighEdge() {
+ if (!mFadingHighEdge) {
+ return false;
+ }
+ final int c = getChildCount();
+ for (int i = c - 1; i >= 0; i--) {
+ View view = getChildAt(i);
+ if (mLayoutManager.getOpticalRight(view) > getWidth()
+ - getPaddingRight() + mHighFadeShaderOffset) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Bitmap getTempBitmapLow() {
+ if (mTempBitmapLow == null
+ || mTempBitmapLow.getWidth() != mLowFadeShaderLength
+ || mTempBitmapLow.getHeight() != getHeight()) {
+ mTempBitmapLow = Bitmap.createBitmap(mLowFadeShaderLength, getHeight(),
+ Bitmap.Config.ARGB_8888);
+ }
+ return mTempBitmapLow;
+ }
+
+ private Bitmap getTempBitmapHigh() {
+ if (mTempBitmapHigh == null
+ || mTempBitmapHigh.getWidth() != mHighFadeShaderLength
+ || mTempBitmapHigh.getHeight() != getHeight()) {
+ if (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(),
+ Bitmap.Config.ARGB_8888);
+ }
+ }
+ return mTempBitmapLow;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ final boolean needsFadingLow = needsFadingLowEdge();
+ final boolean needsFadingHigh = needsFadingHighEdge();
+ if (!needsFadingLow) {
+ mTempBitmapLow = null;
+ }
+ if (!needsFadingHigh) {
+ mTempBitmapHigh = null;
+ }
+ if (!needsFadingLow && !needsFadingHigh) {
+ super.draw(canvas);
+ return;
+ }
+
+ int lowEdge = mFadingLowEdge? getPaddingLeft() - mLowFadeShaderOffset - mLowFadeShaderLength : 0;
+ int highEdge = mFadingHighEdge ? getWidth() - getPaddingRight()
+ + mHighFadeShaderOffset + mHighFadeShaderLength : getWidth();
+
+ // draw not-fade content
+ int save = canvas.save();
+ canvas.clipRect(lowEdge + mLowFadeShaderLength, 0,
+ highEdge - mHighFadeShaderLength, getHeight());
+ super.draw(canvas);
+ canvas.restoreToCount(save);
+
+ Canvas tmpCanvas = new Canvas();
+ mTempRect.top = 0;
+ mTempRect.bottom = getHeight();
+ if (needsFadingLow && mLowFadeShaderLength > 0) {
+ Bitmap tempBitmap = getTempBitmapLow();
+ tempBitmap.eraseColor(Color.TRANSPARENT);
+ tmpCanvas.setBitmap(tempBitmap);
+ // draw original content
+ int tmpSave = tmpCanvas.save();
+ tmpCanvas.clipRect(0, 0, mLowFadeShaderLength, getHeight());
+ tmpCanvas.translate(-lowEdge, 0);
+ super.draw(tmpCanvas);
+ tmpCanvas.restoreToCount(tmpSave);
+ // draw fading out
+ mTempPaint.setShader(mLowFadeShader);
+ tmpCanvas.drawRect(0, 0, mLowFadeShaderLength, getHeight(), mTempPaint);
+ // copy back to canvas
+ mTempRect.left = 0;
+ mTempRect.right = mLowFadeShaderLength;
+ canvas.translate(lowEdge, 0);
+ canvas.drawBitmap(tempBitmap, mTempRect, mTempRect, null);
+ canvas.translate(-lowEdge, 0);
+ }
+ if (needsFadingHigh && mHighFadeShaderLength > 0) {
+ Bitmap tempBitmap = getTempBitmapHigh();
+ tempBitmap.eraseColor(Color.TRANSPARENT);
+ tmpCanvas.setBitmap(tempBitmap);
+ // draw original content
+ int tmpSave = tmpCanvas.save();
+ tmpCanvas.clipRect(0, 0, mHighFadeShaderLength, getHeight());
+ tmpCanvas.translate(-(highEdge - mHighFadeShaderLength), 0);
+ super.draw(tmpCanvas);
+ tmpCanvas.restoreToCount(tmpSave);
+ // draw fading out
+ mTempPaint.setShader(mHighFadeShader);
+ tmpCanvas.drawRect(0, 0, mHighFadeShaderLength, getHeight(), mTempPaint);
+ // copy back to canvas
+ mTempRect.left = 0;
+ mTempRect.right = mHighFadeShaderLength;
+ canvas.translate(highEdge - mHighFadeShaderLength, 0);
+ canvas.drawBitmap(tempBitmap, mTempRect, mTempRect, null);
+ canvas.translate(-(highEdge - mHighFadeShaderLength), 0);
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.java b/v17/leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.java
new file mode 100644
index 0000000..0700995
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/HorizontalHoverCardSwitcher.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.v17.leanback.widget;
+
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup.MarginLayoutParams;
+
+/**
+ * Helper class that stay bellow a HorizontalGridView and shows a hover card and align
+ * the hover card left to left of selected child view. If there is no space when scroll
+ * to the end, right edge hover card will be aligned to right of parent view excluding
+ * right padding.
+ */
+public final class HorizontalHoverCardSwitcher extends PresenterSwitcher {
+ // left and right of selected card view
+ int mCardLeft, mCardRight;
+
+ private int[] mTmpOffsets = new int[2];
+ private Rect mTmpRect = new Rect();
+
+ @Override
+ protected void insertView(View view) {
+ // append hovercard to the end of container
+ getParentViewGroup().addView(view);
+ }
+
+ @Override
+ protected void onViewSelected(View view) {
+ int rightLimit = getParentViewGroup().getWidth() -
+ getParentViewGroup().getPaddingRight();
+ // measure the hover card width, if it's too large, align hover card
+ // right edge with row view's right edge
+ view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ MarginLayoutParams params = (MarginLayoutParams) view.getLayoutParams();
+ if (mCardLeft + view.getMeasuredWidth() > rightLimit) {
+ params.leftMargin = rightLimit - view.getMeasuredWidth();
+ } else {
+ params.leftMargin = mCardLeft;
+ }
+ view.requestLayout();
+ }
+
+ /**
+ * Select a childView inside a grid view and create/bind a corresponding hover card view
+ * for the object.
+ */
+ public void select(HorizontalGridView gridView, View childView, Object object) {
+ ViewGroup parent = getParentViewGroup();
+ gridView.getViewSelectedOffsets(childView, mTmpOffsets);
+ mTmpRect.set(0, 0, childView.getWidth(), childView.getHeight());
+ parent.offsetDescendantRectToMyCoords(childView, mTmpRect);
+ mCardLeft = mTmpRect.left - mTmpOffsets[0];
+ mCardRight = mTmpRect.right - mTmpOffsets[0];
+ select(object);
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java b/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
new file mode 100644
index 0000000..663a58b
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ImageCardView.java
@@ -0,0 +1,163 @@
+/*
+ * 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.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ImageCardView extends BaseCardView {
+
+ private ImageView mImageView;
+ private View mInfoArea;
+ private TextView mTitleView;
+ private TextView mContentView;
+ private ImageView mBadgeImage;
+ private ImageView mBadgeFadeMask;
+
+ public ImageCardView(Context context) {
+ this(context, null);
+ }
+
+ public ImageCardView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.imageCardViewStyle);
+ }
+
+ public ImageCardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View v = inflater.inflate(R.layout.lb_image_card_view, this);
+
+ mImageView = (ImageView) v.findViewById(R.id.main_image);
+ mImageView.setVisibility(View.INVISIBLE);
+ mInfoArea = v.findViewById(R.id.info_field);
+ mTitleView = (TextView) v.findViewById(R.id.title_text);
+ mContentView = (TextView) v.findViewById(R.id.content_text);
+ mBadgeImage = (ImageView) v.findViewById(R.id.extra_badge);
+ mBadgeFadeMask = (ImageView) v.findViewById(R.id.fade_mask);
+ }
+
+ public void setMainImage(Drawable drawable) {
+ if (mImageView == null) {
+ return;
+ }
+
+ mImageView.setImageDrawable(drawable);
+ if (drawable == null) {
+ mImageView.setVisibility(View.INVISIBLE);
+ } else {
+ mImageView.setVisibility(View.VISIBLE);
+ fadeIn(mImageView);
+ }
+ }
+
+ public void setMainImageDimensions(int width, int height) {
+ ViewGroup.LayoutParams lp = mImageView.getLayoutParams();
+ lp.width = width;
+ lp.height = height;
+ mImageView.setLayoutParams(lp);
+ }
+
+ public Drawable getMainImage() {
+ if (mImageView == null) {
+ return null;
+ }
+
+ return mImageView.getDrawable();
+ }
+
+ public void setTitleText(CharSequence text) {
+ if (mTitleView == null) {
+ return;
+ }
+
+ mTitleView.setText(text);
+ setTextMaxLines();
+ }
+
+ public CharSequence getTitleText() {
+ if (mTitleView == null) {
+ return null;
+ }
+
+ return mTitleView.getText();
+ }
+
+ public void setContentText(CharSequence text) {
+ if (mContentView == null) {
+ return;
+ }
+
+ mContentView.setText(text);
+ setTextMaxLines();
+ }
+
+ public CharSequence getContentText() {
+ if (mContentView == null) {
+ return null;
+ }
+
+ return mContentView.getText();
+ }
+
+ public void setBadgeImage(Drawable drawable) {
+ if (mBadgeImage == null) {
+ return;
+ }
+
+ if (drawable != null) {
+ mBadgeImage.setImageDrawable(drawable);
+ mBadgeImage.setVisibility(View.VISIBLE);
+ mBadgeFadeMask.setVisibility(View.VISIBLE);
+ } else {
+ mBadgeImage.setVisibility(View.GONE);
+ mBadgeFadeMask.setVisibility(View.GONE);
+ }
+ }
+
+ public Drawable getBadgeImage() {
+ if (mBadgeImage == null) {
+ return null;
+ }
+
+ return mBadgeImage.getDrawable();
+ }
+
+ private void fadeIn(View v) {
+ v.setAlpha(0f);
+ v.animate().alpha(1f).setDuration(v.getContext().getResources().getInteger(
+ android.R.integer.config_shortAnimTime)).start();
+ }
+
+ private void setTextMaxLines() {
+ if (TextUtils.isEmpty(getTitleText())) {
+ mContentView.setMaxLines(2);
+ } else {
+ mContentView.setMaxLines(1);
+ }
+ if (TextUtils.isEmpty(getContentText())) {
+ mTitleView.setMaxLines(2);
+ } else {
+ mTitleView.setMaxLines(1);
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java b/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
new file mode 100644
index 0000000..cefb431
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ItemAlignment.java
@@ -0,0 +1,151 @@
+/*
+ * 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 static android.support.v7.widget.RecyclerView.HORIZONTAL;
+import static android.support.v7.widget.RecyclerView.VERTICAL;
+import static android.support.v17.leanback.widget.BaseGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED;
+
+import android.graphics.Rect;
+import android.support.v17.leanback.widget.GridLayoutManager.LayoutParams;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Defines alignment position on two directions of an item view. Typically item
+ * view alignment is at the center of the view. The class allows defining
+ * alignment at left/right or fixed offset/percentage position; it also allows
+ * using descendant view by id match.
+ */
+class ItemAlignment {
+
+ final static class Axis {
+ private int mOrientation;
+ private int mOffset = 0;
+ private float mOffsetPercent = 50;
+ private int mViewId = 0;
+ private Rect mRect = new Rect();
+
+ Axis(int orientation) {
+ mOrientation = orientation;
+ }
+
+ public void setItemAlignmentOffset(int offset) {
+ mOffset = offset;
+ }
+
+ public int getItemAlignmentOffset() {
+ return mOffset;
+ }
+
+ public void setItemAlignmentOffsetPercent(float percent) {
+ if ( (percent < 0 || percent > 100) &&
+ percent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) {
+ throw new IllegalArgumentException();
+ }
+ mOffsetPercent = percent;
+ }
+
+ public float getItemAlignmentOffsetPercent() {
+ return mOffsetPercent;
+ }
+
+ public void setItemAlignmentViewId(int viewId) {
+ mViewId = viewId;
+ }
+
+ public int getItemAlignmentViewId() {
+ return mViewId;
+ }
+
+ public int getAlignmentPosition(View itemView) {
+
+ LayoutParams p = (LayoutParams) itemView.getLayoutParams();
+ View view = itemView;
+ if (mViewId != 0) {
+ view = itemView.findViewById(mViewId);
+ if (view == null) {
+ view = itemView;
+ }
+ }
+ int alignPos;
+ if (mOrientation == HORIZONTAL) {
+ if (mOffset >= 0) {
+ alignPos = mOffset;
+ } else {
+ alignPos = p.getOpticalWidth(itemView) + mOffset;
+ }
+ if (mOffsetPercent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) {
+ alignPos += (p.getOpticalWidth(itemView) * mOffsetPercent) / 100;
+ }
+ if (itemView != view) {
+ mRect.left = alignPos;
+ ((ViewGroup) itemView).offsetDescendantRectToMyCoords(view, mRect);
+ alignPos = mRect.left;
+ }
+ } else {
+ if (mOffset >= 0) {
+ alignPos = mOffset;
+ } else {
+ alignPos = p.getOpticalHeight(itemView) + mOffset;
+ }
+ if (mOffsetPercent != ITEM_ALIGN_OFFSET_PERCENT_DISABLED) {
+ alignPos += (p.getOpticalHeight(itemView) * mOffsetPercent) / 100;
+ }
+ if (itemView != view) {
+ mRect.top = alignPos;
+ ((ViewGroup) itemView).offsetDescendantRectToMyCoords(view, mRect);
+ alignPos = mRect.top;
+ }
+ }
+ return alignPos;
+ }
+ }
+
+ private int mOrientation = HORIZONTAL;
+
+ final public Axis vertical = new Axis(VERTICAL);
+
+ final public Axis horizontal = new Axis(HORIZONTAL);
+
+ private Axis mMainAxis = horizontal;
+
+ private Axis mSecondAxis = vertical;
+
+ final public Axis mainAxis() {
+ return mMainAxis;
+ }
+
+ final public Axis secondAxis() {
+ return mSecondAxis;
+ }
+
+ final public void setOrientation(int orientation) {
+ mOrientation = orientation;
+ if (mOrientation == HORIZONTAL) {
+ mMainAxis = horizontal;
+ mSecondAxis = vertical;
+ } else {
+ mMainAxis = vertical;
+ mSecondAxis = horizontal;
+ }
+ }
+
+ final public int getOrientation() {
+ return mOrientation;
+ }
+
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
new file mode 100644
index 0000000..bc4a476
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ItemBridgeAdapter.java
@@ -0,0 +1,306 @@
+/*
+ * 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.v7.widget.RecyclerView;
+import android.support.v17.leanback.R;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * Bridge from Presenter to RecyclerView.Adapter. Public to allow use by third
+ * party presenters.
+ */
+public class ItemBridgeAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "ItemBridgeAdapter";
+ private static final boolean DEBUG = false;
+
+ /**
+ * Interface for listening to view holder operations.
+ */
+ public static class AdapterListener {
+ public void onAddPresenter(Presenter presenter) {
+ }
+ public void onCreate(ViewHolder viewHolder) {
+ }
+ public void onBind(ViewHolder viewHolder) {
+ }
+ public void onUnbind(ViewHolder viewHolder) {
+ }
+ public void onAttachedToWindow(ViewHolder viewHolder) {
+ }
+ public void onDetachedFromWindow(ViewHolder viewHolder) {
+ }
+ }
+
+ /**
+ * Interface for wrapping a view created by presenter into another view.
+ * The wrapper must be immediate parent of the wrapped view.
+ */
+ public static abstract class Wrapper {
+ public abstract View createWrapper(View root);
+ public abstract void wrap(View wrapper, View wrapped);
+ }
+
+ private ObjectAdapter mAdapter;
+ private Wrapper mWrapper;
+ private PresenterSelector mPresenterSelector;
+ private FocusHighlight mFocusHighlight;
+ private AdapterListener mAdapterListener;
+ private ArrayList<Presenter> mPresenters = new ArrayList<Presenter>();
+
+ final class OnFocusChangeListener implements View.OnFocusChangeListener {
+ View.OnFocusChangeListener mChainedListener;
+
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (DEBUG) Log.v(TAG, "onFocusChange " + hasFocus + " " + view
+ + " mFocusHighlight" + mFocusHighlight);
+ if (mWrapper != null) {
+ view = (View) view.getParent();
+ }
+ if (mFocusHighlight != null) {
+ mFocusHighlight.onItemFocused(view, hasFocus);
+ }
+ if (mChainedListener != null) {
+ mChainedListener.onFocusChange(view, hasFocus);
+ }
+ }
+ }
+
+ public class ViewHolder extends RecyclerView.ViewHolder {
+ final Presenter mPresenter;
+ final Presenter.ViewHolder mHolder;
+ final OnFocusChangeListener mFocusChangeListener = new OnFocusChangeListener();
+ Object mItem;
+ Object mExtraObject;
+
+ /**
+ * Get {@link Presenter}.
+ */
+ public final Presenter getPresenter() {
+ return mPresenter;
+ }
+
+ /**
+ * Get {@link Presenter.ViewHolder}.
+ */
+ public final Presenter.ViewHolder getViewHolder() {
+ return mHolder;
+ }
+
+ /**
+ * Get currently bound object.
+ */
+ public final Object getItem() {
+ return mItem;
+ }
+
+ /**
+ * Get extra object associated with the view. Developer can attach
+ * any customized UI object in addition to {@link Presenter.ViewHolder}.
+ * A typical use case is attaching an animator object.
+ */
+ public final Object getExtraObject() {
+ return mExtraObject;
+ }
+
+ /**
+ * Set extra object associated with the view. Developer can attach
+ * any customized UI object in addition to {@link Presenter.ViewHolder}.
+ * A typical use case is attaching an animator object.
+ */
+ public void setExtraObject(Object object) {
+ mExtraObject = object;
+ }
+
+ ViewHolder(Presenter presenter, View view, Presenter.ViewHolder holder) {
+ super(view);
+ mPresenter = presenter;
+ mHolder = holder;
+ }
+ }
+
+ private ObjectAdapter.DataObserver mDataObserver = new ObjectAdapter.DataObserver() {
+ @Override
+ public void onChanged() {
+ ItemBridgeAdapter.this.notifyDataSetChanged();
+ }
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ ItemBridgeAdapter.this.notifyItemRangeChanged(positionStart, itemCount);
+ }
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ ItemBridgeAdapter.this.notifyItemRangeInserted(positionStart, itemCount);
+ }
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ ItemBridgeAdapter.this.notifyItemRangeRemoved(positionStart, itemCount);
+ }
+ };
+
+ public ItemBridgeAdapter(ObjectAdapter adapter, PresenterSelector presenterSelector) {
+ setAdapter(adapter);
+ mPresenterSelector = presenterSelector;
+ }
+
+ public ItemBridgeAdapter(ObjectAdapter adapter) {
+ this(adapter, null);
+ }
+
+ public ItemBridgeAdapter() {
+ }
+
+ public void setAdapter(ObjectAdapter adapter) {
+ if (mAdapter != null) {
+ mAdapter.unregisterObserver(mDataObserver);
+ }
+ mAdapter = adapter;
+ if (mAdapter == null) {
+ return;
+ }
+
+ mAdapter.registerObserver(mDataObserver);
+ if (hasStableIds() != mAdapter.hasStableIds()) {
+ setHasStableIds(mAdapter.hasStableIds());
+ }
+ }
+
+ public void setWrapper(Wrapper wrapper) {
+ mWrapper = wrapper;
+ }
+
+ public Wrapper getWrapper() {
+ return mWrapper;
+ }
+
+ void setFocusHighlight(FocusHighlight listener) {
+ mFocusHighlight = listener;
+ if (DEBUG) Log.v(TAG, "setFocusHighlight " + mFocusHighlight);
+ }
+
+ public void clear() {
+ setAdapter(null);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mAdapter.size();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ PresenterSelector presenterSelector = mPresenterSelector != null ?
+ mPresenterSelector : mAdapter.getPresenterSelector();
+ Object item = mAdapter.get(position);
+ Presenter presenter = presenterSelector.getPresenter(item);
+ int type = mPresenters.indexOf(presenter);
+ if (type < 0) {
+ mPresenters.add(presenter);
+ type = mPresenters.indexOf(presenter);
+ if (mAdapterListener != null) {
+ mAdapterListener.onAddPresenter(presenter);
+ }
+ }
+ return type;
+ }
+
+ /**
+ * {@link View.OnFocusChangeListener} that assigned in
+ * {@link Presenter#onCreateViewHolder(ViewGroup)} may be chained, user should never change
+ * {@link View.OnFocusChangeListener} after that.
+ */
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ if (DEBUG) Log.v(TAG, "onCreateViewHolder viewType " + viewType);
+ Presenter presenter = mPresenters.get(viewType);
+ Presenter.ViewHolder presenterVh;
+ View view;
+ if (mWrapper != null) {
+ view = mWrapper.createWrapper(parent);
+ presenterVh = presenter.onCreateViewHolder(parent);
+ mWrapper.wrap(view, presenterVh.view);
+ } else {
+ presenterVh = presenter.onCreateViewHolder(parent);
+ view = presenterVh.view;
+ }
+ ViewHolder viewHolder = new ViewHolder(presenter, view, presenterVh);
+ if (mAdapterListener != null) {
+ mAdapterListener.onCreate(viewHolder);
+ }
+ View presenterView = viewHolder.mHolder.view;
+ if (presenterView != null) {
+ viewHolder.mFocusChangeListener.mChainedListener = presenterView.getOnFocusChangeListener();
+ presenterView.setOnFocusChangeListener(viewHolder.mFocusChangeListener);
+ }
+ return viewHolder;
+ }
+
+ public void setAdapterListener(AdapterListener listener) {
+ mAdapterListener = listener;
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ if (DEBUG) Log.v(TAG, "onBindViewHolder position " + position);
+ ViewHolder viewHolder = (ViewHolder) holder;
+ viewHolder.mItem = mAdapter.get(position);
+
+ viewHolder.mPresenter.onBindViewHolder(viewHolder.mHolder, viewHolder.mItem);
+
+ if (mAdapterListener != null) {
+ mAdapterListener.onBind(viewHolder);
+ }
+ }
+
+ @Override
+ public void onViewRecycled(RecyclerView.ViewHolder holder) {
+ ViewHolder viewHolder = (ViewHolder) holder;
+ viewHolder.mPresenter.onUnbindViewHolder(viewHolder.mHolder);
+
+ viewHolder.mItem = null;
+
+ if (mAdapterListener != null) {
+ mAdapterListener.onUnbind(viewHolder);
+ }
+ }
+
+ @Override
+ public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
+ ViewHolder viewHolder = (ViewHolder) holder;
+ if (mAdapterListener != null) {
+ mAdapterListener.onAttachedToWindow(viewHolder);
+ }
+ viewHolder.mPresenter.onViewAttachedToWindow(viewHolder.mHolder);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) {
+ ViewHolder viewHolder = (ViewHolder) holder;
+ viewHolder.mPresenter.onViewDetachedFromWindow(viewHolder.mHolder);
+ if (mAdapterListener != null) {
+ mAdapterListener.onDetachedFromWindow(viewHolder);
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return mAdapter.getId(position);
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRow.java b/v17/leanback/src/android/support/v17/leanback/widget/ListRow.java
new file mode 100644
index 0000000..3ce18f7
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ListRow.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+/**
+ * A row composed of a optional {@link HeaderItem}, and an {@link ObjectAdapter}
+ * describing children.
+ */
+public class ListRow extends Row {
+ private final ObjectAdapter mAdapter;
+
+ /**
+ * Get the {@link ObjectAdapter} that represents a list of objects.
+ */
+ public final ObjectAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ public ListRow(HeaderItem header, ObjectAdapter adapter) {
+ super(header);
+ mAdapter = adapter;
+ verify();
+ }
+
+ public ListRow(long id, HeaderItem header, ObjectAdapter adapter) {
+ super(id, header);
+ mAdapter = adapter;
+ verify();
+ }
+
+ public ListRow(ObjectAdapter adapter) {
+ super();
+ mAdapter = adapter;
+ verify();
+ }
+
+ private void verify() {
+ if (mAdapter == null) {
+ throw new IllegalArgumentException("ObjectAdapter cannot be null");
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java b/v17/leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java
new file mode 100644
index 0000000..c5b2f49
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ListRowHoverCardView.java
@@ -0,0 +1,74 @@
+/*
+ * 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.support.v17.leanback.R;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * ListRowHoverCardView contains a title and description.
+ */
+public final class ListRowHoverCardView extends LinearLayout {
+
+ private final TextView mTitleView;
+ private final TextView mDescriptionView;
+
+ public ListRowHoverCardView(Context context) {
+ this(context, null);
+ }
+
+ public ListRowHoverCardView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ListRowHoverCardView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ LayoutInflater inflater = LayoutInflater.from(context);
+ inflater.inflate(R.layout.lb_list_row_hovercard, this);
+ mTitleView = (TextView) findViewById(R.id.title);
+ mDescriptionView = (TextView) findViewById(R.id.description);
+ }
+
+ public final CharSequence getTitle() {
+ return mTitleView.getText();
+ }
+
+ public final void setTitle(CharSequence text) {
+ if (!TextUtils.isEmpty(text)) {
+ mTitleView.setText(text);
+ mTitleView.setVisibility(View.VISIBLE);
+ } else {
+ mTitleView.setVisibility(View.GONE);
+ }
+ }
+
+ public final CharSequence getDescription() {
+ return mDescriptionView.getText();
+ }
+
+ public final void setDescription(CharSequence text) {
+ if (!TextUtils.isEmpty(text)) {
+ mDescriptionView.setText(text);
+ mDescriptionView.setVisibility(View.VISIBLE);
+ } else {
+ mDescriptionView.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
new file mode 100644
index 0000000..0de39ab
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ListRowPresenter.java
@@ -0,0 +1,377 @@
+/*
+ * 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 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.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.FrameLayout;
+
+/**
+ * ListRowPresenter renders {@link ListRow} using a
+ * {@link HorizontalGridView} hosted in a {@link ListRowView}.
+ *
+ * <h3>Hover card</h3>
+ * Optionally, {@link #setHoverCardPresenterSelector(PresenterSelector)} can be used to
+ * display a view for the currently focused list item below the rendered
+ * list. This view is known as a hover card.
+ *
+ * <h3>Selection animation</h3>
+ * ListRowPresenter disables {@link RowPresenter}'s default dimming effect and draw
+ * a dim overlay on top of each individual child items. Subclass may override and disable
+ * {@link #isUsingDefaultListSelectEffect()} and write its own dim effect in
+ * {@link #onSelectLevelChanged(RowPresenter.ViewHolder)}.
+ *
+ * <h3>Shadow</h3>
+ * ListRowPresenter applies a default shadow to child of each view. Call
+ * {@link #setShadowEnabled(boolean)} to disable shadow. Subclass may override and return
+ * false in {@link #isUsingDefaultShadow()} and replace with its own shadow implementation.
+ */
+public class ListRowPresenter extends RowPresenter {
+
+ private static final String TAG = "ListRowPresenter";
+ private static final boolean DEBUG = false;
+
+ public static class ViewHolder extends RowPresenter.ViewHolder {
+ final ListRowPresenter mListRowPresenter;
+ final HorizontalGridView mGridView;
+ final ItemBridgeAdapter mItemBridgeAdapter = new ItemBridgeAdapter();
+ final HorizontalHoverCardSwitcher mHoverCardViewSwitcher = new HorizontalHoverCardSwitcher();
+ final ColorOverlayDimmer mColorDimmer;
+
+ public ViewHolder(View rootView, HorizontalGridView gridView, ListRowPresenter p) {
+ super(rootView);
+ mGridView = gridView;
+ mListRowPresenter = p;
+ mColorDimmer = ColorOverlayDimmer.createDefault(rootView.getContext());
+ }
+
+ public final ListRowPresenter getListRowPresenter() {
+ return mListRowPresenter;
+ }
+
+ public final HorizontalGridView getGridView() {
+ return mGridView;
+ }
+ }
+
+ private PresenterSelector mHoverCardPresenterSelector;
+ private int mZoomFactor;
+ private boolean mShadowEnabled = true;
+ private int mBrowseRowsFadingEdgeLength = -1;
+
+ /**
+ * Constructs a ListRowPresenter with defaults.
+ * Uses {@link FocusHighlight#ZOOM_FACTOR_MEDIUM} for focus zooming.
+ */
+ public ListRowPresenter() {
+ this(FocusHighlight.ZOOM_FACTOR_MEDIUM);
+ }
+
+ /**
+ * Constructs a ListRowPresenter with the given parameters.
+ *
+ * @param zoomFactor Controls the zoom factor used when an item view is focused. One of
+ * {@link FocusHighlight#ZOOM_FACTOR_NONE},
+ * {@link FocusHighlight#ZOOM_FACTOR_SMALL},
+ * {@link FocusHighlight#ZOOM_FACTOR_MEDIUM},
+ * {@link FocusHighlight#ZOOM_FACTOR_LARGE}
+ */
+ public ListRowPresenter(int zoomFactor) {
+ mZoomFactor = zoomFactor;
+ }
+
+ /**
+ * Returns the zoom factor used for focus highlighting.
+ */
+ public final int getZoomFactor() {
+ return mZoomFactor;
+ }
+
+ private ItemBridgeAdapter.Wrapper mCardWrapper = new ItemBridgeAdapter.Wrapper() {
+ @Override
+ public View createWrapper(View root) {
+ ShadowOverlayContainer wrapper = new ShadowOverlayContainer(root.getContext());
+ wrapper.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ wrapper.initialize(needsDefaultShadow(), needsDefaultListSelectEffect());
+ return wrapper;
+ }
+ @Override
+ public void wrap(View wrapper, View wrapped) {
+ ((ShadowOverlayContainer) wrapper).wrap(wrapped);
+ }
+ };
+
+ @Override
+ protected void initializeRowViewHolder(RowPresenter.ViewHolder holder) {
+ super.initializeRowViewHolder(holder);
+ final ViewHolder rowViewHolder = (ViewHolder) holder;
+ if (needsDefaultListSelectEffect() || needsDefaultShadow()) {
+ rowViewHolder.mItemBridgeAdapter.setWrapper(mCardWrapper);
+ }
+ if (needsDefaultListSelectEffect()) {
+ ShadowOverlayContainer.prepareParentForShadow(rowViewHolder.mGridView);
+ ((ViewGroup) rowViewHolder.view).setClipChildren(false);
+ if (rowViewHolder.mContainerViewHolder != null) {
+ ((ViewGroup) rowViewHolder.mContainerViewHolder.view).setClipChildren(false);
+ }
+ }
+ FocusHighlightHelper.setupBrowseItemFocusHighlight(rowViewHolder.mItemBridgeAdapter, mZoomFactor);
+ rowViewHolder.mGridView.setOnChildSelectedListener(
+ new OnChildSelectedListener() {
+ @Override
+ public void onChildSelected(ViewGroup parent, View view, int position, long id) {
+ selectChildView(rowViewHolder, view);
+ }
+ });
+ rowViewHolder.mItemBridgeAdapter.setAdapterListener(
+ new ItemBridgeAdapter.AdapterListener() {
+ @Override
+ public void onCreate(final ItemBridgeAdapter.ViewHolder viewHolder) {
+ // Only when having an OnItemClickListner, we will attach the OnClickListener.
+ if (getOnItemClickedListener() != null) {
+ viewHolder.mHolder.view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
+ rowViewHolder.mGridView.getChildViewHolder(viewHolder.itemView);
+ if (getOnItemClickedListener() != null) {
+ getOnItemClickedListener().onItemClicked(ibh.mItem,
+ (ListRow) rowViewHolder.mRow);
+ }
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
+ if (viewHolder.itemView instanceof ShadowOverlayContainer) {
+ int dimmedColor = rowViewHolder.mColorDimmer.getPaint().getColor();
+ ((ShadowOverlayContainer) viewHolder.itemView).setOverlayColor(dimmedColor);
+ }
+ }
+ });
+ }
+
+ final boolean needsDefaultListSelectEffect() {
+ return isUsingDefaultListSelectEffect() && getSelectEffectEnabled();
+ }
+
+ /**
+ * Set {@link PresenterSelector} used for showing a select object in a hover card.
+ */
+ public final void setHoverCardPresenterSelector(PresenterSelector selector) {
+ mHoverCardPresenterSelector = selector;
+ }
+
+ /**
+ * Get {@link PresenterSelector} used for showing a select object in a hover card.
+ */
+ public final PresenterSelector getHoverCardPresenterSelector() {
+ return mHoverCardPresenterSelector;
+ }
+
+ /*
+ * Perform operations when a child of horizontal grid view is selected.
+ */
+ private void selectChildView(ViewHolder rowViewHolder, View view) {
+ ItemBridgeAdapter.ViewHolder ibh = null;
+ if (view != null) {
+ ibh = (ItemBridgeAdapter.ViewHolder)
+ rowViewHolder.mGridView.getChildViewHolder(view);
+ }
+ if (view == null) {
+ if (mHoverCardPresenterSelector != null) {
+ rowViewHolder.mHoverCardViewSwitcher.unselect();
+ }
+ if (getOnItemSelectedListener() != null) {
+ getOnItemSelectedListener().onItemSelected(null, rowViewHolder.mRow);
+ }
+ } else if (rowViewHolder.mExpanded && rowViewHolder.mSelected) {
+ if (mHoverCardPresenterSelector != null) {
+ rowViewHolder.mHoverCardViewSwitcher.select(rowViewHolder.mGridView, view,
+ ibh.mItem);
+ }
+ if (getOnItemSelectedListener() != null) {
+ getOnItemSelectedListener().onItemSelected(ibh.mItem, rowViewHolder.mRow);
+ }
+ }
+ }
+
+ @Override
+ protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
+ ListRowView rowView = new ListRowView(parent.getContext());
+ setupFadingEffect(rowView);
+ return new ViewHolder(rowView, rowView.getGridView(), this);
+ }
+
+ @Override
+ protected void onRowViewSelected(RowPresenter.ViewHolder holder, boolean selected) {
+ updateFooterViewSwitcher((ViewHolder) holder);
+ }
+
+ /*
+ * Show or hide hover card when row selection or expanded state is changed.
+ */
+ private void updateFooterViewSwitcher(ViewHolder vh) {
+ if (vh.mExpanded && vh.mSelected) {
+ if (mHoverCardPresenterSelector != null) {
+ vh.mHoverCardViewSwitcher.init((ViewGroup) vh.view,
+ mHoverCardPresenterSelector);
+ }
+ ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder)
+ vh.mGridView.findViewHolderForPosition(
+ vh.mGridView.getSelectedPosition());
+ selectChildView(vh, ibh == null ? null : ibh.itemView);
+ } else {
+ if (mHoverCardPresenterSelector != null) {
+ vh.mHoverCardViewSwitcher.clear();
+ }
+ }
+ }
+
+ private void setupFadingEffect(ListRowView rowView) {
+ // content is completely faded at 1/2 padding of left, fading length is 1/2 of padding.
+ HorizontalGridView gridView = rowView.getGridView();
+ if (mBrowseRowsFadingEdgeLength < 0) {
+ TypedArray ta = gridView.getContext()
+ .obtainStyledAttributes(R.styleable.LeanbackTheme);
+ mBrowseRowsFadingEdgeLength = (int) ta.getDimension(
+ R.styleable.LeanbackTheme_browseRowsFadingEdgeLength, 0);
+ ta.recycle();
+ }
+ gridView.setFadingLeftEdgeLength(mBrowseRowsFadingEdgeLength);
+ }
+
+ @Override
+ protected void onRowViewExpanded(RowPresenter.ViewHolder holder, boolean expanded) {
+ super.onRowViewExpanded(holder, expanded);
+ ViewHolder vh = (ViewHolder) holder;
+ vh.getGridView().setFadingLeftEdge(!expanded);
+ updateFooterViewSwitcher(vh);
+ }
+
+ @Override
+ protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
+ super.onBindRowViewHolder(holder, item);
+ ViewHolder vh = (ViewHolder) holder;
+ ListRow rowItem = (ListRow) item;
+ vh.mItemBridgeAdapter.clear();
+ vh.mItemBridgeAdapter.setAdapter(rowItem.getAdapter());
+ vh.mGridView.setAdapter(vh.mItemBridgeAdapter);
+ }
+
+ @Override
+ protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
+ ((ViewHolder) holder).mGridView.setAdapter(null);
+ super.onUnbindRowViewHolder(holder);
+ }
+
+ /**
+ * ListRowPresenter overrides the default select effect of {@link RowPresenter}
+ * and return false.
+ */
+ @Override
+ public final boolean isUsingDefaultSelectEffect() {
+ return false;
+ }
+
+ /**
+ * Returns true so that default select effect is applied to each individual
+ * child of {@link HorizontalGridView}. Subclass may return false to disable
+ * the default implementation.
+ * @see #onSelectLevelChanged(RowPresenter.ViewHolder)
+ */
+ public boolean isUsingDefaultListSelectEffect() {
+ return true;
+ }
+
+ /**
+ * Returns true if SDK >= 18, where default shadow
+ * is applied to each individual child of {@link HorizontalGridView}.
+ * Subclass may return false to disable.
+ */
+ public boolean isUsingDefaultShadow() {
+ return ShadowOverlayContainer.supportsShadow();
+ }
+
+ /**
+ * Enable or disable child shadow.
+ * This is not only for enable/disable default shadow implementation but also subclass must
+ * respect this flag.
+ */
+ public final void setShadowEnabled(boolean enabled) {
+ mShadowEnabled = enabled;
+ }
+
+ /**
+ * Returns true if child shadow is enabled.
+ * This is not only for enable/disable default shadow implementation but also subclass must
+ * respect this flag.
+ */
+ public final boolean getShadowEnabled() {
+ return mShadowEnabled;
+ }
+
+ final boolean needsDefaultShadow() {
+ return isUsingDefaultShadow() && getShadowEnabled();
+ }
+
+ @Override
+ public boolean canDrawOutOfBounds() {
+ return needsDefaultShadow();
+ }
+
+ /**
+ * Applies select level to header and draw a default color dim over each child
+ * of {@link HorizontalGridView}.
+ * <p>
+ * Subclass may override this method. A subclass
+ * needs to call super.onSelectLevelChanged() for applying header select level
+ * and optionally applying a default select level to each child view of
+ * {@link HorizontalGridView} if {@link #isUsingDefaultListSelectEffect()}
+ * is true. Subclass may override {@link #isUsingDefaultListSelectEffect()} to return
+ * false and deal with the individual item select level by itself.
+ * </p>
+ */
+ @Override
+ protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) {
+ super.onSelectLevelChanged(holder);
+ if (needsDefaultListSelectEffect()) {
+ ViewHolder vh = (ViewHolder) holder;
+ vh.mColorDimmer.setActiveLevel(holder.mSelectLevel);
+ int dimmedColor = vh.mColorDimmer.getPaint().getColor();
+ for (int i = 0, count = vh.mGridView.getChildCount(); i < count; i++) {
+ ShadowOverlayContainer wrapper = (ShadowOverlayContainer) vh.mGridView.getChildAt(i);
+ wrapper.setOverlayColor(dimmedColor);
+ }
+ if (vh.mGridView.getFadingLeftEdge()) {
+ vh.mGridView.invalidate();
+ }
+ }
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java b/v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java
new file mode 100644
index 0000000..41da46f
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ListRowView.java
@@ -0,0 +1,62 @@
+/*
+ * 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.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.
+ */
+public final class ListRowView extends LinearLayout {
+
+ private HorizontalGridView mGridView;
+
+ public ListRowView(Context context) {
+ this(context, null);
+ }
+
+ public ListRowView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ListRowView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ LayoutInflater inflater = LayoutInflater.from(context);
+ inflater.inflate(R.layout.lb_list_row, this);
+
+ mGridView = (HorizontalGridView) findViewById(R.id.row_content);
+ // Uncomment this to experiment with page-based scrolling.
+ // mGridView.setFocusScrollStrategy(HorizontalGridView.FOCUS_SCROLL_PAGE);
+
+ setOrientation(LinearLayout.VERTICAL);
+ setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+ }
+
+ public HorizontalGridView getGridView() {
+ return mGridView;
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java b/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
new file mode 100644
index 0000000..011b9c6
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ObjectAdapter.java
@@ -0,0 +1,249 @@
+/*
+ * 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.database.Observable;
+
+/**
+ * Adapter for leanback activities. Provides access to a data model and is
+ * decoupled from the presentation of the items via {@link PresenterSelector}.
+ */
+public abstract class ObjectAdapter {
+
+ public static final int NO_ID = -1;
+
+ /**
+ * A DataObserver can be notified when an ObjectAdapter's underlying data
+ * changes. Separate methods provide notifications about different types of
+ * changes.
+ */
+ 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
+ * notification methods.
+ */
+ public void onChanged() {
+ }
+
+ /**
+ * Called when a range of items in the ObjectAdapter has changed. The
+ * basic ordering and structure of the ObjectAdapter has not changed.
+ *
+ * @param positionStart The position of the first item that changed.
+ * @param itemCount The number of items changed.
+ */
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ onChanged();
+ }
+
+ /**
+ * Called when a range of items is inserted into the ObjectAdapter.
+ *
+ * @param positionStart The position of the first inserted item.
+ * @param itemCount The number of items inserted.
+ */
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ onChanged();
+ }
+
+ /**
+ * Called when a range of items is removed from the ObjectAdapter.
+ *
+ * @param positionStart The position of the first removed item.
+ * @param itemCount The number of items removed.
+ */
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ onChanged();
+ }
+ }
+
+ private static final class DataObservable extends Observable<DataObserver> {
+
+ public void notifyChanged() {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onChanged();
+ }
+ }
+
+ public void notifyItemRangeChanged(int positionStart, int itemCount) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeChanged(positionStart, itemCount);
+ }
+ }
+
+ public void notifyItemRangeInserted(int positionStart, int itemCount) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
+ }
+ }
+
+ public void notifyItemRangeRemoved(int positionStart, int itemCount) {
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
+ }
+ }
+ }
+
+ private final DataObservable mObservable = new DataObservable();
+ private boolean mHasStableIds;
+ private PresenterSelector mPresenterSelector;
+
+ /**
+ * Construct an adapter with the given {@link PresenterSelector}.
+ */
+ public ObjectAdapter(PresenterSelector presenterSelector) {
+ setPresenterSelector(presenterSelector);
+ }
+
+ /**
+ * Construct an adapter that uses the given {@link Presenter} for all items.
+ */
+ public ObjectAdapter(Presenter presenter) {
+ setPresenterSelector(new SinglePresenterSelector(presenter));
+ }
+
+ /**
+ * Construct an adapter.
+ */
+ public ObjectAdapter() {
+ }
+
+ /**
+ * Set the presenter selector. May not be null.
+ */
+ public final void setPresenterSelector(PresenterSelector presenterSelector) {
+ if (presenterSelector == null) {
+ throw new IllegalArgumentException("Presenter selector must not be null");
+ }
+ final boolean update = (mPresenterSelector != null);
+ final boolean selectorChanged = update && mPresenterSelector != presenterSelector;
+
+ mPresenterSelector = presenterSelector;
+
+ if (selectorChanged) {
+ onPresenterSelectorChanged();
+ }
+ if (update) {
+ notifyChanged();
+ }
+ }
+
+ /**
+ * Called when {@link #setPresenterSelector(PresenterSelector)} is called
+ * and the PresenterSelector differs from the previous one.
+ */
+ protected void onPresenterSelectorChanged() {
+ }
+
+ /**
+ * Returns the presenter selector;
+ */
+ public final PresenterSelector getPresenterSelector() {
+ return mPresenterSelector;
+ }
+
+ /**
+ * Register a DataObserver for data change notifications.
+ */
+ public final void registerObserver(DataObserver observer) {
+ mObservable.registerObserver(observer);
+ }
+
+ /**
+ * Unregister a DataObserver for data change notifications.
+ */
+ public final void unregisterObserver(DataObserver observer) {
+ mObservable.unregisterObserver(observer);
+ }
+
+ /**
+ * Unregister all DataObservers for this ObservableList.
+ */
+ public final void unregisterAllObservers() {
+ mObservable.unregisterAll();
+ }
+
+ final protected void notifyItemRangeChanged(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeChanged(positionStart, itemCount);
+ }
+
+ final protected void notifyItemRangeInserted(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeInserted(positionStart, itemCount);
+ }
+
+ final protected void notifyItemRangeRemoved(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeRemoved(positionStart, itemCount);
+ }
+
+ final protected void notifyChanged() {
+ mObservable.notifyChanged();
+ }
+
+ /**
+ * 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.
+ */
+ public final boolean hasStableIds() {
+ return mHasStableIds;
+ }
+
+ /**
+ * Sets whether the item ids are stable across changes to the underlying
+ * data.
+ */
+ public final void setHasStableIds(boolean hasStableIds) {
+ boolean changed = mHasStableIds != hasStableIds;
+ mHasStableIds = hasStableIds;
+
+ if (changed) {
+ onHasStableIdsChanged();
+ }
+ }
+
+ /**
+ * Called when {@link #setHasStableIds(boolean)} is called and the status
+ * of stable ids has changed.
+ */
+ protected void onHasStableIdsChanged() {
+ }
+
+ /**
+ * Returns the {@link Presenter} for the given item from the adapter.
+ */
+ public final Presenter getPresenter(Object item) {
+ if (mPresenterSelector == null) {
+ throw new IllegalStateException("Presenter selector must not be null");
+ }
+ return mPresenterSelector.getPresenter(item);
+ }
+
+ /**
+ * Returns the number of items in the adapter.
+ */
+ public abstract int size();
+
+ /**
+ * Returns the item for the given position.
+ */
+ public abstract Object get(int position);
+
+ /**
+ * Returns id for the given position.
+ */
+ public long getId(int position) {
+ return NO_ID;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java b/v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
new file mode 100644
index 0000000..531c1cf
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/OnActionClickedListener.java
@@ -0,0 +1,25 @@
+/*
+ * 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.view.View;
+
+/**
+ * Interface for receiving notification when an action is clicked.
+ */
+public interface OnActionClickedListener {
+
+ public void onActionClicked(Action action);
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java b/v17/leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java
new file mode 100644
index 0000000..f5f18f8
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/OnChildSelectedListener.java
@@ -0,0 +1,40 @@
+/*
+ * 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.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Interface definition for a callback to be invoked when a child of this
+ * viewgroup has been selected.
+ */
+public interface OnChildSelectedListener {
+ /**
+ * Callback method to be invoked when a child of this viewgroup has been
+ * selected.
+ *
+ * <p>This method may be called during layout, so implementations of this
+ * interface need to be careful not to ... (todo).
+ *
+ * @param parent The ViewGroup where the selection happened.
+ * @param view The view within the ViewGroup that is selected, or null if no
+ * view is selected.
+ * @param position The position of the view in the adapter, or NO_POSITION
+ * if no view is selected.
+ * @param id The id of the child that is selected, or NO_ID if no view is
+ * selected.
+ */
+ void onChildSelected(ViewGroup parent, View view, int position, long id);
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemClickedListener.java b/v17/leanback/src/android/support/v17/leanback/widget/OnItemClickedListener.java
new file mode 100644
index 0000000..9530f90
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/OnItemClickedListener.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.v17.leanback.widget;
+
+import android.view.View;
+
+/**
+ * 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>
+ */
+public interface OnItemClickedListener {
+
+ public void onItemClicked(Object item, Row row);
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.java b/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.java
new file mode 100644
index 0000000..946c69d
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/OnItemSelectedListener.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.v17.leanback.widget;
+
+/**
+ * Interface for receiving notification when a row or item becomes selected.
+ */
+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
+ * 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 item The item that is currently selected.
+ * @param row The row that is currently selected.
+ */
+ public void onItemSelected(Object item, Row row);
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Presenter.java b/v17/leanback/src/android/support/v17/leanback/widget/Presenter.java
new file mode 100644
index 0000000..deffa45
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/Presenter.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.v17.leanback.widget;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A Presenter is used to generate {@link View}s and bind Objects to them on
+ * demand. It is closely related to concept of an {@link
+ * android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, but is
+ * not position-based.
+ *
+ * <p>
+ * A trivial Presenter that takes a string and renders it into a {@link
+ * android.widget.TextView TextView}:
+ *
+ * <pre class="prettyprint">
+ * public class StringTextViewPresenter extends Presenter {
+ * // This class does not need a custom ViewHolder, since it does not use
+ * // a complex layout.
+ *
+ * {@literal @}Override
+ * public ViewHolder onCreateViewHolder(ViewGroup parent) {
+ * return new ViewHolder(new TextView(parent.getContext()));
+ * }
+ *
+ * {@literal @}Override
+ * public void onBindViewHolder(ViewHolder viewHolder, Object item) {
+ * String str = (String) item;
+ * TextView textView = (TextView) viewHolder.mView;
+ *
+ * textView.setText(item);
+ * }
+ *
+ * {@literal @}Override
+ * public void onUnbindViewHolder(ViewHolder viewHolder) {
+ * // Nothing to unbind for TextView, but if this viewHolder had
+ * // allocated bitmaps, they can be released here.
+ * }
+ * }
+ * </pre>
+ */
+public abstract class Presenter {
+ /**
+ * ViewHolder can be subclassed and used to cache any view accessors needed
+ * to improve binding performance (for example, results of findViewById)
+ * without needing to subclass a View.
+ */
+ public static class ViewHolder {
+ public final View view;
+
+ public ViewHolder(View view) {
+ this.view = view;
+ }
+ }
+
+ /**
+ * Creates a new {@link View}.
+ */
+ public abstract ViewHolder onCreateViewHolder(ViewGroup parent);
+
+ /**
+ * Binds a {@link View} to an item.
+ */
+ public abstract void onBindViewHolder(ViewHolder viewHolder, Object item);
+
+ /**
+ * Unbinds a {@link View} from an item. Any expensive references may be
+ * released here, and any fields that are not bound for every item should be
+ * cleared here.
+ */
+ public abstract void onUnbindViewHolder(ViewHolder viewHolder);
+
+ /**
+ * Called when a view created by this presenter has been attached to a window.
+ *
+ * <p>This can be used as a reasonable signal that the view is about to be seen
+ * by the user. If the adapter previously freed any resources in
+ * {@link #onViewDetachedFromWindow(ViewHolder)}
+ * those resources should be restored here.</p>
+ *
+ * @param holder Holder of the view being attached
+ */
+ public void onViewAttachedToWindow(ViewHolder holder) {
+ }
+
+ /**
+ * Called when a view created by this presenter has been detached from its window.
+ *
+ * <p>Becoming detached from the window is not necessarily a permanent condition;
+ * the consumer of an presenter's views may choose to cache views offscreen while they
+ * are not visible, attaching an detaching them as appropriate.</p>
+ *
+ * @param holder Holder of the view being detached
+ */
+ public void onViewDetachedFromWindow(ViewHolder holder) {
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PresenterSelector.java b/v17/leanback/src/android/support/v17/leanback/widget/PresenterSelector.java
new file mode 100644
index 0000000..c38957d
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PresenterSelector.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;
+
+/**
+ * A PresenterSelector is used to obtain a {@link Presenter} for a given Object.
+ */
+public abstract class PresenterSelector {
+ /**
+ * Returns a presenter for the given item.
+ */
+ public abstract Presenter getPresenter(Object item);
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java b/v17/leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java
new file mode 100644
index 0000000..8a9c726
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/PresenterSwitcher.java
@@ -0,0 +1,106 @@
+/*
+ * 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.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An abstract helper class that switches view in parent view using {@link PresenterSelector}
+ * subclass should define {@link #insertView(View)} of how to add the view
+ * in parent and optionally override {@link #onViewSelected(View)}.
+ */
+public abstract class PresenterSwitcher {
+
+ private ViewGroup mParent;
+ private PresenterSelector mPresenterSelector;
+ private Presenter mCurrentPresenter;
+ private Presenter.ViewHolder mCurrentViewHolder;
+
+ /**
+ * Initialize switcher with a parent view to insert view into and a
+ * {@link PresenterSelector} for choose {@link Presenter} for object.
+ * This will destroy any existing views.
+ */
+ public void init(ViewGroup parent, PresenterSelector presenterSelector) {
+ clear();
+ mParent = parent;
+ mPresenterSelector = presenterSelector;
+ }
+
+ public void select(Object object) {
+ switchView(object);
+ showView(true);
+ }
+
+ public void unselect() {
+ showView(false);
+ }
+
+ public final ViewGroup getParentViewGroup() {
+ return mParent;
+ }
+
+ private void showView(boolean show) {
+ if (mCurrentViewHolder != null) {
+ showView(mCurrentViewHolder.view, show);
+ }
+ }
+
+ private void switchView(Object object) {
+ Presenter presenter = mPresenterSelector.getPresenter(object);
+ if (presenter != mCurrentPresenter) {
+ showView(false);
+ clear();
+ mCurrentPresenter = presenter;
+ if (mCurrentPresenter == null) {
+ return;
+ }
+ mCurrentViewHolder = mCurrentPresenter.onCreateViewHolder(mParent);
+ insertView(mCurrentViewHolder.view);
+ } else {
+ if (mCurrentPresenter == null) {
+ return;
+ }
+ mCurrentPresenter.onUnbindViewHolder(mCurrentViewHolder);
+ }
+ mCurrentPresenter.onBindViewHolder(mCurrentViewHolder, object);
+ onViewSelected(mCurrentViewHolder.view);
+ }
+
+ protected abstract void insertView(View view);
+
+ /**
+ * Called when a view is bound to the object of {@link #select(Object)}.
+ */
+ protected void onViewSelected(View view) {
+ }
+
+ protected void showView(View view, boolean visible) {
+ view.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+
+ /**
+ * Destroy created views.
+ */
+ public void clear() {
+ if (mCurrentPresenter != null) {
+ mCurrentPresenter.onUnbindViewHolder(mCurrentViewHolder);
+ mParent.removeView(mCurrentViewHolder.view);
+ mCurrentViewHolder = null;
+ mCurrentPresenter = null;
+ }
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/Row.java b/v17/leanback/src/android/support/v17/leanback/widget/Row.java
new file mode 100644
index 0000000..893d5c0
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/Row.java
@@ -0,0 +1,91 @@
+/*
+ * 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 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).
+ */
+public class Row {
+
+ private static final int FLAG_ID_USE_MASK = 1;
+ private static final int FLAG_ID_USE_HEADER = 1;
+ private static final int FLAG_ID_USE_ID = 0;
+
+ private int mFlags = FLAG_ID_USE_HEADER;
+ private HeaderItem mHeaderItem;
+ private long mId = NO_ID;
+
+ public Row(long id, HeaderItem headerItem) {
+ setId(id);
+ setHeaderItem(headerItem);
+ }
+
+ public Row(HeaderItem headerItem) {
+ setHeaderItem(headerItem);
+ }
+
+ public Row() {
+ }
+
+ /**
+ * Get optional {@link HeaderItem} that represents metadata for the row.
+ */
+ public final HeaderItem getHeaderItem() {
+ return mHeaderItem;
+ }
+
+ /**
+ * Set the {@link HeaderItem} that represents metadata for the row.
+ */
+ public final void setHeaderItem(HeaderItem headerItem) {
+ mHeaderItem = headerItem;
+ }
+
+ /**
+ * Set id for this row.
+ */
+ public final void setId(long id) {
+ mId = id;
+ setFlags(FLAG_ID_USE_ID, FLAG_ID_USE_MASK);
+ }
+
+ /**
+ * 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.
+ */
+ public final long getId() {
+ if ( (mFlags & FLAG_ID_USE_MASK) == FLAG_ID_USE_HEADER) {
+ HeaderItem header = getHeaderItem();
+ if (header != null) {
+ return header.getId();
+ }
+ return NO_ID;
+ } else {
+ return mId;
+ }
+ }
+
+ final void setFlags(int flags, int mask) {
+ mFlags = (mFlags & ~mask) | (flags & mask);
+ }
+
+ final int getFlags() {
+ return mFlags;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java b/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.java
new file mode 100644
index 0000000..05dff40
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RowContainerView.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.v17.leanback.widget;
+
+import android.content.Context;
+import android.support.v17.leanback.R;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+/**
+ * RowContainerView wraps header and user defined row view
+ */
+final class RowContainerView extends LinearLayout {
+
+ private ViewGroup mHeaderDock;
+
+ public RowContainerView(Context context) {
+ this(context, null, 0);
+ }
+
+ public RowContainerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RowContainerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setOrientation(VERTICAL);
+ LayoutInflater inflater = LayoutInflater.from(context);
+ inflater.inflate(R.layout.lb_row_container, this);
+
+ mHeaderDock = (ViewGroup) findViewById(R.id.lb_row_container_header_dock);
+ setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ }
+
+ public void addHeaderView(View headerView) {
+ if (mHeaderDock.indexOfChild(headerView) < 0) {
+ mHeaderDock.addView(headerView, 0);
+ }
+ }
+
+ public void removeHeaderView(View headerView) {
+ if (mHeaderDock.indexOfChild(headerView) >= 0) {
+ mHeaderDock.removeView(headerView);
+ }
+ }
+
+ public void addRowView(View view) {
+ addView(view);
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
new file mode 100644
index 0000000..b31d5f0
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderPresenter.java
@@ -0,0 +1,78 @@
+/*
+ * 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.graphics.ColorOverlayDimmer;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * RowHeaderPresenter provides a default implementation for header using TextView.
+ * If subclass override and creates its own view, subclass must also override
+ * {@link #onSelectLevelChanged(ViewHolder)}.
+ */
+public class RowHeaderPresenter extends Presenter {
+
+ public static class ViewHolder extends Presenter.ViewHolder {
+ float mSelectLevel;
+ int mOriginalTextColor;
+ ColorOverlayDimmer mColorDimmer;
+ public ViewHolder(View view) {
+ super(view);
+ }
+ public final float getSelectLevel() {
+ return mSelectLevel;
+ }
+ }
+
+ @Override
+ public Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) {
+ RowHeaderView headerView = new RowHeaderView(parent.getContext());
+ ViewHolder viewHolder = new ViewHolder(headerView);
+ viewHolder.mOriginalTextColor = headerView.getCurrentTextColor();
+ return viewHolder;
+ }
+
+ @Override
+ public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ setSelectLevel((ViewHolder) viewHolder, 0);
+ Row rowItem = (Row) item;
+ if (rowItem != null) {
+ HeaderItem headerItem = rowItem.getHeaderItem();
+ if (headerItem != null) {
+ String text = headerItem.getName();
+ ((RowHeaderView) viewHolder.view).setText(text);
+ }
+ }
+ }
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
+ ((RowHeaderView) viewHolder.view).setText(null);
+ }
+
+ public final void setSelectLevel(ViewHolder holder, float selectLevel) {
+ holder.mSelectLevel = selectLevel;
+ onSelectLevelChanged(holder);
+ }
+
+ 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));
+ }
+}
\ No newline at end of file
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java b/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
new file mode 100644
index 0000000..0a8f98e
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RowHeaderView.java
@@ -0,0 +1,38 @@
+/*
+ * 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.Context;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+/**
+ * RowHeaderView is a header text view.
+ */
+public final class RowHeaderView extends TextView {
+
+ public RowHeaderView(Context context) {
+ this(context, null);
+ }
+
+ public RowHeaderView(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.rowHeaderStyle);
+ }
+
+ public RowHeaderView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
new file mode 100644
index 0000000..787597f
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/RowPresenter.java
@@ -0,0 +1,386 @@
+/*
+ * 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.app.HeadersFragment;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A presenter that renders {@link Row}.
+ *
+ * <h3>Customize UI widgets</h3>
+ * When 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
+ * 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:
+ * <ul>
+ * <li>
+ * 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)}.
+ * </li>
+ * <li>
+ * 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)}.
+ * </li>
+ * </ul>
+ *
+ * <h3>User events:</h3>
+ * RowPresenter provides {@link OnItemSelectedListener} and {@link OnItemClickedListener}.
+ * If 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.
+ *
+ * <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.
+ * </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.
+ * </p>
+ */
+public abstract class RowPresenter extends Presenter {
+
+ static class ContainerViewHolder extends Presenter.ViewHolder {
+ /**
+ * wrapped row view holder
+ */
+ final ViewHolder mRowViewHolder;
+
+ public ContainerViewHolder(RowContainerView containerView, ViewHolder rowViewHolder) {
+ super(containerView);
+ containerView.addRowView(rowViewHolder.view);
+ if (rowViewHolder.mHeaderViewHolder != null) {
+ containerView.addHeaderView(rowViewHolder.mHeaderViewHolder.view);
+ }
+ mRowViewHolder = rowViewHolder;
+ mRowViewHolder.mContainerViewHolder = this;
+ }
+ }
+
+ public static class ViewHolder extends Presenter.ViewHolder {
+ ContainerViewHolder mContainerViewHolder;
+ RowHeaderPresenter.ViewHolder mHeaderViewHolder;
+ Row mRow;
+ boolean mSelected;
+ boolean mExpanded;
+ boolean mInitialzed;
+ float mSelectLevel = 0f; // initially unselected
+ public ViewHolder(View view) {
+ super(view);
+ }
+ public final Row getRow() {
+ return mRow;
+ }
+ public final boolean isExpanded() {
+ return mExpanded;
+ }
+ public final boolean isSelected() {
+ return mSelected;
+ }
+ public final float getSelectLevel() {
+ return mSelectLevel;
+ }
+ public final RowHeaderPresenter.ViewHolder getHeaderViewHolder() {
+ return mHeaderViewHolder;
+ }
+ }
+
+ private RowHeaderPresenter mHeaderPresenter = new RowHeaderPresenter();
+ private OnItemSelectedListener mOnItemSelectedListener;
+ private OnItemClickedListener mOnItemClickedListener;
+
+ boolean mSelectEffectEnabled = true;
+
+ @Override
+ public final Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) {
+ ViewHolder vh = createRowViewHolder(parent);
+ vh.mInitialzed = false;
+ Presenter.ViewHolder result;
+ if (needsRowContainerView()) {
+ RowContainerView containerView = new RowContainerView(parent.getContext());
+ if (mHeaderPresenter != null) {
+ vh.mHeaderViewHolder = (RowHeaderPresenter.ViewHolder)
+ mHeaderPresenter.onCreateViewHolder((ViewGroup) vh.view);
+ }
+ result = new ContainerViewHolder(containerView, vh);
+ } else {
+ result = vh;
+ }
+ initializeRowViewHolder(vh);
+ if (!vh.mInitialzed) {
+ throw new RuntimeException("super.initializeRowViewHolder() must be called");
+ }
+ return result;
+ }
+
+ /**
+ * Called to create a ViewHolder object for row, subclass of {@link RowPresenter}
+ * should override and return a different concrete ViewHolder object.
+ */
+ 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
+ * super.initializeRowViewHolder(ViewHolder).
+ */
+ 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.
+ */
+ public final void setHeaderPresenter(RowHeaderPresenter headerPresenter) {
+ mHeaderPresenter = headerPresenter;
+ }
+
+ /**
+ * Get optional presenter used for rendering header. May return null.
+ */
+ public final RowHeaderPresenter getHeaderPresenter() {
+ return mHeaderPresenter;
+ }
+
+ /**
+ * Get wrapped {@link RowPresenter.ViewHolder}
+ */
+ public final ViewHolder getRowViewHolder(Presenter.ViewHolder holder) {
+ if (holder instanceof ContainerViewHolder) {
+ return ((ContainerViewHolder) holder).mRowViewHolder;
+ } else {
+ return (ViewHolder) holder;
+ }
+ }
+
+ /**
+ * Change expanded state of row view.
+ */
+ public final void setRowViewExpanded(Presenter.ViewHolder holder, boolean expanded) {
+ ViewHolder rowViewHolder = getRowViewHolder(holder);
+ rowViewHolder.mExpanded = expanded;
+ onRowViewExpanded(rowViewHolder, expanded);
+ }
+
+ /**
+ * Change select state of row view.
+ */
+ public final void setRowViewSelected(Presenter.ViewHolder holder, boolean selected) {
+ ViewHolder rowViewHolder = getRowViewHolder(holder);
+ rowViewHolder.mSelected = selected;
+ onRowViewSelected(rowViewHolder, selected);
+ }
+
+ /**
+ * 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.
+ */
+ 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);
+ }
+ }
+ }
+
+ /**
+ * 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.
+ */
+ protected void onRowViewSelected(ViewHolder vh, boolean selected) {
+ if (selected && mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onItemSelected(null, vh.getRow());
+ }
+ }
+
+ /**
+ * Set current select level from 0(unselected) to 1(selected).
+ * Subclass should override {@link #onSelectLevelChanged(ViewHolder)}.
+ */
+ public final void setSelectLevel(Presenter.ViewHolder vh, float level) {
+ ViewHolder rowViewHolder = getRowViewHolder(vh);
+ rowViewHolder.mSelectLevel = level;
+ onSelectLevelChanged(rowViewHolder);
+ }
+
+ /**
+ * Get current select level from 0(unselected) to 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.
+ */
+ protected void onSelectLevelChanged(ViewHolder vh) {
+ if (getSelectEffectEnabled() && vh.mHeaderViewHolder != null) {
+ mHeaderPresenter.setSelectLevel(vh.mHeaderViewHolder, vh.mSelectLevel);
+ }
+ }
+
+ /**
+ * Enables or disables the row selection effect.
+ * This is not only for enable/disable default dim implementation but also subclass must
+ * respect this flag.
+ */
+ 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.
+ */
+ 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)}.
+ */
+ public boolean isUsingDefaultSelectEffect() {
+ return true;
+ }
+
+ final boolean needsDefaultSelectEffect() {
+ return isUsingDefaultSelectEffect() && getSelectEffectEnabled();
+ }
+
+ final boolean needsRowContainerView() {
+ return mHeaderPresenter != null;
+ }
+
+ /**
+ * Return true if the Row view can draw outside bounds.
+ */
+ public boolean canDrawOutOfBounds() {
+ return false;
+ }
+
+ @Override
+ public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ onBindRowViewHolder(getRowViewHolder(viewHolder), item);
+ }
+
+ protected void onBindRowViewHolder(ViewHolder vh, Object item) {
+ vh.mRow = (Row) item;
+ if (vh.mHeaderViewHolder != null) {
+ mHeaderPresenter.onBindViewHolder(vh.mHeaderViewHolder, item);
+ }
+ }
+
+ @Override
+ public final void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
+ onUnbindRowViewHolder(getRowViewHolder(viewHolder));
+ }
+
+ protected void onUnbindRowViewHolder(ViewHolder vh) {
+ if (vh.mHeaderViewHolder != null) {
+ mHeaderPresenter.onUnbindViewHolder(vh.mHeaderViewHolder);
+ }
+ vh.mRow = null;
+ }
+
+ @Override
+ public final void onViewAttachedToWindow(Presenter.ViewHolder holder) {
+ onRowViewAttachedToWindow(getRowViewHolder(holder));
+ }
+
+ protected void onRowViewAttachedToWindow(ViewHolder vh) {
+ if (vh.mHeaderViewHolder != null) {
+ mHeaderPresenter.onViewAttachedToWindow(vh.mHeaderViewHolder);
+ }
+ }
+
+ @Override
+ public final void onViewDetachedFromWindow(Presenter.ViewHolder holder) {
+ onRowViewDetachedFromWindow(getRowViewHolder(holder));
+ }
+
+ protected void onRowViewDetachedFromWindow(ViewHolder vh) {
+ if (vh.mHeaderViewHolder != null) {
+ mHeaderPresenter.onViewDetachedFromWindow(vh.mHeaderViewHolder);
+ }
+ }
+
+ /**
+ * 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 setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ }
+
+ /**
+ * Get 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.
+ */
+ public final void setOnItemClickedListener(OnItemClickedListener listener) {
+ mOnItemClickedListener = listener;
+ }
+
+ /**
+ * Set listener for item click event.
+ */
+ public final OnItemClickedListener getOnItemClickedListener() {
+ return mOnItemClickedListener;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java b/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
new file mode 100644
index 0000000..029db3e
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
@@ -0,0 +1,191 @@
+/*
+ * 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.os.Handler;
+import android.os.SystemClock;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.support.v17.leanback.R;
+import android.widget.TextView;
+
+/**
+ * SearchBar is a search widget.
+ */
+public class SearchBar extends RelativeLayout {
+ private static final String TAG = SearchBar.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ /**
+ * Listener for search query changes
+ */
+ public interface SearchBarListener {
+
+ /**
+ * Method invoked when the search bar detects a change in the query.
+ *
+ * @param query The current full query.
+ */
+ public void onSearchQueryChange(String query);
+
+ /**
+ * Method invoked when the search query is submitted.
+ *
+ * @param query The query being submitted.
+ */
+ public void onSearchQuerySubmit(String query);
+
+ /**
+ * Method invoked when the IME is being dismissed.
+ *
+ * @param query The query set in the search bar at the time the IME is being dismissed.
+ */
+ public void onKeyboardDismiss(String query);
+ }
+
+ private SearchBarListener mSearchBarListener;
+ private SearchEditText mSearchTextEditor;
+ private String mSearchQuery;
+ private final Handler mHandler = new Handler();
+
+ public SearchBar(Context context) {
+ this(context, null);
+ }
+
+ public SearchBar(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SearchBar(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mSearchQuery = "";
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mSearchTextEditor = (SearchEditText)findViewById(R.id.lb_search_text_editor);
+ mSearchTextEditor.setOnFocusChangeListener(new OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (DEBUG) Log.v(TAG, "onFocusChange " + hasFocus);
+ if (hasFocus) {
+ showNativeKeyboard();
+ }
+ }
+ });
+ mSearchTextEditor.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
+ setSearchQuery(charSequence.toString());
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+
+ }
+ });
+ mSearchTextEditor.setOnKeyboardDismissListener(
+ new SearchEditText.OnKeyboardDismissListener() {
+ @Override
+ public void onKeyboardDismiss() {
+ if (null != mSearchBarListener) {
+ mSearchBarListener.onKeyboardDismiss(mSearchQuery);
+ }
+ }
+ });
+ mSearchTextEditor.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView textView, int action, KeyEvent keyEvent) {
+ if (EditorInfo.IME_ACTION_SEARCH == action && null != mSearchBarListener) {
+ mSearchBarListener.onSearchQuerySubmit(mSearchQuery);
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSearchTextEditor.requestFocus();
+ mSearchTextEditor.requestFocusFromTouch();
+ }
+ });
+ }
+
+ /**
+ * Set a listener for when the term search changes
+ * @param listener
+ */
+ public void setSearchBarListener(SearchBarListener listener) {
+ mSearchBarListener = listener;
+ }
+
+ /**
+ * Set the search query
+ * @param query the search query to use
+ */
+ public void setSearchQuery(String query) {
+ if (query.equals(mSearchQuery)) {
+ return;
+ }
+ mSearchQuery = query;
+ if (null != mSearchBarListener) {
+ mSearchBarListener.onSearchQueryChange(mSearchQuery);
+ }
+ }
+
+ /**
+ * Set the hint text shown in the search bar.
+ * @param hint The hint to use.
+ */
+ public void setHint(String hint) {
+ mSearchTextEditor.setHint(hint);
+ }
+
+ protected void showNativeKeyboard() {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSearchTextEditor.requestFocusFromTouch();
+ mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
+ SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN,
+ mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
+ mSearchTextEditor.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
+ SystemClock.uptimeMillis(), MotionEvent.ACTION_UP,
+ mSearchTextEditor.getWidth(), mSearchTextEditor.getHeight(), 0));
+ }
+ });
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java b/v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java
new file mode 100644
index 0000000..41353c9
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SearchEditText.java
@@ -0,0 +1,66 @@
+/*
+ * 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.support.v17.leanback.R;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.widget.EditText;
+
+/**
+ * EditText widget that monitors keyboard changes.
+ */
+public class SearchEditText extends EditText {
+ private static final String TAG = SearchEditText.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ public interface OnKeyboardDismissListener {
+ public void onKeyboardDismiss();
+ }
+
+ private OnKeyboardDismissListener mKeyboardDismissListener;
+
+ public SearchEditText(Context context) {
+ this(context, null);
+ }
+
+ public SearchEditText(Context context, AttributeSet attrs) {
+ this(context, attrs, R.style.TextAppearance_Leanback_SearchTextEdit);
+ }
+
+ public SearchEditText(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+ if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+ if (DEBUG) Log.v(TAG, "Keyboard being dismissed");
+ mKeyboardDismissListener.onKeyboardDismiss();
+ return true;
+ }
+ return super.onKeyPreIme(keyCode, event);
+ }
+
+ /**
+ * Set a keyboard dismissed listener.
+ *
+ * @param listener The listener.
+ */
+ public void setOnKeyboardDismissListener(OnKeyboardDismissListener listener) {
+ mKeyboardDismissListener = listener;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java b/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
new file mode 100644
index 0000000..1fb1c93
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SearchOrbView.java
@@ -0,0 +1,90 @@
+/*
+ * 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.Rect;
+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;
+
+public class SearchOrbView extends LinearLayout implements View.OnClickListener {
+ private final static String TAG = SearchOrbView.class.getSimpleName();
+ private final static boolean DEBUG = false;
+
+ private OnClickListener mListener;
+ private LinearLayout mSearchOrbView;
+
+ public SearchOrbView(Context context) {
+ this(context, null);
+ }
+
+ public SearchOrbView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SearchOrbView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ LayoutInflater inflater = (LayoutInflater) context
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mSearchOrbView = (LinearLayout) inflater.inflate(R.layout.lb_search_orb, this, true);
+
+ // By default we are not visible
+ setVisibility(INVISIBLE);
+ setFocusable(true);
+ mSearchOrbView.setAlpha(0.5f);
+
+ setOnClickListener(this);
+ }
+
+ @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);
+ if (DEBUG) Log.v(TAG, "onFocusChanged " + gainFocus + " " + direction);
+ if (gainFocus) {
+ mSearchOrbView.setAlpha(1.0f);
+ } else {
+ mSearchOrbView.setAlpha(0.5f);
+ }
+ }
+
+ /**
+ * Set the on click listener for the orb
+ * @param listener The listener.
+ */
+ public void setOnOrbClickedListener(OnClickListener listener) {
+ mListener = listener;
+ if (null != listener) {
+ setVisibility(View.VISIBLE);
+ } else {
+ setVisibility(View.INVISIBLE);
+ }
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java b/v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
new file mode 100644
index 0000000..00ed8d4
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ShadowHelper.java
@@ -0,0 +1,120 @@
+/*
+ * 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.os.Build;
+import android.view.ViewGroup;
+
+
+/**
+ * Helper for shadow.
+ */
+final class ShadowHelper {
+
+ final static ShadowHelper sInstance = new ShadowHelper();
+ boolean mSupportsShadow;
+ ShadowHelperVersionImpl mImpl;
+
+ /**
+ * Interface implemented by classes that support Shadow.
+ */
+ static interface ShadowHelperVersionImpl {
+
+ public void prepareParent(ViewGroup parent);
+
+ public Object addShadow(ViewGroup shadowContainer);
+
+ public void setShadowFocusLevel(Object impl, float level);
+
+ }
+
+ /**
+ * Interface used when we do not support Shadow animations.
+ */
+ private static final class ShadowHelperStubImpl implements ShadowHelperVersionImpl {
+
+ @Override
+ public void prepareParent(ViewGroup parent) {
+ // do nothing
+ }
+
+ @Override
+ public Object addShadow(ViewGroup shadowContainer) {
+ // do nothing
+ return null;
+ }
+
+ @Override
+ public void setShadowFocusLevel(Object impl, float level) {
+ // do nothing
+ }
+
+ }
+
+ /**
+ * Implementation used on JBMR2 (and above).
+ */
+ private static final class ShadowHelperJbmr2Impl implements ShadowHelperVersionImpl {
+
+ @Override
+ public void prepareParent(ViewGroup parent) {
+ ShadowHelperJbmr2.prepareParent(parent);
+ }
+
+ @Override
+ public Object addShadow(ViewGroup shadowContainer) {
+ return ShadowHelperJbmr2.addShadow(shadowContainer);
+ }
+
+ @Override
+ public void setShadowFocusLevel(Object impl, float level) {
+ ShadowHelperJbmr2.setShadowFocusLevel(impl, level);
+ }
+
+ }
+
+ /**
+ * Returns the ShadowHelper.
+ */
+ private ShadowHelper() {
+ if (Build.VERSION.SDK_INT >= 18) {
+ mSupportsShadow = true;
+ mImpl = new ShadowHelperJbmr2Impl();
+ } else {
+ mSupportsShadow = false;
+ mImpl = new ShadowHelperStubImpl();
+ }
+ }
+
+ public static ShadowHelper getInstance() {
+ return sInstance;
+ }
+
+ public boolean supportsShadow() {
+ return mSupportsShadow;
+ }
+
+ public void prepareParent(ViewGroup parent) {
+ mImpl.prepareParent(parent);
+ }
+
+ public Object addShadow(ViewGroup shadowContainer) {
+ return mImpl.addShadow(shadowContainer);
+ }
+
+ public void setShadowFocusLevel(Object impl, float level) {
+ mImpl.setShadowFocusLevel(impl, level);
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java b/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
new file mode 100644
index 0000000..62bc191
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/ShadowOverlayContainer.java
@@ -0,0 +1,200 @@
+/*
+ * 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.support.v17.leanback.R;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * ShadowOverlayContainer Provides a SDK version independent wrapper container
+ * to take care of shadow and/or color overlay.
+ * <p>
+ * Shadow and color dimmer overlay are both optional. When shadow is used, it's
+ * user's responsibility to properly call setClipChildren(false) on parent views if
+ * the shadow can appear outside bounds of parent views.
+ * {@link #prepareParentForShadow(ViewGroup)} must be called on parent of container
+ * before using shadow. Depending on sdk version, optical bounds might be applied
+ * to parent.
+ * </p>
+ * <p>
+ * {@link #initialize(boolean, boolean)} must be first called on the container to initialize
+ * shadows and/or color overlay. Then call {@link #wrap(View)} to insert wrapped view
+ * into container.
+ * </p>
+ * <p>
+ * Call {@link #setShadowFocusLevel(float)} to control shadow alpha.
+ * </p>
+ * <p>
+ * Call {@link #setOverlayColor(int)} to control overlay color.
+ * </p>
+ */
+public class ShadowOverlayContainer extends ViewGroup {
+
+ private boolean mInitialized;
+ private View mColorDimOverlay;
+ private Object mShadowImpl;
+ private View mWrappedView;
+
+ public ShadowOverlayContainer(Context context) {
+ this(context, null, 0);
+ }
+
+ public ShadowOverlayContainer(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ShadowOverlayContainer(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ /**
+ * Return true if the platform sdk supports shadow.
+ */
+ public static boolean supportsShadow() {
+ return ShadowHelper.getInstance().supportsShadow();
+ }
+
+ /**
+ * {@link #prepareParentForShadow(ViewGroup)} must be called on parent of container
+ * before using shadow. Depending on sdk version, optical bounds might be applied
+ * to parent.
+ */
+ public static void prepareParentForShadow(ViewGroup parent) {
+ ShadowHelper.getInstance().prepareParent(parent);
+ }
+
+ /**
+ * Initialize shadows and/or color overlay. Both are optional.
+ */
+ public void initialize(boolean hasShadow, boolean hasColorDimOverlay) {
+ if (mInitialized) {
+ throw new IllegalStateException();
+ }
+ mInitialized = true;
+ if (hasShadow) {
+ mShadowImpl = ShadowHelper.getInstance().addShadow(this);
+ }
+ if (hasColorDimOverlay) {
+ mColorDimOverlay = LayoutInflater.from(getContext())
+ .inflate(R.layout.lb_card_color_overlay, this, false);
+ addView(mColorDimOverlay);
+ }
+ }
+
+ /**
+ * Set shadow focus level (0 to 1). 0 for unfocused, 1f for fully focused.
+ */
+ public void setShadowFocusLevel(float level) {
+ if (mShadowImpl != null) {
+ if (level < 0f) {
+ level = 0f;
+ } else if (level > 1f) {
+ level = 1f;
+ }
+ ShadowHelper.getInstance().setShadowFocusLevel(mShadowImpl, level);
+ }
+ }
+
+ /**
+ * Set color (with alpha) of the overlay.
+ */
+ public void setOverlayColor(int overlayColor) {
+ if (mColorDimOverlay != null) {
+ mColorDimOverlay.setBackgroundColor(overlayColor);
+ }
+ }
+
+ /**
+ * Inserts view into the wrapper.
+ */
+ public void wrap(View view) {
+ if (!mInitialized || mWrappedView != null) {
+ throw new IllegalStateException();
+ }
+ if (mColorDimOverlay != null) {
+ addView(view, indexOfChild(mColorDimOverlay));
+ } else {
+ addView(view);
+ }
+ mWrappedView = view;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ if (mWrappedView == null) {
+ throw new IllegalStateException();
+ }
+ // padding and child margin are not supported.
+ // first measure the wrapped view, then measure the shadow view and/or overlay view.
+ int childWidthMeasureSpec, childHeightMeasureSpec;
+ LayoutParams lp = mWrappedView.getLayoutParams();
+ if (lp.width == LayoutParams.MATCH_PARENT) {
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec
+ (MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.EXACTLY);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
+ }
+ if (lp.height == LayoutParams.MATCH_PARENT) {
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec
+ (MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.EXACTLY);
+ } else {
+ childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
+ }
+ mWrappedView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ int measuredWidth = mWrappedView.getMeasuredWidth();
+ int measuredHeight = mWrappedView.getMeasuredHeight();
+
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ if (child == mWrappedView) {
+ continue;
+ }
+ lp = child.getLayoutParams();
+ if (lp.width == LayoutParams.MATCH_PARENT) {
+ childWidthMeasureSpec = MeasureSpec.makeMeasureSpec
+ (measuredWidth, MeasureSpec.EXACTLY);
+ } else {
+ childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, lp.width);
+ }
+
+ if (lp.height == LayoutParams.MATCH_PARENT) {
+ childHeightMeasureSpec = MeasureSpec.makeMeasureSpec
+ (measuredHeight, MeasureSpec.EXACTLY);
+ } else {
+ childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, 0, lp.height);
+ }
+ child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+ }
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View child = getChildAt(i);
+ if (child.getVisibility() != GONE) {
+ final int width = child.getMeasuredWidth();
+ final int height = child.getMeasuredHeight();
+ child.layout(0, 0, width, height);
+ }
+ }
+ }
+
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java b/v17/leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java
new file mode 100644
index 0000000..261b638
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SinglePresenterSelector.java
@@ -0,0 +1,35 @@
+/*
+ * 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;
+
+/**
+ * A {@link PresenterSelector} that always returns the same {@link Presenter}.
+ * Useful for rows of items of the same type that are all rendered the same way.
+ */
+public final class SinglePresenterSelector extends PresenterSelector {
+
+ private final Presenter mPresenter;
+
+ /**
+ * @param presenter The Presenter to return for every item.
+ */
+ public SinglePresenterSelector(Presenter presenter) {
+ mPresenter = presenter;
+ }
+
+ @Override
+ public Presenter getPresenter(Object item) {
+ return mPresenter;
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java b/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
new file mode 100644
index 0000000..b4de4a6
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGrid.java
@@ -0,0 +1,309 @@
+/*
+ * 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.v4.util.CircularArray;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A dynamic data structure that maintains staggered grid position information
+ * for each individual child. The algorithm ensures that each row will be kept
+ * as balanced as possible when prepending and appending a child.
+ *
+ * <p>
+ * You may keep view {@link StaggeredGrid.Location} inside StaggeredGrid as much
+ * as possible since prepending and appending views is not symmetric: layout
+ * going from 0 to N will likely produce a different result than layout going
+ * from N to 0 for the staggered cases. If a user scrolls from 0 to N then
+ * scrolls back to 0 and we don't keep history location information, edges of
+ * the very beginning of rows will not be aligned. It is recommended to keep a
+ * list of tens of thousands of {@link StaggeredGrid.Location}s which will be
+ * big enough to remember a typical user's scroll history. There are situations
+ * where StaggeredGrid falls back to the simple case where we do not need save a
+ * huge list of locations inside StaggeredGrid:
+ * <ul>
+ * <li>Only one row (e.g., a single row listview)</li>
+ * <li> Each item has the same length (not staggered at all)</li>
+ * </ul>
+ *
+ * <p>
+ * This class is abstract and can be replaced with different implementations.
+ */
+abstract class StaggeredGrid {
+
+ /**
+ * TODO: document this
+ */
+ public static interface Provider {
+ /**
+ * Return how many items are in the adapter.
+ */
+ public abstract int getCount();
+
+ /**
+ * Create the object at a given row.
+ */
+ public abstract void createItem(int index, int row, boolean append);
+ }
+
+ /**
+ * Location of an item in the grid. For now it only saves row index but
+ * more information may be added in the future.
+ */
+ public final static class Location {
+ /**
+ * The index of the row for this Location.
+ */
+ public final int row;
+
+ /**
+ * Create a Location with the given row index.
+ */
+ public Location(int row) {
+ this.row = row;
+ }
+ }
+
+ /**
+ * TODO: document this
+ */
+ public final static class Row {
+ /**
+ * first view start location
+ */
+ public int low;
+ /**
+ * last view end location
+ */
+ public int high;
+ }
+
+ protected Provider mProvider;
+ protected int mNumRows = 1; // mRows.length
+ protected Row[] mRows;
+ protected CircularArray<Location> mLocations = new CircularArray<Location>(64);
+ private ArrayList<Integer>[] mTmpItemPositionsInRows;
+
+ /**
+ * A constant representing a default starting index, indicating that the
+ * developer did not provide a start index.
+ */
+ public static final int START_DEFAULT = -1;
+
+ // the first index that grid will layout
+ protected int mStartIndex = START_DEFAULT;
+ // the row to layout the first index
+ protected int mStartRow = START_DEFAULT;
+
+ protected int mFirstIndex = -1;
+
+ /**
+ * Sets the {@link Provider} for this staggered grid.
+ *
+ * @param provider The provider for this staggered grid.
+ */
+ public void setProvider(Provider provider) {
+ mProvider = provider;
+ }
+
+ /**
+ * Sets the array of {@link Row}s to fill into. For views that represent a
+ * horizontal list, this will be the rows of the view. For views that
+ * represent a vertical list, this will be the columns.
+ *
+ * @param row The array of {@link Row}s to be filled.
+ */
+ public final void setRows(Row[] row) {
+ if (row == null || row.length == 0) {
+ throw new IllegalArgumentException();
+ }
+ mNumRows = row.length;
+ mRows = row;
+ mTmpItemPositionsInRows = new ArrayList[mNumRows];
+ for (int i = 0; i < mNumRows; i++) {
+ mTmpItemPositionsInRows[i] = new ArrayList(32);
+ }
+ }
+
+ /**
+ * Returns the number of rows in the staggered grid.
+ */
+ public final int getNumRows() {
+ return mNumRows;
+ }
+
+ /**
+ * Set the first item index and the row index to load when there are no
+ * items.
+ *
+ * @param startIndex the index of the first item
+ * @param startRow the index of the row
+ */
+ public final void setStart(int startIndex, int startRow) {
+ mStartIndex = startIndex;
+ mStartRow = startRow;
+ }
+
+ /**
+ * Returns the first index in the staggered grid.
+ */
+ public final int getFirstIndex() {
+ return mFirstIndex;
+ }
+
+ /**
+ * Returns the last index in the staggered grid.
+ */
+ public final int getLastIndex() {
+ return mFirstIndex + mLocations.size() - 1;
+ }
+
+ /**
+ * Returns the size of the saved {@link Location}s.
+ */
+ public final int getSize() {
+ return mLocations.size();
+ }
+
+ /**
+ * Returns the {@link Location} at the given index.
+ */
+ public final Location getLocation(int index) {
+ if (mLocations.size() == 0) {
+ return null;
+ }
+ return mLocations.get(index - mFirstIndex);
+ }
+
+ /**
+ * Removes the first element.
+ */
+ public final void removeFirst() {
+ mFirstIndex++;
+ mLocations.popFirst();
+ }
+
+ /**
+ * Removes the last element.
+ */
+ public final void removeLast() {
+ mLocations.popLast();
+ }
+
+ public final void debugPrint(PrintWriter pw) {
+ for (int i = 0, size = mLocations.size(); i < size; i++) {
+ Location loc = mLocations.get(i);
+ pw.print("<" + (mFirstIndex + i) + "," + loc.row + ">");
+ pw.print(" ");
+ pw.println();
+ }
+ }
+
+ protected final int getMaxHighRowIndex() {
+ int maxHighRowIndex = 0;
+ for (int i = 1; i < mNumRows; i++) {
+ if (mRows[i].high > mRows[maxHighRowIndex].high) {
+ maxHighRowIndex = i;
+ }
+ }
+ return maxHighRowIndex;
+ }
+
+ protected final int getMinHighRowIndex() {
+ int minHighRowIndex = 0;
+ for (int i = 1; i < mNumRows; i++) {
+ if (mRows[i].high < mRows[minHighRowIndex].high) {
+ minHighRowIndex = i;
+ }
+ }
+ return minHighRowIndex;
+ }
+
+ 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);
+ return loc;
+ }
+
+ /**
+ * Append items until the high edge reaches upTo.
+ */
+ public abstract void appendItems(int upTo);
+
+ protected final int getMaxLowRowIndex() {
+ int maxLowRowIndex = 0;
+ for (int i = 1; i < mNumRows; i++) {
+ if (mRows[i].low > mRows[maxLowRowIndex].low) {
+ maxLowRowIndex = i;
+ }
+ }
+ return maxLowRowIndex;
+ }
+
+ protected final int getMinLowRowIndex() {
+ int minLowRowIndex = 0;
+ for (int i = 1; i < mNumRows; i++) {
+ if (mRows[i].low < mRows[minLowRowIndex].low) {
+ minLowRowIndex = i;
+ }
+ }
+ return minLowRowIndex;
+ }
+
+ protected final Location prependItemToRow(int itemIndex, int rowIndex) {
+ mProvider.createItem(itemIndex, rowIndex, false);
+ Location loc = new Location(rowIndex);
+ mFirstIndex = itemIndex;
+ mLocations.addFirst(loc);
+ return loc;
+ }
+
+ /**
+ * Return array of Lists for all rows, each List contains item positions
+ * on that row between startPos(included) and endPositions(included).
+ * Returned value is read only, do not change it.
+ */
+ public final List<Integer>[] getItemPositionsInRows(int startPos, int endPos) {
+ for (int i = 0; i < mNumRows; i++) {
+ mTmpItemPositionsInRows[i].clear();
+ }
+ if (startPos >= 0) {
+ for (int i = startPos; i <= endPos; i++) {
+ mTmpItemPositionsInRows[getLocation(i).row].add(i);
+ }
+ }
+ return mTmpItemPositionsInRows;
+ }
+
+ /**
+ * Prepend items until the low edge reaches downTo.
+ */
+ public abstract void prependItems(int downTo);
+
+ /**
+ * Strip items, keep a contiguous subset of items; the subset should include
+ * at least one item on every row that currently has at least one item.
+ *
+ * <p>
+ * TODO: document this better
+ */
+ public abstract void stripDownTo(int itemIndex);
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java b/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java
new file mode 100644
index 0000000..9f2a06c
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/StaggeredGridDefault.java
@@ -0,0 +1,152 @@
+/*
+ * 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;
+
+/**
+ * A default implementation of {@link StaggeredGrid}.
+ *
+ * This implementation tries to fill items in consecutive row order. The next
+ * item is always in same row or in the next row.
+ */
+final class StaggeredGridDefault extends StaggeredGrid {
+
+ @Override
+ public void appendItems(int upTo) {
+ int count = mProvider.getCount();
+ int itemIndex;
+ int rowIndex;
+ if (mLocations.size() > 0) {
+ itemIndex = getLastIndex() + 1;
+ rowIndex = (mLocations.getLast().row + 1) % mNumRows;
+ } else {
+ itemIndex = mStartIndex != START_DEFAULT ? mStartIndex : 0;
+ rowIndex = mStartRow != START_DEFAULT ? mStartRow : itemIndex % mNumRows;
+ }
+
+ top_loop:
+ while (true) {
+ // find highest row (.high is biggest)
+ int maxHighRowIndex = mLocations.size() > 0 ? getMaxHighRowIndex() : -1;
+ int maxHigh = maxHighRowIndex != -1 ? mRows[maxHighRowIndex].high : Integer.MIN_VALUE;
+ // fill from current row till last row so that each row will grow longer than
+ // the previous highest row.
+ for (; rowIndex < mNumRows; rowIndex++) {
+ // fill one item to a row
+ if (itemIndex == count) {
+ break top_loop;
+ }
+ appendItemToRow(itemIndex++, rowIndex);
+ // fill more item to the row to make sure this row is longer than
+ // the previous highest row.
+ if (maxHighRowIndex == -1) {
+ maxHighRowIndex = getMaxHighRowIndex();
+ maxHigh = mRows[maxHighRowIndex].high;
+ } else if (rowIndex != maxHighRowIndex) {
+ while (mRows[rowIndex].high < maxHigh) {
+ if (itemIndex == count) {
+ break top_loop;
+ }
+ appendItemToRow(itemIndex++, rowIndex);
+ }
+ }
+ }
+ if (mRows[getMinHighRowIndex()].high >= upTo) {
+ break;
+ }
+ // start fill from row 0 again
+ rowIndex = 0;
+ }
+ }
+
+ @Override
+ public void prependItems(int downTo) {
+ if (mProvider.getCount() <= 0) return;
+ int itemIndex;
+ int rowIndex;
+ if (mLocations.size() > 0) {
+ itemIndex = getFirstIndex() - 1;
+ rowIndex = mLocations.getFirst().row;
+ if (rowIndex == 0) {
+ rowIndex = mNumRows - 1;
+ } else {
+ rowIndex--;
+ }
+ } else {
+ itemIndex = mStartIndex != START_DEFAULT ? mStartIndex : 0;
+ rowIndex = mStartRow != START_DEFAULT ? mStartRow : itemIndex % mNumRows;
+ }
+
+ top_loop:
+ while (true) {
+ int minLowRowIndex = mLocations.size() > 0 ? getMinLowRowIndex() : -1;
+ int minLow = minLowRowIndex != -1 ? mRows[minLowRowIndex].low : Integer.MAX_VALUE;
+ for (; rowIndex >=0 ; rowIndex--) {
+ if (itemIndex < 0) {
+ break top_loop;
+ }
+ prependItemToRow(itemIndex--, rowIndex);
+ if (minLowRowIndex == -1) {
+ minLowRowIndex = getMinLowRowIndex();
+ minLow = mRows[minLowRowIndex].low;
+ } else if (rowIndex != minLowRowIndex) {
+ while (mRows[rowIndex].low > minLow) {
+ if (itemIndex < 0) {
+ break top_loop;
+ }
+ prependItemToRow(itemIndex--, rowIndex);
+ }
+ }
+ }
+ if (mRows[getMaxLowRowIndex()].low <= downTo) {
+ break;
+ }
+ rowIndex = mNumRows - 1;
+ }
+ }
+
+ @Override
+ public final void stripDownTo(int itemIndex) {
+ // because we layout the items in the order that next item is either same row
+ // or next row, so we can easily find the row range by searching items forward and
+ // backward until we see the row is 0 or mNumRow - 1
+ Location loc = getLocation(itemIndex);
+ if (loc == null) {
+ return;
+ }
+ int firstIndex = getFirstIndex();
+ int lastIndex = getLastIndex();
+ int row = loc.row;
+
+ int endIndex = itemIndex;
+ int endRow = row;
+ while (endRow < mNumRows - 1 && endIndex < lastIndex) {
+ endIndex++;
+ endRow = getLocation(endIndex).row;
+ }
+
+ int startIndex = itemIndex;
+ int startRow = row;
+ while (startRow > 0 && startIndex > firstIndex) {
+ startIndex--;
+ startRow = getLocation(startIndex).row;
+ }
+ // trim information
+ for (int i = firstIndex; i < startIndex; i++) {
+ removeFirst();
+ }
+ for (int i = endIndex; i < lastIndex; i++) {
+ removeLast();
+ }
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java b/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
new file mode 100644
index 0000000..586ebf9
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridPresenter.java
@@ -0,0 +1,244 @@
+/*
+ * 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.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.util.Log;
+
+/**
+ * A presenter that renders objects in a vertical grid.
+ *
+ */
+public class VerticalGridPresenter extends Presenter {
+ private static final String TAG = "GridPresenter";
+ private static final boolean DEBUG = false;
+
+ public static class ViewHolder extends Presenter.ViewHolder {
+ final ItemBridgeAdapter mItemBridgeAdapter = new ItemBridgeAdapter();
+ final VerticalGridView mGridView;
+ boolean mInitialized;
+
+ public ViewHolder(VerticalGridView view) {
+ super(view);
+ mGridView = view;
+ }
+
+ public VerticalGridView getGridView() {
+ return mGridView;
+ }
+ }
+
+ private int mNumColumns = -1;
+ private int mZoomFactor;
+ private boolean mShadowEnabled = true;
+ private OnItemSelectedListener mOnItemSelectedListener;
+ private OnItemClickedListener mOnItemClickedListener;
+
+ public VerticalGridPresenter() {
+ this(FocusHighlight.ZOOM_FACTOR_MEDIUM);
+ }
+
+ public VerticalGridPresenter(int zoomFactor) {
+ mZoomFactor = zoomFactor;
+ }
+
+ /**
+ * Sets the number of columns in the vertical grid.
+ */
+ public void setNumberOfColumns(int numColumns) {
+ if (numColumns < 0) {
+ throw new IllegalArgumentException("Invalid number of columns");
+ }
+ if (mNumColumns != numColumns) {
+ mNumColumns = numColumns;
+ }
+ }
+
+ /**
+ * Returns the number of columns in the vertical grid.
+ */
+ public int getNumberOfColumns() {
+ return mNumColumns;
+ }
+
+ /**
+ * Enable or disable child shadow.
+ * This is not only for enable/disable default shadow implementation but also subclass must
+ * respect this flag.
+ */
+ public final void setShadowEnabled(boolean enabled) {
+ mShadowEnabled = enabled;
+ }
+
+ /**
+ * Returns true if child shadow is enabled.
+ * This is not only for enable/disable default shadow implementation but also subclass must
+ * respect this flag.
+ */
+ public final boolean getShadowEnabled() {
+ return mShadowEnabled;
+ }
+
+ /**
+ * Returns true if opticalBounds is supported (SDK >= 18) so that default shadow
+ * is applied to each individual child of {@link VerticalGridView}.
+ * Subclass may return false to disable.
+ */
+ public boolean isUsingDefaultShadow() {
+ return ShadowOverlayContainer.supportsShadow();
+ }
+
+ final boolean needsDefaultShadow() {
+ return isUsingDefaultShadow() && getShadowEnabled();
+ }
+
+ @Override
+ public final ViewHolder onCreateViewHolder(ViewGroup parent) {
+ ViewHolder vh = createGridViewHolder(parent);
+ vh.mInitialized = false;
+ initializeGridViewHolder(vh);
+ if (!vh.mInitialized) {
+ throw new RuntimeException("super.initializeGridViewHolder() must be called");
+ }
+ return vh;
+ }
+
+ /**
+ * Subclass may override this to inflate a different layout.
+ */
+ protected ViewHolder createGridViewHolder(ViewGroup parent) {
+ View root = LayoutInflater.from(parent.getContext()).inflate(
+ R.layout.lb_vertical_grid, parent, false);
+ return new ViewHolder((VerticalGridView) root.findViewById(R.id.browse_grid));
+ }
+
+ private ItemBridgeAdapter.Wrapper mWrapper = new ItemBridgeAdapter.Wrapper() {
+ @Override
+ public View createWrapper(View root) {
+ ShadowOverlayContainer wrapper = new ShadowOverlayContainer(root.getContext());
+ wrapper.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ wrapper.initialize(needsDefaultShadow(), false);
+ return wrapper;
+ }
+ @Override
+ public void wrap(View wrapper, View wrapped) {
+ ((ShadowOverlayContainer) wrapper).wrap(wrapped);
+ }
+ };
+
+ protected void initializeGridViewHolder(ViewHolder vh) {
+ if (mNumColumns == -1) {
+ throw new IllegalStateException("Number of columns must be set");
+ }
+ if (DEBUG) Log.v(TAG, "mNumColumns " + mNumColumns);
+ vh.getGridView().setNumColumns(mNumColumns);
+ vh.mInitialized = true;
+
+ if (needsDefaultShadow()) {
+ vh.mItemBridgeAdapter.setWrapper(mWrapper);
+ ShadowOverlayContainer.prepareParentForShadow(vh.getGridView());
+ ((ViewGroup) vh.view).setClipChildren(false);
+ }
+ FocusHighlightHelper.setupBrowseItemFocusHighlight(vh.mItemBridgeAdapter, mZoomFactor);
+
+ final ViewHolder gridViewHolder = vh;
+ vh.getGridView().setOnChildSelectedListener(new OnChildSelectedListener() {
+ @Override
+ public void onChildSelected(ViewGroup parent, View view, int position, long id) {
+ selectChildView(gridViewHolder, view);
+ }
+ });
+
+ vh.mItemBridgeAdapter.setAdapterListener(new ItemBridgeAdapter.AdapterListener() {
+ @Override
+ public void onCreate(final ItemBridgeAdapter.ViewHolder itemViewHolder) {
+ // Only when having an OnItemClickListner, we attach the OnClickListener.
+ if (getOnItemClickedListener() != 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);
+ }
+ }
+ });
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
+ if (DEBUG) Log.v(TAG, "onBindViewHolder " + item);
+ ViewHolder vh = (ViewHolder) viewHolder;
+ vh.mItemBridgeAdapter.setAdapter((ObjectAdapter) item);
+ vh.getGridView().setAdapter(vh.mItemBridgeAdapter);
+ }
+
+ @Override
+ public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) {
+ if (DEBUG) Log.v(TAG, "onUnbindViewHolder");
+ ViewHolder vh = (ViewHolder) viewHolder;
+ vh.mItemBridgeAdapter.setAdapter(null);
+ vh.getGridView().setAdapter(null);
+ }
+
+ /**
+ * Sets the item selected listener.
+ * Since this is a grid the row parameter is always null.
+ */
+ public final void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ }
+
+ /**
+ * Returns the item selected listener.
+ */
+ public final OnItemSelectedListener getOnItemSelectedListener() {
+ return mOnItemSelectedListener;
+ }
+
+ /**
+ * 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.
+ */
+ public final void setOnItemClickedListener(OnItemClickedListener listener) {
+ mOnItemClickedListener = listener;
+ }
+
+ /**
+ * Returns the item clicked listener.
+ */
+ public final OnItemClickedListener getOnItemClickedListener() {
+ return mOnItemClickedListener;
+ }
+
+ 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);
+ }
+ };
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java b/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.java
new file mode 100644
index 0000000..0b3f453
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/VerticalGridView.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.v17.leanback.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v17.leanback.R;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+
+/**
+ * A view that shows items in a vertically scrolling list. The items come from
+ * the {@link RecyclerView.Adapter} associated with this view.
+ */
+public class VerticalGridView extends BaseGridView {
+
+ public VerticalGridView(Context context) {
+ this(context, null);
+ }
+
+ public VerticalGridView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public VerticalGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mLayoutManager.setOrientation(RecyclerView.VERTICAL);
+ initAttributes(context, attrs);
+ }
+
+ 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));
+ setNumColumns(a.getInt(R.styleable.lbVerticalGridView_numberOfColumns, 1));
+ a.recycle();
+ }
+
+ /**
+ * Set the number of columns.
+ */
+ public void setNumColumns(int numColumns) {
+ mLayoutManager.setNumRows(numColumns);
+ requestLayout();
+ }
+
+ /**
+ * Set the column width.
+ */
+ public void setColumnWidth(int width) {
+ mLayoutManager.setRowHeight(width);
+ requestLayout();
+ }
+}
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java b/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
new file mode 100644
index 0000000..7d79fc5
--- /dev/null
+++ b/v17/leanback/src/android/support/v17/leanback/widget/WindowAlignment.java
@@ -0,0 +1,261 @@
+/*
+ * 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 static android.support.v17.leanback.widget.BaseGridView.WINDOW_ALIGN_LOW_EDGE;
+import static android.support.v17.leanback.widget.BaseGridView.WINDOW_ALIGN_HIGH_EDGE;
+import static android.support.v17.leanback.widget.BaseGridView.WINDOW_ALIGN_BOTH_EDGE;
+import static android.support.v17.leanback.widget.BaseGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED;
+
+import static android.support.v7.widget.RecyclerView.HORIZONTAL;
+
+/**
+ * Maintains Window Alignment information of two axis.
+ */
+class WindowAlignment {
+
+ /**
+ * Maintains alignment information in one direction.
+ */
+ public static class Axis {
+ /**
+ * mScrollCenter is used to calculate dynamic transformation based on how far a view
+ * is from the mScrollCenter. For example, the views with center close to mScrollCenter
+ * will be scaled up.
+ */
+ private float mScrollCenter;
+ /**
+ * Right or bottom edge of last child.
+ */
+ private int mMaxEdge;
+ /**
+ * Left or top edge of first child, typically should be zero.
+ */
+ private int mMinEdge;
+
+ private int mWindowAlignment = WINDOW_ALIGN_BOTH_EDGE;
+
+ private int mWindowAlignmentOffset = 0;
+
+ private float mWindowAlignmentOffsetPercent = 50f;
+
+ private int mSize;
+
+ private int mPaddingLow;
+
+ private int mPaddingHigh;
+
+ private String mName; // for debugging
+
+ public Axis(String name) {
+ reset();
+ mName = name;
+ }
+
+ final public int getWindowAlignment() {
+ return mWindowAlignment;
+ }
+
+ final public void setWindowAlignment(int windowAlignment) {
+ mWindowAlignment = windowAlignment;
+ }
+
+ final public int getWindowAlignmentOffset() {
+ return mWindowAlignmentOffset;
+ }
+
+ final public void setWindowAlignmentOffset(int offset) {
+ mWindowAlignmentOffset = offset;
+ }
+
+ final public void setWindowAlignmentOffsetPercent(float percent) {
+ if ((percent < 0 || percent > 100)
+ && percent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) {
+ throw new IllegalArgumentException();
+ }
+ mWindowAlignmentOffsetPercent = percent;
+ }
+
+ final public float getWindowAlignmentOffsetPercent() {
+ return mWindowAlignmentOffsetPercent;
+ }
+
+ final public int getScrollCenter() {
+ return (int) mScrollCenter;
+ }
+
+ /** set minEdge, Integer.MIN_VALUE means unknown*/
+ final public void setMinEdge(int minEdge) {
+ mMinEdge = minEdge;
+ }
+
+ public void invalidateScrollMin() {
+ mMinEdge = Integer.MIN_VALUE;
+ }
+
+ /** update max edge, Integer.MAX_VALUE means unknown*/
+ final public void setMaxEdge(int maxEdge) {
+ mMaxEdge = maxEdge;
+ }
+
+ public void invalidateScrollMax() {
+ mMaxEdge = Integer.MAX_VALUE;
+ }
+
+ final public float updateScrollCenter(float scrollTarget) {
+ mScrollCenter = scrollTarget;
+ return scrollTarget;
+ }
+
+ private void reset() {
+ mScrollCenter = Integer.MIN_VALUE;
+ mMinEdge = Integer.MIN_VALUE;
+ mMaxEdge = Integer.MAX_VALUE;
+ }
+
+ final public boolean isMinUnknown() {
+ return mMinEdge == Integer.MIN_VALUE;
+ }
+
+ final public boolean isMaxUnknown() {
+ return mMaxEdge == Integer.MAX_VALUE;
+ }
+
+ final public void setSize(int size) {
+ mSize = size;
+ }
+
+ final public int getSize() {
+ return mSize;
+ }
+
+ final public void setPadding(int paddingLow, int paddingHigh) {
+ mPaddingLow = paddingLow;
+ mPaddingHigh = paddingHigh;
+ }
+
+ final public int getPaddingLow() {
+ return mPaddingLow;
+ }
+
+ final public int getPaddingHigh() {
+ return mPaddingHigh;
+ }
+
+ final public int getClientSize() {
+ return mSize - mPaddingLow - mPaddingHigh;
+ }
+
+ final public int getSystemScrollPos() {
+ return getSystemScrollPos((int) mScrollCenter);
+ }
+
+ final public int getSystemScrollPos(int scrollCenter) {
+ int middlePosition;
+ if (mWindowAlignmentOffset >= 0) {
+ middlePosition = mWindowAlignmentOffset - mPaddingLow;
+ } else {
+ middlePosition = mSize + mWindowAlignmentOffset - mPaddingLow;
+ }
+ if (mWindowAlignmentOffsetPercent != WINDOW_ALIGN_OFFSET_PERCENT_DISABLED) {
+ middlePosition += (int) (mSize * mWindowAlignmentOffsetPercent / 100);
+ }
+ int clientSize = getClientSize();
+ int afterMiddlePosition = clientSize - middlePosition;
+ boolean isMinUnknown = isMinUnknown();
+ boolean isMaxUnknown = isMaxUnknown();
+ if (!isMinUnknown && !isMaxUnknown &&
+ (mWindowAlignment & WINDOW_ALIGN_BOTH_EDGE) == WINDOW_ALIGN_BOTH_EDGE) {
+ if (mMaxEdge - mMinEdge <= clientSize) {
+ // total children size is less than view port and we want to align
+ // both edge: align first child to left edge of view port
+ return mMinEdge - mPaddingLow;
+ }
+ }
+ if (!isMinUnknown) {
+ if ((mWindowAlignment & WINDOW_ALIGN_LOW_EDGE) != 0 &&
+ 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;
+ }
+ }
+ if (!isMaxUnknown) {
+ if ((mWindowAlignment & WINDOW_ALIGN_HIGH_EDGE) != 0 &&
+ 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);
+ }
+ }
+ // else put scroll center in middle of view port
+ return scrollCenter - middlePosition - mPaddingLow;
+ }
+
+ @Override
+ public String toString() {
+ return "center: " + mScrollCenter + " min:" + mMinEdge +
+ " max:" + mMaxEdge;
+ }
+
+ }
+
+ private int mOrientation = HORIZONTAL;
+
+ final public Axis vertical = new Axis("vertical");
+
+ final public Axis horizontal = new Axis("horizontal");
+
+ private Axis mMainAxis = horizontal;
+
+ private Axis mSecondAxis = vertical;
+
+ final public Axis mainAxis() {
+ return mMainAxis;
+ }
+
+ final public Axis secondAxis() {
+ return mSecondAxis;
+ }
+
+ final public void setOrientation(int orientation) {
+ mOrientation = orientation;
+ if (mOrientation == HORIZONTAL) {
+ mMainAxis = horizontal;
+ mSecondAxis = vertical;
+ } else {
+ mMainAxis = vertical;
+ mSecondAxis = horizontal;
+ }
+ }
+
+ final public int getOrientation() {
+ return mOrientation;
+ }
+
+ final public void reset() {
+ mainAxis().reset();
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuffer().append("horizontal=")
+ .append(horizontal.toString())
+ .append("vertical=")
+ .append(vertical.toString())
+ .toString();
+ }
+
+}
diff --git a/v4/Android.mk b/v4/Android.mk
index 0cdb05d..526a1c6 100644
--- a/v4/Android.mk
+++ b/v4/Android.mk
@@ -133,10 +133,22 @@
# -----------------------------------------------------------------------
+# 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
+LOCAL_SDK_VERSION := current
+LOCAL_SRC_FILES := $(call all-java-files-under, api20)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4-kitkat
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# -----------------------------------------------------------------------
+
# Here is the final static library that apps can link against.
include $(CLEAR_VARS)
LOCAL_MODULE := android-support-v4
LOCAL_SDK_VERSION := 4
LOCAL_SRC_FILES := $(call all-java-files-under, java)
-LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4-kitkat
+LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4-api20
+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
new file mode 100644
index 0000000..e737695
--- /dev/null
+++ b/v4/api20/android/support/v4/app/NotificationCompatApi20.java
@@ -0,0 +1,85 @@
+/*
+ * 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.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.widget.RemoteViews;
+
+class NotificationCompatApi20 {
+ public static class Builder implements NotificationBuilderWithBuilderAccessor,
+ NotificationBuilderWithActions {
+ private Notification.Builder b;
+
+ public Builder(Context context, Notification n,
+ CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
+ RemoteViews tickerView, int number,
+ PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
+ int mProgressMax, int mProgress, boolean mProgressIndeterminate,
+ boolean useChronometer, int priority, CharSequence subText, boolean localOnly,
+ Bundle extras) {
+ b = new Notification.Builder(context)
+ .setWhen(n.when)
+ .setSmallIcon(n.icon, n.iconLevel)
+ .setContent(n.contentView)
+ .setTicker(n.tickerText, tickerView)
+ .setSound(n.sound, n.audioStreamType)
+ .setVibrate(n.vibrate)
+ .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS)
+ .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0)
+ .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0)
+ .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0)
+ .setDefaults(n.defaults)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setSubText(subText)
+ .setContentInfo(contentInfo)
+ .setContentIntent(contentIntent)
+ .setDeleteIntent(n.deleteIntent)
+ .setFullScreenIntent(fullScreenIntent,
+ (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0)
+ .setLargeIcon(largeIcon)
+ .setNumber(number)
+ .setUsesChronometer(useChronometer)
+ .setPriority(priority)
+ .setProgress(mProgressMax, mProgress, mProgressIndeterminate)
+ .setLocalOnly(localOnly)
+ .setExtras(extras);
+ }
+
+ @Override
+ public void addAction(int icon, CharSequence title, PendingIntent intent) {
+ b.addAction(icon, title, intent);
+ }
+
+ @Override
+ public Notification.Builder getBuilder() {
+ return b;
+ }
+
+ public Notification build() {
+ return b.build();
+ }
+ }
+
+ public static boolean getLocalOnly(Notification notif) {
+ return (notif.flags & Notification.FLAG_LOCAL_ONLY) != 0;
+ }
+}
diff --git a/v4/build.gradle b/v4/build.gradle
index e432d56..d39c5db 100644
--- a/v4/build.gradle
+++ b/v4/build.gradle
@@ -18,9 +18,10 @@
def jbMr1SS = createApiSourceset('jellybeanmr1', 'jellybean-mr1', '17', jbSS)
def jbMr2SS = createApiSourceset('jellybeanmr2', 'jellybean-mr2', '18', jbMr1SS)
def kitkatSS = createApiSourceset('kitkat', 'kitkat', '19', jbMr2SS)
+def api20SS = createApiSourceset('api20', 'api20', 'current', kitkatSS)
// setup the main code to depend on the above through the highest platform-specific sourceset.
-setupDependencies('compile', kitkatSS)
+setupDependencies('compile', api20SS)
// --------------------------
def createApiSourceset(String name, String folder, String apiLevel, SourceSet previousSource) {
@@ -52,10 +53,10 @@
}
dependencies {
+ compile project(':support-annotations')
compile getAndroidPrebuilt('4')
}
-
uploadArchives {
repositories {
mavenDeployer {
diff --git a/v4/honeycomb/android/support/v4/app/NotificationBuilderWithBuilderAccessor.java b/v4/honeycomb/android/support/v4/app/NotificationBuilderWithBuilderAccessor.java
new file mode 100644
index 0000000..4de2e21
--- /dev/null
+++ b/v4/honeycomb/android/support/v4/app/NotificationBuilderWithBuilderAccessor.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.app;
+
+import android.app.Notification;
+
+/**
+ * Interface implemented by notification compat builders that support
+ * an accessor for {@link Notification.Builder}. {@link Notification.Builder}
+ * was introduced in HoneyComb.
+ */
+interface NotificationBuilderWithBuilderAccessor {
+ public Notification.Builder getBuilder();
+}
diff --git a/v4/honeycomb/android/support/v4/view/ViewCompatHC.java b/v4/honeycomb/android/support/v4/view/ViewCompatHC.java
index a237831..0a1bb57 100644
--- a/v4/honeycomb/android/support/v4/view/ViewCompatHC.java
+++ b/v4/honeycomb/android/support/v4/view/ViewCompatHC.java
@@ -52,4 +52,12 @@
public static int getMeasuredState(View view) {
return view.getMeasuredState();
}
+
+ public static float getTranslationX(View view) {
+ return view.getTranslationX();
+ }
+
+ public static float getTranslationY(View view) {
+ return view.getTranslationY();
+ }
}
diff --git a/v4/java/android/support/v4/app/ActionBarDrawerToggle.java b/v4/java/android/support/v4/app/ActionBarDrawerToggle.java
index 5c7e733..7589fa9 100644
--- a/v4/java/android/support/v4/app/ActionBarDrawerToggle.java
+++ b/v4/java/android/support/v4/app/ActionBarDrawerToggle.java
@@ -24,6 +24,9 @@
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Build;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.DrawerLayout;
@@ -64,6 +67,7 @@
* @return Delegate to use for ActionBarDrawableToggles, or null if the Activity
* does not wish to override the default behavior.
*/
+ @Nullable
Delegate getDrawerToggleDelegate();
}
@@ -72,6 +76,7 @@
* @return Up indicator drawable as defined in the Activity's theme, or null if one is not
* defined.
*/
+ @Nullable
Drawable getThemeUpIndicator();
/**
@@ -80,14 +85,14 @@
* @param upDrawable - Drawable to set as up indicator
* @param contentDescRes - Content description to set
*/
- void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes);
+ void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes);
/**
* Set the Action Bar's up indicator content description.
*
* @param contentDescRes - Content description to set
*/
- void setActionBarDescription(int contentDescRes);
+ void setActionBarDescription(@StringRes int contentDescRes);
}
private interface ActionBarDrawerToggleImpl {
@@ -211,7 +216,8 @@
* for accessibility
*/
public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout,
- int drawerImageRes, int openDrawerContentDescRes, int closeDrawerContentDescRes) {
+ @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes,
+ @StringRes int closeDrawerContentDescRes) {
mActivity = activity;
// Allow the Activity to provide an impl
diff --git a/v4/java/android/support/v4/app/ActivityCompat.java b/v4/java/android/support/v4/app/ActivityCompat.java
index a30eff2..f456a1b 100644
--- a/v4/java/android/support/v4/app/ActivityCompat.java
+++ b/v4/java/android/support/v4/app/ActivityCompat.java
@@ -20,6 +20,7 @@
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
/**
@@ -84,7 +85,7 @@
* supplied here; there are no supported definitions for
* building it manually.
*/
- public static void startActivity(Activity activity, Intent intent, Bundle options) {
+ public static void startActivity(Activity activity, Intent intent, @Nullable Bundle options) {
if (Build.VERSION.SDK_INT >= 16) {
ActivityCompatJB.startActivity(activity, intent, options);
} else {
@@ -112,7 +113,8 @@
* supplied here; there are no supported definitions for
* building it manually.
*/
- public static void startActivityForResult(Activity activity, Intent intent, int requestCode, Bundle options) {
+ public static void startActivityForResult(Activity activity, Intent intent, int requestCode,
+ @Nullable Bundle options) {
if (Build.VERSION.SDK_INT >= 16) {
ActivityCompatJB.startActivityForResult(activity, intent, requestCode, options);
} else {
diff --git a/v4/java/android/support/v4/app/DialogFragment.java b/v4/java/android/support/v4/app/DialogFragment.java
index 8c67bf5..3c7773a 100644
--- a/v4/java/android/support/v4/app/DialogFragment.java
+++ b/v4/java/android/support/v4/app/DialogFragment.java
@@ -21,12 +21,18 @@
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.StyleRes;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Static library support version of the framework's {@link android.app.DialogFragment}.
* Used to write apps that run on platforms prior to Android 3.0. When running
@@ -37,6 +43,11 @@
public class DialogFragment extends Fragment
implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener {
+ /** @hide */
+ @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface DialogStyle {}
+
/**
* Style for {@link #setStyle(int, int)}: a basic,
* normal dialog.
@@ -98,7 +109,7 @@
* @param theme Optional custom theme. If 0, an appropriate theme (based
* on the style) will be selected for you.
*/
- public void setStyle(int style, int theme) {
+ public void setStyle(@DialogStyle int style, @StyleRes int theme) {
mStyle = style;
if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) {
mTheme = android.R.style.Theme_Panel;
@@ -195,6 +206,7 @@
return mDialog;
}
+ @StyleRes
public int getTheme() {
return mTheme;
}
@@ -333,6 +345,7 @@
*
* @return Return a new Dialog instance to be displayed by the Fragment.
*/
+ @NonNull
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new Dialog(getActivity(), getTheme());
}
diff --git a/v4/java/android/support/v4/app/Fragment.java b/v4/java/android/support/v4/app/Fragment.java
index 621bbbb..ab23595 100644
--- a/v4/java/android/support/v4/app/Fragment.java
+++ b/v4/java/android/support/v4/app/Fragment.java
@@ -25,6 +25,8 @@
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
import android.support.v4.util.SimpleArrayMap;
import android.support.v4.util.DebugUtils;
import android.util.AttributeSet;
@@ -609,7 +611,7 @@
*
* @param resId Resource id for the CharSequence text
*/
- public final CharSequence getText(int resId) {
+ public final CharSequence getText(@StringRes int resId) {
return getResources().getText(resId);
}
@@ -619,7 +621,7 @@
*
* @param resId Resource id for the string
*/
- public final String getString(int resId) {
+ public final String getString(@StringRes int resId) {
return getResources().getString(resId);
}
@@ -632,7 +634,7 @@
* @param formatArgs The format arguments that will be used for substitution.
*/
- public final String getString(int resId, Object... formatArgs) {
+ public final String getString(@StringRes int resId, Object... formatArgs) {
return getResources().getString(resId, formatArgs);
}
@@ -1013,8 +1015,8 @@
*
* @return Return the View for the fragment's UI, or null.
*/
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
return null;
}
@@ -1028,7 +1030,7 @@
* @param savedInstanceState If non-null, this fragment is being re-constructed
* from a previous saved state as given here.
*/
- public void onViewCreated(View view, Bundle savedInstanceState) {
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
}
/**
@@ -1037,6 +1039,7 @@
*
* @return The fragment's root view, or null if it has no layout.
*/
+ @Nullable
public View getView() {
return mView;
}
@@ -1054,7 +1057,7 @@
* @param savedInstanceState If the fragment is being re-created from
* a previous saved state, this is the state.
*/
- public void onActivityCreated(Bundle savedInstanceState) {
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
mCalled = true;
}
@@ -1069,7 +1072,7 @@
* @param savedInstanceState If the fragment is being re-created from
* a previous saved state, this is the state.
*/
- public void onViewStateRestored(Bundle savedInstanceState) {
+ public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
mCalled = true;
}
@@ -1198,6 +1201,7 @@
mRestored = false;
mBackStackNesting = 0;
mFragmentManager = null;
+ mChildFragmentManager = null;
mActivity = null;
mFragmentId = 0;
mContainerId = 0;
diff --git a/v4/java/android/support/v4/app/FragmentActivity.java b/v4/java/android/support/v4/app/FragmentActivity.java
index 596653a..ad57bb8 100644
--- a/v4/java/android/support/v4/app/FragmentActivity.java
+++ b/v4/java/android/support/v4/app/FragmentActivity.java
@@ -26,6 +26,7 @@
import android.os.Handler;
import android.os.Message;
import android.os.Parcelable;
+import android.support.annotation.NonNull;
import android.support.v4.util.SimpleArrayMap;
import android.util.AttributeSet;
import android.util.Log;
@@ -238,7 +239,7 @@
* Add support for inflating the <fragment> tag.
*/
@Override
- public View onCreateView(String name, Context context, AttributeSet attrs) {
+ public View onCreateView(String name, @NonNull Context context, @NonNull AttributeSet attrs) {
if (!"fragment".equals(name)) {
return super.onCreateView(name, context, attrs);
}
diff --git a/v4/java/android/support/v4/app/FragmentManager.java b/v4/java/android/support/v4/app/FragmentManager.java
index f07d3dd..2def8ed 100644
--- a/v4/java/android/support/v4/app/FragmentManager.java
+++ b/v4/java/android/support/v4/app/FragmentManager.java
@@ -23,6 +23,8 @@
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.IdRes;
+import android.support.annotation.StringRes;
import android.support.v4.util.DebugUtils;
import android.support.v4.util.LogWriter;
import android.util.Log;
@@ -90,12 +92,14 @@
* Return the full bread crumb title resource identifier for the entry,
* or 0 if it does not have one.
*/
+ @StringRes
public int getBreadCrumbTitleRes();
/**
* Return the short bread crumb title resource identifier for the entry,
* or 0 if it does not have one.
*/
+ @StringRes
public int getBreadCrumbShortTitleRes();
/**
@@ -164,7 +168,7 @@
* on the back stack associated with this ID are searched.
* @return The fragment if found or null otherwise.
*/
- public abstract Fragment findFragmentById(int id);
+ public abstract Fragment findFragmentById(@IdRes int id);
/**
* Finds a fragment that was identified by the given tag either when inflated
@@ -392,7 +396,7 @@
* Callbacks from FragmentManagerImpl to its container.
*/
interface FragmentContainer {
- public View findViewById(int id);
+ public View findViewById(@IdRes int id);
}
/**
@@ -1079,7 +1083,9 @@
makeInactive(f);
} else {
f.mActivity = null;
+ f.mParentFragment = null;
f.mFragmentManager = null;
+ f.mChildFragmentManager = null;
}
}
}
diff --git a/v4/java/android/support/v4/app/FragmentTransaction.java b/v4/java/android/support/v4/app/FragmentTransaction.java
index 23fedf9..d984d36 100644
--- a/v4/java/android/support/v4/app/FragmentTransaction.java
+++ b/v4/java/android/support/v4/app/FragmentTransaction.java
@@ -16,6 +16,16 @@
package android.support.v4.app;
+import android.support.annotation.AnimRes;
+import android.support.annotation.IdRes;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.StyleRes;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
/**
* Static library support version of the framework's {@link android.app.FragmentTransaction}.
* Used to write apps that run on platforms prior to Android 3.0. When running
@@ -32,7 +42,7 @@
/**
* Calls {@link #add(int, Fragment, String)} with a null tag.
*/
- public abstract FragmentTransaction add(int containerViewId, Fragment fragment);
+ public abstract FragmentTransaction add(@IdRes int containerViewId, Fragment fragment);
/**
* Add a fragment to the activity state. This fragment may optionally
@@ -49,12 +59,13 @@
*
* @return Returns the same FragmentTransaction instance.
*/
- public abstract FragmentTransaction add(int containerViewId, Fragment fragment, String tag);
+ public abstract FragmentTransaction add(@IdRes int containerViewId, Fragment fragment,
+ @Nullable String tag);
/**
* Calls {@link #replace(int, Fragment, String)} with a null tag.
*/
- public abstract FragmentTransaction replace(int containerViewId, Fragment fragment);
+ public abstract FragmentTransaction replace(@IdRes int containerViewId, Fragment fragment);
/**
* Replace an existing fragment that was added to a container. This is
@@ -72,7 +83,8 @@
*
* @return Returns the same FragmentTransaction instance.
*/
- public abstract FragmentTransaction replace(int containerViewId, Fragment fragment, String tag);
+ public abstract FragmentTransaction replace(@IdRes int containerViewId, Fragment fragment,
+ @Nullable String tag);
/**
* Remove an existing fragment. If it was added to a container, its view
@@ -146,7 +158,12 @@
* Bit mask that is set for all exit transitions.
*/
public static final int TRANSIT_EXIT_MASK = 0x2000;
-
+
+ /** @hide */
+ @IntDef({TRANSIT_NONE, TRANSIT_FRAGMENT_OPEN, TRANSIT_FRAGMENT_CLOSE})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface Transit {}
+
/** Not set up for a transition. */
public static final int TRANSIT_UNSET = -1;
/** No animation for transition. */
@@ -164,7 +181,8 @@
* entering and exiting in this transaction. These animations will not be
* played when popping the back stack.
*/
- public abstract FragmentTransaction setCustomAnimations(int enter, int exit);
+ public abstract FragmentTransaction setCustomAnimations(@AnimRes int enter,
+ @AnimRes int exit);
/**
* Set specific animation resources to run for the fragments that are
@@ -172,21 +190,21 @@
* and <code>popExit</code> animations will be played for enter/exit
* operations specifically when popping the back stack.
*/
- public abstract FragmentTransaction setCustomAnimations(int enter, int exit,
- int popEnter, int popExit);
+ public abstract FragmentTransaction setCustomAnimations(@AnimRes int enter,
+ @AnimRes int exit, @AnimRes int popEnter, @AnimRes int popExit);
/**
* Select a standard transition animation for this transaction. May be
* one of {@link #TRANSIT_NONE}, {@link #TRANSIT_FRAGMENT_OPEN},
* or {@link #TRANSIT_FRAGMENT_CLOSE}
*/
- public abstract FragmentTransaction setTransition(int transit);
+ public abstract FragmentTransaction setTransition(@Transit int transit);
/**
* Set a custom style resource that will be used for resolving transit
* animations.
*/
- public abstract FragmentTransaction setTransitionStyle(int styleRes);
+ public abstract FragmentTransaction setTransitionStyle(@StyleRes int styleRes);
/**
* Add this transaction to the back stack. This means that the transaction
@@ -195,7 +213,7 @@
*
* @param name An optional name for this back stack state, or null.
*/
- public abstract FragmentTransaction addToBackStack(String name);
+ public abstract FragmentTransaction addToBackStack(@Nullable String name);
/**
* Returns true if this FragmentTransaction is allowed to be added to the back
@@ -219,7 +237,7 @@
*
* @param res A string resource containing the title.
*/
- public abstract FragmentTransaction setBreadCrumbTitle(int res);
+ public abstract FragmentTransaction setBreadCrumbTitle(@StringRes int res);
/**
* Like {@link #setBreadCrumbTitle(int)} but taking a raw string; this
@@ -234,7 +252,7 @@
*
* @param res A string resource containing the title.
*/
- public abstract FragmentTransaction setBreadCrumbShortTitle(int res);
+ public abstract FragmentTransaction setBreadCrumbShortTitle(@StringRes int res);
/**
* Like {@link #setBreadCrumbShortTitle(int)} but taking a raw string; this
diff --git a/v4/java/android/support/v4/app/NavUtils.java b/v4/java/android/support/v4/app/NavUtils.java
index ea034be..841bc56 100644
--- a/v4/java/android/support/v4/app/NavUtils.java
+++ b/v4/java/android/support/v4/app/NavUtils.java
@@ -23,6 +23,7 @@
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.support.annotation.Nullable;
import android.support.v4.content.IntentCompat;
import android.util.Log;
@@ -274,6 +275,7 @@
* @return The fully qualified class name of sourceActivity's parent activity or null if
* it was not specified
*/
+ @Nullable
public static String getParentActivityName(Activity sourceActivity) {
try {
return getParentActivityName(sourceActivity, sourceActivity.getComponentName());
@@ -292,6 +294,7 @@
* @return The fully qualified class name of sourceActivity's parent activity or null if
* it was not specified
*/
+ @Nullable
public static String getParentActivityName(Context context, ComponentName componentName)
throws NameNotFoundException {
PackageManager pm = context.getPackageManager();
diff --git a/v4/java/android/support/v4/app/NotificationCompat.java b/v4/java/android/support/v4/app/NotificationCompat.java
index f2bc034..5ea5db6 100644
--- a/v4/java/android/support/v4/app/NotificationCompat.java
+++ b/v4/java/android/support/v4/app/NotificationCompat.java
@@ -24,6 +24,7 @@
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
+import android.os.Bundle;
import android.widget.RemoteViews;
import java.util.ArrayList;
@@ -32,6 +33,90 @@
* introduced after API level 4 in a backwards compatible fashion.
*/
public class NotificationCompat {
+
+ /**
+ * Use all default values (where applicable).
+ */
+ public static final int DEFAULT_ALL = ~0;
+
+ /**
+ * Use the default notification sound. This will ignore any sound set using
+ * {@link Builder#setSound}
+ *
+ * @see Builder#setDefaults
+ */
+ public static final int DEFAULT_SOUND = 1;
+
+ /**
+ * Use the default notification vibrate. This will ignore any vibrate set using
+ * {@link Builder#setVibrate}. Using phone vibration requires the
+ * {@link android.Manifest.permission#VIBRATE VIBRATE} permission.
+ *
+ * @see Builder#setDefaults
+ */
+ public static final int DEFAULT_VIBRATE = 2;
+
+ /**
+ * Use the default notification lights. This will ignore the
+ * {@link #FLAG_SHOW_LIGHTS} bit, and values set with {@link Builder#setLights}.
+ *
+ * @see Builder#setDefaults
+ */
+ public static final int DEFAULT_LIGHTS = 4;
+
+ /**
+ * Use this constant as the value for audioStreamType to request that
+ * the default stream type for notifications be used. Currently the
+ * default stream type is {@link AudioManager#STREAM_NOTIFICATION}.
+ */
+ public static final int STREAM_DEFAULT = -1;
+
+ /**
+ * Bit set in the Notification flags field when LEDs should be turned on
+ * for this notification.
+ */
+ public static final int FLAG_SHOW_LIGHTS = 0x00000001;
+
+ /**
+ * Bit set in the Notification flags field if this notification is in
+ * reference to something that is ongoing, like a phone call. It should
+ * not be set if this notification is in reference to something that
+ * happened at a particular point in time, like a missed phone call.
+ */
+ public static final int FLAG_ONGOING_EVENT = 0x00000002;
+
+ /**
+ * Bit set in the Notification flags field if
+ * the audio will be repeated until the notification is
+ * cancelled or the notification window is opened.
+ */
+ public static final int FLAG_INSISTENT = 0x00000004;
+
+ /**
+ * Bit set in the Notification flags field if the notification's sound,
+ * vibrate and ticker should only be played if the notification is not already showing.
+ */
+ public static final int FLAG_ONLY_ALERT_ONCE = 0x00000008;
+
+ /**
+ * Bit set in the Notification flags field if the notification should be canceled when
+ * it is clicked by the user.
+ */
+ public static final int FLAG_AUTO_CANCEL = 0x00000010;
+
+ /**
+ * Bit set in the Notification flags field if the notification should not be canceled
+ * when the user clicks the Clear all button.
+ */
+ public static final int FLAG_NO_CLEAR = 0x00000020;
+
+ /**
+ * Bit set in the Notification flags field if this notification represents a currently
+ * running service. This will normally be set for you by
+ * {@link android.app.Service#startForeground}.
+ */
+ public static final int FLAG_FOREGROUND_SERVICE = 0x00000040;
+
/**
* Obsolete flag indicating high-priority notifications; use the priority field instead.
*
@@ -76,15 +161,119 @@
*/
public static final int PRIORITY_MAX = 2;
+ /**
+ * Notification extras key: this is the title of the notification,
+ * as supplied to {@link Builder#setContentTitle(CharSequence)}.
+ */
+ public static final String EXTRA_TITLE = "android.title";
+
+ /**
+ * Notification extras key: this is the title of the notification when shown in expanded form,
+ * e.g. as supplied to {@link BigTextStyle#setBigContentTitle(CharSequence)}.
+ */
+ public static final String EXTRA_TITLE_BIG = EXTRA_TITLE + ".big";
+
+ /**
+ * Notification extras key: this is the main text payload, as supplied to
+ * {@link Builder#setContentText(CharSequence)}.
+ */
+ public static final String EXTRA_TEXT = "android.text";
+
+ /**
+ * Notification extras key: this is a third line of text, as supplied to
+ * {@link Builder#setSubText(CharSequence)}.
+ */
+ public static final String EXTRA_SUB_TEXT = "android.subText";
+
+ /**
+ * Notification extras key: this is a small piece of additional text as supplied to
+ * {@link Builder#setContentInfo(CharSequence)}.
+ */
+ public static final String EXTRA_INFO_TEXT = "android.infoText";
+
+ /**
+ * Notification extras key: this is a line of summary information intended to be shown
+ * alongside expanded notifications, as supplied to (e.g.)
+ * {@link BigTextStyle#setSummaryText(CharSequence)}.
+ */
+ public static final String EXTRA_SUMMARY_TEXT = "android.summaryText";
+
+ /**
+ * Notification extras key: this is the resource ID of the notification's main small icon, as
+ * supplied to {@link Builder#setSmallIcon(int)}.
+ */
+ public static final String EXTRA_SMALL_ICON = "android.icon";
+
+ /**
+ * Notification extras key: this is a bitmap to be used instead of the small icon when showing the
+ * notification payload, as
+ * supplied to {@link Builder#setLargeIcon(android.graphics.Bitmap)}.
+ */
+ public static final String EXTRA_LARGE_ICON = "android.largeIcon";
+
+ /**
+ * Notification extras key: this is a bitmap to be used instead of the one from
+ * {@link Builder#setLargeIcon(android.graphics.Bitmap)} when the notification is
+ * shown in its expanded form, as supplied to
+ * {@link BigPictureStyle#bigLargeIcon(android.graphics.Bitmap)}.
+ */
+ public static final String EXTRA_LARGE_ICON_BIG = EXTRA_LARGE_ICON + ".big";
+
+ /**
+ * Notification extras key: this is the progress value supplied to
+ * {@link Builder#setProgress(int, int, boolean)}.
+ */
+ public static final String EXTRA_PROGRESS = "android.progress";
+
+ /**
+ * Notification extras key: this is the maximum value supplied to
+ * {@link Builder#setProgress(int, int, boolean)}.
+ */
+ public static final String EXTRA_PROGRESS_MAX = "android.progressMax";
+
+ /**
+ * Notification extras key: whether the progress bar is indeterminate, supplied to
+ * {@link Builder#setProgress(int, int, boolean)}.
+ */
+ public static final String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate";
+
+ /**
+ * Notification extras key: whether the when field set using {@link Builder#setWhen} should
+ * be shown as a count-up timer (specifically a {@link android.widget.Chronometer}) instead
+ * of a timestamp, as supplied to {@link Builder#setUsesChronometer(boolean)}.
+ */
+ public static final String EXTRA_SHOW_CHRONOMETER = "android.showChronometer";
+
+ /**
+ * Notification extras key: this is a bitmap to be shown in {@link BigPictureStyle} expanded
+ * notifications, supplied to {@link BigPictureStyle#bigPicture(android.graphics.Bitmap)}.
+ */
+ public static final String EXTRA_PICTURE = "android.picture";
+
+ /**
+ * Notification extras key: An array of CharSequences to show in {@link InboxStyle} expanded
+ * notifications, each of which was supplied to {@link InboxStyle#addLine(CharSequence)}.
+ */
+ public static final String EXTRA_TEXT_LINES = "android.textLines";
+
+ /**
+ * Notification extras key: An array of people that this notification relates to, specified
+ * by contacts provider contact URI.
+ */
+ public static final String EXTRA_PEOPLE = "android.people";
+
private static final NotificationCompatImpl IMPL;
interface NotificationCompatImpl {
public Notification build(Builder b);
+ public Bundle getExtras(Notification n);
+ public boolean getLocalOnly(Notification n);
}
static class NotificationCompatImplBase implements NotificationCompatImpl {
+ @Override
public Notification build(Builder b) {
- Notification result = (Notification) b.mNotification;
+ Notification result = b.mNotification;
result.setLatestEventInfo(b.mContext, b.mContentTitle,
b.mContentText, b.mContentIntent);
// translate high priority requests into legacy flag
@@ -93,11 +282,22 @@
}
return result;
}
+
+ @Override
+ public Bundle getExtras(Notification n) {
+ return null;
+ }
+
+ @Override
+ public boolean getLocalOnly(Notification n) {
+ return false;
+ }
}
static class NotificationCompatImplGingerbread extends NotificationCompatImplBase {
+ @Override
public Notification build(Builder b) {
- Notification result = (Notification) b.mNotification;
+ Notification result = b.mNotification;
result.setLatestEventInfo(b.mContext, b.mContentTitle,
b.mContentText, b.mContentIntent);
result = NotificationCompatGingerbread.add(result, b.mContext,
@@ -110,7 +310,8 @@
}
}
- static class NotificationCompatImplHoneycomb implements NotificationCompatImpl {
+ static class NotificationCompatImplHoneycomb extends NotificationCompatImplBase {
+ @Override
public Notification build(Builder b) {
return NotificationCompatHoneycomb.add(b.mContext, b.mNotification,
b.mContentTitle, b.mContentText, b.mContentInfo, b.mTickerView,
@@ -118,7 +319,8 @@
}
}
- static class NotificationCompatImplIceCreamSandwich implements NotificationCompatImpl {
+ static class NotificationCompatImplIceCreamSandwich extends NotificationCompatImplBase {
+ @Override
public Notification build(Builder b) {
return NotificationCompatIceCreamSandwich.add(b.mContext, b.mNotification,
b.mContentTitle, b.mContentText, b.mContentInfo, b.mTickerView,
@@ -127,45 +329,120 @@
}
}
- static class NotificationCompatImplJellybean implements NotificationCompatImpl {
+ static class NotificationCompatImplJellybean extends NotificationCompatImplBase {
+ @Override
public Notification build(Builder b) {
- NotificationCompatJellybean jbBuilder = new NotificationCompatJellybean(
+ NotificationCompatJellybean.Builder builder = new NotificationCompatJellybean.Builder(
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);
- for (Action action: b.mActions) {
- jbBuilder.addAction(action.icon, action.title, action.actionIntent);
+ b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mExtras);
+ addActionsToBuilder(builder, b.mActions);
+ addStyleToBuilderJellybean(builder, b.mStyle);
+ return builder.build();
+ }
+
+ @Override
+ public Bundle getExtras(Notification n) {
+ return NotificationCompatJellybean.getExtras(n);
+ }
+
+ @Override
+ public boolean getLocalOnly(Notification n) {
+ return NotificationCompatJellybean.getLocalOnly(n);
+ }
+ }
+
+ static class NotificationCompatImplKitKat extends NotificationCompatImplBase {
+ @Override
+ public Notification build(Builder b) {
+ NotificationCompatKitKat.Builder builder = new NotificationCompatKitKat.Builder(
+ 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);
+ addActionsToBuilder(builder, b.mActions);
+ addStyleToBuilderJellybean(builder, b.mStyle);
+ return builder.build();
+ }
+
+ @Override
+ public Bundle getExtras(Notification n) {
+ return NotificationCompatKitKat.getExtras(n);
+ }
+
+ @Override
+ public boolean getLocalOnly(Notification n) {
+ return NotificationCompatKitKat.getLocalOnly(n);
+ }
+ }
+
+ static class NotificationCompatImplApi20 extends NotificationCompatImplBase {
+ @Override
+ public Notification build(Builder b) {
+ NotificationCompatApi20.Builder builder = new NotificationCompatApi20.Builder(
+ 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);
+ addActionsToBuilder(builder, b.mActions);
+ addStyleToBuilderJellybean(builder, b.mStyle);
+ return builder.build();
+ }
+
+ @Override
+ public Bundle getExtras(Notification n) {
+ return NotificationCompatKitKat.getExtras(n);
+ }
+
+ @Override
+ public boolean getLocalOnly(Notification n) {
+ return NotificationCompatApi20.getLocalOnly(n);
+ }
+ }
+
+ private static void addActionsToBuilder(NotificationBuilderWithActions builder,
+ ArrayList<Action> actions) {
+ for (Action action : actions) {
+ builder.addAction(action.icon, action.title, action.actionIntent);
+ }
+ }
+
+ private static void addStyleToBuilderJellybean(NotificationBuilderWithBuilderAccessor builder,
+ Style style) {
+ if (style != null) {
+ if (style instanceof BigTextStyle) {
+ BigTextStyle bigTextStyle = (BigTextStyle) style;
+ NotificationCompatJellybean.addBigTextStyle(builder,
+ bigTextStyle.mBigContentTitle,
+ bigTextStyle.mSummaryTextSet,
+ bigTextStyle.mSummaryText,
+ bigTextStyle.mBigText);
+ } else if (style instanceof InboxStyle) {
+ InboxStyle inboxStyle = (InboxStyle) style;
+ NotificationCompatJellybean.addInboxStyle(builder,
+ inboxStyle.mBigContentTitle,
+ inboxStyle.mSummaryTextSet,
+ inboxStyle.mSummaryText,
+ inboxStyle.mTexts);
+ } else if (style instanceof BigPictureStyle) {
+ BigPictureStyle bigPictureStyle = (BigPictureStyle) style;
+ NotificationCompatJellybean.addBigPictureStyle(builder,
+ bigPictureStyle.mBigContentTitle,
+ bigPictureStyle.mSummaryTextSet,
+ bigPictureStyle.mSummaryText,
+ bigPictureStyle.mPicture,
+ bigPictureStyle.mBigLargeIcon,
+ bigPictureStyle.mBigLargeIconSet);
}
- if (b.mStyle != null) {
- if (b.mStyle instanceof BigTextStyle) {
- BigTextStyle style = (BigTextStyle) b.mStyle;
- jbBuilder.addBigTextStyle(style.mBigContentTitle,
- style.mSummaryTextSet,
- style.mSummaryText,
- style.mBigText);
- } else if (b.mStyle instanceof InboxStyle) {
- InboxStyle style = (InboxStyle) b.mStyle;
- jbBuilder.addInboxStyle(style.mBigContentTitle,
- style.mSummaryTextSet,
- style.mSummaryText,
- style.mTexts);
- } else if (b.mStyle instanceof BigPictureStyle) {
- BigPictureStyle style = (BigPictureStyle) b.mStyle;
- jbBuilder.addBigPictureStyle(style.mBigContentTitle,
- style.mSummaryTextSet,
- style.mSummaryText,
- style.mPicture,
- style.mBigLargeIcon,
- style.mBigLargeIconSet);
- }
- }
- return(jbBuilder.build());
}
}
static {
- if (Build.VERSION.SDK_INT >= 16) {
+ // TODO: Add NotificationCompatApi20 when SDK_INT is incremented.
+ if (Build.VERSION.SDK_INT >= 19) {
+ IMPL = new NotificationCompatImplKitKat();
+ } else if (Build.VERSION.SDK_INT >= 16) {
IMPL = new NotificationCompatImplJellybean();
} else if (Build.VERSION.SDK_INT >= 14) {
IMPL = new NotificationCompatImplIceCreamSandwich();
@@ -217,6 +494,8 @@
int mProgress;
boolean mProgressIndeterminate;
ArrayList<Action> mActions = new ArrayList<Action>();
+ boolean mLocalOnly = false;
+ Bundle mExtras;
Notification mNotification = new Notification();
@@ -444,7 +723,7 @@
/**
* Set the sound to play. It will play on the stream you supply.
*
- * @see #STREAM_DEFAULT
+ * @see Notification#STREAM_DEFAULT
* @see AudioManager for the <code>STREAM_</code> constants.
*/
public Builder setSound(Uri sound, int streamType) {
@@ -516,6 +795,17 @@
}
/**
+ * Set whether or not this notification is only relevant to the current device.
+ *
+ * <p>Some notifications can be bridged to other devices for remote display.
+ * This hint can be set to recommend this notification not be bridged.
+ */
+ public Builder setLocalOnly(boolean b) {
+ mLocalOnly = b;
+ 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
@@ -551,6 +841,12 @@
* interrupted for a higher-priority notification.
* The system sets a notification's priority based on various factors including the
* setPriority value. The effect may differ slightly on different platforms.
+ *
+ * @param pri Relative priority for this notification. Must be one of
+ * the priority constants defined by {@link NotificationCompat}.
+ * Acceptable values range from {@link
+ * NotificationCompat#PRIORITY_MIN} (-2) to {@link
+ * NotificationCompat#PRIORITY_MAX} (2).
*/
public Builder setPriority(int pri) {
mPriority = pri;
@@ -558,6 +854,56 @@
}
/**
+ * Merge additional metadata into this notification.
+ *
+ * <p>Values within the Bundle will replace existing extras values in this Builder.
+ *
+ * @see Notification#extras
+ */
+ public Builder addExtras(Bundle bag) {
+ if (mExtras == null) {
+ mExtras = new Bundle(bag);
+ } else {
+ mExtras.putAll(bag);
+ }
+ return this;
+ }
+
+ /**
+ * Set metadata for this notification.
+ *
+ * <p>A reference to the Bundle is held for the lifetime of this Builder, and the Bundle's
+ * current contents are copied into the Notification each time {@link #build()} is
+ * called.
+ *
+ * <p>Replaces any existing extras values with those from the provided Bundle.
+ * Use {@link #addExtras} to merge in metadata instead.
+ *
+ * @see Notification#extras
+ */
+ public Builder setExtras(Bundle bag) {
+ mExtras = bag;
+ return this;
+ }
+
+ /**
+ * Get the current metadata Bundle used by this notification Builder.
+ *
+ * <p>The returned Bundle is shared with this Builder.
+ *
+ * <p>The current contents of this Bundle are copied into the Notification each time
+ * {@link #build()} is called.
+ *
+ * @see Notification#extras
+ */
+ public Bundle getExtras() {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ return mExtras;
+ }
+
+ /**
* Add an action to this notification. Actions are typically displayed by
* the system as a button adjacent to the notification content.
* <br>
@@ -601,7 +947,7 @@
*/
@Deprecated
public Notification getNotification() {
- return (Notification) IMPL.build(this);
+ return IMPL.build(this);
}
/**
@@ -609,7 +955,7 @@
* object.
*/
public Notification build() {
- return (Notification) IMPL.build(this);
+ return IMPL.build(this);
}
}
@@ -620,8 +966,7 @@
* If the platform does not provide rich notification styles, methods in this class have no
* effect.
*/
- public static abstract class Style
- {
+ public static abstract class Style {
Builder mBuilder;
CharSequence mBigContentTitle;
CharSequence mSummaryText;
@@ -844,4 +1189,23 @@
this.actionIntent = intent_;
}
}
+
+ /**
+ * Gets the {@link Notification#extras} field from a notification in a backwards
+ * compatible manner. Extras field was supported from JellyBean (Api level 16)
+ * forwards. This function will return null on older api levels.
+ */
+ public static Bundle getExtras(Notification notif) {
+ return IMPL.getExtras(notif);
+ }
+
+ /**
+ * Get whether or not this notification is only relevant to the current device.
+ *
+ * <p>Some notifications can be bridged to other devices for remote display.
+ * If this hint is set, it is recommend that this notification not be bridged.
+ */
+ public static boolean getLocalOnly(Notification notif) {
+ return IMPL.getLocalOnly(notif);
+ }
}
diff --git a/v4/java/android/support/v4/app/NotificationCompatExtras.java b/v4/java/android/support/v4/app/NotificationCompatExtras.java
new file mode 100644
index 0000000..de6e8cd
--- /dev/null
+++ b/v4/java/android/support/v4/app/NotificationCompatExtras.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.v4.app;
+
+/**
+ * Well-known extras used by {@link NotificationCompat} for backwards compatibility.
+ */
+public final class NotificationCompatExtras {
+ /**
+ * Extras key used internally by {@link NotificationCompat} to store the value of
+ * the {@code Notification.FLAG_LOCAL_ONLY} field before it was available.
+ * If possible, use {@link NotificationCompat#getLocalOnly} instead.
+ */
+ public static final String EXTRA_LOCAL_ONLY = NotificationCompatJellybean.EXTRA_LOCAL_ONLY;
+
+ private NotificationCompatExtras() {}
+}
diff --git a/v4/java/android/support/v4/app/ShareCompat.java b/v4/java/android/support/v4/app/ShareCompat.java
index 52c4b12..87ebc49 100644
--- a/v4/java/android/support/v4/app/ShareCompat.java
+++ b/v4/java/android/support/v4/app/ShareCompat.java
@@ -24,6 +24,7 @@
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
+import android.support.annotation.StringRes;
import android.support.v4.content.IntentCompat;
import android.support.v4.view.MenuItemCompat;
import android.text.Html;
@@ -403,7 +404,7 @@
* @param resId Resource ID of the title string to use
* @return This IntentBuilder for method chaining
*/
- public IntentBuilder setChooserTitle(int resId) {
+ public IntentBuilder setChooserTitle(@StringRes int resId) {
return setChooserTitle(mActivity.getText(resId));
}
diff --git a/v4/java/android/support/v4/net/ConnectivityManagerCompat.java b/v4/java/android/support/v4/net/ConnectivityManagerCompat.java
index 2170ab6..14f150a 100644
--- a/v4/java/android/support/v4/net/ConnectivityManagerCompat.java
+++ b/v4/java/android/support/v4/net/ConnectivityManagerCompat.java
@@ -108,10 +108,14 @@
* {@link ConnectivityManager#CONNECTIVITY_ACTION} broadcast. This obtains
* the current state from {@link ConnectivityManager} instead of using the
* potentially-stale value from
- * {@link ConnectivityManager#EXTRA_NETWORK_INFO}.
+ * {@link ConnectivityManager#EXTRA_NETWORK_INFO}. May be {@code null}.
*/
public static NetworkInfo getNetworkInfoFromBroadcast(ConnectivityManager cm, Intent intent) {
final NetworkInfo info = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
- return cm.getNetworkInfo(info.getType());
+ if (info != null) {
+ return cm.getNetworkInfo(info.getType());
+ } else {
+ return null;
+ }
}
}
diff --git a/v4/java/android/support/v4/text/TextUtilsCompat.java b/v4/java/android/support/v4/text/TextUtilsCompat.java
index 3400866..436d72f 100644
--- a/v4/java/android/support/v4/text/TextUtilsCompat.java
+++ b/v4/java/android/support/v4/text/TextUtilsCompat.java
@@ -16,6 +16,8 @@
package android.support.v4.text;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import java.util.Locale;
@@ -27,7 +29,8 @@
* @param s the string to be encoded
* @return the encoded string
*/
- public static String htmlEncode(String s) {
+ @NonNull
+ public static String htmlEncode(@NonNull String s) {
StringBuilder sb = new StringBuilder();
char c;
for (int i = 0; i < s.length(); i++) {
@@ -69,7 +72,7 @@
*
* Be careful: this code will need to be updated when vertical scripts will be supported
*/
- public static int getLayoutDirectionFromLocale(Locale locale) {
+ public static int getLayoutDirectionFromLocale(@Nullable Locale locale) {
if (locale != null && !locale.equals(ROOT)) {
final String scriptSubtag = ICUCompat.getScript(
ICUCompat.addLikelySubtags(locale.toString()));
diff --git a/v4/java/android/support/v4/util/CircularArray.java b/v4/java/android/support/v4/util/CircularArray.java
new file mode 100644
index 0000000..91a27da
--- /dev/null
+++ b/v4/java/android/support/v4/util/CircularArray.java
@@ -0,0 +1,126 @@
+/*
+ * 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.util;
+
+/**
+ * A circular array implementation that provides O(1) random read and O(1)
+ * prepend and O(1) append.
+ */
+public class CircularArray<E>
+{
+ private E[] mElements;
+ private int mHead;
+ private int mTail;
+ private int mCapacityBitmask;
+
+ private void doubleCapacity() {
+ int n = mElements.length;
+ int r = n - mHead;
+ int newCapacity = n << 1;
+ if (newCapacity < 0) {
+ throw new RuntimeException("Too big");
+ }
+ Object[] a = new Object[newCapacity];
+ System.arraycopy(mElements, mHead, a, 0, r);
+ System.arraycopy(mElements, 0, a, r, mHead);
+ mElements = (E[])a;
+ mHead = 0;
+ mTail = n;
+ mCapacityBitmask = newCapacity - 1;
+ }
+
+ /**
+ * Create a CircularArray with default capacity.
+ */
+ public CircularArray() {
+ this(8);
+ }
+
+ /**
+ * Create a CircularArray with capacity for at least minCapacity elements.
+ *
+ * @param minCapacity The minimum capacity required for the circular array.
+ */
+ public CircularArray(int minCapacity) {
+ if (minCapacity <= 0) {
+ throw new IllegalArgumentException("capacity must be positive");
+ }
+ int arrayCapacity = minCapacity;
+ // If minCapacity isn't a power of 2, round up to the next highest power
+ // of 2.
+ if (Integer.bitCount(minCapacity) != 1) {
+ arrayCapacity = 1 << (Integer.highestOneBit(minCapacity) + 1);
+ }
+ mCapacityBitmask = arrayCapacity - 1;
+ mElements = (E[]) new Object[arrayCapacity];
+ }
+
+ public final void addFirst(E e) {
+ mHead = (mHead - 1) & mCapacityBitmask;
+ mElements[mHead] = e;
+ if (mHead == mTail) {
+ doubleCapacity();
+ }
+ }
+
+ public final void addLast(E e) {
+ mElements[mTail] = e;
+ mTail = (mTail + 1) & mCapacityBitmask;
+ if (mTail == mHead) {
+ doubleCapacity();
+ }
+ }
+
+ public final E popFirst() {
+ if (mHead == mTail) throw new ArrayIndexOutOfBoundsException();
+ E result = mElements[mHead];
+ mElements[mHead] = null;
+ mHead = (mHead + 1) & mCapacityBitmask;
+ return result;
+ }
+
+ public final E popLast() {
+ if (mHead == mTail) throw new ArrayIndexOutOfBoundsException();
+ int t = (mTail - 1) & mCapacityBitmask;
+ E result = mElements[t];
+ mElements[t] = null;
+ mTail = t;
+ return result;
+ }
+
+ public final E getFirst() {
+ if (mHead == mTail) throw new ArrayIndexOutOfBoundsException();
+ return mElements[mHead];
+ }
+
+ public final E getLast() {
+ if (mHead == mTail) throw new ArrayIndexOutOfBoundsException();
+ return mElements[(mTail - 1) & mCapacityBitmask];
+ }
+
+ public final E get(int i) {
+ if (i < 0 || i >= size()) throw new ArrayIndexOutOfBoundsException();
+ int p = (mHead + i) & mCapacityBitmask;
+ return mElements[p];
+ }
+
+ public final int size() {
+ return (mTail - mHead) & mCapacityBitmask;
+ }
+
+ public final boolean isEmpty() {
+ return mHead == mTail;
+ }
+
+}
diff --git a/v4/java/android/support/v4/util/Pools.java b/v4/java/android/support/v4/util/Pools.java
new file mode 100644
index 0000000..e3907a1
--- /dev/null
+++ b/v4/java/android/support/v4/util/Pools.java
@@ -0,0 +1,166 @@
+/*
+ * 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.util;
+
+
+/**
+ * Helper class for crating pools of objects. An example use looks like this:
+ * <pre>
+ * public class MyPooledClass {
+ *
+ * private static final SynchronizedPool<MyPooledClass> sPool =
+ * new SynchronizedPool<MyPooledClass>(10);
+ *
+ * public static MyPooledClass obtain() {
+ * MyPooledClass instance = sPool.acquire();
+ * return (instance != null) ? instance : new MyPooledClass();
+ * }
+ *
+ * public void recycle() {
+ * // Clear state if needed.
+ * sPool.release(this);
+ * }
+ *
+ * . . .
+ * }
+ * </pre>
+ *
+ */
+public final class Pools {
+
+ /**
+ * Interface for managing a pool of objects.
+ *
+ * @param <T> The pooled type.
+ */
+ public static interface Pool<T> {
+
+ /**
+ * @return An instance from the pool if such, null otherwise.
+ */
+ public T acquire();
+
+ /**
+ * Release an instance to the pool.
+ *
+ * @param instance The instance to release.
+ * @return Whether the instance was put in the pool.
+ *
+ * @throws IllegalStateException If the instance is already in the pool.
+ */
+ public boolean release(T instance);
+ }
+
+ private Pools() {
+ /* do nothing - hiding constructor */
+ }
+
+ /**
+ * Simple (non-synchronized) pool of objects.
+ *
+ * @param <T> The pooled type.
+ */
+ public static class SimplePool<T> implements Pool<T> {
+ private final Object[] mPool;
+
+ private int mPoolSize;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param maxPoolSize The max pool size.
+ *
+ * @throws IllegalArgumentException If the max pool size is less than zero.
+ */
+ public SimplePool(int maxPoolSize) {
+ if (maxPoolSize <= 0) {
+ throw new IllegalArgumentException("The max pool size must be > 0");
+ }
+ mPool = new Object[maxPoolSize];
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T acquire() {
+ if (mPoolSize > 0) {
+ final int lastPooledIndex = mPoolSize - 1;
+ T instance = (T) mPool[lastPooledIndex];
+ mPool[lastPooledIndex] = null;
+ mPoolSize--;
+ return instance;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean release(T instance) {
+ if (isInPool(instance)) {
+ throw new IllegalStateException("Already in the pool!");
+ }
+ if (mPoolSize < mPool.length) {
+ mPool[mPoolSize] = instance;
+ mPoolSize++;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isInPool(T instance) {
+ for (int i = 0; i < mPoolSize; i++) {
+ if (mPool[i] == instance) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Synchronized) pool of objects.
+ *
+ * @param <T> The pooled type.
+ */
+ public static class SynchronizedPool<T> extends SimplePool<T> {
+ private final Object mLock = new Object();
+
+ /**
+ * Creates a new instance.
+ *
+ * @param maxPoolSize The max pool size.
+ *
+ * @throws IllegalArgumentException If the max pool size is less than zero.
+ */
+ public SynchronizedPool(int maxPoolSize) {
+ super(maxPoolSize);
+ }
+
+ @Override
+ public T acquire() {
+ synchronized (mLock) {
+ return super.acquire();
+ }
+ }
+
+ @Override
+ public boolean release(T element) {
+ synchronized (mLock) {
+ return super.release(element);
+ }
+ }
+ }
+}
diff --git a/v4/java/android/support/v4/view/PagerTabStrip.java b/v4/java/android/support/v4/view/PagerTabStrip.java
index 21488b8..834035c 100644
--- a/v4/java/android/support/v4/view/PagerTabStrip.java
+++ b/v4/java/android/support/v4/view/PagerTabStrip.java
@@ -21,6 +21,8 @@
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorRes;
+import android.support.annotation.DrawableRes;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
@@ -136,7 +138,7 @@
*
* @param resId Resource ID of a color resource to load
*/
- public void setTabIndicatorColorResource(int resId) {
+ public void setTabIndicatorColorResource(@ColorRes int resId) {
setTabIndicatorColor(getContext().getResources().getColor(resId));
}
@@ -180,7 +182,7 @@
}
@Override
- public void setBackgroundResource(int resId) {
+ public void setBackgroundResource(@DrawableRes int resId) {
super.setBackgroundResource(resId);
if (!mDrawFullUnderlineSet) {
mDrawFullUnderline = resId == 0;
diff --git a/v4/java/android/support/v4/view/ViewCompat.java b/v4/java/android/support/v4/view/ViewCompat.java
index bb64afc..abfee23 100644
--- a/v4/java/android/support/v4/view/ViewCompat.java
+++ b/v4/java/android/support/v4/view/ViewCompat.java
@@ -21,17 +21,32 @@
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.IntDef;
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat;
+import android.util.Log;
import android.view.View;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
/**
* Helper for accessing features in {@link View} introduced after API
* level 4 in a backwards compatible fashion.
*/
public class ViewCompat {
+ private static final String TAG = "ViewCompat";
+
+ /** @hide */
+ @IntDef({OVER_SCROLL_ALWAYS, OVER_SCROLL_IF_CONTENT_SCROLLS, OVER_SCROLL_IF_CONTENT_SCROLLS})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface OverScroll {}
+
/**
* Always allow a user to over-scroll this view, provided it is a
* view that can scroll.
@@ -51,6 +66,16 @@
private static final long FAKE_FRAME_TIME = 10;
+ /** @hide */
+ @IntDef({
+ IMPORTANT_FOR_ACCESSIBILITY_AUTO,
+ IMPORTANT_FOR_ACCESSIBILITY_YES,
+ IMPORTANT_FOR_ACCESSIBILITY_NO,
+ IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface ImportantForAccessibility {}
+
/**
* Automatically determine whether a view is important for accessibility.
*/
@@ -72,6 +97,15 @@
*/
public static final int IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS = 0x00000004;
+ /** @hide */
+ @IntDef({
+ ACCESSIBILITY_LIVE_REGION_NONE,
+ ACCESSIBILITY_LIVE_REGION_POLITE,
+ ACCESSIBILITY_LIVE_REGION_ASSERTIVE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface AccessibilityLiveRegion {}
+
/**
* Live region mode specifying that accessibility services should not
* automatically announce changes to this view. This is the default live
@@ -97,6 +131,11 @@
*/
public static final int ACCESSIBILITY_LIVE_REGION_ASSERTIVE = 0x00000002;
+ /** @hide */
+ @IntDef({LAYER_TYPE_NONE, LAYER_TYPE_SOFTWARE, LAYER_TYPE_HARDWARE})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface LayerType {}
+
/**
* Indicates that the view does not have a layer.
*/
@@ -145,6 +184,23 @@
*/
public static final int LAYER_TYPE_HARDWARE = 2;
+ /** @hide */
+ @IntDef({
+ LAYOUT_DIRECTION_LTR,
+ LAYOUT_DIRECTION_RTL,
+ LAYOUT_DIRECTION_INHERIT,
+ LAYOUT_DIRECTION_LOCALE})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface LayoutDirectionMode {}
+
+ /** @hide */
+ @IntDef({
+ LAYOUT_DIRECTION_LTR,
+ LAYOUT_DIRECTION_RTL
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface ResolvedLayoutDirectionMode {}
+
/**
* Horizontal layout direction of this view is from Left to Right.
*/
@@ -229,9 +285,22 @@
public int getMeasuredState(View view);
public int getAccessibilityLiveRegion(View view);
public void setAccessibilityLiveRegion(View view, int mode);
+ public int getPaddingStart(View view);
+ public int getPaddingEnd(View view);
+ public void setPaddingRelative(View view, int start, int top, int end, int bottom);
+ public void dispatchStartTemporaryDetach(View view);
+ public void dispatchFinishTemporaryDetach(View view);
+ public float getTranslationX(View view);
+ public float getTranslationY(View view);
+ public int getMinimumWidth(View view);
+ public int getMinimumHeight(View view);
}
static class BaseViewCompatImpl implements ViewCompatImpl {
+ private Method mDispatchStartTemporaryDetach;
+ private Method mDispatchFinishTemporaryDetach;
+ private boolean mTempDetachBound;
+
public boolean canScrollHorizontally(View v, int direction) {
return false;
}
@@ -264,10 +333,10 @@
// Do nothing; API doesn't exist
}
public void postInvalidateOnAnimation(View view) {
- view.postInvalidateDelayed(getFrameTime());
+ view.invalidate();
}
public void postInvalidateOnAnimation(View view, int left, int top, int right, int bottom) {
- view.postInvalidateDelayed(getFrameTime(), left, top, right, bottom);
+ view.invalidate(left, top, right, bottom);
}
public void postOnAnimation(View view, Runnable action) {
view.postDelayed(action, getFrameTime());
@@ -361,6 +430,87 @@
public void setAccessibilityLiveRegion(View view, int mode) {
// No-op
}
+
+ @Override
+ public int getPaddingStart(View view) {
+ return view.getPaddingLeft();
+ }
+
+ @Override
+ public int getPaddingEnd(View view) {
+ return view.getPaddingRight();
+ }
+
+ @Override
+ public void setPaddingRelative(View view, int start, int top, int end, int bottom) {
+ view.setPadding(start, top, end, bottom);
+ }
+
+ @Override
+ public void dispatchStartTemporaryDetach(View view) {
+ if (!mTempDetachBound) {
+ bindTempDetach();
+ }
+ if (mDispatchStartTemporaryDetach != null) {
+ try {
+ mDispatchStartTemporaryDetach.invoke(view);
+ } catch (Exception e) {
+ Log.d(TAG, "Error calling dispatchStartTemporaryDetach", e);
+ }
+ } else {
+ // Try this instead
+ view.onStartTemporaryDetach();
+ }
+ }
+
+ @Override
+ public void dispatchFinishTemporaryDetach(View view) {
+ if (!mTempDetachBound) {
+ bindTempDetach();
+ }
+ if (mDispatchFinishTemporaryDetach != null) {
+ try {
+ mDispatchFinishTemporaryDetach.invoke(view);
+ } catch (Exception e) {
+ Log.d(TAG, "Error calling dispatchFinishTemporaryDetach", e);
+ }
+ } else {
+ // Try this instead
+ view.onFinishTemporaryDetach();
+ }
+ }
+
+ private void bindTempDetach() {
+ try {
+ mDispatchStartTemporaryDetach = View.class.getDeclaredMethod(
+ "dispatchStartTemporaryDetach");
+ mDispatchFinishTemporaryDetach = View.class.getDeclaredMethod(
+ "dispatchFinishTemporaryDetach");
+ } catch (NoSuchMethodException e) {
+ Log.e(TAG, "Couldn't find method", e);
+ }
+ mTempDetachBound = true;
+ }
+
+ @Override
+ public float getTranslationX(View view) {
+ return 0;
+ }
+
+ @Override
+ public float getTranslationY(View view) {
+ return 0;
+ }
+
+ @Override
+ public int getMinimumWidth(View view) {
+ return 0;
+ }
+
+ @Override
+ public int getMinimumHeight(View view) {
+ return 0;
+ }
}
static class EclairMr1ViewCompatImpl extends BaseViewCompatImpl {
@@ -422,6 +572,14 @@
public int getMeasuredState(View view) {
return ViewCompatHC.getMeasuredState(view);
}
+ @Override
+ public float getTranslationX(View view) {
+ return ViewCompatHC.getTranslationX(view);
+ }
+ @Override
+ public float getTranslationY(View view) {
+ return ViewCompatHC.getTranslationY(view);
+ }
}
static class ICSViewCompatImpl extends HCViewCompatImpl {
@@ -482,6 +640,12 @@
}
@Override
public void setImportantForAccessibility(View view, int mode) {
+ // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS is not available
+ // on this platform so replace with IMPORTANT_FOR_ACCESSIBILITY_NO
+ // which is closer semantically.
+ if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
+ mode = IMPORTANT_FOR_ACCESSIBILITY_NO;
+ }
ViewCompatJB.setImportantForAccessibility(view, mode);
}
@Override
@@ -501,6 +665,16 @@
public ViewParent getParentForAccessibility(View view) {
return ViewCompatJB.getParentForAccessibility(view);
}
+
+ @Override
+ public int getMinimumWidth(View view) {
+ return ViewCompatJB.getMinimumWidth(view);
+ }
+
+ @Override
+ public int getMinimumHeight(View view) {
+ return ViewCompatJB.getMinimumHeight(view);
+ }
}
static class JbMr1ViewCompatImpl extends JBViewCompatImpl {
@@ -529,6 +703,21 @@
public void setLayoutDirection(View view, int layoutDirection) {
ViewCompatJellybeanMr1.setLayoutDirection(view, layoutDirection);
}
+
+ @Override
+ public int getPaddingStart(View view) {
+ return ViewCompatJellybeanMr1.getPaddingStart(view);
+ }
+
+ @Override
+ public int getPaddingEnd(View view) {
+ return ViewCompatJellybeanMr1.getPaddingEnd(view);
+ }
+
+ @Override
+ public void setPaddingRelative(View view, int start, int top, int end, int bottom) {
+ ViewCompatJellybeanMr1.setPaddingRelative(view, start, top, end, bottom);
+ }
}
static class KitKatViewCompatImpl extends JbMr1ViewCompatImpl {
@@ -541,6 +730,11 @@
public void setAccessibilityLiveRegion(View view, int mode) {
ViewCompatKitKat.setAccessibilityLiveRegion(view, mode);
}
+
+ @Override
+ public void setImportantForAccessibility(View view, int mode) {
+ ViewCompatJB.setImportantForAccessibility(view, mode);
+ }
}
static final ViewCompatImpl IMPL;
@@ -594,6 +788,7 @@
* @param v The View against which to invoke the method.
* @return This view's over-scroll mode.
*/
+ @OverScroll
public static int getOverScrollMode(View v) {
return IMPL.getOverScrollMode(v);
}
@@ -610,7 +805,7 @@
* @param v The View against which to invoke the method.
* @param overScrollMode The new over-scroll mode for this view.
*/
- public static void setOverScrollMode(View v, int overScrollMode) {
+ public static void setOverScrollMode(View v, @OverScroll int overScrollMode) {
IMPL.setOverScrollMode(v, overScrollMode);
}
@@ -833,6 +1028,7 @@
* @see #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
* @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO
*/
+ @ImportantForAccessibility
public static int getImportantForAccessibility(View view) {
return IMPL.getImportantForAccessibility(view);
}
@@ -841,6 +1037,12 @@
* Sets how to determine whether this view is important for accessibility
* which is if it fires accessibility events and if it is reported to
* accessibility services that query the screen.
+ * <p>
+ * <em>Note:</em> If the current paltform version does not support the
+ * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS} mode, then
+ * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO} will be used as it is the
+ * closest terms of semantics.
+ * </p>
*
* @param view The view whose property to set.
* @param mode How to determine whether this view is important for accessibility.
@@ -850,7 +1052,8 @@
* @see #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
* @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO
*/
- public static void setImportantForAccessibility(View view, int mode) {
+ public static void setImportantForAccessibility(View view,
+ @ImportantForAccessibility int mode) {
IMPL.setImportantForAccessibility(view, mode);
}
@@ -949,7 +1152,7 @@
* and can be null. It is ignored when the layer type is
* {@link #LAYER_TYPE_NONE}
*/
- public static void setLayerType(View view, int layerType, Paint paint) {
+ public static void setLayerType(View view, @LayerType int layerType, Paint paint) {
IMPL.setLayerType(view, layerType, paint);
}
@@ -969,6 +1172,7 @@
* @see #LAYER_TYPE_SOFTWARE
* @see #LAYER_TYPE_HARDWARE
*/
+ @LayerType
public static int getLayerType(View view) {
return IMPL.getLayerType(view);
}
@@ -991,7 +1195,7 @@
* @param view The view on which to invoke the corresponding method.
* @param labeledId The labeled view id.
*/
- public static void setLabelFor(View view, int labeledId) {
+ public static void setLabelFor(View view, @IdRes int labeledId) {
IMPL.setLabelFor(view, labeledId);
}
@@ -1039,6 +1243,7 @@
* For compatibility, this will return {@link #LAYOUT_DIRECTION_LTR} if API version
* is lower than Jellybean MR1 (API 17)
*/
+ @ResolvedLayoutDirectionMode
public static int getLayoutDirection(View view) {
return IMPL.getLayoutDirection(view);
}
@@ -1059,7 +1264,7 @@
* proceeds up the parent chain of the view to get the value. If there is no parent, then it
* will return the default {@link #LAYOUT_DIRECTION_LTR}.
*/
- public static void setLayoutDirection(View view, int layoutDirection) {
+ public static void setLayoutDirection(View view, @LayoutDirectionMode int layoutDirection) {
IMPL.setLayoutDirection(view, layoutDirection);
}
@@ -1152,7 +1357,8 @@
*
* @see ViewCompat#setAccessibilityLiveRegion(View, int)
*/
- public int getAccessibilityLiveRegion(View view) {
+ @AccessibilityLiveRegion
+ public static int getAccessibilityLiveRegion(View view) {
return IMPL.getAccessibilityLiveRegion(view);
}
@@ -1184,7 +1390,106 @@
* <li>{@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}
* </ul>
*/
- public void setAccessibilityLiveRegion(View view, int mode) {
+ public static void setAccessibilityLiveRegion(View view, @AccessibilityLiveRegion int mode) {
IMPL.setAccessibilityLiveRegion(view, mode);
}
+
+ /**
+ * Returns the start padding of the specified view depending on its resolved layout direction.
+ * If there are inset and enabled scrollbars, this value may include the space
+ * required to display the scrollbars as well.
+ *
+ * @param view The view to get padding for
+ * @return the start padding in pixels
+ */
+ public static int getPaddingStart(View view) {
+ return IMPL.getPaddingStart(view);
+ }
+
+ /**
+ * Returns the end padding of the specified view depending on its resolved layout direction.
+ * If there are inset and enabled scrollbars, this value may include the space
+ * required to display the scrollbars as well.
+ *
+ * @param view The view to get padding for
+ * @return the end padding in pixels
+ */
+ public static int getPaddingEnd(View view) {
+ return IMPL.getPaddingEnd(view);
+ }
+
+ /**
+ * Sets the relative padding. The view may add on the space required to display
+ * the scrollbars, depending on the style and visibility of the scrollbars.
+ * So the values returned from {@link #getPaddingStart}, {@link View#getPaddingTop},
+ * {@link #getPaddingEnd} and {@link View#getPaddingBottom} may be different
+ * from the values set in this call.
+ *
+ * @param view The view on which to set relative padding
+ * @param start the start padding in pixels
+ * @param top the top padding in pixels
+ * @param end the end padding in pixels
+ * @param bottom the bottom padding in pixels
+ */
+ public static void setPaddingRelative(View view, int start, int top, int end, int bottom) {
+ IMPL.setPaddingRelative(view, start, top, end, bottom);
+ }
+
+ /**
+ * Notify a view that it is being temporarily detached.
+ */
+ public static void dispatchStartTemporaryDetach(View view) {
+ IMPL.dispatchStartTemporaryDetach(view);
+ }
+
+ /**
+ * Notify a view that its temporary detach has ended; the view is now reattached.
+ */
+ public static void dispatchFinishTemporaryDetach(View view) {
+ IMPL.dispatchFinishTemporaryDetach(view);
+ }
+
+ /**
+ * The horizontal location of this view relative to its {@link View#getLeft() left} position.
+ * This position is post-layout, in addition to wherever the object's
+ * layout placed it.
+ *
+ * @return The horizontal position of this view relative to its left position, in pixels.
+ */
+ public static float getTranslationX(View view) {
+ return IMPL.getTranslationX(view);
+ }
+
+ /**
+ * The vertical location of this view relative to its {@link View#getTop() left} position.
+ * This position is post-layout, in addition to wherever the object's
+ * layout placed it.
+ *
+ * @return The vertical position of this view relative to its top position, in pixels.
+ */
+ public static float getTranslationY(View view) {
+ return IMPL.getTranslationY(view);
+ }
+
+ /**
+ * Returns the minimum width of the view.
+ *
+ * <p>Prior to API 16 this will return 0.</p>
+ *
+ * @return the minimum width the view will try to be.
+ */
+ public static int getMinimumWidth(View view) {
+ return IMPL.getMinimumWidth(view);
+ }
+
+ /**
+ * Returns the minimum height of the view.
+ *
+ * <p>Prior to API 16 this will return 0.</p>
+ *
+ * @return the minimum height the view will try to be.
+ */
+ public static int getMinimumHeight(View view) {
+ return IMPL.getMinimumHeight(view);
+ }
}
diff --git a/v4/java/android/support/v4/view/ViewPager.java b/v4/java/android/support/v4/view/ViewPager.java
index e90744c..e9fc1a8 100644
--- a/v4/java/android/support/v4/view/ViewPager.java
+++ b/v4/java/android/support/v4/view/ViewPager.java
@@ -28,6 +28,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
+import android.support.annotation.DrawableRes;
import android.support.v4.os.ParcelableCompat;
import android.support.v4.os.ParcelableCompatCreatorCallbacks;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
@@ -740,7 +741,7 @@
*
* @param resId Resource ID of a drawable to display between pages
*/
- public void setPageMarginDrawable(int resId) {
+ public void setPageMarginDrawable(@DrawableRes int resId) {
setPageMarginDrawable(getContext().getResources().getDrawable(resId));
}
diff --git a/v4/java/android/support/v4/widget/DrawerLayout.java b/v4/java/android/support/v4/widget/DrawerLayout.java
index 92ef51d..e4ba5cf 100644
--- a/v4/java/android/support/v4/widget/DrawerLayout.java
+++ b/v4/java/android/support/v4/widget/DrawerLayout.java
@@ -27,6 +27,9 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.KeyEventCompat;
@@ -40,9 +43,12 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
import android.view.ViewParent;
import android.view.accessibility.AccessibilityEvent;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
@@ -73,6 +79,11 @@
public class DrawerLayout extends ViewGroup {
private static final String TAG = "DrawerLayout";
+ /** @hide */
+ @IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface State {}
+
/**
* Indicates that any drawers are in an idle, settled state. No animation is in progress.
*/
@@ -88,6 +99,11 @@
*/
public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING;
+ /** @hide */
+ @IntDef({LOCK_MODE_UNLOCKED, LOCK_MODE_LOCKED_CLOSED, LOCK_MODE_LOCKED_OPEN})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface LockMode {}
+
/**
* The drawer is unlocked.
*/
@@ -105,6 +121,12 @@
*/
public static final int LOCK_MODE_LOCKED_OPEN = 2;
+ /** @hide */
+ @IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface EdgeGravity {}
+
+
private static final int MIN_DRAWER_MARGIN = 64; // dp
private static final int DEFAULT_SCRIM_COLOR = 0x99000000;
@@ -132,6 +154,9 @@
android.R.attr.layout_gravity
};
+ private final ChildAccessibilityDelegate mChildAccessibilityDelegate =
+ new ChildAccessibilityDelegate();
+
private int mMinDrawerMargin;
private int mScrimColor = DEFAULT_SCRIM_COLOR;
@@ -193,7 +218,7 @@
*
* @param newState The new drawer motion state
*/
- public void onDrawerStateChanged(int newState);
+ public void onDrawerStateChanged(@State int newState);
}
/**
@@ -249,6 +274,9 @@
// So that we can catch the back button
setFocusableInTouchMode(true);
+ ViewCompat.setImportantForAccessibility(this,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
+
ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate());
ViewGroupCompat.setMotionEventSplittingEnabled(this, false);
}
@@ -260,7 +288,7 @@
* @param shadowDrawable Shadow drawable to use at the edge of a drawer
* @param gravity Which drawer the shadow should apply to
*/
- public void setDrawerShadow(Drawable shadowDrawable, int gravity) {
+ public void setDrawerShadow(Drawable shadowDrawable, @EdgeGravity int gravity) {
/*
* TODO Someone someday might want to set more complex drawables here.
* They're probably nuts, but we might want to consider registering callbacks,
@@ -286,7 +314,7 @@
* @param resId Resource id of a shadow drawable to use at the edge of a drawer
* @param gravity Which drawer the shadow should apply to
*/
- public void setDrawerShadow(int resId, int gravity) {
+ public void setDrawerShadow(@DrawableRes int resId, @EdgeGravity int gravity) {
setDrawerShadow(getResources().getDrawable(resId), gravity);
}
@@ -323,7 +351,7 @@
* @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED},
* {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}.
*/
- public void setDrawerLockMode(int lockMode) {
+ public void setDrawerLockMode(@LockMode int lockMode) {
setDrawerLockMode(lockMode, Gravity.LEFT);
setDrawerLockMode(lockMode, Gravity.RIGHT);
}
@@ -347,7 +375,7 @@
* @see #LOCK_MODE_LOCKED_CLOSED
* @see #LOCK_MODE_LOCKED_OPEN
*/
- public void setDrawerLockMode(int lockMode, int edgeGravity) {
+ public void setDrawerLockMode(@LockMode int lockMode, @EdgeGravity int edgeGravity) {
final int absGravity = GravityCompat.getAbsoluteGravity(edgeGravity,
ViewCompat.getLayoutDirection(this));
if (absGravity == Gravity.LEFT) {
@@ -395,7 +423,7 @@
* @see #LOCK_MODE_LOCKED_CLOSED
* @see #LOCK_MODE_LOCKED_OPEN
*/
- public void setDrawerLockMode(int lockMode, View drawerView) {
+ public void setDrawerLockMode(@LockMode int lockMode, View drawerView) {
if (!isDrawerView(drawerView)) {
throw new IllegalArgumentException("View " + drawerView + " is not a " +
"drawer with appropriate layout_gravity");
@@ -411,7 +439,8 @@
* @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or
* {@link #LOCK_MODE_LOCKED_OPEN}.
*/
- public int getDrawerLockMode(int edgeGravity) {
+ @LockMode
+ public int getDrawerLockMode(@EdgeGravity int edgeGravity) {
final int absGravity = GravityCompat.getAbsoluteGravity(
edgeGravity, ViewCompat.getLayoutDirection(this));
if (absGravity == Gravity.LEFT) {
@@ -429,6 +458,7 @@
* @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or
* {@link #LOCK_MODE_LOCKED_OPEN}.
*/
+ @LockMode
public int getDrawerLockMode(View drawerView) {
final int absGravity = getDrawerViewAbsoluteGravity(drawerView);
if (absGravity == Gravity.LEFT) {
@@ -449,7 +479,7 @@
* drawer to set the title for.
* @param title The title for the drawer.
*/
- public void setDrawerTitle(int edgeGravity, CharSequence title) {
+ public void setDrawerTitle(@EdgeGravity int edgeGravity, CharSequence title) {
final int absGravity = GravityCompat.getAbsoluteGravity(
edgeGravity, ViewCompat.getLayoutDirection(this));
if (absGravity == Gravity.LEFT) {
@@ -467,7 +497,8 @@
* @return The title of the drawer, or null if none set.
* @see #setDrawerTitle(int, CharSequence)
*/
- public CharSequence getDrawerTitle(int edgeGravity) {
+ @Nullable
+ public CharSequence getDrawerTitle(@EdgeGravity int edgeGravity) {
final int absGravity = GravityCompat.getAbsoluteGravity(
edgeGravity, ViewCompat.getLayoutDirection(this));
if (absGravity == Gravity.LEFT) {
@@ -482,7 +513,7 @@
* Resolve the shared state of all drawers from the component ViewDragHelpers.
* Should be called whenever a ViewDragHelper's state changes.
*/
- void updateDrawerState(int forGravity, int activeState, View activeDrawer) {
+ void updateDrawerState(int forGravity, @State int activeState, View activeDrawer) {
final int leftState = mLeftDragger.getViewDragState();
final int rightState = mRightDragger.getViewDragState();
@@ -521,6 +552,16 @@
mListener.onDrawerClosed(drawerView);
}
+ // If no drawer is opened, all drawers are not shown
+ // for accessibility and the content is shown.
+ View content = getChildAt(0);
+ if (content != null) {
+ ViewCompat.setImportantForAccessibility(content,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ ViewCompat.setImportantForAccessibility(drawerView,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+
// Only send WINDOW_STATE_CHANGE if the host has window focus. This
// may change if support for multiple foreground windows (e.g. IME)
// improves.
@@ -540,7 +581,19 @@
if (mListener != null) {
mListener.onDrawerOpened(drawerView);
}
+
+ // If a drawer is opened, only it is shown for
+ // accessibility and the content is not shown.
+ View content = getChildAt(0);
+ if (content != null) {
+ ViewCompat.setImportantForAccessibility(content,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ }
+ ViewCompat.setImportantForAccessibility(drawerView,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
+
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ drawerView.requestFocus();
}
}
@@ -627,7 +680,7 @@
* @param gravity Absolute gravity value
* @return LEFT or RIGHT as appropriate, or a hex string
*/
- static String gravityToString(int gravity) {
+ static String gravityToString(@EdgeGravity int gravity) {
if ((gravity & Gravity.LEFT) == Gravity.LEFT) {
return "LEFT";
}
@@ -1093,7 +1146,7 @@
* @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right.
* GravityCompat.START or GravityCompat.END may also be used.
*/
- public void openDrawer(int gravity) {
+ public void openDrawer(@EdgeGravity int gravity) {
final View drawerView = findDrawerWithGravity(gravity);
if (drawerView == null) {
throw new IllegalArgumentException("No drawer view found with gravity " +
@@ -1133,7 +1186,7 @@
* @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right.
* GravityCompat.START or GravityCompat.END may also be used.
*/
- public void closeDrawer(int gravity) {
+ public void closeDrawer(@EdgeGravity int gravity) {
final View drawerView = findDrawerWithGravity(gravity);
if (drawerView == null) {
throw new IllegalArgumentException("No drawer view found with gravity " +
@@ -1168,7 +1221,7 @@
* @param drawerGravity Gravity of the drawer to check
* @return true if the given drawer view is in an open state
*/
- public boolean isDrawerOpen(int drawerGravity) {
+ public boolean isDrawerOpen(@EdgeGravity int drawerGravity) {
final View drawerView = findDrawerWithGravity(drawerGravity);
if (drawerView != null) {
return isDrawerOpen(drawerView);
@@ -1193,13 +1246,13 @@
/**
* Check if a given drawer view is currently visible on-screen. The drawer
- * may be only peeking onto the screen, fully extended, or anywhere inbetween.
+ * may be only peeking onto the screen, fully extended, or anywhere in between.
* If there is no drawer with the given gravity this method will return false.
*
* @param drawerGravity Gravity of the drawer to check
* @return true if the given drawer is visible on-screen
*/
- public boolean isDrawerVisible(int drawerGravity) {
+ public boolean isDrawerVisible(@EdgeGravity int drawerGravity) {
final View drawerView = findDrawerWithGravity(drawerGravity);
if (drawerView != null) {
return isDrawerVisible(drawerView);
@@ -1336,6 +1389,35 @@
return ss;
}
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ // Until a drawer is open, it is hidden from accessibility.
+ if (index > 0 || (index < 0 && getChildCount() > 0)) {
+ ViewCompat.setImportantForAccessibility(child,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ // Also set a delegate to break the child-parent relation if the
+ // child is hidden. For details (see incluceChildForAccessibility).
+ ViewCompat.setAccessibilityDelegate(child, mChildAccessibilityDelegate);
+ } else {
+ // Initially, the content is shown for accessibility.
+ ViewCompat.setImportantForAccessibility(child,
+ ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+ super.addView(child, index, params);
+ }
+
+ private static boolean includeChildForAccessibilitiy(View child) {
+ // If the child is not important for accessibility we make
+ // sure this hides the entire subtree rooted at it as the
+ // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDATS is not
+ // supported on older platforms but we want to hide the entire
+ // content and not opened drawers if a drawer is opened.
+ return ViewCompat.getImportantForAccessibility(child)
+ != ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ && ViewCompat.getImportantForAccessibility(child)
+ != ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO;
+ }
+
/**
* State persisted across instances
*/
@@ -1633,31 +1715,8 @@
final int childCount = v.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = v.getChildAt(i);
- if (filter(child)) {
- continue;
- }
-
- // Adding children that are marked as not important for
- // accessibility will break the hierarchy, so we need to check
- // that value and re-parent views if necessary.
- final int importance = ViewCompat.getImportantForAccessibility(child);
- switch (importance) {
- case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS:
- // Always skip NO_HIDE views and their descendants.
- break;
- case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO:
- // Re-parent children of NO view groups, skip NO views.
- if (child instanceof ViewGroup) {
- addChildrenForAccessibility(info, (ViewGroup) child);
- }
- break;
- case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO:
- // Force AUTO views to YES and add them.
- ViewCompat.setImportantForAccessibility(
- child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
- case ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES:
- info.addChild(child);
- break;
+ if (includeChildForAccessibilitiy(child)) {
+ info.addChild(child);
}
}
}
@@ -1665,17 +1724,12 @@
@Override
public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
AccessibilityEvent event) {
- if (!filter(child)) {
+ if (includeChildForAccessibilitiy(child)) {
return super.onRequestSendAccessibilityEvent(host, child, event);
}
return false;
}
- public boolean filter(View child) {
- final View openDrawer = findOpenDrawer();
- return openDrawer != null && openDrawer != child;
- }
-
/**
* This should really be in AccessibilityNodeInfoCompat, but there unfortunately
* seem to be a few elements that are not easily cloneable using the underlying API.
@@ -1707,4 +1761,18 @@
dest.addAction(src.getActions());
}
}
+
+ final class ChildAccessibilityDelegate extends AccessibilityDelegateCompat {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View child,
+ AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(child, info);
+ if (!includeChildForAccessibilitiy(child)) {
+ // If we are ignoring the sub-tree rooted at the child,
+ // break the connection to the rest of the node tree.
+ // For details refer to includeChildForAccessibilitiy.
+ info.setParent(null);
+ }
+ }
+ }
}
diff --git a/v4/java/android/support/v4/widget/ListViewAutoScrollHelper.java b/v4/java/android/support/v4/widget/ListViewAutoScrollHelper.java
index 0b3b30d..b23ca23 100644
--- a/v4/java/android/support/v4/widget/ListViewAutoScrollHelper.java
+++ b/v4/java/android/support/v4/widget/ListViewAutoScrollHelper.java
@@ -59,6 +59,10 @@
public boolean canTargetScrollVertically(int direction) {
final ListView target = mTarget;
final int itemCount = target.getCount();
+ if (itemCount == 0) {
+ return false;
+ }
+
final int childCount = target.getChildCount();
final int firstPosition = target.getFirstVisiblePosition();
final int lastPosition = firstPosition + childCount;
diff --git a/v4/java/android/support/v4/widget/ScrollerCompat.java b/v4/java/android/support/v4/widget/ScrollerCompat.java
index fec045a..cf71e8e 100644
--- a/v4/java/android/support/v4/widget/ScrollerCompat.java
+++ b/v4/java/android/support/v4/widget/ScrollerCompat.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.os.Build;
+import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.Scroller;
@@ -29,7 +30,10 @@
* the APIs from Scroller or OverScroller.</p>
*/
public class ScrollerCompat {
+ private static final String TAG = "ScrollerCompat";
+
Object mScroller;
+ ScrollerCompatImpl mImpl;
interface ScrollerCompatImpl {
Object createScroller(Context context, Interpolator interpolator);
@@ -52,7 +56,118 @@
int getFinalY(Object scroller);
}
+ static final int CHASE_FRAME_TIME = 16; // ms per target frame
+
+ static class Chaser {
+ private int mX;
+ private int mY;
+ private int mTargetX;
+ private int mTargetY;
+ private float mTranslateSmoothing = 2;
+ private long mLastTime;
+ private boolean mAborted;
+
+ @Override
+ public String toString() {
+ return "{x=" + mX + " y=" + mY + " targetX=" + mTargetX + " targetY=" + mTargetY +
+ " smoothing=" + mTranslateSmoothing + " lastTime=" + mLastTime + "}";
+ }
+
+ public int getCurrX() {
+ return mX;
+ }
+
+ public int getCurrY() {
+ return mY;
+ }
+
+ public int getFinalX() {
+ return mTargetX;
+ }
+
+ public int getFinalY() {
+ return mTargetY;
+ }
+
+ public void setCurrentPosition(int x, int y) {
+ mX = x;
+ mY = y;
+ mAborted = false;
+ }
+
+ public void setSmoothing(float smoothing) {
+ if (smoothing < 0) {
+ throw new IllegalArgumentException("smoothing value must be positive");
+ }
+ mTranslateSmoothing = smoothing;
+ }
+
+ public boolean isSmoothingEnabled() {
+ return mTranslateSmoothing > 0;
+ }
+
+ public void setTarget(int targetX, int targetY) {
+ mTargetX = targetX;
+ mTargetY = targetY;
+ }
+
+ public void abort() {
+ mX = mTargetX;
+ mY = mTargetY;
+ mLastTime = AnimationUtils.currentAnimationTimeMillis();
+ mAborted = true;
+ }
+
+ public boolean isFinished() {
+ return mAborted || (mX == mTargetX && mY == mTargetY);
+ }
+
+ public boolean computeScrollOffset() {
+ if (isSmoothingEnabled() && !isFinished()) {
+ final long now = AnimationUtils.currentAnimationTimeMillis();
+ final long dt = now - mLastTime;
+ final float framesElapsed = (float) dt / CHASE_FRAME_TIME;
+
+ if (framesElapsed > 0) {
+ for (int i = 0; i < framesElapsed; i++) {
+ final int totalDx = mTargetX - mX;
+ final int totalDy = mTargetY - mY;
+
+ final int dx = (int) (totalDx / mTranslateSmoothing);
+ final int dy = (int) (totalDy / mTranslateSmoothing);
+
+ mX += dx;
+ mY += dy;
+
+ // Handle cropping at the end
+ if (mX != mTargetX && dx == 0) {
+ mX = mTargetX;
+ }
+ if (mY != mTargetY && dy == 0) {
+ mY = mTargetY;
+ }
+ }
+
+ mLastTime = now;
+ }
+ return true;
+ }
+ return false;
+ }
+ }
+
static class ScrollerCompatImplBase implements ScrollerCompatImpl {
+ private Chaser mChaser;
+
+ public ScrollerCompatImplBase() {
+ mChaser = createChaser();
+ }
+
+ protected Chaser createChaser() {
+ // Override if running on a platform version where this isn't needed
+ return new Chaser();
+ }
+
@Override
public Object createScroller(Context context, Interpolator interpolator) {
return interpolator != null ?
@@ -61,16 +176,23 @@
@Override
public boolean isFinished(Object scroller) {
- return ((Scroller) scroller).isFinished();
+ return (!isSmoothingEnabled() || mChaser.isFinished()) &&
+ ((Scroller) scroller).isFinished();
}
@Override
public int getCurrX(Object scroller) {
+ if (isSmoothingEnabled()) {
+ return mChaser.getCurrX();
+ }
return ((Scroller) scroller).getCurrX();
}
@Override
public int getCurrY(Object scroller) {
+ if (isSmoothingEnabled()) {
+ return mChaser.getCurrY();
+ }
return ((Scroller) scroller).getCurrY();
}
@@ -81,34 +203,65 @@
@Override
public boolean computeScrollOffset(Object scroller) {
- return ((Scroller) scroller).computeScrollOffset();
+ final Scroller s = (Scroller) scroller;
+ final boolean result = s.computeScrollOffset();
+ if (isSmoothingEnabled()) {
+ mChaser.setTarget(s.getCurrX(), s.getCurrY());
+ if (isSmoothingEnabled() && !mChaser.isFinished()) {
+ return mChaser.computeScrollOffset() || result;
+ }
+ }
+ return result;
+ }
+
+ private boolean isSmoothingEnabled() {
+ return mChaser != null && mChaser.isSmoothingEnabled();
}
@Override
public void startScroll(Object scroller, int startX, int startY, int dx, int dy) {
+ if (isSmoothingEnabled()) {
+ mChaser.abort();
+ mChaser.setCurrentPosition(startX, startY);
+ }
((Scroller) scroller).startScroll(startX, startY, dx, dy);
}
@Override
public void startScroll(Object scroller, int startX, int startY, int dx, int dy,
int duration) {
+ if (isSmoothingEnabled()) {
+ mChaser.abort();
+ mChaser.setCurrentPosition(startX, startY);
+ }
((Scroller) scroller).startScroll(startX, startY, dx, dy, duration);
}
@Override
public void fling(Object scroller, int startX, int startY, int velX, int velY,
int minX, int maxX, int minY, int maxY) {
+ if (isSmoothingEnabled()) {
+ mChaser.abort();
+ mChaser.setCurrentPosition(startX, startY);
+ }
((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY);
}
@Override
public void fling(Object scroller, int startX, int startY, int velX, int velY,
int minX, int maxX, int minY, int maxY, int overX, int overY) {
+ if (isSmoothingEnabled()) {
+ mChaser.abort();
+ mChaser.setCurrentPosition(startX, startY);
+ }
((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY);
}
@Override
public void abortAnimation(Object scroller) {
+ if (mChaser != null) {
+ mChaser.abort();
+ }
((Scroller) scroller).abortAnimation();
}
@@ -141,23 +294,40 @@
}
static class ScrollerCompatImplGingerbread implements ScrollerCompatImpl {
+ private Chaser mChaser;
+
+ public ScrollerCompatImplGingerbread() {
+ mChaser = createChaser();
+ }
+
@Override
public Object createScroller(Context context, Interpolator interpolator) {
return ScrollerCompatGingerbread.createScroller(context, interpolator);
}
+ protected Chaser createChaser() {
+ return new Chaser();
+ }
+
@Override
public boolean isFinished(Object scroller) {
- return ScrollerCompatGingerbread.isFinished(scroller);
+ return (!isSmoothingEnabled() || mChaser.isFinished()) &&
+ ScrollerCompatGingerbread.isFinished(scroller);
}
@Override
public int getCurrX(Object scroller) {
+ if (isSmoothingEnabled()) {
+ return mChaser.getCurrX();
+ }
return ScrollerCompatGingerbread.getCurrX(scroller);
}
@Override
public int getCurrY(Object scroller) {
+ if (isSmoothingEnabled()) {
+ return mChaser.getCurrY();
+ }
return ScrollerCompatGingerbread.getCurrY(scroller);
}
@@ -168,23 +338,47 @@
@Override
public boolean computeScrollOffset(Object scroller) {
- return ScrollerCompatGingerbread.computeScrollOffset(scroller);
+ final boolean result = ScrollerCompatGingerbread.computeScrollOffset(scroller);
+ if (isSmoothingEnabled()) {
+ mChaser.setTarget(ScrollerCompatGingerbread.getCurrX(scroller),
+ ScrollerCompatGingerbread.getCurrY(scroller));
+ if (!mChaser.isFinished()) {
+ return mChaser.computeScrollOffset() || result;
+ }
+ }
+ return result;
+ }
+
+ private boolean isSmoothingEnabled() {
+ return mChaser != null && mChaser.isSmoothingEnabled();
}
@Override
public void startScroll(Object scroller, int startX, int startY, int dx, int dy) {
+ if (isSmoothingEnabled()) {
+ mChaser.abort();
+ mChaser.setCurrentPosition(startX, startY);
+ }
ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy);
}
@Override
public void startScroll(Object scroller, int startX, int startY, int dx, int dy,
int duration) {
+ if (isSmoothingEnabled()) {
+ mChaser.abort();
+ mChaser.setCurrentPosition(startX, startY);
+ }
ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy, duration);
}
@Override
public void fling(Object scroller, int startX, int startY, int velX, int velY,
int minX, int maxX, int minY, int maxY) {
+ if (isSmoothingEnabled()) {
+ mChaser.abort();
+ mChaser.setCurrentPosition(startX, startY);
+ }
ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY,
minX, maxX, minY, maxY);
}
@@ -192,12 +386,19 @@
@Override
public void fling(Object scroller, int startX, int startY, int velX, int velY,
int minX, int maxX, int minY, int maxY, int overX, int overY) {
+ if (isSmoothingEnabled()) {
+ mChaser.abort();
+ mChaser.setCurrentPosition(startX, startY);
+ }
ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY,
minX, maxX, minY, maxY, overX, overY);
}
@Override
public void abortAnimation(Object scroller) {
+ if (mChaser != null) {
+ mChaser.abort();
+ }
ScrollerCompatGingerbread.abortAnimation(scroller);
}
@@ -235,18 +436,6 @@
}
}
- static final ScrollerCompatImpl IMPL;
- static {
- final int version = Build.VERSION.SDK_INT;
- if (version >= 14) { // ICS
- IMPL = new ScrollerCompatImplIcs();
- } else if (version >= 9) { // Gingerbread
- IMPL = new ScrollerCompatImplGingerbread();
- } else {
- IMPL = new ScrollerCompatImplBase();
- }
- }
-
public static ScrollerCompat create(Context context) {
return create(context, null);
}
@@ -256,7 +445,23 @@
}
ScrollerCompat(Context context, Interpolator interpolator) {
- mScroller = IMPL.createScroller(context, interpolator);
+ this(Build.VERSION.SDK_INT, context, interpolator);
+
+ }
+
+ /**
+ * Private constructer where API version can be provided.
+ * Useful for unit testing.
+ */
+ private ScrollerCompat(int apiVersion, Context context, Interpolator interpolator) {
+ if (apiVersion >= 14) { // ICS
+ mImpl = new ScrollerCompatImplIcs();
+ } else if (apiVersion>= 9) { // Gingerbread
+ mImpl = new ScrollerCompatImplGingerbread();
+ } else {
+ mImpl = new ScrollerCompatImplBase();
+ }
+ mScroller = mImpl.createScroller(context, interpolator);
}
/**
@@ -265,7 +470,7 @@
* @return True if the scroller has finished scrolling, false otherwise.
*/
public boolean isFinished() {
- return IMPL.isFinished(mScroller);
+ return mImpl.isFinished(mScroller);
}
/**
@@ -274,7 +479,7 @@
* @return The new X offset as an absolute distance from the origin.
*/
public int getCurrX() {
- return IMPL.getCurrX(mScroller);
+ return mImpl.getCurrX(mScroller);
}
/**
@@ -283,21 +488,21 @@
* @return The new Y offset as an absolute distance from the origin.
*/
public int getCurrY() {
- return IMPL.getCurrY(mScroller);
+ return mImpl.getCurrY(mScroller);
}
/**
* @return The final X position for the scroll in progress, if known.
*/
public int getFinalX() {
- return IMPL.getFinalX(mScroller);
+ return mImpl.getFinalX(mScroller);
}
/**
* @return The final Y position for the scroll in progress, if known.
*/
public int getFinalY() {
- return IMPL.getFinalY(mScroller);
+ return mImpl.getFinalY(mScroller);
}
/**
@@ -311,7 +516,7 @@
* negative.
*/
public float getCurrVelocity() {
- return IMPL.getCurrVelocity(mScroller);
+ return mImpl.getCurrVelocity(mScroller);
}
/**
@@ -320,7 +525,7 @@
* new location.
*/
public boolean computeScrollOffset() {
- return IMPL.computeScrollOffset(mScroller);
+ return mImpl.computeScrollOffset(mScroller);
}
/**
@@ -338,7 +543,7 @@
* content up.
*/
public void startScroll(int startX, int startY, int dx, int dy) {
- IMPL.startScroll(mScroller, startX, startY, dx, dy);
+ mImpl.startScroll(mScroller, startX, startY, dx, dy);
}
/**
@@ -355,7 +560,7 @@
* @param duration Duration of the scroll in milliseconds.
*/
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
- IMPL.startScroll(mScroller, startX, startY, dx, dy, duration);
+ mImpl.startScroll(mScroller, startX, startY, dx, dy, duration);
}
/**
@@ -379,7 +584,7 @@
*/
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
- IMPL.fling(mScroller, startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
+ mImpl.fling(mScroller, startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
}
/**
@@ -407,7 +612,7 @@
*/
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY, int overX, int overY) {
- IMPL.fling(mScroller, startX, startY, velocityX, velocityY,
+ mImpl.fling(mScroller, startX, startY, velocityX, velocityY,
minX, maxX, minY, maxY, overX, overY);
}
@@ -416,7 +621,7 @@
* position.
*/
public void abortAnimation() {
- IMPL.abortAnimation(mScroller);
+ mImpl.abortAnimation(mScroller);
}
@@ -434,7 +639,7 @@
* desired distance from finalX. Absolute value - must be positive.
*/
public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
- IMPL.notifyHorizontalEdgeReached(mScroller, startX, finalX, overX);
+ mImpl.notifyHorizontalEdgeReached(mScroller, startX, finalX, overX);
}
/**
@@ -451,7 +656,7 @@
* desired distance from finalY. Absolute value - must be positive.
*/
public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
- IMPL.notifyVerticalEdgeReached(mScroller, startY, finalY, overY);
+ mImpl.notifyVerticalEdgeReached(mScroller, startY, finalY, overY);
}
/**
@@ -468,6 +673,6 @@
* interpolating back to a valid value.
*/
public boolean isOverScrolled() {
- return IMPL.isOverScrolled(mScroller);
+ return mImpl.isOverScrolled(mScroller);
}
}
diff --git a/v4/java/android/support/v4/widget/SlidingPaneLayout.java b/v4/java/android/support/v4/widget/SlidingPaneLayout.java
index 1c23994..391ba99 100644
--- a/v4/java/android/support/v4/widget/SlidingPaneLayout.java
+++ b/v4/java/android/support/v4/widget/SlidingPaneLayout.java
@@ -29,6 +29,7 @@
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.DrawableRes;
import android.support.v4.view.AccessibilityDelegateCompat;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
@@ -117,9 +118,14 @@
private int mCoveredFadeColor;
/**
- * Drawable used to draw the shadow between panes.
+ * Drawable used to draw the shadow between panes by default.
*/
- private Drawable mShadowDrawable;
+ private Drawable mShadowDrawableLeft;
+
+ /**
+ * Drawable used to draw the shadow between panes to support RTL (right to left language).
+ */
+ private Drawable mShadowDrawableRight;
/**
* The size of the overhang in pixels.
@@ -262,7 +268,6 @@
ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback());
- mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density);
}
@@ -345,8 +350,11 @@
}
void updateObscuredViewsVisibility(View panel) {
- final int leftBound = getPaddingLeft();
- final int rightBound = getWidth() - getPaddingRight();
+ final boolean isLayoutRtl = isLayoutRtlSupport();
+ final int startBound = isLayoutRtl ? (getWidth() - getPaddingRight()) :
+ getPaddingLeft();
+ final int endBound = isLayoutRtl ? getPaddingLeft() :
+ (getWidth() - getPaddingRight());
final int topBound = getPaddingTop();
final int bottomBound = getHeight() - getPaddingBottom();
final int left;
@@ -370,9 +378,11 @@
break;
}
- final int clampedChildLeft = Math.max(leftBound, child.getLeft());
+ final int clampedChildLeft = Math.max((isLayoutRtl ? endBound :
+ startBound), child.getLeft());
final int clampedChildTop = Math.max(topBound, child.getTop());
- final int clampedChildRight = Math.min(rightBound, child.getRight());
+ final int clampedChildRight = Math.min((isLayoutRtl ? startBound :
+ endBound), child.getRight());
final int clampedChildBottom = Math.min(bottomBound, child.getBottom());
final int vis;
if (clampedChildLeft >= left && clampedChildTop >= top &&
@@ -641,14 +651,19 @@
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
-
+ final boolean isLayoutRtl = isLayoutRtlSupport();
+ if (isLayoutRtl) {
+ mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
+ } else {
+ mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
+ }
final int width = r - l;
- final int paddingLeft = getPaddingLeft();
- final int paddingRight = getPaddingRight();
+ final int paddingStart = isLayoutRtl ? getPaddingRight() : getPaddingLeft();
+ final int paddingEnd = isLayoutRtl ? getPaddingLeft() : getPaddingRight();
final int paddingTop = getPaddingTop();
final int childCount = getChildCount();
- int xStart = paddingLeft;
+ int xStart = paddingStart;
int nextXStart = xStart;
if (mFirstLayout) {
@@ -670,12 +685,13 @@
if (lp.slideable) {
final int margin = lp.leftMargin + lp.rightMargin;
final int range = Math.min(nextXStart,
- width - paddingRight - mOverhangSize) - xStart - margin;
+ width - paddingEnd - mOverhangSize) - xStart - margin;
mSlideRange = range;
- lp.dimWhenOffset = xStart + lp.leftMargin + range + childWidth / 2 >
- width - paddingRight;
+ final int lpMargin = isLayoutRtl ? lp.rightMargin : lp.leftMargin;
+ lp.dimWhenOffset = xStart + lpMargin + range + childWidth / 2 >
+ width - paddingEnd;
final int pos = (int) (range * mSlideOffset);
- xStart += pos + lp.leftMargin;
+ xStart += pos + lpMargin;
mSlideOffset = (float) pos / mSlideRange;
} else if (mCanSlide && mParallaxBy != 0) {
offset = (int) ((1 - mSlideOffset) * mParallaxBy);
@@ -684,8 +700,16 @@
xStart = nextXStart;
}
- final int childLeft = xStart - offset;
- final int childRight = childLeft + childWidth;
+ final int childRight;
+ final int childLeft;
+ if (isLayoutRtl) {
+ childRight = width - xStart + offset;
+ childLeft = childRight - childWidth;
+ } else {
+ childLeft = xStart - offset;
+ childRight = childLeft + childWidth;
+ }
+
final int childTop = paddingTop;
final int childBottom = childTop + child.getMeasuredHeight();
child.layout(childLeft, paddingTop, childRight, childBottom);
@@ -918,11 +942,17 @@
mSlideOffset = 0;
return;
}
-
+ final boolean isLayoutRtl = isLayoutRtlSupport();
final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams();
- final int leftBound = getPaddingLeft() + lp.leftMargin;
- mSlideOffset = (float) (newLeft - leftBound) / mSlideRange;
+ int childWidth = mSlideableView.getWidth();
+ final int newStart = isLayoutRtl ? getWidth() - newLeft - childWidth : newLeft;
+
+ final int paddingStart = isLayoutRtl ? getPaddingRight() : getPaddingLeft();
+ final int lpMargin = isLayoutRtl ? lp.rightMargin : lp.leftMargin;
+ final int startBound = paddingStart + lpMargin;
+
+ mSlideOffset = (float) (newStart - startBound) / mSlideRange;
if (mParallaxBy != 0) {
parallaxOtherViews(mSlideOffset);
@@ -968,7 +998,11 @@
if (mCanSlide && !lp.slideable && mSlideableView != null) {
// Clip against the slider; no sense drawing what will immediately be covered.
canvas.getClipBounds(mTmpRect);
- mTmpRect.right = Math.min(mTmpRect.right, mSlideableView.getLeft());
+ if (isLayoutRtlSupport()) {
+ mTmpRect.left = Math.max(mTmpRect.left, mSlideableView.getRight());
+ } else {
+ mTmpRect.right = Math.min(mTmpRect.right, mSlideableView.getLeft());
+ }
canvas.clipRect(mTmpRect);
}
@@ -1016,10 +1050,18 @@
return false;
}
+ final boolean isLayoutRtl = isLayoutRtlSupport();
final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams();
- final int leftBound = getPaddingLeft() + lp.leftMargin;
- int x = (int) (leftBound + slideOffset * mSlideRange);
+ int x;
+ if (isLayoutRtl) {
+ int startBound = getPaddingRight() + lp.rightMargin;
+ int childWidth = mSlideableView.getWidth();
+ x = (int) (getWidth() - (startBound + slideOffset * mSlideRange + childWidth));
+ } else {
+ int startBound = getPaddingLeft() + lp.leftMargin;
+ x = (int) (startBound + slideOffset * mSlideRange);
+ }
if (mDragHelper.smoothSlideViewTo(mSlideableView, x, mSlideableView.getTop())) {
setAllChildrenVisible();
@@ -1042,13 +1084,35 @@
}
/**
+ * @deprecated Renamed to {@link #setShadowDrawableLeft(Drawable d)} to support LTR (left to
+ * right language) and {@link #setShadowDrawableRight(Drawable d)} to support RTL (right to left
+ * language) during opening/closing.
+ *
+ * @param d drawable to use as a shadow
+ */
+ @Deprecated
+ public void setShadowDrawable(Drawable d) {
+ setShadowDrawableLeft(d);
+ }
+
+ /**
* Set a drawable to use as a shadow cast by the right pane onto the left pane
* during opening/closing.
*
* @param d drawable to use as a shadow
*/
- public void setShadowDrawable(Drawable d) {
- mShadowDrawable = d;
+ public void setShadowDrawableLeft(Drawable d) {
+ mShadowDrawableLeft = d;
+ }
+
+ /**
+ * Set a drawable to use as a shadow cast by the left pane onto the right pane
+ * during opening/closing to support right to left language.
+ *
+ * @param d drawable to use as a shadow
+ */
+ public void setShadowDrawableRight(Drawable d) {
+ mShadowDrawableRight = d;
}
/**
@@ -1057,32 +1121,72 @@
*
* @param resId Resource ID of a drawable to use
*/
- public void setShadowResource(int resId) {
+ @Deprecated
+ public void setShadowResource(@DrawableRes int resId) {
setShadowDrawable(getResources().getDrawable(resId));
}
+ /**
+ * Set a drawable to use as a shadow cast by the right pane onto the left pane
+ * during opening/closing.
+ *
+ * @param resId Resource ID of a drawable to use
+ */
+ public void setShadowResourceLeft(int resId) {
+ setShadowDrawableLeft(getResources().getDrawable(resId));
+ }
+
+ /**
+ * Set a drawable to use as a shadow cast by the left pane onto the right pane
+ * during opening/closing to support right to left language.
+ *
+ * @param resId Resource ID of a drawable to use
+ */
+ public void setShadowResourceRight(int resId) {
+ setShadowDrawableRight(getResources().getDrawable(resId));
+ }
+
+
@Override
public void draw(Canvas c) {
super.draw(c);
+ final boolean isLayoutRtl = isLayoutRtlSupport();
+ Drawable shadowDrawable;
+ if (isLayoutRtl) {
+ shadowDrawable = mShadowDrawableRight;
+ } else {
+ shadowDrawable = mShadowDrawableLeft;
+ }
final View shadowView = getChildCount() > 1 ? getChildAt(1) : null;
- if (shadowView == null || mShadowDrawable == null) {
+ if (shadowView == null || shadowDrawable == null) {
// No need to draw a shadow if we don't have one.
return;
}
- final int shadowWidth = mShadowDrawable.getIntrinsicWidth();
- final int right = shadowView.getLeft();
final int top = shadowView.getTop();
final int bottom = shadowView.getBottom();
- final int left = right - shadowWidth;
- mShadowDrawable.setBounds(left, top, right, bottom);
- mShadowDrawable.draw(c);
+
+ final int shadowWidth = shadowDrawable.getIntrinsicWidth();
+ final int left;
+ final int right;
+ if (isLayoutRtlSupport()) {
+ left = shadowView.getRight();
+ right = left + shadowWidth;
+ } else {
+ right = shadowView.getLeft();
+ left = right - shadowWidth;
+ }
+
+ shadowDrawable.setBounds(left, top, right, bottom);
+ shadowDrawable.draw(c);
}
private void parallaxOtherViews(float slideOffset) {
+ final boolean isLayoutRtl = isLayoutRtlSupport();
final LayoutParams slideLp = (LayoutParams) mSlideableView.getLayoutParams();
- final boolean dimViews = slideLp.dimWhenOffset && slideLp.leftMargin <= 0;
+ final boolean dimViews = slideLp.dimWhenOffset &&
+ (isLayoutRtl ? slideLp.rightMargin : slideLp.leftMargin) <= 0;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View v = getChildAt(i);
@@ -1093,10 +1197,11 @@
final int newOffset = (int) ((1 - slideOffset) * mParallaxBy);
final int dx = oldOffset - newOffset;
- v.offsetLeftAndRight(dx);
+ v.offsetLeftAndRight(isLayoutRtl ? -dx : dx);
if (dimViews) {
- dimChildView(v, 1 - mParallaxOffset, mCoveredFadeColor);
+ dimChildView(v, isLayoutRtl ? mParallaxOffset - 1 :
+ 1 - mParallaxOffset, mCoveredFadeColor);
}
}
}
@@ -1132,7 +1237,7 @@
}
}
- return checkV && ViewCompat.canScrollHorizontally(v, -dx);
+ return checkV && ViewCompat.canScrollHorizontally(v, (isLayoutRtlSupport() ? dx : -dx));
}
boolean isDimmed(View child) {
@@ -1228,9 +1333,20 @@
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams();
- int left = getPaddingLeft() + lp.leftMargin;
- if (xvel > 0 || (xvel == 0 && mSlideOffset > 0.5f)) {
- left += mSlideRange;
+
+ int left;
+ if (isLayoutRtlSupport()) {
+ int startToRight = getPaddingRight() + lp.rightMargin;
+ if (xvel < 0 || (xvel == 0 && mSlideOffset > 0.5f)) {
+ startToRight += mSlideRange;
+ }
+ int childWidth = mSlideableView.getWidth();
+ left = getWidth() - startToRight - childWidth;
+ } else {
+ left = getPaddingLeft() + lp.leftMargin;
+ if (xvel > 0 || (xvel == 0 && mSlideOffset > 0.5f)) {
+ left += mSlideRange;
+ }
}
mDragHelper.settleCapturedViewAt(left, releasedChild.getTop());
invalidate();
@@ -1244,11 +1360,18 @@
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams();
- final int leftBound = getPaddingLeft() + lp.leftMargin;
- final int rightBound = leftBound + mSlideRange;
- final int newLeft = Math.min(Math.max(left, leftBound), rightBound);
-
+ final int newLeft;
+ if (isLayoutRtlSupport()) {
+ int startBound = getWidth() -
+ (getPaddingRight() + lp.rightMargin + mSlideableView.getWidth());
+ int endBound = startBound - mSlideRange;
+ newLeft = Math.max(Math.min(left, startBound), endBound);
+ } else {
+ int startBound = getPaddingLeft() + lp.leftMargin;
+ int endBound = startBound + mSlideRange;
+ newLeft = Math.min(Math.max(left, startBound), endBound);
+ }
return newLeft;
}
@@ -1514,4 +1637,8 @@
mPostedRunnables.remove(this);
}
}
+
+ private boolean isLayoutRtlSupport() {
+ return ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
+ }
}
diff --git a/v4/java/android/support/v4/widget/SwipeRefreshLayout.java b/v4/java/android/support/v4/widget/SwipeRefreshLayout.java
index ca68acd..725a418 100644
--- a/v4/java/android/support/v4/widget/SwipeRefreshLayout.java
+++ b/v4/java/android/support/v4/widget/SwipeRefreshLayout.java
@@ -20,9 +20,11 @@
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
+import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
+import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
@@ -56,28 +58,35 @@
* refresh of the content wherever this gesture is used.</p>
*/
public class SwipeRefreshLayout extends ViewGroup {
+ private static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName();
+
private static final long RETURN_TO_ORIGINAL_POSITION_TIMEOUT = 300;
private static final float ACCELERATE_INTERPOLATION_FACTOR = 1.5f;
private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
private static final float PROGRESS_BAR_HEIGHT = 4;
private static final float MAX_SWIPE_DISTANCE_FACTOR = .6f;
private static final int REFRESH_TRIGGER_DISTANCE = 120;
+ private static final int INVALID_POINTER = -1;
private SwipeProgressBar mProgressBar; //the thing that shows progress is going
private View mTarget; //the content that gets pulled down
private int mOriginalOffsetTop;
private OnRefreshListener mListener;
- private MotionEvent mDownEvent;
private int mFrom;
private boolean mRefreshing = false;
private int mTouchSlop;
private float mDistanceToTriggerSync = -1;
- private float mPrevY;
private int mMediumAnimationDuration;
private float mFromPercentage = 0;
private float mCurrPercentage = 0;
private int mProgressBarHeight;
private int mCurrentTargetOffsetTop;
+
+ private float mInitialMotionY;
+ private float mLastMotionY;
+ private boolean mIsBeingDragged;
+ private int mActivePointerId = INVALID_POINTER;
+
// Target is returning to its start offset because it was cancelled or a
// refresh was triggered.
private boolean mReturningToStart;
@@ -255,23 +264,33 @@
}
/**
+ * @deprecated Use {@link #setColorSchemeResources(int, int, int, int)}
+ */
+ @Deprecated
+ public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) {
+ setColorSchemeResources(colorRes1, colorRes2, colorRes3, colorRes4);
+ }
+
+ /**
+ * Set the four colors used in the progress animation from color resources.
+ * The first color will also be the color of the bar that grows in response
+ * to a user swipe gesture.
+ */
+ public void setColorSchemeResources(int colorRes1, int colorRes2, int colorRes3,
+ int colorRes4) {
+ final Resources res = getResources();
+ setColorSchemeColors(res.getColor(colorRes1), res.getColor(colorRes2),
+ res.getColor(colorRes3), res.getColor(colorRes4));
+ }
+
+ /**
* Set the four colors used in the progress animation. The first color will
* also be the color of the bar that grows in response to a user swipe
* gesture.
- *
- * @param colorRes1 Color resource.
- * @param colorRes2 Color resource.
- * @param colorRes3 Color resource.
- * @param colorRes4 Color resource.
*/
- public void setColorScheme(int colorRes1, int colorRes2, int colorRes3, int colorRes4) {
+ public void setColorSchemeColors(int color1, int color2, int color3, int color4) {
ensureTarget();
- final Resources res = getResources();
- final int color1 = res.getColor(colorRes1);
- final int color2 = res.getColor(colorRes2);
- final int color3 = res.getColor(colorRes3);
- final int color4 = res.getColor(colorRes4);
- mProgressBar.setColorScheme(color1, color2, color3,color4);
+ mProgressBar.setColorScheme(color1, color2, color3, color4);
}
/**
@@ -363,14 +382,59 @@
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
ensureTarget();
- boolean handled = false;
- if (mReturningToStart && ev.getAction() == MotionEvent.ACTION_DOWN) {
+
+ final int action = MotionEventCompat.getActionMasked(ev);
+
+ if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
- if (isEnabled() && !mReturningToStart && !canChildScrollUp()) {
- handled = onTouchEvent(ev);
+
+ if (!isEnabled() || mReturningToStart || canChildScrollUp()) {
+ // Fail fast if we're not in a state where a swipe is possible
+ return false;
}
- return !handled ? super.onInterceptTouchEvent(ev) : handled;
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mLastMotionY = mInitialMotionY = ev.getY();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+ mIsBeingDragged = false;
+ mCurrPercentage = 0;
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ if (mActivePointerId == INVALID_POINTER) {
+ Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
+ return false;
+ }
+
+ final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ if (pointerIndex < 0) {
+ Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
+ return false;
+ }
+
+ final float y = MotionEventCompat.getY(ev, pointerIndex);
+ final float yDiff = y - mInitialMotionY;
+ if (yDiff > mTouchSlop) {
+ mLastMotionY = y;
+ mIsBeingDragged = true;
+ }
+ break;
+
+ case MotionEventCompat.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ mIsBeingDragged = false;
+ mCurrPercentage = 0;
+ mActivePointerId = INVALID_POINTER;
+ break;
+ }
+
+ return mIsBeingDragged;
}
@Override
@@ -379,59 +443,84 @@
}
@Override
- public boolean onTouchEvent(MotionEvent event) {
- final int action = event.getAction();
- boolean handled = false;
+ public boolean onTouchEvent(MotionEvent ev) {
+ final int action = MotionEventCompat.getActionMasked(ev);
+
+ if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
+ mReturningToStart = false;
+ }
+
+ if (!isEnabled() || mReturningToStart || canChildScrollUp()) {
+ // Fail fast if we're not in a state where a swipe is possible
+ return false;
+ }
+
switch (action) {
case MotionEvent.ACTION_DOWN:
+ mLastMotionY = mInitialMotionY = ev.getY();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+ mIsBeingDragged = false;
mCurrPercentage = 0;
- mDownEvent = MotionEvent.obtain(event);
- mPrevY = mDownEvent.getY();
break;
+
case MotionEvent.ACTION_MOVE:
- if (mDownEvent != null && !mReturningToStart) {
- final float eventY = event.getY();
- float yDiff = eventY - mDownEvent.getY();
- if (yDiff > mTouchSlop) {
- // User velocity passed min velocity; trigger a refresh
- if (yDiff > mDistanceToTriggerSync) {
- // User movement passed distance; trigger a refresh
- startRefresh();
- handled = true;
- break;
+ final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ if (pointerIndex < 0) {
+ Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
+ return false;
+ }
+
+ final float y = MotionEventCompat.getY(ev, pointerIndex);
+ final float yDiff = y - mInitialMotionY;
+
+ if (!mIsBeingDragged && yDiff > mTouchSlop) {
+ mIsBeingDragged = true;
+ }
+
+ if (mIsBeingDragged) {
+ // User velocity passed min velocity; trigger a refresh
+ if (yDiff > mDistanceToTriggerSync) {
+ // User movement passed distance; trigger a refresh
+ startRefresh();
+ } else {
+ // Just track the user's movement
+ setTriggerPercentage(
+ mAccelerateInterpolator.getInterpolation(
+ yDiff / mDistanceToTriggerSync));
+ updateContentOffsetTop((int) (yDiff));
+ if (mLastMotionY > y && mTarget.getTop() == getPaddingTop()) {
+ // If the user puts the view back at the top, we
+ // don't need to. This shouldn't be considered
+ // cancelling the gesture as the user can restart from the top.
+ removeCallbacks(mCancel);
} else {
- // Just track the user's movement
- setTriggerPercentage(
- mAccelerateInterpolator.getInterpolation(
- yDiff / mDistanceToTriggerSync));
- float offsetTop = yDiff;
- if (mPrevY > eventY) {
- offsetTop = yDiff - mTouchSlop;
- }
- updateContentOffsetTop((int) (offsetTop));
- if (mPrevY > eventY && (mTarget.getTop() < mTouchSlop)) {
- // If the user puts the view back at the top, we
- // don't need to. This shouldn't be considered
- // cancelling the gesture as the user can restart from the top.
- removeCallbacks(mCancel);
- } else {
- updatePositionTimeout();
- }
- mPrevY = event.getY();
- handled = true;
+ updatePositionTimeout();
}
}
+ mLastMotionY = y;
}
break;
+
+ case MotionEventCompat.ACTION_POINTER_DOWN: {
+ final int index = MotionEventCompat.getActionIndex(ev);
+ mLastMotionY = MotionEventCompat.getY(ev, index);
+ mActivePointerId = MotionEventCompat.getPointerId(ev, index);
+ break;
+ }
+
+ case MotionEventCompat.ACTION_POINTER_UP:
+ onSecondaryPointerUp(ev);
+ break;
+
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
- if (mDownEvent != null) {
- mDownEvent.recycle();
- mDownEvent = null;
- }
- break;
+ mIsBeingDragged = false;
+ mCurrPercentage = 0;
+ mActivePointerId = INVALID_POINTER;
+ return false;
}
- return handled;
+
+ return true;
}
private void startRefresh() {
@@ -461,6 +550,18 @@
postDelayed(mCancel, RETURN_TO_ORIGINAL_POSITION_TIMEOUT);
}
+ private void onSecondaryPointerUp(MotionEvent ev) {
+ final int pointerIndex = MotionEventCompat.getActionIndex(ev);
+ final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mLastMotionY = MotionEventCompat.getY(ev, newPointerIndex);
+ mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
+ }
+ }
+
/**
* Classes that wish to be notified when the swipe gesture correctly
* triggers a refresh should implement this interface.
@@ -486,4 +587,4 @@
public void onAnimationRepeat(Animation animation) {
}
}
-}
\ No newline at end of file
+}
diff --git a/v4/jellybean-mr1/android/support/v4/view/ViewCompatJellybeanMr1.java b/v4/jellybean-mr1/android/support/v4/view/ViewCompatJellybeanMr1.java
index be7192d..63552e4 100644
--- a/v4/jellybean-mr1/android/support/v4/view/ViewCompatJellybeanMr1.java
+++ b/v4/jellybean-mr1/android/support/v4/view/ViewCompatJellybeanMr1.java
@@ -43,4 +43,16 @@
public static void setLayoutDirection(View view, int layoutDirection) {
view.setLayoutDirection(layoutDirection);
}
+
+ public static int getPaddingStart(View view) {
+ return view.getPaddingStart();
+ }
+
+ public static int getPaddingEnd(View view) {
+ return view.getPaddingEnd();
+ }
+
+ public static void setPaddingRelative(View view, int start, int top, int end, int bottom) {
+ view.setPaddingRelative(start, top, end, bottom);
+ }
}
diff --git a/v4/jellybean/android/support/v4/app/NotificationBuilderWithActions.java b/v4/jellybean/android/support/v4/app/NotificationBuilderWithActions.java
new file mode 100644
index 0000000..656dc43
--- /dev/null
+++ b/v4/jellybean/android/support/v4/app/NotificationBuilderWithActions.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.app;
+
+import android.app.PendingIntent;
+import android.graphics.Bitmap;
+
+/**
+ * Interface implemented by notification compat builders that support adding actions.
+ */
+interface NotificationBuilderWithActions {
+ public void addAction(int icon, CharSequence title, PendingIntent intent);
+}
diff --git a/v4/jellybean/android/support/v4/app/NotificationCompatJellybean.java b/v4/jellybean/android/support/v4/app/NotificationCompatJellybean.java
index 8fa7e98..b5968a5 100644
--- a/v4/jellybean/android/support/v4/app/NotificationCompatJellybean.java
+++ b/v4/jellybean/android/support/v4/app/NotificationCompatJellybean.java
@@ -20,75 +20,126 @@
import android.app.PendingIntent;
import android.content.Context;
import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.util.Log;
import android.widget.RemoteViews;
+
+import java.lang.reflect.Field;
import java.util.ArrayList;
class NotificationCompatJellybean {
- private Notification.Builder b;
- public NotificationCompatJellybean(Context context, Notification n,
- CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
- RemoteViews tickerView, int number,
- PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
- int mProgressMax, int mProgress, boolean mProgressIndeterminate,
- boolean useChronometer, int priority, CharSequence subText) {
- b = new Notification.Builder(context)
- .setWhen(n.when)
- .setSmallIcon(n.icon, n.iconLevel)
- .setContent(n.contentView)
- .setTicker(n.tickerText, tickerView)
- .setSound(n.sound, n.audioStreamType)
- .setVibrate(n.vibrate)
- .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS)
- .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0)
- .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0)
- .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0)
- .setDefaults(n.defaults)
- .setContentTitle(contentTitle)
- .setContentText(contentText)
- .setSubText(subText)
- .setContentInfo(contentInfo)
- .setContentIntent(contentIntent)
- .setDeleteIntent(n.deleteIntent)
- .setFullScreenIntent(fullScreenIntent,
- (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0)
- .setLargeIcon(largeIcon)
- .setNumber(number)
- .setUsesChronometer(useChronometer)
- .setPriority(priority)
- .setProgress(mProgressMax, mProgress, mProgressIndeterminate);
+ public static final String TAG = "NotificationCompat";
+
+ /** Extras key used for Jellybean SDK and below. */
+ static final String EXTRA_LOCAL_ONLY = "android.support.localOnly";
+
+ private static final Object sExtrasLock = new Object();
+ private static Field sExtrasField;
+ private static boolean sExtrasFieldAccessFailed;
+
+ public static class Builder implements NotificationBuilderWithBuilderAccessor,
+ NotificationBuilderWithActions {
+ private Notification.Builder b;
+ private final boolean mLocalOnly;
+ private final Bundle mExtras;
+
+ public Builder(Context context, Notification n,
+ CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
+ RemoteViews tickerView, int number,
+ PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
+ int mProgressMax, int mProgress, boolean mProgressIndeterminate,
+ boolean useChronometer, int priority, CharSequence subText, boolean localOnly,
+ Bundle extras) {
+ b = new Notification.Builder(context)
+ .setWhen(n.when)
+ .setSmallIcon(n.icon, n.iconLevel)
+ .setContent(n.contentView)
+ .setTicker(n.tickerText, tickerView)
+ .setSound(n.sound, n.audioStreamType)
+ .setVibrate(n.vibrate)
+ .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS)
+ .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0)
+ .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0)
+ .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0)
+ .setDefaults(n.defaults)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setSubText(subText)
+ .setContentInfo(contentInfo)
+ .setContentIntent(contentIntent)
+ .setDeleteIntent(n.deleteIntent)
+ .setFullScreenIntent(fullScreenIntent,
+ (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0)
+ .setLargeIcon(largeIcon)
+ .setNumber(number)
+ .setUsesChronometer(useChronometer)
+ .setPriority(priority)
+ .setProgress(mProgressMax, mProgress, mProgressIndeterminate);
+ mLocalOnly = localOnly;
+ mExtras = extras;
+ }
+
+ @Override
+ public void addAction(int icon, CharSequence title, PendingIntent intent) {
+ b.addAction(icon, title, intent);
+ }
+
+ @Override
+ public Notification.Builder getBuilder() {
+ return b;
+ }
+
+ public Notification build() {
+ Notification notif = b.build();
+ if (mExtras != null) {
+ // Merge in developer provided extras, but let the values already set
+ // for keys take precedence.
+ Bundle extras = getExtras(notif);
+ Bundle mergeBundle = new Bundle(mExtras);
+ for (String key : mExtras.keySet()) {
+ if (extras.containsKey(key)) {
+ mergeBundle.remove(key);
+ }
+ }
+ extras.putAll(mergeBundle);
+ }
+ if (mLocalOnly) {
+ getExtras(notif).putBoolean(EXTRA_LOCAL_ONLY, mLocalOnly);
+ }
+ return notif;
+ }
}
- public void addAction(int icon, CharSequence title, PendingIntent intent) {
- b.addAction(icon, title, intent);
- }
-
- public void addBigTextStyle(CharSequence bigContentTitle, boolean useSummary,
+ public static void addBigTextStyle(NotificationBuilderWithBuilderAccessor b,
+ CharSequence bigContentTitle, boolean useSummary,
CharSequence summaryText, CharSequence bigText) {
- Notification.BigTextStyle style = new Notification.BigTextStyle(b)
+ Notification.BigTextStyle style = new Notification.BigTextStyle(b.getBuilder())
.setBigContentTitle(bigContentTitle)
.bigText(bigText);
if (useSummary) {
style.setSummaryText(summaryText);
- }
+ }
}
- public void addBigPictureStyle(CharSequence bigContentTitle, boolean useSummary,
+ public static void addBigPictureStyle(NotificationBuilderWithBuilderAccessor b,
+ CharSequence bigContentTitle, boolean useSummary,
CharSequence summaryText, Bitmap bigPicture, Bitmap bigLargeIcon,
boolean bigLargeIconSet) {
- Notification.BigPictureStyle style = new Notification.BigPictureStyle(b)
- .setBigContentTitle(bigContentTitle)
- .bigPicture(bigPicture);
- if (bigLargeIconSet) {
- style.bigLargeIcon(bigLargeIcon);
- }
+ Notification.BigPictureStyle style = new Notification.BigPictureStyle(b.getBuilder())
+ .setBigContentTitle(bigContentTitle)
+ .bigPicture(bigPicture);
+ if (bigLargeIconSet) {
+ style.bigLargeIcon(bigLargeIcon);
+ }
if (useSummary) {
style.setSummaryText(summaryText);
- }
+ }
}
- public void addInboxStyle(CharSequence bigContentTitle, boolean useSummary,
+ public static void addInboxStyle(NotificationBuilderWithBuilderAccessor b,
+ CharSequence bigContentTitle, boolean useSummary,
CharSequence summaryText, ArrayList<CharSequence> texts) {
- Notification.InboxStyle style = new Notification.InboxStyle(b)
+ Notification.InboxStyle style = new Notification.InboxStyle(b.getBuilder())
.setBigContentTitle(bigContentTitle);
if (useSummary) {
style.setSummaryText(summaryText);
@@ -98,7 +149,43 @@
}
}
- public Notification build() {
- return b.build();
+ /**
+ * Get the extras Bundle from a notification using reflection. Extras were present in
+ * Jellybean notifications, but the field was private until KitKat.
+ */
+ public static Bundle getExtras(Notification notif) {
+ synchronized (sExtrasLock) {
+ if (sExtrasFieldAccessFailed) {
+ return null;
+ }
+ try {
+ if (sExtrasField == null) {
+ Field extrasField = Notification.class.getDeclaredField("extras");
+ if (!Bundle.class.isAssignableFrom(extrasField.getType())) {
+ Log.e(TAG, "Notification.extras field is not of type Bundle");
+ sExtrasFieldAccessFailed = true;
+ return null;
+ }
+ extrasField.setAccessible(true);
+ sExtrasField = extrasField;
+ }
+ Bundle extras = (Bundle) sExtrasField.get(notif);
+ if (extras == null) {
+ extras = new Bundle();
+ sExtrasField.set(notif, extras);
+ }
+ return extras;
+ } catch (IllegalAccessException e) {
+ Log.e(TAG, "Unable to access notification extras", e);
+ } catch (NoSuchFieldException e) {
+ Log.e(TAG, "Unable to access notification extras", e);
+ }
+ sExtrasFieldAccessFailed = true;
+ return null;
+ }
+ }
+
+ public static boolean getLocalOnly(Notification notif) {
+ return getExtras(notif).getBoolean(EXTRA_LOCAL_ONLY);
}
}
diff --git a/v4/jellybean/android/support/v4/view/ViewCompatJB.java b/v4/jellybean/android/support/v4/view/ViewCompatJB.java
index 7456d28..2db62fc 100644
--- a/v4/jellybean/android/support/v4/view/ViewCompatJB.java
+++ b/v4/jellybean/android/support/v4/view/ViewCompatJB.java
@@ -69,4 +69,12 @@
public static ViewParent getParentForAccessibility(View view) {
return view.getParentForAccessibility();
}
+
+ public static int getMinimumWidth(View view) {
+ return view.getMinimumWidth();
+ }
+
+ public static int getMinimumHeight(View view) {
+ return view.getMinimumHeight();
+ }
}
diff --git a/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.java b/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.java
new file mode 100644
index 0000000..d6959e3
--- /dev/null
+++ b/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.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.v4.app;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.widget.RemoteViews;
+
+class NotificationCompatKitKat {
+ public static class Builder implements NotificationBuilderWithBuilderAccessor,
+ NotificationBuilderWithActions {
+ private Notification.Builder b;
+ private Bundle mExtras;
+
+ public Builder(Context context, Notification n,
+ CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
+ RemoteViews tickerView, int number,
+ PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
+ int mProgressMax, int mProgress, boolean mProgressIndeterminate,
+ boolean useChronometer, int priority, CharSequence subText, boolean localOnly,
+ Bundle extras) {
+ b = new Notification.Builder(context)
+ .setWhen(n.when)
+ .setSmallIcon(n.icon, n.iconLevel)
+ .setContent(n.contentView)
+ .setTicker(n.tickerText, tickerView)
+ .setSound(n.sound, n.audioStreamType)
+ .setVibrate(n.vibrate)
+ .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS)
+ .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0)
+ .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0)
+ .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0)
+ .setDefaults(n.defaults)
+ .setContentTitle(contentTitle)
+ .setContentText(contentText)
+ .setSubText(subText)
+ .setContentInfo(contentInfo)
+ .setContentIntent(contentIntent)
+ .setDeleteIntent(n.deleteIntent)
+ .setFullScreenIntent(fullScreenIntent,
+ (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0)
+ .setLargeIcon(largeIcon)
+ .setNumber(number)
+ .setUsesChronometer(useChronometer)
+ .setPriority(priority)
+ .setProgress(mProgressMax, mProgress, mProgressIndeterminate);
+ mExtras = extras;
+ if (localOnly) {
+ getExtras().putBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY, localOnly);
+ }
+ }
+
+ @Override
+ public void addAction(int icon, CharSequence title, PendingIntent intent) {
+ b.addAction(icon, title, intent);
+ }
+
+ @Override
+ public Notification.Builder getBuilder() {
+ return b;
+ }
+
+ public Notification build() {
+ if (mExtras != null) {
+ b.setExtras(mExtras);
+ }
+ return b.build();
+ }
+
+ private Bundle getExtras() {
+ if (mExtras == null) {
+ mExtras = new Bundle();
+ }
+ return mExtras;
+ }
+ }
+
+ public static Bundle getExtras(Notification notif) {
+ return notif.extras;
+ }
+
+ public static boolean getLocalOnly(Notification notif) {
+ return getExtras(notif).getBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY);
+ }
+}
diff --git a/v4/tests/java/android/support/v4/widget/DonutScrollerCompatTest.java b/v4/tests/java/android/support/v4/widget/DonutScrollerCompatTest.java
new file mode 100644
index 0000000..dfa7e68
--- /dev/null
+++ b/v4/tests/java/android/support/v4/widget/DonutScrollerCompatTest.java
@@ -0,0 +1,25 @@
+/*
+ * 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.widget;
+
+import android.os.Build;
+
+public class DonutScrollerCompatTest extends ScrollerCompatTestBase {
+
+ public DonutScrollerCompatTest() {
+ super(Build.VERSION_CODES.DONUT);
+ }
+}
diff --git a/v4/tests/java/android/support/v4/widget/GingerbreadScrollerCompatTest.java b/v4/tests/java/android/support/v4/widget/GingerbreadScrollerCompatTest.java
new file mode 100644
index 0000000..a5c96b0
--- /dev/null
+++ b/v4/tests/java/android/support/v4/widget/GingerbreadScrollerCompatTest.java
@@ -0,0 +1,25 @@
+/*
+ * 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.widget;
+
+import android.os.Build;
+
+public class GingerbreadScrollerCompatTest extends ScrollerCompatTestBase {
+
+ public GingerbreadScrollerCompatTest() {
+ super(Build.VERSION_CODES.GINGERBREAD);
+ }
+}
diff --git a/v4/tests/java/android/support/v4/widget/IcsScrollerCompatTest.java b/v4/tests/java/android/support/v4/widget/IcsScrollerCompatTest.java
new file mode 100644
index 0000000..ccdc68d
--- /dev/null
+++ b/v4/tests/java/android/support/v4/widget/IcsScrollerCompatTest.java
@@ -0,0 +1,25 @@
+/*
+ * 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.widget;
+
+import android.os.Build;
+
+public class IcsScrollerCompatTest extends ScrollerCompatTestBase {
+
+ public IcsScrollerCompatTest() {
+ super(Build.VERSION_CODES.ICE_CREAM_SANDWICH);
+ }
+}
diff --git a/v4/tests/java/android/support/v4/widget/ScrollerCompatTestBase.java b/v4/tests/java/android/support/v4/widget/ScrollerCompatTestBase.java
new file mode 100644
index 0000000..93ab8bf
--- /dev/null
+++ b/v4/tests/java/android/support/v4/widget/ScrollerCompatTestBase.java
@@ -0,0 +1,62 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.view.animation.Interpolator;
+
+import java.lang.reflect.Constructor;
+
+abstract public class ScrollerCompatTestBase extends AndroidTestCase {
+
+ private final int mApiLevel;
+
+ private ScrollerCompat mScroller;
+
+ public ScrollerCompatTestBase(int apiLevel) {
+ mApiLevel = apiLevel;
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ Constructor<ScrollerCompat> constructor = ScrollerCompat.class
+ .getDeclaredConstructor(int.class, Context.class, Interpolator.class);
+ constructor.setAccessible(true);
+ mScroller = constructor.newInstance(mApiLevel, getContext(), null);
+ }
+
+ public void testTargetReached() throws InterruptedException {
+ mScroller.fling(0, 0, 0, 1000,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
+ int target = mScroller.getFinalY();
+ while (mScroller.computeScrollOffset()) {
+ Thread.sleep(100);
+ }
+ assertEquals("given enough time, scroller should reach target position", target,
+ mScroller.getCurrY());
+ }
+
+ public void testAbort() throws InterruptedException {
+ mScroller.fling(0, 0, 0, 10000,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
+ assertTrue("Scroller should have some offset", mScroller.computeScrollOffset());
+ mScroller.abortAnimation();
+ assertFalse("Scroller should clear offset after being aborted",
+ mScroller.computeScrollOffset());
+ }
+}
diff --git a/v7/appcompat/Android.mk b/v7/appcompat/Android.mk
index 82e4816..a9ab60d 100644
--- a/v7/appcompat/Android.mk
+++ b/v7/appcompat/Android.mk
@@ -20,7 +20,7 @@
# in their makefiles to include the resources in their package.
include $(CLEAR_VARS)
LOCAL_MODULE := android-support-v7-appcompat
-LOCAL_SDK_VERSION := 19
+LOCAL_SDK_VERSION := current
LOCAL_SRC_FILES := $(call all-java-files-under,src)
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_JAVA_LIBRARIES += android-support-v4
diff --git a/v7/appcompat/build.gradle b/v7/appcompat/build.gradle
index d8aff0c..26a3660 100644
--- a/v7/appcompat/build.gradle
+++ b/v7/appcompat/build.gradle
@@ -7,7 +7,7 @@
}
android {
- compileSdkVersion 19
+ compileSdkVersion 'current'
buildToolsVersion "19.0.1"
sourceSets {
diff --git a/v7/appcompat/project.properties b/v7/appcompat/project.properties
index dfa4dd0..91d2b02 100644
--- a/v7/appcompat/project.properties
+++ b/v7/appcompat/project.properties
@@ -11,5 +11,5 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
-target=android-16
+target=android-19
android.library=true
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBar.java b/v7/appcompat/src/android/support/v7/app/ActionBar.java
index a8a6383..1ff246c 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBar.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBar.java
@@ -16,9 +16,17 @@
package android.support.v7.app;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
+import android.support.annotation.LayoutRes;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
@@ -69,6 +77,11 @@
*/
public abstract class ActionBar {
+ /** @hide */
+ @IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface NavigationMode {}
+
/**
* Standard navigation mode. Consists of either a logo or icon and title text with an optional
* subtitle. Clicking any of these elements will dispatch onOptionsItemSelected to the host
@@ -88,6 +101,17 @@
*/
public static final int NAVIGATION_MODE_TABS = 2;
+ /** @hide */
+ @IntDef(flag=true, value={
+ DISPLAY_USE_LOGO,
+ DISPLAY_SHOW_HOME,
+ DISPLAY_HOME_AS_UP,
+ DISPLAY_SHOW_TITLE,
+ DISPLAY_SHOW_CUSTOM
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface DisplayOptions {}
+
/**
* Use logo instead of icon if available. This flag will cause appropriate navigation modes to
* use a wider logo in place of the standard icon.
@@ -185,7 +209,7 @@
* @param resId Resource ID of a layout to inflate into the ActionBar.
* @see #setDisplayOptions(int, int)
*/
- public abstract void setCustomView(int resId);
+ public abstract void setCustomView(@LayoutRes int resId);
/**
* Set the icon to display in the 'home' section of the action bar. The action bar will use an
@@ -198,7 +222,7 @@
* @see #setDisplayUseLogoEnabled(boolean)
* @see #setDisplayShowHomeEnabled(boolean)
*/
- public abstract void setIcon(int resId);
+ public abstract void setIcon(@DrawableRes int resId);
/**
* Set the icon to display in the 'home' section of the action bar. The action bar will use an
@@ -224,7 +248,7 @@
* @see #setDisplayUseLogoEnabled(boolean)
* @see #setDisplayShowHomeEnabled(boolean)
*/
- public abstract void setLogo(int resId);
+ public abstract void setLogo(@DrawableRes int resId);
/**
* Set the logo to display in the 'home' section of the action bar. The action bar will use a
@@ -295,7 +319,7 @@
* @see #setTitle(CharSequence)
* @see #setDisplayOptions(int, int)
*/
- public abstract void setTitle(int resId);
+ public abstract void setTitle(@StringRes int resId);
/**
* Set the action bar's subtitle. This will only be displayed if {@link #DISPLAY_SHOW_TITLE} is
@@ -305,7 +329,7 @@
* @see #setSubtitle(int)
* @see #setDisplayOptions(int, int)
*/
- public abstract void setSubtitle(CharSequence subtitle);
+ public abstract void setSubtitle(@Nullable CharSequence subtitle);
/**
* Set the action bar's subtitle. This will only be displayed if {@link #DISPLAY_SHOW_TITLE} is
@@ -315,7 +339,7 @@
* @see #setSubtitle(CharSequence)
* @see #setDisplayOptions(int, int)
*/
- public abstract void setSubtitle(int resId);
+ public abstract void setSubtitle(@StringRes int resId);
/**
* Set display options. This changes all display option bits at once. To change a limited subset
@@ -324,7 +348,7 @@
* @param options A combination of the bits defined by the DISPLAY_ constants defined in
* ActionBar.
*/
- public abstract void setDisplayOptions(int options);
+ public abstract void setDisplayOptions(@DisplayOptions int options);
/**
* Set selected display options. Only the options specified by mask will be changed. To change
@@ -339,7 +363,7 @@
* ActionBar.
* @param mask A bit mask declaring which display options should be changed.
*/
- public abstract void setDisplayOptions(int options, int mask);
+ public abstract void setDisplayOptions(@DisplayOptions int options, int mask);
/**
* Set whether to display the activity logo rather than the activity icon. A logo is often a
@@ -442,6 +466,7 @@
*
* @return The current ActionBar title or null.
*/
+ @Nullable
public abstract CharSequence getTitle();
/**
@@ -450,6 +475,7 @@
*
* @return The current ActionBar subtitle or null.
*/
+ @Nullable
public abstract CharSequence getSubtitle();
/**
@@ -464,6 +490,7 @@
*
* @return The current navigation mode.
*/
+ @NavigationMode
public abstract int getNavigationMode();
/**
@@ -474,11 +501,12 @@
* @see #NAVIGATION_MODE_LIST
* @see #NAVIGATION_MODE_TABS
*/
- public abstract void setNavigationMode(int mode);
+ public abstract void setNavigationMode(@NavigationMode int mode);
/**
* @return The current set of display options.
*/
+ @DisplayOptions
public abstract int getDisplayOptions();
/**
@@ -567,6 +595,7 @@
*
* @return The currently selected tab or null
*/
+ @Nullable
public abstract Tab getSelectedTab();
/**
@@ -679,7 +708,7 @@
* @see #setDisplayHomeAsUpEnabled(boolean)
* @see #setHomeActionContentDescription(int)
*/
- public void setHomeAsUpIndicator(Drawable indicator) {}
+ public void setHomeAsUpIndicator(@Nullable Drawable indicator) {}
/**
* Set an alternate drawable to display next to the icon/logo/title
@@ -693,14 +722,14 @@
* call {@link #setHomeActionContentDescription(int) setHomeActionContentDescription()}
* to provide a correct description of the action for accessibility support.</p>
*
- * @param resId Resource ID of a drawable to use for the up indicator, or null
+ * @param resId Resource ID of a drawable to use for the up indicator, or 0
* to use the theme's default
*
* @see #setDisplayOptions(int, int)
* @see #setDisplayHomeAsUpEnabled(boolean)
* @see #setHomeActionContentDescription(int)
*/
- public void setHomeAsUpIndicator(int resId) {}
+ public void setHomeAsUpIndicator(@DrawableRes int resId) {}
/**
* Set an alternate description for the Home/Up action, when enabled.
@@ -719,7 +748,7 @@
* @see #setHomeAsUpIndicator(int)
* @see #setHomeAsUpIndicator(android.graphics.drawable.Drawable)
*/
- public void setHomeActionContentDescription(CharSequence description) {}
+ public void setHomeActionContentDescription(@Nullable CharSequence description) {}
/**
* Set an alternate description for the Home/Up action, when enabled.
@@ -739,7 +768,7 @@
* @see #setHomeAsUpIndicator(int)
* @see #setHomeAsUpIndicator(android.graphics.drawable.Drawable)
*/
- public void setHomeActionContentDescription(int resId) {}
+ public void setHomeActionContentDescription(@StringRes int resId) {}
/**
* Listener for receiving {@link ActionBar} navigation events.
@@ -846,7 +875,7 @@
* @param resId Resource ID referring to the drawable to use as an icon
* @return The current instance for call chaining
*/
- public abstract Tab setIcon(int resId);
+ public abstract Tab setIcon(@DrawableRes int resId);
/**
* Set the text displayed on this tab. Text may be truncated if there is not room to display
@@ -864,7 +893,7 @@
* @param resId A resource ID referring to the text that should be displayed
* @return The current instance for call chaining
*/
- public abstract Tab setText(int resId);
+ public abstract Tab setText(@StringRes int resId);
/**
* Set a custom view to be used for this tab. This overrides values set by {@link
@@ -882,7 +911,7 @@
* @param layoutResId A layout resource to inflate and use as a custom tab view
* @return The current instance for call chaining
*/
- public abstract Tab setCustomView(int layoutResId);
+ public abstract Tab setCustomView(@LayoutRes int layoutResId);
/**
* Retrieve a previously set custom view for this tab.
@@ -927,7 +956,7 @@
* @see #setContentDescription(CharSequence)
* @see #getContentDescription()
*/
- public abstract Tab setContentDescription(int resId);
+ public abstract Tab setContentDescription(@StringRes int resId);
/**
* Set a description of this tab's content for use in accessibility support. If no content
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java
index b56b448..ae4c6c4 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivity.java
@@ -21,6 +21,7 @@
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.LayoutRes;
import android.support.v4.app.ActionBarDrawerToggle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentActivity;
@@ -72,7 +73,7 @@
}
@Override
- public void setContentView(int layoutResID) {
+ public void setContentView(@LayoutRes int layoutResID) {
mImpl.setContentView(layoutResID);
}
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java
index 798e359..276aa19 100644
--- a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegate.java
@@ -44,7 +44,9 @@
private static final String TAG = "ActionBarActivityDelegate";
static ActionBarActivityDelegate createDelegate(ActionBarActivity activity) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+ if (Build.VERSION.SDK_INT >= 20) {
+ return new ActionBarActivityDelegateApi20(activity);
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
return new ActionBarActivityDelegateJBMR2(activity);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
return new ActionBarActivityDelegateJB(activity);
diff --git a/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateApi20.java b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateApi20.java
new file mode 100644
index 0000000..3a06fe4
--- /dev/null
+++ b/v7/appcompat/src/android/support/v7/app/ActionBarActivityDelegateApi20.java
@@ -0,0 +1,40 @@
+/*
+ * 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.app;
+
+import android.view.Window;
+
+class ActionBarActivityDelegateApi20 extends ActionBarActivityDelegateJBMR2 {
+
+ ActionBarActivityDelegateApi20(ActionBarActivity activity) {
+ super(activity);
+ }
+
+ @Override
+ Window.Callback createWindowCallbackWrapper(Window.Callback cb) {
+ return new WindowCallbackWrapperApi20(cb);
+ }
+
+ class WindowCallbackWrapperApi20 extends WindowCallbackWrapper {
+
+ WindowCallbackWrapperApi20(Window.Callback wrapped) {
+ super(wrapped);
+ }
+
+ }
+
+}
diff --git a/v7/appcompat/src/android/support/v7/widget/PopupMenu.java b/v7/appcompat/src/android/support/v7/widget/PopupMenu.java
index 2f561d1..ec7687a 100644
--- a/v7/appcompat/src/android/support/v7/widget/PopupMenu.java
+++ b/v7/appcompat/src/android/support/v7/widget/PopupMenu.java
@@ -17,6 +17,7 @@
import android.content.Context;
+import android.support.annotation.MenuRes;
import android.support.v7.internal.view.SupportMenuInflater;
import android.support.v7.internal.view.menu.MenuBuilder;
import android.support.v7.internal.view.menu.MenuPopupHelper;
@@ -96,7 +97,7 @@
* popupMenu.getMenuInflater().inflate(menuRes, popupMenu.getMenu()).
* @param menuRes Menu resource to inflate
*/
- public void inflate(int menuRes) {
+ public void inflate(@MenuRes int menuRes) {
getMenuInflater().inflate(menuRes, mMenu);
}
diff --git a/v7/mediarouter/project.properties b/v7/mediarouter/project.properties
index dfa4dd0..484dab0 100644
--- a/v7/mediarouter/project.properties
+++ b/v7/mediarouter/project.properties
@@ -11,5 +11,5 @@
#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
-target=android-16
+target=android-17
android.library=true
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteActionProvider.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteActionProvider.java
index 2fd2488..3b14e2b 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteActionProvider.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteActionProvider.java
@@ -17,6 +17,8 @@
package android.support.v7.app;
import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v4.view.ActionProvider;
import android.support.v7.media.MediaRouter;
import android.support.v7.media.MediaRouteSelector;
@@ -151,6 +153,7 @@
*
* @return The selector, never null.
*/
+ @NonNull
public MediaRouteSelector getRouteSelector() {
return mSelector;
}
@@ -161,7 +164,7 @@
*
* @param selector The selector, must not be null.
*/
- public void setRouteSelector(MediaRouteSelector selector) {
+ public void setRouteSelector(@NonNull MediaRouteSelector selector) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
@@ -195,6 +198,7 @@
*
* @return The dialog factory, never null.
*/
+ @NonNull
public MediaRouteDialogFactory getDialogFactory() {
return mDialogFactory;
}
@@ -205,7 +209,7 @@
*
* @param factory The dialog factory, must not be null.
*/
- public void setDialogFactory(MediaRouteDialogFactory factory) {
+ public void setDialogFactory(@NonNull MediaRouteDialogFactory factory) {
if (factory == null) {
throw new IllegalArgumentException("factory must not be null");
}
@@ -222,6 +226,7 @@
/**
* Gets the associated media route button, or null if it has not yet been created.
*/
+ @Nullable
public MediaRouteButton getMediaRouteButton() {
return mButton;
}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java
index c3d34ec..f5103fa 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteButton.java
@@ -23,6 +23,7 @@
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.graphics.drawable.DrawableCompat;
@@ -144,6 +145,7 @@
*
* @return The selector, never null.
*/
+ @NonNull
public MediaRouteSelector getRouteSelector() {
return mSelector;
}
@@ -179,6 +181,7 @@
*
* @return The dialog factory, never null.
*/
+ @NonNull
public MediaRouteDialogFactory getDialogFactory() {
return mDialogFactory;
}
@@ -189,7 +192,7 @@
*
* @param factory The dialog factory, must not be null.
*/
- public void setDialogFactory(MediaRouteDialogFactory factory) {
+ public void setDialogFactory(@NonNull MediaRouteDialogFactory factory) {
if (factory == null) {
throw new IllegalArgumentException("factory must not be null");
}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
index bd28f51..3a87f02 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteChooserDialog.java
@@ -19,6 +19,7 @@
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.v7.media.MediaRouter;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.mediarouter.R;
@@ -73,6 +74,7 @@
*
* @return The selector, never null.
*/
+ @NonNull
public MediaRouteSelector getRouteSelector() {
return mSelector;
}
@@ -82,7 +84,7 @@
*
* @param selector The selector, must not be null.
*/
- public void setRouteSelector(MediaRouteSelector selector) {
+ public void setRouteSelector(@NonNull MediaRouteSelector selector) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
@@ -128,7 +130,7 @@
* @param route The route to consider, never null.
* @return True if the route should be included in the chooser dialog.
*/
- public boolean onFilterRoute(MediaRouter.RouteInfo route) {
+ public boolean onFilterRoute(@NonNull MediaRouter.RouteInfo route) {
return !route.isDefault() && route.isEnabled() && route.matchesSelector(mSelector);
}
diff --git a/v7/mediarouter/src/android/support/v7/app/MediaRouteDialogFactory.java b/v7/mediarouter/src/android/support/v7/app/MediaRouteDialogFactory.java
index 834b50d..1ae284f 100644
--- a/v7/mediarouter/src/android/support/v7/app/MediaRouteDialogFactory.java
+++ b/v7/mediarouter/src/android/support/v7/app/MediaRouteDialogFactory.java
@@ -16,6 +16,8 @@
package android.support.v7.app;
+import android.support.annotation.NonNull;
+
/**
* The media route dialog factory is responsible for creating the media route
* chooser and controller dialogs as needed.
@@ -39,6 +41,7 @@
*
* @return The default media route dialog factory, never null.
*/
+ @NonNull
public static MediaRouteDialogFactory getDefault() {
return sDefault;
}
@@ -51,6 +54,7 @@
*
* @return The media route chooser dialog fragment, must not be null.
*/
+ @NonNull
public MediaRouteChooserDialogFragment onCreateChooserDialogFragment() {
return new MediaRouteChooserDialogFragment();
}
@@ -63,6 +67,7 @@
*
* @return The media route controller dialog fragment, must not be null.
*/
+ @NonNull
public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
return new MediaRouteControllerDialogFragment();
}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java
index 54596b1..e011877 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteProvider.java
@@ -21,6 +21,8 @@
import android.content.Intent;
import android.os.Handler;
import android.os.Message;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v7.media.MediaRouter.ControlRequestCallback;
/**
@@ -73,7 +75,7 @@
*
* @param context The context.
*/
- public MediaRouteProvider(Context context) {
+ public MediaRouteProvider(@NonNull Context context) {
this(context, null);
}
@@ -116,7 +118,7 @@
*
* @param callback The callback to use, or null if none.
*/
- public final void setCallback(Callback callback) {
+ public final void setCallback(@Nullable Callback callback) {
MediaRouter.checkCallingThread();
mCallback = callback;
}
@@ -129,6 +131,7 @@
*
* @see #onDiscoveryRequestChanged
*/
+ @Nullable
public final MediaRouteDiscoveryRequest getDiscoveryRequest() {
return mDiscoveryRequest;
}
@@ -184,7 +187,7 @@
*
* @see MediaRouter#addCallback
*/
- public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ public void onDiscoveryRequestChanged(@Nullable MediaRouteDiscoveryRequest request) {
}
/**
@@ -199,6 +202,7 @@
*
* @see Callback#onDescriptorChanged
*/
+ @Nullable
public final MediaRouteProviderDescriptor getDescriptor() {
return mDescriptor;
}
@@ -214,7 +218,7 @@
*
* @see Callback#onDescriptorChanged
*/
- public final void setDescriptor(MediaRouteProviderDescriptor descriptor) {
+ public final void setDescriptor(@Nullable MediaRouteProviderDescriptor descriptor) {
MediaRouter.checkCallingThread();
if (mDescriptor != descriptor) {
@@ -245,6 +249,7 @@
* @return The route controller. Returns null if there is no such route or if the route
* cannot be controlled using the route controller interface.
*/
+ @Nullable
public RouteController onCreateRouteController(String routeId) {
return null;
}
@@ -354,7 +359,7 @@
*
* @see MediaControlIntent
*/
- public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
+ public boolean onControlRequest(Intent intent, @Nullable ControlRequestCallback callback) {
return false;
}
}
@@ -369,8 +374,8 @@
* @param provider The media route provider that changed, never null.
* @param descriptor The new media route provider descriptor, or null if none.
*/
- public void onDescriptorChanged(MediaRouteProvider provider,
- MediaRouteProviderDescriptor descriptor) {
+ public void onDescriptorChanged(@NonNull MediaRouteProvider provider,
+ @Nullable MediaRouteProviderDescriptor descriptor) {
}
}
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java b/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java
index c6869f3..4323d69 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouteSelector.java
@@ -17,6 +17,8 @@
import android.content.IntentFilter;
import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
@@ -200,7 +202,7 @@
* @param bundle The bundle, or null if none.
* @return The new instance, or null if the bundle was null.
*/
- public static MediaRouteSelector fromBundle(Bundle bundle) {
+ public static MediaRouteSelector fromBundle(@Nullable Bundle bundle) {
return bundle != null ? new MediaRouteSelector(bundle, null) : null;
}
@@ -220,7 +222,7 @@
* Creates a media route selector descriptor builder whose initial contents are
* copied from an existing selector.
*/
- public Builder(MediaRouteSelector selector) {
+ public Builder(@NonNull MediaRouteSelector selector) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
@@ -238,7 +240,8 @@
* {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
* @return The builder instance for chaining.
*/
- public Builder addControlCategory(String category) {
+ @NonNull
+ public Builder addControlCategory(@NonNull String category) {
if (category == null) {
throw new IllegalArgumentException("category must not be null");
}
@@ -259,7 +262,8 @@
* such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
* @return The builder instance for chaining.
*/
- public Builder addControlCategories(Collection<String> categories) {
+ @NonNull
+ public Builder addControlCategories(@NonNull Collection<String> categories) {
if (categories == null) {
throw new IllegalArgumentException("categories must not be null");
}
@@ -278,7 +282,8 @@
* @param selector The media route selector whose contents are to be added.
* @return The builder instance for chaining.
*/
- public Builder addSelector(MediaRouteSelector selector) {
+ @NonNull
+ public Builder addSelector(@NonNull MediaRouteSelector selector) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
@@ -290,6 +295,7 @@
/**
* Builds the {@link MediaRouteSelector media route selector}.
*/
+ @NonNull
public MediaRouteSelector build() {
if (mControlCategories == null) {
return EMPTY;
diff --git a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
index 7b83cc0..8d906b4 100644
--- a/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
+++ b/v7/mediarouter/src/android/support/v7/media/MediaRouter.java
@@ -27,11 +27,16 @@
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v4.hardware.display.DisplayManagerCompat;
import android.support.v7.media.MediaRouteProvider.ProviderMetadata;
import android.util.Log;
import android.view.Display;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
@@ -70,6 +75,17 @@
final Context mContext;
final ArrayList<CallbackRecord> mCallbackRecords = new ArrayList<CallbackRecord>();
+ /** @hide */
+ @IntDef(flag = true,
+ value = {
+ CALLBACK_FLAG_PERFORM_ACTIVE_SCAN,
+ CALLBACK_FLAG_REQUEST_DISCOVERY,
+ CALLBACK_FLAG_UNFILTERED_EVENTS
+ }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface CallbackFlags {}
+
/**
* Flag for {@link #addCallback}: Actively scan for routes while this callback
* is registered.
@@ -156,7 +172,7 @@
* @return The media router instance for the context. The application must hold
* a strong reference to this object as long as it is in use.
*/
- public static MediaRouter getInstance(Context context) {
+ public static MediaRouter getInstance(@NonNull Context context) {
if (context == null) {
throw new IllegalArgumentException("context must not be null");
}
@@ -195,6 +211,7 @@
*
* @return The default route, which is guaranteed to never be null.
*/
+ @NonNull
public RouteInfo getDefaultRoute() {
checkCallingThread();
return sGlobal.getDefaultRoute();
@@ -245,6 +262,7 @@
* @see RouteInfo#supportsControlCategory
* @see RouteInfo#supportsControlRequest
*/
+ @NonNull
public RouteInfo getSelectedRoute() {
checkCallingThread();
return sGlobal.getSelectedRoute();
@@ -262,7 +280,8 @@
* @see RouteInfo#matchesSelector
* @see RouteInfo#isDefault
*/
- public RouteInfo updateSelectedRoute(MediaRouteSelector selector) {
+ @NonNull
+ public RouteInfo updateSelectedRoute(@NonNull MediaRouteSelector selector) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
@@ -284,7 +303,7 @@
*
* @param route The route to select.
*/
- public void selectRoute(RouteInfo route) {
+ public void selectRoute(@NonNull RouteInfo route) {
if (route == null) {
throw new IllegalArgumentException("route must not be null");
}
@@ -310,7 +329,7 @@
* May be zero or {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE}.
* @return True if a matching route may be available.
*/
- public boolean isRouteAvailable(MediaRouteSelector selector, int flags) {
+ public boolean isRouteAvailable(@NonNull MediaRouteSelector selector, int flags) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
@@ -407,7 +426,8 @@
* {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}.
* @see #removeCallback
*/
- public void addCallback(MediaRouteSelector selector, Callback callback, int flags) {
+ public void addCallback(@NonNull MediaRouteSelector selector, @NonNull Callback callback,
+ @CallbackFlags int flags) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
@@ -452,7 +472,7 @@
* @param callback The callback to remove.
* @see #addCallback
*/
- public void removeCallback(Callback callback) {
+ public void removeCallback(@NonNull Callback callback) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
@@ -491,7 +511,7 @@
* @see MediaRouteProvider
* @see #removeCallback
*/
- public void addProvider(MediaRouteProvider providerInstance) {
+ public void addProvider(@NonNull MediaRouteProvider providerInstance) {
if (providerInstance == null) {
throw new IllegalArgumentException("providerInstance must not be null");
}
@@ -515,7 +535,7 @@
* @see MediaRouteProvider
* @see #addCallback
*/
- public void removeProvider(MediaRouteProvider providerInstance) {
+ public void removeProvider(@NonNull MediaRouteProvider providerInstance) {
if (providerInstance == null) {
throw new IllegalArgumentException("providerInstance must not be null");
}
@@ -538,7 +558,7 @@
*
* @param remoteControlClient The {@link android.media.RemoteControlClient} to register.
*/
- public void addRemoteControlClient(Object remoteControlClient) {
+ public void addRemoteControlClient(@NonNull Object remoteControlClient) {
if (remoteControlClient == null) {
throw new IllegalArgumentException("remoteControlClient must not be null");
}
@@ -555,7 +575,7 @@
*
* @param remoteControlClient The {@link android.media.RemoteControlClient} to register.
*/
- public void removeRemoteControlClient(Object remoteControlClient) {
+ public void removeRemoteControlClient(@NonNull Object remoteControlClient) {
if (remoteControlClient == null) {
throw new IllegalArgumentException("remoteControlClient must not be null");
}
@@ -608,6 +628,11 @@
private Bundle mExtras;
private MediaRouteDescriptor mDescriptor;
+ /** @hide */
+ @IntDef({PLAYBACK_TYPE_LOCAL,PLAYBACK_TYPE_REMOTE})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface PlaybackType {}
+
/**
* The default playback type, "local", indicating the presentation of the media
* is happening on the same device (e.g. a phone, a tablet) as where it is
@@ -625,6 +650,11 @@
*/
public static final int PLAYBACK_TYPE_REMOTE = 1;
+ /** @hide */
+ @IntDef({PLAYBACK_VOLUME_FIXED,PLAYBACK_VOLUME_VARIABLE})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface PlaybackVolume {}
+
/**
* Playback information indicating the playback volume is fixed, i.e. it cannot be
* controlled from this object. An example of fixed playback volume is a remote player,
@@ -670,6 +700,7 @@
*
* @return The unique id of the route, never null.
*/
+ @NonNull
public String getId() {
return mUniqueId;
}
@@ -697,6 +728,7 @@
*
* @return The description of the route, or null if none.
*/
+ @Nullable
public String getDescription() {
return mDescription;
}
@@ -768,7 +800,7 @@
* @return True if the route supports at least one of the capabilities
* described in the media route selector.
*/
- public boolean matchesSelector(MediaRouteSelector selector) {
+ public boolean matchesSelector(@NonNull MediaRouteSelector selector) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
@@ -794,7 +826,7 @@
* @see MediaControlIntent
* @see #getControlFilters
*/
- public boolean supportsControlCategory(String category) {
+ public boolean supportsControlCategory(@NonNull String category) {
if (category == null) {
throw new IllegalArgumentException("category must not be null");
}
@@ -829,7 +861,7 @@
* @see MediaControlIntent
* @see #getControlFilters
*/
- public boolean supportsControlAction(String category, String action) {
+ public boolean supportsControlAction(@NonNull String category, @NonNull String action) {
if (category == null) {
throw new IllegalArgumentException("category must not be null");
}
@@ -862,7 +894,7 @@
* @see MediaControlIntent
* @see #getControlFilters
*/
- public boolean supportsControlRequest(Intent intent) {
+ public boolean supportsControlRequest(@NonNull Intent intent) {
if (intent == null) {
throw new IllegalArgumentException("intent must not be null");
}
@@ -895,7 +927,8 @@
*
* @see MediaControlIntent
*/
- public void sendControlRequest(Intent intent, ControlRequestCallback callback) {
+ public void sendControlRequest(@NonNull Intent intent,
+ @Nullable ControlRequestCallback callback) {
if (intent == null) {
throw new IllegalArgumentException("intent must not be null");
}
@@ -910,6 +943,7 @@
* @return The type of playback associated with this route: {@link #PLAYBACK_TYPE_LOCAL}
* or {@link #PLAYBACK_TYPE_REMOTE}.
*/
+ @PlaybackType
public int getPlaybackType() {
return mPlaybackType;
}
@@ -929,6 +963,7 @@
* @return How volume is handled on the route: {@link #PLAYBACK_VOLUME_FIXED}
* or {@link #PLAYBACK_VOLUME_VARIABLE}.
*/
+ @PlaybackVolume
public int getVolumeHandling() {
return mVolumeHandling;
}
@@ -1012,6 +1047,7 @@
* @see MediaControlIntent#CATEGORY_LIVE_VIDEO
* @see android.app.Presentation
*/
+ @Nullable
public Display getPresentationDisplay() {
checkCallingThread();
if (mPresentationDisplayId >= 0 && mPresentationDisplay == null) {
@@ -1024,6 +1060,7 @@
* Gets a collection of extra properties about this route that were supplied
* by its media route provider, or null if none.
*/
+ @Nullable
public Bundle getExtras() {
return mExtras;
}
diff --git a/v7/recyclerview/Android.mk b/v7/recyclerview/Android.mk
new file mode 100644
index 0000000..b3da0bd
--- /dev/null
+++ b/v7/recyclerview/Android.mk
@@ -0,0 +1,67 @@
+# 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.
+
+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-recyclerview-res
+# LOCAL_SDK_VERSION := current
+# LOCAL_SRC_FILES := $(call all-java-files-under, dummy)
+# LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+# LOCAL_AAPT_FLAGS := \
+# --auto-add-overlay
+# LOCAL_JAR_EXCLUDE_FILES := none
+# include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# # A helper sub-library that makes direct use of JellyBean APIs.
+# include $(CLEAR_VARS)
+# LOCAL_MODULE := android-support-v7-recyclerview-jellybean
+# LOCAL_SDK_VERSION := 16
+# LOCAL_SRC_FILES := $(call all-java-files-under, jellybean)
+# include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# # A helper sub-library that makes direct use of JellyBean MR1 APIs.
+# include $(CLEAR_VARS)
+# LOCAL_MODULE := android-support-v7-recyclerview-jellybean-mr1
+# LOCAL_SDK_VERSION := 17
+# LOCAL_SRC_FILES := $(call all-java-files-under, jellybean-mr1)
+# LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-recyclerview-jellybean
+# include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# # A helper sub-library that makes direct use of JellyBean MR2 APIs.
+# include $(CLEAR_VARS)
+# LOCAL_MODULE := android-support-v7-recyclerview-jellybean-mr2
+# LOCAL_SDK_VERSION := current
+# LOCAL_SRC_FILES := $(call all-java-files-under, jellybean-mr2)
+# LOCAL_STATIC_JAVA_LIBRARIES := android-support-v7-recyclerview-jellybean-mr1
+# 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-recyclerview
+LOCAL_SDK_VERSION := 7
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+# LOCAL_JAVA_LIBRARIES := android-support-v4 android-support-v7-recyclerview-res
+LOCAL_JAVA_LIBRARIES := \
+ android-support-v4 \
+ android-support-annotations
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/v7/recyclerview/AndroidManifest.xml b/v7/recyclerview/AndroidManifest.xml
new file mode 100644
index 0000000..421395f
--- /dev/null
+++ b/v7/recyclerview/AndroidManifest.xml
@@ -0,0 +1,19 @@
+<?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.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.support.v7.recyclerview">
+ <uses-sdk android:minSdkVersion="7"/>
+</manifest>
diff --git a/v7/recyclerview/README.txt b/v7/recyclerview/README.txt
new file mode 100644
index 0000000..3c9de34
--- /dev/null
+++ b/v7/recyclerview/README.txt
@@ -0,0 +1 @@
+Library Project including RecyclerView and associated utilities.
diff --git a/v7/recyclerview/build.gradle b/v7/recyclerview/build.gradle
new file mode 100644
index 0000000..f7d0607
--- /dev/null
+++ b/v7/recyclerview/build.gradle
@@ -0,0 +1,24 @@
+apply plugin: 'android-library'
+
+archivesBaseName = 'recyclerview-v7'
+
+dependencies {
+ compile project(':support-v4')
+}
+
+android {
+ compileSdkVersion 7
+ buildToolsVersion "19.0.1"
+
+ sourceSets {
+ main.manifest.srcFile 'AndroidManifest.xml'
+ main.java.srcDir 'src'
+ androidTest.setRoot('tests')
+ androidTest.java.srcDir 'tests/src'
+ }
+
+ lintOptions {
+ // TODO: fix errors and reenable.
+ abortOnError false
+ }
+}
\ No newline at end of file
diff --git a/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
new file mode 100644
index 0000000..bd4cae7
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearLayoutManager.java
@@ -0,0 +1,1473 @@
+/*
+ * 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.content.Context;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.graphics.PointF;
+import android.support.annotation.Nullable;
+import android.support.v4.view.ViewCompat;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+/**
+ * A {@link android.support.v7.widget.RecyclerView.LayoutManager} implementation which provides
+ * similar functionality to {@link android.widget.ListView}.
+ */
+public class LinearLayoutManager extends RecyclerView.LayoutManager {
+
+ private static final String TAG = "LinearLayoutManager";
+
+ private static final boolean DEBUG = false;
+
+ public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
+
+ public static final int VERTICAL = LinearLayout.VERTICAL;
+
+ public static final int INVALID_OFFSET = Integer.MIN_VALUE;
+
+
+ /**
+ * While trying to find next view to focus, LinearLayoutManager will not try to scroll more
+ * than
+ * this factor times the total space of the list. If layout is vertical, total space is the
+ * height minus padding, if layout is horizontal, total space is the width minus padding.
+ */
+ private static final float MAX_SCROLL_FACTOR = 0.33f;
+
+
+ /**
+ * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ 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
+ * the same object.
+ */
+ private RenderState mRenderState;
+
+ /**
+ * 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.
+ */
+ private OrientationHelper mOrientationHelper;
+
+ /**
+ * We need to track this so that we can ignore current position when it changes.
+ */
+ private boolean mLastStackFromEnd;
+
+
+ /**
+ * Defines if layout should be calculated from end to start.
+ *
+ * @see #mShouldReverseLayout
+ */
+ private boolean mReverseLayout = false;
+
+ /**
+ * This keeps the final value for how LayoutManager shouls start laying out views.
+ * It is calculated by checking {@link #getReverseLayout()} and View's layout direction.
+ * {@link #layoutChildren(RecyclerView.Adapter, RecyclerView.Recycler, boolean)} is run.
+ */
+ private boolean mShouldReverseLayout = false;
+
+ /**
+ * Works the same way as {@link android.widget.AbsListView#setStackFromBottom(boolean)} and
+ * it supports both orientations.
+ * see {@link android.widget.AbsListView#setStackFromBottom(boolean)}
+ */
+ private boolean mStackFromEnd = false;
+
+ /**
+ * 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;
+
+ private SavedState mPendingSavedState = null;
+
+ /**
+ * Creates a vertical LinearLayoutManager
+ *
+ * @param context Current context, will be used to access resources.
+ */
+ public LinearLayoutManager(Context context) {
+ this(context, VERTICAL, false);
+ }
+
+ /**
+ * @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.
+ */
+ public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
+ setOrientation(orientation);
+ setReverseLayout(reverseLayout);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ if (mPendingSavedState != null) {
+ return new SavedState(mPendingSavedState);
+ }
+ SavedState state = new SavedState();
+ if (getChildCount() > 0) {
+ boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout;
+ state.mOrientation = mOrientation;
+ state.mAnchorLayoutFromEnd = didLayoutFromEnd;
+
+ if (didLayoutFromEnd) {
+ final View refChild = getChildClosestToEnd();
+ state.mAnchorOffset = mOrientationHelper.getEndAfterPadding() -
+ mOrientationHelper.getDecoratedEnd(refChild);
+ state.mAnchorPosition = getPosition(refChild);
+ } else {
+ final View refChild = getChildClosestToStart();
+ state.mAnchorPosition = getPosition(refChild);
+ state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild) -
+ mOrientationHelper.getStartAfterPadding();
+ }
+ } else {
+ state.mAnchorPosition = 0;
+ state.mAnchorOffset = 0;
+ }
+ state.mStackFromEnd = mStackFromEnd;
+ state.mReverseLayout = mReverseLayout;
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (state instanceof SavedState) {
+ mPendingSavedState = (SavedState) state;
+ requestLayout();
+ if (DEBUG) {
+ Log.d(TAG, "loaded saved state");
+ }
+ } else if (DEBUG) {
+ Log.d(TAG, "invalid saved state class");
+ }
+ }
+
+ /**
+ * @return true if {@link #getOrientation()} is {@link #HORIZONTAL}
+ */
+ @Override
+ public boolean canScrollHorizontally() {
+ return mOrientation == HORIZONTAL;
+ }
+
+ /**
+ * @return true if {@link #getOrientation()} is {@link #VERTICAL}
+ */
+ @Override
+ public boolean canScrollVertically() {
+ return mOrientation == VERTICAL;
+ }
+
+ /**
+ * Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)}
+ */
+ public void setStackFromEnd(boolean stackFromEnd) {
+ mStackFromEnd = stackFromEnd;
+ requestLayout();
+ }
+
+ public boolean getStackFromEnd() {
+ return mStackFromEnd;
+ }
+
+ /**
+ * Returns the current orientaion of the layout.
+ *
+ * @return Current orientation.
+ * @see #mOrientation
+ * @see #setOrientation(int)
+ */
+ public int getOrientation() {
+ return mOrientation;
+ }
+
+ /**
+ * Sets the orientation of the layout. {@link android.support.v7.widget.LinearLayoutManager}
+ * will do its best to keep scroll position.
+ *
+ * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
+ */
+ public void setOrientation(int orientation) {
+ if (orientation != HORIZONTAL && orientation != VERTICAL) {
+ throw new IllegalArgumentException("invalid orientation.");
+ }
+ if (orientation == mOrientation) {
+ return;
+ }
+ mOrientation = orientation;
+ mOrientationHelper = null;
+ requestLayout();
+ }
+
+ /**
+ * Calculates the view 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;
+ }
+ }
+
+ /**
+ * Returns if views are laid out from the opposite direction of the layout.
+ *
+ * @return If layout is reversed or not.
+ * @see {@link #setReverseLayout(boolean)}
+ */
+ public boolean getReverseLayout() {
+ return mReverseLayout;
+ }
+
+ /**
+ * 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.
+ *
+ * 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
+ * from LTR.
+ *
+ * If you are looking for the exact same behavior of
+ * {@link android.widget.AbsListView#setStackFromBottom(boolean)}, use
+ * {@link #setStackFromEnd(boolean)}
+ */
+ public void setReverseLayout(boolean reverseLayout) {
+ if (reverseLayout == mReverseLayout) {
+ return;
+ }
+ mReverseLayout = reverseLayout;
+ requestLayout();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View findViewByPosition(int position) {
+ final int childCount = getChildCount();
+ if (childCount == 0) {
+ return null;
+ }
+ final int firstChild = getPosition(getChildAt(0));
+ final int viewPosition = position - firstChild;
+ if (viewPosition >= 0 && viewPosition < childCount) {
+ return getChildAt(viewPosition);
+ }
+ return null;
+ }
+
+ /**
+ * <p>Returns the amount of extra space that should be rendered 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>
+ *
+ * @return The extra space that should be laid out (in pixels).
+ */
+ protected int getExtraLayoutSpace(RecyclerView.State state) {
+ if (state.hasTargetScrollPosition()) {
+ return mOrientationHelper.getTotalSpace();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.Adapter adapter,
+ int position) {
+ LinearSmoothScroller linearSmoothScroller =
+ new LinearSmoothScroller(recyclerView.getContext()) {
+ @Override
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ return LinearLayoutManager.this
+ .computeScrollVectorForPosition(targetPosition);
+ }
+ };
+ linearSmoothScroller.setTargetPosition(position);
+ startSmoothScroll(linearSmoothScroller);
+ }
+
+ public PointF computeScrollVectorForPosition(int targetPosition) {
+ if (getChildCount() == 0) {
+ return null;
+ }
+ final int firstChildPos = getPosition(getChildAt(0));
+ final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
+ if (mOrientation == HORIZONTAL) {
+ return new PointF(direction, 0);
+ } else {
+ return new PointF(0, direction);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onLayoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
+ boolean structureChanged, RecyclerView.State state) {
+ // layout algorithm:
+ // 1) by checking children and other variables, find an anchor coordinate and an anchor
+ // item position.
+ // 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
+
+ if (mPendingSavedState != null) {
+ setOrientation(mPendingSavedState.mOrientation);
+ setReverseLayout(mPendingSavedState.mReverseLayout);
+ setStackFromEnd(mPendingSavedState.mStackFromEnd);
+ mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
+ }
+ ensureRenderState();
+ // resolve layout direction
+ resolveShouldLayoutReverse();
+
+ // validate scroll position if exists
+ if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
+ // validate it
+ if (mPendingScrollPosition < 0 || mPendingScrollPosition >= adapter.getItemCount()) {
+ mPendingScrollPosition = RecyclerView.NO_POSITION;
+ mPendingScrollPositionOffset = INVALID_OFFSET;
+ if (DEBUG) {
+ Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition);
+ }
+ }
+ }
+ // this value might be updated if there is a target scroll position without an offset
+ boolean layoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
+
+ final boolean stackFromEndChanged = mLastStackFromEnd != mStackFromEnd;
+
+ int anchorCoordinate, anchorItemPosition;
+ if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
+ // 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) {
+ // Anchor offset depends on how that child was laid out. Here, we update it
+ // according to our current view bounds
+ layoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
+ if (layoutFromEnd) {
+ anchorCoordinate = mOrientationHelper.getEndAfterPadding() -
+ mPendingSavedState.mAnchorOffset;
+ } else {
+ anchorCoordinate = mOrientationHelper.getStartAfterPadding() +
+ mPendingSavedState.mAnchorOffset;
+ }
+ } else if (mPendingScrollPositionOffset == INVALID_OFFSET) {
+ View child = findViewByPosition(mPendingScrollPosition);
+ if (child != null) {
+ final int startGap = mOrientationHelper.getDecoratedStart(child)
+ - mOrientationHelper.getStartAfterPadding();
+ final int endGap = mOrientationHelper.getEndAfterPadding() -
+ mOrientationHelper.getDecoratedEnd(child);
+ final int childSize = mOrientationHelper.getDecoratedMeasurement(child);
+ if (childSize > mOrientationHelper.getTotalSpace()) {
+ // item does not fit. fix depending on layout direction
+ anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding()
+ : mOrientationHelper.getStartAfterPadding();
+ } else if (startGap < 0) {
+ anchorCoordinate = mOrientationHelper.getStartAfterPadding();
+ layoutFromEnd = false;
+ } else if (endGap < 0) {
+ anchorCoordinate = mOrientationHelper.getEndAfterPadding();
+ layoutFromEnd = true;
+ } else {
+ anchorCoordinate = layoutFromEnd
+ ? mOrientationHelper.getDecoratedEnd(child)
+ : mOrientationHelper.getDecoratedStart(child);
+ }
+ } else { // item is not visible.
+ if (getChildCount() > 0) {
+ // get position of any child, does not matter
+ int pos = getPosition(getChildAt(0));
+ if (mPendingScrollPosition < pos == mShouldReverseLayout) {
+ anchorCoordinate = mOrientationHelper.getEndAfterPadding();
+ layoutFromEnd = true;
+ } else {
+ anchorCoordinate = mOrientationHelper.getStartAfterPadding();
+ layoutFromEnd = false;
+ }
+ } else {
+ anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding()
+ : mOrientationHelper.getStartAfterPadding();
+ }
+ }
+ } else {
+ // override layout from end values for consistency
+ if (mShouldReverseLayout) {
+ anchorCoordinate = mOrientationHelper.getEndAfterPadding()
+ - mPendingScrollPositionOffset;
+ layoutFromEnd = true;
+ } else {
+ anchorCoordinate = mOrientationHelper.getStartAfterPadding()
+ + mPendingScrollPositionOffset;
+ layoutFromEnd = false;
+ }
+ }
+ } else if (getChildCount() > 0 && !stackFromEndChanged) {
+ if (layoutFromEnd) {
+ View referenceChild = getChildClosestToEnd();
+ anchorCoordinate = mOrientationHelper.getDecoratedEnd(referenceChild);
+ anchorItemPosition = getPosition(referenceChild);
+ } else {
+ View referenceChild = getChildClosestToStart();
+ anchorCoordinate = mOrientationHelper.getDecoratedStart(referenceChild);
+ anchorItemPosition = getPosition(referenceChild);
+ }
+ } else {
+ anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() :
+ mOrientationHelper.getStartAfterPadding();
+ anchorItemPosition = mStackFromEnd ? adapter.getItemCount() - 1 : 0;
+ }
+
+ detachAndScrapAttachedViews(recycler);
+ final int extraForStart;
+ final int extraForEnd;
+ final int extra = getExtraLayoutSpace(state);
+ boolean before = state.getTargetScrollPosition() < anchorItemPosition;
+ if (before == mShouldReverseLayout) {
+ extraForEnd = extra;
+ extraForStart = 0;
+ } else {
+ extraForStart = extra;
+ extraForEnd = 0;
+ }
+ // first fill towards start
+ updateRenderStateToFillStart(anchorItemPosition, anchorCoordinate);
+ mRenderState.mExtra = extraForStart;
+ if (!layoutFromEnd) {
+ mRenderState.mCurrentPosition += mRenderState.mItemDirection;
+ }
+ fill(recycler, adapter, mRenderState, state, false);
+
+ // fill towards end
+ updateRenderStateToFillEnd(anchorItemPosition, anchorCoordinate);
+ mRenderState.mExtra = extraForEnd;
+ if (layoutFromEnd) {
+ mRenderState.mCurrentPosition += mRenderState.mItemDirection;
+ }
+ fill(recycler, adapter, mRenderState, state, false);
+
+ // 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
+ // we re-calculate it.
+ // find which side we should check for gaps.
+ if (mShouldReverseLayout ^ mStackFromEnd) {
+ fixLayoutEndGap(adapter, recycler, state, true);
+ fixLayoutStartGap(adapter, recycler, state, false);
+ } else {
+ fixLayoutStartGap(adapter, recycler, state, true);
+ fixLayoutEndGap(adapter, recycler, state, false);
+ }
+ }
+
+ removeAndRecycleScrap(recycler);
+ mPendingScrollPosition = RecyclerView.NO_POSITION;
+ mPendingScrollPositionOffset = INVALID_OFFSET;
+ mLastStackFromEnd = mStackFromEnd;
+ mPendingSavedState = null; // we don't need this anymore
+ if (DEBUG) {
+ validateChildOrder();
+ }
+ }
+
+ private void fixLayoutEndGap(RecyclerView.Adapter adapter,
+ RecyclerView.Recycler recycler, RecyclerView.State state,
+ boolean canOffsetChildren) {
+ View endChild = getChildClosestToEnd();
+ int gap = mOrientationHelper.getEndAfterPadding()
+ - mOrientationHelper.getDecoratedEnd(endChild);
+ if (gap > 0) {
+ scrollBy(-gap, adapter, recycler, state);
+ } else {
+ return; // nothing to fix
+ }
+ if (canOffsetChildren) {
+ // re-calculate gap, see if we could fix it
+ gap = mOrientationHelper.getEndAfterPadding()
+ - mOrientationHelper.getDecoratedEnd(endChild);
+ if (gap > 0) {
+ mOrientationHelper.offsetChildren(gap);
+ }
+ }
+ }
+
+ private void fixLayoutStartGap(RecyclerView.Adapter adapter,
+ RecyclerView.Recycler recycler, RecyclerView.State state,
+ boolean canOffsetChildren) {
+ View startChild = getChildClosestToStart();
+ int gap = mOrientationHelper.getDecoratedStart(startChild) -
+ mOrientationHelper.getStartAfterPadding();
+ if (gap > 0) {
+ // check if we should fix this gap.
+ scrollBy(gap, adapter, recycler, state);
+ } else {
+ return; // nothing to fix
+ }
+ if (canOffsetChildren) {
+ // re-calculate gap, see if we could fix it
+ gap = mOrientationHelper.getDecoratedStart(startChild) -
+ mOrientationHelper.getStartAfterPadding();
+ if (gap > 0) {
+ mOrientationHelper.offsetChildren(-gap);
+ }
+ }
+ }
+
+ 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 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 boolean isLayoutRTL() {
+ return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
+ }
+
+ private void ensureRenderState() {
+ if (mRenderState == null) {
+ mRenderState = new RenderState();
+ }
+ if (mOrientationHelper == null) {
+ mOrientationHelper = mOrientation == HORIZONTAL ? createHorizontalOrientationHelper()
+ : createVerticalOrientationHelper();
+ }
+ }
+
+ /**
+ * <p>Scroll the RecyclerView to make the position visible.</p>
+ *
+ * <p>RecyclerView will scroll the minimum amount that is necessary to make the
+ * target position visible. If you are looking for a similar behavior to
+ * {@link android.widget.ListView#setSelection(int)} or
+ * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use
+ * {@link #scrollToPositionWithOffset(int, int)}.</p>
+ *
+ * <p>Note that scroll position change will not be reflected until the next layout call.</p>
+ *
+ * @param position Scroll to this adapter position
+ * @see #scrollToPositionWithOffset(int, int)
+ */
+ @Override
+ public void scrollToPosition(int position) {
+ mPendingScrollPosition = position;
+ mPendingScrollPositionOffset = INVALID_OFFSET;
+ requestLayout();
+ }
+
+ /**
+ * <p>Scroll to the specified adapter position with the given offset from layout start.</p>
+ *
+ * <p>Note that scroll position change will not be reflected until the next layout call.</p>
+ *
+ * <p>If you are just trying to make a position visible, use {@link
+ * #scrollToPosition(int)}.</p>
+ *
+ * @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) {
+ mPendingScrollPosition = position;
+ mPendingScrollPositionOffset = offset;
+ requestLayout();
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int scrollHorizontallyBy(int dx, RecyclerView.Adapter adapter,
+ RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (mOrientation == VERTICAL) {
+ return 0;
+ }
+ return scrollBy(dx, adapter, recycler, state);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int scrollVerticallyBy(int dy, RecyclerView.Adapter adapter,
+ RecyclerView.Recycler recycler, RecyclerView.State state) {
+ if (mOrientation == HORIZONTAL) {
+ return 0;
+ }
+ return scrollBy(dy, adapter, recycler, state);
+ }
+
+ @Override
+ public int computeHorizontalScrollOffset(RecyclerView.Adapter adapter) {
+ if (getChildCount() == 0) {
+ return 0;
+ }
+ final int topPosition = getPosition(getChildClosestToStart());
+ return mShouldReverseLayout ? adapter.getItemCount() - 1 - topPosition : topPosition;
+ }
+
+ @Override
+ public int computeVerticalScrollOffset(RecyclerView.Adapter adapter) {
+ if (getChildCount() == 0) {
+ return 0;
+ }
+ final int topPosition = getPosition(getChildClosestToStart());
+ return mShouldReverseLayout ? adapter.getItemCount() - 1 - topPosition : topPosition;
+ }
+
+ @Override
+ public int computeHorizontalScrollExtent(RecyclerView.Adapter adapter) {
+ return getChildCount();
+ }
+
+ @Override
+ public int computeVerticalScrollExtent(RecyclerView.Adapter adapter) {
+ return getChildCount();
+ }
+
+ @Override
+ public int computeHorizontalScrollRange(RecyclerView.Adapter adapter) {
+ return adapter.getItemCount();
+ }
+
+ @Override
+ public int computeVerticalScrollRange(RecyclerView.Adapter adapter) {
+ return adapter.getItemCount();
+ }
+
+ private void updateRenderState(int layoutDirection, int requiredSpace,
+ boolean canUseExistingSpace, RecyclerView.State state) {
+ mRenderState.mExtra = getExtraLayoutSpace(state);
+ mRenderState.mLayoutDirection = layoutDirection;
+ int fastScrollSpace;
+ if (layoutDirection == RenderState.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);
+ // 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);
+ fastScrollSpace = -mOrientationHelper.getDecoratedStart(child)
+ + mOrientationHelper.getStartAfterPadding();
+ }
+ mRenderState.mAvailable = requiredSpace;
+ if (canUseExistingSpace) {
+ mRenderState.mAvailable -= fastScrollSpace;
+ }
+ mRenderState.mScrollingOffset = fastScrollSpace;
+ }
+
+ private int scrollBy(int dy, RecyclerView.Adapter adapter, 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;
+ final int absDy = Math.abs(dy);
+ updateRenderState(layoutDirection, absDy, true, state);
+ final int freeScroll = mRenderState.mScrollingOffset;
+ final int consumed = freeScroll + fill(recycler, adapter, mRenderState, state, false);
+ if (consumed < 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Don't have any more elements to scroll");
+ }
+ return 0;
+ }
+ final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
+ mOrientationHelper.offsetChildren(-scrolled);
+ if (DEBUG) {
+ Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
+ }
+ return scrolled;
+ }
+
+ /**
+ * Recycles children between given indices.
+ *
+ * @param startIndex inclusive
+ * @param endIndex exclusive
+ */
+ private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
+ if (startIndex == endIndex) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items");
+ }
+ if (endIndex > startIndex) {
+ for (int i = endIndex - 1; i >= startIndex; i--) {
+ removeAndRecycleViewAt(i, recycler);
+ }
+ } else {
+ for (int i = startIndex; i > endIndex; i--) {
+ removeAndRecycleViewAt(i, recycler);
+ }
+ }
+ }
+
+ /**
+ * Recycles views that went out of bounds after scrolling towards the end of the layout.
+ *
+ * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView}
+ * @param dt This can be used to add additional padding to the visible area. This is used
+ * to
+ * detect children that will go out of bounds after scrolling, without actually
+ * moving them.
+ */
+ private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
+ if (dt < 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Called recycle from start with a negative value. This might happen"
+ + " during layout changes but may be sign of a bug");
+ }
+ return;
+ }
+ final int limit = mOrientationHelper.getStartAfterPadding() + dt;
+ final int childCount = getChildCount();
+ if (mShouldReverseLayout) {
+ for (int i = childCount - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (mOrientationHelper.getDecoratedEnd(child) > limit) {// stop here
+ recycleChildren(recycler, childCount - 1, i);
+ return;
+ }
+ }
+ } else {
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (mOrientationHelper.getDecoratedEnd(child) > limit) {// stop here
+ recycleChildren(recycler, 0, i);
+ return;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Recycles views that went out of bounds after scrolling towards the start of the layout.
+ *
+ * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView}
+ * @param dt This can be used to add additional padding to the visible area. This is used
+ * to detect children that will go out of bounds after scrolling, without
+ * actually moving them.
+ */
+ private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt) {
+ final int childCount = getChildCount();
+ if (dt < 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Called recycle from end with a negative value. This might happen"
+ + " during layout changes but may be sign of a bug");
+ }
+ return;
+ }
+ final int limit = mOrientationHelper.getEndAfterPadding() - dt;
+ if (mShouldReverseLayout) {
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (mOrientationHelper.getDecoratedStart(child) < limit) {// stop here
+ recycleChildren(recycler, 0, i);
+ return;
+ }
+ }
+ } else {
+ for (int i = childCount - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (mOrientationHelper.getDecoratedStart(child) < limit) {// stop here
+ recycleChildren(recycler, childCount - 1, i);
+ return;
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Helper method to call appropriate recycle method depending on current render layout
+ * direction
+ *
+ * @param recycler Current recycler that is attached to RecyclerView
+ * @param renderState Current render 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}
+ * @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
+ */
+ private void recycleByRenderState(RecyclerView.Recycler recycler, RenderState renderState) {
+ if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
+ recycleViewsFromEnd(recycler, renderState.mScrollingOffset);
+ } else {
+ recycleViewsFromStart(recycler, renderState.mScrollingOffset);
+ }
+ }
+
+ /**
+ * The magic functions :). Fills the given layout, defined by the renderState. 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 adapter Current adapter that is attached to RecyclerView
+ * @param renderState 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, RecyclerView.Adapter adapter,
+ RenderState renderState, 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) {
+ // TODO ugly bug fix. should not happen
+ if (renderState.mAvailable < 0) {
+ renderState.mScrollingOffset += renderState.mAvailable;
+ }
+ recycleByRenderState(recycler, renderState);
+ }
+ int remainingSpace = renderState.mAvailable + renderState.mExtra;
+ while (remainingSpace > 0 && renderState.hasMore(adapter)) {
+ View view = renderState.next(recycler, adapter);
+ if (mShouldReverseLayout) {
+ if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
+ addView(view);
+ } else {
+ addView(view, 0);
+ }
+ } else {
+ if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
+ addView(view, 0);
+ } else {
+ addView(view);
+ }
+ }
+ measureChildWithMargins(view, 0, 0);
+ int consumed = mOrientationHelper.getDecoratedMeasurement(view);
+ int left, top, right, bottom;
+ if (mOrientation == VERTICAL) {
+ if (isLayoutRTL()) {
+ right = getWidth() - getPaddingRight();
+ left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
+ } else {
+ left = getPaddingLeft();
+ right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
+ }
+ if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
+ bottom = renderState.mOffset;
+ top = renderState.mOffset - consumed;
+ } else {
+ top = renderState.mOffset;
+ bottom = renderState.mOffset + consumed;
+ }
+ } else {
+ top = getPaddingTop();
+ bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
+
+ if (renderState.mLayoutDirection == RenderState.LAYOUT_START) {
+ right = renderState.mOffset;
+ left = renderState.mOffset - consumed;
+ } else {
+ left = renderState.mOffset;
+ right = renderState.mOffset + consumed;
+ }
+ }
+ // We calculate everything with View's bounding box (which includes decor and margins)
+ // To calculate correct layout position, we subtract margins.
+ RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
+ layoutDecorated(view, left + params.leftMargin, top + params.topMargin
+ , right - params.rightMargin, bottom - params.bottomMargin);
+ if (DEBUG) {
+ Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
+ }
+ renderState.mOffset += consumed * renderState.mLayoutDirection;
+ renderState.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;
+ }
+ recycleByRenderState(recycler, renderState);
+ }
+ if (stopOnFocusable && view.isFocusable()) {
+ break;
+ }
+
+ if (state != null && state.getTargetScrollPosition() == getPosition(view)) {
+ break;
+ }
+ }
+ if (DEBUG) {
+ validateChildOrder();
+ }
+ return start - renderState.mAvailable;
+ }
+
+ /**
+ * Converts a focusDirection to orientation.
+ *
+ * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@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.
+ */
+ private int convertFocusDirectionToLayoutDirection(int focusDirection) {
+ switch (focusDirection) {
+ case View.FOCUS_BACKWARD:
+ return RenderState.LAYOUT_START;
+ case View.FOCUS_FORWARD:
+ return RenderState.LAYOUT_END;
+ case View.FOCUS_UP:
+ return mOrientation == VERTICAL ? RenderState.LAYOUT_START
+ : RenderState.INVALID_LAYOUT;
+ case View.FOCUS_DOWN:
+ return mOrientation == VERTICAL ? RenderState.LAYOUT_END
+ : RenderState.INVALID_LAYOUT;
+ case View.FOCUS_LEFT:
+ return mOrientation == HORIZONTAL ? RenderState.LAYOUT_START
+ : RenderState.INVALID_LAYOUT;
+ case View.FOCUS_RIGHT:
+ return mOrientation == HORIZONTAL ? RenderState.LAYOUT_END
+ : RenderState.INVALID_LAYOUT;
+ default:
+ if (DEBUG) {
+ Log.d(TAG, "Unknown focus request:" + focusDirection);
+ }
+ return RenderState.INVALID_LAYOUT;
+ }
+
+ }
+
+ /**
+ * Convenience method to find the child closes to start. Caller should check it has enough
+ * children.
+ *
+ * @return The child closes to start of the layout from user's perspective.
+ */
+ private View getChildClosestToStart() {
+ return getChildAt(mShouldReverseLayout ? getChildCount() - 1 : 0);
+ }
+
+ /**
+ * Convenience method to find the child closes to end. Caller should check it has enough
+ * children.
+ *
+ * @return The child closes to end of the layout from user's perspective.
+ */
+ private View getChildClosestToEnd() {
+ return getChildAt(mShouldReverseLayout ? 0 : getChildCount() - 1);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public View onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Adapter adapter,
+ RecyclerView.Recycler recycler, RecyclerView.State state) {
+ resolveShouldLayoutReverse();
+ if (getChildCount() == 0) {
+ return null;
+ }
+
+ final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection);
+ if (layoutDir == RenderState.INVALID_LAYOUT) {
+ return null;
+ }
+ final View referenceChild;
+ if (layoutDir == RenderState.LAYOUT_START) {
+ referenceChild = getChildClosestToStart();
+ } else {
+ referenceChild = getChildClosestToEnd();
+ }
+ ensureRenderState();
+ final int maxScroll = (int) (MAX_SCROLL_FACTOR * (mOrientationHelper.getEndAfterPadding() -
+ mOrientationHelper.getStartAfterPadding()));
+ updateRenderState(layoutDir, maxScroll, false, state);
+ mRenderState.mScrollingOffset = RenderState.SCOLLING_OFFSET_NaN;
+ fill(recycler, adapter, mRenderState, state, true);
+ final View nextFocus;
+ if (layoutDir == RenderState.LAYOUT_START) {
+ nextFocus = getChildClosestToStart();
+ } else {
+ nextFocus = getChildClosestToEnd();
+ }
+ if (nextFocus == referenceChild || !nextFocus.isFocusable()) {
+ return null;
+ }
+ return nextFocus;
+ }
+
+ /**
+ * Used for debugging.
+ * Logs the internal representation of children to default logger.
+ */
+ private void logChildren() {
+ Log.d(TAG, "internal representation of views on the screen");
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ Log.d(TAG, "item " + getPosition(child) + ", coord:"
+ + mOrientationHelper.getDecoratedStart(child));
+ }
+ Log.d(TAG, "==============");
+ }
+
+ /**
+ * Used for debugging.
+ * Validates that child views are laid out in correct order. This is important because rest of
+ * the algorithm relies on this constraint.
+ *
+ * In default layout, child 0 should be closest to screen position 0 and last child should be
+ * closest to position WIDTH or HEIGHT.
+ * In reverse layout, last child should be closes to screen position 0 and first child should
+ * be closest to position WIDTH or HEIGHT
+ */
+ private void validateChildOrder() {
+ Log.d(TAG, "validating child count " + getChildCount());
+ if (getChildCount() < 1) {
+ return;
+ }
+ int lastPos = getPosition(getChildAt(0));
+ int lastScreenLoc = mOrientationHelper.getDecoratedStart(getChildAt(0));
+ if (mShouldReverseLayout) {
+ for (int i = 1; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ int pos = getPosition(child);
+ int screenLoc = mOrientationHelper.getDecoratedStart(child);
+ if (pos < lastPos) {
+ logChildren();
+ throw new RuntimeException("detected invalid position. loc invalid? " +
+ (screenLoc < lastScreenLoc));
+ }
+ if (screenLoc > lastScreenLoc) {
+ logChildren();
+ throw new RuntimeException("detected invalid location");
+ }
+ }
+ } else {
+ for (int i = 1; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ int pos = getPosition(child);
+ int screenLoc = mOrientationHelper.getDecoratedStart(child);
+ if (pos < lastPos) {
+ logChildren();
+ throw new RuntimeException("detected invalid position. loc invalid? " +
+ (screenLoc < lastScreenLoc));
+ }
+ if (screenLoc < lastScreenLoc) {
+ logChildren();
+ throw new RuntimeException("detected invalid location");
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
+ * space.
+ */
+ private static class RenderState {
+
+ final static String TAG = "LinearLayoutManager#RenderState";
+
+ 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;
+
+ /**
+ * Pixel offset where rendering should start
+ */
+ int mOffset;
+
+ /**
+ * 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 when RenderState 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.
+ */
+ int mScrollingOffset;
+
+ /**
+ * 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 to avoid recycling visible children.
+ */
+ int mExtra = 0;
+
+ /**
+ * @return true if there are more items in the data adapter
+ */
+ boolean hasMore(RecyclerView.Adapter adapter) {
+ return mCurrentPosition >= 0 && mCurrentPosition < adapter.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, RecyclerView.Adapter adapter) {
+ final View view = recycler.getViewForPosition(adapter, mCurrentPosition);
+ mCurrentPosition += mItemDirection;
+ return view;
+ }
+
+ void log() {
+ Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:" +
+ mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection);
+ }
+ }
+
+ 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;
+
+ int mAnchorPosition;
+
+ int mAnchorOffset;
+
+ boolean mReverseLayout;
+
+ boolean mStackFromEnd;
+
+ boolean mAnchorLayoutFromEnd;
+
+
+ public SavedState() {
+
+ }
+
+ SavedState(Parcel in) {
+ mOrientation = in.readInt();
+ mAnchorPosition = in.readInt();
+ mAnchorOffset = in.readInt();
+ mReverseLayout = in.readInt() == 1;
+ mStackFromEnd = in.readInt() == 1;
+ mAnchorLayoutFromEnd = in.readInt() == 1;
+ }
+
+ public SavedState(SavedState other) {
+ mOrientation = other.mOrientation;
+ mAnchorPosition = other.mAnchorPosition;
+ mAnchorOffset = other.mAnchorOffset;
+ mReverseLayout = other.mReverseLayout;
+ mStackFromEnd = other.mStackFromEnd;
+ mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mOrientation);
+ dest.writeInt(mAnchorPosition);
+ dest.writeInt(mAnchorOffset);
+ dest.writeInt(mReverseLayout ? 1 : 0);
+ dest.writeInt(mStackFromEnd ? 1 : 0);
+ dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
+ }
+
+ 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/src/android/support/v7/widget/LinearSmoothScroller.java b/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
new file mode 100644
index 0000000..b3f286b
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/LinearSmoothScroller.java
@@ -0,0 +1,332 @@
+/*
+ * 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.util.DisplayMetrics;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+
+/**
+ * {@link RecyclerView.SmoothScroller} implementation which uses
+ * {@link android.view.animation.LinearInterpolator} until the target position becames a child of
+ * the RecyclerView and then uses
+ * {@link android.view.animation.DecelerateInterpolator} to slowly approach to target position.
+ */
+abstract public class LinearSmoothScroller extends RecyclerView.SmoothScroller {
+
+ private static final String TAG = "LinearSmoothScroller";
+
+ private static final boolean DEBUG = false;
+
+ private static final float MILLISECONDS_PER_INCH = 25f;
+
+ private static final int TARGET_SEEK_SCROLL_DISTANCE_PX = 10000;
+
+ /**
+ * Align child view's left or top with parent view's left or top
+ *
+ * @see #calculateDtToFit(int, int, int, int, int)
+ * @see #calculateDxToMakeVisible(android.view.View, int)
+ * @see #calculateDyToMakeVisible(android.view.View, int)
+ */
+ public static final int SNAP_TO_START = -1;
+
+ /**
+ * Align child view's right or bottom with parent view's right or bottom
+ *
+ * @see #calculateDtToFit(int, int, int, int, int)
+ * @see #calculateDxToMakeVisible(android.view.View, int)
+ * @see #calculateDyToMakeVisible(android.view.View, int)
+ */
+ public static final int SNAP_TO_END = 1;
+
+ /**
+ * <p>Decides if the child should be snapped from start or end, depending on where it
+ * currently is in relation to its parent.</p>
+ * <p>For instance, if the view is virtually on the left of RecyclerView, using
+ * {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
+ *
+ * @see #calculateDtToFit(int, int, int, int, int)
+ * @see #calculateDxToMakeVisible(android.view.View, int)
+ * @see #calculateDyToMakeVisible(android.view.View, int)
+ */
+ public static final int SNAP_TO_ANY = 0;
+
+ // Trigger a scroll to a further distance than TARGET_SEEK_SCROLL_DISTANCE_PX so that if target
+ // view is not laid out until interim target position is reached, we can detect the case before
+ // scrolling slows down and reschedule another interim target scroll
+ private static final float TARGET_SEEK_EXTRA_SCROLL_RATIO = 1.2f;
+
+ protected final LinearInterpolator mLinearInterpolator = new LinearInterpolator();
+
+ protected final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator();
+
+ protected PointF mTargetVector;
+
+ private final float MILLISECONDS_PER_PX;
+
+ // Temporary variables to keep track of the interim scroll target. These values do not
+ // point to a real item position, rather point to an estimated location pixels.
+ protected int mInterimTargetDx = 0, mInterimTargetDy = 0;
+
+ public LinearSmoothScroller(Context context) {
+ MILLISECONDS_PER_PX = calculateSpeedPerPixel(context.getResources().getDisplayMetrics());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStart() {
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+ final int dx = calculateDxToMakeVisible(targetView, getHorizontalSnapPreference());
+ final int dy = calculateDyToMakeVisible(targetView, getVerticalSnapPreference());
+ final int distance = (int) Math.sqrt(dx * dx + dy * dy);
+ final int time = calculateTimeForDeceleration(distance);
+ action.update(-dx, -dy, time, mDecelerateInterpolator);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onSeekTargetStep(int dx, int dy, RecyclerView.State state, Action action) {
+ if (getChildCount() == 0) {
+ stop();
+ }
+ if (DEBUG && mTargetVector != null
+ && ((mTargetVector.x * dx < 0 || mTargetVector.y * dy < 0))) {
+ throw new IllegalStateException("Scroll happened in the opposite direction"
+ + " of the target. Some calculations are wrong");
+ }
+ mInterimTargetDx = clampApplyScroll(mInterimTargetDx, dx);
+ mInterimTargetDy = clampApplyScroll(mInterimTargetDy, dy);
+
+ if (mInterimTargetDx == 0 && mInterimTargetDy == 0) {
+ updateActionForInterimTarget(action);
+ } // everything is valid, keep going
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onStop() {
+ mInterimTargetDx = mInterimTargetDy = 0;
+ mTargetVector = null;
+ }
+
+ /**
+ * Calculates the scroll speed.
+ *
+ * @param displayMetrics DisplayMetrics to be used for real dimension calculations
+ * @return The time (in ms) it should take for each pixel. For instance, if returned value is
+ * 2 ms, it means scrolling 1000 pixels with LinearInterpolation should take 2 seconds.
+ */
+ protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
+ return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
+ }
+
+ /**
+ * <p>Calculates the time for deceleration so that transition from LinearInterpolator to
+ * DecelerateInterpolator looks smooth.</p>
+ *
+ * @param dx Distance to scroll
+ * @return Time for DecelerateInterpolator to smoothly traverse the distance when transitioning
+ * from LinearInterpolation
+ */
+ protected int calculateTimeForDeceleration(int dx) {
+ // we want to cover same area with the linear interpolator for the first 10% of the
+ // interpolation. After that, deceleration will take control.
+ // 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);
+ }
+
+ /**
+ * Calculates the time it should take to scroll the given distance (in pixels)
+ *
+ * @param dx Distance in pixels that we want to scroll
+ * @return Time in milliseconds
+ * @see #calculateSpeedPerPixel(android.util.DisplayMetrics)
+ */
+ protected int calculateTimeForScrolling(int dx) {
+ return Math.round(Math.abs(dx) * MILLISECONDS_PER_PX);
+ }
+
+ /**
+ * When scrolling towards a child view, this method defines whether we should align the left
+ * or the right edge of the child with the parent RecyclerView.
+ *
+ * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
+ * @see #SNAP_TO_START
+ * @see #SNAP_TO_END
+ * @see #SNAP_TO_ANY
+ */
+ protected int getHorizontalSnapPreference() {
+ return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
+ mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
+ }
+
+ /**
+ * When scrolling towards a child view, this method defines whether we should align the top
+ * or the bottom edge of the child with the parent RecyclerView.
+ *
+ * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
+ * @see #SNAP_TO_START
+ * @see #SNAP_TO_END
+ * @see #SNAP_TO_ANY
+ */
+ protected int getVerticalSnapPreference() {
+ return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
+ mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
+ }
+
+ /**
+ * When the target scroll position is not a child of the RecyclerView, this method calculates
+ * a direction vector towards that child and triggers a smooth scroll.
+ *
+ * @see #computeScrollVectorForPosition(int)
+ */
+ protected void updateActionForInterimTarget(Action action) {
+ // find an interim target position
+ PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
+ if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
+ Log.e(TAG, "To support smooth scrolling, you should override \n"
+ + "LayoutManager#computeScrollVectorForPosition.\n"
+ + "Falling back to instant scroll");
+ final int target = getTargetPosition();
+ stop();
+ instantScrollToPosition(target);
+ return;
+ }
+ normalize(scrollVector);
+ mTargetVector = scrollVector;
+
+ mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
+ mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
+ final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
+ // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
+ // interim target. Since we track the distance travelled in onSeekTargetStep callback, it
+ // won't actually scroll more than what we need.
+ action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO)
+ , (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO)
+ , (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
+ }
+
+ private int clampApplyScroll(int tmpDt, int dt) {
+ final int before = tmpDt;
+ tmpDt -= dt;
+ if (before * tmpDt <= 0) { // changed sign, reached 0 or was 0, reset
+ return 0;
+ }
+ return tmpDt;
+ }
+
+ /**
+ * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
+ * {@link #calculateDyToMakeVisible(android.view.View, int)}
+ */
+ public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
+ snapPreference) {
+ switch (snapPreference) {
+ case SNAP_TO_START:
+ return boxStart - viewStart;
+ case SNAP_TO_END:
+ return boxEnd - viewEnd;
+ case SNAP_TO_ANY:
+ final int dtStart = boxStart - viewStart;
+ if (dtStart > 0) {
+ return dtStart;
+ }
+ final int dtEnd = boxEnd - viewEnd;
+ if (dtEnd < 0) {
+ return dtEnd;
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("snap preference should be one of the"
+ + " constants defined in SmoothScroller, starting with SNAP_");
+ }
+ return 0;
+ }
+
+ /**
+ * Calculates the vertical scroll amount necessary to make the given view fully visible
+ * inside the RecyclerView.
+ *
+ * @param view The view which we want to make fully visible
+ * @param snapPreference The edge which the view should snap to when entering the visible
+ * area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
+ * {@link #SNAP_TO_END}.
+ * @return The vertical scroll amount necessary to make the view visible with the given
+ * snap preference.
+ */
+ public int calculateDyToMakeVisible(View view, int snapPreference) {
+ final RecyclerView.LayoutManager layoutManager = getLayoutManager();
+ if (!layoutManager.canScrollVertically()) {
+ return 0;
+ }
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ final int top = layoutManager.getDecoratedTop(view) - params.topMargin;
+ final int bottom = layoutManager.getDecoratedBottom(view) + params.bottomMargin;
+ final int start = layoutManager.getPaddingTop();
+ final int end = layoutManager.getHeight() - layoutManager.getPaddingBottom();
+ return calculateDtToFit(top, bottom, start, end, snapPreference);
+ }
+
+ /**
+ * Calculates the horizontal scroll amount necessary to make the given view fully visible
+ * inside the RecyclerView.
+ *
+ * @param view The view which we want to make fully visible
+ * @param snapPreference The edge which the view should snap to when entering the visible
+ * area. One of {@link #SNAP_TO_START}, {@link #SNAP_TO_END} or
+ * {@link #SNAP_TO_END}
+ * @return The vertical scroll amount necessary to make the view visible with the given
+ * snap preference.
+ */
+ public int calculateDxToMakeVisible(View view, int snapPreference) {
+ final RecyclerView.LayoutManager layoutManager = getLayoutManager();
+ if (!layoutManager.canScrollHorizontally()) {
+ return 0;
+ }
+ final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
+ view.getLayoutParams();
+ final int left = layoutManager.getDecoratedLeft(view) - params.leftMargin;
+ final int right = layoutManager.getDecoratedRight(view) + params.rightMargin;
+ final int start = layoutManager.getPaddingLeft();
+ final int end = layoutManager.getWidth() - layoutManager.getPaddingRight();
+ return calculateDtToFit(left, right, start, end, snapPreference);
+ }
+
+ abstract public PointF computeScrollVectorForPosition(int targetPosition);
+}
diff --git a/v7/recyclerview/src/android/support/v7/widget/PositionMap.java b/v7/recyclerview/src/android/support/v7/widget/PositionMap.java
new file mode 100644
index 0000000..3777937
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/PositionMap.java
@@ -0,0 +1,460 @@
+/*
+ * 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.v7.widget;
+
+import java.util.ArrayList;
+
+/**
+ * Like a SparseArray, but with the ability to offset key ranges for bulk insertions/deletions.
+ */
+class PositionMap<E> implements Cloneable {
+ private static final Object DELETED = new Object();
+ private boolean mGarbage = false;
+
+ private int[] mKeys;
+ private Object[] mValues;
+ private int mSize;
+
+ /**
+ * Creates a new SparseArray containing no mappings.
+ */
+ public PositionMap() {
+ this(10);
+ }
+
+ /**
+ * Creates a new PositionMap containing no mappings that will not
+ * require any additional memory allocation to store the specified
+ * number of mappings. If you supply an initial capacity of 0, the
+ * sparse array will be initialized with a light-weight representation
+ * not requiring any additional array allocations.
+ */
+ public PositionMap(int initialCapacity) {
+ if (initialCapacity == 0) {
+ mKeys = ContainerHelpers.EMPTY_INTS;
+ mValues = ContainerHelpers.EMPTY_OBJECTS;
+ } else {
+ initialCapacity = idealIntArraySize(initialCapacity);
+ mKeys = new int[initialCapacity];
+ mValues = new Object[initialCapacity];
+ }
+ mSize = 0;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public PositionMap<E> clone() {
+ PositionMap<E> clone = null;
+ try {
+ clone = (PositionMap<E>) super.clone();
+ clone.mKeys = mKeys.clone();
+ clone.mValues = mValues.clone();
+ } catch (CloneNotSupportedException cnse) {
+ /* ignore */
+ }
+ return clone;
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or <code>null</code>
+ * if no such mapping has been made.
+ */
+ public E get(int key) {
+ return get(key, null);
+ }
+
+ /**
+ * Gets the Object mapped from the specified key, or the specified Object
+ * if no such mapping has been made.
+ */
+ @SuppressWarnings("unchecked")
+ public E get(int key, E valueIfKeyNotFound) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i < 0 || mValues[i] == DELETED) {
+ return valueIfKeyNotFound;
+ } else {
+ return (E) mValues[i];
+ }
+ }
+
+ /**
+ * Removes the mapping from the specified key, if there was any.
+ */
+ public void delete(int key) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ if (mValues[i] != DELETED) {
+ mValues[i] = DELETED;
+ mGarbage = true;
+ }
+ }
+ }
+
+ /**
+ * Alias for {@link #delete(int)}.
+ */
+ public void remove(int key) {
+ delete(key);
+ }
+
+ /**
+ * Removes the mapping at the specified index.
+ */
+ public void removeAt(int index) {
+ if (mValues[index] != DELETED) {
+ mValues[index] = DELETED;
+ mGarbage = true;
+ }
+ }
+
+ /**
+ * Remove a range of mappings as a batch.
+ *
+ * @param index Index to begin at
+ * @param size Number of mappings to remove
+ */
+ public void removeAtRange(int index, int size) {
+ final int end = Math.min(mSize, index + size);
+ for (int i = index; i < end; i++) {
+ removeAt(i);
+ }
+ }
+
+ public void insertKeyRange(int keyStart, int count) {
+
+ }
+
+ public void removeKeyRange(ArrayList<E> removedItems, int keyStart, int count) {
+
+ }
+
+ private void gc() {
+ // Log.e("SparseArray", "gc start with " + mSize);
+
+ int n = mSize;
+ int o = 0;
+ int[] keys = mKeys;
+ Object[] values = mValues;
+
+ for (int i = 0; i < n; i++) {
+ Object val = values[i];
+
+ if (val != DELETED) {
+ if (i != o) {
+ keys[o] = keys[i];
+ values[o] = val;
+ values[i] = null;
+ }
+
+ o++;
+ }
+ }
+
+ mGarbage = false;
+ mSize = o;
+
+ // Log.e("SparseArray", "gc end with " + mSize);
+ }
+
+ /**
+ * Adds a mapping from the specified key to the specified value,
+ * replacing the previous mapping from the specified key if there
+ * was one.
+ */
+ public void put(int key, E value) {
+ int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
+
+ if (i >= 0) {
+ mValues[i] = value;
+ } else {
+ i = ~i;
+
+ if (i < mSize && mValues[i] == DELETED) {
+ mKeys[i] = key;
+ mValues[i] = value;
+ return;
+ }
+
+ if (mGarbage && mSize >= mKeys.length) {
+ gc();
+
+ // Search again because indices may have changed.
+ i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+
+ if (mSize >= mKeys.length) {
+ int n = idealIntArraySize(mSize + 1);
+
+ int[] nkeys = new int[n];
+ Object[] nvalues = new Object[n];
+
+ // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+ mKeys = nkeys;
+ mValues = nvalues;
+ }
+
+ if (mSize - i != 0) {
+ // Log.e("SparseArray", "move " + (mSize - i));
+ System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
+ System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
+ }
+
+ mKeys[i] = key;
+ mValues[i] = value;
+ mSize++;
+ }
+ }
+
+ /**
+ * Returns the number of key-value mappings that this SparseArray
+ * currently stores.
+ */
+ public int size() {
+ if (mGarbage) {
+ gc();
+ }
+
+ return mSize;
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the key from the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ */
+ public int keyAt(int index) {
+ if (mGarbage) {
+ gc();
+ }
+
+ return mKeys[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, returns
+ * the value from the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ */
+ @SuppressWarnings("unchecked")
+ public E valueAt(int index) {
+ if (mGarbage) {
+ gc();
+ }
+
+ return (E) mValues[index];
+ }
+
+ /**
+ * Given an index in the range <code>0...size()-1</code>, sets a new
+ * value for the <code>index</code>th key-value mapping that this
+ * SparseArray stores.
+ */
+ public void setValueAt(int index, E value) {
+ if (mGarbage) {
+ gc();
+ }
+
+ mValues[index] = value;
+ }
+
+ /**
+ * Returns the index for which {@link #keyAt} would return the
+ * specified key, or a negative number if the specified
+ * key is not mapped.
+ */
+ public int indexOfKey(int key) {
+ if (mGarbage) {
+ gc();
+ }
+
+ return ContainerHelpers.binarySearch(mKeys, mSize, key);
+ }
+
+ /**
+ * Returns an index for which {@link #valueAt} would return the
+ * specified key, or a negative number if no keys map to the
+ * specified value.
+ * <p>Beware that this is a linear search, unlike lookups by key,
+ * and that multiple keys can map to the same value and this will
+ * find only one of them.
+ * <p>Note also that unlike most collections' {@code indexOf} methods,
+ * this method compares values using {@code ==} rather than {@code equals}.
+ */
+ public int indexOfValue(E value) {
+ if (mGarbage) {
+ gc();
+ }
+
+ for (int i = 0; i < mSize; i++)
+ if (mValues[i] == value)
+ return i;
+
+ return -1;
+ }
+
+ /**
+ * Removes all key-value mappings from this SparseArray.
+ */
+ public void clear() {
+ int n = mSize;
+ Object[] values = mValues;
+
+ for (int i = 0; i < n; i++) {
+ values[i] = null;
+ }
+
+ mSize = 0;
+ mGarbage = false;
+ }
+
+ /**
+ * Puts a key/value pair into the array, optimizing for the case where
+ * the key is greater than all existing keys in the array.
+ */
+ public void append(int key, E value) {
+ if (mSize != 0 && key <= mKeys[mSize - 1]) {
+ put(key, value);
+ return;
+ }
+
+ if (mGarbage && mSize >= mKeys.length) {
+ gc();
+ }
+
+ int pos = mSize;
+ if (pos >= mKeys.length) {
+ int n = idealIntArraySize(pos + 1);
+
+ int[] nkeys = new int[n];
+ Object[] nvalues = new Object[n];
+
+ // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
+ System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
+ System.arraycopy(mValues, 0, nvalues, 0, mValues.length);
+
+ mKeys = nkeys;
+ mValues = nvalues;
+ }
+
+ mKeys[pos] = key;
+ mValues[pos] = value;
+ mSize = pos + 1;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * <p>This implementation composes a string by iterating over its mappings. If
+ * this map contains itself as a value, the string "(this Map)"
+ * will appear in its place.
+ */
+ @Override
+ public String toString() {
+ if (size() <= 0) {
+ return "{}";
+ }
+
+ StringBuilder buffer = new StringBuilder(mSize * 28);
+ buffer.append('{');
+ for (int i=0; i<mSize; i++) {
+ if (i > 0) {
+ buffer.append(", ");
+ }
+ int key = keyAt(i);
+ buffer.append(key);
+ buffer.append('=');
+ Object value = valueAt(i);
+ if (value != this) {
+ buffer.append(value);
+ } else {
+ buffer.append("(this Map)");
+ }
+ }
+ buffer.append('}');
+ return buffer.toString();
+ }
+
+ static int idealByteArraySize(int need) {
+ for (int i = 4; i < 32; i++)
+ if (need <= (1 << i) - 12)
+ return (1 << i) - 12;
+
+ return need;
+ }
+
+ static int idealBooleanArraySize(int need) {
+ return idealByteArraySize(need);
+ }
+
+ static int idealShortArraySize(int need) {
+ return idealByteArraySize(need * 2) / 2;
+ }
+
+ static int idealCharArraySize(int need) {
+ return idealByteArraySize(need * 2) / 2;
+ }
+
+ static int idealIntArraySize(int need) {
+ return idealByteArraySize(need * 4) / 4;
+ }
+
+ static int idealFloatArraySize(int need) {
+ return idealByteArraySize(need * 4) / 4;
+ }
+
+ static int idealObjectArraySize(int need) {
+ return idealByteArraySize(need * 4) / 4;
+ }
+
+ static int idealLongArraySize(int need) {
+ return idealByteArraySize(need * 8) / 8;
+ }
+
+ static class ContainerHelpers {
+ static final boolean[] EMPTY_BOOLEANS = new boolean[0];
+ static final int[] EMPTY_INTS = new int[0];
+ static final long[] EMPTY_LONGS = new long[0];
+ static final Object[] EMPTY_OBJECTS = new Object[0];
+
+ // This is Arrays.binarySearch(), but doesn't do any argument validation.
+ static int binarySearch(int[] array, int size, int value) {
+ int lo = 0;
+ int hi = size - 1;
+
+ while (lo <= hi) {
+ final int mid = (lo + hi) >>> 1;
+ final int midVal = array[mid];
+
+ if (midVal < value) {
+ lo = mid + 1;
+ } else if (midVal > value) {
+ hi = mid - 1;
+ } else {
+ return mid; // value found
+ }
+ }
+ return ~lo; // value not present
+ }
+ }
+
+}
diff --git a/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
new file mode 100644
index 0000000..2a32b55
--- /dev/null
+++ b/v7/recyclerview/src/android/support/v7/widget/RecyclerView.java
@@ -0,0 +1,5089 @@
+/*
+ * 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.v7.widget;
+
+import android.content.Context;
+import android.database.Observable;
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.Nullable;
+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 android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+import android.view.FocusFinder;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.animation.Interpolator;
+
+import java.util.ArrayList;
+
+/**
+ * A flexible view for providing a limited window into a large data set.
+ *
+ * <h3>Glossary of terms:</h3>
+ *
+ * <ul>
+ * <li><em>Adapter:</em> A subclass of {@link Adapter} responsible for providing views
+ * that represent items in a data set.</li>
+ * <li><em>Position:</em> The position of a data item within an <em>Adapter</em>.</li>
+ * <li><em>Index:</em> The index of an attached child view as used in a call to
+ * {@link ViewGroup#getChildAt}. Contrast with <em>Position.</em></li>
+ * <li><em>Binding:</em> The process of preparing a child view to display data corresponding
+ * to a <em>position</em> within the adapter.</li>
+ * <li><em>Recycle (view):</em> A view previously used to display data for a specific adapter
+ * position may be placed in a cache for later reuse to display the same type of data again
+ * later. This can drastically improve performance by skipping initial layout inflation
+ * or construction.</li>
+ * <li><em>Scrap (view):</em> A child view that has entered into a temporarily detached
+ * state during layout. Scrap views may be reused without becoming fully detached
+ * from the parent RecyclerView, either unmodified if no rebinding is required or modified
+ * by the adapter if the view was considered <em>dirty</em>.</li>
+ * <li><em>Dirty (view):</em> A child view that must be rebound by the adapter before
+ * being displayed.</li>
+ * </ul>
+ */
+public class RecyclerView extends ViewGroup {
+ private static final String TAG = "RecyclerView";
+
+ private static final boolean DEBUG = false;
+
+ private static final boolean DISPATCH_TEMP_DETACH = false;
+ public static final int HORIZONTAL = 0;
+ public static final int VERTICAL = 1;
+
+ public static final int NO_POSITION = -1;
+ public static final long NO_ID = -1;
+ public static final int INVALID_TYPE = -1;
+
+ private static final int MAX_SCROLL_DURATION = 2000;
+
+ private final RecyclerViewDataObserver mObserver = new RecyclerViewDataObserver();
+
+ private final Recycler mRecycler = new Recycler();
+
+ private SavedState mPendingSavedState;
+
+ /**
+ * Note: this Runnable is only ever posted if:
+ * 1) We've been through first layout
+ * 2) We know we have a fixed size (mHasFixedSize)
+ * 3) We're attached
+ */
+ private final Runnable mUpdateChildViewsRunnable = new Runnable() {
+ public void run() {
+ eatRequestLayout();
+ updateChildViews();
+ resumeRequestLayout(true);
+ }
+ };
+
+ private final Rect mTempRect = new Rect();
+
+ private final ArrayList<UpdateOp> mPendingUpdates = new ArrayList<UpdateOp>();
+ private Pools.Pool<UpdateOp> mUpdateOpPool = new Pools.SimplePool<UpdateOp>(UpdateOp.POOL_SIZE);
+
+ private Adapter mAdapter;
+ private LayoutManager mLayout;
+ private RecyclerListener mRecyclerListener;
+ private final ArrayList<ItemDecoration> mItemDecorations = new ArrayList<ItemDecoration>();
+ private final ArrayList<OnItemTouchListener> mOnItemTouchListeners =
+ new ArrayList<OnItemTouchListener>();
+ private OnItemTouchListener mActiveOnItemTouchListener;
+ private boolean mIsAttached;
+ private boolean mHasFixedSize;
+ private boolean mFirstLayoutComplete;
+ private boolean mEatRequestLayout;
+ private boolean mLayoutRequestEaten;
+ private boolean mAdapterUpdateDuringMeasure;
+ private boolean mStructureChanged;
+ private final boolean mPostUpdatesOnAnimation;
+
+ private EdgeEffectCompat mLeftGlow, mTopGlow, mRightGlow, mBottomGlow;
+
+ private static final int INVALID_POINTER = -1;
+
+ /**
+ * The RecyclerView is not currently scrolling.
+ * @see #getScrollState()
+ */
+ public static final int SCROLL_STATE_IDLE = 0;
+
+ /**
+ * The RecyclerView is currently being dragged by outside input such as user touch input.
+ * @see #getScrollState()
+ */
+ public static final int SCROLL_STATE_DRAGGING = 1;
+
+ /**
+ * The RecyclerView is currently animating to a final position while not under
+ * outside control.
+ * @see #getScrollState()
+ */
+ public static final int SCROLL_STATE_SETTLING = 2;
+
+ // Touch/scrolling handling
+
+ private int mScrollState = SCROLL_STATE_IDLE;
+ private int mScrollPointerId = INVALID_POINTER;
+ private VelocityTracker mVelocityTracker;
+ private int mInitialTouchX;
+ private int mInitialTouchY;
+ private int mLastTouchX;
+ private int mLastTouchY;
+ private final int mTouchSlop;
+ private final int mMinFlingVelocity;
+ private final int mMaxFlingVelocity;
+
+ private final ViewFlinger mViewFlinger = new ViewFlinger();
+
+ private final State mState = new State();
+
+ private OnScrollListener mScrollListener;
+
+ private static final Interpolator sQuinticInterpolator = new Interpolator() {
+ public float getInterpolation(float t) {
+ t -= 1.0f;
+ return t * t * t * t * t + 1.0f;
+ }
+ };
+
+ public RecyclerView(Context context) {
+ this(context, null);
+ }
+
+ public RecyclerView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RecyclerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final int version = Build.VERSION.SDK_INT;
+ mPostUpdatesOnAnimation = version >= 16;
+
+ final ViewConfiguration vc = ViewConfiguration.get(context);
+ mTouchSlop = vc.getScaledTouchSlop();
+ mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+ mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
+ setWillNotDraw(ViewCompat.getOverScrollMode(this) == ViewCompat.OVER_SCROLL_NEVER);
+ }
+
+ /**
+ * RecyclerView can perform several optimizations if it can know in advance that changes in
+ * adapter content cannot change the size of the RecyclerView itself.
+ * If your use of RecyclerView falls into this category, set this to true.
+ *
+ * @param hasFixedSize true if adapter changes cannot affect the size of the RecyclerView.
+ */
+ public void setHasFixedSize(boolean hasFixedSize) {
+ mHasFixedSize = hasFixedSize;
+ }
+
+ /**
+ * @return true if the app has specified that changes in adapter content cannot change
+ * the size of the RecyclerView itself.
+ */
+ public boolean hasFixedSize() {
+ return mHasFixedSize;
+ }
+
+ /**
+ * Set a new adapter to provide child views on demand.
+ *
+ * @param adapter The new adapter to set, or null to set no adapter.
+ */
+ public void setAdapter(Adapter adapter) {
+ if (mAdapter != null) {
+ mAdapter.unregisterAdapterDataObserver(mObserver);
+ }
+ final Adapter oldAdapter = mAdapter;
+ mAdapter = adapter;
+ if (adapter != null) {
+ adapter.registerAdapterDataObserver(mObserver);
+ }
+ if (mLayout != null) {
+ mLayout.onAdapterChanged(oldAdapter, mAdapter);
+ }
+ mRecycler.onAdapterChanged(oldAdapter, mAdapter);
+ mStructureChanged = true;
+ markKnownViewsInvalid();
+ requestLayout();
+ }
+
+ /**
+ * Retrieves the previously set adapter or null if no adapter is set.
+ *
+ * @return The previously set adapter
+ * @see #setAdapter(Adapter)
+ */
+ public Adapter getAdapter() {
+ return mAdapter;
+ }
+
+ /**
+ * Register a listener that will be notified whenever a child view is recycled.
+ *
+ * <p>This listener will be called when a LayoutManager or the RecyclerView decides
+ * that a child view is no longer needed. If an application associates expensive
+ * or heavyweight data with item views, this may be a good place to release
+ * or free those resources.</p>
+ *
+ * @param listener Listener to register, or null to clear
+ */
+ public void setRecyclerListener(RecyclerListener listener) {
+ mRecyclerListener = listener;
+ }
+
+ /**
+ * Set the {@link LayoutManager} that this RecyclerView will use.
+ *
+ * <p>In contrast to other adapter-backed views such as {@link android.widget.ListView}
+ * or {@link android.widget.GridView}, RecyclerView allows client code to provide custom
+ * layout arrangements for child views. These arrangements are controlled by the
+ * {@link LayoutManager}. A LayoutManager must be provided for RecyclerView to function.</p>
+ *
+ * <p>Several default strategies are provided for common uses such as lists and grids.</p>
+ *
+ * @param layout LayoutManager to use
+ */
+ public void setLayoutManager(LayoutManager layout) {
+ if (layout == mLayout) {
+ return;
+ }
+
+ mRecycler.clear();
+ removeAllViews();
+ if (mLayout != null) {
+ if (mIsAttached) {
+ mLayout.onDetachedFromWindow(this);
+ }
+ mLayout.mRecyclerView = null;
+ }
+ 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;
+ if (mIsAttached) {
+ mLayout.onAttachedToWindow(this);
+ }
+ }
+ requestLayout();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ SavedState state = new SavedState(super.onSaveInstanceState());
+ if (mPendingSavedState != null) {
+ state.copyFrom(mPendingSavedState);
+ } else if (mLayout != null) {
+ state.mLayoutState = mLayout.onSaveInstanceState();
+ } else {
+ state.mLayoutState = null;
+ }
+
+ return state;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ mPendingSavedState = (SavedState) state;
+ super.onRestoreInstanceState(mPendingSavedState.getSuperState());
+ if (mLayout != null && mPendingSavedState.mLayoutState != null) {
+ mLayout.onRestoreInstanceState(mPendingSavedState.mLayoutState);
+ }
+ }
+
+ /**
+ * Return the {@link LayoutManager} currently responsible for
+ * layout policy for this RecyclerView.
+ *
+ * @return The currently bound LayoutManager
+ */
+ public LayoutManager getLayoutManager() {
+ return mLayout;
+ }
+
+ /**
+ * Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null;
+ * if no pool is set for this view a new one will be created. See
+ * {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information.
+ *
+ * @return The pool used to store recycled item views for reuse.
+ * @see #setRecycledViewPool(RecycledViewPool)
+ */
+ public RecycledViewPool getRecycledViewPool() {
+ return mRecycler.getRecycledViewPool();
+ }
+
+ /**
+ * Recycled view pools allow multiple RecyclerViews to share a common pool of scrap views.
+ * This can be useful if you have multiple RecyclerViews with adapters that use the same
+ * view types, for example if you have several data sets with the same kinds of item views
+ * displayed by a {@link android.support.v4.view.ViewPager ViewPager}.
+ *
+ * @param pool Pool to set. If this parameter is null a new pool will be created and used.
+ */
+ public void setRecycledViewPool(RecycledViewPool pool) {
+ mRecycler.setRecycledViewPool(pool);
+ }
+
+ /**
+ * Set the number of offscreen views to retain before adding them to the potentially shared
+ * {@link #getRecycledViewPool() recycled view pool}.
+ *
+ * <p>The offscreen view cache stays aware of changes in the attached adapter, allowing
+ * a LayoutManager to reuse those views unmodified without needing to return to the adapter
+ * to rebind them.</p>
+ *
+ * @param size Number of views to cache offscreen before returning them to the general
+ * recycled view pool
+ */
+ public void setItemViewCacheSize(int size) {
+ mRecycler.setViewCacheSize(size);
+ }
+
+ /**
+ * Return the current scrolling state of the RecyclerView.
+ *
+ * @return {@link #SCROLL_STATE_IDLE}, {@link #SCROLL_STATE_DRAGGING} or
+ * {@link #SCROLL_STATE_SETTLING}
+ */
+ public int getScrollState() {
+ return mScrollState;
+ }
+
+ private void setScrollState(int state) {
+ if (state == mScrollState) {
+ return;
+ }
+ mScrollState = state;
+ if (state != SCROLL_STATE_SETTLING) {
+ stopScroll();
+ }
+ if (mScrollListener != null) {
+ mScrollListener.onScrollStateChanged(state);
+ }
+ }
+
+ /**
+ * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
+ * affect both measurement and drawing of individual item views.
+ *
+ * <p>Item decorations are ordered. Decorations placed earlier in the list will
+ * be run/queried/drawn first for their effects on item views. Padding added to views
+ * will be nested; a padding added by an earlier decoration will mean further
+ * item decorations in the list will be asked to draw/pad within the previous decoration's
+ * given area.</p>
+ *
+ * @param decor Decoration to add
+ * @param index Position in the decoration chain to insert this decoration at. If this value
+ * is negative the decoration will be added at the end.
+ */
+ public void addItemDecoration(ItemDecoration decor, int index) {
+ if (mItemDecorations.isEmpty()) {
+ setWillNotDraw(false);
+ }
+ if (index < 0) {
+ mItemDecorations.add(decor);
+ } else {
+ mItemDecorations.add(index, decor);
+ }
+ markItemDecorInsetsDirty();
+ requestLayout();
+ }
+
+ /**
+ * Add an {@link ItemDecoration} to this RecyclerView. Item decorations can
+ * affect both measurement and drawing of individual item views.
+ *
+ * <p>Item decorations are ordered. Decorations placed earlier in the list will
+ * be run/queried/drawn first for their effects on item views. Padding added to views
+ * will be nested; a padding added by an earlier decoration will mean further
+ * item decorations in the list will be asked to draw/pad within the previous decoration's
+ * given area.</p>
+ *
+ * @param decor Decoration to add
+ */
+ public void addItemDecoration(ItemDecoration decor) {
+ addItemDecoration(decor, -1);
+ }
+
+ /**
+ * Remove an {@link ItemDecoration} from this RecyclerView.
+ *
+ * <p>The given decoration will no longer impact the measurement and drawing of
+ * item views.</p>
+ *
+ * @param decor Decoration to remove
+ * @see #addItemDecoration(ItemDecoration)
+ */
+ public void removeItemDecoration(ItemDecoration decor) {
+ mItemDecorations.remove(decor);
+ if (mItemDecorations.isEmpty()) {
+ setWillNotDraw(ViewCompat.getOverScrollMode(this) == ViewCompat.OVER_SCROLL_NEVER);
+ }
+ markItemDecorInsetsDirty();
+ requestLayout();
+ }
+
+ /**
+ * Set a listener that will be notified of any changes in scroll state or position.
+ *
+ * @param listener Listener to set or null to clear
+ */
+ public void setOnScrollListener(OnScrollListener listener) {
+ mScrollListener = listener;
+ }
+
+ /**
+ * Convenience method to scroll to a certain position.
+ *
+ * RecyclerView does not implement scrolling logic, rather forwards the call to
+ * {@link android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)}
+ * @param position Scroll to this adapter position
+ * @see android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)
+ */
+ public void scrollToPosition(int position) {
+ stopScroll();
+ mLayout.scrollToPosition(position);
+ awakenScrollBars();
+ }
+
+ /**
+ * <p>Starts a smooth scroll to an adapter position.</p>
+ *
+ * <p>To support smooth scrolling, you must override
+ * {@link LayoutManager#smoothScrollToPosition(RecyclerView, RecyclerView.Adapter, int)} and
+ * create a {@link SmoothScroller}.</p>
+ * <p>{@link LayoutManager} is responsible for creating the actual scroll action. If you want to
+ * provide a custom smooth scroll logic, override
+ * {@link LayoutManager#smoothScrollToPosition(RecyclerView, RecyclerView.Adapter, int)} in your
+ * LayoutManager.</p>
+ *
+ * @param position The adapter position to scroll to
+ * @see LayoutManager#smoothScrollToPosition(RecyclerView, RecyclerView.Adapter, int)
+ */
+ public void smoothScrollToPosition(int position) {
+ mLayout.smoothScrollToPosition(this, mAdapter, position);
+ }
+
+ @Override
+ public void scrollTo(int x, int y) {
+ throw new UnsupportedOperationException(
+ "RecyclerView does not support scrolling to an absolute position.");
+ }
+
+ @Override
+ public void scrollBy(int x, int y) {
+ if (mLayout == null) {
+ throw new IllegalStateException("Cannot scroll without a LayoutManager set. " +
+ "Call setLayoutManager with a non-null argument.");
+ }
+ final boolean canScrollHorizontal = mLayout.canScrollHorizontally();
+ final boolean canScrollVertical = mLayout.canScrollVertically();
+ if (canScrollHorizontal || canScrollVertical) {
+ scrollByInternal(canScrollHorizontal ? x : 0, canScrollVertical ? y : 0);
+ }
+ }
+
+ /**
+ * Does not perform bounds checking. Used by internal methods that have already validated input.
+ */
+ void scrollByInternal(int x, int y) {
+ int overscrollX = 0, overscrollY = 0;
+ if (mAdapter != null) {
+ eatRequestLayout();
+ if (x != 0) {
+ final int hresult = mLayout.scrollHorizontallyBy(x, getAdapter(), mRecycler, mState);
+ overscrollX = x - hresult;
+ }
+ if (y != 0) {
+ final int vresult = mLayout.scrollVerticallyBy(y, getAdapter(), mRecycler, mState);
+ overscrollY = y - vresult;
+ }
+ resumeRequestLayout(false);
+ }
+
+ if (!mItemDecorations.isEmpty()) {
+ invalidate();
+ }
+ if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) {
+ pullGlows(overscrollX, overscrollY);
+ }
+ if (mScrollListener != null && (x != 0 || y != 0)) {
+ mScrollListener.onScrolled(x, y);
+ }
+ if (!awakenScrollBars()) {
+ invalidate();
+ }
+ }
+
+ /**
+ * <p>Compute the horizontal offset of the horizontal scrollbar's thumb within the horizontal
+ * range. This value is used to compute the length of the thumb within the scrollbar's track.
+ * </p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollExtent()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link android.support.v7.widget.RecyclerView.LayoutManager#computeHorizontalScrollOffset(
+ * RecyclerView.Adapter)} in your LayoutManager. </p>
+ *
+ * @return The horizontal offset of the scrollbar's thumb
+ * @see android.support.v7.widget.RecyclerView.LayoutManager#computeHorizontalScrollOffset
+ * (RecyclerView.Adapter)
+ */
+ @Override
+ protected int computeHorizontalScrollOffset() {
+ return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollOffset(mAdapter)
+ : 0;
+ }
+
+ /**
+ * <p>Compute the horizontal extent of the horizontal scrollbar's thumb within the
+ * horizontal range. This value is used to compute the length of the thumb within the
+ * scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeHorizontalScrollRange()} and {@link #computeHorizontalScrollOffset()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link android.support.v7.widget.RecyclerView.LayoutManager#computeHorizontalScrollExtent(
+ * RecyclerView.Adapter)} in your LayoutManager.</p>
+ *
+ * @return The horizontal extent of the scrollbar's thumb
+ * @see android.support.v7.widget.RecyclerView.LayoutManager#computeHorizontalScrollExtent(
+ * RecyclerView.Adapter)
+ */
+ @Override
+ protected int computeHorizontalScrollExtent() {
+ return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollExtent(mAdapter)
+ : 0;
+ }
+
+ /**
+ * <p>Compute the horizontal range that the horizontal scrollbar represents.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeHorizontalScrollExtent()} and {@link #computeHorizontalScrollOffset()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link android.support.v7.widget.RecyclerView.LayoutManager#computeHorizontalScrollRange(
+ * RecyclerView.Adapter)} in your LayoutManager.</p>
+ *
+ * @return The total horizontal range represented by the vertical scrollbar
+ * @see android.support.v7.widget.RecyclerView.LayoutManager#computeHorizontalScrollRange(
+ * RecyclerView.Adapter)
+ */
+ @Override
+ protected int computeHorizontalScrollRange() {
+ return mLayout.canScrollHorizontally() ? mLayout.computeHorizontalScrollRange(mAdapter) : 0;
+ }
+
+ /**
+ * <p>Compute the vertical offset of the vertical scrollbar's thumb within the vertical range.
+ * This value is used to compute the length of the thumb within the scrollbar's track. </p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link android.support.v7.widget.RecyclerView.LayoutManager#computeVerticalScrollOffset(
+ * RecyclerView.Adapter)} in your LayoutManager.</p>
+ *
+ * @return The vertical offset of the scrollbar's thumb
+ * @see android.support.v7.widget.RecyclerView.LayoutManager#computeVerticalScrollOffset
+ * (RecyclerView.Adapter)
+ */
+ @Override
+ protected int computeVerticalScrollOffset() {
+ return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollOffset(mAdapter) : 0;
+ }
+
+ /**
+ * <p>Compute the vertical extent of the vertical scrollbar's thumb within the vertical range.
+ * This value is used to compute the length of the thumb within the scrollbar's track.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollOffset()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link android.support.v7.widget.RecyclerView.LayoutManager#computeVerticalScrollExtent(
+ * RecyclerView.Adapter)} in your LayoutManager.</p>
+ *
+ * @return The vertical extent of the scrollbar's thumb
+ * @see android.support.v7.widget.RecyclerView.LayoutManager#computeVerticalScrollExtent(
+ * RecyclerView.Adapter)
+ */
+ @Override
+ protected int computeVerticalScrollExtent() {
+ return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollExtent(mAdapter) : 0;
+ }
+
+ /**
+ * <p>Compute the vertical range that the vertical scrollbar represents.</p>
+ *
+ * <p>The range is expressed in arbitrary units that must be the same as the units used by
+ * {@link #computeVerticalScrollExtent()} and {@link #computeVerticalScrollOffset()}.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * <p>If you want to support scroll bars, override
+ * {@link android.support.v7.widget.RecyclerView.LayoutManager#computeVerticalScrollRange(
+ * RecyclerView.Adapter)} in your LayoutManager.</p>
+ *
+ * @return The total vertical range represented by the vertical scrollbar
+ * @see android.support.v7.widget.RecyclerView.LayoutManager#computeVerticalScrollRange(
+ * RecyclerView.Adapter)
+ */
+ @Override
+ protected int computeVerticalScrollRange() {
+ return mLayout.canScrollVertically() ? mLayout.computeVerticalScrollRange(mAdapter) : 0;
+ }
+
+
+ void eatRequestLayout() {
+ if (!mEatRequestLayout) {
+ mEatRequestLayout = true;
+ mLayoutRequestEaten = false;
+ }
+ }
+
+ void resumeRequestLayout(boolean performLayoutChildren) {
+ if (mEatRequestLayout) {
+ if (performLayoutChildren && mLayoutRequestEaten &&
+ mLayout != null && mAdapter != null) {
+ layoutChildren();
+ }
+ mEatRequestLayout = false;
+ mLayoutRequestEaten = false;
+ }
+ }
+
+ /**
+ * Animate a scroll by the given amount of pixels along either axis.
+ *
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ */
+ public void smoothScrollBy(int dx, int dy) {
+ if (dx != 0 || dy != 0) {
+ mViewFlinger.smoothScrollBy(dx, dy);
+ }
+ }
+
+ /**
+ * Begin a standard fling with an initial velocity along each axis in pixels per second.
+ * If the velocity given is below the system-defined minimum this method will return false
+ * and no fling will occur.
+ *
+ * @param velocityX Initial horizontal velocity in pixels per second
+ * @param velocityY Initial vertical velocity in pixels per second
+ * @return true if the fling was started, false if the velocity was too low to fling
+ */
+ public boolean fling(int velocityX, int velocityY) {
+ if (Math.abs(velocityX) < mMinFlingVelocity) {
+ velocityX = 0;
+ }
+ if (Math.abs(velocityY) < mMinFlingVelocity) {
+ velocityY = 0;
+ }
+ velocityX = Math.max(-mMaxFlingVelocity, Math.min(velocityX, mMaxFlingVelocity));
+ velocityY = Math.max(-mMaxFlingVelocity, Math.min(velocityY, mMaxFlingVelocity));
+ if (velocityX != 0 || velocityY != 0) {
+ mViewFlinger.fling(velocityX, velocityY);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Stop any current scroll in progress, such as one started by
+ * {@link #smoothScrollBy(int, int)}, {@link #fling(int, int)} or a touch-initiated fling.
+ */
+ public void stopScroll() {
+ mViewFlinger.stop();
+ mLayout.stopSmoothScroller();
+ }
+
+ /**
+ * Apply a pull to relevant overscroll glow effects
+ */
+ private void pullGlows(int overscrollX, int overscrollY) {
+ if (overscrollX < 0) {
+ if (mLeftGlow == null) {
+ mLeftGlow = new EdgeEffectCompat(getContext());
+ mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
+ getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
+ }
+ mLeftGlow.onPull(-overscrollX / (float) getWidth());
+ } else if (overscrollX > 0) {
+ if (mRightGlow == null) {
+ mRightGlow = new EdgeEffectCompat(getContext());
+ mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
+ getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
+ }
+ mRightGlow.onPull(overscrollX / (float) getWidth());
+ }
+
+ if (overscrollY < 0) {
+ if (mTopGlow == null) {
+ mTopGlow = new EdgeEffectCompat(getContext());
+ mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
+ getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
+ }
+ mTopGlow.onPull(-overscrollY / (float) getHeight());
+ } else if (overscrollY > 0) {
+ if (mBottomGlow == null) {
+ mBottomGlow = new EdgeEffectCompat(getContext());
+ mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
+ getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
+ }
+ mBottomGlow.onPull(overscrollY / (float) getHeight());
+ }
+
+ if (overscrollX != 0 || overscrollY != 0) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
+ private void releaseGlows() {
+ boolean needsInvalidate = false;
+ if (mLeftGlow != null) needsInvalidate = mLeftGlow.onRelease();
+ if (mTopGlow != null) needsInvalidate |= mTopGlow.onRelease();
+ if (mRightGlow != null) needsInvalidate |= mRightGlow.onRelease();
+ if (mBottomGlow != null) needsInvalidate |= mBottomGlow.onRelease();
+ if (needsInvalidate) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
+ void absorbGlows(int velocityX, int velocityY) {
+ if (velocityX < 0) {
+ if (mLeftGlow == null) {
+ mLeftGlow = new EdgeEffectCompat(getContext());
+ mLeftGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
+ getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
+ }
+ mLeftGlow.onAbsorb(-velocityX);
+ } else if (velocityX > 0) {
+ if (mRightGlow == null) {
+ mRightGlow = new EdgeEffectCompat(getContext());
+ mRightGlow.setSize(getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
+ getMeasuredWidth() - getPaddingLeft() - getPaddingRight());
+ }
+ mRightGlow.onAbsorb(velocityX);
+ }
+
+ if (velocityY < 0) {
+ if (mTopGlow == null) {
+ mTopGlow = new EdgeEffectCompat(getContext());
+ mTopGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
+ getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
+ }
+ mTopGlow.onAbsorb(-velocityY);
+ } else if (velocityY > 0) {
+ if (mBottomGlow == null) {
+ mBottomGlow = new EdgeEffectCompat(getContext());
+ mBottomGlow.setSize(getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
+ getMeasuredHeight() - getPaddingTop() - getPaddingBottom());
+ }
+ mBottomGlow.onAbsorb(velocityY);
+ }
+
+ if (velocityX != 0 || velocityY != 0) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
+ // Focus handling
+
+ @Override
+ public View focusSearch(View focused, int direction) {
+ View result = mLayout.onInterceptFocusSearch(focused, direction);
+ if (result != null) {
+ return result;
+ }
+ final FocusFinder ff = FocusFinder.getInstance();
+ result = ff.findNextFocus(this, focused, direction);
+ if (result == null && mAdapter != null) {
+ eatRequestLayout();
+ result = mLayout.onFocusSearchFailed(focused, direction, mAdapter, mRecycler);
+ resumeRequestLayout(false);
+ }
+ return result != null ? result : super.focusSearch(focused, direction);
+ }
+
+ @Override
+ public void requestChildFocus(View child, View focused) {
+ if (!mLayout.onRequestChildFocus(this, child, focused)) {
+ mTempRect.set(0, 0, focused.getWidth(), focused.getHeight());
+ offsetDescendantRectToMyCoords(focused, mTempRect);
+ offsetRectIntoDescendantCoords(child, mTempRect);
+ requestChildRectangleOnScreen(child, mTempRect, !mFirstLayoutComplete);
+ }
+ super.requestChildFocus(child, focused);
+ }
+
+ @Override
+ public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
+ return mLayout.requestChildRectangleOnScreen(child, rect, immediate);
+ }
+
+ @Override
+ public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
+ if (!mLayout.onAddFocusables(this, views, direction, focusableMode)) {
+ super.addFocusables(views, direction, focusableMode);
+ }
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mIsAttached = true;
+ mFirstLayoutComplete = false;
+ if (mLayout != null) {
+ mLayout.onAttachedToWindow(this);
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ 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);
+ }
+ }
+
+ /**
+ * Add an {@link OnItemTouchListener} to intercept touch events before they are dispatched
+ * to child views or this view's standard scrolling behavior.
+ *
+ * <p>Client code may use listeners to implement item manipulation behavior. Once a listener
+ * returns true from
+ * {@link OnItemTouchListener#onInterceptTouchEvent(RecyclerView, MotionEvent)} its
+ * {@link OnItemTouchListener#onTouchEvent(RecyclerView, MotionEvent)} method will be called
+ * for each incoming MotionEvent until the end of the gesture.</p>
+ *
+ * @param listener Listener to add
+ */
+ public void addOnItemTouchListener(OnItemTouchListener listener) {
+ mOnItemTouchListeners.add(listener);
+ }
+
+ /**
+ * Remove an {@link OnItemTouchListener}. It will no longer be able to intercept touch events.
+ *
+ * @param listener Listener to remove
+ */
+ public void removeOnItemTouchListener(OnItemTouchListener listener) {
+ mOnItemTouchListeners.remove(listener);
+ if (mActiveOnItemTouchListener == listener) {
+ mActiveOnItemTouchListener = null;
+ }
+ }
+
+ private boolean dispatchOnItemTouchIntercept(MotionEvent e) {
+ final int action = e.getAction();
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_DOWN) {
+ mActiveOnItemTouchListener = null;
+ }
+
+ final int listenerCount = mOnItemTouchListeners.size();
+ for (int i = 0; i < listenerCount; i++) {
+ final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
+ if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
+ mActiveOnItemTouchListener = listener;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean dispatchOnItemTouch(MotionEvent e) {
+ final int action = e.getAction();
+ if (mActiveOnItemTouchListener != null) {
+ if (action == MotionEvent.ACTION_DOWN) {
+ // Stale state from a previous gesture, we're starting a new one. Clear it.
+ mActiveOnItemTouchListener = null;
+ } else {
+ mActiveOnItemTouchListener.onTouchEvent(this, e);
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ // Clean up for the next gesture.
+ mActiveOnItemTouchListener = null;
+ }
+ return true;
+ }
+ }
+
+ // Listeners will have already received the ACTION_DOWN via dispatchOnItemTouchIntercept
+ // as called from onInterceptTouchEvent; skip it.
+ if (action != MotionEvent.ACTION_DOWN) {
+ final int listenerCount = mOnItemTouchListeners.size();
+ for (int i = 0; i < listenerCount; i++) {
+ final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
+ if (listener.onInterceptTouchEvent(this, e)) {
+ mActiveOnItemTouchListener = listener;
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent e) {
+ if (dispatchOnItemTouchIntercept(e)) {
+ cancelTouch();
+ return true;
+ }
+
+ final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
+ final boolean canScrollVertically = mLayout.canScrollVertically();
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(e);
+
+ final int action = MotionEventCompat.getActionMasked(e);
+ final int actionIndex = MotionEventCompat.getActionIndex(e);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
+ mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
+ mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
+
+ if (mScrollState == SCROLL_STATE_SETTLING) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ setScrollState(SCROLL_STATE_DRAGGING);
+ }
+ break;
+
+ case MotionEventCompat.ACTION_POINTER_DOWN:
+ mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
+ mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
+ mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
+ if (index < 0) {
+ Log.e(TAG, "Error processing scroll; pointer index for id " +
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
+ return false;
+ }
+
+ final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
+ final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
+ if (mScrollState != SCROLL_STATE_DRAGGING) {
+ final int dx = x - mInitialTouchX;
+ final int dy = y - mInitialTouchY;
+ boolean startScroll = false;
+ if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
+ mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
+ startScroll = true;
+ }
+ if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
+ mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
+ startScroll = true;
+ }
+ if (startScroll) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ setScrollState(SCROLL_STATE_DRAGGING);
+ }
+ }
+ } break;
+
+ case MotionEventCompat.ACTION_POINTER_UP: {
+ onPointerUp(e);
+ } break;
+
+ case MotionEvent.ACTION_UP: {
+ mVelocityTracker.clear();
+ } break;
+
+ case MotionEvent.ACTION_CANCEL: {
+ cancelTouch();
+ }
+ }
+ return mScrollState == SCROLL_STATE_DRAGGING;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent e) {
+ if (dispatchOnItemTouch(e)) {
+ cancelTouch();
+ return true;
+ }
+
+ final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
+ final boolean canScrollVertically = mLayout.canScrollVertically();
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(e);
+
+ final int action = MotionEventCompat.getActionMasked(e);
+ final int actionIndex = MotionEventCompat.getActionIndex(e);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ mScrollPointerId = MotionEventCompat.getPointerId(e, 0);
+ mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
+ mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);
+ } break;
+
+ case MotionEventCompat.ACTION_POINTER_DOWN: {
+ mScrollPointerId = MotionEventCompat.getPointerId(e, actionIndex);
+ mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, actionIndex) + 0.5f);
+ mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, actionIndex) + 0.5f);
+ } break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final int index = MotionEventCompat.findPointerIndex(e, mScrollPointerId);
+ if (index < 0) {
+ Log.e(TAG, "Error processing scroll; pointer index for id " +
+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");
+ return false;
+ }
+
+ final int x = (int) (MotionEventCompat.getX(e, index) + 0.5f);
+ final int y = (int) (MotionEventCompat.getY(e, index) + 0.5f);
+ if (mScrollState != SCROLL_STATE_DRAGGING) {
+ final int dx = x - mInitialTouchX;
+ final int dy = y - mInitialTouchY;
+ boolean startScroll = false;
+ if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
+ mLastTouchX = mInitialTouchX + mTouchSlop * (dx < 0 ? -1 : 1);
+ startScroll = true;
+ }
+ if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
+ mLastTouchY = mInitialTouchY + mTouchSlop * (dy < 0 ? -1 : 1);
+ startScroll = true;
+ }
+ if (startScroll) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ setScrollState(SCROLL_STATE_DRAGGING);
+ }
+ }
+ if (mScrollState == SCROLL_STATE_DRAGGING) {
+ final int dx = x - mLastTouchX;
+ final int dy = y - mLastTouchY;
+ scrollByInternal(canScrollHorizontally ? -dx : 0,
+ canScrollVertically ? -dy : 0);
+ }
+ mLastTouchX = x;
+ mLastTouchY = y;
+ } break;
+
+ case MotionEventCompat.ACTION_POINTER_UP: {
+ onPointerUp(e);
+ } break;
+
+ case MotionEvent.ACTION_UP: {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
+ final float xvel = canScrollHorizontally ?
+ -VelocityTrackerCompat.getXVelocity(mVelocityTracker, mScrollPointerId) : 0;
+ final float yvel = canScrollVertically ?
+ -VelocityTrackerCompat.getYVelocity(mVelocityTracker, mScrollPointerId) : 0;
+ if (!((xvel != 0 || yvel != 0) && fling((int) xvel, (int) yvel))) {
+ setScrollState(SCROLL_STATE_IDLE);
+ }
+ mVelocityTracker.clear();
+ releaseGlows();
+ } break;
+
+ case MotionEvent.ACTION_CANCEL: {
+ cancelTouch();
+ } break;
+ }
+
+ return true;
+ }
+
+ private void cancelTouch() {
+ mVelocityTracker.clear();
+ releaseGlows();
+ setScrollState(SCROLL_STATE_IDLE);
+ }
+
+ private void onPointerUp(MotionEvent e) {
+ final int actionIndex = MotionEventCompat.getActionIndex(e);
+ if (MotionEventCompat.getPointerId(e, actionIndex) == mScrollPointerId) {
+ // Pick a new pointer to pick up the slack.
+ final int newIndex = actionIndex == 0 ? 1 : 0;
+ mScrollPointerId = MotionEventCompat.getPointerId(e, newIndex);
+ mInitialTouchX = mLastTouchX = (int) (MotionEventCompat.getX(e, newIndex) + 0.5f);
+ mInitialTouchY = mLastTouchY = (int) (MotionEventCompat.getY(e, newIndex) + 0.5f);
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ if (mAdapterUpdateDuringMeasure) {
+ eatRequestLayout();
+ updateChildViews();
+ mAdapterUpdateDuringMeasure = false;
+ resumeRequestLayout(false);
+ }
+
+ mLayout.onMeasure(widthSpec, heightSpec);
+
+ final int widthSize = getMeasuredWidth();
+ final int heightSize = getMeasuredHeight();
+
+ if (mLeftGlow != null) mLeftGlow.setSize(heightSize, widthSize);
+ if (mTopGlow != null) mTopGlow.setSize(widthSize, heightSize);
+ if (mRightGlow != null) mRightGlow.setSize(heightSize, widthSize);
+ if (mBottomGlow != null) mBottomGlow.setSize(widthSize, heightSize);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ if (mAdapter == null) {
+ Log.e(TAG, "No adapter attached; skipping layout");
+ return;
+ }
+ eatRequestLayout();
+ layoutChildren();
+ resumeRequestLayout(false);
+ mFirstLayoutComplete = true;
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mEatRequestLayout) {
+ super.requestLayout();
+ } else {
+ mLayoutRequestEaten = true;
+ }
+ }
+
+ void layoutChildren() {
+ mLayout.onLayoutChildren(mAdapter, mRecycler, mStructureChanged, mState);
+ // We don't need pending state anymore.
+ mPendingSavedState = null;
+ mStructureChanged = false;
+ }
+
+ void markItemDecorInsetsDirty() {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
+ }
+ }
+
+ @Override
+ public void draw(Canvas c) {
+ super.draw(c);
+
+ final int count = mItemDecorations.size();
+ for (int i = 0; i < count; i++) {
+ mItemDecorations.get(i).onDrawOver(c, this);
+ }
+
+ boolean needsInvalidate = false;
+ if (mLeftGlow != null && !mLeftGlow.isFinished()) {
+ final int restore = c.save();
+ c.rotate(270);
+ c.translate(-getHeight() + getPaddingTop(), 0);
+ needsInvalidate = mLeftGlow != null && mLeftGlow.draw(c);
+ c.restoreToCount(restore);
+ }
+ if (mTopGlow != null && !mTopGlow.isFinished()) {
+ c.translate(getPaddingLeft(), getPaddingTop());
+ needsInvalidate |= mTopGlow != null && mTopGlow.draw(c);
+ }
+ if (mRightGlow != null && !mRightGlow.isFinished()) {
+ final int restore = c.save();
+ final int width = getWidth();
+
+ c.rotate(90);
+ c.translate(-getPaddingTop(), -width);
+ needsInvalidate |= mRightGlow != null && mRightGlow.draw(c);
+ c.restoreToCount(restore);
+ }
+ if (mBottomGlow != null && !mBottomGlow.isFinished()) {
+ final int restore = c.save();
+ c.rotate(180);
+ c.translate(-getWidth() + getPaddingLeft(), -getHeight() + getPaddingTop());
+ needsInvalidate |= mBottomGlow != null && mBottomGlow.draw(c);
+ c.restoreToCount(restore);
+ }
+
+ if (needsInvalidate) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas c) {
+ super.onDraw(c);
+
+ final int count = mItemDecorations.size();
+ for (int i = 0; i < count; i++) {
+ mItemDecorations.get(i).onDraw(c, this);
+ }
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams && mLayout.checkLayoutParams((LayoutParams) p);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ if (mLayout == null) {
+ throw new IllegalStateException("RecyclerView has no LayoutManager");
+ }
+ return mLayout.generateDefaultLayoutParams();
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ if (mLayout == null) {
+ throw new IllegalStateException("RecyclerView has no LayoutManager");
+ }
+ return mLayout.generateLayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ if (mLayout == null) {
+ throw new IllegalStateException("RecyclerView has no LayoutManager");
+ }
+ return mLayout.generateLayoutParams(p);
+ }
+
+ 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);
+
+ mLayout.onItemsAdded(this, op.positionStart, op.itemCount);
+
+ // TODO Animate it in
+ break;
+ case UpdateOp.REMOVE:
+ if (DEBUG) {
+ Log.d(TAG, "UpdateOp.REMOVE start=" + op.positionStart + " count=" +
+ op.itemCount);
+ }
+ offsetPositionRecordsForRemove(op.positionStart, op.itemCount);
+
+ mLayout.onItemsRemoved(this, op.positionStart, op.itemCount);
+
+ // TODO Animate it away
+ break;
+ case UpdateOp.UPDATE:
+ viewRangeUpdate(op.positionStart, op.itemCount);
+ break;
+ }
+ recycleUpdateOp(op);
+ }
+ mPendingUpdates.clear();
+ }
+
+ void offsetPositionRecordsForInsert(int positionStart, int itemCount) {
+ boolean needsLayout = false;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
+ if (holder != null && holder.mPosition >= positionStart) {
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForInsert attached child " + i + " holder " +
+ holder + " now at position " + (holder.mPosition + itemCount));
+ }
+ holder.mPosition += itemCount;
+ needsLayout = true;
+ mStructureChanged = true;
+ }
+ }
+ mRecycler.offsetPositionRecordsForInsert(positionStart, itemCount);
+ if (needsLayout) {
+ requestLayout();
+ }
+ }
+
+ void offsetPositionRecordsForRemove(int positionStart, int itemCount) {
+ boolean needsLayout = false;
+ final int positionEnd = positionStart + itemCount;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
+ if (holder != null) {
+ if (holder.mPosition >= positionEnd) {
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i +
+ " holder " + holder + " now at position " +
+ (holder.mPosition - itemCount));
+ }
+ holder.mPosition -= itemCount;
+ needsLayout = true;
+ mStructureChanged = true;
+ } else if (holder.mPosition >= positionStart) {
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForRemove attached child " + i +
+ " holder " + holder + " now REMOVED");
+ }
+ holder.addFlags(ViewHolder.FLAG_REMOVED);
+ needsLayout = true;
+ mStructureChanged = true;
+ }
+ }
+ }
+ mRecycler.offsetPositionRecordsForRemove(positionStart, itemCount);
+ if (needsLayout) {
+ requestLayout();
+ }
+ }
+
+ /**
+ * Rebind existing views for the given range, or create as needed.
+ *
+ * @param positionStart Adapter position to start at
+ * @param itemCount Number of views that must explicitly be rebound
+ */
+ void viewRangeUpdate(int positionStart, int itemCount) {
+ final int childCount = getChildCount();
+ final int positionEnd = positionStart + itemCount;
+
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
+ if (holder == null) {
+ continue;
+ }
+
+ final int position = holder.getPosition();
+ if (position >= positionStart && position < positionEnd) {
+ holder.addFlags(ViewHolder.FLAG_UPDATE);
+ // Binding an attached view will request a layout if needed.
+ mAdapter.bindViewHolder(holder, holder.getPosition());
+ }
+ }
+ mRecycler.viewRangeUpdate(positionStart, itemCount);
+ }
+
+ /**
+ * 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();
+
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
+ if (holder != null) {
+ holder.addFlags(ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+ }
+ }
+ 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
+ * @return The child view's ViewHolder
+ */
+ public ViewHolder getChildViewHolder(View child) {
+ if (child.getParent() != this) {
+ throw new IllegalArgumentException("View " + child + " is not a direct child of " +
+ this);
+ }
+ return getChildViewHolderInt(child);
+ }
+
+ static ViewHolder getChildViewHolderInt(View child) {
+ if (child == null) {
+ return null;
+ }
+ return ((LayoutParams) child.getLayoutParams()).mViewHolder;
+ }
+
+ /**
+ * Return the adapter position that the given child view corresponds to.
+ *
+ * @param child Child View to query
+ * @return Adapter position corresponding to the given view or {@link #NO_POSITION}
+ */
+ public int getChildPosition(View child) {
+ final ViewHolder holder = getChildViewHolderInt(child);
+ return holder != null ? holder.getPosition() : NO_POSITION;
+ }
+
+ /**
+ * Return the stable item id that the given child view corresponds to.
+ *
+ * @param child Child View to query
+ * @return Item id corresponding to the given view or {@link #NO_ID}
+ */
+ public long getChildItemId(View child) {
+ if (mAdapter == null || !mAdapter.hasStableIds()) {
+ return NO_ID;
+ }
+ final ViewHolder holder = getChildViewHolderInt(child);
+ return holder != null ? holder.getItemId() : NO_ID;
+ }
+
+ /**
+ * Return the ViewHolder for the item in the given position of the data set.
+ *
+ * @param position The position of the item in the data set of the adapter
+ * @return The ViewHolder at <code>position</code>
+ */
+ public ViewHolder findViewHolderForPosition(int position) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
+ if (holder != null && holder.getPosition() == position) {
+ return holder;
+ }
+ }
+ return mRecycler.findViewHolderForPosition(position);
+ }
+
+ /**
+ * Return the ViewHolder for the item with the given id. The RecyclerView must
+ * use an Adapter with {@link Adapter#setHasStableIds(boolean) stableIds} to
+ * return a non-null value.
+ *
+ * @param id The id for the requested item
+ * @return The ViewHolder with the given <code>id</code>, of null if there
+ * is no such item.
+ */
+ public ViewHolder findViewHolderForItemId(long id) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final ViewHolder holder = getChildViewHolderInt(getChildAt(i));
+ if (holder != null && holder.getItemId() == id) {
+ return holder;
+ }
+ }
+ return mRecycler.findViewHolderForItemId(id);
+ }
+
+ /**
+ * Return the ViewHolder for the child view positioned underneath the coordinates (x, y).
+ *
+ * @param x Horizontal position in pixels to search
+ * @param y Vertical position in pixels to search
+ * @return The ViewHolder for the child under (x, y) or null if no child is found
+ *
+ * @deprecated This method will be removed. Use {@link #findChildViewUnder(float, float)}
+ * along with {@link #getChildViewHolder(View)}
+ */
+ public ViewHolder findViewHolderForChildUnder(int x, int y) {
+ final View child = findChildViewUnder(x, y);
+ if (child != null) {
+ return getChildViewHolderInt(child);
+ }
+ return null;
+ }
+
+ /**
+ * Find the topmost view under the given point.
+ *
+ * @param x Horizontal position in pixels to search
+ * @param y Vertical position in pixels to search
+ * @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();
+ for (int i = count - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ final float translationX = ViewCompat.getTranslationX(child);
+ final float translationY = ViewCompat.getTranslationY(child);
+ if (x >= child.getLeft() + translationX &&
+ x <= child.getRight() + translationX &&
+ y >= child.getTop() + translationY &&
+ y <= child.getBottom() + translationY) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return the ViewHolder for the child view of the RecyclerView that is at the
+ * given index.
+ *
+ * @param childIndex The index of the child in the RecyclerView's child list.
+ * @return The ViewHolder for the given <code>childIndex</code>
+ *
+ * @deprecated Use {@link #getChildViewHolder(View)} and {@link #getChildAt(int)}
+ */
+ public ViewHolder getViewHolderForChildAt(int childIndex) {
+ return getChildViewHolderInt(getChildAt(childIndex));
+ }
+
+ /**
+ * Offset the bounds of all child views by <code>dy</code> pixels.
+ * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}.
+ *
+ * @param dy Vertical pixel offset to apply to the bounds of all child views
+ */
+ public void offsetChildrenVertical(int dy) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).offsetTopAndBottom(dy);
+ }
+ }
+
+ /**
+ * Called when an item view is attached to this RecyclerView.
+ *
+ * <p>Subclasses of RecyclerView may want to perform extra bookkeeping or modifications
+ * of child views as they become attached. This will be called before a
+ * {@link LayoutManager} measures or lays out the view and is a good time to perform these
+ * changes.</p>
+ *
+ * @param child Child view that is now attached to this RecyclerView and its associated window
+ */
+ public void onChildAttachedToWindow(View child) {
+ }
+
+ /**
+ * Called when an item view is detached from this RecyclerView.
+ *
+ * <p>Subclasses of RecyclerView may want to perform extra bookkeeping or modifications
+ * of child views as they become detached. This will be called as a
+ * {@link LayoutManager} fully detaches the child view from the parent and its window.</p>
+ *
+ * @param child Child view that is now detached from this RecyclerView and its associated window
+ */
+ public void onChildDetachedFromWindow(View child) {
+ }
+
+ /**
+ * Offset the bounds of all child views by <code>dx</code> pixels.
+ * Useful for implementing simple scrolling in {@link LayoutManager LayoutManagers}.
+ *
+ * @param dx Horizontal pixel offset to apply to the bounds of all child views
+ */
+ public void offsetChildrenHorizontal(int dx) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).offsetLeftAndRight(dx);
+ }
+ }
+
+ Rect getItemDecorInsetsForChild(View child) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.mInsetsDirty) {
+ return lp.mDecorInsets;
+ }
+
+ final Rect insets = lp.mDecorInsets;
+ insets.set(0, 0, 0, 0);
+ 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);
+ insets.left += mTempRect.left;
+ insets.top += mTempRect.top;
+ insets.right += mTempRect.right;
+ insets.bottom += mTempRect.bottom;
+ }
+ lp.mInsetsDirty = false;
+ return insets;
+ }
+
+ private class ViewFlinger implements Runnable {
+ private int mLastFlingX;
+ private int mLastFlingY;
+ private ScrollerCompat mScroller;
+ private Interpolator mInterpolator = sQuinticInterpolator;
+
+
+ // When set to true, postOnAnimation callbacks are delayed until the run method completes
+ private boolean mEatRunOnAnimationRequest = false;
+
+ // Tracks if postAnimationCallback should be re-attached when it is done
+ private boolean mReSchedulePostAnimationCallback = false;
+
+ public ViewFlinger() {
+ mScroller = ScrollerCompat.create(getContext(), sQuinticInterpolator);
+ }
+
+ @Override
+ public void run() {
+ disableRunOnAnimationRequests();
+ // keep a local reference so that if it is changed during onAnimation method, it wont cause
+ // unexpected behaviors
+ final ScrollerCompat scroller = mScroller;
+ final SmoothScroller smoothScroller = mLayout.mSmoothScroller;
+ if (scroller.computeScrollOffset()) {
+ final int x = scroller.getCurrX();
+ final int y = scroller.getCurrY();
+ final int dx = x - mLastFlingX;
+ final int dy = y - mLastFlingY;
+ mLastFlingX = x;
+ mLastFlingY = y;
+ int overscrollX = 0, overscrollY = 0;
+ if (mAdapter != null) {
+ eatRequestLayout();
+ if (dx != 0) {
+ final int hresult = mLayout.scrollHorizontallyBy(dx, getAdapter(), mRecycler
+ , mState);
+ overscrollX = dx - hresult;
+ }
+ if (dy != 0) {
+ final int vresult = mLayout.scrollVerticallyBy(dy, getAdapter(), mRecycler,
+ mState);
+ overscrollY = dy - vresult;
+ }
+
+ if (smoothScroller != null && !smoothScroller.isPendingInitialRun() &&
+ smoothScroller.isRunning()) {
+ smoothScroller.onAnimation(dx - overscrollX, dy - overscrollY);
+ }
+ resumeRequestLayout(false);
+ }
+ if (!mItemDecorations.isEmpty()) {
+ invalidate();
+ }
+
+ if (overscrollX != 0 || overscrollY != 0) {
+ final int vel = (int) scroller.getCurrVelocity();
+
+ int velX = 0;
+ if (overscrollX != x) {
+ velX = overscrollX < 0 ? -vel : overscrollX > 0 ? vel : 0;
+ }
+
+ int velY = 0;
+ if (overscrollY != y) {
+ velY = overscrollY < 0 ? -vel : overscrollY > 0 ? vel : 0;
+ }
+
+ if (ViewCompat.getOverScrollMode(RecyclerView.this) !=
+ ViewCompat.OVER_SCROLL_NEVER) {
+ absorbGlows(velX, velY);
+ }
+ if ((velX != 0 || overscrollX == x || scroller.getFinalX() == 0) &&
+ (velY != 0 || overscrollY == y || scroller.getFinalY() == 0)) {
+ scroller.abortAnimation();
+ }
+ }
+
+ if (mScrollListener != null && (x != 0 || y != 0)) {
+ mScrollListener.onScrolled(dx, dy);
+ }
+
+ if (!awakenScrollBars()) {
+ invalidate();
+ }
+
+ if (scroller.isFinished()) {
+ setScrollState(SCROLL_STATE_IDLE);
+ } else {
+ postOnAnimation();
+ }
+ }
+ // call this after the onAnimation is complete not to have inconsistent callbacks etc.
+ if (smoothScroller != null && smoothScroller.isPendingInitialRun()) {
+ smoothScroller.onAnimation(0, 0);
+ }
+ enableRunOnAnimationRequests();
+ }
+
+ private void disableRunOnAnimationRequests() {
+ mReSchedulePostAnimationCallback = false;
+ mEatRunOnAnimationRequest = true;
+ }
+
+ private void enableRunOnAnimationRequests() {
+ mEatRunOnAnimationRequest = false;
+ if (mReSchedulePostAnimationCallback) {
+ postOnAnimation();
+ }
+ }
+
+ void postOnAnimation() {
+ if (mEatRunOnAnimationRequest) {
+ mReSchedulePostAnimationCallback = true;
+ } else {
+ ViewCompat.postOnAnimation(RecyclerView.this, this);
+ }
+ }
+
+ public void fling(int velocityX, int velocityY) {
+ setScrollState(SCROLL_STATE_SETTLING);
+ mLastFlingX = mLastFlingY = 0;
+ mScroller.fling(0, 0, velocityX, velocityY,
+ Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
+ postOnAnimation();
+ }
+
+ public void smoothScrollBy(int dx, int dy) {
+ smoothScrollBy(dx, dy, 0, 0);
+ }
+
+ public void smoothScrollBy(int dx, int dy, int vx, int vy) {
+ smoothScrollBy(dx, dy, computeScrollDuration(dx, dy, vx, vy));
+ }
+
+ private float distanceInfluenceForSnapDuration(float f) {
+ f -= 0.5f; // center the values about 0.
+ f *= 0.3f * Math.PI / 2.0f;
+ return (float) Math.sin(f);
+ }
+
+ private int computeScrollDuration(int dx, int dy, int vx, int vy) {
+ final int absDx = Math.abs(dx);
+ final int absDy = Math.abs(dy);
+ final boolean horizontal = absDx > absDy;
+ final int velocity = (int) Math.sqrt(vx * vx + vy * vy);
+ final int delta = (int) Math.sqrt(dx * dx + dy * dy);
+ final int containerSize = horizontal ? getWidth() : getHeight();
+ final int halfContainerSize = containerSize / 2;
+ final float distanceRatio = Math.min(1.f, 1.f * delta / containerSize);
+ final float distance = halfContainerSize + halfContainerSize *
+ distanceInfluenceForSnapDuration(distanceRatio);
+
+ final int duration;
+ if (velocity > 0) {
+ duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+ } else {
+ float absDelta = (float) (horizontal ? absDx : absDy);
+ duration = (int) (((absDelta / containerSize) + 1) * 300);
+ }
+ return Math.min(duration, MAX_SCROLL_DURATION);
+ }
+
+ public void smoothScrollBy(int dx, int dy, int duration) {
+ smoothScrollBy(dx, dy, duration, sQuinticInterpolator);
+ }
+
+ public void smoothScrollBy(int dx, int dy, int duration, Interpolator interpolator) {
+ if (mInterpolator != interpolator) {
+ mInterpolator = interpolator;
+ mScroller = ScrollerCompat.create(getContext(), interpolator);
+ }
+ setScrollState(SCROLL_STATE_SETTLING);
+ mLastFlingX = mLastFlingY = 0;
+ mScroller.startScroll(0, 0, dx, dy, duration);
+ postOnAnimation();
+ }
+
+ public void stop() {
+ removeCallbacks(this);
+ mScroller.abortAnimation();
+ }
+
+ }
+
+ private class RecyclerViewDataObserver extends AdapterDataObserver {
+ @Override
+ public void onChanged() {
+ if (mAdapter.hasStableIds()) {
+ // TODO Determine what actually changed
+ markKnownViewsInvalid();
+ mStructureChanged = true;
+ requestLayout();
+ } else {
+ markKnownViewsInvalid();
+ mStructureChanged = true;
+ requestLayout();
+ }
+ }
+
+ @Override
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ postAdapterUpdate(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount));
+ }
+
+ @Override
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ postAdapterUpdate(obtainUpdateOp(UpdateOp.ADD, positionStart, itemCount));
+ }
+
+ @Override
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ postAdapterUpdate(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount));
+ }
+ }
+
+ public static class RecycledViewPool {
+ private SparseArray<ArrayList<ViewHolder>> mScrap =
+ new SparseArray<ArrayList<ViewHolder>>();
+ private SparseIntArray mMaxScrap = new SparseIntArray();
+ private int mAttachCount = 0;
+
+ private static final int DEFAULT_MAX_SCRAP = 5;
+
+ public void clear() {
+ mScrap.clear();
+ }
+
+ /** @deprecated No longer needed */
+ public void reset(int typeCount) {
+ }
+
+ public void setMaxRecycledViews(int viewType, int max) {
+ mMaxScrap.put(viewType, max);
+ final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
+ if (scrapHeap != null) {
+ while (scrapHeap.size() > max) {
+ scrapHeap.remove(scrapHeap.size() - 1);
+ }
+ }
+ }
+
+ public ViewHolder getRecycledView(int viewType) {
+ final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
+ if (scrapHeap != null && !scrapHeap.isEmpty()) {
+ final int index = scrapHeap.size() - 1;
+ final ViewHolder scrap = scrapHeap.get(index);
+ scrapHeap.remove(index);
+ return scrap;
+ }
+ return null;
+ }
+
+ 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.mItemId = NO_ID;
+ scrap.clearFlagsForSharedPool();
+ scrapHeap.add(scrap);
+ }
+
+ void attach(Adapter adapter) {
+ mAttachCount++;
+ }
+
+ void detach() {
+ mAttachCount--;
+ }
+
+
+ void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter) {
+ if (mAttachCount == 1) {
+ clear();
+ }
+ }
+
+ private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
+ ArrayList<ViewHolder> scrap = mScrap.get(viewType);
+ if (scrap == null) {
+ scrap = new ArrayList<ViewHolder>();
+ mScrap.put(viewType, scrap);
+ if (mMaxScrap.indexOfKey(viewType) < 0) {
+ mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
+ }
+ }
+ return scrap;
+ }
+ }
+
+ /**
+ * A Recycler is responsible for managing scrapped or detached item views for reuse.
+ *
+ * <p>A "scrapped" view is a view that is still attached to its parent RecyclerView but
+ * that has been marked for removal or reuse.</p>
+ *
+ * <p>Typical use of a Recycler by a {@link LayoutManager} will be to obtain views for
+ * an adapter's data set representing the data at a given position or item ID.
+ * If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
+ * If not, the view can be quickly reused by the LayoutManager with no further work.
+ * Clean views that have not {@link android.view.View#isLayoutRequested() requested layout}
+ * may be repositioned by a LayoutManager without remeasurement.</p>
+ */
+ public final class Recycler {
+ private final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<ViewHolder>();
+
+ private final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
+ private int mViewCacheMax = DEFAULT_CACHE_SIZE;
+
+ private RecycledViewPool mRecyclerPool;
+
+ private static final int DEFAULT_CACHE_SIZE = 2;
+
+ /**
+ * Clear scrap views out of this recycler. Detached views contained within a
+ * recycled view pool will remain.
+ */
+ public void clear() {
+ mAttachedScrap.clear();
+ mCachedViews.clear();
+ }
+
+ /**
+ * Set the maximum number of detached, valid views we should retain for later use.
+ *
+ * @param viewCount Number of views to keep before sending views to the shared pool
+ */
+ public void setViewCacheSize(int viewCount) {
+ mViewCacheMax = viewCount;
+ while (mCachedViews.size() > viewCount) {
+ mCachedViews.remove(mCachedViews.size() - 1);
+ }
+ }
+
+ /**
+ * @deprecated Use {@link #getViewForPosition(Adapter, int)}
+ * instead. This method will be removed.
+ */
+ public View getViewForPosition(int position) {
+ return getViewForPosition(mAdapter, position);
+ }
+
+ /**
+ * 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>
+ *
+ * <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>
+ *
+ * @param adapter Adapter to use for binding and creating views
+ * @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(Adapter adapter, int position) {
+ ViewHolder holder;
+ final int type = adapter.getItemViewType(position);
+ if (adapter.hasStableIds()) {
+ final long id = adapter.getItemId(position);
+ holder = getScrapViewForId(id, type);
+ } else {
+ holder = getScrapViewForPosition(position, type);
+ }
+
+ if (holder == null) {
+ holder = adapter.createViewHolder(RecyclerView.this, type);
+ 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...");
+ }
+ adapter.bindViewHolder(holder, 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).mViewHolder = holder;
+
+ return holder.itemView;
+ }
+
+ /**
+ * @deprecated Renamed to {@link #recycleView(android.view.View)} to cause
+ * less confusion between temporarily detached scrap views and fully detached
+ * recycled views. This method will be removed.
+ */
+ public void addDetachedScrapView(View scrap) {
+ recycleView(scrap);
+ }
+
+ /**
+ * Recycle a detached view. The specified view will be added to a pool of views
+ * for later rebinding and reuse.
+ *
+ * <p>A view must be fully detached before it may be recycled.</p>
+ *
+ * @param view Removed view for recycling
+ */
+ public void recycleView(View view) {
+ recycleViewHolder(getChildViewHolderInt(view));
+ }
+
+ void recycleViewHolder(ViewHolder holder) {
+ if (holder.isScrap() || holder.itemView.getParent() != null) {
+ throw new IllegalArgumentException(
+ "Scrapped or attached views may not be recycled.");
+ }
+
+ // 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);
+ break;
+ }
+ }
+ }
+ if (mCachedViews.size() < mViewCacheMax && !holder.isRemoved()
+ && !holder.isInvalid()) {
+ mCachedViews.add(holder);
+ } else if (holder.isRecyclable()) {
+ getRecycledViewPool().putRecycledView(holder);
+ dispatchViewRecycled(holder);
+ }
+ }
+
+ /**
+ * Used as a fast path for unscrapping and recycling a view during a bulk operation.
+ * The caller must call {@link #clearScrap()} when it's done to update the recycler's
+ * internal bookkeeping.
+ */
+ void quickRecycleScrapView(View view) {
+ final ViewHolder holder = getChildViewHolderInt(view);
+ holder.mScrapContainer = null;
+ recycleViewHolder(holder);
+ }
+
+ /**
+ * @deprecated This method will be removed. Adding and removing views is the responsibility
+ * of the LayoutManager; the Recycler will only be responsible for marking and tracking
+ * views for reuse. This method no longer matches the definition of 'scrap'.
+ */
+ public void detachAndScrapView(View scrap) {
+ if (scrap.getParent() != RecyclerView.this) {
+ throw new IllegalArgumentException("View " + scrap + " is not attached to " +
+ RecyclerView.this);
+ }
+ mLayout.removeView(scrap);
+ recycleView(scrap);
+ }
+
+ /**
+ * @deprecated This method will be removed. Moved to
+ * {@link LayoutManager#detachAndScrapAttachedViews(android.support.v7.widget.RecyclerView.Recycler)}
+ * to keep LayoutManager as the owner of attach/detach operations.
+ */
+ public void scrapAllViewsAttached() {
+ mLayout.detachAndScrapAttachedViews(this);
+ }
+
+ /**
+ * Mark an attached view as scrap.
+ *
+ * <p>"Scrap" views are still attached to their parent RecyclerView but are eligible
+ * for rebinding and reuse. Requests for a view for a given position may return a
+ * reused or rebound scrap view instance.</p>
+ *
+ * @param view View to scrap
+ */
+ void scrapView(View view) {
+ final ViewHolder holder = getChildViewHolderInt(view);
+ holder.setScrapContainer(this);
+ mAttachedScrap.add(holder);
+ }
+
+ /**
+ * Remove a previously scrapped view from the pool of eligible scrap.
+ *
+ * <p>This view will no longer be eligible for reuse until re-scrapped or
+ * until it is explicitly removed and recycled.</p>
+ */
+ void unscrapView(ViewHolder holder) {
+ mAttachedScrap.remove(holder);
+ }
+
+ /**
+ * @deprecated This method will be removed. Adding and removing views should be done
+ * through the LayoutManager. Use
+ * {@link LayoutManager#removeAndRecycleScrap(android.support.v7.widget.RecyclerView.Recycler)}
+ * instead.
+ */
+ public void detachDirtyScrapViews() {
+ mLayout.removeAndRecycleScrap(this);
+ }
+
+ int getScrapCount() {
+ return mAttachedScrap.size();
+ }
+
+ View getScrapViewAt(int index) {
+ return mAttachedScrap.get(index).itemView;
+ }
+
+ void clearScrap() {
+ mAttachedScrap.clear();
+ }
+
+ ViewHolder getScrapViewForPosition(int position, int type) {
+ 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() &&
+ !holder.isRemoved()) {
+ if (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);
+ }
+ return holder;
+ }
+ }
+
+ // Search in our first-level recycled view cache.
+ 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;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "getScrapViewForPosition(" + position + ", " + type +
+ ") found match in cache: " + holder);
+ }
+ 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);
+ }
+
+ ViewHolder getScrapViewForId(long id, int type) {
+ // Look in our attached views first
+ final int count = mAttachedScrap.size();
+ for (int i = 0; i < count; i++) {
+ final ViewHolder holder = mAttachedScrap.get(i);
+ if (holder.getItemId() == id) {
+ if (type == holder.getItemViewType()) {
+ mAttachedScrap.remove(i);
+ holder.setScrapContainer(null);
+ return holder;
+ } else {
+ break;
+ }
+ }
+ }
+
+ // Search the first-level cache
+ final int cacheSize = mCachedViews.size();
+ for (int i = 0; i < cacheSize; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder.getItemId() == id) {
+ mCachedViews.remove(i);
+ return holder;
+ }
+ }
+
+ // 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);
+ }
+
+ void dispatchViewRecycled(ViewHolder holder) {
+ if (mRecyclerListener != null) {
+ mRecyclerListener.onViewRecycled(holder);
+ }
+ if (mAdapter != null) {
+ mAdapter.onViewRecycled(holder);
+ }
+ if (DEBUG) Log.d(TAG, "dispatchViewRecycled: " + holder);
+ }
+
+ void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter) {
+ clear();
+ getRecycledViewPool().onAdapterChanged(oldAdapter, newAdapter);
+ }
+
+ void offsetPositionRecordsForInsert(int insertedAt, int count) {
+ final int cachedCount = mCachedViews.size();
+ for (int i = 0; i < cachedCount; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder != null && holder.getPosition() >= insertedAt) {
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForInsert cached " + i + " holder " +
+ holder + " now at position " + (holder.mPosition + count));
+ }
+ holder.mPosition += count;
+ }
+ }
+ }
+
+ void offsetPositionRecordsForRemove(int removedFrom, int count) {
+ final int removedEnd = removedFrom + count;
+ final int cachedCount = mCachedViews.size();
+ for (int i = cachedCount - 1; i >= 0; i--) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder != null) {
+ if (holder.getPosition() >= removedEnd) {
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForRemove cached " + i +
+ " holder " + holder + " now at position " +
+ (holder.mPosition - count));
+ }
+ holder.mPosition -= count;
+ } else if (holder.getPosition() >= removedFrom) {
+ // Item for this view was removed. Dump it from the cache.
+ if (DEBUG) {
+ Log.d(TAG, "offsetPositionRecordsForRemove cached " + i +
+ " holder " + holder + " now placed in pool");
+ }
+ mCachedViews.remove(i);
+ getRecycledViewPool().putRecycledView(holder);
+ dispatchViewRecycled(holder);
+ }
+ }
+ }
+ }
+
+ void setRecycledViewPool(RecycledViewPool pool) {
+ if (mRecyclerPool != null) {
+ mRecyclerPool.detach();
+ }
+ mRecyclerPool = pool;
+ if (pool != null) {
+ mRecyclerPool.attach(getAdapter());
+ }
+ }
+
+ RecycledViewPool getRecycledViewPool() {
+ if (mRecyclerPool == null) {
+ mRecyclerPool = new RecycledViewPool();
+ }
+ 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();
+ for (int i = 0; i < cachedCount; i++) {
+ final ViewHolder holder = mCachedViews.get(i);
+ if (holder == null) {
+ continue;
+ }
+
+ final int pos = holder.getPosition();
+ if (pos >= positionStart && pos < positionEnd) {
+ holder.addFlags(ViewHolder.FLAG_UPDATE);
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+ }
+ }
+
+ /**
+ * Base class for an Adapter
+ *
+ * <p>Adapters provide a binding from an app-specific data set to views that are displayed
+ * within a {@link RecyclerView}.</p>
+ */
+ public static abstract class Adapter<VH extends ViewHolder> {
+ private final AdapterDataObservable mObservable = new AdapterDataObservable();
+ private boolean mHasStableIds = false;
+
+ /** @deprecated */
+ private int mViewTypeCount = 1;
+
+ public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
+ public abstract void onBindViewHolder(VH holder, int position);
+
+ public final VH createViewHolder(ViewGroup parent, int viewType) {
+ final VH holder = onCreateViewHolder(parent, viewType);
+ holder.mItemViewType = viewType;
+ return holder;
+ }
+
+ public final void bindViewHolder(VH holder, int position) {
+ holder.mPosition = position;
+ if (hasStableIds()) {
+ holder.mItemId = getItemId(position);
+ }
+ onBindViewHolder(holder, position);
+ holder.setFlags(ViewHolder.FLAG_BOUND,
+ ViewHolder.FLAG_BOUND | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_INVALID);
+ }
+
+ /**
+ * Return the view type of the item at <code>position</code> for the purposes
+ * of view recycling.
+ *
+ * <p>The default implementation of this method returns 0, making the assumption of
+ * a single view type for the adapter. Unlike ListView adapters, types need not
+ * be contiguous. Consider using id resources to uniquely identify item view types.
+ *
+ * @param position position to query
+ * @return integer value identifying the type of the view needed to represent the item at
+ * <code>position</code>. Type codes need not be contiguous.
+ */
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ /**
+ * Set the number of item view types required by this adapter to display its data set.
+ * This may not be changed while the adapter has observers - e.g. while the adapter
+ * is set on a {#link RecyclerView}.
+ *
+ * @param count Number of item view types required
+ * @see #getItemViewTypeCount()
+ * @see #getItemViewType(int)
+ *
+ * @deprecated This method is no longer necessary. View types are now unbounded.
+ */
+ public void setItemViewTypeCount(int count) {
+ Log.w(TAG, "setItemViewTypeCount is deprecated and no longer needed.");
+ mViewTypeCount = count;
+ }
+
+ /**
+ * Retrieve the number of item view types required by this adapter to display its data set.
+ *
+ * @return Number of item view types supported
+ * @see #setItemViewTypeCount(int)
+ * @see #getItemViewType(int)
+ *
+ * @deprecated This method is no longer necessary. View types are now unbounded.
+ */
+ public final int getItemViewTypeCount() {
+ Log.w(TAG, "getItemViewTypeCount is no longer needed. " +
+ "View type count is now unbounded.");
+ return mViewTypeCount;
+ }
+
+ public void setHasStableIds(boolean hasStableIds) {
+ if (hasObservers()) {
+ throw new IllegalStateException("Cannot change whether this adapter has " +
+ "stable IDs while the adapter has registered observers.");
+ }
+ mHasStableIds = hasStableIds;
+ }
+
+ /**
+ * Return the stable ID for the item at <code>position</code>. If {@link #hasStableIds()}
+ * would return false this method should return {@link #NO_ID}. The default implementation
+ * of this method returns {@link #NO_ID}.
+ *
+ * @param position Adapter position to query
+ * @return the stable ID of the item at position
+ */
+ public long getItemId(int position) {
+ return NO_ID;
+ }
+
+ public abstract int getItemCount();
+
+ /**
+ * Returns true if this adapter publishes a unique <code>long</code> value that can
+ * act as a key for the item at a given position in the data set. If that item is relocated
+ * in the data set, the ID returned for that item should be the same.
+ *
+ * @return true if this adapter's items have stable IDs
+ */
+ public final boolean hasStableIds() {
+ return mHasStableIds;
+ }
+
+ /**
+ * Called when a view created by this adapter has been recycled.
+ *
+ * <p>A view is recycled when a {@link LayoutManager} decides that it no longer
+ * needs to be attached to its parent {@link RecyclerView}. This can be because it has
+ * fallen out of visibility or a set of cached views represented by views still
+ * attached to the parent RecyclerView. If an item view has large or expensive data
+ * bound to it such as large bitmaps, this may be a good place to release those
+ * resources.</p>
+ *
+ * @param holder The ViewHolder for the view being recycled
+ */
+ public void onViewRecycled(VH holder) {
+ }
+
+ /**
+ * Called when a view created by this adapter has been attached to a window.
+ *
+ * <p>This can be used as a reasonable signal that the view is about to be seen
+ * by the user. If the adapter previously freed any resources in
+ * {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow}
+ * those resources should be restored here.</p>
+ *
+ * @param holder Holder of the view being attached
+ */
+ public void onViewAttachedToWindow(VH holder) {
+ }
+
+ /**
+ * Called when a view created by this adapter has been detached from its window.
+ *
+ * <p>Becoming detached from the window is not necessarily a permanent condition;
+ * the consumer of an Adapter's views may choose to cache views offscreen while they
+ * are not visible, attaching an detaching them as appropriate.</p>
+ *
+ * @param holder Holder of the view being detached
+ */
+ public void onViewDetachedFromWindow(VH holder) {
+ }
+
+ /**
+ * Returns true if one or more observers are attached to this adapter.
+ * @return true if this adapter has observers
+ */
+ public final boolean hasObservers() {
+ return mObservable.hasObservers();
+ }
+
+ /**
+ * Register a new observer to listen for data changes.
+ *
+ * <p>The adapter may publish a variety of events describing specific changes.
+ * Not all adapters may support all change types and some may fall back to a generic
+ * {@link android.support.v7.widget.RecyclerView.AdapterDataObserver#onChanged()
+ * "something changed"} event if more specific data is not available.</p>
+ *
+ * <p>Components registering observers with an adapter are responsible for
+ * {@link #unregisterAdapterDataObserver(android.support.v7.widget.RecyclerView.AdapterDataObserver)
+ * unregistering} those observers when finished.</p>
+ *
+ * @param observer Observer to register
+ *
+ * @see #unregisterAdapterDataObserver(android.support.v7.widget.RecyclerView.AdapterDataObserver)
+ */
+ public void registerAdapterDataObserver(AdapterDataObserver observer) {
+ mObservable.registerObserver(observer);
+ }
+
+ /**
+ * Unregister an observer currently listening for data changes.
+ *
+ * <p>The unregistered observer will no longer receive events about changes
+ * to the adapter.</p>
+ *
+ * @param observer Observer to unregister
+ *
+ * @see #registerAdapterDataObserver(android.support.v7.widget.RecyclerView.AdapterDataObserver)
+ */
+ public void unregisterAdapterDataObserver(AdapterDataObserver observer) {
+ mObservable.unregisterObserver(observer);
+ }
+
+ /**
+ * Notify any registered observers that the data set has changed.
+ *
+ * <p>There are two different classes of data change events, item changes and structural
+ * changes. Item changes are when a single item has its data updated but no positional
+ * changes have occurred. Structural changes are when items are inserted, deleted or moved
+ * within the data set.</p>
+ *
+ * <p>This event does not specify what about the data set has changed, forcing
+ * any observers to assume that all existing items and structure may no longer be valid.
+ * LayoutManagers will be forced to fully rebind and relayout all visible views.</p>
+ *
+ * <p><code>RecyclerView</code> will attempt to synthesize visible structural change events
+ * for adapters that report that they have {@link #hasStableIds() stable IDs} when
+ * this method is used. This can help for the purposes of animation and visual
+ * object persistence but individual item views will still need to be rebound
+ * and relaid out.</p>
+ *
+ * <p>If you are writing an adapter it will always be more efficient to use the more
+ * specific change events if you can. Rely on <code>notifyDataSetChanged()</code>
+ * as a last resort.</p>
+ *
+ * @see #notifyItemChanged(int)
+ * @see #notifyItemInserted(int)
+ * @see #notifyItemRemoved(int)
+ * @see #notifyItemRangeChanged(int, int)
+ * @see #notifyItemRangeInserted(int, int)
+ * @see #notifyItemRangeRemoved(int, int)
+ */
+ public final void notifyDataSetChanged() {
+ mObservable.notifyChanged();
+ }
+
+ /**
+ * Notify any registered observers that the item at <code>position</code> has changed.
+ *
+ * <p>This is an item change event, not a structural change event. It indicates that any
+ * reflection of the data at <code>position</code> is out of date and should be updated.
+ * The item at <code>position</code> retains the same identity.</p>
+ *
+ * @param position Position of the item that has changed
+ *
+ * @see #notifyItemRangeChanged(int, int)
+ */
+ public final void notifyItemChanged(int position) {
+ mObservable.notifyItemRangeChanged(position, 1);
+ }
+
+ /**
+ * Notify any registered observers that the <code>itemCount</code> items starting at
+ * position <code>positionStart</code> have changed.
+ *
+ * <p>This is an item change event, not a structural change event. It indicates that
+ * any reflection of the data in the given position range is out of date and should
+ * be updated. The items in the given range retain the same identity.</p>
+ *
+ * @param positionStart Position of the first item that has changed
+ * @param itemCount Number of items that have changed
+ *
+ * @see #notifyItemChanged(int)
+ */
+ public final void notifyItemRangeChanged(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeChanged(positionStart, itemCount);
+ }
+
+ /**
+ * Notify any registered observers that the item reflected at <code>position</code>
+ * has been newly inserted. The item previously at <code>position</code> is now at
+ * position <code>position + 1</code>.
+ *
+ * <p>This is a structural change event. Representations of other existing items in the
+ * data set are still considered up to date and will not be rebound, though their
+ * positions may be altered.</p>
+ *
+ * @param position Position of the newly inserted item in the data set
+ *
+ * @see #notifyItemRangeInserted(int, int)
+ */
+ public final void notifyItemInserted(int position) {
+ mObservable.notifyItemRangeInserted(position, 1);
+ }
+
+ /**
+ * Notify any registered observers that the currently reflected <code>itemCount</code>
+ * items starting at <code>positionStart</code> have been newly inserted. The items
+ * previously located at <code>positionStart</code> and beyond can now be found starting
+ * at position <code>positionStart + itemCount</code>.
+ *
+ * <p>This is a structural change event. Representations of other existing items in the
+ * data set are still considered up to date and will not be rebound, though their positions
+ * may be altered.</p>
+ *
+ * @param positionStart Position of the first item that was inserted
+ * @param itemCount Number of items inserted
+ *
+ * @see #notifyItemInserted(int)
+ */
+ public final void notifyItemRangeInserted(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeInserted(positionStart, itemCount);
+ }
+
+ /**
+ * Notify any registered observers that the item previously located at <code>position</code>
+ * has been removed from the data set. The items previously located at and after
+ * <code>position</code> may now be found at <code>oldPosition - 1</code>.
+ *
+ * <p>This is a structural change event. Representations of other existing items in the
+ * data set are still considered up to date and will not be rebound, though their positions
+ * may be altered.</p>
+ *
+ * @param position Position of the item that has now been removed
+ *
+ * @see #notifyItemRangeRemoved(int, int)
+ */
+ public final void notifyItemRemoved(int position) {
+ mObservable.notifyItemRangeRemoved(position, 1);
+ }
+
+ /**
+ * Notify any registered observers that the <code>itemCount</code> items previously
+ * located at <code>positionStart</code> have been removed from the data set. The items
+ * previously located at and after <code>positionStart + itemCount</code> may now be found
+ * at <code>oldPosition - itemCount</code>.
+ *
+ * <p>This is a structural change event. Representations of other existing items in the data
+ * set are still considered up to date and will not be rebound, though their positions
+ * may be altered.</p>
+ *
+ * @param positionStart Previous position of the first item that was removed
+ * @param itemCount Number of items removed from the data set
+ */
+ public final void notifyItemRangeRemoved(int positionStart, int itemCount) {
+ mObservable.notifyItemRangeRemoved(positionStart, itemCount);
+ }
+ }
+
+ /**
+ * A <code>LayoutManager</code> is responsible for measuring and positioning item views
+ * within a <code>RecyclerView</code> as well as determining the policy for when to recycle
+ * item views that are no longer visible to the user. By changing the <code>LayoutManager</code>
+ * a <code>RecyclerView</code> can be used to implement a standard vertically scrolling list,
+ * a uniform grid, staggered grids, horizontally scrolling collections and more. Several stock
+ * layout managers are provided for general use.
+ */
+ public static abstract class LayoutManager {
+ RecyclerView mRecyclerView;
+
+ @Nullable
+ SmoothScroller mSmoothScroller;
+
+ /**
+ * @deprecated LayoutManagers should not access the RecyclerView they are bound to directly.
+ * See the other methods on LayoutManager for accessing child views and
+ * container properties instead. <em>This method will be removed.</em>
+ */
+ public final RecyclerView getRecyclerView() {
+ return mRecyclerView;
+ }
+
+ /**
+ * Calls {@code RecyclerView#requestLayout} on the underlying RecyclerView
+ */
+ public void requestLayout() {
+ if(mRecyclerView != null) {
+ mRecyclerView.requestLayout();
+ }
+ }
+
+ /**
+ * Called when this LayoutManager is both attached to a RecyclerView and that RecyclerView
+ * is attached to a window.
+ *
+ * <p>Subclass implementations should always call through to the superclass implementation.
+ * </p>
+ *
+ * @param view The RecyclerView this LayoutManager is bound to
+ */
+ public void onAttachedToWindow(RecyclerView view) {
+ }
+
+ /**
+ * Called when this LayoutManager is detached from its parent RecyclerView or when
+ * its parent RecyclerView is detached from its window.
+ *
+ * <p>Subclass implementations should always call through to the superclass implementation.
+ * </p>
+ *
+ * @param view The RecyclerView this LayoutManager is bound to
+ */
+ public void onDetachedFromWindow(RecyclerView view) {
+ }
+
+ /**
+ * @deprecated Use
+ * {@link #onLayoutChildren(RecyclerView.Adapter, RecyclerView.Recycler, boolean,
+ * RecyclerView.State)}
+ */
+ @Deprecated
+ public void layoutChildren(Adapter adapter, Recycler recycler) {
+ }
+
+
+ /**
+ * @deprecated Use
+ * {@link #onLayoutChildren(Adapter, Recycler, boolean, RecyclerView.State)}
+ */
+ @Deprecated
+ public void layoutChildren(Adapter adapter, Recycler recycler, boolean structureChanged) {
+ layoutChildren(adapter, recycler);
+ }
+
+ /**
+ * Lay out all relevant child views from the given adapter.
+ *
+ * @param adapter Adapter that will supply and bind views from a data set
+ * @param recycler Recycler to use for fetching potentially cached views for a
+ * position
+ * @param structureChanged true if the structure of the data set has changed since
+ * the last call to onLayoutChildren, false otherwise
+ * @param state Transient state of RecyclerView
+ */
+ public void onLayoutChildren(Adapter adapter, Recycler recycler, boolean structureChanged,
+ State state) {
+ Log.e(TAG, "You must override onLayoutChildren(Adapter adapter, Recycler recycler, "
+ + "boolean structureChanged, State state)");
+ layoutChildren(adapter, recycler, structureChanged);
+ }
+
+ /**
+ * Create a default <code>LayoutParams</code> object for a child of the RecyclerView.
+ *
+ * <p>LayoutManagers will often want to use a custom <code>LayoutParams</code> type
+ * to store extra information specific to the layout. Client code should subclass
+ * {@link RecyclerView.LayoutParams} for this purpose.</p>
+ *
+ * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
+ * you must also override
+ * {@link #checkLayoutParams(LayoutParams)},
+ * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
+ * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
+ *
+ * @return A new LayoutParams for a child view
+ */
+ public abstract LayoutParams generateDefaultLayoutParams();
+
+ /**
+ * Determines the validity of the supplied LayoutParams object.
+ *
+ * <p>This should check to make sure that the object is of the correct type
+ * and all values are within acceptable ranges. The default implementation
+ * returns <code>true</code> for non-null params.</p>
+ *
+ * @param lp LayoutParams object to check
+ * @return true if this LayoutParams object is valid, false otherwise
+ */
+ public boolean checkLayoutParams(LayoutParams lp) {
+ return lp != null;
+ }
+
+ /**
+ * Create a LayoutParams object suitable for this LayoutManager, copying relevant
+ * values from the supplied LayoutParams object if possible.
+ *
+ * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
+ * you must also override
+ * {@link #checkLayoutParams(LayoutParams)},
+ * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
+ * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
+ *
+ * @param lp Source LayoutParams object to copy values from
+ * @return a new LayoutParams object
+ */
+ public LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ if (lp instanceof LayoutParams) {
+ return new LayoutParams((LayoutParams) lp);
+ } else if (lp instanceof MarginLayoutParams) {
+ return new LayoutParams((MarginLayoutParams) lp);
+ } else {
+ return new LayoutParams(lp);
+ }
+ }
+
+ /**
+ * Create a LayoutParams object suitable for this LayoutManager from
+ * an inflated layout resource.
+ *
+ * <p><em>Important:</em> if you use your own custom <code>LayoutParams</code> type
+ * you must also override
+ * {@link #checkLayoutParams(LayoutParams)},
+ * {@link #generateLayoutParams(android.view.ViewGroup.LayoutParams)} and
+ * {@link #generateLayoutParams(android.content.Context, android.util.AttributeSet)}.</p>
+ *
+ * @param c Context for obtaining styled attributes
+ * @param attrs AttributeSet describing the supplied arguments
+ * @return a new LayoutParams object
+ */
+ public LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
+ return new LayoutParams(c, attrs);
+ }
+
+ /**
+ * Scroll horizontally by dx pixels in screen coordinates and return the distance traveled.
+ * The default implementation does nothing and returns 0.
+ *
+ * @param dx distance to scroll by in pixels. X increases as scroll position
+ * approaches the right.
+ * @param adapter Adapter that will supply and bind views from a data set
+ * @param recycler Recycler to use for fetching potentially cached views for a
+ * position
+ * @param state Transient state of RecyclerView
+ * @return The actual distance scrolled. The return value will be negative if dx was
+ * negative and scrolling proceeeded in that direction.
+ * <code>Math.abs(result)</code> may be less than dx if a boundary was reached.
+ */
+ public int scrollHorizontallyBy(int dx, Adapter adapter, Recycler recycler,
+ State state) {
+ Log.e(TAG, "you must override "
+ + "scrollHorizontallyBy(dx,adapter,recycler,state)");
+ return scrollHorizontallyBy(dx, adapter, recycler);
+ }
+
+
+ /**
+ * @deprecated Use
+ * {@link #scrollHorizontallyBy(int, Adapter, Recycler, RecyclerView.State)}
+ */
+ @Deprecated
+ public int scrollHorizontallyBy(int dx, Adapter adapter, Recycler recycler) {
+ return scrollHorizontallyBy(dx, recycler);
+ }
+
+ /**
+ * @deprecated Use
+ * {@link #scrollHorizontallyBy(int, Adapter, Recycler, RecyclerView.State)}
+ */
+ @Deprecated
+ public int scrollHorizontallyBy(int dx, Recycler recycler) {
+ return 0;
+ }
+
+ /**
+ * Scroll vertically by dy pixels in screen coordinates and return the distance traveled.
+ * The default implementation does nothing and returns 0.
+ *
+ * @param dy distance to scroll in pixels. Y increases as scroll position
+ * approaches the bottom.
+ * @param adapter Adapter that will supply and bind views from a data set
+ * @param recycler Recycler to use for fetching potentially cached views for a
+ * position
+ * @param state Transient state of RecyclerView
+ * @return The actual distance scrolled. The return value will be negative if dy was
+ * negative and scrolling proceeeded in that direction.
+ * <code>Math.abs(result)</code> may be less than dy if a boundary was reached.
+ */
+ public int scrollVerticallyBy(int dy, Adapter adapter, Recycler recycler,
+ State state) {
+ Log.e(TAG, "you should override "
+ + "scrollVerticallyBy(dx,adapter,recycler,state)");
+ return scrollVerticallyBy(dy, adapter, recycler);
+ }
+
+
+ /**
+ * @deprecated Use
+ * {@link #scrollVerticallyBy(int, Adapter, Recycler, RecyclerView.State)}
+ */
+ @Deprecated
+ public int scrollVerticallyBy(int dy, Adapter adapter, Recycler recycler) {
+ return scrollVerticallyBy(dy, recycler);
+ }
+
+ /**
+ * @deprecated API changed to include the Adapter to use. Override
+ * {@link #scrollVerticallyBy(int, Adapter, Recycler, RecyclerView.State)} instead.
+ */
+ public int scrollVerticallyBy(int dy, Recycler recycler) {
+ return 0;
+ }
+
+ /**
+ * Query if horizontal scrolling is currently supported. The default implementation
+ * returns false.
+ *
+ * @return True if this LayoutManager can scroll the current contents horizontally
+ */
+ public boolean canScrollHorizontally() {
+ return false;
+ }
+
+ /**
+ * Query if vertical scrolling is currently supported. The default implementation
+ * returns false.
+ *
+ * @return True if this LayoutManager can scroll the current contents vertically
+ */
+ public boolean canScrollVertically() {
+ return false;
+ }
+
+ /**
+ * Scroll to the specified adapter position.
+ *
+ * Actual position of the item on the screen depends on the LayoutManager implementation.
+ * @param position Scroll to this adapter position.
+ */
+ public void scrollToPosition(int position) {
+ if (DEBUG) {
+ Log.e(TAG, "You MUST implement scrollToPosition. It will soon become abstract");
+ }
+ }
+
+ /**
+ * <p>Smooth scroll to the specified adapter position.</p>
+ * <p>To support smooth scrolling, override this method, create your {@link SmoothScroller}
+ * instance and call {@link #startSmoothScroll(SmoothScroller)}.
+ * </p>
+ * @param recyclerView The RecyclerView to which this layout manager is attached
+ * @param adapter
+ * @param position Scroll to this adapter position.
+ */
+ public void smoothScrollToPosition(RecyclerView recyclerView, Adapter adapter,
+ int position) {
+ Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling");
+ }
+
+ /**
+ * <p>Starts a smooth scroll using the provided SmoothScroller.</p>
+ * <p>Calling this method will cancel any previous smooth scroll request.</p>
+ * @param smoothScroller Unstance which defines how smooth scroll should be animated
+ */
+ public void startSmoothScroll(SmoothScroller smoothScroller) {
+ if (mSmoothScroller != null && smoothScroller != mSmoothScroller
+ && mSmoothScroller.isRunning()) {
+ mSmoothScroller.stop();
+ }
+ mSmoothScroller = smoothScroller;
+ mSmoothScroller.start(mRecyclerView, this);
+ }
+
+ /**
+ * @return true if RecycylerView is currently in the state of smooth scrolling.
+ */
+ public boolean isSmoothScrolling() {
+ return mSmoothScroller != null && mSmoothScroller.isRunning();
+ }
+
+
+ /**
+ * Returns the resolved layout direction for this RecyclerView.
+ *
+ * @return {@link android.support.v4.view.ViewCompat#LAYOUT_DIRECTION_RTL} if the layout
+ * direction is RTL or returns
+ * {@link android.support.v4.view.ViewCompat#LAYOUT_DIRECTION_LTR} if the layout direction
+ * is not RTL.
+ */
+ public int getLayoutDirection() {
+ return ViewCompat.getLayoutDirection(mRecyclerView);
+ }
+
+ /**
+ * 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(android.support.v7.widget.RecyclerView.Adapter, int)}.
+ *
+ * @param child View to add
+ * @param index Index to add child at
+ */
+ 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);
+ }
+ }
+ }
+
+ /**
+ * 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(android.support.v7.widget.RecyclerView.Adapter, int)}.
+ *
+ * @param child View to add
+ */
+ public void addView(View child) {
+ addView(child, -1);
+ }
+
+ /**
+ * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should
+ * use this method to completely remove a child view that is no longer needed.
+ * LayoutManagers should strongly consider recycling removed views using
+ * {@link Recycler#recycleView(android.view.View)}.
+ *
+ * @param child View to remove
+ */
+ public void removeView(View child) {
+ final Adapter adapter = mRecyclerView.getAdapter();
+ if (adapter != null) {
+ adapter.onViewDetachedFromWindow(getChildViewHolderInt(child));
+ }
+ mRecyclerView.onChildDetachedFromWindow(child);
+ mRecyclerView.removeView(child);
+ }
+
+ /**
+ * Remove a view from the currently attached RecyclerView if needed. LayoutManagers should
+ * use this method to completely remove a child view that is no longer needed.
+ * LayoutManagers should strongly consider recycling removed views using
+ * {@link Recycler#recycleView(android.view.View)}.
+ *
+ * @param index Index of the child view to remove
+ */
+ public void removeViewAt(int index) {
+ final View child = mRecyclerView.getChildAt(index);
+ if (child != null) {
+ final Adapter adapter = mRecyclerView.getAdapter();
+ if (adapter != null) {
+ adapter.onViewDetachedFromWindow(getChildViewHolderInt(child));
+ }
+ mRecyclerView.onChildDetachedFromWindow(child);
+ mRecyclerView.removeViewAt(index);
+ }
+ }
+
+ /**
+ * Remove all views from the currently attached RecyclerView. This will not recycle
+ * any of the affected views; the LayoutManager is responsible for doing so if desired.
+ */
+ public void removeAllViews() {
+ final Adapter adapter = mRecyclerView.getAdapter();
+ if (adapter != null) {
+ final int childCount = mRecyclerView.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = mRecyclerView.getChildAt(i);
+ adapter.onViewDetachedFromWindow(getChildViewHolderInt(child));
+ mRecyclerView.onChildDetachedFromWindow(child);
+ }
+ }
+ mRecyclerView.removeAllViews();
+ }
+
+ /**
+ * Returns the adapter position of the item represented by the given View.
+ *
+ * @param view The view to query
+ * @return The adapter position of the item which is rendered by this View.
+ */
+ public int getPosition(View view) {
+ return ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewPosition();
+ }
+
+ /**
+ * <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
+ * child views.</p>
+ *
+ * @param position Position of the item in adapter
+ * @return The child view that represents the given position or null if the position is not
+ * visible
+ */
+ public View findViewByPosition(int position) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (getPosition(child) == position) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Temporarily detach a child view.
+ *
+ * <p>LayoutManagers may want to perform a lightweight detach operation to rearrange
+ * views currently attached to the RecyclerView. Generally LayoutManager implementations
+ * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}
+ * so that the detached view may be rebound and reused.</p>
+ *
+ * <p>If a LayoutManager uses this method to detach a view, it <em>must</em>
+ * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach}
+ * or {@link #removeDetachedView(android.view.View) fully remove} the detached view
+ * before the LayoutManager entry point method called by RecyclerView returns.</p>
+ *
+ * @param child Child to detach
+ */
+ public void detachView(View child) {
+ if (DISPATCH_TEMP_DETACH) {
+ ViewCompat.dispatchStartTemporaryDetach(child);
+ }
+ mRecyclerView.detachViewFromParent(child);
+ }
+
+ /**
+ * Temporarily detach a child view.
+ *
+ * <p>LayoutManagers may want to perform a lightweight detach operation to rearrange
+ * views currently attached to the RecyclerView. Generally LayoutManager implementations
+ * will want to use {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)}
+ * so that the detached view may be rebound and reused.</p>
+ *
+ * <p>If a LayoutManager uses this method to detach a view, it <em>must</em>
+ * {@link #attachView(android.view.View, int, RecyclerView.LayoutParams) reattach}
+ * or {@link #removeDetachedView(android.view.View) fully remove} the detached view
+ * before the LayoutManager entry point method called by RecyclerView returns.</p>
+ *
+ * @param index Index of the child to detach
+ */
+ public void detachViewAt(int index) {
+ if (DISPATCH_TEMP_DETACH) {
+ ViewCompat.dispatchStartTemporaryDetach(mRecyclerView.getChildAt(index));
+ }
+ mRecyclerView.detachViewFromParent(index);
+ }
+
+ /**
+ * Reattach a previously {@link #detachView(android.view.View) detached} view.
+ * This method should not be used to reattach views that were previously
+ * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
+ *
+ * @param child Child to reattach
+ * @param index Intended child index for child
+ * @param lp LayoutParams for child
+ */
+ public void attachView(View child, int index, LayoutParams lp) {
+ mRecyclerView.attachViewToParent(child, index, lp);
+ if (DISPATCH_TEMP_DETACH) {
+ ViewCompat.dispatchFinishTemporaryDetach(child);
+ }
+ }
+
+ /**
+ * Reattach a previously {@link #detachView(android.view.View) detached} view.
+ * This method should not be used to reattach views that were previously
+ * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
+ *
+ * @param child Child to reattach
+ * @param index Intended child index for child
+ */
+ public void attachView(View child, int index) {
+ attachView(child, index, (LayoutParams) child.getLayoutParams());
+ }
+
+ /**
+ * Reattach a previously {@link #detachView(android.view.View) detached} view.
+ * This method should not be used to reattach views that were previously
+ * {@link #detachAndScrapView(android.view.View, RecyclerView.Recycler)} scrapped}.
+ *
+ * @param child Child to reattach
+ */
+ public void attachView(View child) {
+ attachView(child, -1);
+ }
+
+ /**
+ * Finish removing a view that was previously temporarily
+ * {@link #detachView(android.view.View) detached}.
+ *
+ * @param child Detached child to remove
+ */
+ public void removeDetachedView(View child) {
+ mRecyclerView.removeDetachedView(child, false);
+ }
+
+ /**
+ * 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
+ * different data.</p>
+ *
+ * @param child Child to detach and scrap
+ * @param recycler Recycler to deposit the new scrap view into
+ */
+ public void detachAndScrapView(View child, Recycler recycler) {
+ detachView(child);
+ recycler.scrapView(child);
+ }
+
+ /**
+ * 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
+ * different data.</p>
+ *
+ * @param index Index of child to detach and scrap
+ * @param recycler Recycler to deposit the new scrap view into
+ */
+ public void detachAndScrapViewAt(int index, Recycler recycler) {
+ final View child = getChildAt(index);
+ detachViewAt(index);
+ recycler.scrapView(child);
+ }
+
+ /**
+ * Remove a child view and recycle it using the given Recycler.
+ *
+ * @param child Child to remove and recycle
+ * @param recycler Recycler to use to recycle child
+ */
+ public void removeAndRecycleView(View child, Recycler recycler) {
+ removeView(child);
+ recycler.recycleView(child);
+ }
+
+ /**
+ * Remove a child view and recycle it using the given Recycler.
+ *
+ * @param index Index of child to remove and recycle
+ * @param recycler Recycler to use to recycle child
+ */
+ public void removeAndRecycleViewAt(int index, Recycler recycler) {
+ final View view = getChildAt(index);
+ removeViewAt(index);
+ recycler.recycleView(view);
+ }
+
+ /**
+ * Return the current number of child views attached to the parent RecyclerView.
+ * This does not include child views that were temporarily detached and/or scrapped.
+ *
+ * @return Number of attached children
+ */
+ public int getChildCount() {
+ return mRecyclerView != null ? mRecyclerView.getChildCount() : 0;
+ }
+
+ /**
+ * Return the child view at the given index
+ * @param index Index of child to return
+ * @return Child view at index
+ */
+ public View getChildAt(int index) {
+ return mRecyclerView != null ? mRecyclerView.getChildAt(index) : null;
+ }
+
+ /**
+ * Return the width of the parent RecyclerView
+ *
+ * @return Width in pixels
+ */
+ public int getWidth() {
+ return mRecyclerView != null ? mRecyclerView.getWidth() : 0;
+ }
+
+ /**
+ * Return the height of the parent RecyclerView
+ *
+ * @return Height in pixels
+ */
+ public int getHeight() {
+ return mRecyclerView != null ? mRecyclerView.getHeight() : 0;
+ }
+
+ /**
+ * Return the left padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingLeft() {
+ return mRecyclerView != null ? mRecyclerView.getPaddingLeft() : 0;
+ }
+
+ /**
+ * Return the top padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingTop() {
+ return mRecyclerView != null ? mRecyclerView.getPaddingTop() : 0;
+ }
+
+ /**
+ * Return the right padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingRight() {
+ return mRecyclerView != null ? mRecyclerView.getPaddingRight() : 0;
+ }
+
+ /**
+ * Return the bottom padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingBottom() {
+ return mRecyclerView != null ? mRecyclerView.getPaddingBottom() : 0;
+ }
+
+ /**
+ * Return the start padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingStart() {
+ return mRecyclerView != null ? ViewCompat.getPaddingStart(mRecyclerView) : 0;
+ }
+
+ /**
+ * Return the end padding of the parent RecyclerView
+ *
+ * @return Padding in pixels
+ */
+ public int getPaddingEnd() {
+ return mRecyclerView != null ? ViewCompat.getPaddingEnd(mRecyclerView) : 0;
+ }
+
+ /**
+ * Returns true if the RecyclerView this LayoutManager is bound to has focus.
+ *
+ * @return True if the RecyclerView has focus, false otherwise.
+ * @see View#isFocused()
+ */
+ public boolean isFocused() {
+ return mRecyclerView != null && mRecyclerView.isFocused();
+ }
+
+ /**
+ * Returns true if the RecyclerView this LayoutManager is bound to has or contains focus.
+ *
+ * @return true if the RecyclerView has or contains focus
+ * @see View#hasFocus()
+ */
+ public boolean hasFocus() {
+ return mRecyclerView != null && mRecyclerView.hasFocus();
+ }
+
+ /**
+ * Return the number of items in the adapter bound to the parent RecyclerView
+ *
+ * @return Items in the bound adapter
+ */
+ public int getItemCount() {
+ final Adapter a = mRecyclerView != null ? mRecyclerView.getAdapter() : null;
+ return a != null ? a.getItemCount() : 0;
+ }
+
+ /**
+ * Offset all child views attached to the parent RecyclerView by dx pixels along
+ * the horizontal axis.
+ *
+ * @param dx Pixels to offset by
+ */
+ public void offsetChildrenHorizontal(int dx) {
+ if (mRecyclerView != null) {
+ mRecyclerView.offsetChildrenHorizontal(dx);
+ }
+ }
+
+ /**
+ * Offset all child views attached to the parent RecyclerView by dy pixels along
+ * the vertical axis.
+ *
+ * @param dy Pixels to offset by
+ */
+ public void offsetChildrenVertical(int dy) {
+ if (mRecyclerView != null) {
+ mRecyclerView.offsetChildrenVertical(dy);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @param recycler Recycler to scrap views into
+ */
+ public void detachAndScrapAttachedViews(Recycler recycler) {
+ final int childCount = getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View v = getChildAt(i);
+ detachViewAt(i);
+ recycler.scrapView(v);
+ }
+ }
+
+ /**
+ * Remove and recycle all scrap views currently tracked by Recycler. Recycled views
+ * will be made available for later reuse.
+ *
+ * @param recycler Recycler tracking scrap views to remove
+ */
+ public void removeAndRecycleScrap(Recycler recycler) {
+ final int scrapCount = recycler.getScrapCount();
+ for (int i = 0; i < scrapCount; i++) {
+ final View scrap = recycler.getScrapViewAt(i);
+ mRecyclerView.removeDetachedView(scrap, false);
+ recycler.quickRecycleScrapView(scrap);
+ }
+ recycler.clearScrap();
+ }
+
+ /**
+ * Measure a child view using standard measurement policy, taking the padding
+ * of the parent RecyclerView and any added item decorations into account.
+ *
+ * <p>If the RecyclerView can be scrolled in either dimension the caller may
+ * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
+ *
+ * @param child Child view to measure
+ * @param widthUsed Width in pixels currently consumed by other views, if relevant
+ * @param heightUsed Height in pixels currently consumed by other views, if relevant
+ */
+ public void measureChild(View child, int widthUsed, int heightUsed) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
+ widthUsed += insets.left + insets.right;
+ heightUsed += insets.top + insets.bottom;
+
+ final int widthSpec = getChildMeasureSpec(getWidth(),
+ getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
+ canScrollHorizontally());
+ final int heightSpec = getChildMeasureSpec(getHeight(),
+ getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
+ canScrollVertically());
+ child.measure(widthSpec, heightSpec);
+ }
+
+ /**
+ * Measure a child view using standard measurement policy, taking the padding
+ * of the parent RecyclerView, any added item decorations and the child margins
+ * into account.
+ *
+ * <p>If the RecyclerView can be scrolled in either dimension the caller may
+ * pass 0 as the widthUsed or heightUsed parameters as they will be irrelevant.</p>
+ *
+ * @param child Child view to measure
+ * @param widthUsed Width in pixels currently consumed by other views, if relevant
+ * @param heightUsed Height in pixels currently consumed by other views, if relevant
+ */
+ public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
+ widthUsed += insets.left + insets.right;
+ heightUsed += insets.top + insets.bottom;
+
+ final int widthSpec = getChildMeasureSpec(getWidth(),
+ getPaddingLeft() + getPaddingRight() +
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
+ canScrollHorizontally());
+ final int heightSpec = getChildMeasureSpec(getHeight(),
+ getPaddingTop() + getPaddingBottom() +
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
+ canScrollVertically());
+ child.measure(widthSpec, heightSpec);
+ }
+
+ /**
+ * Calculate a MeasureSpec value for measuring a child view in one dimension.
+ *
+ * @param parentSize Size of the parent view where the child will be placed
+ * @param padding Total space currently consumed by other elements of parent
+ * @param childDimension Desired size of the child view, or MATCH_PARENT/WRAP_CONTENT.
+ * Generally obtained from the child view's LayoutParams
+ * @param canScroll true if the parent RecyclerView can scroll in this dimension
+ *
+ * @return a MeasureSpec value for the child view
+ */
+ public static int getChildMeasureSpec(int parentSize, int padding, int childDimension,
+ boolean canScroll) {
+ int size = Math.max(0, parentSize - padding);
+ int resultSize = 0;
+ int resultMode = 0;
+
+ if (canScroll) {
+ if (childDimension >= 0) {
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else {
+ // MATCH_PARENT can't be applied since we can scroll in this dimension, wrap
+ // instead using UNSPECIFIED.
+ resultSize = 0;
+ resultMode = MeasureSpec.UNSPECIFIED;
+ }
+ } else {
+ if (childDimension >= 0) {
+ resultSize = childDimension;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.FILL_PARENT) {
+ resultSize = size;
+ resultMode = MeasureSpec.EXACTLY;
+ } else if (childDimension == LayoutParams.WRAP_CONTENT) {
+ resultSize = size;
+ resultMode = MeasureSpec.AT_MOST;
+ }
+ }
+ return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
+ }
+
+ /**
+ * Returns the measured width of the given child, plus the additional size of
+ * any insets applied by {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child view to query
+ * @return child's measured width plus <code>ItemDecoration</code> insets
+ *
+ * @see View#getMeasuredWidth()
+ */
+ public int getDecoratedMeasuredWidth(View child) {
+ final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ return child.getMeasuredWidth() + insets.left + insets.right;
+ }
+
+ /**
+ * Returns the measured height of the given child, plus the additional size of
+ * any insets applied by {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child view to query
+ * @return child's measured height plus <code>ItemDecoration</code> insets
+ *
+ * @see View#getMeasuredHeight()
+ */
+ public int getDecoratedMeasuredHeight(View child) {
+ final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ return child.getMeasuredHeight() + insets.top + insets.bottom;
+ }
+
+ /**
+ * Lay out the given child view within the RecyclerView using coordinates that
+ * include any current {@link ItemDecoration ItemDecorations}.
+ *
+ * <p>LayoutManagers should prefer working in sizes and coordinates that include
+ * item decoration insets whenever possible. This allows the LayoutManager to effectively
+ * ignore decoration insets within measurement and layout code. See the following
+ * methods:</p>
+ * <ul>
+ * <li>{@link #measureChild(View, int, int)}</li>
+ * <li>{@link #measureChildWithMargins(View, int, int)}</li>
+ * <li>{@link #getDecoratedLeft(View)}</li>
+ * <li>{@link #getDecoratedTop(View)}</li>
+ * <li>{@link #getDecoratedRight(View)}</li>
+ * <li>{@link #getDecoratedBottom(View)}</li>
+ * <li>{@link #getDecoratedMeasuredWidth(View)}</li>
+ * <li>{@link #getDecoratedMeasuredHeight(View)}</li>
+ * </ul>
+ *
+ * @param child Child to lay out
+ * @param left Left edge, with item decoration insets included
+ * @param top Top edge, with item decoration insets included
+ * @param right Right edge, with item decoration insets included
+ * @param bottom Bottom edge, with item decoration insets included
+ *
+ * @see View#layout(int, int, int, int)
+ */
+ public void layoutDecorated(View child, int left, int top, int right, int bottom) {
+ final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ child.layout(left + insets.left, top + insets.top, right - insets.right,
+ bottom - insets.bottom);
+ }
+
+ /**
+ * Returns the left edge of the given child view within its parent, offset by any applied
+ * {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child to query
+ * @return Child left edge with offsets applied
+ */
+ public int getDecoratedLeft(View child) {
+ final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ return child.getLeft() - insets.left;
+ }
+
+ /**
+ * Returns the top edge of the given child view within its parent, offset by any applied
+ * {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child to query
+ * @return Child top edge with offsets applied
+ */
+ public int getDecoratedTop(View child) {
+ final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ return child.getTop() - insets.top;
+ }
+
+ /**
+ * Returns the right edge of the given child view within its parent, offset by any applied
+ * {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child to query
+ * @return Child right edge with offsets applied
+ */
+ public int getDecoratedRight(View child) {
+ final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ return child.getRight() + insets.right;
+ }
+
+ /**
+ * Returns the bottom edge of the given child view within its parent, offset by any applied
+ * {@link ItemDecoration ItemDecorations}.
+ *
+ * @param child Child to query
+ * @return Child bottom edge with offsets applied
+ */
+ public int getDecoratedBottom(View child) {
+ final Rect insets = ((LayoutParams) child.getLayoutParams()).mDecorInsets;
+ return child.getBottom() + insets.bottom;
+ }
+
+ /**
+ * Called when searching for a focusable view in the given direction has failed
+ * for the current content of the RecyclerView.
+ *
+ * <p>This is the LayoutManager's opportunity to populate views in the given direction
+ * to fulfill the request if it can. The LayoutManager should attach and return
+ * the view to be focused. The default implementation returns null.</p>
+ *
+ * @param focused The currently focused view
+ * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+ * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+ * or 0 for not applicable
+ * @param adapter Adapter to use for obtaining new views
+ * @param recycler The recycler to use for obtaining views for currently offscreen items
+ * @param state Transient state of RecyclerView
+ * @return The chosen view to be focused
+ */
+ public View onFocusSearchFailed(View focused, int direction, Adapter adapter,
+ Recycler recycler, State state) {
+ Log.e(TAG, "You must override onFocusSearchFailed(View focused, int direction, "
+ + "Adapter adapter, Recycler recycler, State state)");
+ return this.onFocusSearchFailed(focused, direction, adapter, recycler);
+ }
+
+ /**
+ * @deprecated Use {@link #onFocusSearchFailed(View, int, RecyclerView.Adapter,
+ * RecyclerView.Recycler, RecyclerView.State)}
+ */
+ @Deprecated
+ public View onFocusSearchFailed(View focused, int direction, Adapter adapter,
+ Recycler recycler) {
+ return onFocusSearchFailed(focused, direction, recycler);
+ }
+
+ /**
+ * @deprecated API changed to supply the Adapter. Override
+ * {@link #onFocusSearchFailed(android.view.View, int, Adapter, Recycler)} instead.
+ */
+ public View onFocusSearchFailed(View focused, int direction, Recycler recycler) {
+ return null;
+ }
+
+ /**
+ * This method gives a LayoutManager an opportunity to intercept the initial focus search
+ * before the default behavior of {@link FocusFinder} is used. If this method returns
+ * null FocusFinder will attempt to find a focusable child view. If it fails
+ * then {@link #onFocusSearchFailed(View, int, RecyclerView.Adapter, RecyclerView.Recycler)}
+ * will be called to give the LayoutManager an opportunity to add new views for items
+ * that did not have attached views representing them. The LayoutManager should not add
+ * or remove views from this method.
+ *
+ * @param focused The currently focused view
+ * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+ * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+ * @return A descendant view to focus or null to fall back to default behavior.
+ * The default implementation returns null.
+ */
+ public View onInterceptFocusSearch(View focused, int direction) {
+ return null;
+ }
+
+ /**
+ * @deprecated This method will be removed. Override {@link #requestChildRectangleOnScreen(
+ * RecyclerView, android.view.View, android.graphics.Rect, boolean)} instead.
+ */
+ public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
+ return requestChildRectangleOnScreen(mRecyclerView, child, rect, immediate);
+ }
+
+ /**
+ * Called when a child of the RecyclerView wants a particular rectangle to be positioned
+ * onto the screen. See {@link ViewParent#requestChildRectangleOnScreen(android.view.View,
+ * android.graphics.Rect, boolean)} for more details.
+ *
+ * <p>The base implementation will attempt to perform a standard programmatic scroll
+ * to bring the given rect into view, within the padded area of the RecyclerView.</p>
+ *
+ * @param child The direct child making the request.
+ * @param rect The rectangle in the child's coordinates the child
+ * wishes to be on the screen.
+ * @param immediate True to forbid animated or delayed scrolling,
+ * false otherwise
+ * @return Whether the group scrolled to handle the operation
+ */
+ public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect,
+ boolean immediate) {
+ final int parentLeft = getPaddingLeft();
+ final int parentTop = getPaddingTop();
+ final int parentRight = getWidth() - getPaddingRight();
+ final int parentBottom = getHeight() - getPaddingBottom();
+ final int childLeft = child.getLeft() + rect.left;
+ final int childTop = child.getTop() + rect.top;
+ final int childRight = childLeft + rect.right;
+ final int childBottom = childTop + rect.bottom;
+
+ final int offScreenLeft = Math.min(0, childLeft - parentLeft);
+ final int offScreenTop = Math.min(0, childTop - parentTop);
+ final int offScreenRight = Math.max(0, childRight - parentRight);
+ final int offScreenBottom = Math.max(0, childBottom - parentBottom);
+
+ // Favor the "start" layout direction over the end when bringing one side or the other
+ // of a large rect into view.
+ final int dx;
+ if (ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_RTL) {
+ dx = offScreenRight != 0 ? offScreenRight : offScreenLeft;
+ } else {
+ dx = offScreenLeft != 0 ? offScreenLeft : offScreenRight;
+ }
+
+ // Favor bringing the top into view over the bottom
+ final int dy = offScreenTop != 0 ? offScreenTop : offScreenBottom;
+
+ if (dx != 0 || dy != 0) {
+ if (immediate) {
+ parent.scrollBy(dx, dy);
+ } else {
+ parent.smoothScrollBy(dx, dy);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Called when a descendant view of the RecyclerView requests focus.
+ *
+ * <p>A LayoutManager wishing to keep focused views aligned in a specific
+ * portion of the view may implement that behavior in an override of this method.</p>
+ *
+ * <p>If the LayoutManager executes different behavior that should override the default
+ * 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 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 onRequestChildFocus(child, focused);
+ }
+
+ /**
+ * @deprecated This method will be removed. Override
+ * {@link #onRequestChildFocus(RecyclerView, android.view.View, android.view.View)}
+ * instead.
+ */
+ public boolean onRequestChildFocus(View child, View focused) {
+ return false;
+ }
+
+ /**
+ * @deprecated This method will be removed. Override
+ * {@link #onAdapterChanged(RecyclerView.Adapter, RecyclerView.Adapter)}
+ * instead.
+ */
+ public void onAdapterChanged() {
+ removeAllViews();
+ }
+
+ /**
+ * Called if the RecyclerView this LayoutManager is bound to has a different adapter set.
+ * The LayoutManager may use this opportunity to clear caches and configure state such
+ * that it can relayout appropriately with the new data and potentially new view types.
+ *
+ * <p>The default implementation removes all currently attached views.</p>
+ *
+ * @param oldAdapter The previous adapter instance. Will be null if there was previously no
+ * adapter.
+ * @param newAdapter The new adapter instance. Might be null if
+ * {@link #setAdapter(RecyclerView.Adapter)} is called with {@code null}.
+ */
+ public void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter) {
+ onAdapterChanged();
+ }
+
+ /**
+ * Called to populate focusable views within the RecyclerView.
+ *
+ * <p>The LayoutManager implementation should return <code>true</code> if the default
+ * behavior of {@link ViewGroup#addFocusables(java.util.ArrayList, int)} should be
+ * suppressed.</p>
+ *
+ * <p>The default implementation returns <code>false</code> to trigger RecyclerView
+ * to fall back to the default ViewGroup behavior.</p>
+ *
+ * @param recyclerView The RecyclerView hosting this LayoutManager
+ * @param views List of output views. This method should add valid focusable views
+ * to this list.
+ * @param direction One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
+ * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+ * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
+ * @param focusableMode The type of focusables to be added.
+ *
+ * @return true to suppress the default behavior, false to add default focusables after
+ * this method returns.
+ *
+ * @see #FOCUSABLES_ALL
+ * @see #FOCUSABLES_TOUCH_MODE
+ */
+ public boolean onAddFocusables(RecyclerView recyclerView, ArrayList<View> views,
+ int direction, int focusableMode) {
+ return false;
+ }
+
+ /**
+ * 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.)
+ *
+ * @param recyclerView
+ * @param positionStart
+ * @param itemCount
+ */
+ public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
+ }
+
+ /**
+ * Called when items have been removed from the adapter.
+ *
+ * @param recyclerView
+ * @param positionStart
+ * @param itemCount
+ */
+ public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
+ }
+
+
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeHorizontalScrollExtent()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param adapter Current adapter which is attached to RecyclerView
+ * @return The horizontal extent of the scrollbar's thumb
+ * @see RecyclerView#computeHorizontalScrollExtent()
+ */
+ public int computeHorizontalScrollExtent(Adapter adapter) {
+ return 0;
+ }
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeHorizontalScrollOffset()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param adapter Current adapter which is attached to RecyclerView
+ * @return The horizontal offset of the scrollbar's thumb
+ * @see RecyclerView#computeHorizontalScrollOffset()
+ */
+ public int computeHorizontalScrollOffset(Adapter adapter) {
+ return 0;
+ }
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeHorizontalScrollRange()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param adapter Current adapter which is attached to RecyclerView
+ * @return The total horizontal range represented by the vertical scrollbar
+ * @see RecyclerView#computeHorizontalScrollRange()
+ */
+ public int computeHorizontalScrollRange(Adapter adapter) {
+ return 0;
+ }
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeVerticalScrollRange()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param adapter Current adapter which is attached to RecyclerView
+ * @return The total vertical range represented by the vertical scrollbar
+ * @see RecyclerView#computeVerticalScrollRange()
+ */
+ public int computeVerticalScrollRange(Adapter adapter) {
+ return 0;
+ }
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeVerticalScrollOffset()} for details.</p>
+ *
+ * * <p>Default implementation returns 0.</p>
+ *
+ * @param adapter Current adapter which is attached to RecyclerView
+ * @return The vertical offset of the scrollbar's thumb
+ * @see RecyclerView#computeVerticalScrollOffset()
+ */
+ public int computeVerticalScrollOffset(Adapter adapter) {
+ return 0;
+ }
+
+ /**
+ * <p>Override this method if you want to support scroll bars.</p>
+ *
+ * <p>Read {@link RecyclerView#computeVerticalScrollExtent()} for details.</p>
+ *
+ * <p>Default implementation returns 0.</p>
+ *
+ * @param adapter Current adapter which is attached to RecyclerView
+ * @return The vertical extent of the scrollbar's thumb
+ * @see RecyclerView#computeVerticalScrollExtent()
+ */
+ public int computeVerticalScrollExtent(Adapter adapter) {
+ return 0;
+ }
+
+ /**
+ * Measure the attached RecyclerView. Implementations must call
+ * {@link #setMeasuredDimension(int, int)} before returning.
+ *
+ * <p>The default implementation will handle EXACTLY measurements and respect
+ * the minimum width and height properties of the host RecyclerView if measured
+ * as UNSPECIFIED. AT_MOST measurements will be treated as EXACTLY and the RecyclerView
+ * will consume all available space.</p>
+ *
+ * @param widthSpec Width {@link android.view.View.MeasureSpec}
+ * @param heightSpec Height {@link android.view.View.MeasureSpec}
+ */
+ public void onMeasure(int widthSpec, int heightSpec) {
+ final int widthMode = MeasureSpec.getMode(widthSpec);
+ final int heightMode = MeasureSpec.getMode(heightSpec);
+ final int widthSize = MeasureSpec.getSize(widthSpec);
+ final int heightSize = MeasureSpec.getSize(heightSpec);
+
+ int width = 0;
+ int height = 0;
+
+ switch (widthMode) {
+ case MeasureSpec.EXACTLY:
+ case MeasureSpec.AT_MOST:
+ width = widthSize;
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ default:
+ width = getMinimumWidth();
+ break;
+ }
+
+ switch (heightMode) {
+ case MeasureSpec.EXACTLY:
+ case MeasureSpec.AT_MOST:
+ height = heightSize;
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ default:
+ height = getMinimumHeight();
+ break;
+ }
+
+ setMeasuredDimension(width, height);
+ }
+
+ /**
+ * {@link View#setMeasuredDimension(int, int) Set the measured dimensions} of the
+ * host RecyclerView.
+ *
+ * @param widthSize Measured width
+ * @param heightSize Measured height
+ */
+ public void setMeasuredDimension(int widthSize, int heightSize) {
+ mRecyclerView.setMeasuredDimension(widthSize, heightSize);
+ }
+
+ /**
+ * @return The host RecyclerView's {@link View#getMinimumWidth()}
+ */
+ public int getMinimumWidth() {
+ return ViewCompat.getMinimumWidth(mRecyclerView);
+ }
+
+ /**
+ * @return The host RecyclerView's {@link View#getMinimumHeight()}
+ */
+ public int getMinimumHeight() {
+ return ViewCompat.getMinimumHeight(mRecyclerView);
+ }
+ /**
+ * <p>Called when the LayoutManager should save its state. This is a good time to save your
+ * scroll position, configuration and anything else that may be required to restore the same
+ * layout state if the LayoutManager is recreated.</p>
+ * <p>RecyclerView does NOT verify if the LayoutManager has changed between state save and
+ * restore. This will let you share information between your LayoutManagers but it is also
+ * your responsibility to make sure they use the same parcelable class.</p>
+ *
+ * @return Necessary information for LayoutManager to be able to restore its state
+ */
+ public Parcelable onSaveInstanceState() {
+ return null;
+ }
+
+
+ public void onRestoreInstanceState(Parcelable state) {
+
+ }
+
+ void stopSmoothScroller() {
+ if (mSmoothScroller != null) {
+ mSmoothScroller.stop();
+ }
+ }
+
+ private void onSmoothScrollerStopped(SmoothScroller smoothScroller) {
+ if (mSmoothScroller == smoothScroller) {
+ mSmoothScroller = null;
+ }
+ }
+ }
+
+ /**
+ * An ItemDecoration allows the application to add a special drawing and layout offset
+ * to specific item views from the adapter's data set. This can be useful for drawing dividers
+ * 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>
+ */
+ public static abstract class ItemDecoration {
+ /**
+ * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
+ * Any content drawn by this method will be drawn before the item views are drawn,
+ * and will thus appear underneath the views.
+ *
+ * @param c Canvas to draw into
+ * @param parent RecyclerView this ItemDecoration is drawing into
+ */
+ public void onDraw(Canvas c, RecyclerView parent) {
+ }
+
+ /**
+ * Draw any appropriate decorations into the Canvas supplied to the RecyclerView.
+ * Any content drawn by this method will be drawn after the item views are drawn
+ * and will thus appear over the views.
+ *
+ * @param c Canvas to draw into
+ * @param parent RecyclerView this ItemDecoration is drawing into
+ */
+ public void onDrawOver(Canvas c, RecyclerView parent) {
+ }
+
+ /**
+ * 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.
+ * The default implementation sets the bounds of outRect to 0 and returns.
+ *
+ * <p>If this ItemDecoration does not affect the positioning of item views it should set
+ * all four fields of <code>outRect</code> (left, top, right, bottom) to zero
+ * 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
+ */
+ public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
+ outRect.set(0, 0, 0, 0);
+ }
+ }
+
+ /**
+ * An OnItemTouchListener allows the application to intercept touch events in progress at the
+ * view hierarchy level of the RecyclerView before those touch events are considered for
+ * RecyclerView's own scrolling behavior.
+ *
+ * <p>This can be useful for applications that wish to implement various forms of gestural
+ * manipulation of item views within the RecyclerView. OnItemTouchListeners may intercept
+ * a touch interaction already in progress even if the RecyclerView is already handling that
+ * gesture stream itself for the purposes of scrolling.</p>
+ */
+ public interface OnItemTouchListener {
+ /**
+ * Silently observe and/or take over touch events sent to the RecyclerView
+ * before they are handled by either the RecyclerView itself or its child views.
+ *
+ * <p>The onInterceptTouchEvent methods of each attached OnItemTouchListener will be run
+ * in the order in which each listener was added, before any other touch processing
+ * by the RecyclerView itself or child views occurs.</p>
+ *
+ * @param e MotionEvent describing the touch event. All coordinates are in
+ * the RecyclerView's coordinate system.
+ * @return true if this OnItemTouchListener wishes to begin intercepting touch events, false
+ * to continue with the current behavior and continue observing future events in
+ * the gesture.
+ */
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e);
+
+ /**
+ * Process a touch event as part of a gesture that was claimed by returning true from
+ * a previous call to {@link #onInterceptTouchEvent}.
+ *
+ * @param e MotionEvent describing the touch event. All coordinates are in
+ * the RecyclerView's coordinate system.
+ */
+ public void onTouchEvent(RecyclerView rv, MotionEvent e);
+ }
+
+ /**
+ * An OnScrollListener can be set on a RecyclerView to receive messages
+ * when a scrolling event has occurred on that RecyclerView.
+ *
+ * @see RecyclerView#setOnScrollListener(OnScrollListener)
+ */
+ public interface OnScrollListener {
+ public void onScrollStateChanged(int newState);
+ public void onScrolled(int dx, int dy);
+ }
+
+ /**
+ * A RecyclerListener can be set on a RecyclerView to receive messages whenever
+ * a view is recycled.
+ *
+ * @see RecyclerView#setRecyclerListener(RecyclerListener)
+ */
+ public interface RecyclerListener {
+
+ /**
+ * This method is called whenever the view in the ViewHolder is recycled.
+ *
+ * @param holder The ViewHolder containing the view that was recycled
+ */
+ public void onViewRecycled(ViewHolder holder);
+ }
+
+ /**
+ * A ViewHolder describes an item view and metadata about its place within the RecyclerView.
+ *
+ * <p>{@link Adapter} implementations should subclass ViewHolder and add fields for caching
+ * potentially expensive {@link View#findViewById(int)} results.</p>
+ *
+ * <p>While {@link LayoutParams} belong to the {@link LayoutManager},
+ * {@link ViewHolder ViewHolders} belong to the adapter. Adapters should feel free to use
+ * their own custom ViewHolder implementations to store data that makes binding view contents
+ * easier. Implementations should assume that individual item views will hold strong references
+ * to <code>ViewHolder</code> objects and that <code>RecyclerView</code> instances may hold
+ * strong references to extra off-screen item views for caching purposes</p>
+ */
+ public static abstract class ViewHolder {
+ public final View itemView;
+
+ int mPosition = NO_POSITION;
+ long mItemId = NO_ID;
+ int mItemViewType = INVALID_TYPE;
+
+ /**
+ * This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
+ * are all valid.
+ */
+ static final int FLAG_BOUND = 1 << 0;
+
+ /**
+ * The data this ViewHolder's view reflects is stale and needs to be rebound
+ * by the adapter. mPosition and mItemId are consistent.
+ */
+ static final int FLAG_UPDATE = 1 << 1;
+
+ /**
+ * This ViewHolder's data is invalid. The identity implied by mPosition and mItemId
+ * are not to be trusted and may no longer match the item view type.
+ * This ViewHolder must be fully rebound to different data.
+ */
+ static final int FLAG_INVALID = 1 << 2;
+
+ /**
+ * This ViewHolder points at data that represents an item previously removed from the
+ * data set. Its view may still be used for things like outgoing animations.
+ */
+ static final int FLAG_REMOVED = 1 << 3;
+
+ /**
+ * This ViewHolder should not be recycled. This flag is set via setIsRecyclable()
+ * and is intended to keep views around during animations.
+ */
+ static final int FLAG_NOT_RECYCLABLE = 1 << 4;
+
+ private int mFlags;
+
+ // If non-null, view is currently considered scrap and may be reused for other data by the
+ // scrap container.
+ private Recycler mScrapContainer = null;
+
+ public ViewHolder(View itemView) {
+ if (itemView == null) {
+ throw new IllegalArgumentException("itemView may not be null");
+ }
+ this.itemView = itemView;
+ }
+
+ public final int getPosition() {
+ return mPosition;
+ }
+
+ public final long getItemId() {
+ return mItemId;
+ }
+
+ public final int getItemViewType() {
+ return mItemViewType;
+ }
+
+ boolean isScrap() {
+ return mScrapContainer != null;
+ }
+
+ void unScrap() {
+ mScrapContainer.unscrapView(this);
+ mScrapContainer = null;
+ }
+
+ void setScrapContainer(Recycler recycler) {
+ mScrapContainer = recycler;
+ }
+
+ boolean isInvalid() {
+ return (mFlags & FLAG_INVALID) != 0;
+ }
+
+ boolean needsUpdate() {
+ return (mFlags & FLAG_UPDATE) != 0;
+ }
+
+ boolean isBound() {
+ return (mFlags & FLAG_BOUND) != 0;
+ }
+
+ boolean isRemoved() {
+ return (mFlags & FLAG_REMOVED) != 0;
+ }
+
+ void setFlags(int flags, int mask) {
+ mFlags = (mFlags & ~mask) | (flags & mask);
+ }
+
+ void addFlags(int flags) {
+ mFlags |= flags;
+ }
+
+ void clearFlagsForSharedPool() {
+ mFlags = 0;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("ViewHolder{" +
+ Integer.toHexString(hashCode()) + " position=" + mPosition + " id=" + mItemId);
+ 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");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ public final void setIsRecyclable(boolean recyclable) {
+ // TODO: might want this to be a refcount instead
+ if (recyclable) {
+ mFlags &= ~FLAG_NOT_RECYCLABLE;
+ } else {
+ mFlags |= FLAG_NOT_RECYCLABLE;
+ }
+ }
+
+ public final boolean isRecyclable() {
+ return (mFlags & FLAG_NOT_RECYCLABLE) == 0 &&
+ !ViewCompat.hasTransientState(itemView);
+ }
+ }
+
+ /**
+ * 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 {
+ ViewHolder mViewHolder;
+ final Rect mDecorInsets = new Rect();
+ boolean mInsetsDirty = true;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(MarginLayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super((ViewGroup.LayoutParams) source);
+ }
+
+ /**
+ * Returns true if the view this LayoutParams is attached to needs to have its content
+ * updated from the corresponding adapter.
+ *
+ * @return true if the view should have its content updated
+ */
+ public boolean viewNeedsUpdate() {
+ return mViewHolder.needsUpdate();
+ }
+
+ /**
+ * Returns true if the view this LayoutParams is attached to is now representing
+ * potentially invalid data. A LayoutManager should scrap/recycle it.
+ *
+ * @return true if the view is invalid
+ */
+ public boolean isViewInvalid() {
+ return mViewHolder.isInvalid();
+ }
+
+ /**
+ * Returns true if the adapter data item corresponding to the view this LayoutParams
+ * is attached to has been removed from the data set. A LayoutManager may choose to
+ * treat it differently in order to animate its outgoing or disappearing state.
+ *
+ * @return true if the item the view corresponds to was removed from the data set
+ */
+ public boolean isItemRemoved() {
+ return mViewHolder.isRemoved();
+ }
+
+ /**
+ * Returns the position that the view this LayoutParams is attached to corresponds to.
+ *
+ * @return the adapter position this view was bound from
+ */
+ public int getViewPosition() {
+ return mViewHolder.getPosition();
+ }
+ }
+
+ /**
+ * Observer base class for watching changes to an {@link Adapter}.
+ * See {@link Adapter#registerAdapterDataObserver(AdapterDataObserver)}.
+ */
+ public static abstract class AdapterDataObserver {
+ public void onChanged() {
+ // Do nothing
+ }
+
+ public void onItemRangeChanged(int positionStart, int itemCount) {
+ // do nothing
+ }
+
+ public void onItemRangeInserted(int positionStart, int itemCount) {
+ // do nothing
+ }
+
+ public void onItemRangeRemoved(int positionStart, int itemCount) {
+ // do nothing
+ }
+ }
+
+ /**
+ * <p>Base class for smooth scrolling. Handles basic tracking of the target view position and
+ * provides methods to trigger a programmatic scroll.</p>
+ *
+ * @see LinearSmoothScroller
+ */
+ public static abstract class SmoothScroller {
+
+ private int mTargetPosition = RecyclerView.NO_POSITION;
+
+ private RecyclerView mRecyclerView;
+
+ private LayoutManager mLayoutManager;
+
+ private boolean mPendingInitialRun;
+
+ private boolean mRunning;
+
+ private View mTargetView;
+
+ private final Action mRecyclingAction;
+
+ public SmoothScroller() {
+ mRecyclingAction = new Action(0, 0);
+ }
+
+ /**
+ * Starts a smooth scroll for the given target position.
+ * <p>In each animation step, {@link RecyclerView} will check
+ * for the target view and call either
+ * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
+ * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)} until
+ * SmoothScroller is stopped.</p>
+ *
+ * <p>Note that if RecyclerView finds the target view, it will automatically stop the
+ * SmoothScroller. This <b>does not</b> mean that scroll will stop, it only means it will
+ * stop calling SmoothScroller in each animation step.</p>
+ */
+ void start(RecyclerView recyclerView, LayoutManager layoutManager) {
+ mRecyclerView = recyclerView;
+ mLayoutManager = layoutManager;
+ if (mTargetPosition == RecyclerView.NO_POSITION) {
+ throw new IllegalArgumentException("Invalid target position");
+ }
+ mRecyclerView.mState.withTarget(mTargetPosition);
+ mRunning = true;
+ mPendingInitialRun = true;
+ mTargetView = findViewByPosition(getTargetPosition());
+ onStart();
+ mRecyclerView.mViewFlinger.postOnAnimation();
+ }
+
+ public void setTargetPosition(int targetPosition) {
+ mTargetPosition = targetPosition;
+ }
+
+ /**
+ * @return The LayoutManager to which this SmoothScroller is attached
+ */
+ public LayoutManager getLayoutManager() {
+ return mLayoutManager;
+ }
+
+ /**
+ * Stops running the SmoothScroller in each animation callback. Note that this does not
+ * cancel any existing {@link Action} updated by
+ * {@link #onTargetFound(android.view.View, RecyclerView.State, SmoothScroller.Action)} or
+ * {@link #onSeekTargetStep(int, int, RecyclerView.State, SmoothScroller.Action)}.
+ */
+ final protected void stop() {
+ if (mRunning) {
+ onStop();
+ }
+ mRecyclerView.mState.withTarget(RecyclerView.NO_POSITION);
+ mTargetView = null;
+ mTargetPosition = RecyclerView.NO_POSITION;
+ mPendingInitialRun = false;
+ mRunning = false;
+ // trigger a cleanup
+ mLayoutManager.onSmoothScrollerStopped(this);
+ // clear references to avoid any potential leak by a custom smooth scroller
+ mLayoutManager = null;
+ mRecyclerView = null;
+ }
+
+ /**
+ * Returns true if SmoothScroller has beens started but has not received the first
+ * animation
+ * callback yet.
+ *
+ * @return True if this SmoothScroller is waiting to start
+ */
+ public boolean isPendingInitialRun() {
+ return mPendingInitialRun;
+ }
+
+
+ /**
+ * @return True if SmoothScroller is currently active
+ */
+ public boolean isRunning() {
+ return mRunning;
+ }
+
+ /**
+ * Returns the adapter position of the target item
+ *
+ * @return Adapter position of the target item or
+ * {@link RecyclerView#NO_POSITION} if no target view is set.
+ */
+ public int getTargetPosition() {
+ return mTargetPosition;
+ }
+
+ private void onAnimation(int dx, int dy) {
+ if (!mRunning || mTargetPosition == RecyclerView.NO_POSITION) {
+ stop();
+ }
+ mPendingInitialRun = false;
+ if (mTargetView != null) {
+ // verify target position
+ if (getChildPosition(mTargetView) == mTargetPosition) {
+ onTargetFound(mTargetView, mRecyclerView.mState, mRecyclingAction);
+ mRecyclingAction.runInNecessary(mRecyclerView);
+ stop();
+ } else {
+ Log.e(TAG, "Passed over target position while smooth scrolling.");
+ mTargetView = null;
+ }
+ }
+ if (mRunning) {
+ onSeekTargetStep(dx, dy, mRecyclerView.mState, mRecyclingAction);
+ mRecyclingAction.runInNecessary(mRecyclerView);
+ }
+ }
+
+ /**
+ * @see RecyclerView#getChildPosition(android.view.View)
+ */
+ public int getChildPosition(View view) {
+ return mRecyclerView.getChildPosition(view);
+ }
+
+ /**
+ * @see RecyclerView#getChildCount()
+ */
+ public int getChildCount() {
+ return mRecyclerView.getChildCount();
+ }
+
+ /**
+ * @see RecyclerView.LayoutManager#findViewByPosition(int)
+ */
+ public View findViewByPosition(int position) {
+ return mRecyclerView.mLayout.findViewByPosition(position);
+ }
+
+ /**
+ * @see RecyclerView#scrollToPosition(int)
+ */
+ public void instantScrollToPosition(int position) {
+ mRecyclerView.scrollToPosition(position);
+ }
+
+ protected void onChildAttachedToWindow(View child) {
+ if (getChildPosition(child) == getTargetPosition()) {
+ mTargetView = child;
+ if (DEBUG) {
+ Log.d(TAG, "smooth scroll target view has been attached");
+ }
+ }
+ }
+
+ /**
+ * Normalizes the vector.
+ * @param scrollVector The vector that points to the target scroll position
+ */
+ protected void normalize(PointF scrollVector) {
+ final double magnitute = Math.sqrt(scrollVector.x * scrollVector.x + scrollVector.y *
+ scrollVector.y);
+ scrollVector.x /= magnitute;
+ scrollVector.y /= magnitute;
+ }
+
+ /**
+ * Called when smooth scroll is started. This might be a good time to do setup.
+ */
+ abstract protected void onStart();
+
+ /**
+ * Called when smooth scroller is stopped. This is a good place to cleanup your state etc.
+ * @see #stop()
+ */
+ abstract protected void onStop();
+
+ /**
+ * <p>RecyclerView will call this method each time it scrolls until it can find the target
+ * position in the layout.</p>
+ * <p>SmoothScroller should check dx, dy and if scroll should be changed, update the
+ * provided {@link Action} to define the next scroll.</p>
+ *
+ * @param dx Last scroll amount horizontally
+ * @param dy Last scroll amount verticaully
+ * @param state Transient state of RecyclerView
+ * @param action If you want to trigger a new smooth scroll and cancel the previous one,
+ * update this object.
+ */
+ abstract protected void onSeekTargetStep(int dx, int dy, State state, Action action);
+
+ /**
+ * Called when the target position is laid out. This is the last callback SmoothScroller
+ * will receive and it should update the provided {@link Action} to define the scroll
+ * details towards the target view.
+ * @param targetView The view element which render the target position.
+ * @param state Transient state of RecyclerView
+ * @param action Action instance that you should update to define final scroll action
+ * towards the targetView
+ * @return An {@link Action} to finalize the smooth scrolling
+ */
+ abstract protected void onTargetFound(View targetView, State state, Action action);
+
+ /**
+ * Holds information about a smooth scroll request by a {@link SmoothScroller}.
+ */
+ public static class Action {
+
+ public static final int UNDEFINED_DURATION = Integer.MIN_VALUE;
+
+ private int mDx;
+
+ private int mDy;
+
+ private int mDuration;
+
+ private Interpolator mInterpolator;
+
+ private boolean changed = false;
+
+ // we track this variable to inform custom implementer if they are updating the action
+ // in every animation callback
+ private int consecutiveUpdates = 0;
+
+ /**
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ */
+ public Action(int dx, int dy) {
+ this(dx, dy, UNDEFINED_DURATION, null);
+ }
+
+ /**
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ * @param duration Duration of the animation in milliseconds
+ */
+ public Action(int dx, int dy, int duration) {
+ this(dx, dy, duration, null);
+ }
+
+ /**
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ * @param duration Duration of the animation in milliseconds
+ * @param interpolator Interpolator to be used when calculating scroll position in each
+ * animation step
+ */
+ public Action(int dx, int dy, int duration, Interpolator interpolator) {
+ mDx = dx;
+ mDy = dy;
+ mDuration = duration;
+ mInterpolator = interpolator;
+ }
+ private void runInNecessary(RecyclerView recyclerView) {
+ if (changed) {
+ validate();
+ if (mInterpolator == null) {
+ if (mDuration == UNDEFINED_DURATION) {
+ recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy);
+ } else {
+ recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration);
+ }
+ } else {
+ recyclerView.mViewFlinger.smoothScrollBy(mDx, mDy, mDuration, mInterpolator);
+ }
+ consecutiveUpdates ++;
+ if (consecutiveUpdates > 10) {
+ // A new action is being set in every animation step. This looks like a bad
+ // implementation. Inform developer.
+ Log.e(TAG, "Smooth Scroll action is being updated too frequently. Make sure"
+ + " you are not changing it unless necessary");
+ }
+ changed = false;
+ } else {
+ consecutiveUpdates = 0;
+ }
+ }
+
+ private void validate() {
+ if (mInterpolator != null && mDuration < 1) {
+ throw new IllegalStateException("If you provide an interpolator, you must"
+ + " set a positive duration");
+ } else if (mDuration < 1) {
+ throw new IllegalStateException("Scroll duration must be a positive number");
+ }
+ }
+
+ public int getDx() {
+ return mDx;
+ }
+
+ public void setDx(int dx) {
+ changed = true;
+ mDx = dx;
+ }
+
+ public int getDy() {
+ return mDy;
+ }
+
+ public void setDy(int dy) {
+ changed = true;
+ mDy = dy;
+ }
+
+ public int getDuration() {
+ return mDuration;
+ }
+
+ public void setDuration(int duration) {
+ changed = true;
+ mDuration = duration;
+ }
+
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ /**
+ * Sets the interpolator to calculate scroll steps
+ * @param interpolator The interpolator to use. If you specify an interpolator, you must
+ * also set the duration.
+ * @see #setDuration(int)
+ */
+ public void setInterpolator(Interpolator interpolator) {
+ changed = true;
+ mInterpolator = interpolator;
+ }
+
+ /**
+ * Updates the action with given parameters.
+ * @param dx Pixels to scroll horizontally
+ * @param dy Pixels to scroll vertically
+ * @param duration Duration of the animation in milliseconds
+ * @param interpolator Interpolator to be used when calculating scroll position in each
+ * animation step
+ */
+ public void update(int dx, int dy, int duration, Interpolator interpolator) {
+ mDx = dx;
+ mDy = dy;
+ mDuration = duration;
+ mInterpolator = interpolator;
+ changed = true;
+ }
+ }
+ }
+
+ static class AdapterDataObservable extends Observable<AdapterDataObserver> {
+ public boolean hasObservers() {
+ return !mObservers.isEmpty();
+ }
+
+ public void notifyChanged() {
+ // since onChanged() is implemented by the app, it could do anything, including
+ // removing itself from {@link mObservers} - and that could cause problems if
+ // an iterator is used on the ArrayList {@link mObservers}.
+ // to avoid such problems, just march thru the list in the reverse order.
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onChanged();
+ }
+ }
+
+ public void notifyItemRangeChanged(int positionStart, int itemCount) {
+ // since onItemRangeChanged() is implemented by the app, it could do anything, including
+ // removing itself from {@link mObservers} - and that could cause problems if
+ // an iterator is used on the ArrayList {@link mObservers}.
+ // to avoid such problems, just march thru the list in the reverse order.
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeChanged(positionStart, itemCount);
+ }
+ }
+
+ public void notifyItemRangeInserted(int positionStart, int itemCount) {
+ // since onItemRangeInserted() is implemented by the app, it could do anything,
+ // including removing itself from {@link mObservers} - and that could cause problems if
+ // an iterator is used on the ArrayList {@link mObservers}.
+ // to avoid such problems, just march thru the list in the reverse order.
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeInserted(positionStart, itemCount);
+ }
+ }
+
+ public void notifyItemRangeRemoved(int positionStart, int itemCount) {
+ // since onItemRangeRemoved() is implemented by the app, it could do anything, including
+ // removing itself from {@link mObservers} - and that could cause problems if
+ // an iterator is used on the ArrayList {@link mObservers}.
+ // to avoid such problems, just march thru the list in the reverse order.
+ for (int i = mObservers.size() - 1; i >= 0; i--) {
+ mObservers.get(i).onItemRangeRemoved(positionStart, itemCount);
+ }
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+
+ Parcelable mLayoutState;
+
+ /**
+ * called by CREATOR
+ */
+ SavedState(Parcel in) {
+ super(in);
+ mLayoutState = in.readParcelable(LayoutManager.class.getClassLoader());
+ }
+
+ /**
+ * Called by onSaveInstanceState
+ */
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeParcelable(mLayoutState, 0);
+ }
+
+ private void copyFrom(SavedState other) {
+ mLayoutState = other.mLayoutState;
+ }
+
+ 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];
+ }
+ };
+ }
+ /**
+ * <p>Contains useful information about the current RecyclerView state like target scroll
+ * position or view focus. State object can also keep arbitrary data, identified by resource
+ * ids.</p>
+ * <p>Often times, RecyclerView components will need to pass information between each other.
+ * To provide a well defined data bus between components, RecyclerView passes the same State
+ * object to component callbacks and these components can use it to exchange data.</p>
+ * <p>If you implement custom components, you can use State's put/get/remove methods to pass
+ * data between your components without needing to manage their lifecycles.</p>
+ */
+ public class State {
+
+ private int mTargetPosition = RecyclerView.NO_POSITION;
+
+ private SparseArray<Object> mData;
+
+ State reset() {
+ mTargetPosition = RecyclerView.NO_POSITION;
+ if (mData != null) {
+ mData.clear();
+ }
+ return this;
+ }
+
+ /**
+ * 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.
+ */
+ public void remove(int resourceId) {
+ if (mData == null) {
+ return;
+ }
+ mData.remove(resourceId);
+ }
+
+ /**
+ * Gets the Object mapped from the specified id, or <code>null</code>
+ * if no such data exists.
+ *
+ * @param resourceId Id of the resource you want to remove. It is suggested to use R.id.*
+ * to
+ * preserve cross functionality and avoid conflicts.
+ */
+ public <T> T get(int resourceId) {
+ if (mData == null) {
+ return null;
+ }
+ return (T) mData.get(resourceId);
+ }
+
+ /**
+ * Adds a mapping from the specified id to the specified value, replacing the previous
+ * mapping from the specified key if there was one.
+ *
+ * @param resourceId Id of the resource you want to add. It is suggested to use R.id.* to
+ * preserve cross functionality and avoid conflicts.
+ * @param data The data you want to associate with the resourceId.
+ */
+ public void put(int resourceId, Object data) {
+ if (mData == null) {
+ mData = new SparseArray<Object>();
+ }
+ mData.put(resourceId, data);
+ }
+
+ /**
+ * If scroll is triggered to make a certain item visible, this value will return the
+ * adapter index of that item.
+ * @return Adapter index of the target item or
+ * {@link RecyclerView#NO_POSITION} if there is no target
+ * position.
+ */
+ public int getTargetScrollPosition() {
+ return mTargetPosition;
+ }
+
+ /**
+ * Returns if current scroll has a target position.
+ * @return true if scroll is being triggered to make a certain position visible
+ * @see #getTargetScrollPosition()
+ */
+ public boolean hasTargetScrollPosition() {
+ return mTargetPosition != RecyclerView.NO_POSITION;
+ }
+
+ State withTarget(int targetPosition) {
+ mTargetPosition = targetPosition;
+ return this;
+ }
+ }
+}
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewTest.java
new file mode 100644
index 0000000..eee2003
--- /dev/null
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/RecyclerViewTest.java
@@ -0,0 +1,309 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+import android.test.AndroidTestCase;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import java.util.UUID;
+
+public class RecyclerViewTest extends AndroidTestCase {
+
+ RecyclerView mRecyclerView;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mRecyclerView = new RecyclerView(mContext);
+ }
+
+ public void testMeasureWithoutLayoutManager() {
+ Throwable measureThrowable = null;
+ try {
+ measure();
+ } catch (Throwable throwable) {
+ measureThrowable = throwable;
+ }
+ assertTrue("Calling measure without a layout manager should throw exception"
+ , measureThrowable instanceof NullPointerException);
+ }
+
+ private void measure() {
+ mRecyclerView.measure(View.MeasureSpec.AT_MOST | 320, View.MeasureSpec.AT_MOST | 240);
+ }
+
+ private void layout() {
+ mRecyclerView.layout(0, 0, 320, 320);
+ }
+
+ private void safeLayout() {
+ try {
+ layout();
+ } catch (Throwable t) {
+
+ }
+ }
+
+ public void testLayoutWithoutLayoutManager() throws InterruptedException {
+ MockLayoutManager layoutManager = new MockLayoutManager();
+ mRecyclerView.setLayoutManager(layoutManager);
+ safeLayout();
+ assertEquals("layout manager should not be called if there is no adapter attached",
+ 0, layoutManager.mLayoutCount);
+ }
+
+ public void testLayout() throws InterruptedException {
+ MockLayoutManager layoutManager = new MockLayoutManager();
+ mRecyclerView.setLayoutManager(layoutManager);
+ mRecyclerView.setAdapter(new MockAdapter(3));
+ layout();
+ assertEquals("when both layout manager and activity is set, recycler view should call"
+ + " layout manager's layout method", 1, layoutManager.mLayoutCount);
+ }
+
+ public void testObservingAdapters() {
+ MockAdapter adapterOld = new MockAdapter(1);
+ mRecyclerView.setAdapter(adapterOld);
+ assertTrue("attached adapter should have observables", adapterOld.hasObservers());
+
+ MockAdapter adapterNew = new MockAdapter(2);
+ mRecyclerView.setAdapter(adapterNew);
+ assertFalse("detached adapter should lose observable", adapterOld.hasObservers());
+ assertTrue("new adapter should have observers", adapterNew.hasObservers());
+
+ mRecyclerView.setAdapter(null);
+ assertNull("adapter should be removed successfully", mRecyclerView.getAdapter());
+ assertFalse("when adapter is removed, observables should be removed too",
+ adapterNew.hasObservers());
+ }
+
+ public void testAdapterChangeCallbacks() {
+ MockLayoutManager layoutManager = new MockLayoutManager();
+ mRecyclerView.setLayoutManager(layoutManager);
+ MockAdapter adapterOld = new MockAdapter(1);
+ mRecyclerView.setAdapter(adapterOld);
+ layoutManager.assertPrevNextAdapters(null, adapterOld);
+
+ MockAdapter adapterNew = new MockAdapter(2);
+ mRecyclerView.setAdapter(adapterNew);
+ layoutManager.assertPrevNextAdapters("switching adapters should trigger correct callbacks"
+ , adapterOld, adapterNew);
+
+ mRecyclerView.setAdapter(null);
+ layoutManager.assertPrevNextAdapters(
+ "Setting adapter null should trigger correct callbacks",
+ adapterNew, null);
+ }
+
+ public void testSavedStateWithStatelessLayoutManager() throws InterruptedException {
+ mRecyclerView.setLayoutManager(new MockLayoutManager() {
+ @Override
+ public Parcelable onSaveInstanceState() {
+ return null;
+ }
+ });
+ mRecyclerView.setAdapter(new MockAdapter(3));
+ Parcel parcel = Parcel.obtain();
+ String parcelSuffix = UUID.randomUUID().toString();
+ Parcelable savedState = mRecyclerView.onSaveInstanceState();
+ savedState.writeToParcel(parcel, 0);
+ parcel.writeString(parcelSuffix);
+
+ // reset position for reading
+ parcel.setDataPosition(0);
+ RecyclerView restored = new RecyclerView(mContext);
+ restored.setLayoutManager(new MockLayoutManager());
+ mRecyclerView.setAdapter(new MockAdapter(3));
+ // restore
+ savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
+ restored.onRestoreInstanceState(savedState);
+
+ assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
+ parcel.readString());
+ assertEquals("When unmarshalling, all of the parcel should be read", 0, parcel.dataAvail());
+
+ }
+
+ public void testSavedState() throws InterruptedException {
+ MockLayoutManager mlm = new MockLayoutManager();
+ mRecyclerView.setLayoutManager(mlm);
+ mRecyclerView.setAdapter(new MockAdapter(3));
+ layout();
+ 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);
+
+ // reset for reading
+ parcel.setDataPosition(0);
+ // re-create
+ savedState = RecyclerView.SavedState.CREATOR.createFromParcel(parcel);
+
+ RecyclerView restored = new RecyclerView(mContext);
+ MockLayoutManager mlmRestored = new MockLayoutManager();
+ restored.setLayoutManager(mlmRestored);
+ restored.setAdapter(new MockAdapter(3));
+ restored.onRestoreInstanceState(savedState);
+
+ assertEquals("Parcel reading should not go out of bounds", parcelSuffix,
+ parcel.readString());
+ assertEquals("When unmarshalling, all of the parcel should be read", 0, parcel.dataAvail());
+ assertEquals("uuid in layout manager should be preserved properly", mlm.mUuid,
+ mlmRestored.mUuid);
+ assertNotSame("stateless parameter should not be preserved", mlm.mLayoutCount,
+ mlmRestored.mLayoutCount);
+ layout();
+
+
+ }
+
+ static class MockLayoutManager extends RecyclerView.LayoutManager {
+
+ int mLayoutCount = 0;
+
+ int mAdapterChangedCount = 0;
+
+ RecyclerView.Adapter mPrevAdapter;
+
+ RecyclerView.Adapter mNextAdapter;
+
+ String mUuid = UUID.randomUUID().toString();
+
+ @Override
+ public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
+ RecyclerView.Adapter newAdapter) {
+ super.onAdapterChanged(oldAdapter, newAdapter);
+ mPrevAdapter = oldAdapter;
+ mNextAdapter = newAdapter;
+ mAdapterChangedCount++;
+ }
+
+ @Override
+ public void onLayoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
+ boolean structureChanged, RecyclerView.State state) {
+ mLayoutCount += 1;
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ LayoutManagerSavedState lss = new LayoutManagerSavedState();
+ lss.mUuid = mUuid;
+ return lss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ super.onRestoreInstanceState(state);
+ if (state instanceof LayoutManagerSavedState) {
+ mUuid = ((LayoutManagerSavedState) state).mUuid;
+ }
+ }
+
+ @Override
+ public RecyclerView.LayoutParams generateDefaultLayoutParams() {
+ return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ public void assertPrevNextAdapters(String message, RecyclerView.Adapter prevAdapter,
+ RecyclerView.Adapter nextAdapter) {
+ assertSame(message, prevAdapter, mPrevAdapter);
+ assertSame(message, nextAdapter, mNextAdapter);
+ }
+
+ public void assertPrevNextAdapters(RecyclerView.Adapter prevAdapter,
+ RecyclerView.Adapter nextAdapter) {
+ assertPrevNextAdapters("Adapters from onAdapterChanged callback should match",
+ prevAdapter, nextAdapter);
+ }
+ }
+
+ static class LayoutManagerSavedState implements Parcelable {
+
+ String mUuid;
+
+ public LayoutManagerSavedState(Parcel in) {
+ mUuid = in.readString();
+ }
+
+ public LayoutManagerSavedState() {
+
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mUuid);
+ }
+
+ public static final Parcelable.Creator<LayoutManagerSavedState> CREATOR
+ = new Parcelable.Creator<LayoutManagerSavedState>() {
+ @Override
+ public LayoutManagerSavedState createFromParcel(Parcel in) {
+ return new LayoutManagerSavedState(in);
+ }
+
+ @Override
+ public LayoutManagerSavedState[] newArray(int size) {
+ return new LayoutManagerSavedState[size];
+ }
+ };
+ }
+
+ static class MockAdapter extends RecyclerView.Adapter {
+
+ private int mCount = 0;
+
+ MockAdapter(int count) {
+ this.mCount = count;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new MockViewHolder(new TextView(parent.getContext()));
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+
+ }
+
+ @Override
+ public int getItemCount() {
+ return mCount;
+ }
+ }
+
+ static class MockViewHolder extends RecyclerView.ViewHolder {
+
+ public MockViewHolder(View itemView) {
+ super(itemView);
+ }
+ }
+
+}
+