diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..c6cbe562a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 000000000..73aeaaf17 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +android-widgets-app \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 000000000..96cc43efa --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 000000000..e7bedf337 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 000000000..97626ba45 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 000000000..4854b109d --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..5d1998103 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..7d89a41b0 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 000000000..7f68460d8 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..b8fb0ff6d --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + applicationId "org.nativescript.android_widgets_app" + minSdkVersion 17 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.1.1' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..95065db74 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in d:\Android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/app/src/androidTest/java/org/nativescript/android_widgets_app/ApplicationTest.java b/app/src/androidTest/java/org/nativescript/android_widgets_app/ApplicationTest.java new file mode 100644 index 000000000..5977a0362 --- /dev/null +++ b/app/src/androidTest/java/org/nativescript/android_widgets_app/ApplicationTest.java @@ -0,0 +1,13 @@ +package org.nativescript.android_widgets_app; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9fcb5fd87 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..cde69bccc Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..c133a0cbd Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..bfa42f0e7 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..324e72cdd Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..aee44e138 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..3ab3e9cbc --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..c2464c47f --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + android-widgets-app + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..5885930df --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/test/java/org/nativescript/android_widgets_app/ExampleUnitTest.java b/app/src/test/java/org/nativescript/android_widgets_app/ExampleUnitTest.java new file mode 100644 index 000000000..33889b265 --- /dev/null +++ b/app/src/test/java/org/nativescript/android_widgets_app/ExampleUnitTest.java @@ -0,0 +1,15 @@ +package org.nativescript.android_widgets_app; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * To work on unit tests, switch the Test Artifact in the Build Variants view. + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..b16936538 --- /dev/null +++ b/build.gradle @@ -0,0 +1,23 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.0.0-alpha1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..1d3591c8a --- /dev/null +++ b/gradle.properties @@ -0,0 +1,18 @@ +# 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. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# 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 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..05ef575b0 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..f23df6e46 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Oct 21 11:34:03 PDT 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 000000000..9d82f7891 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# 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 +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# 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 + +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" ] ; 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 + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..8a0b282aa --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@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 + +@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= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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 Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_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=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +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/settings.gradle b/settings.gradle new file mode 100644 index 000000000..55c206981 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':widgets' diff --git a/widgets/.gitignore b/widgets/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/widgets/.gitignore @@ -0,0 +1 @@ +/build diff --git a/widgets/build.gradle b/widgets/build.gradle new file mode 100644 index 000000000..16bbd24e6 --- /dev/null +++ b/widgets/build.gradle @@ -0,0 +1,25 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.2" + + defaultConfig { + minSdkVersion 17 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.1.1' +} diff --git a/widgets/proguard-rules.pro b/widgets/proguard-rules.pro new file mode 100644 index 000000000..95065db74 --- /dev/null +++ b/widgets/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in d:\Android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/widgets/src/androidTest/java/org/nativescript/widgets/ApplicationTest.java b/widgets/src/androidTest/java/org/nativescript/widgets/ApplicationTest.java new file mode 100644 index 000000000..72ea62641 --- /dev/null +++ b/widgets/src/androidTest/java/org/nativescript/widgets/ApplicationTest.java @@ -0,0 +1,13 @@ +package org.nativescript.widgets; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} \ No newline at end of file diff --git a/widgets/src/main/AndroidManifest.xml b/widgets/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a28a68853 --- /dev/null +++ b/widgets/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/widgets/src/main/java/org/nativescript/widgets/AbsoluteLayout.java b/widgets/src/main/java/org/nativescript/widgets/AbsoluteLayout.java new file mode 100644 index 000000000..21ebd1fd8 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/AbsoluteLayout.java @@ -0,0 +1,83 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class AbsoluteLayout extends LayoutBase { + + public AbsoluteLayout(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int measureWidth = 0; + int measureHeight = 0; + int childMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + int count = this.getChildCount(); + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.measureChild(child, childMeasureSpec, childMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + measureWidth = Math.max(measureWidth, childLayoutParams.left + childMeasuredWidth); + measureHeight = Math.max(measureHeight, childLayoutParams.top + childMeasuredHeight); + } + + // Add in our padding + measureWidth += this.getPaddingLeft() + this.getPaddingRight(); + measureHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum height + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int leftPadding = this.getPaddingLeft(); + int topPadding = this.getPaddingTop(); + int count = this.getChildCount(); + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + + int childLeft = leftPadding + childLayoutParams.left; + int childTop = topPadding + childLayoutParams.top; + int childRight = childLeft + childWidth + childLayoutParams.leftMargin + childLayoutParams.rightMargin; + int childBottom = childTop + childHeight + childLayoutParams.topMargin + childLayoutParams.bottomMargin; + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childRight, childBottom); + } + + CommonLayoutParams.restoreOriginalParams(this); + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/CommonLayoutParams.java b/widgets/src/main/java/org/nativescript/widgets/CommonLayoutParams.java new file mode 100644 index 000000000..2bd555639 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/CommonLayoutParams.java @@ -0,0 +1,407 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.FrameLayout; + +/** + * @author hhristov + */ +public class CommonLayoutParams extends FrameLayout.LayoutParams { + + static final String TAG = "NSLayout"; + static int debuggable = -1; + private static final StringBuilder sb = new StringBuilder(); + + public CommonLayoutParams() { + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + } + + public float widthPercent = 0; + public float heightPercent = 0; + + public float topMarginPercent = 0; + public float leftMarginPercent = 0; + public float bottomMarginPercent = 0; + public float rightMarginPercent = 0; + + public int widthOriginal = -1; + public int heightOriginal = -1; + + public int topMarginOriginal = -1; + public int leftMarginOriginal = -1; + public int bottomMarginOriginal = -1; + public int rightMarginOriginal = -1; + + public int left = 0; + public int top = 0; + public int row = 0; + public int column = 0; + public int rowSpan = 1; + public int columnSpan = 1; + public Dock dock = Dock.left; + + protected static int getDesiredWidth(View view) { + CommonLayoutParams lp = (CommonLayoutParams) view.getLayoutParams(); + return view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + } + + protected static int getDesiredHeight(View view) { + CommonLayoutParams lp = (CommonLayoutParams) view.getLayoutParams(); + return view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + } + + // We use our own layout method because the one in FrameLayout is broken when margins are set and gravity is CENTER_VERTICAL or CENTER_HORIZONTAL. + @SuppressLint("RtlHardcoded") + protected static void layoutChild(View child, int left, int top, int right, int bottom) { + if (child.getVisibility() == View.GONE) { + return; + } + + int childTop = 0; + int childLeft = 0; + + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + + CommonLayoutParams lp = (CommonLayoutParams) child.getLayoutParams(); + int gravity = lp.gravity; + if (gravity == -1) { + gravity = Gravity.FILL; + } + + int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + // If we have explicit height and gravity is FILL we need to be centered otherwise our explicit height won't be taken into account. + if (lp.height >= 0 && verticalGravity == Gravity.FILL_VERTICAL) { + verticalGravity = Gravity.CENTER_VERTICAL; + } + + switch (verticalGravity) { + case Gravity.TOP: + childTop = top + lp.topMargin; + break; + + case Gravity.CENTER_VERTICAL: + childTop = top + (bottom - top - childHeight + lp.topMargin - lp.bottomMargin) / 2; + break; + + case Gravity.BOTTOM: + childTop = bottom - childHeight - lp.bottomMargin; + break; + + case Gravity.FILL_VERTICAL: + default: + childTop = top + lp.topMargin; + childHeight = bottom - top - (lp.topMargin + lp.bottomMargin); + break; + } + + int horizontalGravity = Gravity.getAbsoluteGravity(gravity, child.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; + + // If we have explicit width and gravity is FILL we need to be centered otherwise our explicit width won't be taken into account. + if (lp.width >= 0 && horizontalGravity == Gravity.FILL_HORIZONTAL) { + horizontalGravity = Gravity.CENTER_HORIZONTAL; + } + + switch (horizontalGravity) { + case Gravity.LEFT: + childLeft = left + lp.leftMargin; + break; + + case Gravity.CENTER_HORIZONTAL: + childLeft = left + (right - left - childWidth + lp.leftMargin - lp.rightMargin) / 2; + break; + + case Gravity.RIGHT: + childLeft = right - childWidth - lp.rightMargin; + break; + + case Gravity.FILL_HORIZONTAL: + default: + childLeft = left + lp.leftMargin; + childWidth = right - left - (lp.leftMargin + lp.rightMargin); + break; + } + + int childRight = Math.round(childLeft + childWidth); + int childBottom = Math.round(childTop + childHeight); + childLeft = Math.round(childLeft); + childTop = Math.round(childTop); + + // Re-measure TextView because it is not centered if layout width is larger than measure width. + if (child instanceof android.widget.TextView) { + + boolean canChangeWidth = lp.width < 0; + boolean canChangeHeight = lp.height < 0; + + int measuredWidth = child.getMeasuredWidth(); + int measuredHeight = child.getMeasuredHeight(); + + int width = childRight - childLeft; + int height = childBottom - childTop; + if ((Math.abs(measuredWidth - width) > 1 && canChangeWidth) || (Math.abs(measuredHeight - height) > 1 && canChangeHeight)) { + int widthMeasureSpec = MeasureSpec.makeMeasureSpec(canChangeWidth ? width : lp.width, MeasureSpec.EXACTLY); + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(canChangeHeight ? height : lp.height, MeasureSpec.EXACTLY); + if (debuggable > 0) { + sb.setLength(0); + sb.append("remeasure "); + sb.append(child); + sb.append(" with "); + sb.append(MeasureSpec.toString(widthMeasureSpec)); + sb.append(", "); + sb.append(MeasureSpec.toString(heightMeasureSpec)); + log(TAG, sb.toString()); + } + + child.measure(widthMeasureSpec, heightMeasureSpec); + } + } + + if (debuggable > 0) { + sb.setLength(0); + sb.append(child.getParent().toString()); + sb.append(" :layoutChild: "); + sb.append(child.toString()); + sb.append(" "); + sb.append(childLeft); + sb.append(", "); + sb.append(childTop); + sb.append(", "); + sb.append(childRight); + sb.append(", "); + sb.append(childBottom); + log(TAG, sb.toString()); + } + + child.layout(childLeft, childTop, childRight, childBottom); + } + + protected static void measureChild(View child, int widthMeasureSpec, int heightMeasureSpec) { + if (child.getVisibility() == View.GONE) { + return; + } + + // Negative means not initialized. + if (debuggable < 0) { + try { + Context context = child.getContext(); + ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), android.content.pm.PackageManager.GET_META_DATA); + android.os.Bundle bundle = ai.metaData; + Boolean debugLayouts = bundle != null ? bundle.getBoolean("debugLayouts", false) : false; + debuggable = debugLayouts ? 1 : 0; + } catch (NameNotFoundException e) { + debuggable = 0; + Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage()); + } catch (NullPointerException e) { + debuggable = 0; + Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage()); + } + } + + int childWidthMeasureSpec = getMeasureSpec(child, widthMeasureSpec, true); + int childHeightMeasureSpec = getMeasureSpec(child, heightMeasureSpec, false); + + if (debuggable > 0) { + sb.setLength(0); + sb.append(child.getParent().toString()); + sb.append(" :measureChild: "); + sb.append(child.toString()); + sb.append(" "); + sb.append(MeasureSpec.toString(childWidthMeasureSpec)); + sb.append(", "); + sb.append(MeasureSpec.toString(childHeightMeasureSpec)); + log(TAG, sb.toString()); + } + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + /** + * Iterates over children and changes their width and height to one calculated from percentage + * values. + * + * @param viewGroup The parent ViewGroup. + * @param widthMeasureSpec Width MeasureSpec of the parent ViewGroup. + * @param heightMeasureSpec Height MeasureSpec of the parent ViewGroup. + */ + protected static void adjustChildrenLayoutParams(ViewGroup viewGroup, int widthMeasureSpec, int heightMeasureSpec) { + + int availableWidth = MeasureSpec.getSize(widthMeasureSpec); + int widthSpec = MeasureSpec.getMode(widthMeasureSpec); + + int availableHeight = MeasureSpec.getSize(heightMeasureSpec); + int heightSpec = MeasureSpec.getMode(heightMeasureSpec); + + for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { + View child = viewGroup.getChildAt(i); + LayoutParams params = child.getLayoutParams(); + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) child.getLayoutParams(); + if (widthSpec != MeasureSpec.UNSPECIFIED) { + if (lp.widthPercent > 0) { + lp.widthOriginal = lp.width; + lp.width = (int) (availableWidth * lp.widthPercent); + } + else { + lp.widthOriginal = -1; + } + + if (lp.leftMarginPercent > 0) { + lp.leftMarginOriginal = lp.leftMargin; + lp.leftMargin = (int) (availableWidth * lp.leftMarginPercent); + } + else { + lp.leftMarginOriginal = -1; + } + + if (lp.rightMarginPercent > 0) { + lp.rightMarginOriginal = lp.rightMargin; + lp.rightMargin = (int) (availableWidth * lp.rightMarginPercent); + } + else { + lp.rightMarginOriginal = -1; + } + } + + if (heightSpec != MeasureSpec.UNSPECIFIED) { + if (lp.heightPercent > 0) { + lp.heightOriginal = lp.height; + lp.height = (int) (availableHeight * lp.heightPercent); + } + else { + lp.heightOriginal = -1; + } + + if (lp.topMarginPercent > 0) { + lp.topMarginOriginal = lp.topMargin; + lp.topMargin = (int) (availableHeight * lp.topMarginPercent); + } + else { + lp.topMarginOriginal = -1; + } + + if (lp.bottomMarginPercent > 0) { + lp.bottomMarginOriginal = lp.bottomMargin; + lp.bottomMargin = (int) (availableHeight * lp.bottomMarginPercent); + } + else { + lp.bottomMarginOriginal = -1; + } + } + } + } + } + + /** + * Iterates over children and restores their original dimensions that were changed for + * percentage values. + */ + protected static void restoreOriginalParams(ViewGroup viewGroup) { + for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { + View view = viewGroup.getChildAt(i); + LayoutParams params = view.getLayoutParams(); + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + if (lp.widthPercent > 0) { + lp.width = lp.widthOriginal; + } + if (lp.heightPercent > 0) { + lp.height = lp.heightOriginal; + } + if (lp.leftMarginPercent > 0) { + lp.leftMargin = lp.leftMarginOriginal; + } + if (lp.topMarginPercent > 0) { + lp.topMargin = lp.topMarginOriginal; + } + if (lp.rightMarginPercent > 0) { + lp.rightMargin = lp.rightMarginOriginal; + } + if (lp.bottomMarginPercent > 0) { + lp.bottomMargin = lp.bottomMarginOriginal; + } + } + } + } + + static void log(String tag, String message) { + Log.d(tag, message); + } + + static StringBuilder getStringBuilder() { + sb.setLength(0); + return sb; + } + + private static int getMeasureSpec(View view, int parentMeasureSpec, boolean horizontal) { + + int parentLength = MeasureSpec.getSize(parentMeasureSpec); + int parentSpecMode = MeasureSpec.getMode(parentMeasureSpec); + + CommonLayoutParams lp = (CommonLayoutParams) view.getLayoutParams(); + final int margins = horizontal ? lp.leftMargin + lp.rightMargin : lp.topMargin + lp.bottomMargin; + + int resultSize = 0; + int resultMode = MeasureSpec.UNSPECIFIED; + + int measureLength = Math.max(0, parentLength - margins); + int childLength = horizontal ? lp.width : lp.height; + + // We want a specific size... let be it. + if (childLength >= 0) { + if (parentSpecMode != MeasureSpec.UNSPECIFIED) { + resultSize = Math.min(parentLength, childLength); + } else { + resultSize = childLength; + } + + resultMode = MeasureSpec.EXACTLY; + } else { + switch (parentSpecMode) { + // Parent has imposed an exact size on us + case MeasureSpec.EXACTLY: + resultSize = measureLength; + int gravity = LayoutBase.getGravity(view); + boolean stretched; + if (horizontal) { + final int horizontalGravity = Gravity.getAbsoluteGravity(gravity, view.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; + stretched = horizontalGravity == Gravity.FILL_HORIZONTAL; + } else { + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + stretched = verticalGravity == Gravity.FILL_VERTICAL; + } + + // if stretched - view wants to be our size. So be it. + // else - view wants to determine its own size. It can't be bigger than us. + resultMode = stretched ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; + break; + + // Parent has imposed a maximum size on us + case MeasureSpec.AT_MOST: + resultSize = measureLength; + resultMode = MeasureSpec.AT_MOST; + break; + + case MeasureSpec.UNSPECIFIED: + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + break; + } + } + + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } +} \ No newline at end of file diff --git a/widgets/src/main/java/org/nativescript/widgets/ContentLayout.java b/widgets/src/main/java/org/nativescript/widgets/ContentLayout.java new file mode 100644 index 000000000..283436dda --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/ContentLayout.java @@ -0,0 +1,80 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class ContentLayout extends LayoutBase { + + public ContentLayout(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int measureWidth = 0; + int measureHeight = 0; + + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.measureChild(child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + measureWidth = Math.max(measureWidth, childMeasuredWidth); + measureHeight = Math.max(measureHeight, childMeasuredHeight); + } + + // Add in our padding + measureWidth += this.getPaddingLeft() + this.getPaddingRight(); + measureHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childLeft = paddingLeft; + int childTop = paddingTop; + + int childRight = right - left - (paddingLeft + paddingRight); + int childBottom = bottom - top - (paddingRight + paddingBottom); + + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childRight, childBottom); + } + + CommonLayoutParams.restoreOriginalParams(this); + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/Dock.java b/widgets/src/main/java/org/nativescript/widgets/Dock.java new file mode 100644 index 000000000..e35a4e8ff --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/Dock.java @@ -0,0 +1,15 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public enum Dock { + left, + top, + right, + bottom +} diff --git a/widgets/src/main/java/org/nativescript/widgets/DockLayout.java b/widgets/src/main/java/org/nativescript/widgets/DockLayout.java new file mode 100644 index 000000000..05baaa99e --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/DockLayout.java @@ -0,0 +1,178 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class DockLayout extends LayoutBase { + + private boolean _stretchLastChild = true; + + public DockLayout(Context context) { + super(context); + } + + public boolean getStretchLastChild() { + return this._stretchLastChild; + } + public void setStretchLastChild(boolean value) { + this._stretchLastChild = value; + this.requestLayout(); + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int measureWidth = 0; + int measureHeight = 0; + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + int remainingWidth = widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : width - horizontalPadding; + int remainingHeight = heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : height - verticalPadding; + + int tempHeight = 0; + int tempWidth = 0; + int childWidthMeasureSpec = 0; + int childHeightMeasureSpec = 0; + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + if (this._stretchLastChild && (i == (count - 1))) { + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(remainingWidth, widthMode); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(remainingHeight, heightMode); + } + else { + // Measure children with AT_MOST even if our mode is EXACT + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(remainingWidth, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(remainingHeight, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode); + } + + CommonLayoutParams.measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + Dock dock = childLayoutParams.dock; + switch (dock) { + case top: + case bottom: + remainingHeight = Math.max(0, remainingHeight - childMeasuredHeight); + tempHeight += childMeasuredHeight; + measureWidth = Math.max(measureWidth, tempWidth + childMeasuredWidth); + measureHeight = Math.max(measureHeight, tempHeight); + break; + + case left: + case right: + default: + remainingWidth = Math.max(0, remainingWidth - childMeasuredWidth); + tempWidth += childMeasuredWidth; + measureWidth = Math.max(measureWidth, tempWidth); + measureHeight = Math.max(measureHeight, tempHeight + childMeasuredHeight); + break; + } + } + + // Add in our padding + measureWidth += horizontalPadding; + measureHeight += verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childLeft = this.getPaddingLeft(); + int childTop = this.getPaddingTop(); + + int x = childLeft; + int y = childTop; + + int remainingWidth = Math.max(0, right - left - (this.getPaddingLeft() + this.getPaddingRight())); + int remainingHeight = Math.max(0, bottom - top - (this.getPaddingTop() + this.getPaddingBottom())); + + int count = this.getChildCount(); + View childToStretch = null; + if (count > 0 && this._stretchLastChild) { + count--; + childToStretch = this.getChildAt(count); + } + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + int childWidth = CommonLayoutParams.getDesiredWidth(child); + int childHeight = CommonLayoutParams.getDesiredHeight(child); + + switch (childLayoutParams.dock) { + case top: + childLeft = x; + childTop = y; + childWidth = remainingWidth; + y += childHeight; + remainingHeight = Math.max(0, remainingHeight - childHeight); + break; + + case bottom: + childLeft = x; + childTop = y + remainingHeight - childHeight; + childWidth = remainingWidth; + remainingHeight = Math.max(0, remainingHeight - childHeight); + break; + + case right: + childLeft = x + remainingWidth - childWidth; + childTop = y; + childHeight = remainingHeight; + remainingWidth = Math.max(0, remainingWidth - childWidth); + break; + + case left: + default: + childLeft = x; + childTop = y; + childHeight = remainingHeight; + x += childWidth; + remainingWidth = Math.max(0, remainingWidth - childWidth); + break; + } + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childLeft + childWidth, childTop + childHeight); + } + + if (childToStretch != null) { + CommonLayoutParams.layoutChild(childToStretch, x, y, x + remainingWidth, y + remainingHeight); + } + + CommonLayoutParams.restoreOriginalParams(this); + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/GridLayout.java b/widgets/src/main/java/org/nativescript/widgets/GridLayout.java new file mode 100644 index 000000000..860850332 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/GridLayout.java @@ -0,0 +1,1166 @@ +/** + * + */ +package org.nativescript.widgets; + +import java.util.ArrayList; +import java.util.HashMap; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; + +/** + * @author hhristov + * + */ +public class GridLayout extends LayoutBase { + + private MeasureHelper helper = new MeasureHelper(this); + + private ArrayList _rows = new ArrayList(); + private ArrayList _cols = new ArrayList(); + private ArrayList columnOffsets = new ArrayList(); + private ArrayList rowOffsets = new ArrayList(); + private HashMap map = new HashMap(); + + public GridLayout(Context context) { + super(context); + } + + private static void validateItemSpec(ItemSpec itemSpec) { + if (itemSpec == null) { + throw new Error("itemSpec is null."); + } + + if (itemSpec.owner != null) { + throw new Error("itemSpec is already added to GridLayout."); + } + } + + public void addRow(ItemSpec itemSpec) { + validateItemSpec(itemSpec); + itemSpec.owner = this; + this._rows.add(itemSpec); + + ItemGroup rowGroup = new ItemGroup(itemSpec); + this.helper.rows.add(rowGroup); + + this.requestLayout(); + } + + public void addColumn(ItemSpec itemSpec) { + validateItemSpec(itemSpec); + itemSpec.owner = this; + this._cols.add(itemSpec); + + ItemGroup columnGroup = new ItemGroup(itemSpec); + this.helper.columns.add(columnGroup); + + this.requestLayout(); + } + + public void removeColumn(ItemSpec itemSpec) { + if (itemSpec == null) { + throw new Error("itemSpec is null."); + } + + int index = this._cols.indexOf(itemSpec); + if (itemSpec.owner != this || index < 0) { + throw new Error("itemSpec is not child of this GridLayout"); + } + + this.removeColumnAt(index); + } + + public void removeColumnAt(int index) { + this._cols.remove(index); + this.helper.columns.get(index).children.clear(); + this.helper.columns.remove(index); + this.requestLayout(); + } + + public void removeRow(ItemSpec itemSpec) { + if (itemSpec == null) { + throw new Error("itemSpec is null."); + } + + int index = this._rows.indexOf(itemSpec); + if (itemSpec.owner != this || index < 0) { + throw new Error("itemSpec is not child of this GridLayout"); + } + + this.removeRowAt(index); + } + + public void removeRowAt(int index) { + this._rows.remove(index); + this.helper.rows.get(index).children.clear(); + this.helper.rows.remove(index); + this.requestLayout(); + } + + public ItemSpec[] getColumns() { + ItemSpec copy[] = new ItemSpec[this._cols.size()]; + copy = this._cols.toArray(copy); + return copy; + } + + public ItemSpec[] getRows() { + ItemSpec copy[] = new ItemSpec[this._rows.size()]; + copy = this._rows.toArray(copy); + return copy; + } + + @Override + public void addView(View child) { + super.addView(child); + this.addToMap(child); + } + + @Override + public void addView(View child, int index) { + super.addView(child, index); + this.addToMap(child); + } + + @Override + public void addView(View child, LayoutParams params) { + super.addView(child, params); + this.addToMap(child); + } + + @Override + public void removeView(View view) { + this.removeFromMap(view); + super.removeView(view); + } + + @Override + public void removeViewAt(int index) { + View view = this.getChildAt(index); + this.removeFromMap(view); + super.removeViewAt(index); + } + + @Override + public void removeViews(int start, int count) { + int end = start + count; + for (int i = start; i < end; i++) { + View view = this.getChildAt(i); + this.removeFromMap(view); + } + + super.removeViews(start, count); + } + + private int getColumnIndex(CommonLayoutParams lp) { + return Math.max(0, Math.min(lp.column, this._cols.size() - 1)); + } + + private int getRowIndex(CommonLayoutParams lp) { + return Math.max(0, Math.min(lp.row, this._rows.size() - 1)); + } + + private ItemSpec getColumnSpec(CommonLayoutParams lp) { + if (this._cols.size() == 0) { + return this.helper.singleColumn; + } + + int columnIndex = Math.min(lp.column, this._cols.size() - 1); + return this._cols.get(columnIndex); + } + + private ItemSpec getRowSpec(CommonLayoutParams lp) { + if (this._rows.size() == 0) { + return this.helper.singleRow; + } + + int rowIndex = Math.min(lp.row, this._rows.size() - 1); + return this._rows.get(rowIndex); + } + + private int getColumnSpan(CommonLayoutParams lp, int columnIndex) { + if (this._cols.size() == 0) { + return 1; + } + + return Math.min(lp.columnSpan, this._cols.size() - columnIndex); + } + + private int getRowSpan(CommonLayoutParams lp, int rowIndex) { + if (this._rows.size() == 0) { + return 1; + } + + return Math.min(lp.rowSpan, this._rows.size() - rowIndex); + } + + private void updateMeasureSpecs(View child, MeasureSpecs measureSpec) { + CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); + int columnIndex = this.getColumnIndex(lp); + ItemSpec column = this.getColumnSpec(lp); + int rowIndex = this.getRowIndex(lp); + ItemSpec row = this.getRowSpec(lp); + int columnSpan = this.getColumnSpan(lp, columnIndex); + int rowSpan = this.getRowSpan(lp, rowIndex); + + measureSpec.setColumn(column); + measureSpec.setRow(row); + measureSpec.setColumnIndex(columnIndex); + measureSpec.setRowIndex(rowIndex); + measureSpec.setColumnSpan(columnSpan); + measureSpec.setRowSpan(rowSpan); + measureSpec.autoColumnsCount = 0; + measureSpec.autoRowsCount = 0; + measureSpec.measured = false; + measureSpec.pixelHeight = 0; + measureSpec.pixelWidth = 0; + measureSpec.starColumnsCount = 0; + measureSpec.starRowsCount = 0; + } + + private void addToMap(View child) { + MeasureSpecs measureSpec = new MeasureSpecs(child); + this.map.put(child, measureSpec); + } + + private void removeFromMap(View child) { + this.map.remove(child); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int measureWidth = 0; + int measureHeight = 0; + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + boolean infinityWidth = widthMode == MeasureSpec.UNSPECIFIED; + boolean infinityHeight = heightMode == MeasureSpec.UNSPECIFIED; + + this.helper.width = width - horizontalPadding; + this.helper.height = height - verticalPadding; + + int gravity = LayoutBase.getGravity(this); + int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + final int layoutDirection = this.getLayoutDirection(); + final int horizontalGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection) & Gravity.HORIZONTAL_GRAVITY_MASK; + + this.helper.stretchedHorizontally = horizontalGravity == Gravity.FILL_HORIZONTAL && !infinityWidth; + this.helper.stretchedVertically = verticalGravity == Gravity.FILL_VERTICAL && !infinityHeight; + + this.helper.setInfinityWidth(infinityWidth); + this.helper.setInfinityHeight(infinityHeight); + + this.helper.clearMeasureSpecs(); + this.helper.init(); + + int childrenCount = this.getChildCount(); + for (int i = 0; i < childrenCount; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + MeasureSpecs measureSpecs = this.map.get(child); + this.updateMeasureSpecs(child, measureSpecs); + this.helper.addMeasureSpec(measureSpecs); + } + + this.helper.measure(); + + // Add in our padding + measureWidth = this.helper.measuredWidth + horizontalPadding; + measureHeight = this.helper.measuredHeight + verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int paddingLeft = this.getPaddingLeft(); + int paddingTop = this.getPaddingTop(); + + this.columnOffsets.clear(); + this.rowOffsets.clear(); + + this.columnOffsets.add(paddingLeft); + this.rowOffsets.add(paddingTop); + + float offset = this.columnOffsets.get(0); + int roundedOffset = paddingLeft; + int roundedLength = 0; + float actualLength = 0; + int size = this.helper.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.helper.columns.get(i); + offset += columnGroup.length; + + actualLength = offset - roundedOffset; + roundedLength = Math.round(actualLength); + columnGroup.rowOrColumn._actualLength = roundedLength; + roundedOffset += roundedLength; + + this.columnOffsets.add(roundedOffset); + } + + offset = this.rowOffsets.get(0); + roundedOffset = this.getPaddingTop(); + roundedLength = 0; + actualLength = 0; + size = this.helper.rows.size(); + for (int i = 0; i < size; i++) { + ItemGroup rowGroup = this.helper.rows.get(i); + offset += rowGroup.length; + + actualLength = offset - roundedOffset; + roundedLength = Math.round(actualLength); + rowGroup.rowOrColumn._actualLength = roundedLength; + roundedOffset += roundedLength; + + this.rowOffsets.add(roundedOffset); + } + + int columns = this.helper.columns.size(); + for (int i = 0; i < columns; i++) { + ItemGroup columnGroup = this.helper.columns.get(i); + int children = columnGroup.children.size(); + for (int j = 0; j < children; j++) { + + MeasureSpecs measureSpec = columnGroup.children.get(j); + int childLeft = this.columnOffsets.get(measureSpec.getColumnIndex()); + int childRight = this.columnOffsets.get(measureSpec.getColumnIndex() + measureSpec.getColumnSpan()); + int childTop = this.rowOffsets.get(measureSpec.getRowIndex()); + int childBottom = this.rowOffsets.get(measureSpec.getRowIndex() + measureSpec.getRowSpan()); + + // No need to include margins in the width, height + CommonLayoutParams.layoutChild(measureSpec.child, childLeft, childTop, childRight, childBottom); + } + } + + CommonLayoutParams.restoreOriginalParams(this); + } +} + +class MeasureSpecs { + + private int _columnSpan = 1; + private int _rowSpan = 1; + + public int pixelWidth = 0; + public int pixelHeight = 0; + + public int starColumnsCount = 0; + public int starRowsCount = 0; + + public int autoColumnsCount = 0; + public int autoRowsCount = 0; + + public boolean measured = false; + + public final View child; + private ItemSpec column; + private ItemSpec row; + private int columnIndex; + private int rowIndex; + + MeasureSpecs(View child) { + this.child = child; + } + + public boolean getSpanned() { + return this._columnSpan > 1 || this._rowSpan > 1; + } + + public boolean getIsStar() { + return this.starRowsCount > 0 || this.starColumnsCount > 0; + } + + public int getColumnSpan() { + return this._columnSpan; + } + + public int getRowSpan() { + return this._rowSpan; + } + + public void setRowSpan(int value) { + // cannot have zero rowSpan. + this._rowSpan = Math.max(1, value); + } + + public void setColumnSpan(int value) { + // cannot have zero colSpan. + this._columnSpan = Math.max(1, value); + } + + public int getRowIndex() { + return this.rowIndex; + } + + public int getColumnIndex() { + return this.columnIndex; + } + + public void setRowIndex(int value) { + this.rowIndex = value; + } + + public void setColumnIndex(int value) { + this.columnIndex = value; + } + + public ItemSpec getRow() { + return this.row; + } + + public ItemSpec getColumn() { + return this.column; + } + + public void setRow(ItemSpec value) { + this.row = value; + } + + public void setColumn(ItemSpec value) { + this.column = value; + } +} + +class ItemGroup { + float length = 0; + int measuredCount = 0; + ItemSpec rowOrColumn; + ArrayList children = new ArrayList(); + + public int measureToFix = 0; + public int currentMeasureToFixCount = 0; + private boolean infinityLength = false; + + ItemGroup(ItemSpec spec) { + this.rowOrColumn = spec; + } + + public void setIsLengthInfinity(boolean infinityLength) { + this.infinityLength = infinityLength; + } + + public void init() { + this.measuredCount = 0; + this.currentMeasureToFixCount = 0; + this.length = this.rowOrColumn.getIsAbsolute() ? this.rowOrColumn.getValue() : 0; + } + + public boolean getAllMeasured() { + return this.measuredCount == this.children.size(); + } + + public boolean getCanBeFixed() { + return this.currentMeasureToFixCount == this.measureToFix; + } + + public boolean getIsAuto() { + return this.rowOrColumn.getIsAuto() || (this.rowOrColumn.getIsStar() && this.infinityLength); + } + + public boolean getIsStar() { + return this.rowOrColumn.getIsStar() && !this.infinityLength; + } + + public boolean getIsAbsolute() { + return this.rowOrColumn.getIsAbsolute(); + } +} + +class MeasureHelper { + public final ItemSpec singleRow = new ItemSpec(); + public final ItemSpec singleColumn = new ItemSpec(); + public final GridLayout grid; + + static int infinity = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + ArrayList rows = new ArrayList(); + ArrayList columns = new ArrayList(); + + public int width; + public int height; + public boolean stretchedHorizontally = false; + public boolean stretchedVertically = false; + + private boolean infinityWidth = false; + private boolean infinityHeight = false; + + int measuredWidth; + int measuredHeight; + + private float columnStarValue; + private float rowStarValue; + + private boolean fakeRowAdded = false; + private boolean fakeColumnAdded = false; + + MeasureHelper(GridLayout grid) { + this.grid = grid; + } + + public boolean getColumnsFixed() { + return this.columnStarValue >= 0; + } + + public void setInfinityWidth(boolean value) { + if (this.infinityWidth != value) { + this.infinityWidth = value; + + int size = this.columns.size(); + for(int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + columnGroup.setIsLengthInfinity(value); + } + } + } + + public void setInfinityHeight(boolean value) { + if (this.infinityHeight != value) { + this.infinityHeight = value; + + int size = this.rows.size(); + for(int i = 0; i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + rowGroup.setIsLengthInfinity(value); + } + } + } + + public void addMeasureSpec(MeasureSpecs measureSpec) { + // Get column stats + int size = measureSpec.getColumnIndex() + measureSpec.getColumnSpan(); + for (int i = measureSpec.getColumnIndex(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + measureSpec.autoColumnsCount++; + } + else if (columnGroup.getIsStar()) { + measureSpec.starColumnsCount += columnGroup.rowOrColumn.getValue(); + } + else if (columnGroup.getIsAbsolute()) { + measureSpec.pixelWidth += columnGroup.rowOrColumn.getValue(); + } + } + + if (measureSpec.autoColumnsCount > 0 && measureSpec.starColumnsCount == 0) { + // Determine which auto columns are affected by this element + for (int i = measureSpec.getColumnIndex(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + columnGroup.measureToFix++; + } + } + } + + // Get row stats + size = measureSpec.getRowIndex() + measureSpec.getRowSpan(); + for (int i = measureSpec.getRowIndex(); i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + measureSpec.autoRowsCount++; + } + else if (rowGroup.getIsStar()) { + measureSpec.starRowsCount += rowGroup.rowOrColumn.getValue(); + } + else if (rowGroup.getIsAbsolute()) { + measureSpec.pixelHeight += rowGroup.rowOrColumn.getValue(); + } + } + + if (measureSpec.autoRowsCount > 0 && measureSpec.starRowsCount == 0) { + // Determine which auto rows are affected by this element + for (int i = measureSpec.getRowIndex(); i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.measureToFix++; + } + } + } + + this.columns.get(measureSpec.getColumnIndex()).children.add(measureSpec); + this.rows.get(measureSpec.getRowIndex()).children.add(measureSpec); + } + + public void clearMeasureSpecs() { + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + this.columns.get(i).children.clear(); + } + + size = this.rows.size(); + for (int i = 0; i < size; i++) { + this.rows.get(i).children.clear(); + } + } + + private static void initList(ArrayList list) { + int size = list.size(); + for(int i = 0; i < size; i++) { + ItemGroup item = list.get(i); + item.init(); + } + } + + private ItemGroup singleRowGroup = new ItemGroup(singleRow); + private ItemGroup singleColumnGroup = new ItemGroup(singleColumn); + + void init() { + + int rows = this.rows.size(); + if (rows == 0) { + singleRowGroup.setIsLengthInfinity(this.infinityHeight); + this.rows.add(singleRowGroup); + this.fakeRowAdded = true; + } else if (rows > 1 && this.fakeRowAdded) { + this.rows.remove(0); + this.fakeRowAdded = false; + } + + int cols = this.columns.size(); + if (cols == 0) { + this.fakeColumnAdded = true; + singleColumnGroup.setIsLengthInfinity(this.infinityWidth); + this.columns.add(singleColumnGroup); + } + else if (cols > 1 && this.fakeColumnAdded) { + this.columns.remove(0); + this.fakeColumnAdded = false; + } + + initList(this.rows); + initList(this.columns); + + this.columnStarValue = -1; + this.rowStarValue = -1; + } + + private void itemMeasured(MeasureSpecs measureSpec, boolean isFakeMeasure) { + if (!isFakeMeasure) { + this.columns.get(measureSpec.getColumnIndex()).measuredCount++; + this.rows.get(measureSpec.getRowIndex()).measuredCount++; + measureSpec.measured = true; + } + + if (measureSpec.autoColumnsCount > 0 && measureSpec.starColumnsCount == 0) { + int size = measureSpec.getColumnIndex() + measureSpec.getColumnSpan(); + for (int i = measureSpec.getColumnIndex(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + columnGroup.currentMeasureToFixCount++; + } + } + } + + if (measureSpec.autoRowsCount > 0 && measureSpec.starRowsCount == 0) { + int size = measureSpec.getRowIndex() + measureSpec.getRowSpan(); + for (int i = measureSpec.getRowIndex(); i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.currentMeasureToFixCount++; + } + } + } + } + + private void fixColumns() { + float currentColumnWidth = 0; + int columnStarCount = 0; + + int columnCount = this.columns.size(); + for(int i = 0; i < columnCount; i++) { + ItemGroup item = this.columns.get(i); + + // Star columns are still zeros (not calculated). + currentColumnWidth += item.length; + if (item.rowOrColumn.getIsStar()) { + columnStarCount += item.rowOrColumn.getValue(); + } + } + + this.columnStarValue = columnStarCount > 0 ? (this.width - currentColumnWidth) / columnStarCount : 0; + + if (this.stretchedHorizontally) { + for (int i = 0; i < columnCount; i++) { + ItemGroup item = this.columns.get(i); + if (item.getIsStar()) { + item.length = item.rowOrColumn.getValue() * this.columnStarValue; + } + } + } + } + + private void fixRows() { + float currentRowHeight = 0; + int rowStarCount = 0; + + int rowCount = this.rows.size(); + for(int i = 0; i < rowCount; i++) { + ItemGroup item = this.rows.get(i); + + // Star rows are still zeros (not calculated). + currentRowHeight += item.length; + if (item.rowOrColumn.getIsStar()) { + rowStarCount += item.rowOrColumn.getValue(); + } + } + + this.rowStarValue = rowStarCount > 0 ? (this.height - currentRowHeight) / rowStarCount : 0; + + if(this.stretchedVertically) { + for (int i = 0; i < rowCount; i++) { + ItemGroup item = this.rows.get(i); + if (item.getIsStar()) { + item.length = item.rowOrColumn.getValue() * this.rowStarValue; + } + } + } + } + + private void fakeMeasure() { + // Fake measure - measure all elements that have star rows and auto columns - with infinity Width and infinity Height + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getAllMeasured()) { + continue; + } + + int childrenCount = columnGroup.children.size(); + for (int j = 0; j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.starRowsCount > 0 && measureSpec.autoColumnsCount > 0 && measureSpec.starColumnsCount == 0) { + this.measureChild(measureSpec, true); + } + } + } + } + + private void measureFixedColumnsNoStarRows() { + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + int childrenCount = columnGroup.children.size(); + for (int j = 0; j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.starColumnsCount > 0 && measureSpec.starRowsCount == 0) { + this.measureChildFixedColumns(measureSpec); + } + } + } + } + + private void measureNoStarColumnsFixedRows() { + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + int childrenCount = columnGroup.children.size(); + for (int j = 0; j < childrenCount ; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.starRowsCount > 0 && measureSpec.starColumnsCount == 0) { + this.measureChildFixedRows(measureSpec); + } + } + } + } + + private static boolean canFix(ArrayList list) { + int size = list.size(); + for (int i = 0; i < size; i++) { + ItemGroup item = list.get(i); + if(!item.getCanBeFixed()) { + return false; + } + } + + return true; + } + + private static int getMeasureLength(ArrayList list) { + int result = 0; + int size = list.size(); + for (int i = 0; i < size; i++) { + ItemGroup item = list.get(i); + result += item.length; + } + + return result; + } + + public void measure() { + + // Measure auto & pixel columns and rows (no spans). + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + int childrenCount = columnGroup.children.size(); + for (int j = 0; j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.getIsStar() || measureSpec.getSpanned()) { + continue; + } + + this.measureChild(measureSpec, false); + } + } + + // Measure auto & pixel columns and rows (with spans). + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + int childrenCount = columnGroup.children.size(); + for (int j = 0; j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.getIsStar() || !measureSpec.getSpanned()) { + continue; + } + + this.measureChild(measureSpec, false); + } + } + + // try fix stars! + boolean fixColumns = canFix(this.columns); + boolean fixRows = canFix(this.rows); + + if (fixColumns) { + this.fixColumns(); + } + + if (fixRows) { + this.fixRows(); + } + + if (!fixColumns && !fixRows) { + // Fake measure - measure all elements that have star rows and auto columns - with infinity Width and infinity Height + // should be able to fix rows after that + this.fakeMeasure(); + + this.fixColumns(); + + // Measure all elements that have star columns(already fixed), but no stars at the rows + this.measureFixedColumnsNoStarRows(); + + this.fixRows(); + } + else if (fixColumns && !fixRows) { + // Measure all elements that have star columns(already fixed) but no stars at the rows + this.measureFixedColumnsNoStarRows(); + + // Then + this.fixRows(); + } + else if (!fixColumns && fixRows) { + // Measure all elements that have star rows(already fixed) but no star at the columns + this.measureNoStarColumnsFixedRows(); + + // Then + this.fixColumns(); + } + + // Rows and columns are fixed here - measure remaining elements + size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + int childCount = columnGroup.children.size(); + for (int j = 0; j < childCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (!measureSpec.measured) { + this.measureChildFixedColumnsAndRows(measureSpec); + } + } + } + + this.measuredWidth = getMeasureLength(this.columns); + this.measuredHeight = getMeasureLength(this.rows); + } + + private void measureChild(MeasureSpecs measureSpec, boolean isFakeMeasure) { + int widthMeasureSpec = (measureSpec.autoColumnsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelWidth, MeasureSpec.EXACTLY); + int heightMeasureSpec = (isFakeMeasure || measureSpec.autoRowsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelHeight, MeasureSpec.EXACTLY); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + int columnSpanEnd = measureSpec.getColumnIndex() + measureSpec.getColumnSpan(); + int rowSpanEnd = measureSpec.getRowIndex() + measureSpec.getRowSpan(); + + if (measureSpec.autoColumnsCount > 0) { + int remainingSpace = childMeasuredWidth; + + for (int i = measureSpec.getColumnIndex(); i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + remainingSpace -= columnGroup.length; + } + + if (remainingSpace > 0) { + int growSize = remainingSpace / measureSpec.autoColumnsCount; + for (int i = measureSpec.getColumnIndex(); i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + columnGroup.length += growSize; + } + } + } + } + + if (!isFakeMeasure && measureSpec.autoRowsCount > 0) { + int remainingSpace = childMeasuredHeight; + + for (int i = measureSpec.getRowIndex(); i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + remainingSpace -= rowGroup.length; + } + + if (remainingSpace > 0) { + int growSize = remainingSpace / measureSpec.autoRowsCount; + for (int i = measureSpec.getRowIndex(); i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.length += growSize; + } + } + } + } + + this.itemMeasured(measureSpec, isFakeMeasure); + } + + private void measureChildFixedColumns(MeasureSpecs measureSpec) { + int columnIndex = measureSpec.getColumnIndex(); + int columnSpanEnd = columnIndex + measureSpec.getColumnSpan(); + int rowIndex = measureSpec.getRowIndex(); + int rowSpanEnd = rowIndex + measureSpec.getRowSpan(); + + int columnsWidth = 0; + int growSize = 0; + + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (!columnGroup.getIsStar()) { + columnsWidth += columnGroup.length; + } + } + + int measureWidth = (int)(columnsWidth + measureSpec.starColumnsCount * this.columnStarValue); + + int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measureWidth, this.stretchedHorizontally ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST); + int heightMeasureSpec = (measureSpec.autoRowsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelHeight, MeasureSpec.EXACTLY); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + // Distribute width over star columns + if (!this.stretchedHorizontally) { + int remainingSpace = childMeasuredWidth; + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + remainingSpace -= columnGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.starColumnsCount; + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsStar()) { + columnGroup.length += growSize; + } + } + } + } + + // Distribute height over auto rows + if (measureSpec.autoRowsCount > 0) { + int remainingSpace = childMeasuredHeight; + + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + remainingSpace -= rowGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.autoRowsCount; + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.length += growSize; + } + } + } + } + + this.itemMeasured(measureSpec, false); + } + + private void measureChildFixedRows(MeasureSpecs measureSpec) { + int columnIndex = measureSpec.getColumnIndex(); + int columnSpanEnd = columnIndex + measureSpec.getColumnSpan(); + int rowIndex = measureSpec.getRowIndex(); + int rowSpanEnd = rowIndex + measureSpec.getRowSpan(); + int rowsHeight = 0; + + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (!rowGroup.getIsStar()) { + rowsHeight += rowGroup.length; + } + } + + int measureHeight = (int)(rowsHeight + measureSpec.starRowsCount * this.rowStarValue); + + int widthMeasureSpec = (measureSpec.autoColumnsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelWidth, MeasureSpec.EXACTLY); + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(measureHeight, this.stretchedVertically ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + int remainingSpace = 0; + int growSize = 0; + + // Distribute width over auto columns + if (measureSpec.autoColumnsCount > 0) { + remainingSpace = childMeasuredWidth; + + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + remainingSpace -= columnGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.autoColumnsCount; + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + + if (columnGroup.getIsAuto()) { + columnGroup.length += growSize; + } + } + } + } + + // Distribute height over star rows + if (!this.stretchedVertically) { + remainingSpace = childMeasuredHeight; + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + remainingSpace -= rowGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.starRowsCount; + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsStar()) { + rowGroup.length += growSize; + } + } + } + } + + this.itemMeasured(measureSpec, false); + } + + private void measureChildFixedColumnsAndRows(MeasureSpecs measureSpec) { + int columnIndex = measureSpec.getColumnIndex(); + int columnSpanEnd = columnIndex + measureSpec.getColumnSpan(); + int rowIndex = measureSpec.getRowIndex(); + int rowSpanEnd = rowIndex + measureSpec.getRowSpan(); + + ItemGroup columnGroup; + ItemGroup rowGroup; + + float columnsWidth = 0; + for (int i = columnIndex; i < columnSpanEnd; i++) { + columnGroup = this.columns.get(i); + if (!columnGroup.getIsStar()) { + columnsWidth += columnGroup.length; + } + } + + float rowsHeight = 0; + for (int i = rowIndex; i < rowSpanEnd; i++) { + rowGroup = this.rows.get(i); + if (!rowGroup.getIsStar()) { + rowsHeight += rowGroup.length; + } + } + + int measureWidth = (int)(columnsWidth + measureSpec.starColumnsCount * this.columnStarValue); + int measureHeight = (int)(rowsHeight + measureSpec.starRowsCount * this.rowStarValue); + + // if (have stars) & (not stretch) - at most + int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measureWidth, + (measureSpec.starColumnsCount > 0 && !this.stretchedHorizontally) ? MeasureSpec.AT_MOST : MeasureSpec.EXACTLY); + + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(measureHeight, + (measureSpec.starRowsCount > 0 && !this.stretchedVertically) ? MeasureSpec.AT_MOST : MeasureSpec.EXACTLY); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + int remainingSpace = childMeasuredWidth; + int growSize = 0; + + if (!this.stretchedHorizontally) { + for (int i = columnIndex; i < columnSpanEnd; i++) { + columnGroup = this.columns.get(i); + remainingSpace -= columnGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.starColumnsCount; + for (int i = columnIndex; i < columnSpanEnd; i++) { + columnGroup = this.columns.get(i); + if (columnGroup.getIsStar()) { + columnGroup.length += growSize; + } + } + } + } + + remainingSpace = childMeasuredHeight; + + if (!this.stretchedVertically) { + for (int i = rowIndex; i < rowSpanEnd; i++) { + rowGroup = this.rows.get(i); + remainingSpace -= rowGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.starRowsCount; + for (int i = rowIndex; i < rowSpanEnd; i++) { + rowGroup = this.rows.get(i); + if (rowGroup.getIsStar()) { + rowGroup.length += growSize; + } + } + } + } + + this.itemMeasured(measureSpec, false); + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/GridUnitType.java b/widgets/src/main/java/org/nativescript/widgets/GridUnitType.java new file mode 100644 index 000000000..d52a3b047 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/GridUnitType.java @@ -0,0 +1,14 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public enum GridUnitType { + auto, + pixel, + star +} diff --git a/widgets/src/main/java/org/nativescript/widgets/HorizontalScrollView.java b/widgets/src/main/java/org/nativescript/widgets/HorizontalScrollView.java new file mode 100644 index 000000000..d1c38ec9b --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/HorizontalScrollView.java @@ -0,0 +1,251 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +/** + * @author hhristov + * + */ +public class HorizontalScrollView extends android.widget.HorizontalScrollView { + + private final Rect mTempRect = new Rect(); + + private int contentMeasuredWidth = 0; + private int contentMeasuredHeight = 0; + private int scrollableLength = 0; + private SavedState mSavedState; + private boolean isFirstLayout = true; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + public HorizontalScrollView(Context context) { + super(context); + } + + public int getScrollableLength() { + return this.scrollableLength; + } + + @Override + public void requestLayout() { + this.mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mIsLayoutDirty) { + this.scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + // Don't call measure because it will measure content twice. + // ScrollView is expected to have single child so we measure only the first child. + View child = this.getChildCount() > 0 ? this.getChildAt(0) : null; + if (child == null) { + this.scrollableLength = 0; + this.contentMeasuredWidth = 0; + this.contentMeasuredHeight = 0; + } + else { + CommonLayoutParams.measureChild(child, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightMeasureSpec); + this.contentMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + this.contentMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + // Android ScrollView does not account to child margins so we set them as paddings. Otherwise you can never scroll to bottom. + CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); + this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); + } + + // Don't add in our paddings because they are already added as child margins. (we will include them twice if we add them). + // Check the previous line - this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); + //this.contentMeasuredWidth += this.getPaddingLeft() + this.getPaddingRight(); + //this.contentMeasuredHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum height + this.contentMeasuredWidth = Math.max(this.contentMeasuredWidth, this.getSuggestedMinimumWidth()); + this.contentMeasuredHeight = Math.max(this.contentMeasuredHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(this.contentMeasuredWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(this.contentMeasuredHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childWidth = 0; + if (this.getChildCount() > 0) { + View child = this.getChildAt(0); + childWidth = child.getMeasuredWidth(); + + int width = right - left; + int height = bottom - top; + + this.scrollableLength = this.contentMeasuredWidth - width; + CommonLayoutParams.layoutChild(child, 0, 0, Math.max(this.contentMeasuredWidth, width), height); + this.scrollableLength = Math.max(0, this.scrollableLength); + } + + this.mIsLayoutDirty = false; + // Give a child focus if it needs it + if (this.mChildToScrollTo != null && isViewDescendantOf(this.mChildToScrollTo, this)) { + this.scrollToChild(this.mChildToScrollTo); + } + + this.mChildToScrollTo = null; + + int scrollX = this.getScrollX(); + int scrollY = this.getScrollY(); + + if (this.isFirstLayout) { + this.isFirstLayout = false; + + final int scrollRange = Math.max(0, childWidth - (right - left - this.getPaddingLeft() - this.getPaddingRight())); + if (this.mSavedState != null) { + scrollX = (this.isLayoutRtl() == mSavedState.isLayoutRtl) ? mSavedState.scrollPosition : (scrollRange - this.mSavedState.scrollPosition); + mSavedState = null; + } else { + if (this.isLayoutRtl()) { + scrollX = scrollRange - scrollX; + } // mScrollX default value is "0" for LTR + } + // Don't forget to clamp + if (scrollX > scrollRange) { + scrollX = scrollRange; + } else if (scrollX < 0) { + scrollX = 0; + } + } + + // Calling this with the present values causes it to re-claim them + this.scrollTo(scrollX, scrollY); + CommonLayoutParams.restoreOriginalParams(this); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + this.mSavedState = ss; + this.requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = this.getScrollX(); + ss.isLayoutRtl = this.isLayoutRtl(); + return ss; + } + + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) { + this.scrollBy(scrollDelta, 0); + } + } + + private boolean isLayoutRtl() { + return (this.getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + /** + * Return true if child is a descendant of parent, (or equal to the parent). + */ + static boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + static class SavedState extends BaseSavedState { + public int scrollPosition; + public boolean isLayoutRtl; + + SavedState(Parcelable superState) { + super(superState); + } + + public SavedState(Parcel source) { + super(source); + scrollPosition = source.readInt(); + isLayoutRtl = (source.readInt() == 0) ? true : false; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(scrollPosition); + dest.writeInt(isLayoutRtl ? 1 : 0); + } + + @Override + public String toString() { + return "HorizontalScrollView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " scrollPosition=" + scrollPosition + + " isLayoutRtl=" + isLayoutRtl + "}"; + } + + public static final Creator CREATOR + = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/ImageView.java b/widgets/src/main/java/org/nativescript/widgets/ImageView.java new file mode 100644 index 000000000..0d8876f96 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/ImageView.java @@ -0,0 +1,168 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.graphics.*; +import android.graphics.drawable.Drawable; + +/** + * @author hhristov + * + */ +public class ImageView extends android.widget.ImageView { + private float cornerRadius = 0; + private float borderWidth = 0; + + private Path path = new Path(); + private RectF rect = new RectF(); + + private double scaleW = 1; + private double scaleH = 1; + + public ImageView(Context context) { + super(context); + this.setScaleType(ScaleType.FIT_CENTER); + } + + public float getCornerRadius() { + return this.cornerRadius; + } + + public void setCornerRadius(float radius) { + if (radius != this.cornerRadius) { + this.cornerRadius = radius; + this.invalidate(); + } + } + + public float getBorderWidth() { + return this.borderWidth; + } + + public void setBorderWidth(float radius) { + if (radius != this.borderWidth) { + this.borderWidth = radius; + this.invalidate(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + Drawable drawable = this.getDrawable(); + int measureWidth; + int measureHeight; + if (drawable != null) { + measureWidth = drawable.getIntrinsicWidth(); + measureHeight = drawable.getIntrinsicHeight(); + } else { + measureWidth = 0; + measureHeight = 0; + } + + boolean finiteWidth = widthMode != MeasureSpec.UNSPECIFIED; + boolean finiteHeight = heightMode != MeasureSpec.UNSPECIFIED; + + if (measureWidth != 0 && measureHeight != 0 && (finiteWidth || finiteHeight)) { + this.computeScaleFactor(width, height, finiteWidth, finiteHeight, measureWidth, measureHeight); + int resultW = (int) Math.floor(measureWidth * this.scaleW); + int resultH = (int) Math.floor(measureHeight * this.scaleH); + + measureWidth = finiteWidth ? Math.min(resultW, width) : resultW; + measureHeight = finiteHeight ? Math.min(resultH, height) : resultH; + } + + measureWidth += this.getPaddingLeft() + this.getPaddingRight(); + measureHeight += this.getPaddingTop() + this.getPaddingBottom(); + + measureWidth = Math.max(measureWidth, getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, getSuggestedMinimumHeight()); + + if (CommonLayoutParams.debuggable > 0) { + StringBuilder sb = CommonLayoutParams.getStringBuilder(); + sb.append("ImageView onMeasure: "); + sb.append(MeasureSpec.toString(widthMeasureSpec)); + sb.append(", "); + sb.append(MeasureSpec.toString(heightMeasureSpec)); + sb.append(", stretch: "); + sb.append(this.getScaleType()); + sb.append(", measureWidth: "); + sb.append(measureWidth); + sb.append(", measureHeight: "); + sb.append(measureHeight); + + CommonLayoutParams.log(CommonLayoutParams.TAG, sb.toString()); + } + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + private void computeScaleFactor(int measureWidth, int measureHeight, boolean widthIsFinite, boolean heightIsFinite, double nativeWidth, double nativeHeight) { + + this.scaleW = 1; + this.scaleH = 1; + + ScaleType scale = this.getScaleType(); + if ((scale == ScaleType.CENTER_CROP || scale == ScaleType.FIT_CENTER || scale == ScaleType.FIT_XY) && + (widthIsFinite || heightIsFinite)) { + + this.scaleW = (nativeWidth > 0) ? measureWidth / nativeWidth : 0d; + this.scaleH = (nativeHeight > 0) ? measureHeight / nativeHeight : 0d; + + if (!widthIsFinite) { + this.scaleW = scaleH; + } else if (!heightIsFinite) { + this.scaleH = scaleW; + } else { + // No infinite dimensions. + switch (scale) { + case FIT_CENTER: + this.scaleH = this.scaleW < this.scaleH ? this.scaleW : this.scaleH; + this.scaleW = this.scaleH; + break; + case CENTER_CROP: + this.scaleH = this.scaleW > this.scaleH ? this.scaleW : this.scaleH; + this.scaleW = this.scaleH; + break; + default: + break; + } + } + } + } + + @Override + protected void onDraw(Canvas canvas) { + // floor the border width to avoid gaps between the border and the image + float roundedBorderWidth = (float) Math.floor(this.borderWidth); + float innerRadius = Math.max(0, this.cornerRadius - roundedBorderWidth); + + // The border width is included in the padding so there is no need for + // clip if there is no inner border radius. + if (innerRadius != 0) { + this.rect.set( + roundedBorderWidth, + roundedBorderWidth, + this.getWidth() - roundedBorderWidth, + this.getHeight() - roundedBorderWidth); + + this.path.reset(); + this.path.addRoundRect(rect, innerRadius, innerRadius, Path.Direction.CW); + + canvas.clipPath(this.path); + } + + super.onDraw(canvas); + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/ItemSpec.java b/widgets/src/main/java/org/nativescript/widgets/ItemSpec.java new file mode 100644 index 000000000..64fc3a884 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/ItemSpec.java @@ -0,0 +1,60 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public class ItemSpec { + + private int _value; + private GridUnitType _unitType; + + public ItemSpec() { + this(1, GridUnitType.star); + } + + public ItemSpec(int value, GridUnitType unitType) { + this._value = value; + this._unitType = unitType; + } + + GridLayout owner; + int _actualLength = 0; + + @Override + public boolean equals(Object o) { + if (!(o instanceof ItemSpec)) { + return false; + } + + ItemSpec other = (ItemSpec)o; + return (this._unitType == other._unitType) && (this._value == other._value) && (this.owner == other.owner); + } + + public GridUnitType getGridUnitType() { + return this._unitType; + } + + public boolean getIsAbsolute() { + return this._unitType == GridUnitType.pixel; + } + + public boolean getIsAuto() { + return this._unitType == GridUnitType.auto; + } + + public boolean getIsStar() { + return this._unitType == GridUnitType.star; + } + + public int getValue() { + return this._value; + } + + public int getActualLength() { + return this._actualLength; + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java b/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java new file mode 100644 index 000000000..06db0b1ec --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java @@ -0,0 +1,67 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * @author hhristov + * + */ +public abstract class LayoutBase extends ViewGroup { + + public LayoutBase(Context context) { + super(context); + } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new CommonLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new CommonLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkLayoutParams(LayoutParams p) { + return p instanceof CommonLayoutParams; + } + + @Override + protected LayoutParams generateLayoutParams(LayoutParams p) { + return new CommonLayoutParams(); + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + protected static int getGravity(View view) { + int gravity = -1; + LayoutParams params = view.getLayoutParams(); + if (params instanceof FrameLayout.LayoutParams) { + gravity = ((FrameLayout.LayoutParams)params).gravity; + } + + if (gravity == -1) { + gravity = Gravity.FILL; + } + + return gravity; + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/Orientation.java b/widgets/src/main/java/org/nativescript/widgets/Orientation.java new file mode 100644 index 000000000..bd91e6cfc --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/Orientation.java @@ -0,0 +1,13 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public enum Orientation { + horzontal, + vertical +} diff --git a/widgets/src/main/java/org/nativescript/widgets/StackLayout.java b/widgets/src/main/java/org/nativescript/widgets/StackLayout.java new file mode 100644 index 000000000..4186ffac7 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/StackLayout.java @@ -0,0 +1,218 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.Gravity; +import android.view.View; + +/** + * @author hhristov + * + */ +public class StackLayout extends LayoutBase { + + private int _totalLength = 0; + private Orientation _orientation = Orientation.vertical; + + public StackLayout(Context context) { + super(context); + } + + public Orientation getOrientation() { + return this._orientation; + } + public void setOrientation(Orientation value) { + this._orientation = value; + this.requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int childState = 0; + int measureWidth = 0; + int measureHeight = 0; + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + boolean isVertical = this._orientation == Orientation.vertical; + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + int count = this.getChildCount(); + int measureSpecMode; + int remainingLength; + + int mode = isVertical ? heightMode : widthMode; + if (mode == MeasureSpec.UNSPECIFIED) { + measureSpecMode = MeasureSpec.UNSPECIFIED; + remainingLength = 0; + } + else { + measureSpecMode = MeasureSpec.AT_MOST; + remainingLength = isVertical ? height - verticalPadding : width - horizontalPadding; + } + + int childMeasureSpec; + if (isVertical) { + int childWidth = (widthMode == MeasureSpec.UNSPECIFIED) ? 0 : width - horizontalPadding; + childWidth = Math.max(0, childWidth); + childMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, widthMode); + } + else { + int childHeight = (heightMode == MeasureSpec.UNSPECIFIED) ? 0 : height - verticalPadding; + childHeight = Math.max(0, childHeight); + childMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, heightMode); + } + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + if (isVertical) { + CommonLayoutParams.measureChild(child, childMeasureSpec, MeasureSpec.makeMeasureSpec(remainingLength, measureSpecMode)); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + measureWidth = Math.max(measureWidth, childMeasuredWidth); + measureHeight += childMeasuredHeight; + remainingLength = Math.max(0, remainingLength - childMeasuredHeight); + } + else { + CommonLayoutParams.measureChild(child, MeasureSpec.makeMeasureSpec(remainingLength, measureSpecMode), childMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + measureHeight = Math.max(measureHeight, childMeasuredHeight); + measureWidth += childMeasuredWidth; + remainingLength = Math.max(0, remainingLength - childMeasuredWidth); + } + + childState = combineMeasuredStates(childState, child.getMeasuredState()); + } + + // Add in our padding + measureWidth += horizontalPadding; + measureHeight += verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + this._totalLength = isVertical ? measureHeight : measureWidth; + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, isVertical ? childState : 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, isVertical ? 0 : childState); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (this._orientation == Orientation.vertical) { + this.layoutVertical(l, t, r, b); + } + else { + this.layoutHorizontal(l, t, r, b); + } + + CommonLayoutParams.restoreOriginalParams(this); + } + + private void layoutVertical(int left, int top, int right, int bottom) { + + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childTop = 0; + int childLeft = paddingLeft; + int childRight = right - left - paddingRight; + + int gravity = LayoutBase.getGravity(this); + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (verticalGravity) { + case Gravity.CENTER_VERTICAL: + childTop = (bottom - top - this._totalLength) / 2 + paddingTop - paddingBottom; + break; + + case Gravity.BOTTOM: + childTop = bottom - top - this._totalLength + paddingTop - paddingBottom; + break; + + case Gravity.TOP: + case Gravity.FILL_VERTICAL: + default: + childTop = paddingTop; + break; + } + + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + int childHeight = CommonLayoutParams.getDesiredHeight(child); + CommonLayoutParams.layoutChild(child, childLeft, childTop, childRight, childTop + childHeight); + childTop += childHeight; + } + } + + @SuppressLint("RtlHardcoded") + private void layoutHorizontal(int left, int top, int right, int bottom) { + + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childTop = paddingTop; + int childLeft = 0; + int childBottom = bottom - top - paddingBottom; + + int gravity = LayoutBase.getGravity(this); + final int horizontalGravity = Gravity.getAbsoluteGravity(gravity, this.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; + + switch (horizontalGravity) { + case Gravity.CENTER_HORIZONTAL: + childLeft = (right - left - this._totalLength) / 2 + paddingLeft - paddingRight; + break; + + case Gravity.RIGHT: + childLeft = right - left - this._totalLength + paddingLeft - paddingRight; + break; + + case Gravity.LEFT: + case Gravity.FILL_HORIZONTAL: + default: + childLeft = paddingLeft; + break; + } + + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + int childWidth = CommonLayoutParams.getDesiredWidth(child); + CommonLayoutParams.layoutChild(child, childLeft, childTop, childLeft + childWidth, childBottom); + childLeft += childWidth; + } + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/TabItemSpec.java b/widgets/src/main/java/org/nativescript/widgets/TabItemSpec.java new file mode 100644 index 000000000..16bc1e021 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/TabItemSpec.java @@ -0,0 +1,9 @@ +package org.nativescript.widgets; + +import android.graphics.drawable.Drawable; + +public class TabItemSpec { + public String title; + public int iconId; + public Drawable iconDrawable; +} diff --git a/widgets/src/main/java/org/nativescript/widgets/TabLayout.java b/widgets/src/main/java/org/nativescript/widgets/TabLayout.java new file mode 100644 index 000000000..f4eb393f7 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/TabLayout.java @@ -0,0 +1,383 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 org.nativescript.widgets; + +import android.content.Context; +import android.graphics.Typeface; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * To be used with ViewPager to provide a tab indicator component which give + * constant feedback as to the user's scroll progress. + *

+ * To use the component, simply add it to your view hierarchy. Then in your + * {@link android.app.Activity} or {@link android.support.v4.app.Fragment} call + * {@link #setViewPager(ViewPager)} providing it the ViewPager this layout is + * being used for. + *

+ * The colors can be customized in two ways. The first and simplest is to + * provide an array of colors via {@link #setSelectedIndicatorColors(int...)}. + * The alternative is via the {@link TabColorizer} interface which provides you + * complete control over which color is used for any individual position. + *

+ */ +public class TabLayout extends HorizontalScrollView { + /** + * Allows complete control over the colors drawn in the tab layout. Set with + * {@link #setCustomTabColorizer(TabColorizer)}. + */ + public interface TabColorizer { + + /** + * @return return the color of the indicator used when {@code position} + * is selected. + */ + int getIndicatorColor(int position); + + } + + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + private static final int TEXT_MAX_WIDHT = 180; + private static final int SMALL_MIN_HEIGHT = 48; + private static final int LARGE_MIN_HEIGHT = 72; + + private int mTitleOffset; + + private boolean mDistributeEvenly = true; + + private TabItemSpec[] mTabItems; + private ViewPager mViewPager; + private SparseArray mContentDescriptions = new SparseArray(); + private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; + + private final TabStrip mTabStrip; + + public TabLayout(Context context) { + this(context, null); + } + + public TabLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TabLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + // Make sure that the Tab Strips fills this View + setFillViewport(true); + + mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); + + mTabStrip = new TabStrip(context); + addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + /** + * Set the custom {@link TabColorizer} to be used. + * + * If you only require simple customisation then you can use + * {@link #setSelectedIndicatorColors(int...)} to achieve similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + mTabStrip.setCustomTabColorizer(tabColorizer); + } + + public void setDistributeEvenly(boolean distributeEvenly) { + mDistributeEvenly = distributeEvenly; + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors + * are treated as a circular array. Providing one color will mean that all + * tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + mTabStrip.setSelectedIndicatorColors(colors); + } + + /** + * Set the {@link ViewPager.OnPageChangeListener}. When using + * {@link TabLayout} you are required to set any + * {@link ViewPager.OnPageChangeListener} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mViewPagerPageChangeListener = listener; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the + * pager content (number of tabs and tab titles) does not change after this + * call has been made. + */ + public void setViewPager(ViewPager viewPager) { + this.setItems(null, viewPager); + } + + public void setItems(TabItemSpec[] items, ViewPager viewPager) { + mTabStrip.removeAllViews(); + + mViewPager = viewPager; + mTabItems = items; + if (viewPager != null) { + viewPager.addOnPageChangeListener(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + /** + * Updates the UI of an item at specified index + */ + public void updateItemAt(int position, TabItemSpec tabItem) { + LinearLayout ll = (LinearLayout)mTabStrip.getChildAt(position); + ImageView imgView = (ImageView)ll.getChildAt(0); + TextView textView = (TextView)ll.getChildAt(1); + this.setupItem(ll, textView, imgView, tabItem); + } + + /** + * Gets the TextView for tab item at index + */ + public TextView getTextViewForItemAt(int index){ + LinearLayout ll = this.getViewForItemAt(index); + return (ll != null) ? (TextView)ll.getChildAt(1) : null; + } + + /** + * Gets the LinearLayout container for tab item at index + */ + public LinearLayout getViewForItemAt(int index){ + LinearLayout result = null; + + if(this.mTabStrip.getChildCount() > index){ + result = (LinearLayout)this.mTabStrip.getChildAt(index); + } + + return result; + } + + /** + * Create a default view to be used for tabs. + */ + protected View createDefaultTabView(Context context, TabItemSpec tabItem) { + float density = getResources().getDisplayMetrics().density; + int padding = (int) (TAB_VIEW_PADDING_DIPS * density); + + LinearLayout ll = new LinearLayout(context); + ll.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + ll.setGravity(Gravity.CENTER); + ll.setOrientation(LinearLayout.VERTICAL); + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true); + ll.setBackgroundResource(outValue.resourceId); + + ImageView imgView = new ImageView(context); + imgView.setScaleType(ScaleType.FIT_CENTER); + LinearLayout.LayoutParams imgLP = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + imgLP.gravity = Gravity.CENTER; + imgView.setLayoutParams(imgLP); + + TextView textView = new TextView(context); + textView.setGravity(Gravity.CENTER); + textView.setMaxWidth((int) (TEXT_MAX_WIDHT * density)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); + textView.setTypeface(Typeface.DEFAULT_BOLD); + textView.setEllipsize(TextUtils.TruncateAt.END); + textView.setAllCaps(true); + textView.setMaxLines(2); + textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + textView.setPadding(padding, 0, padding, 0); + + this.setupItem(ll, textView, imgView, tabItem); + + ll.addView(imgView); + ll.addView(textView); + return ll; + } + + private void setupItem(LinearLayout ll, TextView textView,ImageView imgView, TabItemSpec tabItem){ + float density = getResources().getDisplayMetrics().density; + + if (tabItem.iconId != 0) { + imgView.setImageResource(tabItem.iconId); + imgView.setVisibility(VISIBLE); + } else if (tabItem.iconDrawable != null) { + imgView.setImageDrawable(tabItem.iconDrawable); + imgView.setVisibility(VISIBLE); + } else { + imgView.setVisibility(GONE); + } + + if (tabItem.title != null && !tabItem.title.isEmpty()) { + textView.setText(tabItem.title); + textView.setVisibility(VISIBLE); + } else { + textView.setVisibility(GONE); + } + + if (imgView.getVisibility() == VISIBLE && textView.getVisibility() == VISIBLE) { + ll.setMinimumHeight((int) (LARGE_MIN_HEIGHT * density)); + } else { + ll.setMinimumHeight((int) (SMALL_MIN_HEIGHT * density)); + } + + if (mDistributeEvenly) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams(); + lp.width = 0; + lp.weight = 1; + } + } + + private void populateTabStrip() { + final PagerAdapter adapter = mViewPager.getAdapter(); + final OnClickListener tabClickListener = new TabClickListener(); + + for (int i = 0; i < adapter.getCount(); i++) { + View tabView = null; + + TabItemSpec tabItem; + if (this.mTabItems != null && this.mTabItems.length > i) { + tabItem = this.mTabItems[i]; + } else { + tabItem = new TabItemSpec(); + tabItem.title = adapter.getPageTitle(i).toString(); + } + + tabView = createDefaultTabView(getContext(), tabItem); + + tabView.setOnClickListener(tabClickListener); + String desc = mContentDescriptions.get(i, null); + if (desc != null) { + tabView.setContentDescription(desc); + } + + mTabStrip.addView(tabView); + if (i == mViewPager.getCurrentItem()) { + tabView.setSelected(true); + } + } + } + + public void setContentDescription(int i, String desc) { + mContentDescriptions.put(i, desc); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mViewPager != null) { + scrollToTab(mViewPager.getCurrentItem(), 0); + } + } + + private void scrollToTab(int tabIndex, int positionOffset) { + final int tabStripChildCount = mTabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + View selectedChild = mTabStrip.getChildAt(tabIndex); + if (selectedChild != null) { + int targetScrollX = selectedChild.getLeft() + positionOffset; + + if (tabIndex > 0 || positionOffset > 0) { + // If we're not at the first child and are mid-scroll, make sure + // we obey the offset + targetScrollX -= mTitleOffset; + } + + scrollTo(targetScrollX, 0); + } + } + + private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onViewPagerPageChanged(position, positionOffset); + + View selectedTitle = mTabStrip.getChildAt(position); + int extraOffset = (selectedTitle != null) ? (int) (positionOffset * selectedTitle.getWidth()) : 0; + scrollToTab(position, extraOffset); + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mTabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + mTabStrip.getChildAt(i).setSelected(position == i); + } + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageSelected(position); + } + } + + } + + private class TabClickListener implements OnClickListener { + @Override + public void onClick(View v) { + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + if (v == mTabStrip.getChildAt(i)) { + mViewPager.setCurrentItem(i); + return; + } + } + } + } +} \ No newline at end of file diff --git a/widgets/src/main/java/org/nativescript/widgets/TabStrip.java b/widgets/src/main/java/org/nativescript/widgets/TabStrip.java new file mode 100644 index 000000000..fa6636e16 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/TabStrip.java @@ -0,0 +1,169 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT 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 org.nativescript.widgets; + + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; + +class TabStrip extends LinearLayout { + + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 0; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 3; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + + private final int mBottomBorderThickness; + private final Paint mBottomBorderPaint; + + private final int mSelectedIndicatorThickness; + private final Paint mSelectedIndicatorPaint; + + private final int mDefaultBottomBorderColor; + + private int mSelectedPosition; + private float mSelectionOffset; + + private TabLayout.TabColorizer mCustomTabColorizer; + private final SimpleTabColorizer mDefaultTabColorizer; + + TabStrip(Context context) { + this(context, null); + } + + TabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, + DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + + mDefaultTabColorizer = new SimpleTabColorizer(); + mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); + + mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + mBottomBorderPaint = new Paint(); + mBottomBorderPaint.setColor(mDefaultBottomBorderColor); + + mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + mSelectedIndicatorPaint = new Paint(); + } + + void setCustomTabColorizer(TabLayout.TabColorizer customTabColorizer) { + mCustomTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void onViewPagerPageChanged(int position, float positionOffset) { + mSelectedPosition = position; + mSelectionOffset = positionOffset; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int height = getHeight(); + final int childCount = getChildCount(); + final TabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null + ? mCustomTabColorizer + : mDefaultTabColorizer; + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mSelectedPosition); + int left = selectedTitle.getLeft(); + int right = selectedTitle.getRight(); + int color = tabColorizer.getIndicatorColor(mSelectedPosition); + + if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, mSelectionOffset); + } + + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mSelectedPosition + 1); + left = (int) (mSelectionOffset * nextTitle.getLeft() + + (1.0f - mSelectionOffset) * left); + right = (int) (mSelectionOffset * nextTitle.getRight() + + (1.0f - mSelectionOffset) * right); + } + + mSelectedIndicatorPaint.setColor(color); + + canvas.drawRect(left, height - mSelectedIndicatorThickness, right, + height, mSelectedIndicatorPaint); + } + + // Thin underline along the entire bottom edge + canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + private static class SimpleTabColorizer implements TabLayout.TabColorizer { + private int[] mIndicatorColors; + + @Override + public final int getIndicatorColor(int position) { + return mIndicatorColors[position % mIndicatorColors.length]; + } + + void setIndicatorColors(int... colors) { + mIndicatorColors = colors; + } + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/VerticalScrollView.java b/widgets/src/main/java/org/nativescript/widgets/VerticalScrollView.java new file mode 100644 index 000000000..53aa9d817 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/VerticalScrollView.java @@ -0,0 +1,191 @@ +/** + * + */ +package org.nativescript.widgets; + +import org.nativescript.widgets.HorizontalScrollView.SavedState; +import android.content.Context; +import android.graphics.Rect; +import android.os.Parcelable; +import android.view.View; +import android.widget.ScrollView; + +/** + * @author hhristov + * + */ +public class VerticalScrollView extends ScrollView { + + private final Rect mTempRect = new Rect(); + + private int contentMeasuredWidth = 0; + private int contentMeasuredHeight = 0; + private int scrollableLength = 0; + private SavedState mSavedState; + private boolean isFirstLayout = true; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + public VerticalScrollView(Context context) { + super(context); + } + + public int getScrollableLength() { + return this.scrollableLength; + } + + @Override + public void requestLayout() { + this.mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!this.mIsLayoutDirty) { + this.scrollToChild(focused); + } + else { + // The child may not be laid out yet, we can't compute the scroll yet + this.mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + // Don't call measure because it will measure content twice. + // ScrollView is expected to have single child so we measure only the first child. + View child = this.getChildCount() > 0 ? this.getChildAt(0) : null; + if (child == null) { + this.scrollableLength = 0; + this.contentMeasuredWidth = 0; + this.contentMeasuredHeight = 0; + this.setPadding(0, 0, 0, 0); + } + else { + CommonLayoutParams.measureChild(child, widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + this.contentMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + this.contentMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + // Android ScrollView does not account to child margins so we set them as paddings. Otherwise you can never scroll to bottom. + CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); + this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); + } + + // Don't add in our paddings because they are already added as child margins. (we will include them twice if we add them). + // check the previous line - this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); +// this.contentMeasuredWidth += this.getPaddingLeft() + this.getPaddingRight(); +// this.contentMeasuredHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum height + this.contentMeasuredWidth = Math.max(this.contentMeasuredWidth, this.getSuggestedMinimumWidth()); + this.contentMeasuredHeight = Math.max(this.contentMeasuredHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(this.contentMeasuredWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(this.contentMeasuredHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childHeight = 0; + if (this.getChildCount() > 0) { + View child = this.getChildAt(0); + childHeight = child.getMeasuredHeight(); + + int width = right - left; + int height = bottom - top; + + this.scrollableLength = this.contentMeasuredHeight - height; + CommonLayoutParams.layoutChild(child, 0, 0, width, Math.max(this.contentMeasuredHeight, height)); + this.scrollableLength = Math.max(0, this.scrollableLength); + } + + this.mIsLayoutDirty = false; + // Give a child focus if it needs it + if (this.mChildToScrollTo != null && HorizontalScrollView.isViewDescendantOf(this.mChildToScrollTo, this)) { + this.scrollToChild(this.mChildToScrollTo); + } + + this.mChildToScrollTo = null; + + int scrollX = this.getScrollX(); + int scrollY = this.getScrollY(); + if (this.isFirstLayout) { + this.isFirstLayout = false; + + final int scrollRange = Math.max(0, childHeight - (bottom - top - this.getPaddingTop() - this.getPaddingBottom())); + if (this.mSavedState != null) { + scrollY = mSavedState.scrollPosition; + mSavedState = null; + } + + // Don't forget to clamp + if (scrollY > scrollRange) { + scrollY = scrollRange; + } else if (scrollY < 0) { + scrollY = 0; + } + } + + // Calling this with the present values causes it to re-claim them + this.scrollTo(scrollX, scrollY); + + CommonLayoutParams.restoreOriginalParams(this); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + this.mSavedState = ss; + this.requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = this.getScrollY(); + return ss; + } + + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) { + this.scrollBy(scrollDelta, 0); + } + } +} diff --git a/widgets/src/main/java/org/nativescript/widgets/WrapLayout.java b/widgets/src/main/java/org/nativescript/widgets/WrapLayout.java new file mode 100644 index 000000000..e246ed923 --- /dev/null +++ b/widgets/src/main/java/org/nativescript/widgets/WrapLayout.java @@ -0,0 +1,237 @@ +/** + * + */ +package org.nativescript.widgets; + +import java.util.ArrayList; +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class WrapLayout extends LayoutBase { + + private int _itemWidth = -1; + private int _itemHeight = -1; + private Orientation _orientation = Orientation.horzontal; + private ArrayList _lengths = new ArrayList(); + + public WrapLayout(Context context) { + super(context); + } + + public Orientation getOrientation() { + return this._orientation; + } + public void setOrientation(Orientation value) { + this._orientation = value; + this.requestLayout(); + } + + public int getItemWidth() { + return this._itemWidth; + } + public void setItemWidth(int value) { + this._itemWidth = value; + this.requestLayout(); + } + + public int getItemHeight() { + return this._itemHeight; + } + public void setItemHeight(int value) { + this._itemHeight = value; + this.requestLayout(); + } + + private static int getViewMeasureSpec(int parentMode, int parentLength, int itemLength) { + if (itemLength > 0) { + return MeasureSpec.makeMeasureSpec(itemLength, MeasureSpec.EXACTLY); + } + else if (parentMode == MeasureSpec.UNSPECIFIED) { + return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + else { + return MeasureSpec.makeMeasureSpec(parentLength, MeasureSpec.AT_MOST); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int measureWidth = 0; + int measureHeight = 0; + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + boolean isVertical = this._orientation == Orientation.vertical; + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + int childWidthMeasureSpec = getViewMeasureSpec(widthMode, width, this._itemWidth); + int childHeightMeasureSpec = getViewMeasureSpec(heightMode, height, this._itemHeight); + + int remainingWidth = widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : width - horizontalPadding; + int remainingHeight = heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : height - verticalPadding; + + int count = this.getChildCount(); + + this._lengths.clear(); + int rowOrColumn = 0; + int maxLength = 0; + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + if (isVertical) { + if (childMeasuredHeight > remainingHeight) { + rowOrColumn++; + maxLength = Math.max(maxLength, measureHeight); + measureHeight = childMeasuredHeight; + remainingWidth = height - childMeasuredHeight; + this._lengths.add(rowOrColumn, childMeasuredWidth); + } + else { + remainingHeight -= childMeasuredHeight; + measureHeight += childMeasuredHeight; + } + } + else { + if (childMeasuredWidth > remainingWidth) { + rowOrColumn++; + maxLength = Math.max(maxLength, measureWidth); + measureWidth = childMeasuredWidth; + remainingWidth = width - childMeasuredWidth; + this._lengths.add(rowOrColumn, childMeasuredHeight); + } + else { + remainingWidth -= childMeasuredWidth; + measureWidth += childMeasuredWidth; + } + } + + if(this._lengths.size() <= rowOrColumn) { + this._lengths.add(rowOrColumn, isVertical ? childMeasuredWidth : childMeasuredHeight); + } + else { + this._lengths.set(rowOrColumn, Math.max(this._lengths.get(rowOrColumn), isVertical ? childMeasuredWidth : childMeasuredHeight)); + } + } + + count = this._lengths.size(); + if (isVertical) { + measureHeight = Math.max(maxLength, measureHeight); + for (int i = 0; i < count; i++) { + measureWidth += this._lengths.get(i); + } + } + else { + measureWidth = Math.max(maxLength, measureWidth); + for (int i = 0; i < count; i++) { + measureHeight += this._lengths.get(i); + } + } + + // Add in our padding + measureWidth += horizontalPadding; + measureHeight += verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + boolean isVertical = this._orientation == Orientation.vertical; + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childLeft = paddingLeft; + int childTop = paddingTop; + int childrenLength = isVertical ? bottom - top - paddingBottom : right - left - paddingRight; + + int rowOrColumn = 0; + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + // Add margins because layoutChild will subtract them. + int childWidth = CommonLayoutParams.getDesiredWidth(child); + int childHeight = CommonLayoutParams.getDesiredHeight(child); + + int length = this._lengths.get(rowOrColumn); + if (isVertical) { + childWidth = length; + childHeight = this._itemHeight > 0 ? this._itemHeight : childHeight; + if (childTop + childHeight > childrenLength) { + // Move to top. + childTop = paddingTop; + + // Move to right with current column width. + childLeft += length; + + // Move to next column. + rowOrColumn++; + + // Take current column width. + childWidth = length = this._lengths.get(rowOrColumn); + } + } + else { + childWidth = this._itemWidth > 0 ? this._itemWidth : childWidth; + childHeight = length; + if (childLeft + childWidth > childrenLength) { + // Move to left. + childLeft = paddingLeft; + + // Move to bottom with current row height. + childTop += length; + + // Move to next column. + rowOrColumn++; + + // Take current row height. + childHeight = length = this._lengths.get(rowOrColumn); + } + } + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childLeft + childWidth, childTop + childHeight); + + if (isVertical) { + // Move next child Top position to bottom. + childTop += childHeight; + } + else { + // Move next child Left position to right. + childLeft += childWidth; + } + } + + CommonLayoutParams.restoreOriginalParams(this); + } +} \ No newline at end of file diff --git a/widgets/src/main/res/values/strings.xml b/widgets/src/main/res/values/strings.xml new file mode 100644 index 000000000..38c1813a7 --- /dev/null +++ b/widgets/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Widgets + diff --git a/widgets/src/test/java/org/nativescript/widgets/ExampleUnitTest.java b/widgets/src/test/java/org/nativescript/widgets/ExampleUnitTest.java new file mode 100644 index 000000000..4f5c4921b --- /dev/null +++ b/widgets/src/test/java/org/nativescript/widgets/ExampleUnitTest.java @@ -0,0 +1,15 @@ +package org.nativescript.widgets; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * To work on unit tests, switch the Test Artifact in the Build Variants view. + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file