Merge "Add PreferenceDialogFragments for Chassis." into pi-car-dev
diff --git a/car-apps-common/res/values/colors.xml b/car-apps-common/res/values/colors.xml
index 3104f1d..37a7bda 100644
--- a/car-apps-common/res/values/colors.xml
+++ b/car-apps-common/res/values/colors.xml
@@ -34,6 +34,7 @@
<item>#757575</item>
</array>
+ <color name="loading_image_placeholder_color">@*android:color/car_grey_800</color>
<color name="improper_image_refs_tint_color">#C8FF0000</color>
<color name="control_bar_background_color">@android:color/transparent</color>
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java b/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java
index 63b276e..0e59a54 100644
--- a/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/ImageBinder.java
@@ -22,10 +22,12 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Size;
+import com.android.car.apps.common.R;
import com.android.car.apps.common.UriUtils;
import java.util.Objects;
@@ -75,6 +77,7 @@
private T mCurrentRef;
private ImageKey mCurrentKey;
private BiConsumer<ImageKey, Drawable> mFetchReceiver;
+ private Drawable mLoadingDrawable;
public ImageBinder(@NonNull PlaceholderType type, @NonNull Size maxImageSize,
@@ -155,10 +158,19 @@
getImageFetcher(context).cancelRequest(mCurrentKey, mFetchReceiver);
onRequestFinished();
}
+ setDrawable(getLoadingDrawable(context));
}
private void onRequestFinished() {
mCurrentKey = null;
mFetchReceiver = null;
}
+
+ private Drawable getLoadingDrawable(Context context) {
+ if (mLoadingDrawable == null) {
+ int color = context.getColor(R.color.loading_image_placeholder_color);
+ mLoadingDrawable = new ColorDrawable(color);
+ }
+ return mLoadingDrawable;
+ }
}
diff --git a/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java b/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
index db6ee1a..f346de0 100644
--- a/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
+++ b/car-apps-common/src/com/android/car/apps/common/imaging/ImageViewBinder.java
@@ -82,10 +82,11 @@
@Override
protected void prepareForNewBinding(Context context) {
- super.prepareForNewBinding(context);
mImageView.setImageBitmap(null);
mImageView.setImageDrawable(null);
mImageView.clearColorFilter();
+ // Call super last to setup the default loading drawable.
+ super.prepareForNewBinding(context);
}
}
diff --git a/car-chassis-lib/.gitignore b/car-chassis-lib/.gitignore
new file mode 100644
index 0000000..fa8bef3
--- /dev/null
+++ b/car-chassis-lib/.gitignore
@@ -0,0 +1,11 @@
+# Local configuration
+local.properties
+gradle-wrapper.properties
+
+# Gradle
+gradle/
+.gradle/
+build/
+
+# IntelliJ
+*.iml
\ No newline at end of file
diff --git a/car-chassis-lib/AndroidManifest-gradle.xml b/car-chassis-lib/AndroidManifest-gradle.xml
new file mode 100644
index 0000000..6844663
--- /dev/null
+++ b/car-chassis-lib/AndroidManifest-gradle.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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="com.android.car.chassis">
+</manifest>
diff --git a/car-chassis-lib/README.md b/car-chassis-lib/README.md
index b3e3c1c..3d1015a 100644
--- a/car-chassis-lib/README.md
+++ b/car-chassis-lib/README.md
@@ -19,10 +19,10 @@
Here is the process for updating this library:
-1. Develop, test and create CL in Gerrit with the desired changes
-2. On Google3, run update.sh and test your changes
-3. Iterate until your changes look okay on both places.
-4. Back on Gerrit, submit your CL
-5. Back on Google3, run update.sh again and submit
+1. Develop, test and upload changes to Gerrit
+2. On Google3, run './update.sh review <cl>' (with <cl> being your Gerrit CL #) and test your changes
+3. Repeat #1 and #2 until your changes look okay on both places.
+4. Back on Gerrit, submit your CL.
+5. Back on Google3, run './update.sh manual' submit
TODO: Automate this process using CaaS (in progress)
diff --git a/car-chassis-lib/build.gradle b/car-chassis-lib/build.gradle
new file mode 100644
index 0000000..21b2b0d
--- /dev/null
+++ b/car-chassis-lib/build.gradle
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ google()
+ jcenter()
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+// Library-level build file
+
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 28
+
+ defaultConfig {
+ minSdkVersion 28
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest-gradle.xml'
+ java.srcDirs = ['src']
+ res.srcDirs = ['res']
+ }
+ }
+}
+
+dependencies {
+ implementation 'androidx.annotation:annotation:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.recyclerview:recyclerview:1.0.0'
+}
diff --git a/car-chassis-lib/gradle.properties b/car-chassis-lib/gradle.properties
new file mode 100644
index 0000000..9dad1c4
--- /dev/null
+++ b/car-chassis-lib/gradle.properties
@@ -0,0 +1,34 @@
+#
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
\ No newline at end of file
diff --git a/car-chassis-lib/gradlew b/car-chassis-lib/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/car-chassis-lib/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=$(save "$@")
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/car-chassis-lib/gradlew.bat b/car-chassis-lib/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/car-chassis-lib/gradlew.bat
@@ -0,0 +1,84 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/car-chassis-lib/res/drawable/ic_arrow_back.xml b/car-chassis-lib/res/drawable/chassis_icon_arrow_back.xml
similarity index 100%
rename from car-chassis-lib/res/drawable/ic_arrow_back.xml
rename to car-chassis-lib/res/drawable/chassis_icon_arrow_back.xml
diff --git a/car-chassis-lib/res/drawable/ic_close.xml b/car-chassis-lib/res/drawable/chassis_icon_close.xml
similarity index 91%
rename from car-chassis-lib/res/drawable/ic_close.xml
rename to car-chassis-lib/res/drawable/chassis_icon_close.xml
index 48ab552..482df0f 100644
--- a/car-chassis-lib/res/drawable/ic_close.xml
+++ b/car-chassis-lib/res/drawable/chassis_icon_close.xml
@@ -17,8 +17,8 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/primary_icon_size"
- android:height="@dimen/primary_icon_size"
+ android:width="24dp"
+ android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
diff --git a/car-chassis-lib/res/drawable/ic_search.xml b/car-chassis-lib/res/drawable/chassis_icon_search.xml
similarity index 92%
rename from car-chassis-lib/res/drawable/ic_search.xml
rename to car-chassis-lib/res/drawable/chassis_icon_search.xml
index 87e7d46..f70e61e 100644
--- a/car-chassis-lib/res/drawable/ic_search.xml
+++ b/car-chassis-lib/res/drawable/chassis_icon_search.xml
@@ -14,8 +14,8 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/primary_icon_size"
- android:height="@dimen/primary_icon_size"
+ android:width="48dp"
+ android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
diff --git a/car-chassis-lib/res/drawable/ic_settings.xml b/car-chassis-lib/res/drawable/chassis_icon_settings.xml
similarity index 94%
rename from car-chassis-lib/res/drawable/ic_settings.xml
rename to car-chassis-lib/res/drawable/chassis_icon_settings.xml
index 5c9e6a7..ebf8576 100644
--- a/car-chassis-lib/res/drawable/ic_settings.xml
+++ b/car-chassis-lib/res/drawable/chassis_icon_settings.xml
@@ -14,8 +14,8 @@
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
- android:width="@dimen/primary_icon_size"
- android:height="@dimen/primary_icon_size"
+ android:width="24dp"
+ android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
diff --git a/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_button_ripple_background.xml b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_button_ripple_background.xml
new file mode 100644
index 0000000..b5f107c
--- /dev/null
+++ b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_button_ripple_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<ripple
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/chassis_card_ripple_background" />
diff --git a/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_divider.xml b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_divider.xml
new file mode 100644
index 0000000..bddaae3
--- /dev/null
+++ b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_divider.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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">
+ <size android:height="0dp" />
+ <solid android:color="@android:color/transparent" />
+</shape>
diff --git a/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_down.xml b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_down.xml
new file mode 100644
index 0000000..380bf46
--- /dev/null
+++ b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_down.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:pathData="M14.83,16.42L24,25.59l9.17,-9.17L36,19.25l-12,12 -12,-12z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_up.xml b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_up.xml
new file mode 100644
index 0000000..2eff62f
--- /dev/null
+++ b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_ic_up.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="48dp"
+ android:height="48dp"
+ android:viewportWidth="48.0"
+ android:viewportHeight="48.0">
+ <path
+ android:pathData="M14.83,30.83L24,21.66l9.17,9.17L36,28 24,16 12,28z"
+ android:fillColor="#FFFFFF"/>
+</vector>
diff --git a/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_scrollbar_thumb.xml b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_scrollbar_thumb.xml
new file mode 100644
index 0000000..9180f1a
--- /dev/null
+++ b/car-chassis-lib/res/drawable/chassis_pagedrecyclerview_scrollbar_thumb.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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">
+ <solid android:color="@color/chassis_scrollbar_thumb" />
+ <corners android:radius="@dimen/chassis_scrollbar_thumb_radius"/>
+</shape>
diff --git a/car-chassis-lib/res/drawable/chassis_toolbar_button_background.xml b/car-chassis-lib/res/drawable/chassis_toolbar_button_background.xml
index 7d29d64..efa63b9 100644
--- a/car-chassis-lib/res/drawable/chassis_toolbar_button_background.xml
+++ b/car-chassis-lib/res/drawable/chassis_toolbar_button_background.xml
@@ -17,5 +17,5 @@
~
-->
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
- android:color="@*android:color/car_card_ripple_background"
+ android:color="@color/chassis_card_ripple_background"
android:radius="@dimen/chassis_toolbar_button_background_radius"/>
diff --git a/car-chassis-lib/res/drawable/divider.xml b/car-chassis-lib/res/drawable/divider.xml
new file mode 100644
index 0000000..164b71a
--- /dev/null
+++ b/car-chassis-lib/res/drawable/divider.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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">
+ <size android:height="2dp"
+ android:width="2dp"/>
+ <solid android:color="@android:color/transparent" />
+</shape>
diff --git a/car-chassis-lib/res/layout/chassis_paged_recycler_view_item.xml b/car-chassis-lib/res/layout/chassis_paged_recycler_view_item.xml
new file mode 100644
index 0000000..6a35b43
--- /dev/null
+++ b/car-chassis-lib/res/layout/chassis_paged_recycler_view_item.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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/nested_recycler_view_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center">
+</FrameLayout>
diff --git a/car-chassis-lib/res/layout/chassis_pagedrecyclerview_scrollbar.xml b/car-chassis-lib/res/layout/chassis_pagedrecyclerview_scrollbar.xml
new file mode 100644
index 0000000..7678940
--- /dev/null
+++ b/car-chassis-lib/res/layout/chassis_pagedrecyclerview_scrollbar.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT 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="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center">
+
+ <ImageButton
+ android:id="@+id/page_up"
+ android:layout_width="@dimen/chassis_scrollbar_button_size"
+ android:layout_height="@dimen/chassis_scrollbar_button_size"
+ android:background="@drawable/chassis_pagedrecyclerview_button_ripple_background"
+ android:contentDescription="@string/chassis_scrollbar_page_up_button"
+ android:focusable="false"
+ android:hapticFeedbackEnabled="false"
+ android:src="@drawable/chassis_pagedrecyclerview_ic_up"
+ android:scaleType="centerInside" />
+
+ <!-- View height is dynamically calculated during layout. -->
+ <View
+ android:id="@+id/scrollbar_thumb"
+ android:layout_width="@dimen/chassis_scrollbar_thumb_width"
+ android:layout_height="0dp"
+ android:layout_gravity="center_horizontal"
+ android:background="@drawable/chassis_pagedrecyclerview_scrollbar_thumb" />
+
+ <ImageButton
+ android:id="@+id/page_down"
+ android:layout_width="@dimen/chassis_scrollbar_button_size"
+ android:layout_height="@dimen/chassis_scrollbar_button_size"
+ android:background="@drawable/chassis_pagedrecyclerview_button_ripple_background"
+ android:contentDescription="@string/chassis_scrollbar_page_down_button"
+ android:focusable="false"
+ android:hapticFeedbackEnabled="false"
+ android:src="@drawable/chassis_pagedrecyclerview_ic_down"
+ android:scaleType="centerInside" />
+</LinearLayout>
diff --git a/car-chassis-lib/res/layout/chassis_search_view.xml b/car-chassis-lib/res/layout/chassis_search_view.xml
index e9942a7..7decb3a 100644
--- a/car-chassis-lib/res/layout/chassis_search_view.xml
+++ b/car-chassis-lib/res/layout/chassis_search_view.xml
@@ -21,9 +21,9 @@
<ImageView
android:id="@+id/icon"
- android:layout_width="@dimen/touch_target_size"
- android:layout_height="@dimen/touch_target_size"
- android:src="@drawable/ic_search"
+ android:layout_width="@dimen/chassis_touch_target_width"
+ android:layout_height="@dimen/chassis_touch_target_height"
+ android:src="@drawable/chassis_icon_search"
android:scaleType="center"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
@@ -33,9 +33,9 @@
android:id="@+id/search_bar"
android:layout_height="match_parent"
android:layout_width="match_parent"
- android:paddingLeft="@dimen/touch_target_size"
- android:hint="@string/chassis_default_search_hint"
- android:textColorHint="@color/search_hint_text_color"
+ android:paddingLeft="@dimen/chassis_touch_target_width"
+ android:hint="@string/chassis_toolbar_default_search_hint"
+ android:textColorHint="@color/chassis_toolbar_search_hint_text_color"
android:inputType="text"
android:singleLine="true"
android:imeOptions="actionDone"
@@ -46,10 +46,10 @@
<ImageView
android:id="@+id/search_close"
- android:layout_width="@dimen/touch_target_size"
- android:layout_height="@dimen/touch_target_size"
+ android:layout_width="@dimen/chassis_touch_target_width"
+ android:layout_height="@dimen/chassis_touch_target_height"
android:background="@drawable/chassis_toolbar_button_background"
- android:src="@drawable/ic_close"
+ android:src="@drawable/chassis_icon_close"
android:scaleType="center"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
diff --git a/car-chassis-lib/res/layout/chassis_toolbar_search_button.xml b/car-chassis-lib/res/layout/chassis_toolbar_search_button.xml
index 91afe95..e42e4ce 100644
--- a/car-chassis-lib/res/layout/chassis_toolbar_search_button.xml
+++ b/car-chassis-lib/res/layout/chassis_toolbar_search_button.xml
@@ -17,8 +17,8 @@
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/search"
- android:layout_width="@dimen/touch_target_size"
- android:layout_height="@dimen/touch_target_size"
- android:src="@drawable/ic_search"
+ android:layout_width="@dimen/chassis_touch_target_width"
+ android:layout_height="@dimen/chassis_touch_target_height"
+ android:src="@drawable/chassis_icon_search"
android:scaleType="center"
android:background="@drawable/chassis_toolbar_button_background"/>
diff --git a/car-chassis-lib/res/layout/chassis_toolbar_settings_button.xml b/car-chassis-lib/res/layout/chassis_toolbar_settings_button.xml
index c295d5c..f044099 100644
--- a/car-chassis-lib/res/layout/chassis_toolbar_settings_button.xml
+++ b/car-chassis-lib/res/layout/chassis_toolbar_settings_button.xml
@@ -17,8 +17,8 @@
<ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/settings"
- android:layout_width="@dimen/touch_target_size"
- android:layout_height="@dimen/touch_target_size"
- android:src="@drawable/ic_settings"
+ android:layout_width="@dimen/chassis_touch_target_width"
+ android:layout_height="@dimen/chassis_touch_target_height"
+ android:src="@drawable/chassis_icon_settings"
android:scaleType="center"
android:background="@drawable/chassis_toolbar_button_background"/>
diff --git a/car-chassis-lib/res/values-night/colors.xml b/car-chassis-lib/res/values-night/colors.xml
index 0ee4613..6e0aebe 100644
--- a/car-chassis-lib/res/values-night/colors.xml
+++ b/car-chassis-lib/res/values-night/colors.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,6 +14,12 @@
limitations under the License.
-->
<resources>
- <color name="chassis_tab_selected_color">@color/chassis_tab_selected_color_dark</color>
- <color name="chassis_tab_unselected_color">@color/chassis_tab_unselected_color_dark</color>
+ <!-- General -->
+
+ <!-- Main text color (titles, text body, etc.) -->
+ <color name="chassis_primary_text_color">#E2FFFFFF</color>
+ <!-- Text color used in subtitles or other secondary text blocks -->
+ <color name="chassis_secondary_text_color">#80FFFFFF</color>
+ <!-- The ripple color for a card. -->
+ <color name="chassis_card_ripple_background">#8F000000</color>
</resources>
diff --git a/car-chassis-lib/res/values-port/dimens.xml b/car-chassis-lib/res/values-port/dimens.xml
index d91a195..543e63b 100644
--- a/car-chassis-lib/res/values-port/dimens.xml
+++ b/car-chassis-lib/res/values-port/dimens.xml
@@ -15,5 +15,5 @@
limitations under the License.
-->
<resources>
- <dimen name="chassis_toolbar_second_row_height">@*android:dimen/car_app_bar_height</dimen>
+ <dimen name="chassis_toolbar_second_row_height">@dimen/chassis_app_bar_height</dimen>
</resources>
diff --git a/car-chassis-lib/res/values-w1280dp/dimens.xml b/car-chassis-lib/res/values-w1280dp/dimens.xml
new file mode 100644
index 0000000..2794db3
--- /dev/null
+++ b/car-chassis-lib/res/values-w1280dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <!-- Margin -->
+ <dimen name="chassis_margin">148dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car-chassis-lib/res/values-w1920dp/dimens.xml b/car-chassis-lib/res/values-w1920dp/dimens.xml
new file mode 100644
index 0000000..dc68fdf
--- /dev/null
+++ b/car-chassis-lib/res/values-w1920dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <!-- Margin -->
+ <dimen name="chassis_margin">192dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car-chassis-lib/res/values-w690dp/dimens.xml b/car-chassis-lib/res/values-w690dp/dimens.xml
new file mode 100644
index 0000000..dbfb227
--- /dev/null
+++ b/car-chassis-lib/res/values-w690dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <!-- Margin -->
+ <dimen name="chassis_margin">112dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/car-chassis-lib/res/values/attrs.xml b/car-chassis-lib/res/values/attrs.xml
index 5a49a20..4caf70b 100644
--- a/car-chassis-lib/res/values/attrs.xml
+++ b/car-chassis-lib/res/values/attrs.xml
@@ -15,18 +15,51 @@
-->
<resources>
<declare-styleable name="ChassisToolbar">
- <!-- The title of the toolbar, only displayed in certain conditions -->
+ <!-- Title of the toolbar, only displayed in certain conditions -->
<attr name="title" format="string"/>
- <!-- The logo drawable for the toolbar. Appears when there's no back/close button shown -->
+ <!-- Logo drawable for the toolbar. Appears when there's no back/close button shown -->
<attr name="logo" format="reference"/>
- <!-- The hint for the search bar in the toolbar -->
+ <!-- Hint for the search bar in the toolbar -->
<attr name="searchHint" format="string"/>
- <!-- The buttons to display, as an array of layout ids. Use @layout/chassis_toolbar_search_button and @layout/chassis_toolbar_settings_button for the search and settings buttons -->
+ <!-- Buttons to display, as an array of layout ids. Use @layout/chassis_toolbar_search_button and @layout/chassis_toolbar_settings_button for the search and settings buttons -->
<attr name="buttons" format="reference"/>
<!-- Whether or not to show the custom buttons while searching. If using chassis_toolbar_search_button, or any other layout with a view with the id of "search", it will always be hidden. -->
<attr name="showButtonsWhileSearching" format="boolean"/>
+ <!-- Initial state of the toolbar. See the Toolbar.State enum for more information -->
+ <attr name="state" format="enum">
+ <enum name="home" value="0"/>
+ <enum name="subpage" value="1"/>
+ <enum name="subpage_custom" value="2"/>
+ <enum name="search" value="3"/>
+ </attr>
+ <!-- Whether or not the toolbar should have a background. Default true. -->
+ <attr name="showBackground" format="boolean"/>
</declare-styleable>
<!-- Theme attribute to specifying a default style for all chassisToolbars -->
<attr name="chassisToolbarStyle" format="reference"/>
+
+ <declare-styleable name="PagedRecyclerView">
+ <!-- Whether to enable the chassis_pagedrecyclerview_divider for linear layout or not. -->
+ <attr name="enableDivider" format="boolean" />
+ <!-- Top offset for paged recycler view. -->
+ <attr name="startOffset" format="integer" />
+ <!-- Bottom offset for paged recycler view for linear layout. -->
+ <attr name="endOffset" format="integer" />
+
+ <!-- Number of columns in a grid layout. -->
+ <attr name="numOfColumns" format="integer" />
+
+ <!-- Paged recycler view layout. -->
+ <attr name="layoutStyle" format="enum">
+ <!-- linear layout -->
+ <enum name="linear" value="0" />
+ <!-- grid layout -->
+ <enum name="grid" value="1" />
+ </attr>
+ </declare-styleable>
+
+ <declare-styleable name="PagedRecyclerViewTheme">
+ <attr name="pagedRecyclerViewStyle" format="reference" />
+ </declare-styleable>
</resources>
diff --git a/car-chassis-lib/res/values/bools.xml b/car-chassis-lib/res/values/bools.xml
new file mode 100644
index 0000000..ff1f439
--- /dev/null
+++ b/car-chassis-lib/res/values/bools.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!-- Whether to display the Scroll Bar or not. Defaults to true. If this is set to false,
+ the PagedRecyclerView will behave exactly like the RecyclerView. -->
+ <bool name="chassis_scrollbar_enable">true</bool>
+
+ <!-- Whether to place the scrollbar z-index above the recycler view. Defaults to
+ true. -->
+ <bool name="chassis_scrollbar_above_recycler_view">true</bool>
+</resources>
diff --git a/car-chassis-lib/res/values/colors.xml b/car-chassis-lib/res/values/colors.xml
index 7c06fa2..caf7266 100644
--- a/car-chassis-lib/res/values/colors.xml
+++ b/car-chassis-lib/res/values/colors.xml
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
-<!-- Copyright (C) 2015 The Android Open Source Project
+<!-- Copyright (C) 2019 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,16 +14,30 @@
limitations under the License.
-->
<resources>
- <color name="primary_text_color">#FFFFFFFF</color>
- <color name="secondary_text_color">#90FFFFFF</color>
- <color name="search_hint_text_color">#33FFFFFF</color>
+ <!-- General -->
- <color name="toolbar_background_color">#E0000000</color>
+ <!-- Main text color (titles, text body, etc.) -->
+ <color name="chassis_primary_text_color">#FFFFFFFF</color>
+ <!-- Text color used in subtitles or other secondary text blocks -->
+ <color name="chassis_secondary_text_color">#90FFFFFF</color>
+ <!-- The ripple color for a card. -->
+ <color name="chassis_card_ripple_background">#27ffffff</color>
- <color name="chassis_tab_selected_color">@color/chassis_tab_selected_color_light</color>
- <color name="chassis_tab_selected_color_dark">#E2FFFFFF</color>
- <color name="chassis_tab_selected_color_light">#FFFFFFFF</color>
- <color name="chassis_tab_unselected_color">@color/chassis_tab_unselected_color_light</color>
- <color name="chassis_tab_unselected_color_dark">#80FFFFFF</color>
- <color name="chassis_tab_unselected_color_light">#90FFFFFF</color>
+ <!-- Tabs -->
+
+ <!-- Selected colors -->
+ <color name="chassis_tab_selected_color">@color/chassis_primary_text_color</color>
+ <!-- Normal colors -->
+ <color name="chassis_tab_unselected_color">@color/chassis_secondary_text_color</color>
+
+ <!-- Toolbar -->
+
+ <!-- Text color applied to the hint displayed inside the search box -->
+ <color name="chassis_toolbar_search_hint_text_color">#33FFFFFF</color>
+ <!-- Toolbar background color -->
+ <color name="chassis_toolbar_background_color">#E0000000</color>
+
+ <!-- Paged Recycler View -->
+ <!-- The color of the scroll bar indicator in the PagedListView. -->
+ <color name="chassis_scrollbar_thumb">#99ffffff</color>
</resources>
diff --git a/car-chassis-lib/res/values/config.xml b/car-chassis-lib/res/values/config.xml
new file mode 100644
index 0000000..8a0875f
--- /dev/null
+++ b/car-chassis-lib/res/values/config.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <!--
+ Configuration for a default scrollbar for the PagedRecyclerView. This component must inherit
+ abstract class ScrollBar. If the ScrollBar is enabled, the component will be initialized from
+ PagedRecyclerView#createScrollBarFromConfig().
+ -->
+ <string name="chassis_scrollbar_component" translatable="false">
+ com.google.android.apps.automotive.chassis.libraries.CarScrollBar
+ </string>
+
+ <!--
+ Whether to include a gutter to the start, end or both sides of the list view items.
+ The gutter width will be the width of the scrollbar, and by default will be set to
+ both. Values are defined as follows:
+ none = 0
+ start = 1
+ end = 2
+ both = 3
+ -->
+ <integer name="chassis_scrollbar_gutter" translatable="false">1</integer>
+
+ <!--
+ Position of the scrollbar. Default to left. Values are defined as follows:
+ start = 0
+ end = 1
+ -->
+ <integer name="chassis_scrollbar_position" translatable="false">0</integer>
+
+ <!-- Width of the scrollbar container. -->
+ <dimen name="chassis_scrollbar_container_width" translatable="false">@*android:dimen/car_margin</dimen>
+
+</resources>
\ No newline at end of file
diff --git a/car-chassis-lib/res/values/dimens.xml b/car-chassis-lib/res/values/dimens.xml
index 6bc61dd..5565bfb 100644
--- a/car-chassis-lib/res/values/dimens.xml
+++ b/car-chassis-lib/res/values/dimens.xml
@@ -14,22 +14,68 @@
limitations under the License.
-->
<resources>
- <dimen name="touch_target_size">@*android:dimen/car_touch_target_size</dimen>
- <dimen name="primary_icon_size">@*android:dimen/car_primary_icon_size</dimen>
- <item name="letter_spacing_body1" format="float" type="dimen">0.0</item>
- <item name="letter_spacing_body3" format="float" type="dimen">0.0</item>
+ <!-- General resources -->
+
+ <dimen name="chassis_touch_target_width">76dp</dimen>
+ <dimen name="chassis_touch_target_height">76dp</dimen>
+ <dimen name="chassis_primary_icon_size">44dp</dimen>
+ <item name="chassis_letter_spacing_body1" format="float" type="dimen">0.0</item>
+ <item name="chassis_letter_spacing_body3" format="float" type="dimen">0.0</item>
+
+ <!-- Application Bar -->
+ <dimen name="chassis_app_bar_height">80dp</dimen>
+
+ <!-- Margin -->
+ <dimen name="chassis_margin">20dp</dimen>
+
+ <!-- Paddings -->
+ <dimen name="chassis_padding_0">4dp</dimen>
+ <dimen name="chassis_padding_1">8dp</dimen>
+ <dimen name="chassis_padding_2">16dp</dimen>
+ <dimen name="chassis_padding_3">24dp</dimen>
+ <dimen name="chassis_padding_4">32dp</dimen>
+ <dimen name="chassis_padding_5">64dp</dimen>
+ <dimen name="chassis_padding_6">96dp</dimen>
<!-- Tabs -->
- <dimen name="chassis_tab_width">135dp</dimen>
+
+ <!-- Exact size of the tab textbox. Use @dimen/wrap_content if this must be flexible -->
+ <dimen name="chassis_tab_text_width">135dp</dimen>
+ <!-- Horizontal padding between tabs -->
<dimen name="chassis_tab_padding_x">12dp</dimen>
- <dimen name="chassis_tab_icon_size">36dp</dimen>
+ <!-- Tab icon width (if icons are enabled) -->
+ <dimen name="chassis_tab_icon_width">36dp</dimen>
+ <!-- Tab icon height (if icons are enabled) -->
+ <dimen name="chassis_tab_icon_height">36dp</dimen>
<!-- Car toolbar -->
- <dimen name="chassis_toolbar_view_nav_button_width">@*android:dimen/car_margin</dimen>
- <dimen name="chassis_toolbar_first_row_height">@*android:dimen/car_app_bar_height</dimen>
+ <dimen name="chassis_toolbar_view_nav_button_width">@dimen/chassis_margin</dimen>
+ <dimen name="chassis_toolbar_first_row_height">@dimen/chassis_app_bar_height</dimen>
<dimen name="chassis_toolbar_second_row_height">0dp</dimen>
- <dimen name="chassis_toolbar_view_icon_size">@*android:dimen/car_primary_icon_size</dimen>
- <dimen name="chassis_toolbar_view_title_margin_start">@*android:dimen/car_padding_2</dimen>
- <dimen name="chassis_toolbar_custom_button_margin">@*android:dimen/car_padding_2</dimen>
+ <dimen name="chassis_toolbar_view_icon_size">@dimen/chassis_primary_icon_size</dimen>
+ <dimen name="chassis_toolbar_view_title_margin_start">@dimen/chassis_padding_2</dimen>
+ <dimen name="chassis_toolbar_custom_button_margin">@dimen/chassis_padding_2</dimen>
<dimen name="chassis_toolbar_button_background_radius">48dp</dimen>
+
+ <!-- Internal artifacts. Do not overlay -->
+ <item name="wrap_content" format="integer" type="dimen">-2</item>
+
+ <!-- Default Scroll Bar for PagedRecyclerView -->
+ <dimen name="chassis_scrollbar_button_size">76dp</dimen>
+ <dimen name="chassis_scrollbar_thumb_width">6dp</dimen>
+ <dimen name="chassis_scrollbar_separator_margin">16dp</dimen>
+ <dimen name="chassis_scrollbar_margin">20dp</dimen>
+ <dimen name="chassis_scrollbar_thumb_radius">100dp</dimen>
+
+ <item name="chassis_button_disabled_alpha" format="float" type="dimen">0.2</item>
+ <item name="chassis_scroller_milliseconds_per_inch" format="float" type="dimen">150</item>
+ <item name="chassis_scroller_deceleration_time_divisor" format="float" type="dimen">0.45</item>
+ <item name="chassis_scroller_interpolator_factor" format="float" type="dimen">1.8</item>
+
+ <item name="chassis_scrollbar_milliseconds_per_inch" format="float" type="dimen">150.0</item>
+ <item name="chassis_scrollbar_deceleration_times_divisor" format="float" type="dimen">0.45</item>
+ <item name="chassis_scrollbar_decelerate_interpolator_factor" format="float" type="dimen">1.8</item>
+
+ <dimen name="chassis_scrollbar_padding_start">0dp</dimen>
+ <dimen name="chassis_scrollbar_padding_end">0dp</dimen>
</resources>
diff --git a/car-chassis-lib/res/values/integers.xml b/car-chassis-lib/res/values/integers.xml
new file mode 100644
index 0000000..083fa2e
--- /dev/null
+++ b/car-chassis-lib/res/values/integers.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+ <!-- Default max string length -->
+ <integer name="chassis_default_max_string_length">120</integer>
+
+</resources>
\ No newline at end of file
diff --git a/car-chassis-lib/res/values/strings.xml b/car-chassis-lib/res/values/strings.xml
index b93e4a1..38ea967 100644
--- a/car-chassis-lib/res/values/strings.xml
+++ b/car-chassis-lib/res/values/strings.xml
@@ -15,5 +15,11 @@
-->
<resources>
<!-- Search hint, displayed inside the search box [CHAR LIMIT=50] -->
- <string name="chassis_default_search_hint">Search…</string>
+ <string name="chassis_toolbar_default_search_hint">Search…</string>
+ <!-- CarUxRestrictions Utility -->
+ <string name="chassis_ellipsis" translatable="false">…</string>
+ <!-- Content description for paged recycler view scroll bar down arrow [CHAR LIMIT=30] -->
+ <string name="chassis_scrollbar_page_down_button">Scroll down</string>
+ <!-- Content description for paged recycler view scroll bar up arrow [CHAR LIMIT=30] -->
+ <string name="chassis_scrollbar_page_up_button">Scroll up</string>
</resources>
diff --git a/car-chassis-lib/res/values/styles.xml b/car-chassis-lib/res/values/styles.xml
index 5214d2d..6d5308f 100644
--- a/car-chassis-lib/res/values/styles.xml
+++ b/car-chassis-lib/res/values/styles.xml
@@ -19,15 +19,15 @@
<style name="ChassisTabItemText">
<item name="android:textAppearance">@style/TextAppearance.Body3</item>
<item name="android:textColor">@color/chassis_tab_item_selector</item>
- <item name="android:layout_width">@dimen/chassis_tab_width</item>
+ <item name="android:layout_width">@dimen/chassis_tab_text_width</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:singleLine">true</item>
<item name="android:gravity">center</item>
</style>
<style name="ChassisTabItemIcon">
- <item name="android:layout_width">@dimen/chassis_tab_icon_size</item>
- <item name="android:layout_height">@dimen/chassis_tab_icon_size</item>
+ <item name="android:layout_width">@dimen/chassis_tab_icon_width</item>
+ <item name="android:layout_height">@dimen/chassis_tab_icon_height</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:tint">@color/chassis_tab_item_selector</item>
<item name="android:tintMode">src_in</item>
@@ -67,7 +67,8 @@
<item name="android:layout_width">@dimen/chassis_toolbar_view_icon_size</item>
<item name="android:layout_height">@dimen/chassis_toolbar_view_icon_size</item>
<item name="android:layout_gravity">center</item>
- <item name="android:src">@drawable/ic_arrow_back</item>
+ <item name="android:tint">@color/chassis_primary_text_color</item>
+ <item name="android:src">@drawable/chassis_icon_arrow_back</item>
<item name="android:scaleType">fitXY</item>
</style>
@@ -80,17 +81,21 @@
<style name="TextAppearance">
<item name="android:fontFamily">roboto-regular</item>
- <item name="android:textColor">@color/primary_text_color</item>
+ <item name="android:textColor">@color/chassis_primary_text_color</item>
</style>
<style name="TextAppearance.Body1" parent="TextAppearance">
<item name="android:textSize">32sp</item>
- <item name="android:letterSpacing">@dimen/letter_spacing_body1</item>
+ <item name="android:letterSpacing">@dimen/chassis_letter_spacing_body1</item>
</style>
<style name="TextAppearance.Body3" parent="TextAppearance">
<item name="android:textSize">24sp</item>
- <item name="android:letterSpacing">@dimen/letter_spacing_body3</item>
+ <item name="android:letterSpacing">@dimen/chassis_letter_spacing_body3</item>
</style>
+ <style name="PagedRecyclerView">
+ </style>
+ <style name="PagedRecyclerView.NestedRecyclerView">
+ </style>
</resources>
diff --git a/car-chassis-lib/res/values/themes.xml b/car-chassis-lib/res/values/themes.xml
new file mode 100644
index 0000000..70ea7ae
--- /dev/null
+++ b/car-chassis-lib/res/values/themes.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES 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">
+ <!-- Base application theme. -->
+ <style name="ChassisTheme" parent="android:Theme.DeviceDefault">
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowNoTitle">true</item>
+ </style>
+</resources>
\ No newline at end of file
diff --git a/car-chassis-lib/settings.gradle b/car-chassis-lib/settings.gradle
new file mode 100644
index 0000000..321a5db
--- /dev/null
+++ b/car-chassis-lib/settings.gradle
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+include ':PaintBooth'
+project(':PaintBooth').projectDir = new File('./tests/paintbooth')
+rootProject.name='Chassis'
diff --git a/car-chassis-lib/src/com/android/car/chassis/Toolbar.java b/car-chassis-lib/src/com/android/car/chassis/Toolbar.java
index eb5ef7f..0cb2373 100644
--- a/car-chassis-lib/src/com/android/car/chassis/Toolbar.java
+++ b/car-chassis-lib/src/com/android/car/chassis/Toolbar.java
@@ -17,7 +17,9 @@
import android.content.Context;
import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
@@ -47,6 +49,8 @@
*/
public class Toolbar extends FrameLayout {
+ private static final String TAG = "ChassisToolbar";
+
/** Enum of states the toolbar can be in. Controls what elements of the toolbar are displayed */
public enum State {
/**
@@ -127,7 +131,7 @@
mTitle.setText(a.getString(R.styleable.ChassisToolbar_title));
setLogo(a.getResourceId(R.styleable.ChassisToolbar_logo, 0));
setButtons(a.getResourceId(R.styleable.ChassisToolbar_buttons, 0));
- setBackground(context.getDrawable(R.color.toolbar_background_color));
+ setBackgroundShown(a.getBoolean(R.styleable.ChassisToolbar_showBackground, true));
mShowButtonsWhileSearching = a.getBoolean(
R.styleable.ChassisToolbar_showButtonsWhileSearching, false);
String searchHint = a.getString(R.styleable.ChassisToolbar_searchHint);
@@ -135,6 +139,26 @@
setSearchHint(searchHint);
}
+ switch (a.getInt(R.styleable.ChassisToolbar_state, 0)) {
+ case 0:
+ setState(State.HOME);
+ break;
+ case 1:
+ setState(State.SUBPAGE);
+ break;
+ case 2:
+ setState(State.SUBPAGE_CUSTOM);
+ break;
+ case 3:
+ setState(State.SEARCH);
+ break;
+ default:
+ if (Log.isLoggable(TAG, Log.WARN)) {
+ Log.w(TAG, "Unknown initial state");
+ }
+ break;
+ }
+
a.recycle();
mTabLayout.addListener(new TabLayout.Listener() {
@@ -223,6 +247,27 @@
}
/**
+ * setBackground is disallowed, to prevent apps from deviating from the intended style too much.
+ */
+ @Override
+ public void setBackground(Drawable d) {
+ throw new UnsupportedOperationException(
+ "You can not change the background of a chassis toolbar, use "
+ + "setBackgroundShown(boolean) or an RRO instead.");
+ }
+
+ /**
+ * Show/hide the background. When hidden, the toolbar is completely transparent.
+ */
+ public void setBackgroundShown(boolean shown) {
+ if (shown) {
+ super.setBackground(getContext().getDrawable(R.color.chassis_toolbar_background_color));
+ } else {
+ super.setBackground(null);
+ }
+ }
+
+ /**
* Sets the buttons to be shown. Click events for these buttons will be received in
* {@link Listener#onCustomButtonPressed(View)}.
*
@@ -336,7 +381,7 @@
View.OnClickListener backClickListener = (v) -> forEachListener(Listener::onBack);
mNavIcon.setVisibility(state != State.HOME ? VISIBLE : INVISIBLE);
- mNavIcon.setImageResource(state != State.HOME ? R.drawable.ic_arrow_back : 0);
+ mNavIcon.setImageResource(state != State.HOME ? R.drawable.chassis_icon_arrow_back : 0);
mLogo.setVisibility(state == State.HOME && mHasLogo ? VISIBLE : INVISIBLE);
mNavIconContainer.setVisibility(state != State.HOME || mHasLogo ? VISIBLE : GONE);
mNavIconContainer.setClickable(state != State.HOME);
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarScrollBar.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarScrollBar.java
new file mode 100644
index 0000000..0ec40d4
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarScrollBar.java
@@ -0,0 +1,594 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Handler;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import androidx.annotation.IntRange;
+import androidx.recyclerview.widget.OrientationHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.chassis.R;
+import com.android.car.chassis.pagedrecyclerview.PagedRecyclerView.ScrollBarPosition;
+
+/**
+ * The default scroll bar widget for the {@link PagedRecyclerView}.
+ *
+ * <p>Inspired by {@link androidx.car.widget.PagedListView}. Most pagination and scrolling logic has
+ * been ported from the PLV with minor updates.
+ */
+class CarScrollBar implements ScrollBar {
+ private float mButtonDisabledAlpha;
+ private static final String TAG = "CarScrollBar";
+ private PagedSnapHelper mSnapHelper;
+
+ private ImageView mUpButton;
+ private View mScrollView;
+ private View mScrollThumb;
+ private ImageView mDownButton;
+ private int mPaddingStart;
+ private int mPaddingEnd;
+
+ private int mSeparatingMargin;
+
+ private RecyclerView mRecyclerView;
+
+ /** The amount of space that the scroll thumb is allowed to roam over. */
+ private int mScrollThumbTrackHeight;
+
+ private final Interpolator mPaginationInterpolator = new AccelerateDecelerateInterpolator();
+
+ private final int mRowsPerPage = -1;
+ private final Handler mHandler = new Handler();
+
+ private OrientationHelper mOrientationHelper;
+
+ @Override
+ public void initialize(
+ RecyclerView rv,
+ int scrollBarContainerWidth,
+ @ScrollBarPosition int scrollBarPosition,
+ boolean scrollBarAboveRecyclerView) {
+
+ this.mRecyclerView = rv;
+
+ LayoutInflater inflater =
+ (LayoutInflater) rv.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ FrameLayout parent = (FrameLayout) getRecyclerView().getParent();
+
+ mScrollView = inflater.inflate(R.layout.chassis_pagedrecyclerview_scrollbar, parent, false);
+ mScrollView.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+
+ Resources res = rv.getContext().getResources();
+ mButtonDisabledAlpha = res.getFloat(R.dimen.chassis_button_disabled_alpha);
+
+ if (scrollBarAboveRecyclerView) {
+ parent.addView(mScrollView);
+ } else {
+ parent.addView(mScrollView, /* index= */ 0);
+ }
+
+ setScrollBarContainerWidth(scrollBarContainerWidth);
+ setScrollBarPosition(scrollBarPosition);
+
+ getRecyclerView().addOnScrollListener(mRecyclerViewOnScrollListener);
+ getRecyclerView().getRecycledViewPool().setMaxRecycledViews(0, 12);
+
+ mSeparatingMargin = res.getDimensionPixelSize(R.dimen.chassis_scrollbar_separator_margin);
+
+ mUpButton = mScrollView.findViewById(R.id.page_up);
+ PaginateButtonClickListener upButtonClickListener =
+ new PaginateButtonClickListener(PaginationListener.PAGE_UP);
+ mUpButton.setOnClickListener(upButtonClickListener);
+
+ mDownButton = mScrollView.findViewById(R.id.page_down);
+ PaginateButtonClickListener downButtonClickListener =
+ new PaginateButtonClickListener(PaginationListener.PAGE_DOWN);
+ mDownButton.setOnClickListener(downButtonClickListener);
+
+ mScrollThumb = mScrollView.findViewById(R.id.scrollbar_thumb);
+
+ mSnapHelper = new PagedSnapHelper(rv.getContext());
+ getRecyclerView().setOnFlingListener(null);
+ mSnapHelper.attachToRecyclerView(getRecyclerView());
+
+ mScrollView.addOnLayoutChangeListener(
+ (View v,
+ int left,
+ int top,
+ int right,
+ int bottom,
+ int oldLeft,
+ int oldTop,
+ int oldRight,
+ int oldBottom) -> {
+ int width = right - left;
+
+ OrientationHelper orientationHelper =
+ getOrientationHelper(getRecyclerView().getLayoutManager());
+
+ // This value will keep track of the top of the current view being laid out.
+ int layoutTop = orientationHelper.getStartAfterPadding() + mPaddingStart;
+
+ // Lay out the up button at the top of the view.
+ layoutViewCenteredFromTop(mUpButton, layoutTop, width);
+ layoutTop = mUpButton.getBottom();
+
+ // Lay out the scroll thumb
+ layoutTop += mSeparatingMargin;
+ layoutViewCenteredFromTop(mScrollThumb, layoutTop, width);
+
+ // Lay out the bottom button at the bottom of the view.
+ int downBottom = orientationHelper.getEndAfterPadding() - mPaddingEnd;
+ layoutViewCenteredFromBottom(mDownButton, downBottom, width);
+
+ mHandler.post(this::calculateScrollThumbTrackHeight);
+ mHandler.post(() -> updatePaginationButtons(/* animate= */ false));
+ });
+ }
+
+ public RecyclerView getRecyclerView() {
+ return mRecyclerView;
+ }
+
+ @Override
+ public void requestLayout() {
+ mScrollView.requestLayout();
+ }
+
+ /**
+ * Sets the width of the container that holds the scrollbar. The scrollbar will be centered
+ * within
+ * this width.
+ *
+ * @param width The width of the scrollbar container.
+ */
+ private void setScrollBarContainerWidth(int width) {
+ ViewGroup.LayoutParams layoutParams = mScrollView.getLayoutParams();
+ layoutParams.width = width;
+ mScrollView.requestLayout();
+ }
+
+ @Override
+ public void setPadding(int paddingStart, int paddingEnd) {
+ this.mPaddingStart = paddingStart;
+ this.mPaddingEnd = paddingEnd;
+ requestLayout();
+ }
+
+ /**
+ * Sets the position of the scrollbar.
+ *
+ * @param position Enum value of the scrollbar position. 0 for Start and 1 for end.
+ */
+ private void setScrollBarPosition(@ScrollBarPosition int position) {
+ FrameLayout.LayoutParams layoutParams =
+ (FrameLayout.LayoutParams) mScrollView.getLayoutParams();
+ if (position == ScrollBarPosition.START) {
+ layoutParams.gravity = Gravity.LEFT;
+ } else {
+ layoutParams.gravity = Gravity.RIGHT;
+ }
+
+ mScrollView.requestLayout();
+ }
+
+ /**
+ * Sets whether or not the up button on the scroll bar is clickable.
+ *
+ * @param enabled {@code true} if the up button is enabled.
+ */
+ private void setUpEnabled(boolean enabled) {
+ mUpButton.setEnabled(enabled);
+ mUpButton.setAlpha(enabled ? 1f : mButtonDisabledAlpha);
+ }
+
+ /**
+ * Sets whether or not the down button on the scroll bar is clickable.
+ *
+ * @param enabled {@code true} if the down button is enabled.
+ */
+ private void setDownEnabled(boolean enabled) {
+ mDownButton.setEnabled(enabled);
+ mDownButton.setAlpha(enabled ? 1f : mButtonDisabledAlpha);
+ }
+
+ /**
+ * Returns whether or not the down button on the scroll bar is clickable.
+ *
+ * @return {@code true} if the down button is enabled. {@code false} otherwise.
+ */
+ private boolean isDownEnabled() {
+ return mDownButton.isEnabled();
+ }
+
+ /** Listener for when the list should paginate. */
+ interface PaginationListener {
+ int PAGE_UP = 0;
+ int PAGE_DOWN = 1;
+
+ /** Called when the linked view should be paged in the given direction */
+ void onPaginate(int direction);
+ }
+
+ /**
+ * Calculate the amount of space that the scroll bar thumb is allowed to roam. The thumb is
+ * allowed to take up the space between the down bottom and the up or alpha jump button,
+ * depending
+ * on if the latter is visible.
+ */
+ private void calculateScrollThumbTrackHeight() {
+ // Subtracting (2 * mSeparatingMargin) for the top/bottom margin above and below the
+ // scroll bar thumb.
+ mScrollThumbTrackHeight = mDownButton.getTop() - (2 * mSeparatingMargin);
+
+ // If there's an alpha jump button, then the thumb is laid out starting from below that.
+ mScrollThumbTrackHeight -= mUpButton.getBottom();
+ }
+
+ /**
+ * Lays out the given View starting from the given {@code top} value downwards and centered
+ * within
+ * the given {@code availableWidth}.
+ *
+ * @param view The view to lay out.
+ * @param top The top value to start laying out from. This value will be the resulting top value
+ * of the view.
+ * @param availableWidth The width in which to center the given view.
+ */
+ private static void layoutViewCenteredFromTop(View view, int top, int availableWidth) {
+ int viewWidth = view.getMeasuredWidth();
+ int viewLeft = (availableWidth - viewWidth) / 2;
+ view.layout(viewLeft, top, viewLeft + viewWidth, top + view.getMeasuredHeight());
+ }
+
+ /**
+ * Lays out the given View starting from the given {@code bottom} value upwards and centered
+ * within the given {@code availableSpace}.
+ *
+ * @param view The view to lay out.
+ * @param bottom The bottom value to start laying out from. This value will be the resulting
+ * bottom value of the view.
+ * @param availableWidth The width in which to center the given view.
+ */
+ private static void layoutViewCenteredFromBottom(View view, int bottom, int availableWidth) {
+ int viewWidth = view.getMeasuredWidth();
+ int viewLeft = (availableWidth - viewWidth) / 2;
+ view.layout(viewLeft, bottom - view.getMeasuredHeight(), viewLeft + viewWidth, bottom);
+ }
+
+ /**
+ * Sets the range, offset and extent of the scroll bar. The range represents the size of a
+ * container for the scrollbar thumb; offset is the distance from the start of the container to
+ * where the thumb should be; and finally, extent is the size of the thumb.
+ *
+ * <p>These values can be expressed in arbitrary units, so long as they share the same units.
+ * The
+ * values should also be positive.
+ *
+ * @param range The range of the scrollbar's thumb
+ * @param offset The offset of the scrollbar's thumb
+ * @param extent The extent of the scrollbar's thumb
+ * @param animate Whether or not the thumb should animate from its current position to the
+ * position specified by the given range, offset and extent.
+ */
+ private void setParameters(
+ @IntRange(from = 0) int range,
+ @IntRange(from = 0) int offset,
+ @IntRange(from = 0) int extent,
+ boolean animate) {
+ // Not laid out yet, so values cannot be calculated.
+ if (!mScrollView.isLaidOut()) {
+ return;
+ }
+
+ // If the scroll bars aren't visible, then no need to update.
+ if (mScrollView.getVisibility() == View.GONE || range == 0) {
+ return;
+ }
+
+ int thumbLength = calculateScrollThumbLength(range, extent);
+ int thumbOffset = calculateScrollThumbOffset(range, offset, thumbLength);
+
+ // Sets the size of the thumb and request a redraw if needed.
+ ViewGroup.LayoutParams lp = mScrollThumb.getLayoutParams();
+
+ if (lp.height != thumbLength) {
+ lp.height = thumbLength;
+ mScrollThumb.requestLayout();
+ }
+
+ moveY(mScrollThumb, thumbOffset, animate);
+ }
+
+ /**
+ * Calculates and returns how big the scroll bar thumb should be based on the given range and
+ * extent.
+ *
+ * @param range The total amount of space the scroll bar is allowed to roam over.
+ * @param extent The amount of space that the scroll bar takes up relative to the range.
+ * @return The height of the scroll bar thumb in pixels.
+ */
+ private int calculateScrollThumbLength(int range, int extent) {
+ // Scale the length by the available space that the thumb can fill.
+ return Math.round(((float) extent / range) * mScrollThumbTrackHeight);
+ }
+
+ /**
+ * Calculates and returns how much the scroll thumb should be offset from the top of where it
+ * has
+ * been laid out.
+ *
+ * @param range The total amount of space the scroll bar is allowed to roam over.
+ * @param offset The amount the scroll bar should be offset, expressed in the same units as the
+ * given range.
+ * @param thumbLength The current length of the thumb in pixels.
+ * @return The amount the thumb should be offset in pixels.
+ */
+ private int calculateScrollThumbOffset(int range, int offset, int thumbLength) {
+ // Ensure that if the user has reached the bottom of the list, then the scroll bar is
+ // aligned to the bottom as well. Otherwise, scale the offset appropriately.
+ // This offset will be a value relative to the parent of this scrollbar, so start by where
+ // the top of mScrollThumb is.
+ return mScrollThumb.getTop()
+ + (isDownEnabled()
+ ? Math.round(((float) offset / range) * mScrollThumbTrackHeight)
+ : mScrollThumbTrackHeight - thumbLength);
+ }
+
+ /** Moves the given view to the specified 'y' position. */
+ private void moveY(final View view, float newPosition, boolean animate) {
+ final int duration = animate ? 200 : 0;
+ view.animate()
+ .y(newPosition)
+ .setDuration(duration)
+ .setInterpolator(mPaginationInterpolator)
+ .start();
+ }
+
+ private class PaginateButtonClickListener implements View.OnClickListener {
+ private final int mPaginateDirection;
+ private PaginationListener mPaginationListener;
+
+ PaginateButtonClickListener(int paginateDirection) {
+ this.mPaginateDirection = paginateDirection;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mPaginationListener != null) {
+ mPaginationListener.onPaginate(mPaginateDirection);
+ }
+ if (mPaginateDirection == PaginationListener.PAGE_DOWN) {
+ pageDown();
+ } else if (mPaginateDirection == PaginationListener.PAGE_UP) {
+ pageUp();
+ }
+ }
+ }
+
+ private final RecyclerView.OnScrollListener mRecyclerViewOnScrollListener =
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ updatePaginationButtons(false);
+ }
+ };
+
+ /** Returns the page the given position is on, starting with page 0. */
+ int getPage(int position) {
+ if (mRowsPerPage == -1) {
+ return -1;
+ }
+ if (mRowsPerPage == 0) {
+ return 0;
+ }
+ return position / mRowsPerPage;
+ }
+
+ private OrientationHelper getOrientationHelper(RecyclerView.LayoutManager layoutManager) {
+ if (mOrientationHelper == null || mOrientationHelper.getLayoutManager() != layoutManager) {
+ // PagedRecyclerView is assumed to be a list that always vertically scrolls.
+ mOrientationHelper = OrientationHelper.createVerticalHelper(layoutManager);
+ }
+ return mOrientationHelper;
+ }
+
+ /**
+ * Scrolls the contents of the RecyclerView up a page. A page is defined as the height of the
+ * {@code PagedRecyclerView}.
+ *
+ * <p>The resulting first item in the list will be snapped to so that it is completely visible.
+ * If
+ * this is not possible due to the first item being taller than the containing {@code
+ * PagedRecyclerView}, then the snapping will not occur.
+ */
+ void pageUp() {
+ int currentOffset = getRecyclerView().computeVerticalScrollOffset();
+ if (getRecyclerView().getLayoutManager() == null
+ || getRecyclerView().getChildCount() == 0
+ || currentOffset == 0) {
+ return;
+ }
+
+ // Use OrientationHelper to calculate scroll distance in order to match snapping behavior.
+ OrientationHelper orientationHelper =
+ getOrientationHelper(getRecyclerView().getLayoutManager());
+ int screenSize = orientationHelper.getTotalSpace();
+
+ int scrollDistance = screenSize;
+ // The iteration order matters. In case where there are 2 items longer than screen size, we
+ // want to focus on upcoming view.
+ for (int i = 0; i < getRecyclerView().getChildCount(); i++) {
+ /*
+ * We treat child View longer than screen size differently:
+ * 1) When it enters screen, next pageUp will align its bottom with parent bottom;
+ * 2) When it leaves screen, next pageUp will align its top with parent top.
+ */
+ View child = getRecyclerView().getChildAt(i);
+ if (child.getHeight() > screenSize) {
+ if (orientationHelper.getDecoratedEnd(child) < screenSize) {
+ // Child view bottom is entering screen. Align its bottom with parent bottom.
+ scrollDistance = screenSize - orientationHelper.getDecoratedEnd(child);
+ } else if (-screenSize < orientationHelper.getDecoratedStart(child)
+ && orientationHelper.getDecoratedStart(child) < 0) {
+ // Child view top is about to enter screen - its distance to parent top
+ // is less than a full scroll. Align child top with parent top.
+ scrollDistance = Math.abs(orientationHelper.getDecoratedStart(child));
+ }
+ // There can be two items that are longer than the screen. We stop at the first one.
+ // This is affected by the iteration order.
+ break;
+ }
+ }
+ // Distance should always be positive. Negate its value to scroll up.
+ getRecyclerView().smoothScrollBy(0, -scrollDistance);
+ }
+
+ /**
+ * Scrolls the contents of the RecyclerView down a page. A page is defined as the height of the
+ * {@code PagedRecyclerView}.
+ *
+ * <p>This method will attempt to bring the last item in the list as the first item. If the
+ * current first item in the list is taller than the {@code PagedRecyclerView}, then it will be
+ * scrolled the length of a page, but not snapped to.
+ */
+ void pageDown() {
+ if (getRecyclerView().getLayoutManager() == null
+ || getRecyclerView().getChildCount() == 0) {
+ return;
+ }
+
+ OrientationHelper orientationHelper =
+ getOrientationHelper(getRecyclerView().getLayoutManager());
+ int screenSize = orientationHelper.getTotalSpace();
+ int scrollDistance = screenSize;
+
+ // If the last item is partially visible, page down should bring it to the top.
+ View lastChild = getRecyclerView().getChildAt(getRecyclerView().getChildCount() - 1);
+ if (getRecyclerView()
+ .getLayoutManager()
+ .isViewPartiallyVisible(
+ lastChild, /* completelyVisible= */ false, /* acceptEndPointInclusion= */
+ false)) {
+ scrollDistance = orientationHelper.getDecoratedStart(lastChild);
+ if (scrollDistance < 0) {
+ // Scroll value can be negative if the child is longer than the screen size and the
+ // visible area of the screen does not show the start of the child.
+ // Scroll to the next screen if the start value is negative
+ scrollDistance = screenSize;
+ }
+ }
+
+ // The iteration order matters. In case where there are 2 items longer than screen size, we
+ // want to focus on upcoming view (the one at the bottom of screen).
+ for (int i = getRecyclerView().getChildCount() - 1; i >= 0; i--) {
+ /* We treat child View longer than screen size differently:
+ * 1) When it enters screen, next pageDown will align its top with parent top;
+ * 2) When it leaves screen, next pageDown will align its bottom with parent bottom.
+ */
+ View child = getRecyclerView().getChildAt(i);
+ if (child.getHeight() > screenSize) {
+ if (orientationHelper.getDecoratedStart(child) > 0) {
+ // Child view top is entering screen. Align its top with parent top.
+ scrollDistance = orientationHelper.getDecoratedStart(child);
+ } else if (screenSize < orientationHelper.getDecoratedEnd(child)
+ && orientationHelper.getDecoratedEnd(child) < 2 * screenSize) {
+ // Child view bottom is about to enter screen - its distance to parent bottom
+ // is less than a full scroll. Align child bottom with parent bottom.
+ scrollDistance = orientationHelper.getDecoratedEnd(child) - screenSize;
+ }
+ // There can be two items that are longer than the screen. We stop at the first one.
+ // This is affected by the iteration order.
+ break;
+ }
+ }
+
+ getRecyclerView().smoothScrollBy(0, scrollDistance);
+ }
+
+ /**
+ * Determines if scrollbar should be visible or not and shows/hides it accordingly. If this is
+ * being called as a result of adapter changes, it should be called after the new layout has
+ * been
+ * calculated because the method of determining scrollbar visibility uses the current layout.
+ * If
+ * this is called after an adapter change but before the new layout, the visibility
+ * determination
+ * may not be correct.
+ *
+ * @param animate {@code true} if the scrollbar should animate to its new position. {@code
+ * false}
+ * if no animation is used
+ */
+ private void updatePaginationButtons(boolean animate) {
+
+ boolean isAtStart = isAtStart();
+ boolean isAtEnd = isAtEnd();
+ RecyclerView.LayoutManager layoutManager = getRecyclerView().getLayoutManager();
+
+ if ((isAtStart && isAtEnd) || layoutManager == null || layoutManager.getItemCount() == 0) {
+ mScrollView.setVisibility(View.INVISIBLE);
+ } else {
+ mScrollView.setVisibility(View.VISIBLE);
+ }
+ setUpEnabled(!isAtStart);
+ setDownEnabled(!isAtEnd);
+
+ if (layoutManager == null) {
+ return;
+ }
+
+ if (layoutManager.canScrollVertically()) {
+ setParameters(
+ getRecyclerView().computeVerticalScrollRange(),
+ getRecyclerView().computeVerticalScrollOffset(),
+ getRecyclerView().computeVerticalScrollExtent(),
+ animate);
+ } else {
+ setParameters(
+ getRecyclerView().computeHorizontalScrollRange(),
+ getRecyclerView().computeHorizontalScrollOffset(),
+ getRecyclerView().computeHorizontalScrollExtent(),
+ animate);
+ }
+
+ mScrollView.invalidate();
+ }
+
+ /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
+ boolean isAtStart() {
+ return mSnapHelper.isAtStart(getRecyclerView().getLayoutManager());
+ }
+
+ /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
+ boolean isAtEnd() {
+ return mSnapHelper.isAtEnd(getRecyclerView().getLayoutManager());
+ }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarUxRestrictionsUtil.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarUxRestrictionsUtil.java
new file mode 100644
index 0000000..7e204bd
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/CarUxRestrictionsUtil.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview;
+
+import static android.car.drivingstate.CarUxRestrictions.UX_RESTRICTIONS_LIMIT_STRING_LENGTH;
+
+import android.car.Car;
+import android.car.CarNotConnectedException;
+import android.car.drivingstate.CarUxRestrictions;
+import android.car.drivingstate.CarUxRestrictions.CarUxRestrictionsInfo;
+import android.car.drivingstate.CarUxRestrictionsManager;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.car.chassis.R;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * Utility class to access Car Restriction Manager.
+ *
+ * <p>This class must be a singleton because only one listener can be registered with {@link
+ * CarUxRestrictionsManager} at a time, as documented in {@link
+ * CarUxRestrictionsManager#registerListener}.
+ */
+public class CarUxRestrictionsUtil {
+ private static final String TAG = "CarUxRestrictionsUtil";
+
+ private final Car mCarApi;
+ private CarUxRestrictionsManager mCarUxRestrictionsManager;
+ @NonNull
+ private CarUxRestrictions mCarUxRestrictions = getDefaultRestrictions();
+
+ private Set<OnUxRestrictionsChangedListener> mObservers;
+ private static CarUxRestrictionsUtil sInstance = null;
+
+ private CarUxRestrictionsUtil(Context context) {
+ CarUxRestrictionsManager.OnUxRestrictionsChangedListener listener =
+ (carUxRestrictions) -> {
+ if (carUxRestrictions == null) {
+ this.mCarUxRestrictions = getDefaultRestrictions();
+ } else {
+ this.mCarUxRestrictions = carUxRestrictions;
+ }
+
+ for (OnUxRestrictionsChangedListener observer : mObservers) {
+ observer.onRestrictionsChanged(this.mCarUxRestrictions);
+ }
+ };
+
+ mCarApi = Car.createCar(context);
+ mObservers = Collections.newSetFromMap(new WeakHashMap<>());
+
+ try {
+ mCarUxRestrictionsManager =
+ (CarUxRestrictionsManager) mCarApi.getCarManager(
+ Car.CAR_UX_RESTRICTION_SERVICE);
+ mCarUxRestrictionsManager.registerListener(listener);
+ listener.onUxRestrictionsChanged(
+ mCarUxRestrictionsManager.getCurrentCarUxRestrictions());
+ } catch (CarNotConnectedException | NullPointerException e) {
+ Log.e(TAG, "Car not connected", e);
+ // mCarUxRestrictions will be the default
+ }
+ }
+
+ @NonNull
+ private static CarUxRestrictions getDefaultRestrictions() {
+ return new CarUxRestrictions.Builder(
+ true, CarUxRestrictions.UX_RESTRICTIONS_FULLY_RESTRICTED, 0)
+ .build();
+ }
+
+ /** Listener interface used to update clients on UxRestrictions changes */
+ public interface OnUxRestrictionsChangedListener {
+ /** Called when CarUxRestrictions changes */
+ void onRestrictionsChanged(@NonNull CarUxRestrictions carUxRestrictions);
+ }
+
+ /** Returns the singleton sInstance of this class */
+ @NonNull
+ public static CarUxRestrictionsUtil getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new CarUxRestrictionsUtil(context);
+ }
+
+ return sInstance;
+ }
+
+ /**
+ * Registers a listener on this class for updates to CarUxRestrictions. Multiple listeners may
+ * be
+ * registered.
+ */
+ public void register(OnUxRestrictionsChangedListener listener) {
+ mObservers.add(listener);
+ listener.onRestrictionsChanged(mCarUxRestrictions);
+ }
+
+ /** Unregisters a registered listener */
+ public void unregister(OnUxRestrictionsChangedListener listener) {
+ mObservers.remove(listener);
+ }
+
+ /**
+ * Returns whether any of the given flags is blocked by the current restrictions. If null is
+ * given, the method returns true for safety.
+ */
+ public static boolean isRestricted(
+ @CarUxRestrictionsInfo int restrictionFlags, @Nullable CarUxRestrictions uxr) {
+ return (uxr == null) || ((uxr.getActiveRestrictions() & restrictionFlags) != 0);
+ }
+
+ /**
+ * Complies the input string with the given UX restrictions. Returns the original string if
+ * already compliant, otherwise a shortened ellipsized string.
+ */
+ public static String complyString(Context context, String str, CarUxRestrictions uxr) {
+
+ if (isRestricted(UX_RESTRICTIONS_LIMIT_STRING_LENGTH, uxr)) {
+ int maxLength =
+ uxr == null
+ ? context.getResources().getInteger(
+ R.integer.chassis_default_max_string_length)
+ : uxr.getMaxRestrictedStringLength();
+
+ if (str.length() > maxLength) {
+ return str.substring(0, maxLength) + context.getString(R.string.chassis_ellipsis);
+ }
+ }
+
+ return str;
+ }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerView.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerView.java
new file mode 100644
index 0000000..7f76e16
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerView.java
@@ -0,0 +1,811 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.car.drivingstate.CarUxRestrictions;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.chassis.R;
+import com.android.car.chassis.pagedrecyclerview.decorations.grid.GridDividerItemDecoration;
+import com.android.car.chassis.pagedrecyclerview.decorations.grid.GridOffsetItemDecoration;
+import com.android.car.chassis.pagedrecyclerview.decorations.linear.LinearDividerItemDecoration;
+import com.android.car.chassis.pagedrecyclerview.decorations.linear.LinearOffsetItemDecoration;
+import com.android.car.chassis.pagedrecyclerview.decorations.linear.LinearOffsetItemDecoration.OffsetPosition;
+
+import java.lang.annotation.Retention;
+
+/**
+ * View that extends a {@link RecyclerView} and creates a nested {@code RecyclerView} which could
+ * potentially include a scrollbar that has page up and down arrows. Interaction with this view is
+ * similar to a {@code RecyclerView} as it takes the same adapter and the layout manager.
+ */
+public final class PagedRecyclerView extends RecyclerView {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "PagedRecyclerView";
+
+ private final CarUxRestrictionsUtil mCarUxRestrictionsUtil;
+ private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener;
+
+ private boolean mScrollBarEnabled;
+ private int mScrollBarContainerWidth;
+ @ScrollBarPosition
+ private int mScrollBarPosition;
+ private boolean mScrollBarAboveRecyclerView;
+ private String mScrollBarClass;
+ private boolean mFullyInitialized;
+ private float mScrollBarPaddingStart;
+ private float mScrollBarPaddingEnd;
+ private Context mContext;
+
+ @Gutter
+ private int mGutter;
+ private int mGutterSize;
+ private RecyclerView mNestedRecyclerView;
+ private Adapter<?> mAdapter;
+ private ScrollBar mScrollBar;
+
+ private GridOffsetItemDecoration mOffsetItemDecoration;
+ private GridDividerItemDecoration mDividerItemDecoration;
+ @PagedRecyclerViewLayout
+ int mPagedRecyclerViewLayout;
+ private int mNumOfColumns;
+
+ /**
+ * The possible values for @{link #setGutter}. The default value is actually {@link
+ * PagedRecyclerView.Gutter#BOTH}.
+ */
+ @IntDef({
+ Gutter.NONE,
+ Gutter.START,
+ Gutter.END,
+ Gutter.BOTH,
+ })
+ @Retention(SOURCE)
+ public @interface Gutter {
+ /**
+ * No gutter on either side of the list items. The items will span the full width of the
+ * RecyclerView
+ */
+ int NONE = 0;
+
+ /** Include a gutter only on the start side (that is, the same side as the scroll bar). */
+ int START = 1;
+
+ /** Include a gutter only on the end side (that is, the opposite side of the scroll bar). */
+ int END = 2;
+
+ /** Include a gutter on both sides of the list items. This is the default behaviour. */
+ int BOTH = 3;
+ }
+
+ /**
+ * The possible values for setScrollbarPosition. The default value is actually {@link
+ * PagedRecyclerView.ScrollBarPosition#START}.
+ */
+ @IntDef({
+ ScrollBarPosition.START,
+ ScrollBarPosition.END,
+ })
+ @Retention(SOURCE)
+ public @interface ScrollBarPosition {
+ /** Position the scrollbar to the left of the screen. This is default. */
+ int START = 0;
+
+ /** Position scrollbar to the right of the screen. */
+ int END = 2;
+ }
+
+ /**
+ * The possible values for setScrollbarPosition. The default value is actually {@link
+ * PagedRecyclerViewLayout#LINEAR}.
+ */
+ @IntDef({
+ PagedRecyclerViewLayout.LINEAR,
+ PagedRecyclerViewLayout.GRID,
+ })
+ @Retention(SOURCE)
+ public @interface PagedRecyclerViewLayout {
+ /** Position the scrollbar to the left of the screen. This is default. */
+ int LINEAR = 0;
+
+ /** Position scrollbar to the right of the screen. */
+ int GRID = 2;
+ }
+
+ /**
+ * Interface for a {@link RecyclerView.Adapter} to cap the number of items.
+ *
+ * <p>NOTE: it is still up to the adapter to use maxItems in {@link
+ * RecyclerView.Adapter#getItemCount()}.
+ *
+ * <p>the recommended way would be with:
+ *
+ * <pre>{@code
+ * {@literal@}Override
+ * public int getItemCount() {
+ * return Math.min(super.getItemCount(), mMaxItems);
+ * }
+ * }</pre>
+ */
+ public interface ItemCap {
+ /** A value to pass to {@link #setMaxItems(int)} that indicates there should be no limit. */
+ int UNLIMITED = -1;
+
+ /**
+ * Sets the maximum number of items available in the adapter. A value less than '0' means
+ * the
+ * list should not be capped.
+ */
+ void setMaxItems(int maxItems);
+ }
+
+ /**
+ * Custom layout manager for the outer recyclerview. Since paddings should be applied by the
+ * inner
+ * recycler view within its bounds, this layout manager should always have 0 padding.
+ */
+ private static class PagedRecyclerViewLayoutManager extends LinearLayoutManager {
+ PagedRecyclerViewLayoutManager(Context context) {
+ super(context);
+ }
+
+ @Override
+ public int getPaddingTop() {
+ return 0;
+ }
+
+ @Override
+ public int getPaddingBottom() {
+ return 0;
+ }
+
+ @Override
+ public int getPaddingStart() {
+ return 0;
+ }
+
+ @Override
+ public int getPaddingEnd() {
+ return 0;
+ }
+
+ @Override
+ public boolean canScrollHorizontally() {
+ return false;
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return false;
+ }
+ }
+
+ /**
+ * Custom layout manager for the outer recyclerview. Since paddings should be applied by the
+ * inner
+ * recycler view within its bounds, this layout manager should always have 0 padding.
+ */
+ private static class GridPagedRecyclerViewLayoutManager extends GridLayoutManager {
+ GridPagedRecyclerViewLayoutManager(Context context, int numOfColumns) {
+ super(context, numOfColumns);
+ }
+
+ @Override
+ public int getPaddingTop() {
+ return 0;
+ }
+
+ @Override
+ public int getPaddingBottom() {
+ return 0;
+ }
+
+ @Override
+ public int getPaddingStart() {
+ return 0;
+ }
+
+ @Override
+ public int getPaddingEnd() {
+ return 0;
+ }
+ }
+
+ public PagedRecyclerView(@NonNull Context context) {
+ this(context, null, 0);
+ }
+
+ public PagedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PagedRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(context);
+ mListener = this::updateCarUxRestrictions;
+
+ init(context, attrs, defStyle);
+ }
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+ TypedArray a =
+ context.obtainStyledAttributes(
+ attrs, R.styleable.PagedRecyclerView, defStyleAttr,
+ R.style.PagedRecyclerView);
+
+ mScrollBarEnabled = context.getResources().getBoolean(R.bool.chassis_scrollbar_enable);
+ mFullyInitialized = false;
+
+ if (!mScrollBarEnabled) {
+ a.recycle();
+ mFullyInitialized = true;
+ return;
+ }
+
+ mNestedRecyclerView =
+ new RecyclerView(context, attrs, R.style.PagedRecyclerView_NestedRecyclerView);
+
+ mScrollBarPaddingStart =
+ context.getResources().getDimension(R.dimen.chassis_scrollbar_padding_start);
+ mScrollBarPaddingEnd =
+ context.getResources().getDimension(R.dimen.chassis_scrollbar_padding_end);
+
+ mPagedRecyclerViewLayout =
+ a.getInt(R.styleable.PagedRecyclerView_layoutStyle, PagedRecyclerViewLayout.LINEAR);
+ mNumOfColumns = a.getInt(R.styleable.PagedRecyclerView_numOfColumns, /* defValue= */ 2);
+ boolean enableDivider =
+ a.getBoolean(R.styleable.PagedRecyclerView_enableDivider, /* defValue= */ true);
+
+ if (mPagedRecyclerViewLayout == PagedRecyclerViewLayout.LINEAR) {
+
+ int linearTopOffset =
+ a.getInteger(R.styleable.PagedRecyclerView_startOffset, /* defValue= */ 0);
+ int linearBottomOffset =
+ a.getInteger(R.styleable.PagedRecyclerView_endOffset, /* defValue= */ 0);
+
+ if (enableDivider) {
+ RecyclerView.ItemDecoration dividerItemDecoration =
+ new LinearDividerItemDecoration(
+ context.getDrawable(R.drawable.chassis_pagedrecyclerview_divider));
+ super.addItemDecoration(dividerItemDecoration);
+ }
+ RecyclerView.ItemDecoration topOffsetItemDecoration =
+ new LinearOffsetItemDecoration(linearTopOffset, OffsetPosition.START);
+ super.addItemDecoration(topOffsetItemDecoration);
+
+ RecyclerView.ItemDecoration bottomOffsetItemDecoration =
+ new LinearOffsetItemDecoration(linearBottomOffset, OffsetPosition.END);
+ super.addItemDecoration(bottomOffsetItemDecoration);
+ } else {
+
+ int gridTopOffset =
+ a.getInteger(R.styleable.PagedRecyclerView_startOffset, /* defValue= */ 0);
+ int gridBottomOffset =
+ a.getInteger(R.styleable.PagedRecyclerView_endOffset, /* defValue= */ 0);
+
+ if (enableDivider) {
+ mDividerItemDecoration =
+ new GridDividerItemDecoration(
+ context.getDrawable(R.drawable.divider),
+ context.getDrawable(R.drawable.divider),
+ mNumOfColumns);
+ super.addItemDecoration(mDividerItemDecoration);
+ }
+
+ mOffsetItemDecoration =
+ new GridOffsetItemDecoration(gridTopOffset, mNumOfColumns,
+ OffsetPosition.START);
+ super.addItemDecoration(mOffsetItemDecoration);
+
+ GridOffsetItemDecoration bottomOffsetItemDecoration =
+ new GridOffsetItemDecoration(gridBottomOffset, mNumOfColumns,
+ OffsetPosition.END);
+ super.addItemDecoration(bottomOffsetItemDecoration);
+ }
+
+ super.setLayoutManager(new PagedRecyclerViewLayoutManager(context));
+ super.setAdapter(new PagedRecyclerViewAdapter());
+ super.setNestedScrollingEnabled(false);
+ super.setClipToPadding(false);
+
+ // Gutter
+ mGutter = context.getResources().getInteger(R.integer.chassis_scrollbar_gutter);
+ mGutterSize = getResources().getDimensionPixelSize(R.dimen.chassis_scrollbar_margin);
+
+ mScrollBarContainerWidth =
+ (int) context.getResources().getDimension(
+ R.dimen.chassis_scrollbar_container_width);
+
+ mScrollBarPosition = context.getResources().getInteger(
+ R.integer.chassis_scrollbar_position);
+
+ mScrollBarAboveRecyclerView =
+ context.getResources().getBoolean(R.bool.chassis_scrollbar_above_recycler_view);
+ mScrollBarClass = context.getResources().getString(R.string.chassis_scrollbar_component);
+ a.recycle();
+ this.mContext = context;
+ // Apply inner RV layout changes after the layout has been calculated for this view.
+ this.getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ // View holder layout is still pending.
+ if (PagedRecyclerView.this.findViewHolderForAdapterPosition(0)
+ == null) {
+ return;
+ }
+
+ PagedRecyclerView.this.getViewTreeObserver()
+ .removeOnGlobalLayoutListener(this);
+ initNestedRecyclerView();
+ setNestedViewLayout();
+
+ mNestedRecyclerView
+ .getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mNestedRecyclerView
+ .getViewTreeObserver()
+ .removeOnGlobalLayoutListener(this);
+ ViewGroup.LayoutParams params =
+ getLayoutParams();
+ params.height = getMeasuredHeight();
+ setLayoutParams(params);
+ createScrollBarFromConfig();
+ mFullyInitialized = true;
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Returns {@code true} if the {@PagedRecyclerView} is fully drawn. Using a global layout
+ * mListener
+ * may not necessarily signify that this view is fully drawn (i.e. when the scrollbar is
+ * enabled).
+ * This is because the inner views (scrollbar and inner recycler view) are drawn after the
+ * outer
+ * views are finished.
+ */
+ public boolean fullyInitialized() {
+ return mFullyInitialized;
+ }
+
+ /** Sets the number of columns in which grid needs to be divided. */
+ public void setNumOfColumns(int numberOfColumns) {
+ mNumOfColumns = numberOfColumns;
+ mOffsetItemDecoration.setNumOfColumns(mNumOfColumns);
+ mDividerItemDecoration.setNumOfColumns(mNumOfColumns);
+ }
+
+ /**
+ * Returns the {@link LayoutManager} for the {@link RecyclerView} displaying the content.
+ *
+ * <p>In cases where the scroll bar is visible and the nested {@link RecyclerView} is displaying
+ * content, {@link #getLayoutManager()} cannot be used because it returns the {@link
+ * LayoutManager} of the outer {@link RecyclerView}. {@link #getLayoutManager()} could not be
+ * overridden to return the effective manager due to interference with accessibility node tree
+ * traversal.
+ */
+ @Nullable
+ public LayoutManager getEffectiveLayoutManager() {
+ if (mScrollBarEnabled) {
+ return mNestedRecyclerView.getLayoutManager();
+ }
+ return super.getLayoutManager();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mCarUxRestrictionsUtil.register(mListener);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mCarUxRestrictionsUtil.unregister(mListener);
+ }
+
+ private void updateCarUxRestrictions(CarUxRestrictions carUxRestrictions) {
+ // If the adapter does not implement ItemCap, then the max items on it cannot be updated.
+ if (!(mAdapter instanceof ItemCap)) {
+ return;
+ }
+
+ int maxItems = ItemCap.UNLIMITED;
+ if ((carUxRestrictions.getActiveRestrictions()
+ & CarUxRestrictions.UX_RESTRICTIONS_LIMIT_CONTENT)
+ != 0) {
+ maxItems = carUxRestrictions.getMaxCumulativeContentItems();
+ }
+
+ int originalCount = mAdapter.getItemCount();
+ ((ItemCap) mAdapter).setMaxItems(maxItems);
+ int newCount = mAdapter.getItemCount();
+
+ if (newCount == originalCount) {
+ return;
+ }
+
+ if (newCount < originalCount) {
+ mAdapter.notifyItemRangeRemoved(newCount, originalCount - newCount);
+ } else {
+ mAdapter.notifyItemRangeInserted(originalCount, newCount - originalCount);
+ }
+ }
+
+ @Override
+ public void setClipToPadding(boolean clipToPadding) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.setClipToPadding(clipToPadding);
+ } else {
+ super.setClipToPadding(clipToPadding);
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public void setAdapter(@Nullable Adapter adapter) {
+
+ if (mPagedRecyclerViewLayout == PagedRecyclerViewLayout.LINEAR) {
+ mNestedRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
+ } else {
+ mNestedRecyclerView.setLayoutManager(
+ new GridPagedRecyclerViewLayoutManager(mContext, mNumOfColumns));
+ setNumOfColumns(mNumOfColumns);
+ }
+
+ this.mAdapter = adapter;
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.setAdapter(adapter);
+ } else {
+ super.setAdapter(adapter);
+ }
+ }
+
+ @Nullable
+ @Override
+ public Adapter<?> getAdapter() {
+ if (mScrollBarEnabled) {
+ return mNestedRecyclerView.getAdapter();
+ }
+ return super.getAdapter();
+ }
+
+ @Override
+ public void setLayoutManager(@Nullable LayoutManager layout) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.setLayoutManager(layout);
+ } else {
+ super.setLayoutManager(layout);
+ }
+ }
+
+ @Override
+ public void setOnScrollChangeListener(OnScrollChangeListener l) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.setOnScrollChangeListener(l);
+ } else {
+ super.setOnScrollChangeListener(l);
+ }
+ }
+
+ @Override
+ public void setVerticalFadingEdgeEnabled(boolean verticalFadingEdgeEnabled) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
+ } else {
+ super.setVerticalFadingEdgeEnabled(verticalFadingEdgeEnabled);
+ }
+ }
+
+ @Override
+ public void setFadingEdgeLength(int length) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.setFadingEdgeLength(length);
+ } else {
+ super.setFadingEdgeLength(length);
+ }
+ }
+
+ @Override
+ public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.addItemDecoration(decor, index);
+ } else {
+ super.addItemDecoration(decor, index);
+ }
+ }
+
+ @Override
+ public void addItemDecoration(@NonNull ItemDecoration decor) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.addItemDecoration(decor);
+ } else {
+ super.addItemDecoration(decor);
+ }
+ }
+
+ @Override
+ public void setItemAnimator(@Nullable ItemAnimator animator) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.setItemAnimator(animator);
+ } else {
+ super.setItemAnimator(animator);
+ }
+ }
+
+ @Override
+ public void setPadding(int left, int top, int right, int bottom) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.setPadding(left, top, right, bottom);
+ if (mScrollBar != null) {
+ mScrollBar.requestLayout();
+ }
+ } else {
+ super.setPadding(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public void setPaddingRelative(int start, int top, int end, int bottom) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.setPaddingRelative(start, top, end, bottom);
+ if (mScrollBar != null) {
+ mScrollBar.requestLayout();
+ }
+ } else {
+ super.setPaddingRelative(start, top, end, bottom);
+ }
+ }
+
+ @Override
+ public ViewHolder findViewHolderForLayoutPosition(int position) {
+ if (mScrollBarEnabled) {
+ return mNestedRecyclerView.findViewHolderForLayoutPosition(position);
+ } else {
+ return super.findViewHolderForLayoutPosition(position);
+ }
+ }
+
+ @Override
+ public ViewHolder findContainingViewHolder(View view) {
+ if (mScrollBarEnabled) {
+ return mNestedRecyclerView.findContainingViewHolder(view);
+ } else {
+ return super.findContainingViewHolder(view);
+ }
+ }
+
+ @Override
+ @Nullable
+ public View findChildViewUnder(float x, float y) {
+ if (mScrollBarEnabled) {
+ return mNestedRecyclerView.findChildViewUnder(x, y);
+ } else {
+ return super.findChildViewUnder(x, y);
+ }
+ }
+
+ @Override
+ public void addOnScrollListener(@NonNull OnScrollListener listener) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.addOnScrollListener(listener);
+ } else {
+ super.addOnScrollListener(listener);
+ }
+ }
+
+ @Override
+ public void removeOnScrollListener(@NonNull OnScrollListener listener) {
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.removeOnScrollListener(listener);
+ } else {
+ super.removeOnScrollListener(listener);
+ }
+ }
+
+ @Override
+ public int getPaddingStart() {
+ return mScrollBarEnabled ? mNestedRecyclerView.getPaddingStart() : super.getPaddingStart();
+ }
+
+ @Override
+ public int getPaddingEnd() {
+ return mScrollBarEnabled ? mNestedRecyclerView.getPaddingEnd() : super.getPaddingEnd();
+ }
+
+ @Override
+ public int getPaddingTop() {
+ return mScrollBarEnabled ? mNestedRecyclerView.getPaddingTop() : super.getPaddingTop();
+ }
+
+ @Override
+ public int getPaddingBottom() {
+ return mScrollBarEnabled ? mNestedRecyclerView.getPaddingBottom()
+ : super.getPaddingBottom();
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.setVisibility(visibility);
+ }
+ }
+
+ private void initNestedRecyclerView() {
+ PagedRecyclerViewAdapter.NestedRowViewHolder vh =
+ (PagedRecyclerViewAdapter.NestedRowViewHolder)
+ this.findViewHolderForAdapterPosition(0);
+ if (vh == null) {
+ throw new Error("Outer RecyclerView failed to initialize.");
+ }
+
+ vh.frameLayout.addView(mNestedRecyclerView);
+ }
+
+ private void createScrollBarFromConfig() {
+ if (DEBUG) {
+ Log.d(TAG, "createScrollBarFromConfig");
+ }
+
+ Class<?> cls;
+ try {
+ cls = getContext().getClassLoader().loadClass(mScrollBarClass);
+ } catch (Throwable t) {
+ throw andLog("Error loading scroll bar component: " + mScrollBarClass, t);
+ }
+ try {
+ mScrollBar = (ScrollBar) cls.getDeclaredConstructor().newInstance();
+ } catch (Throwable t) {
+ throw andLog("Error creating scroll bar component: " + mScrollBarClass, t);
+ }
+
+ mScrollBar.initialize(
+ mNestedRecyclerView, mScrollBarContainerWidth, mScrollBarPosition,
+ mScrollBarAboveRecyclerView);
+
+ mScrollBar.setPadding((int) mScrollBarPaddingStart, (int) mScrollBarPaddingEnd);
+
+ if (DEBUG) {
+ Log.d(TAG, "started " + mScrollBar.getClass().getSimpleName());
+ }
+ }
+
+ /**
+ * Set the nested view's layout to the specified value.
+ *
+ * <p>The mGutter is the space to the start/end of the list view items and will be equal in size
+ * to
+ * the scroll bars. By default, there is a mGutter to both the left and right of the list view
+ * items, to account for the scroll bar.
+ */
+ private void setNestedViewLayout() {
+ int startMargin = 0;
+ int endMargin = 0;
+ if ((mGutter & Gutter.START) != 0) {
+ startMargin = mGutterSize;
+ }
+ if ((mGutter & Gutter.END) != 0) {
+ endMargin = mGutterSize;
+ }
+
+ MarginLayoutParams layoutParams =
+ (MarginLayoutParams) mNestedRecyclerView.getLayoutParams();
+
+ layoutParams.setMarginStart(startMargin);
+ layoutParams.setMarginEnd(endMargin);
+
+ layoutParams.height = LayoutParams.MATCH_PARENT;
+ layoutParams.width = super.getLayoutManager().getWidth() - startMargin - endMargin;
+ // requestLayout() isn't sufficient because we also need to resolveLayoutParams().
+ mNestedRecyclerView.setLayoutParams(layoutParams);
+
+ // If there's a mGutter, set ClipToPadding to false so that CardView's shadow will still
+ // appear outside of the padding.
+ mNestedRecyclerView.setClipToPadding(startMargin == 0 && endMargin == 0);
+ }
+
+ private static RuntimeException andLog(String msg, Throwable t) {
+ Log.e(TAG, msg, t);
+ throw new RuntimeException(msg, t);
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ Parcelable superState = super.onSaveInstanceState();
+ SavedState ss = new SavedState(superState, getContext());
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.saveHierarchyState(ss.mNestedRecyclerViewState);
+ }
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ if (!(state instanceof SavedState)) {
+ Log.w(TAG, "onRestoreInstanceState called with an unsupported state");
+ super.onRestoreInstanceState(state);
+ } else {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ if (mScrollBarEnabled) {
+ mNestedRecyclerView.restoreHierarchyState(ss.mNestedRecyclerViewState);
+ }
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ SparseArray<Parcelable> mNestedRecyclerViewState;
+ Context mContext;
+
+ SavedState(Parcelable superState, Context c) {
+ super(superState);
+ mContext = c;
+ mNestedRecyclerViewState = new SparseArray<>();
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ mNestedRecyclerViewState = in.readSparseArray(mContext.getClassLoader());
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeSparseArray(mNestedRecyclerViewState);
+ }
+
+ 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/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerViewAdapter.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerViewAdapter.java
new file mode 100644
index 0000000..f3167ea
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedRecyclerViewAdapter.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.chassis.R;
+
+/** The adapter for the parent recyclerview in {@link PagedRecyclerView} widget. */
+final class PagedRecyclerViewAdapter
+ extends RecyclerView.Adapter<PagedRecyclerViewAdapter.NestedRowViewHolder> {
+
+ @Override
+ public PagedRecyclerViewAdapter.NestedRowViewHolder onCreateViewHolder(
+ ViewGroup parent, int viewType) {
+ View v =
+ LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.chassis_paged_recycler_view_item, parent, false);
+ return new NestedRowViewHolder(v);
+ }
+
+ // Replace the contents of a view (invoked by the layout manager). Intentionally left empty
+ // since this adapter is an empty shell for the nested recyclerview.
+ @Override
+ public void onBindViewHolder(NestedRowViewHolder holder, int position) {
+ }
+
+ // Return the size of your dataset (invoked by the layout manager)
+ @Override
+ public int getItemCount() {
+ return 1;
+ }
+
+ /** The viewholder class for the parent recyclerview. */
+ static class NestedRowViewHolder extends RecyclerView.ViewHolder {
+ public FrameLayout frameLayout;
+
+ NestedRowViewHolder(View view) {
+ super(view);
+ frameLayout = view.findViewById(R.id.nested_recycler_view_layout);
+ }
+ }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSmoothScroller.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSmoothScroller.java
new file mode 100644
index 0000000..910e370
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSmoothScroller.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview;
+
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import androidx.recyclerview.widget.LinearSmoothScroller;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.chassis.R;
+
+/**
+ * Code drop from {androidx.car.widget.PagedSmoothScroller}
+ *
+ * <p>Custom {@link LinearSmoothScroller} that has:
+ *
+ * <ul>
+ * <li>Custom control over the speed of scrolls.
+ * <li>Scrolling that snaps to start of a child view.
+ * </ul>
+ */
+public class PagedSmoothScroller extends LinearSmoothScroller {
+ private float mMillisecondsPerInch;
+ private float mDecelerationTimeDivisor;
+
+ private Interpolator mInterpolator;
+
+ public PagedSmoothScroller(Context context) {
+ super(context);
+ init(context);
+ }
+
+ private void init(Context context) {
+ mMillisecondsPerInch =
+ context.getResources().getFloat(R.dimen.chassis_scrollbar_milliseconds_per_inch);
+ mDecelerationTimeDivisor =
+ context.getResources().getFloat(
+ R.dimen.chassis_scrollbar_deceleration_times_divisor);
+ mInterpolator =
+ new DecelerateInterpolator(
+ context
+ .getResources()
+ .getFloat(
+ R.dimen.chassis_scrollbar_decelerate_interpolator_factor));
+ }
+
+ @Override
+ protected int getVerticalSnapPreference() {
+ // Returning SNAP_TO_START will ensure that if the top (start) row is partially visible it
+ // will be scrolled downward (END) to make the row fully visible.
+ return SNAP_TO_START;
+ }
+
+ @Override
+ protected void onTargetFound(View targetView, RecyclerView.State state, Action action) {
+ int dy = calculateDyToMakeVisible(targetView, SNAP_TO_START);
+
+ if (dy == 0) {
+ return;
+ }
+
+ final int time = calculateTimeForDeceleration(dy);
+ if (time > 0) {
+ action.update(0, -dy, time, mInterpolator);
+ }
+ }
+
+ @Override
+ protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
+ return mMillisecondsPerInch / displayMetrics.densityDpi;
+ }
+
+ @Override
+ protected int calculateTimeForDeceleration(int dx) {
+ return (int) Math.ceil(calculateTimeForScrolling(dx) / mDecelerationTimeDivisor);
+ }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSnapHelper.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSnapHelper.java
new file mode 100644
index 0000000..0d31a76
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/PagedSnapHelper.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview;
+
+import android.content.Context;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.LinearSnapHelper;
+import androidx.recyclerview.widget.OrientationHelper;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerView.LayoutManager;
+
+/**
+ * Inspired by {@link androidx.car.widget.PagedSnapHelper}
+ *
+ * <p>Extension of a {@link LinearSnapHelper} that will snap to the start of the target child view
+ * to the start of the attached {@link RecyclerView}. The start of the view is defined as the top if
+ * the RecyclerView is scrolling vertically; it is defined as the left (or right if RTL) if the
+ * RecyclerView is scrolling horizontally.
+ */
+public class PagedSnapHelper extends LinearSnapHelper {
+
+ private final Context mContext;
+ private RecyclerView mRecyclerView;
+
+ public PagedSnapHelper(Context context) {
+ this.mContext = context;
+ }
+
+ // Orientation helpers are lazily created per LayoutManager.
+ @Nullable
+ private OrientationHelper mVerticalHelper;
+ @Nullable
+ private OrientationHelper mHorizontalHelper;
+
+ @Override
+ public int[] calculateDistanceToFinalSnap(
+ @NonNull LayoutManager layoutManager, @NonNull View targetView) {
+ int[] out = new int[2];
+ if (layoutManager.canScrollHorizontally()) {
+ out[0] = distanceToTopMargin(targetView, getHorizontalHelper(layoutManager));
+ } else {
+ out[0] = 0;
+ }
+
+ if (layoutManager.canScrollVertically()) {
+ out[1] = distanceToTopMargin(targetView, getVerticalHelper(layoutManager));
+ } else {
+ out[1] = 0;
+ }
+ return out;
+ }
+
+ @Override
+ public View findSnapView(LayoutManager layoutManager) {
+ OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
+
+ if (mRecyclerView.computeVerticalScrollRange() - mRecyclerView.computeVerticalScrollOffset()
+ <= orientationHelper.getTotalSpace()
+ + mRecyclerView.getPaddingTop()
+ + mRecyclerView.getPaddingBottom()) {
+ return null;
+ }
+
+ if (layoutManager.canScrollVertically()) {
+ return findTopView(layoutManager, getVerticalHelper(layoutManager));
+ } else if (layoutManager.canScrollHorizontally()) {
+ return findTopView(layoutManager, getHorizontalHelper(layoutManager));
+ }
+ return null;
+ }
+
+ private static int distanceToTopMargin(@NonNull View targetView, OrientationHelper helper) {
+ final int childTop = helper.getDecoratedStart(targetView);
+ final int containerTop = helper.getStartAfterPadding();
+ return childTop - containerTop;
+ }
+
+ /**
+ * Finds the view to snap to. The view to snap to is the child of the LayoutManager that is
+ * closest to the start of the RecyclerView. The "start" depends on if the LayoutManager is
+ * scrolling horizontally or vertically. If it is horizontally scrolling, then the start is the
+ * view on the left (right if RTL). Otherwise, it is the top-most view.
+ *
+ * @param layoutManager The current {@link RecyclerView.LayoutManager} for the attached
+ * RecyclerView.
+ * @return The View closest to the start of the RecyclerView.
+ */
+ private static View findTopView(LayoutManager layoutManager, OrientationHelper helper) {
+ int childCount = layoutManager.getChildCount();
+ if (childCount == 0) {
+ return null;
+ }
+
+ View closestChild = null;
+ int absClosest = Integer.MAX_VALUE;
+
+ for (int i = 0; i < childCount; i++) {
+ View child = layoutManager.getChildAt(i);
+ if (child == null) {
+ continue;
+ }
+ int absDistance = Math.abs(distanceToTopMargin(child, helper));
+
+ /** if child top is closer than previous closest, set it as closest */
+ if (absDistance < absClosest) {
+ absClosest = absDistance;
+ closestChild = child;
+ }
+ }
+ return closestChild;
+ }
+
+ /**
+ * Returns the percentage of the given view that is visible, relative to its containing
+ * RecyclerView.
+ *
+ * @param view The View to get the percentage visible of.
+ * @param helper An {@link OrientationHelper} to aid with calculation.
+ * @return A float indicating the percentage of the given view that is visible.
+ */
+ private static float getPercentageVisible(View view, OrientationHelper helper) {
+
+ int start = helper.getStartAfterPadding();
+ int end = helper.getEndAfterPadding();
+
+ int viewHeight = helper.getDecoratedMeasurement(view);
+
+ int viewStart = helper.getDecoratedStart(view);
+ int viewEnd = helper.getDecoratedEnd(view);
+
+ if (viewEnd < start) {
+ // The is outside of the bounds of the recyclerView.
+ return 0f;
+ } else if (viewStart >= start && viewEnd <= end) {
+ // The view is within the bounds of the RecyclerView, so it's fully visible.
+ return 1.f;
+ } else if (viewStart <= start && viewEnd >= end) {
+ // The view is larger than the height of the RecyclerView.
+ return 1.f - ((float) (Math.abs(viewStart) + Math.abs(viewEnd)) / viewHeight);
+ } else if (viewStart < start) {
+ // The view is above the start of the RecyclerView, so subtract the start offset
+ // from the total height.
+ return 1.f - ((float) Math.abs(viewStart) / helper.getDecoratedMeasurement(view));
+ } else {
+ // The view is below the end of the RecyclerView, so subtract the end offset from the
+ // total height.
+ return 1.f - ((float) Math.abs(viewEnd) / helper.getDecoratedMeasurement(view));
+ }
+ }
+
+ @Override
+ public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
+ this.mRecyclerView = recyclerView;
+ super.attachToRecyclerView(recyclerView);
+ }
+
+ /**
+ * Returns a scroller specific to this {@code PagedSnapHelper}. This scroller is used for all
+ * smooth scrolling operations, including flings.
+ *
+ * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+ * {@link
+ * RecyclerView}.
+ * @return a {@link RecyclerView.SmoothScroller} which will handle the scrolling.
+ */
+ @Override
+ protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
+ return new PagedSmoothScroller(mContext);
+ }
+
+ /**
+ * Calculate the estimated scroll distance in each direction given velocities on both axes. This
+ * method will clamp the maximum scroll distance so that a single fling will never scroll more
+ * than one page.
+ *
+ * @param velocityX Fling velocity on the horizontal axis.
+ * @param velocityY Fling velocity on the vertical axis.
+ * @return An array holding the calculated distances in x and y directions respectively.
+ */
+ @Override
+ public int[] calculateScrollDistance(int velocityX, int velocityY) {
+ int[] outDist = super.calculateScrollDistance(velocityX, velocityY);
+
+ if (mRecyclerView == null) {
+ return outDist;
+ }
+
+ RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+ if (layoutManager == null || layoutManager.getChildCount() == 0) {
+ return outDist;
+ }
+
+ int lastChildPosition = isAtEnd(layoutManager) ? 0 : layoutManager.getChildCount() - 1;
+
+ OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
+ View lastChild = layoutManager.getChildAt(lastChildPosition);
+ float percentageVisible = getPercentageVisible(lastChild, orientationHelper);
+
+ int maxDistance = layoutManager.getHeight();
+ if (percentageVisible > 0.f) {
+ // The max and min distance is the total height of the RecyclerView minus the height of
+ // the last child. This ensures that each scroll will never scroll more than a single
+ // page on the RecyclerView. That is, the max scroll will make the last child the
+ // first child and vice versa when scrolling the opposite way.
+ maxDistance -= layoutManager.getDecoratedMeasuredHeight(lastChild);
+ }
+
+ int minDistance = -maxDistance;
+
+ outDist[0] = clamp(outDist[0], minDistance, maxDistance);
+ outDist[1] = clamp(outDist[1], minDistance, maxDistance);
+
+ return outDist;
+ }
+
+ /** Returns {@code true} if the RecyclerView is completely displaying the first item. */
+ boolean isAtStart(RecyclerView.LayoutManager layoutManager) {
+ if (layoutManager == null || layoutManager.getChildCount() == 0) {
+ return true;
+ }
+
+ View firstChild = layoutManager.getChildAt(0);
+ OrientationHelper orientationHelper =
+ layoutManager.canScrollVertically()
+ ? getVerticalHelper(layoutManager)
+ : getHorizontalHelper(layoutManager);
+
+ // Check that the first child is completely visible and is the first item in the list.
+ return orientationHelper.getDecoratedStart(firstChild)
+ >= orientationHelper.getStartAfterPadding()
+ && layoutManager.getPosition(firstChild) == 0;
+ }
+
+ /** Returns {@code true} if the RecyclerView is completely displaying the last item. */
+ public boolean isAtEnd(RecyclerView.LayoutManager layoutManager) {
+ if (layoutManager == null || layoutManager.getChildCount() == 0) {
+ return true;
+ }
+
+ int childCount = layoutManager.getChildCount();
+ OrientationHelper orientationHelper =
+ layoutManager.canScrollVertically()
+ ? getVerticalHelper(layoutManager)
+ : getHorizontalHelper(layoutManager);
+
+ View lastVisibleChild = layoutManager.getChildAt(childCount - 1);
+
+ // The list has reached the bottom if the last child that is visible is the last item
+ // in the list and it's fully shown.
+ return layoutManager.getPosition(lastVisibleChild) == (layoutManager.getItemCount() - 1)
+ && layoutManager.getDecoratedBottom(lastVisibleChild)
+ <= orientationHelper.getEndAfterPadding();
+ }
+
+ /**
+ * Returns an {@link OrientationHelper} that corresponds to the current scroll direction of the
+ * given {@link RecyclerView.LayoutManager}.
+ */
+ @NonNull
+ private OrientationHelper getOrientationHelper(
+ @NonNull RecyclerView.LayoutManager layoutManager) {
+ return layoutManager.canScrollVertically()
+ ? getVerticalHelper(layoutManager)
+ : getHorizontalHelper(layoutManager);
+ }
+
+ @NonNull
+ private OrientationHelper getVerticalHelper(@NonNull RecyclerView.LayoutManager layoutManager) {
+ if (mVerticalHelper == null || mVerticalHelper.getLayoutManager() != layoutManager) {
+ mVerticalHelper = OrientationHelper.createVerticalHelper(layoutManager);
+ }
+ return mVerticalHelper;
+ }
+
+ @NonNull
+ private OrientationHelper getHorizontalHelper(
+ @NonNull RecyclerView.LayoutManager layoutManager) {
+ if (mHorizontalHelper == null || mHorizontalHelper.getLayoutManager() != layoutManager) {
+ mHorizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
+ }
+ return mHorizontalHelper;
+ }
+
+ /**
+ * Ensures that the given value falls between the range given by the min and max values. This
+ * method does not check that the min value is greater than or equal to the max value. If the
+ * parameters are not well-formed, this method's behavior is undefined.
+ *
+ * @param value The value to clamp.
+ * @param min The minimum value the given value can be.
+ * @param max The maximum value the given value can be.
+ * @return A number that falls between {@code min} or {@code max} or one of those values if the
+ * given value is less than or greater than {@code min} and {@code max} respectively.
+ */
+ private static int clamp(int value, int min, int max) {
+ return Math.max(min, Math.min(max, value));
+ }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/ScrollBar.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/ScrollBar.java
new file mode 100644
index 0000000..40a673b
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/ScrollBar.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.car.chassis.pagedrecyclerview.PagedRecyclerView.ScrollBarPosition;
+
+/**
+ * An abstract class that defines required contract for a custom scroll bar for the {@link
+ * PagedRecyclerView}. All custom scroll bar must inherit from this class.
+ */
+public interface ScrollBar {
+ /**
+ * The concrete class should implement this method to initialize configuration of a scrollbar
+ * view.
+ */
+ void initialize(
+ RecyclerView recyclerView,
+ int scrollBarContainerWidth,
+ @ScrollBarPosition int scrollBarPosition,
+ boolean scrollBarAboveRecyclerView);
+
+ /**
+ * Requests layout of the scrollbar. Should be called when there's been a change that will
+ * affect
+ * the size of the scrollbar view.
+ */
+ void requestLayout();
+
+ /** Sets the padding of the scrollbar, relative to the padding of the RecyclerView. */
+ void setPadding(int padddingStart, int paddingEnd);
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridDividerItemDecoration.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridDividerItemDecoration.java
new file mode 100644
index 0000000..adf9f28
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridDividerItemDecoration.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview.decorations.grid;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/** Adds interior dividers to a RecyclerView with a GridLayoutManager. */
+public class GridDividerItemDecoration extends RecyclerView.ItemDecoration {
+
+ private final Drawable mHorizontalDivider;
+ private final Drawable mVerticalDivider;
+ private int mNumColumns;
+
+ /**
+ * Sole constructor. Takes in {@link Drawable} objects to be used as horizontal and vertical
+ * dividers.
+ *
+ * @param horizontalDivider A divider {@code Drawable} to be drawn on the rows of the grid of
+ * the
+ * RecyclerView
+ * @param verticalDivider A divider {@code Drawable} to be drawn on the columns of the grid of
+ * the
+ * RecyclerView
+ * @param numColumns The number of columns in the grid of the RecyclerView
+ */
+ public GridDividerItemDecoration(
+ Drawable horizontalDivider, Drawable verticalDivider, int numColumns) {
+ this.mHorizontalDivider = horizontalDivider;
+ this.mVerticalDivider = verticalDivider;
+ this.mNumColumns = numColumns;
+ }
+
+ /**
+ * Draws horizontal and/or vertical dividers onto the parent RecyclerView.
+ *
+ * @param canvas The {@link Canvas} onto which dividers will be drawn
+ * @param parent The RecyclerView onto which dividers are being added
+ * @param state The current RecyclerView.State of the RecyclerView
+ */
+ @Override
+ public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
+ drawVerticalDividers(canvas, parent);
+ drawHorizontalDividers(canvas, parent);
+ }
+
+ /**
+ * Determines the size and location of offsets between items in the parent RecyclerView.
+ *
+ * @param outRect The {@link Rect} of offsets to be added around the child view
+ * @param view The child view to be decorated with an offset
+ * @param parent The RecyclerView onto which dividers are being added
+ * @param state The current RecyclerView.State of the RecyclerView
+ */
+ @Override
+ public void getItemOffsets(
+ Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ outRect.set(
+ 0, 0, mHorizontalDivider.getIntrinsicWidth(),
+ mHorizontalDivider.getIntrinsicHeight());
+ }
+
+ /**
+ * Adds horizontal dividers to a RecyclerView with a GridLayoutManager or its subclass.
+ *
+ * @param canvas The {@link Canvas} onto which dividers will be drawn
+ * @param parent The RecyclerView onto which dividers are being added
+ */
+ private void drawHorizontalDividers(Canvas canvas, RecyclerView parent) {
+ int childCount = parent.getChildCount();
+ int rowCount = childCount / mNumColumns;
+ int lastRowChildCount = childCount % mNumColumns;
+
+ for (int i = 1; i < mNumColumns; i++) {
+ int lastRowChildIndex;
+ if (i < lastRowChildCount) {
+ lastRowChildIndex = i + (rowCount * mNumColumns);
+ } else {
+ lastRowChildIndex = i + ((rowCount - 1) * mNumColumns);
+ }
+
+ View firstRowChild = parent.getChildAt(i);
+ View lastRowChild = parent.getChildAt(lastRowChildIndex);
+
+ int dividerTop = firstRowChild.getTop();
+ int dividerRight = firstRowChild.getLeft();
+ int dividerLeft = dividerRight - mHorizontalDivider.getIntrinsicWidth();
+ int dividerBottom = lastRowChild.getBottom();
+
+ mHorizontalDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom);
+ mHorizontalDivider.draw(canvas);
+ }
+ }
+
+ public void setNumOfColumns(int numberOfColumns) {
+ mNumColumns = numberOfColumns;
+ }
+
+ /**
+ * Adds vertical dividers to a RecyclerView with a GridLayoutManager or its subclass.
+ *
+ * @param canvas The {@link Canvas} onto which dividers will be drawn
+ * @param parent The RecyclerView onto which dividers are being added
+ */
+ private void drawVerticalDividers(Canvas canvas, RecyclerView parent) {
+ double childCount = parent.getChildCount();
+ double rowCount = Math.ceil(childCount / mNumColumns);
+ int rightmostChildIndex;
+ for (int i = 1; i <= rowCount; i++) {
+ // we dont want the divider on top of first row.
+ if (i == 1) {
+ continue;
+ }
+ if (i == rowCount) {
+ rightmostChildIndex = ((i - 1) * mNumColumns) - 1;
+ } else {
+ rightmostChildIndex = (i * mNumColumns) - 1;
+ }
+
+ View leftmostChild = parent.getChildAt(mNumColumns * (i - 1));
+ View rightmostChild = parent.getChildAt(rightmostChildIndex);
+
+ // draws on top of each row.
+ int dividerLeft = leftmostChild.getLeft();
+ int dividerBottom = leftmostChild.getTop();
+ int dividerTop = dividerBottom - mVerticalDivider.getIntrinsicHeight();
+ int dividerRight = rightmostChild.getRight();
+
+ mVerticalDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom);
+ mVerticalDivider.draw(canvas);
+ }
+ }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridOffsetItemDecoration.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridOffsetItemDecoration.java
new file mode 100644
index 0000000..e290763
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/grid/GridOffsetItemDecoration.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview.decorations.grid;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.annotation.Retention;
+
+/** Adds an offset to the top of a RecyclerView with a GridLayoutManager or its subclass. */
+public class GridOffsetItemDecoration extends RecyclerView.ItemDecoration {
+
+ private int mOffsetPx;
+ private Drawable mOffsetDrawable;
+ private int mNumColumns;
+ @OffsetPosition
+ private final int mOffsetPosition;
+
+ /** The possible values for setScrollbarPosition. */
+ @IntDef({
+ OffsetPosition.START,
+ OffsetPosition.END,
+ })
+ @Retention(SOURCE)
+ public @interface OffsetPosition {
+ /** Position the offset to the start of the screen. */
+ int START = 0;
+
+ /** Position offset to the end of the screen. */
+ int END = 1;
+ }
+
+ /**
+ * Constructor that takes in the size of the offset to be added to the top of the RecyclerView.
+ *
+ * @param offsetPx The size of the offset to be added to the top of the RecyclerView in pixels
+ * @param numColumns The number of columns in the grid of the RecyclerView
+ * @param offsetPosition Position where offset needs to be applied.
+ */
+ public GridOffsetItemDecoration(int offsetPx, int numColumns, int offsetPosition) {
+ this.mOffsetPx = offsetPx;
+ this.mNumColumns = numColumns;
+ this.mOffsetPosition = offsetPosition;
+ }
+
+ /**
+ * Constructor that takes in a {@link Drawable} to be drawn at the top of the RecyclerView.
+ *
+ * @param offsetDrawable The {@code Drawable} to be added to the top of the RecyclerView
+ * @param numColumns The number of columns in the grid of the RecyclerView
+ */
+ public GridOffsetItemDecoration(Drawable offsetDrawable, int numColumns, int offsetPosition) {
+ this.mOffsetDrawable = offsetDrawable;
+ this.mNumColumns = numColumns;
+ this.mOffsetPosition = offsetPosition;
+ }
+
+ public void setNumOfColumns(int numberOfColumns) {
+ mNumColumns = numberOfColumns;
+ }
+
+ /**
+ * Determines the size and the location of the offset to be added to the top of the
+ * RecyclerView.
+ *
+ * @param outRect The {@link Rect} of offsets to be added around the child view
+ * @param view The child view to be decorated with an offset
+ * @param parent The RecyclerView onto which dividers are being added
+ * @param state The current RecyclerView.State of the RecyclerView
+ */
+ @Override
+ public void getItemOffsets(
+ Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+
+ if (mOffsetPosition == OffsetPosition.START) {
+ boolean childIsInTopRow = parent.getChildAdapterPosition(view) < mNumColumns;
+ if (childIsInTopRow) {
+ if (mOffsetPx > 0) {
+ outRect.top = mOffsetPx;
+ } else if (mOffsetDrawable != null) {
+ outRect.top = mOffsetDrawable.getIntrinsicHeight();
+ }
+ }
+ return;
+ }
+
+ int childCount = state.getItemCount();
+ int lastRowChildCount = getLastRowChildCount(childCount);
+
+ boolean childIsInBottomRow =
+ parent.getChildAdapterPosition(view) >= childCount - lastRowChildCount;
+ if (childIsInBottomRow) {
+ if (mOffsetPx > 0) {
+ outRect.bottom = mOffsetPx;
+ } else if (mOffsetDrawable != null) {
+ outRect.bottom = mOffsetDrawable.getIntrinsicHeight();
+ }
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ super.onDraw(c, parent, state);
+ if (mOffsetDrawable == null) {
+ return;
+ }
+
+ int parentLeft = parent.getPaddingLeft();
+ int parentRight = parent.getWidth() - parent.getPaddingRight();
+
+ if (mOffsetPosition == OffsetPosition.START) {
+
+ int parentTop = parent.getPaddingTop();
+ int offsetDrawableBottom = parentTop + mOffsetDrawable.getIntrinsicHeight();
+
+ mOffsetDrawable.setBounds(parentLeft, parentTop, parentRight, offsetDrawableBottom);
+ mOffsetDrawable.draw(c);
+ return;
+ }
+
+ int childCount = state.getItemCount();
+ int lastRowChildCount = getLastRowChildCount(childCount);
+
+ int offsetDrawableTop = 0;
+ int offsetDrawableBottom = 0;
+
+ for (int i = childCount - lastRowChildCount; i < childCount; i++) {
+ View child = parent.getChildAt(i);
+ offsetDrawableTop = child.getBottom();
+ offsetDrawableBottom = offsetDrawableTop + mOffsetDrawable.getIntrinsicHeight();
+ }
+
+ mOffsetDrawable.setBounds(parentLeft, offsetDrawableTop, parentRight, offsetDrawableBottom);
+ mOffsetDrawable.draw(c);
+ }
+
+ private int getLastRowChildCount(int itemCount) {
+ int lastRowChildCount = itemCount % mNumColumns;
+ if (lastRowChildCount == 0) {
+ lastRowChildCount = mNumColumns;
+ }
+
+ return lastRowChildCount;
+ }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearDividerItemDecoration.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearDividerItemDecoration.java
new file mode 100644
index 0000000..b007cd3
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearDividerItemDecoration.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview.decorations.linear;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+/** Adds interior dividers to a RecyclerView with a LinearLayoutManager or its subclass. */
+public class LinearDividerItemDecoration extends RecyclerView.ItemDecoration {
+
+ private final Drawable mDivider;
+ private int mOrientation;
+
+ /**
+ * Sole constructor. Takes in a {@link Drawable} to be used as the interior
+ * chassis_pagedrecyclerview_divider.
+ *
+ * @param divider A chassis_pagedrecyclerview_divider {@code Drawable} to be drawn on the
+ * RecyclerView
+ */
+ public LinearDividerItemDecoration(Drawable divider) {
+ this.mDivider = divider;
+ }
+
+ /**
+ * Draws horizontal or vertical dividers onto the parent RecyclerView.
+ *
+ * @param canvas The {@link Canvas} onto which dividers will be drawn
+ * @param parent The RecyclerView onto which dividers are being added
+ * @param state The current RecyclerView.State of the RecyclerView
+ */
+ @Override
+ public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
+ if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+ drawHorizontalDividers(canvas, parent);
+ } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+ drawVerticalDividers(canvas, parent);
+ }
+ }
+
+ /**
+ * Determines the size and location of offsets between items in the parent RecyclerView.
+ *
+ * @param outRect The {@link Rect} of offsets to be added around the child view
+ * @param view The child view to be decorated with an offset
+ * @param parent The RecyclerView onto which dividers are being added
+ * @param state The current RecyclerView.State of the RecyclerView
+ */
+ @Override
+ public void getItemOffsets(
+ Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+
+ if (parent.getChildAdapterPosition(view) == 0) {
+ return;
+ }
+
+ mOrientation = ((LinearLayoutManager) parent.getLayoutManager()).getOrientation();
+ if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+ outRect.left = mDivider.getIntrinsicWidth();
+ } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+ outRect.top = mDivider.getIntrinsicHeight();
+ }
+ }
+
+ /**
+ * Adds dividers to a RecyclerView with a LinearLayoutManager or its subclass oriented
+ * horizontally.
+ *
+ * @param canvas The {@link Canvas} onto which horizontal dividers will be drawn
+ * @param parent The RecyclerView onto which horizontal dividers are being added
+ */
+ private void drawHorizontalDividers(Canvas canvas, RecyclerView parent) {
+ int parentTop = parent.getPaddingTop();
+ int parentBottom = parent.getHeight() - parent.getPaddingBottom();
+
+ int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount - 1; i++) {
+ View child = parent.getChildAt(i);
+
+ RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
+
+ int parentLeft = child.getRight() + params.rightMargin;
+ int parentRight = parentLeft + mDivider.getIntrinsicWidth();
+
+ mDivider.setBounds(parentLeft, parentTop, parentRight, parentBottom);
+ mDivider.draw(canvas);
+ }
+ }
+
+ /**
+ * Adds dividers to a RecyclerView with a LinearLayoutManager or its subclass oriented
+ * vertically.
+ *
+ * @param canvas The {@link Canvas} onto which vertical dividers will be drawn
+ * @param parent The RecyclerView onto which vertical dividers are being added
+ */
+ private void drawVerticalDividers(Canvas canvas, RecyclerView parent) {
+ int parentLeft = parent.getPaddingLeft();
+ int parentRight = parent.getWidth() - parent.getPaddingRight();
+
+ int childCount = parent.getChildCount();
+ for (int i = 0; i < childCount - 1; i++) {
+ View child = parent.getChildAt(i);
+
+ RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
+
+ int parentTop = child.getBottom() + params.bottomMargin;
+ int parentBottom = parentTop + mDivider.getIntrinsicHeight();
+
+ mDivider.setBounds(parentLeft, parentTop, parentRight, parentBottom);
+ mDivider.draw(canvas);
+ }
+ }
+}
diff --git a/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearOffsetItemDecoration.java b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearOffsetItemDecoration.java
new file mode 100644
index 0000000..f011843
--- /dev/null
+++ b/car-chassis-lib/src/com/android/car/chassis/pagedrecyclerview/decorations/linear/LinearOffsetItemDecoration.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.pagedrecyclerview.decorations.linear;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+
+import androidx.annotation.IntDef;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Adds an offset to the start of a RecyclerView using a LinearLayoutManager or its subclass.
+ *
+ * <p>If the RecyclerView.LayoutManager is oriented vertically, the offset will be added to the top
+ * of the RecyclerView. If the LayoutManager is oriented horizontally, the offset will be added to
+ * the left of the RecyclerView.
+ */
+public class LinearOffsetItemDecoration extends RecyclerView.ItemDecoration {
+
+ private int mOffsetPx;
+ private Drawable mOffsetDrawable;
+ private int mOrientation;
+ @OffsetPosition
+ private int mOffsetPosition;
+
+ /** The possible values for setScrollbarPosition. */
+ @IntDef({
+ OffsetPosition.START,
+ OffsetPosition.END,
+ })
+ @Retention(SOURCE)
+ public @interface OffsetPosition {
+ /** Position the offset to the start of the screen. */
+ int START = 0;
+
+ /** Position offset to the end of the screen. */
+ int END = 1;
+ }
+
+ /**
+ * Constructor that takes in the size of the offset to be added to the start of the
+ * RecyclerView.
+ *
+ * @param offsetPx The size of the offset to be added to the start of the RecyclerView in pixels
+ * @param offsetPosition Position where offset needs to be applied.
+ */
+ public LinearOffsetItemDecoration(int offsetPx, int offsetPosition) {
+ this.mOffsetPx = offsetPx;
+ this.mOffsetPosition = offsetPosition;
+ }
+
+ /**
+ * Constructor that takes in a {@link Drawable} to be drawn at the start of the RecyclerView.
+ *
+ * @param offsetDrawable The {@code Drawable} to be added to the start of the RecyclerView
+ */
+ public LinearOffsetItemDecoration(Drawable offsetDrawable) {
+ this.mOffsetDrawable = offsetDrawable;
+ }
+
+ /**
+ * Determines the size and location of the offset to be added to the start of the RecyclerView.
+ *
+ * @param outRect The {@link Rect} of offsets to be added around the child view
+ * @param view The child view to be decorated with an offset
+ * @param parent The RecyclerView onto which dividers are being added
+ * @param state The current RecyclerView.State of the RecyclerView
+ */
+ @Override
+ public void getItemOffsets(
+ Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+
+ if (mOffsetPosition == OffsetPosition.START && parent.getChildAdapterPosition(view) > 0) {
+ return;
+ }
+
+ int itemCount = state.getItemCount();
+ if (mOffsetPosition == OffsetPosition.END
+ && parent.getChildAdapterPosition(view) != itemCount - 1) {
+ return;
+ }
+
+ mOrientation = ((LinearLayoutManager) parent.getLayoutManager()).getOrientation();
+ if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+ if (mOffsetPx > 0) {
+ if (mOffsetPosition == OffsetPosition.START) {
+ outRect.left = mOffsetPx;
+ } else {
+ outRect.right = mOffsetPx;
+ }
+ } else if (mOffsetDrawable != null) {
+ if (mOffsetPosition == OffsetPosition.START) {
+ outRect.left = mOffsetDrawable.getIntrinsicWidth();
+ } else {
+ outRect.right = mOffsetDrawable.getIntrinsicWidth();
+ }
+ }
+ } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+ if (mOffsetPx > 0) {
+ if (mOffsetPosition == OffsetPosition.START) {
+ outRect.top = mOffsetPx;
+ } else {
+ outRect.bottom = mOffsetPx;
+ }
+ } else if (mOffsetDrawable != null) {
+ if (mOffsetPosition == OffsetPosition.START) {
+ outRect.top = mOffsetDrawable.getIntrinsicHeight();
+ } else {
+ outRect.bottom = mOffsetDrawable.getIntrinsicHeight();
+ }
+ }
+ }
+ }
+
+ /**
+ * Draws horizontal or vertical offset onto the start of the parent RecyclerView.
+ *
+ * @param c The {@link Canvas} onto which an offset will be drawn
+ * @param parent The RecyclerView onto which an offset is being added
+ * @param state The current RecyclerView.State of the RecyclerView
+ */
+ @Override
+ public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+ super.onDraw(c, parent, state);
+ if (mOffsetDrawable == null) {
+ return;
+ }
+
+ if (mOrientation == LinearLayoutManager.HORIZONTAL) {
+ drawOffsetHorizontal(c, parent);
+ } else if (mOrientation == LinearLayoutManager.VERTICAL) {
+ drawOffsetVertical(c, parent);
+ }
+ }
+
+ private void drawOffsetHorizontal(Canvas canvas, RecyclerView parent) {
+ int parentTop = parent.getPaddingTop();
+ int parentBottom = parent.getHeight() - parent.getPaddingBottom();
+ int parentLeft = 0;
+ int offsetDrawableRight = 0;
+
+ if (mOffsetPosition == OffsetPosition.START) {
+ parentLeft = parent.getPaddingLeft();
+ offsetDrawableRight = parentLeft + mOffsetDrawable.getIntrinsicWidth();
+ } else {
+ View lastChild = parent.getChildAt(parent.getChildCount() - 1);
+ RecyclerView.LayoutParams lastChildLayoutParams =
+ (RecyclerView.LayoutParams) lastChild.getLayoutParams();
+ parentLeft = lastChild.getRight() + lastChildLayoutParams.rightMargin;
+ offsetDrawableRight = parentLeft + mOffsetDrawable.getIntrinsicWidth();
+ }
+
+ mOffsetDrawable.setBounds(parentLeft, parentTop, offsetDrawableRight, parentBottom);
+ mOffsetDrawable.draw(canvas);
+ }
+
+ private void drawOffsetVertical(Canvas canvas, RecyclerView parent) {
+ int parentLeft = parent.getPaddingLeft();
+ int parentRight = parent.getWidth() - parent.getPaddingRight();
+
+ int parentTop = 0;
+ int offsetDrawableBottom = 0;
+
+ if (mOffsetPosition == OffsetPosition.START) {
+ parentTop = parent.getPaddingTop();
+ offsetDrawableBottom = parentTop + mOffsetDrawable.getIntrinsicHeight();
+ } else {
+ View lastChild = parent.getChildAt(parent.getChildCount() - 1);
+ RecyclerView.LayoutParams lastChildLayoutParams =
+ (RecyclerView.LayoutParams) lastChild.getLayoutParams();
+ parentTop = lastChild.getBottom() + lastChildLayoutParams.bottomMargin;
+ offsetDrawableBottom = parentTop + mOffsetDrawable.getIntrinsicHeight();
+ }
+
+ mOffsetDrawable.setBounds(parentLeft, parentTop, parentRight, offsetDrawableBottom);
+ mOffsetDrawable.draw(canvas);
+ }
+}
diff --git a/car-chassis-lib/tests/Android.mk b/car-chassis-lib/tests/Android.mk
new file mode 100644
index 0000000..9f0a4e8
--- /dev/null
+++ b/car-chassis-lib/tests/Android.mk
@@ -0,0 +1,19 @@
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+# Include all makefiles in subdirectories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/car-chassis-lib/tests/paintbooth/Android.mk b/car-chassis-lib/tests/paintbooth/Android.mk
new file mode 100644
index 0000000..5683038
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/Android.mk
@@ -0,0 +1,46 @@
+#
+# Copyright (C) 2019 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+ifneq ($(TARGET_BUILD_PDK), true)
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+
+LOCAL_PACKAGE_NAME := PaintBooth
+
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_CERTIFICATE := platform
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_STATIC_ANDROID_LIBRARIES := \
+ car-chassis-lib
+
+LOCAL_USE_AAPT2 := true
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+LOCAL_DEX_PREOPT := false
+
+include $(BUILD_PACKAGE)
+
+endif
diff --git a/car-chassis-lib/tests/paintbooth/AndroidManifest-gradle.xml b/car-chassis-lib/tests/paintbooth/AndroidManifest-gradle.xml
new file mode 100644
index 0000000..a44ea36
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/AndroidManifest-gradle.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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="com.android.car.chassis.paintbooth">
+
+ <application
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/ChassisTheme">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".DialogSamples"
+ android:exported="false"
+ android:parentActivityName=".MainActivity"/>
+ </application>
+</manifest>
diff --git a/car-chassis-lib/tests/paintbooth/AndroidManifest.xml b/car-chassis-lib/tests/paintbooth/AndroidManifest.xml
new file mode 100644
index 0000000..097cf7e
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/AndroidManifest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT 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="com.android.car.chassis.paintbooth">
+
+ <uses-sdk
+ android:minSdkVersion="28"
+ android:targetSdkVersion="28"/>
+
+ <application
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:theme="@style/ChassisTheme">
+ <activity
+ android:name=".MainActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".DialogSamples"
+ android:exported="false"
+ android:parentActivityName=".MainActivity"/>
+ </application>
+</manifest>
diff --git a/car-chassis-lib/tests/paintbooth/build.gradle b/car-chassis-lib/tests/paintbooth/build.gradle
new file mode 100644
index 0000000..d56a776
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/build.gradle
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 28
+ defaultConfig {
+ applicationId "com.android.car.chassis.paintbooth"
+ minSdkVersion 28
+ targetSdkVersion 28
+ versionCode 1
+ versionName "1.0"
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ sourceSets {
+ main {
+ manifest.srcFile 'AndroidManifest-gradle.xml'
+ java.srcDirs = ['src']
+ res.srcDirs = ['res']
+ }
+ }
+}
+
+dependencies {
+ implementation project(':')
+ implementation 'androidx.annotation:annotation:1.1.0'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+}
diff --git a/car-chassis-lib/tests/paintbooth/res/drawable/ic_launcher.png b/car-chassis-lib/tests/paintbooth/res/drawable/ic_launcher.png
new file mode 100644
index 0000000..2af53a4
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/res/drawable/ic_launcher.png
Binary files differ
diff --git a/car-chassis-lib/tests/paintbooth/res/layout/dialog_samples.xml b/car-chassis-lib/tests/paintbooth/res/layout/dialog_samples.xml
new file mode 100644
index 0000000..29712a9
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/res/layout/dialog_samples.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/dialog_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/dialog_activity_background_color">
+
+ <com.android.car.chassis.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:title="@string/app_name"/>
+
+ <Button
+ android:id="@+id/show_dialog_bt"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Show Dialog"
+ app:layout_constraintBottom_toTopOf="@+id/show_dialog_only_positive_bt"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/toolbar"/>
+
+ <Button
+ android:id="@+id/show_dialog_only_positive_bt"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Show Dialog with only positive button"
+ app:layout_constraintBottom_toTopOf="@+id/show_dialog_with_checkbox_bt"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/show_dialog_bt"/>
+
+ <Button
+ android:id="@+id/show_dialog_with_checkbox_bt"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Show Dialog With Checkbox"
+ app:layout_constraintBottom_toTopOf="@+id/show_dialog_without_title"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/show_dialog_only_positive_bt"/>
+
+ <Button
+ android:id="@+id/show_dialog_without_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Show Dialog without title"
+ app:layout_constraintBottom_toTopOf="@+id/show_toast"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/show_dialog_with_checkbox_bt"/>
+
+ <Button
+ android:id="@+id/show_toast"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Show Toast"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/show_dialog_without_title"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-chassis-lib/tests/paintbooth/res/layout/home_page.xml b/car-chassis-lib/tests/paintbooth/res/layout/home_page.xml
new file mode 100644
index 0000000..5b883ec
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/res/layout/home_page.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/home"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/dialog_activity_background_color">
+
+ <com.android.car.chassis.Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:title="@string/app_name"
+ app:logo="@drawable/ic_launcher"/>
+
+ <Button
+ android:id="@+id/dialog_samples"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Show Dialog Sample Page"
+ app:layout_constraintLeft_toLeftOf="parent"
+ app:layout_constraintRight_toRightOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/toolbar"/>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/car-chassis-lib/tests/paintbooth/res/values/colors.xml b/car-chassis-lib/tests/paintbooth/res/values/colors.xml
new file mode 100644
index 0000000..6e93be6
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <color name="dialog_activity_background_color">#ffff0c</color>
+</resources>
diff --git a/car-chassis-lib/tests/paintbooth/res/values/strings.xml b/car-chassis-lib/tests/paintbooth/res/values/strings.xml
new file mode 100644
index 0000000..9bfdb07
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources>
+ <!-- Application name [CHAR LIMIT=30] -->
+ <string name="app_name">Paint Booth (Gerrit)</string>
+</resources>
diff --git a/car-chassis-lib/tests/paintbooth/src/com/android/car/chassis/paintbooth/DialogSamples.java b/car-chassis-lib/tests/paintbooth/src/com/android/car/chassis/paintbooth/DialogSamples.java
new file mode 100644
index 0000000..693dbd9
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/src/com/android/car/chassis/paintbooth/DialogSamples.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.paintbooth;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.car.chassis.Toolbar;
+
+/**
+ * Activity that shows different dialogs from the device default theme.
+ */
+public class DialogSamples extends Activity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.dialog_samples);
+
+ Button showDialogButton = findViewById(R.id.show_dialog_bt);
+ Button showDialogOnlyPositiveButton = findViewById(R.id.show_dialog_only_positive_bt);
+ Button showDialogWithoutTitleButton = findViewById(R.id.show_dialog_without_title);
+ Button showDialogWithCheckboxButton = findViewById(R.id.show_dialog_with_checkbox_bt);
+ showDialogButton.setOnClickListener(v -> openDialog(false));
+ showDialogOnlyPositiveButton.setOnClickListener(v -> openDialogWithOnlyPositiveButton());
+ showDialogWithoutTitleButton.setOnClickListener(v -> openDialogWithoutTitle());
+ showDialogWithCheckboxButton.setOnClickListener(v -> openDialog(true));
+ Button showToast = findViewById(R.id.show_toast);
+ showToast.setOnClickListener(v -> showToast());
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ toolbar.setState(Toolbar.State.SUBPAGE);
+ toolbar.addListener(new Toolbar.Listener() {
+ @Override
+ public void onBack() {
+ finish();
+ }
+ });
+ }
+
+ private void openDialog(boolean showCheckbox) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+
+ if (showCheckbox) {
+ // Set Custom Title
+ TextView title = new TextView(this);
+ // Title Properties
+ title.setText("Custom Dialog Box");
+ builder.setCustomTitle(title);
+ builder.setMultiChoiceItems(
+ new CharSequence[]{"I am a checkbox"},
+ new boolean[]{false},
+ (dialog, which, isChecked) -> {
+ });
+ } else {
+ builder.setTitle("Standard Alert Dialog").setMessage("With a message to show.");
+ }
+
+ builder
+ .setPositiveButton("OK", (dialoginterface, i) -> {
+ })
+ .setNegativeButton("CANCEL", (dialog, which) -> {
+ });
+ builder.show();
+ }
+
+ private void openDialogWithOnlyPositiveButton() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Standard Alert Dialog").setMessage("With a message to show.");
+ builder.setPositiveButton("OK", (dialoginterface, i) -> {
+ });
+ builder.show();
+ }
+
+ private void openDialogWithoutTitle() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage("I dont have a titile.");
+ builder
+ .setPositiveButton("OK", (dialoginterface, i) -> {
+ })
+ .setNegativeButton("CANCEL", (dialog, which) -> {
+ });
+ builder.show();
+ }
+
+ private void showToast() {
+ Toast.makeText(this, "Toast message looks like this", Toast.LENGTH_LONG).show();
+ }
+}
diff --git a/car-chassis-lib/tests/paintbooth/src/com/android/car/chassis/paintbooth/MainActivity.java b/car-chassis-lib/tests/paintbooth/src/com/android/car/chassis/paintbooth/MainActivity.java
new file mode 100644
index 0000000..b0f4844
--- /dev/null
+++ b/car-chassis-lib/tests/paintbooth/src/com/android/car/chassis/paintbooth/MainActivity.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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 com.android.car.chassis.paintbooth;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.Button;
+
+/**
+ * Paint booth app
+ */
+public class MainActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.home_page);
+
+ Button showDialogSamples = findViewById(R.id.dialog_samples);
+ showDialogSamples.setOnClickListener(v -> startSampleActivity(DialogSamples.class));
+ }
+
+ /**
+ * Launch the given sample activity
+ */
+ private boolean startSampleActivity(Class<?> cls) {
+ Intent intent = new Intent(this, cls);
+ startActivity(intent);
+ return true;
+ }
+}
diff --git a/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java b/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
index ba17316..5ca60d1 100644
--- a/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
+++ b/car-media-common/src/com/android/car/media/common/MediaItemMetadata.java
@@ -147,11 +147,15 @@
Bitmap myBitmap = getBitmapToFlag(context);
Bitmap otherBitmap = other.getBitmapToFlag(context);
- if ((myBitmap != null) && (myBitmap.equals(otherBitmap))) return true;
+ if ((myBitmap != null) || (otherBitmap != null)) {
+ return Objects.equals(myBitmap, otherBitmap);
+ }
Uri myUri = getImageURI();
Uri otherUri = other.getImageURI();
- if ((myUri != null) && (myUri.equals(otherUri))) return true;
+ if ((myUri != null) || (otherUri != null)) {
+ return Objects.equals(myUri, otherUri);
+ }
return getPlaceholderHash() == other.getPlaceholderHash();
}
diff --git a/car-media-common/src/com/android/car/media/common/source/MediaSource.java b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
index db2cc11..09dcbb5 100644
--- a/car-media-common/src/com/android/car/media/common/source/MediaSource.java
+++ b/car-media-common/src/com/android/car/media/common/source/MediaSource.java
@@ -38,10 +38,8 @@
import com.android.car.apps.common.BitmapUtils;
-import java.util.HashSet;
import java.util.List;
import java.util.Objects;
-import java.util.Set;
/**
* This represents a source of media content. It provides convenient methods to access media source
@@ -50,15 +48,6 @@
public class MediaSource {
private static final String TAG = "MediaSource";
- /**
- * Custom media sources which should not be templatized.
- */
- private static final Set<String> CUSTOM_MEDIA_SOURCES = new HashSet<>();
-
- static {
- CUSTOM_MEDIA_SOURCES.add("com.android.car.radio");
- }
-
@NonNull
private final ComponentName mBrowseService;
@NonNull
@@ -201,13 +190,6 @@
return getRoundCroppedBitmap(BitmapUtils.fromDrawable(mIcon, null));
}
- /**
- * Returns {@code true} iff this media source should not be templatized.
- */
- public boolean isCustom() {
- return CUSTOM_MEDIA_SOURCES.contains(getPackageName());
- }
-
private static Bitmap getRoundCroppedBitmap(Bitmap bitmap) {
Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
Bitmap.Config.ARGB_8888);
diff --git a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
index 8eebf95..398cfa1 100644
--- a/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
+++ b/car-telephony-common/src/com/android/car/telephony/common/InMemoryPhoneBook.java
@@ -90,14 +90,12 @@
private InMemoryPhoneBook(Context context) {
mContext = context;
- // TODO(b/138749585): clean up filtering once contact cloud sync is disabled.
QueryParam contactListQueryParam = new QueryParam(
ContactsContract.Data.CONTENT_URI,
null,
- ContactsContract.Data.MIMETYPE + " = ? and "
- + ContactsContract.RawContacts.ACCOUNT_TYPE + " != ?",
+ ContactsContract.Data.MIMETYPE + " = ?",
new String[]{
- ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, "com.google"},
+ ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE},
ContactsContract.Contacts.DISPLAY_NAME + " ASC ");
mContactListAsyncQueryLiveData = new AsyncQueryLiveData<List<Contact>>(mContext,
QueryParam.of(contactListQueryParam)) {