diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..47e4981 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Created by http://www.gitignore.io + +### Android ### +# Built application files +*.apk +*.ap_ + +# Files for the Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ + +# Gradle files +.gradle/ +build/ +.gradletasknamecache + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +#Log Files +*.log + + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm + +## Directory-based project format +.idea/ +# if you remove the above rule, at least ignore user-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# and these sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml + +## File-based project format +*.ipr +*.iml +*.iws + +## Additional for IntelliJ +out/ + +# generated by mpeltonen/sbt-idea plugin +.idea_modules/ + +# generated by JIRA plugin +atlassian-ide-plugin.xml + +# generated by Crashlytics plugin (for Android Studio and Intellij) +com_crashlytics_export_strings.xml + + +### Gradle ### +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + + +### Eclipse ### +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse + + +### OSX ### +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0db4d32 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright Telly, Inc. and other contributors. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR +THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8e164f --- /dev/null +++ b/README.md @@ -0,0 +1,114 @@ +Mr. Vector +========== +![Mr. Vector](http://i.imgur.com/ucFr5T7.png) + +AKA VectorDrawableCompat: A 14+ backport of [VectorDrawable](https://developer.android.com/reference/android/graphics/drawable/VectorDrawable.html). + +### Demo + +![Le demo](http://i.imgur.com/nG4uQiN.gif) + +[![Mr. Vector Demo on Google Play Store](http://developer.android.com/images/brand/en_generic_rgb_wo_60.png)](https://play.google.com/store/apps/details?id=com.telly.mrvector.demo) + +### Usage + +See demo, at this point latest version is `0.1.0` + +```groovy +compile 'com.telly:mrvector:(insert latest version)' +``` + +### Basic inflate +```java +Drawable drawable = MrVector.inflate(getResources(), R.drawable.vector_android); +``` + +Unfortunately due some inflate weirdness (able to read some correctly but not others) for now (will fix promise) you'll have to duplicate (sucks I know) all your `android:` attributes, in example: + + +```xml + + + + +``` + +### Inflate from Layout (WIP) + +Use it as a regular drawable: + +```xml + + + android:icon="@drawable/vector_drawable" + +``` + +```xml + + + android:src="@drawable/vector_drawable" + +``` + +And then from your `Application` or `Activity`: + +```java +\\ ... + {{ + MrVector.register( + R.drawable.vector_drawable, + R.drawable.another_vector_drawable, + \\ ... + ); + }} +\\ ... + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(MrVector.wrap(newBase)); + } +\\ ... +``` + +### Roadmap +Right now only basic inflating works, this is the list of features planed: + +- [ ] Put this in GH issues. +- [ ] Full inflate from layout support (partially implemented except for `TypedArray` calling directly `loadDrawable`, which sadly is key) +- [ ] Get rid of `auto` namespace, use `android` namespace as much as possible (no duplicated attributes). +- [ ] Tint support. +- [ ] Animation support ([AnimatedVectorDrawable](https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable.html)). + +On the long run, it would be nice to see (but no promises): + +- [ ] Per node animation. +- [ ] Additional SVG support (e.g. using [svg-android](https://code.google.com/p/svg-android/) or [svgandroid](https://code.google.com/p/androidsvg/)). +- [ ] SVG animation support. + +### License & About + +See LICENSE file, logo built from [opoloo/androidicons](https://github.com/opoloo/androidicons). + +From [@eveliotc](https://plus.google.com/u/0/+EvelioTarazonaC%C3%A1ceres/posts) @ [Telly](https://telly.com/) \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..7c69fb2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,22 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + version = VERSION_NAME + group = GROUP + + repositories { + mavenCentral() + } +} diff --git a/demo/build.gradle b/demo/build.gradle new file mode 100644 index 0000000..a0e4885 --- /dev/null +++ b/demo/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.android.application' + +dependencies { + compile project(':library') + compile "com.android.support:appcompat-v7:${project.ANDROID_SUPPORT_VERSION}@aar" +} + +android { + compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION) + buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION + + defaultConfig { + applicationId "${project.GROUP}.${project.POM_ARTIFACT_ID}.demo" + minSdkVersion Integer.parseInt(project.ANDROID_BUILD_MIN_SDK_VERSION) + targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION) + versionName project.VERSION_NAME + versionCode Integer.parseInt(project.VERSION_CODE) + buildConfigField "String", "ABOUT_URL", "\"${project.POM_URL}\"" + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} diff --git a/demo/proguard-rules.txt b/demo/proguard-rules.txt new file mode 100644 index 0000000..00c828e --- /dev/null +++ b/demo/proguard-rules.txt @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /opt/homebrew-cask/Caskroom/android-studio-bundle/0.4.2 build-133.970939/Android Studio.app/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/demo/src/main/AndroidManifest.xml b/demo/src/main/AndroidManifest.xml new file mode 100644 index 0000000..444ffc2 --- /dev/null +++ b/demo/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/demo/src/main/java/com/telly/mrvector/demo/BaseActivity.java b/demo/src/main/java/com/telly/mrvector/demo/BaseActivity.java new file mode 100644 index 0000000..e36c0f1 --- /dev/null +++ b/demo/src/main/java/com/telly/mrvector/demo/BaseActivity.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) Telly, Inc. and other contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.telly.mrvector.demo; + +import android.content.Intent; +import android.net.Uri; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; + +import static android.content.Intent.ACTION_VIEW; +import static android.content.Intent.CATEGORY_BROWSABLE; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static com.telly.mrvector.demo.BuildConfig.ABOUT_URL; + +abstract class BaseActivity extends ActionBarActivity { + private static final Uri ABOUT_URI = Uri.parse(ABOUT_URL); + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_menu, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final int id = item.getItemId(); + switch (id) { + case R.id.action_about: + openAboutPage(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + T viewById(int id) { + return (T) super.findViewById(id); + } + + void openAboutPage() { + Intent intent = new Intent(ACTION_VIEW); + intent.addFlags(FLAG_ACTIVITY_NEW_TASK); + intent.setData(ABOUT_URI); + intent.addCategory(CATEGORY_BROWSABLE); + startActivity(intent); + } + +} diff --git a/demo/src/main/java/com/telly/mrvector/demo/BasicInflateActivity.java b/demo/src/main/java/com/telly/mrvector/demo/BasicInflateActivity.java new file mode 100644 index 0000000..0b669ff --- /dev/null +++ b/demo/src/main/java/com/telly/mrvector/demo/BasicInflateActivity.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) Telly, Inc. and other contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.telly.mrvector.demo; + +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.View; +import android.widget.ImageButton; + +import com.telly.mrvector.MrVector; + +import java.util.Random; + +public class BasicInflateActivity extends BaseActivity implements View.OnClickListener { + private static final Random sRandom = new Random(System.currentTimeMillis()); + private static final int[] sDrawables = { + R.drawable.logo, + R.drawable.vector_android + }; + private ImageButton mButton; + private int mLastResId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_basic_inflate); + + mButton = viewById(R.id.button); + mButton.setOnClickListener(this); + nextDrawable(); + } + + @Override + public void onClick(View v) { + nextDrawable(); + } + + private void nextDrawable() { + int resId; + do { + resId = sDrawables[sRandom.nextInt(sDrawables.length)]; + } while (resId == mLastResId); + mLastResId = resId; + Drawable drawable = MrVector.inflate(getResources(), resId); + mButton.setImageDrawable(drawable); + } +} diff --git a/demo/src/main/java/com/telly/mrvector/demo/InflateFromLayoutActivity.java b/demo/src/main/java/com/telly/mrvector/demo/InflateFromLayoutActivity.java new file mode 100644 index 0000000..ac5b7b6 --- /dev/null +++ b/demo/src/main/java/com/telly/mrvector/demo/InflateFromLayoutActivity.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) Telly, Inc. and other contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.telly.mrvector.demo; + +import android.content.Context; +import android.os.Bundle; +import android.view.Menu; + +import com.telly.mrvector.MrVector; + +public class InflateFromLayoutActivity extends BaseActivity { + {{ // Must be static, Ideally you would register all the vector drawables at once + MrVector.register( + R.drawable.vector_android, + R.drawable.sample_vector_drawable + ); + }} + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_inflate_from_layout); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.layout_inflate_menu, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override + protected void attachBaseContext(Context newBase) { + // This allows inflation magic + // Ideally you would do this in your BaseActivity or Application instead of per activity + super.attachBaseContext(MrVector.wrap(newBase)); + } + +} diff --git a/demo/src/main/res/drawable-hdpi/ic_launcher.png b/demo/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100755 index 0000000..0001cc0 Binary files /dev/null and b/demo/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/demo/src/main/res/drawable-mdpi/ic_launcher.png b/demo/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100755 index 0000000..ad79a36 Binary files /dev/null and b/demo/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/demo/src/main/res/drawable-xhdpi/ic_launcher.png b/demo/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100755 index 0000000..8006054 Binary files /dev/null and b/demo/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/demo/src/main/res/drawable-xxhdpi/ic_launcher.png b/demo/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100755 index 0000000..951d607 Binary files /dev/null and b/demo/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/demo/src/main/res/drawable-xxxhdpi/ic_launcher.png b/demo/src/main/res/drawable-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000..915adc9 Binary files /dev/null and b/demo/src/main/res/drawable-xxxhdpi/ic_launcher.png differ diff --git a/demo/src/main/res/drawable/e.xml b/demo/src/main/res/drawable/e.xml new file mode 100644 index 0000000..c9b2549 --- /dev/null +++ b/demo/src/main/res/drawable/e.xml @@ -0,0 +1,44 @@ + + + + + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/drawable/logo.xml b/demo/src/main/res/drawable/logo.xml new file mode 100644 index 0000000..dcf735e --- /dev/null +++ b/demo/src/main/res/drawable/logo.xml @@ -0,0 +1,34 @@ + + + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/drawable/sample_vector_drawable.xml b/demo/src/main/res/drawable/sample_vector_drawable.xml new file mode 100644 index 0000000..bb41f08 --- /dev/null +++ b/demo/src/main/res/drawable/sample_vector_drawable.xml @@ -0,0 +1,48 @@ + + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/drawable/simple.xml b/demo/src/main/res/drawable/simple.xml new file mode 100644 index 0000000..9c134b6 --- /dev/null +++ b/demo/src/main/res/drawable/simple.xml @@ -0,0 +1,38 @@ + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/drawable/vector_android.xml b/demo/src/main/res/drawable/vector_android.xml new file mode 100644 index 0000000..058731e --- /dev/null +++ b/demo/src/main/res/drawable/vector_android.xml @@ -0,0 +1,35 @@ + + + + + + diff --git a/demo/src/main/res/layout/activity_basic_inflate.xml b/demo/src/main/res/layout/activity_basic_inflate.xml new file mode 100644 index 0000000..7a8bbe2 --- /dev/null +++ b/demo/src/main/res/layout/activity_basic_inflate.xml @@ -0,0 +1,30 @@ + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/layout/activity_inflate_from_layout.xml b/demo/src/main/res/layout/activity_inflate_from_layout.xml new file mode 100644 index 0000000..39d08f7 --- /dev/null +++ b/demo/src/main/res/layout/activity_inflate_from_layout.xml @@ -0,0 +1,30 @@ + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/menu/layout_inflate_menu.xml b/demo/src/main/res/menu/layout_inflate_menu.xml new file mode 100644 index 0000000..bcd5f7c --- /dev/null +++ b/demo/src/main/res/menu/layout_inflate_menu.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/demo/src/main/res/menu/main_menu.xml b/demo/src/main/res/menu/main_menu.xml new file mode 100644 index 0000000..9d84a39 --- /dev/null +++ b/demo/src/main/res/menu/main_menu.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/demo/src/main/res/values-w820dp/dimens.xml b/demo/src/main/res/values-w820dp/dimens.xml new file mode 100644 index 0000000..4edec9f --- /dev/null +++ b/demo/src/main/res/values-w820dp/dimens.xml @@ -0,0 +1,3 @@ + + 64dp + diff --git a/demo/src/main/res/values/colors.xml b/demo/src/main/res/values/colors.xml new file mode 100644 index 0000000..eead1c6 --- /dev/null +++ b/demo/src/main/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #ffa4c639 + #ff819b26 + #000000 + #000000 + \ No newline at end of file diff --git a/demo/src/main/res/values/dimens.xml b/demo/src/main/res/values/dimens.xml new file mode 100644 index 0000000..a9fd312 --- /dev/null +++ b/demo/src/main/res/values/dimens.xml @@ -0,0 +1,7 @@ + + 16dp + 16dp + 52dp + 20dp + 69dp + diff --git a/demo/src/main/res/values/integers.xml b/demo/src/main/res/values/integers.xml new file mode 100644 index 0000000..2d8fdec --- /dev/null +++ b/demo/src/main/res/values/integers.xml @@ -0,0 +1,7 @@ + + + 520 + 200 + 5 + 69 + \ No newline at end of file diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml new file mode 100644 index 0000000..71b7bb4 --- /dev/null +++ b/demo/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + + Mr. Vector Demo + Mr. Vector + About + Link + M32.2695312,59.671875 L232.296875,33.7929688 + M32.87501,1.01244444 C15.1159,1.91374444 1.0001,16.6554444 1,34.6218444 C1.0001,53.1681444 16.0465,68.2313444 34.60941,68.2313444 C53.17241,68.2313444 68.21871,53.1681444 68.21891,34.6218444 C68.21871,16.6556444 54.10341,1.91604444 36.34391,1.01244444 C35.77091,0.984444444 35.18961,1.01244444 34.60941,1.01244444 C34.02921,1.01244444 33.44781,0.984444444 32.87501,1.01244444 L32.87501,1.01244444 Z M33.15641,5.93434444 C33.64521,5.90934444 34.11431,5.93434444 34.60941,5.93434444 C35.10461,5.93434444 35.57351,5.90934444 36.06251,5.93434444 C51.22131,6.70584444 63.29661,19.2869444 63.29691,34.6218444 C63.29691,50.4519444 50.45361,63.2627444 34.60941,63.2624444 C18.7652,63.2624444 5.9222,50.4519444 5.9219,34.6218444 C5.9219,19.2869444 17.9974,6.70444444 33.15641,5.93434444 L33.15641,5.93434444 Z M23.92191,21.3563444 C21.58401,21.3563444 19.70321,23.2392444 19.70321,25.5749444 C19.70321,27.9107444 21.58401,29.8406444 23.92191,29.8406444 C26.25971,29.8406444 28.14071,27.9107444 28.14071,25.5749444 C28.14071,23.2392444 26.25971,21.3563444 23.92191,21.3563444 L23.92191,21.3563444 Z M45.29691,21.3563444 C42.95921,21.3563444 41.07821,23.2392444 41.07821,25.5749444 C41.07821,27.9107444 42.95921,29.8406444 45.29691,29.8406444 C47.63481,29.8406444 49.51571,27.9107444 49.51571,25.5749444 C49.51571,23.2392444 47.63481,21.3563444 45.29691,21.3563444 L45.29691,21.3563444 Z + M29.226855,30.54045 C25.574755,30.54045 24.281655,34.01055 21.323655,35.30965 C18.365355,36.60885 14.456755,36.56955 10.921855,33.94705 C10.921855,39.24925 17.892155,44.84835 24.911855,44.84835 C30.176455,44.84835 33.340355,42.33365 34.586855,39.35225 C35.833355,42.33365 38.997055,44.84835 44.261555,44.84835 C51.281455,44.84835 58.297055,39.24925 58.297055,33.94705 C54.762355,36.56955 50.853455,36.60885 47.895455,35.30965 C44.937355,34.01055 43.598955,30.54045 39.946555,30.54045 C37.373355,30.54045 35.493155,31.38725 34.586855,33.31115 C33.680555,31.38725 31.800155,30.54045 29.226855,30.54045 L29.226855,30.54045 Z + diff --git a/demo/src/main/res/values/styles.xml b/demo/src/main/res/values/styles.xml new file mode 100644 index 0000000..2117c31 --- /dev/null +++ b/demo/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/extras/logo.sketch b/extras/logo.sketch new file mode 100644 index 0000000..1a3fa7b Binary files /dev/null and b/extras/logo.sketch differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..1101e44 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,25 @@ +VERSION_NAME=0.1.0 +VERSION_CODE=1 +GROUP=com.telly + +POM_NAME=Mr. Vector Library +POM_ARTIFACT_ID=mrvector +POM_PACKAGING=aar + +POM_DESCRIPTION=Mr. Vector (VectorDrawableCompat): A 14+ backport of VectorDrawable +POM_URL=https://github.com/telly/MrVector +POM_SCM_URL=https://github.com/telly/MrVector +POM_SCM_CONNECTION=scm:git@github.com:telly/MrVector.git +POM_SCM_DEV_CONNECTION=scm:git@github.com:telly/MrVector.git +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo +POM_DEVELOPER_ID=eveliotc +POM_DEVELOPER_NAME=Evelio Tarazona Caceres + +ANDROID_BUILD_MIN_SDK_VERSION=14 +ANDROID_BUILD_TARGET_SDK_VERSION=21 +ANDROID_BUILD_SDK_VERSION=21 +ANDROID_BUILD_TOOLS_VERSION=21.1.2 + +ANDROID_SUPPORT_VERSION=21.0.3 \ 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 0000000..8c0fb64 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 0000000..e683fb5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Dec 19 17:59:17 COT 2014 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/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 + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# 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\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +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"` + + # 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 0000000..aec9973 --- /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/library/build.gradle b/library/build.gradle new file mode 100644 index 0000000..f7c2a6d --- /dev/null +++ b/library/build.gradle @@ -0,0 +1,26 @@ +apply plugin: 'com.android.library' + +dependencies { + compile "com.android.support:support-annotations:${project.ANDROID_SUPPORT_VERSION}" + compile "com.android.support:support-v4:${project.ANDROID_SUPPORT_VERSION}@aar" +} + +android { + compileSdkVersion Integer.parseInt(project.ANDROID_BUILD_SDK_VERSION) + buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION + + defaultConfig { + minSdkVersion Integer.parseInt(project.ANDROID_BUILD_MIN_SDK_VERSION) + targetSdkVersion Integer.parseInt(project.ANDROID_BUILD_TARGET_SDK_VERSION) + versionName project.VERSION_NAME + versionCode Integer.parseInt(project.VERSION_CODE) + } + + buildTypes { + release { + proguardFiles getDefaultProguardFile('proguard-android.pro'), 'proguard-rules.txt' + } + } +} + +apply from: './gradle-mvn-push.gradle' \ No newline at end of file diff --git a/library/gradle-mvn-push.gradle b/library/gradle-mvn-push.gradle new file mode 100644 index 0000000..27fff0d --- /dev/null +++ b/library/gradle-mvn-push.gradle @@ -0,0 +1,114 @@ +/* + * Copyright 2013 Chris Banes + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'maven' +apply plugin: 'signing' + +def isReleaseBuild() { + return VERSION_NAME.contains("SNAPSHOT") == false +} + +def getReleaseRepositoryUrl() { + return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL + : "https://oss.sonatype.org/service/local/staging/deploy/maven2/" +} + +def getSnapshotRepositoryUrl() { + return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL + : "https://oss.sonatype.org/content/repositories/snapshots/" +} + +def getRepositoryUsername() { + return hasProperty('NEXUS_USERNAME') ? NEXUS_USERNAME : "" +} + +def getRepositoryPassword() { + return hasProperty('NEXUS_PASSWORD') ? NEXUS_PASSWORD : "" +} + +afterEvaluate { project -> + uploadArchives { + repositories { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + pom.groupId = GROUP + pom.artifactId = POM_ARTIFACT_ID + pom.version = VERSION_NAME + + repository(url: getReleaseRepositoryUrl()) { + authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) + } + snapshotRepository(url: getSnapshotRepositoryUrl()) { + authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) + } + + pom.project { + name POM_NAME + packaging POM_PACKAGING + description POM_DESCRIPTION + url POM_URL + + scm { + url POM_SCM_URL + connection POM_SCM_CONNECTION + developerConnection POM_SCM_DEV_CONNECTION + } + + licenses { + license { + name POM_LICENCE_NAME + url POM_LICENCE_URL + distribution POM_LICENCE_DIST + } + } + + developers { + developer { + id POM_DEVELOPER_ID + name POM_DEVELOPER_NAME + } + } + } + } + } + } + + signing { + required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } + sign configurations.archives + } + + task androidJavadocs(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + } + + task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { + classifier = 'javadoc' + from androidJavadocs.destinationDir + } + + task androidSourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.srcDirs + } + + artifacts { + archives androidSourcesJar + archives androidJavadocsJar + } +} diff --git a/library/proguard-rules.txt b/library/proguard-rules.txt new file mode 100644 index 0000000..cb8998d --- /dev/null +++ b/library/proguard-rules.txt @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Applications/Android Studio.app/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# 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 *; +#} \ No newline at end of file diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..7845c95 --- /dev/null +++ b/library/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/library/src/main/java/android/content/res/MrResources.java b/library/src/main/java/android/content/res/MrResources.java new file mode 100644 index 0000000..c72f0ff --- /dev/null +++ b/library/src/main/java/android/content/res/MrResources.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) Telly, Inc. and other contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.content.res; + +import android.annotation.TargetApi; +import android.graphics.drawable.Drawable; +import android.os.Build; + +import com.telly.mrvector.MrVector; + +/** + * @hide + */ +public class MrResources extends Resources { + private final Resources mResources; + + public MrResources(Resources resources) { + super(resources.getAssets(), resources.getDisplayMetrics(), resources.getConfiguration()); + mResources = resources; + } + + public boolean oldFor(Resources superResources) { + return superResources != mResources; + } + + @Override + public Drawable getDrawable(int id) throws NotFoundException { + Drawable mr = lookup(id); + if (mr != null) { + return mr; + } + return super.getDrawable(id); + } + + @Override + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) + public Drawable getDrawableForDensity(int id, int density) throws NotFoundException { + Drawable mr = lookup(id, density); + if (mr != null) { + return mr; + } + return super.getDrawableForDensity(id, density); + } + + @Override + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Drawable getDrawable(int id, Theme theme) throws NotFoundException { + return super.getDrawable(id, theme); + } + + + @Override + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public Drawable getDrawableForDensity(int id, int density, Theme theme) { + return super.getDrawableForDensity(id, density, theme); + } + + /* TODO some sorcery to make the calls from TypedArray work + + Drawable loadDrawable(TypedValue value, int id) throws Resources.NotFoundException { + Log.d("vector", "loadDrawable@2 " + id); + return super.loadDrawable(value, id); + } + + Drawable loadDrawable(TypedValue value, int id, Theme theme) throws Resources.NotFoundException { + Log.d("vector", "loadDrawable@3 " + id); + return super.loadDrawable(value, id, theme); + } + */ + + + private Drawable lookup(int id) { + return MrVector.lookup(mResources, id, 0, false); + } + + private Drawable lookup(int id, int density) { + return MrVector.lookup(mResources, id, density, true); + } + +} \ No newline at end of file diff --git a/library/src/main/java/android/support/v4/util/IntSet.java b/library/src/main/java/android/support/v4/util/IntSet.java new file mode 100644 index 0000000..5de4b0b --- /dev/null +++ b/library/src/main/java/android/support/v4/util/IntSet.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) Telly, Inc. and other contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.util; + +import java.util.Arrays; + +import static android.support.v4.util.ContainerHelpers.binarySearch; +import static android.support.v4.util.ContainerHelpers.idealIntArraySize; + +public class IntSet implements Cloneable { + private static final int DELETED = 0; + private int[] mElements; + private int mSize; + private boolean mContainsDeleted; + + /** + * Creates a new ResArray containing no mappings. + */ + public IntSet() { + this(10); + } + + /** + * Creates a new ResArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public IntSet(int initialCapacity) { + if (initialCapacity == 0) { + mElements = ContainerHelpers.EMPTY_INTS; + } else { + mElements = new int[idealIntArraySize(initialCapacity)]; + } + mSize = 0; + } + + @Override + public IntSet clone() { + IntSet clone = null; + try { + clone = (IntSet) super.clone(); + clone.mElements = mElements.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + public void remove(int element) { + if (element == DELETED) { + mContainsDeleted = false; + return; + } + + int i = binarySearch(mElements, mSize, element); + + if (i >= 0) { + mElements[i] = DELETED; + } + } + + public void add(int element) { + if (element == DELETED) { + mContainsDeleted = true; + return; + } + + if (contains(element)) { + return; // already there + } else { + mElements = append(mElements, mSize, element); + mSize++; + } + } + + public void addAll(int[] elements) { + if (elements == null) { + return; + } + for (int element : elements) { + add(element); + } + } + + public boolean contains(int element) { + if (element == DELETED) { + return mContainsDeleted; + } + return binarySearch(mElements, mSize, element) >= 0; + } + + static int[] append(int[] array, int currentSize, int element) { + assert currentSize <= array.length; + + if (currentSize + 1 > array.length) { + int[] newArray = new int[idealIntArraySize(currentSize)]; + System.arraycopy(array, 0, newArray, 0, currentSize); + array = newArray; + } + array[currentSize] = element; + return array; + } + + public int size() { + return mSize; + } + + public void clear() { + mSize = 0; + Arrays.fill(mElements, DELETED); + } + + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i = 0; i < mSize; i++) { + if (i > 0) { + buffer.append(", "); + } + + int element = mElements[i]; + buffer.append(element); + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/library/src/main/java/com/telly/mrvector/MrVector.java b/library/src/main/java/com/telly/mrvector/MrVector.java new file mode 100644 index 0000000..cf8cd7c --- /dev/null +++ b/library/src/main/java/com/telly/mrvector/MrVector.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) Telly, Inc. and other contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.telly.mrvector; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.MrResources; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.support.annotation.DrawableRes; +import android.support.v4.util.IntSet; +import android.util.Log; + +import static com.telly.mrvector.Utils.LOLLIPOP_PLUS; + +/** + * Facade for creating Drawables in a compatible way + */ +public class MrVector { + /** + * Inflates a drawable, using framework implementation when available + * @param resources + * Resources to use for inflation + * @param resId + * drawable resource + * @return + *

Framework {@link android.graphics.drawable.VectorDrawable} if running lollipop or later.

+ *

{@link com.telly.mrvector.VectorDrawable} otherwise.

+ * + * @see #inflateCompatOnly(android.content.res.Resources, int) + */ + public static Drawable inflate(Resources resources, @DrawableRes int resId) { + if (LOLLIPOP_PLUS) { + return resources.getDrawable(resId); + } else { + return inflateCompatOnly(resources, resId); + } + } + + /** + * Inflates a drawable, using {@link com.telly.mrvector.VectorDrawable} implementation always. + * @param resources + * Resources to use for inflation + * @param resId + * drawable resource + * @return + *

Inflated instance of {@link com.telly.mrvector.VectorDrawable}.

+ * + * @see #inflate(android.content.res.Resources, int) + */ + public static Drawable inflateCompatOnly(Resources resources, @DrawableRes int resId) { + return VectorDrawable.create(resources, resId); + } + + private static IntSet sVectorResources; + public static void register(@DrawableRes int... resources) { + if (resources == null || resources.length < 1) { + return; + } + if (sVectorResources == null) { + sVectorResources = new IntSet(resources.length); + } + sVectorResources.addAll(resources); + } + + public static Context wrap(Context context) { + return new MrResourcesContext(context); + } + + static class MrResourcesContext extends ContextWrapper { + private MrResources mMrResources; + public MrResourcesContext(Context base) { + super(base); + } + + @Override + public Resources getResources() { + final Resources superResources = super.getResources(); + if (mMrResources == null || mMrResources.oldFor(superResources)) { + mMrResources = new MrResources(superResources); + } + return mMrResources; + } + } + + /** + * @hide + */ + public static Drawable lookup(Resources res, int id, int density, boolean ignoreDensity) { + Log.d(VectorDrawable.LOGTAG, "Looking up res " + id); + if (sVectorResources == null || !sVectorResources.contains(id)) { + Log.d(VectorDrawable.LOGTAG, "Could not find res " + id); + return null; + } + // TODO support density + Log.d(VectorDrawable.LOGTAG, "Inflating res " + id); + return inflateCompatOnly(res, id); + } + +} diff --git a/library/src/main/java/com/telly/mrvector/PathParser.java b/library/src/main/java/com/telly/mrvector/PathParser.java new file mode 100644 index 0000000..e5220e0 --- /dev/null +++ b/library/src/main/java/com/telly/mrvector/PathParser.java @@ -0,0 +1,654 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package com.telly.mrvector; + +import android.annotation.TargetApi; +import android.graphics.Path; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; + +import static android.os.Build.VERSION_CODES.LOLLIPOP; + +/** + * @hide + */ +@TargetApi(LOLLIPOP) +public class PathParser { + static final String LOGTAG = PathParser.class.getSimpleName(); + + /** + * @param pathData The string representing a path, the same as "d" string in svg file. + * @return the generated Path object. + */ + public static Path createPathFromPathData(String pathData) { + Path path = new Path(); + PathDataNode[] nodes = createNodesFromPathData(pathData); + if (nodes != null) { + PathDataNode.nodesToPath(nodes, path); + return path; + } + return null; + } + + /** + * @param pathData The string representing a path, the same as "d" string in svg file. + * @return an array of the PathDataNode. + */ + public static PathDataNode[] createNodesFromPathData(String pathData) { + if (pathData == null) { + return null; + } + int start = 0; + int end = 1; + + ArrayList list = new ArrayList(); + while (end < pathData.length()) { + end = nextStart(pathData, end); + String s = pathData.substring(start, end).trim(); + if (s.length() > 0) { + float[] val = getFloats(s); + addNode(list, s.charAt(0), val); + } + + start = end; + end++; + } + if ((end - start) == 1 && start < pathData.length()) { + addNode(list, pathData.charAt(start), new float[0]); + } + return list.toArray(new PathDataNode[list.size()]); + } + + /** + * @param source The array of PathDataNode to be duplicated. + * @return a deep copy of the source. + */ + public static PathDataNode[] deepCopyNodes(PathDataNode[] source) { + if (source == null) { + return null; + } + PathDataNode[] copy = new PathParser.PathDataNode[source.length]; + for (int i = 0; i < source.length; i ++) { + copy[i] = new PathDataNode(source[i]); + } + return copy; + } + + /** + * @param nodesFrom The source path represented in an array of PathDataNode + * @param nodesTo The target path represented in an array of PathDataNode + * @return whether the nodesFrom can morph into nodesTo + */ + public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) { + if (nodesFrom == null || nodesTo == null) { + return false; + } + + if (nodesFrom.length != nodesTo.length) { + return false; + } + + for (int i = 0; i < nodesFrom.length; i ++) { + if (nodesFrom[i].mType != nodesTo[i].mType + || nodesFrom[i].mParams.length != nodesTo[i].mParams.length) { + return false; + } + } + return true; + } + + /** + * Update the target's data to match the source. + * Before calling this, make sure canMorph(target, source) is true. + * + * @param target The target path represented in an array of PathDataNode + * @param source The source path represented in an array of PathDataNode + */ + public static void updateNodes(PathDataNode[] target, PathDataNode[] source) { + for (int i = 0; i < source.length; i ++) { + target[i].mType = source[i].mType; + for (int j = 0; j < source[i].mParams.length; j ++) { + target[i].mParams[j] = source[i].mParams[j]; + } + } + } + + private static int nextStart(String s, int end) { + char c; + + while (end < s.length()) { + c = s.charAt(end); + if (((c - 'A') * (c - 'Z') <= 0) || (((c - 'a') * (c - 'z') <= 0))) { + return end; + } + end++; + } + return end; + } + + private static void addNode(ArrayList list, char cmd, float[] val) { + list.add(new PathDataNode(cmd, val)); + } + + private static class ExtractFloatResult { + // We need to return the position of the next separator and whether the + // next float starts with a '-'. + int mEndPosition; + boolean mEndWithNegSign; + } + + /** + * Parse the floats in the string. + * This is an optimized version of parseFloat(s.split(",|\\s")); + * + * @param s the string containing a command and list of floats + * @return array of floats + */ + private static float[] getFloats(String s) { + if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') { + return new float[0]; + } + try { + float[] results = new float[s.length()]; + int count = 0; + int startPosition = 1; + int endPosition = 0; + + ExtractFloatResult result = new ExtractFloatResult(); + int totalLength = s.length(); + + // The startPosition should always be the first character of the + // current number, and endPosition is the character after the current + // number. + while (startPosition < totalLength) { + extract(s, startPosition, result); + endPosition = result.mEndPosition; + + if (startPosition < endPosition) { + results[count++] = Float.parseFloat( + s.substring(startPosition, endPosition)); + } + + if (result.mEndWithNegSign) { + // Keep the '-' sign with next number. + startPosition = endPosition; + } else { + startPosition = endPosition + 1; + } + } + return Arrays.copyOf(results, count); + } catch (NumberFormatException e) { + Log.e(LOGTAG, "error in parsing \"" + s + "\""); + throw e; + } + } + + /** + * Calculate the position of the next comma or space or negative sign + * @param s the string to search + * @param start the position to start searching + * @param result the result of the extraction, including the position of the + * the starting position of next number, whether it is ending with a '-'. + */ + private static void extract(String s, int start, ExtractFloatResult result) { + // Now looking for ' ', ',' or '-' from the start. + int currentIndex = start; + boolean foundSeparator = false; + result.mEndWithNegSign = false; + for (; currentIndex < s.length(); currentIndex++) { + char currentChar = s.charAt(currentIndex); + switch (currentChar) { + case ' ': + case ',': + foundSeparator = true; + break; + case '-': + if (currentIndex != start) { + foundSeparator = true; + result.mEndWithNegSign = true; + } + break; + } + if (foundSeparator) { + break; + } + } + // When there is nothing found, then we put the end position to the end + // of the string. + result.mEndPosition = currentIndex; + } + + /** + * Each PathDataNode represents one command in the "d" attribute of the svg + * file. + * An array of PathDataNode can represent the whole "d" attribute. + */ + public static class PathDataNode { + private char mType; + private float[] mParams; + + private PathDataNode(char type, float[] params) { + mType = type; + mParams = params; + } + + private PathDataNode(PathDataNode n) { + mType = n.mType; + mParams = Arrays.copyOf(n.mParams, n.mParams.length); + } + + /** + * Convert an array of PathDataNode to Path. + * + * @param node The source array of PathDataNode. + * @param path The target Path object. + */ + public static void nodesToPath(PathDataNode[] node, Path path) { + float[] current = new float[4]; + char previousCommand = 'm'; + for (int i = 0; i < node.length; i++) { + addCommand(path, current, previousCommand, node[i].mType, node[i].mParams); + previousCommand = node[i].mType; + } + } + + /** + * The current PathDataNode will be interpolated between the + * nodeFrom and nodeTo according to the + * fraction. + * + * @param nodeFrom The start value as a PathDataNode. + * @param nodeTo The end value as a PathDataNode + * @param fraction The fraction to interpolate. + */ + public void interpolatePathDataNode(PathDataNode nodeFrom, + PathDataNode nodeTo, float fraction) { + for (int i = 0; i < nodeFrom.mParams.length; i++) { + mParams[i] = nodeFrom.mParams[i] * (1 - fraction) + + nodeTo.mParams[i] * fraction; + } + } + + private static void addCommand(Path path, float[] current, + char previousCmd, char cmd, float[] val) { + + int incr = 2; + float currentX = current[0]; + float currentY = current[1]; + float ctrlPointX = current[2]; + float ctrlPointY = current[3]; + float reflectiveCtrlPointX; + float reflectiveCtrlPointY; + + switch (cmd) { + case 'z': + case 'Z': + path.close(); + return; + case 'm': + case 'M': + case 'l': + case 'L': + case 't': + case 'T': + incr = 2; + break; + case 'h': + case 'H': + case 'v': + case 'V': + incr = 1; + break; + case 'c': + case 'C': + incr = 6; + break; + case 's': + case 'S': + case 'q': + case 'Q': + incr = 4; + break; + case 'a': + case 'A': + incr = 7; + break; + } + for (int k = 0; k < val.length; k += incr) { + switch (cmd) { + case 'm': // moveto - Start a new sub-path (relative) + path.rMoveTo(val[k + 0], val[k + 1]); + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'M': // moveto - Start a new sub-path + path.moveTo(val[k + 0], val[k + 1]); + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'l': // lineto - Draw a line from the current point (relative) + path.rLineTo(val[k + 0], val[k + 1]); + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'L': // lineto - Draw a line from the current point + path.lineTo(val[k + 0], val[k + 1]); + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'z': // closepath - Close the current subpath + case 'Z': // closepath - Close the current subpath + path.close(); + break; + case 'h': // horizontal lineto - Draws a horizontal line (relative) + path.rLineTo(val[k + 0], 0); + currentX += val[k + 0]; + break; + case 'H': // horizontal lineto - Draws a horizontal line + path.lineTo(val[k + 0], currentY); + currentX = val[k + 0]; + break; + case 'v': // vertical lineto - Draws a vertical line from the current point (r) + path.rLineTo(0, val[k + 0]); + currentY += val[k + 0]; + break; + case 'V': // vertical lineto - Draws a vertical line from the current point + path.lineTo(currentX, val[k + 0]); + currentY = val[k + 0]; + break; + case 'c': // curveto - Draws a cubic Bézier curve (relative) + path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], + val[k + 4], val[k + 5]); + + ctrlPointX = currentX + val[k + 2]; + ctrlPointY = currentY + val[k + 3]; + currentX += val[k + 4]; + currentY += val[k + 5]; + + break; + case 'C': // curveto - Draws a cubic Bézier curve + path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3], + val[k + 4], val[k + 5]); + currentX = val[k + 4]; + currentY = val[k + 5]; + ctrlPointX = val[k + 2]; + ctrlPointY = val[k + 3]; + break; + case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp) + reflectiveCtrlPointX = 0; + reflectiveCtrlPointY = 0; + if (previousCmd == 'c' || previousCmd == 's' + || previousCmd == 'C' || previousCmd == 'S') { + reflectiveCtrlPointX = currentX - ctrlPointX; + reflectiveCtrlPointY = currentY - ctrlPointY; + } + path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1], + val[k + 2], val[k + 3]); + + ctrlPointX = currentX + val[k + 0]; + ctrlPointY = currentY + val[k + 1]; + currentX += val[k + 2]; + currentY += val[k + 3]; + break; + case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp) + reflectiveCtrlPointX = currentX; + reflectiveCtrlPointY = currentY; + if (previousCmd == 'c' || previousCmd == 's' + || previousCmd == 'C' || previousCmd == 'S') { + reflectiveCtrlPointX = 2 * currentX - ctrlPointX; + reflectiveCtrlPointY = 2 * currentY - ctrlPointY; + } + path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = val[k + 0]; + ctrlPointY = val[k + 1]; + currentX = val[k + 2]; + currentY = val[k + 3]; + break; + case 'q': // Draws a quadratic Bézier (relative) + path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = currentX + val[k + 0]; + ctrlPointY = currentY + val[k + 1]; + currentX += val[k + 2]; + currentY += val[k + 3]; + break; + case 'Q': // Draws a quadratic Bézier + path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]); + ctrlPointX = val[k + 0]; + ctrlPointY = val[k + 1]; + currentX = val[k + 2]; + currentY = val[k + 3]; + break; + case 't': // Draws a quadratic Bézier curve(reflective control point)(relative) + reflectiveCtrlPointX = 0; + reflectiveCtrlPointY = 0; + if (previousCmd == 'q' || previousCmd == 't' + || previousCmd == 'Q' || previousCmd == 'T') { + reflectiveCtrlPointX = currentX - ctrlPointX; + reflectiveCtrlPointY = currentY - ctrlPointY; + } + path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1]); + ctrlPointX = currentX + reflectiveCtrlPointX; + ctrlPointY = currentY + reflectiveCtrlPointY; + currentX += val[k + 0]; + currentY += val[k + 1]; + break; + case 'T': // Draws a quadratic Bézier curve (reflective control point) + reflectiveCtrlPointX = currentX; + reflectiveCtrlPointY = currentY; + if (previousCmd == 'q' || previousCmd == 't' + || previousCmd == 'Q' || previousCmd == 'T') { + reflectiveCtrlPointX = 2 * currentX - ctrlPointX; + reflectiveCtrlPointY = 2 * currentY - ctrlPointY; + } + path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, + val[k + 0], val[k + 1]); + ctrlPointX = reflectiveCtrlPointX; + ctrlPointY = reflectiveCtrlPointY; + currentX = val[k + 0]; + currentY = val[k + 1]; + break; + case 'a': // Draws an elliptical arc + // (rx ry x-axis-rotation large-arc-flag sweep-flag x y) + drawArc(path, + currentX, + currentY, + val[k + 5] + currentX, + val[k + 6] + currentY, + val[k + 0], + val[k + 1], + val[k + 2], + val[k + 3] != 0, + val[k + 4] != 0); + currentX += val[k + 5]; + currentY += val[k + 6]; + ctrlPointX = currentX; + ctrlPointY = currentY; + break; + case 'A': // Draws an elliptical arc + drawArc(path, + currentX, + currentY, + val[k + 5], + val[k + 6], + val[k + 0], + val[k + 1], + val[k + 2], + val[k + 3] != 0, + val[k + 4] != 0); + currentX = val[k + 5]; + currentY = val[k + 6]; + ctrlPointX = currentX; + ctrlPointY = currentY; + break; + } + previousCmd = cmd; + } + current[0] = currentX; + current[1] = currentY; + current[2] = ctrlPointX; + current[3] = ctrlPointY; + } + + private static void drawArc(Path p, + float x0, + float y0, + float x1, + float y1, + float a, + float b, + float theta, + boolean isMoreThanHalf, + boolean isPositiveArc) { + + /* Convert rotation angle from degrees to radians */ + double thetaD = Math.toRadians(theta); + /* Pre-compute rotation matrix entries */ + double cosTheta = Math.cos(thetaD); + double sinTheta = Math.sin(thetaD); + /* Transform (x0, y0) and (x1, y1) into unit space */ + /* using (inverse) rotation, followed by (inverse) scale */ + double x0p = (x0 * cosTheta + y0 * sinTheta) / a; + double y0p = (-x0 * sinTheta + y0 * cosTheta) / b; + double x1p = (x1 * cosTheta + y1 * sinTheta) / a; + double y1p = (-x1 * sinTheta + y1 * cosTheta) / b; + + /* Compute differences and averages */ + double dx = x0p - x1p; + double dy = y0p - y1p; + double xm = (x0p + x1p) / 2; + double ym = (y0p + y1p) / 2; + /* Solve for intersecting unit circles */ + double dsq = dx * dx + dy * dy; + if (dsq == 0.0) { + Log.w(LOGTAG, " Points are coincident"); + return; /* Points are coincident */ + } + double disc = 1.0 / dsq - 1.0 / 4.0; + if (disc < 0.0) { + Log.w(LOGTAG, "Points are too far apart " + dsq); + float adjust = (float) (Math.sqrt(dsq) / 1.99999); + drawArc(p, x0, y0, x1, y1, a * adjust, + b * adjust, theta, isMoreThanHalf, isPositiveArc); + return; /* Points are too far apart */ + } + double s = Math.sqrt(disc); + double sdx = s * dx; + double sdy = s * dy; + double cx; + double cy; + if (isMoreThanHalf == isPositiveArc) { + cx = xm - sdy; + cy = ym + sdx; + } else { + cx = xm + sdy; + cy = ym - sdx; + } + + double eta0 = Math.atan2((y0p - cy), (x0p - cx)); + + double eta1 = Math.atan2((y1p - cy), (x1p - cx)); + + double sweep = (eta1 - eta0); + if (isPositiveArc != (sweep >= 0)) { + if (sweep > 0) { + sweep -= 2 * Math.PI; + } else { + sweep += 2 * Math.PI; + } + } + + cx *= a; + cy *= b; + double tcx = cx; + cx = cx * cosTheta - cy * sinTheta; + cy = tcx * sinTheta + cy * cosTheta; + + arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); + } + + /** + * Converts an arc to cubic Bezier segments and records them in p. + * + * @param p The target for the cubic Bezier segments + * @param cx The x coordinate center of the ellipse + * @param cy The y coordinate center of the ellipse + * @param a The radius of the ellipse in the horizontal direction + * @param b The radius of the ellipse in the vertical direction + * @param e1x E(eta1) x coordinate of the starting point of the arc + * @param e1y E(eta2) y coordinate of the starting point of the arc + * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane + * @param start The start angle of the arc on the ellipse + * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse + */ + private static void arcToBezier(Path p, + double cx, + double cy, + double a, + double b, + double e1x, + double e1y, + double theta, + double start, + double sweep) { + // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html + // and http://www.spaceroots.org/documents/ellipse/node22.html + + // Maximum of 45 degrees per cubic Bezier segment + int numSegments = Math.abs((int) Math.ceil(sweep * 4 / Math.PI)); + + double eta1 = start; + double cosTheta = Math.cos(theta); + double sinTheta = Math.sin(theta); + double cosEta1 = Math.cos(eta1); + double sinEta1 = Math.sin(eta1); + double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1); + double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1); + + double anglePerSegment = sweep / numSegments; + for (int i = 0; i < numSegments; i++) { + double eta2 = eta1 + anglePerSegment; + double sinEta2 = Math.sin(eta2); + double cosEta2 = Math.cos(eta2); + double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2); + double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2); + double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; + double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; + double tanDiff2 = Math.tan((eta2 - eta1) / 2); + double alpha = + Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3; + double q1x = e1x + alpha * ep1x; + double q1y = e1y + alpha * ep1y; + double q2x = e2x - alpha * ep2x; + double q2y = e2y - alpha * ep2y; + + p.cubicTo((float) q1x, + (float) q1y, + (float) q2x, + (float) q2y, + (float) e2x, + (float) e2y); + eta1 = eta2; + e1x = e2x; + e1y = e2y; + ep1x = ep2x; + ep1y = ep2y; + } + } + } +} diff --git a/library/src/main/java/com/telly/mrvector/Utils.java b/library/src/main/java/com/telly/mrvector/Utils.java new file mode 100644 index 0000000..159464d --- /dev/null +++ b/library/src/main/java/com/telly/mrvector/Utils.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) Telly, Inc. and other contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.telly.mrvector; + +import android.annotation.TargetApi; +import android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.util.Log; + +import static android.graphics.PorterDuff.Mode; +import static android.graphics.PorterDuff.Mode.SRC_IN; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.util.LayoutDirection.LTR; +import static com.telly.mrvector.VectorDrawable.LOGTAG; + +/** + * @hide + */ +public class Utils { + /** + * Stolen from Drawable + */ + static final Mode DEFAULT_TINT_MODE = SRC_IN; + static final boolean LOLLIPOP_PLUS = SDK_INT >= LOLLIPOP; + + public static T tryInvoke(Object target, String methodName, Object... args) { + final int argsCount = args == null ? 0 : args.length; + Class[] argTypes = new Class[argsCount]; + for (int i = 0; i < argsCount; i++) { + argTypes[i] = args[i].getClass(); + } + + return tryInvoke(target, methodName, argTypes, args); + } + + public static T tryInvoke(Object target, String methodName, Class[] argTypes, + Object... args) { + try { + return (T) target.getClass().getDeclaredMethod(methodName, argTypes).invoke(target, args); + } catch (Exception pokemon) { + Log.e(LOGTAG, "Unable to invoke " + methodName + " on " + target, pokemon); + } + + return null; + } + + static int getLayoutDirection(Drawable drawable) { + final Integer layoutDirection = tryInvoke(drawable, "getLayoutDirection", (Object[])null); + return layoutDirection == null ? LTR : layoutDirection.intValue(); + } + + /** + * Parses a {@link Mode} from a tintMode + * attribute's enum value. + * + * @hide + */ + static Mode parseTintMode(int value, Mode defaultMode) { + switch (value) { + case 3: return Mode.SRC_OVER; + case 5: return Mode.SRC_IN; + case 9: return Mode.SRC_ATOP; + case 14: return Mode.MULTIPLY; + case 15: return Mode.SCREEN; + case 16: return Mode.ADD; + default: return defaultMode; + } + } + + /** + * Ensures the tint filter is consistent with the current tint color and + * mode. + */ + static PorterDuffColorFilter updateTintFilter(Drawable drawable, PorterDuffColorFilter tintFilter, ColorStateList tint, + PorterDuff.Mode tintMode) { + if (tint == null || tintMode == null) { + return null; + } + + final int color = tint.getColorForState(drawable.getState(), Color.TRANSPARENT); + if (tintFilter == null || !LOLLIPOP_PLUS) { // TODO worth caching them? + return new PorterDuffColorFilter(color, tintMode); + } + + tryInvoke(tintFilter, "setColor", color); + tryInvoke(tintFilter, "setMode", tintMode); + return tintFilter; + } + + @TargetApi(LOLLIPOP) + static int getChangingConfigurations(TypedArray a) { + if (LOLLIPOP_PLUS) { + return a.getChangingConfigurations(); + } + return 0; + } +} diff --git a/library/src/main/java/com/telly/mrvector/VectorDrawable.java b/library/src/main/java/com/telly/mrvector/VectorDrawable.java new file mode 100644 index 0000000..039d853 --- /dev/null +++ b/library/src/main/java/com/telly/mrvector/VectorDrawable.java @@ -0,0 +1,1588 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package com.telly.mrvector; + +import android.annotation.TargetApi; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Drawable; +import android.support.v4.util.ArrayMap; +import android.util.AttributeSet; +import android.util.Log; +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Stack; + +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.util.LayoutDirection.RTL; +import static com.telly.mrvector.Utils.DEFAULT_TINT_MODE; +import static com.telly.mrvector.Utils.LOLLIPOP_PLUS; +import static com.telly.mrvector.Utils.updateTintFilter; + +/** + * This lets you create a drawable based on an XML vector graphic. It can be + * defined in an XML file with the <vector> element. + *

+ * The vector drawable has the following elements: + *

+ *

<vector>
+ *
+ *
Used to defined a vector drawable + *
+ *
android:name
+ *
Defines the name of this vector drawable.
+ *
android:width
+ *
Used to defined the intrinsic width of the drawable. + * This support all the dimension units, normally specified with dp.
+ *
android:height
+ *
Used to defined the intrinsic height the drawable. + * This support all the dimension units, normally specified with dp.
+ *
android:viewportWidth
+ *
Used to defined the width of the viewport space. Viewport is basically + * the virtual canvas where the paths are drawn on.
+ *
android:viewportHeight
+ *
Used to defined the height of the viewport space. Viewport is basically + * the virtual canvas where the paths are drawn on.
+ *
android:tint
+ *
The color to apply to the drawable as a tint. By default, no tint is applied.
+ *
android:tintMode
+ *
The Porter-Duff blending mode for the tint color. The default value is src_in.
+ *
android:autoMirrored
+ *
Indicates if the drawable needs to be mirrored when its layout direction is + * RTL (right-to-left).
+ *
android:alpha
+ *
The opacity of this drawable.
+ *
+ *
+ * + *
+ *
<group>
+ *
Defines a group of paths or subgroups, plus transformation information. + * The transformations are defined in the same coordinates as the viewport. + * And the transformations are applied in the order of scale, rotate then translate. + *
+ *
android:name
+ *
Defines the name of the group.
+ *
android:rotation
+ *
The degrees of rotation of the group.
+ *
android:pivotX
+ *
The X coordinate of the pivot for the scale and rotation of the group. + * This is defined in the viewport space.
+ *
android:pivotY
+ *
The Y coordinate of the pivot for the scale and rotation of the group. + * This is defined in the viewport space.
+ *
android:scaleX
+ *
The amount of scale on the X Coordinate.
+ *
android:scaleY
+ *
The amount of scale on the Y coordinate.
+ *
android:translateX
+ *
The amount of translation on the X coordinate. + * This is defined in the viewport space.
+ *
android:translateY
+ *
The amount of translation on the Y coordinate. + * This is defined in the viewport space.
+ *
+ *
+ * + *
+ *
<path>
+ *
Defines paths to be drawn. + *
+ *
android:name
+ *
Defines the name of the path.
+ *
android:pathData
+ *
Defines path string. This is using exactly same format as "d" attribute + * in the SVG's path data. This is defined in the viewport space.
+ *
android:fillColor
+ *
Defines the color to fill the path (none if not present).
+ *
android:strokeColor
+ *
Defines the color to draw the path outline (none if not present).
+ *
android:strokeWidth
+ *
The width a path stroke.
+ *
android:strokeAlpha
+ *
The opacity of a path stroke.
+ *
android:fillAlpha
+ *
The opacity to fill the path with.
+ *
android:trimPathStart
+ *
The fraction of the path to trim from the start, in the range from 0 to 1.
+ *
android:trimPathEnd
+ *
The fraction of the path to trim from the end, in the range from 0 to 1.
+ *
android:trimPathOffset
+ *
Shift trim region (allows showed region to include the start and end), in the range + * from 0 to 1.
+ *
android:strokeLineCap
+ *
Sets the linecap for a stroked path: butt, round, square.
+ *
android:strokeLineJoin
+ *
Sets the lineJoin for a stroked path: miter,round,bevel.
+ *
android:strokeMiterLimit
+ *
Sets the Miter limit for a stroked path.
+ *
+ *
+ * + *
+ *
<clip-path>
+ *
Defines path to be the current clip. + *
+ *
android:name
+ *
Defines the name of the clip path.
+ *
android:pathData
+ *
Defines clip path string. This is using exactly same format as "d" attribute + * in the SVG's path data.
+ *
+ *
+ *
  • Here is a simple VectorDrawable in this vectordrawable.xml file. + *
    + * <vector xmlns:android="http://schemas.android.com/apk/res/android"
    + *     android:height="64dp"
    + *     android:width="64dp"
    + *     android:viewportHeight="600"
    + *     android:viewportWidth="600" >
    + *     <group
    + *         android:name="rotationGroup"
    + *         android:pivotX="300.0"
    + *         android:pivotY="300.0"
    + *         android:rotation="45.0" >
    + *         <path
    + *             android:name="v"
    + *             android:fillColor="#000000"
    + *             android:pathData="M300,70 l 0,-70 70,70 0,0 -70,70z" />
    + *     </group>
    + * </vector>
    + * 
  • + */ + +public class VectorDrawable extends Drawable { + static final String LOGTAG = VectorDrawable.class.getSimpleName(); + + private static final String SHAPE_CLIP_PATH = "clip-path"; + private static final String SHAPE_GROUP = "group"; + private static final String SHAPE_PATH = "path"; + private static final String SHAPE_VECTOR = "vector"; + + private static final int LINECAP_BUTT = 0; + private static final int LINECAP_ROUND = 1; + private static final int LINECAP_SQUARE = 2; + + private static final int LINEJOIN_MITER = 0; + private static final int LINEJOIN_ROUND = 1; + private static final int LINEJOIN_BEVEL = 2; + + private static final boolean DBG_VECTOR_DRAWABLE = false; + + private VectorDrawableState mVectorState; + + private PorterDuffColorFilter mTintFilter; + private ColorFilter mColorFilter; + + private boolean mMutated; + + // AnimatedVectorDrawable needs to turn off the cache all the time, otherwise, + // caching the bitmap by default is allowed. + private boolean mAllowCaching = true; + + public VectorDrawable() { + mVectorState = new VectorDrawableState(); + } + + private VectorDrawable(VectorDrawableState state, Resources res, Theme theme) { + if (theme != null && state.canApplyThemeCompat()) { + // If we need to apply a theme, implicitly mutate. + mVectorState = new VectorDrawableState(state); + applyTheme(theme); + } else { + mVectorState = state; + } + + mTintFilter = updateTintFilter(this, mTintFilter, state.mTint, state.mTintMode); + } + + @Override + public Drawable mutate() { + if (!mMutated && super.mutate() == this) { + mVectorState = new VectorDrawableState(mVectorState); + mMutated = true; + } + return this; + } + + Object getTargetByName(String name) { + return mVectorState.mVPathRenderer.mVGTargetsMap.get(name); + } + + @Override + public ConstantState getConstantState() { + mVectorState.mChangingConfigurations = getChangingConfigurations(); + return mVectorState; + } + + @Override + public void draw(Canvas canvas) { + final Rect bounds = getBounds(); + if (bounds.width() == 0 || bounds.height() == 0) { + // too small to draw + return; + } + + final int saveCount = canvas.save(); + final boolean needMirroring = needMirroring(); + + canvas.translate(bounds.left, bounds.top); + if (needMirroring) { + canvas.translate(bounds.width(), 0); + canvas.scale(-1.0f, 1.0f); + } + + // Color filters always override tint filters. + final ColorFilter colorFilter = mColorFilter == null ? mTintFilter : mColorFilter; + + if (!mAllowCaching) { + // AnimatedVectorDrawable + if (!mVectorState.hasTranslucentRoot()) { + mVectorState.mVPathRenderer.draw( + canvas, bounds.width(), bounds.height(), colorFilter); + } else { + mVectorState.createCachedBitmapIfNeeded(bounds); + mVectorState.updateCachedBitmap(bounds); + mVectorState.drawCachedBitmapWithRootAlpha(canvas, colorFilter); + } + } else { + // Static Vector Drawable case. + mVectorState.createCachedBitmapIfNeeded(bounds); + if (!mVectorState.canReuseCache()) { + mVectorState.updateCachedBitmap(bounds); + mVectorState.updateCacheStates(); + } + mVectorState.drawCachedBitmapWithRootAlpha(canvas, colorFilter); + } + + canvas.restoreToCount(saveCount); + } + + @Override + public int getAlpha() { + return mVectorState.mVPathRenderer.getRootAlpha(); + } + + @Override + public void setAlpha(int alpha) { + if (mVectorState.mVPathRenderer.getRootAlpha() != alpha) { + mVectorState.mVPathRenderer.setRootAlpha(alpha); + invalidateSelf(); + } + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mColorFilter = colorFilter; + invalidateSelf(); + } + + @Override + public void setTintList(ColorStateList tint) { + final VectorDrawableState state = mVectorState; + if (state.mTint != tint) { + state.mTint = tint; + mTintFilter = updateTintFilter(this, mTintFilter, tint, state.mTintMode); + invalidateSelf(); + } + } + + @Override + public void setTintMode(Mode tintMode) { + final VectorDrawableState state = mVectorState; + if (state.mTintMode != tintMode) { + state.mTintMode = tintMode; + mTintFilter = updateTintFilter(this, mTintFilter, state.mTint, tintMode); + invalidateSelf(); + } + } + + @Override + public boolean isStateful() { + return super.isStateful() || (mVectorState != null && mVectorState.mTint != null + && mVectorState.mTint.isStateful()); + } + + @Override + protected boolean onStateChange(int[] stateSet) { + final VectorDrawableState state = mVectorState; + if (state.mTint != null && state.mTintMode != null) { + mTintFilter = updateTintFilter(this, mTintFilter, state.mTint, state.mTintMode); + invalidateSelf(); + return true; + } + return false; + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public int getIntrinsicWidth() { + return (int) mVectorState.mVPathRenderer.mBaseWidth; + } + + @Override + public int getIntrinsicHeight() { + return (int) mVectorState.mVPathRenderer.mBaseHeight; + } + + @Override + @TargetApi(LOLLIPOP) + public boolean canApplyTheme() { + // TODO THEME Not supported yet + if (!LOLLIPOP_PLUS) { + return false; + } + return super.canApplyTheme() || mVectorState != null && mVectorState.canApplyTheme(); + } + + @Override + @TargetApi(LOLLIPOP) + public void applyTheme(Theme t) { + if (LOLLIPOP_PLUS) { + super.applyTheme(t); + } + + final VectorDrawableState state = mVectorState; + if (state != null && state.mThemeAttrs != null) { + /* TODO THEME Not supported yet + final TypedArray a = t.resolveAttributes(state.mThemeAttrs, R.styleable.VectorDrawable); + try { + state.mCacheDirty = true; + updateStateFromTypedArray(a); + } catch (XmlPullParserException e) { + throw new RuntimeException(e); + } finally { + a.recycle(); + } + */ + + mTintFilter = updateTintFilter(this, mTintFilter, state.mTint, state.mTintMode); + } + + final VPathRenderer path = state.mVPathRenderer; + if (path != null && path.canApplyTheme()) { + path.applyTheme(t); + } + } + + /** + * The size of a pixel when scaled from the intrinsic dimension to the viewport dimension. + * This is used to calculate the path animation accuracy. + * + * @hide + */ + public float getPixelSize() { + if (mVectorState == null && mVectorState.mVPathRenderer == null || + mVectorState.mVPathRenderer.mBaseWidth == 0 || + mVectorState.mVPathRenderer.mBaseHeight == 0 || + mVectorState.mVPathRenderer.mViewportHeight == 0 || + mVectorState.mVPathRenderer.mViewportWidth == 0) { + return 1; // fall back to 1:1 pixel mapping. + } + float intrinsicWidth = mVectorState.mVPathRenderer.mBaseWidth; + float intrinsicHeight = mVectorState.mVPathRenderer.mBaseHeight; + float viewportWidth = mVectorState.mVPathRenderer.mViewportWidth; + float viewportHeight = mVectorState.mVPathRenderer.mViewportHeight; + float scaleX = viewportWidth / intrinsicWidth; + float scaleY = viewportHeight / intrinsicHeight; + return Math.min(scaleX, scaleY); + } + + /** @hide */ + static VectorDrawable create(Resources resources, int rid) { + try { + final XmlPullParser parser = resources.getXml(rid); + final AttributeSet attrs = Xml.asAttributeSet(parser); + int type; + while ((type=parser.next()) != XmlPullParser.START_TAG && + type != XmlPullParser.END_DOCUMENT) { + // Empty loop + } + if (type != XmlPullParser.START_TAG) { + throw new XmlPullParserException("No start tag found"); + } + + final VectorDrawable drawable = new VectorDrawable(); + drawable.inflate(resources, parser, attrs); + + return drawable; + } catch (XmlPullParserException e) { + Log.e(LOGTAG, "parser error", e); + } catch (IOException e) { + Log.e(LOGTAG, "parser error", e); + } + return null; + } + + private static int applyAlpha(int color, float alpha) { + int alphaBytes = Color.alpha(color); + color &= 0x00FFFFFF; + color |= ((int) (alphaBytes * alpha)) << 24; + return color; + } + + @Override + public void inflate(Resources res, XmlPullParser parser, AttributeSet attrs) + throws XmlPullParserException, IOException { + // TODO THEME Not supported yet + Theme theme = null; + final VectorDrawableState state = mVectorState; + final VPathRenderer pathRenderer = new VPathRenderer(); + state.mVPathRenderer = pathRenderer; + + final TypedArray a = res.obtainAttributes(attrs, R.styleable.VectorDrawable); + updateStateFromTypedArray(a); + a.recycle(); + + state.mCacheDirty = true; + inflateInternal(res, parser, attrs, theme); + + mTintFilter = updateTintFilter(this, mTintFilter, state.mTint, state.mTintMode); + } + + private void updateStateFromTypedArray(TypedArray a) throws XmlPullParserException { + final VectorDrawableState state = mVectorState; + final VPathRenderer pathRenderer = state.mVPathRenderer; + + // Account for any configuration changes. + state.mChangingConfigurations |= Utils.getChangingConfigurations(a); + + // Extract the theme attributes, if any. + state.mThemeAttrs = null; // TODO THEME TINT Not supported yet a.extractThemeAttrs(); + + final int tintMode = a.getInt(R.styleable.VectorDrawable_tintMode, -1); + if (tintMode != -1) { + state.mTintMode = Utils.parseTintMode(tintMode, DEFAULT_TINT_MODE); + } + + final ColorStateList tint = a.getColorStateList(R.styleable.VectorDrawable_tint); + if (tint != null) { + state.mTint = tint; + } + + state.mAutoMirrored = a.getBoolean( + R.styleable.VectorDrawable_autoMirrored, state.mAutoMirrored); + + pathRenderer.mViewportWidth = a.getFloat( + R.styleable.VectorDrawable_viewportWidth, pathRenderer.mViewportWidth); + pathRenderer.mViewportHeight = a.getFloat( + R.styleable.VectorDrawable_viewportHeight, pathRenderer.mViewportHeight); + + if (pathRenderer.mViewportWidth <= 0) { + throw new XmlPullParserException(a.getPositionDescription() + + " tag requires viewportWidth > 0"); + } else if (pathRenderer.mViewportHeight <= 0) { + throw new XmlPullParserException(a.getPositionDescription() + + " tag requires viewportHeight > 0"); + } + + pathRenderer.mBaseWidth = a.getDimension( + R.styleable.VectorDrawable_width, pathRenderer.mBaseWidth); + pathRenderer.mBaseHeight = a.getDimension( + R.styleable.VectorDrawable_height, pathRenderer.mBaseHeight); + + if (pathRenderer.mBaseWidth <= 0) { + throw new XmlPullParserException(a.getPositionDescription() + + " tag requires width > 0"); + } else if (pathRenderer.mBaseHeight <= 0) { + throw new XmlPullParserException(a.getPositionDescription() + + " tag requires height > 0"); + } + + final float alphaInFloat = a.getFloat(R.styleable.VectorDrawable_alpha, + pathRenderer.getAlpha()); + pathRenderer.setAlpha(alphaInFloat); + + final String name = a.getString(R.styleable.VectorDrawable_name); + if (name != null) { + pathRenderer.mRootName = name; + pathRenderer.mVGTargetsMap.put(name, pathRenderer); + } + } + + private void inflateInternal(Resources res, XmlPullParser parser, AttributeSet attrs, + Theme theme) throws XmlPullParserException, IOException { + final VectorDrawableState state = mVectorState; + final VPathRenderer pathRenderer = state.mVPathRenderer; + boolean noPathTag = true; + + // Use a stack to help to build the group tree. + // The top of the stack is always the current group. + final Stack groupStack = new Stack(); + groupStack.push(pathRenderer.mRootGroup); + + int eventType = parser.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + final String tagName = parser.getName(); + final VGroup currentGroup = groupStack.peek(); + + if (SHAPE_PATH.equals(tagName)) { + final VFullPath path = new VFullPath(); + path.inflate(res, attrs, theme); + currentGroup.mChildren.add(path); + if (path.getPathName() != null) { + pathRenderer.mVGTargetsMap.put(path.getPathName(), path); + } + noPathTag = false; + state.mChangingConfigurations |= path.mChangingConfigurations; + } else if (SHAPE_CLIP_PATH.equals(tagName)) { + final VClipPath path = new VClipPath(); + path.inflate(res, attrs, theme); + currentGroup.mChildren.add(path); + if (path.getPathName() != null) { + pathRenderer.mVGTargetsMap.put(path.getPathName(), path); + } + state.mChangingConfigurations |= path.mChangingConfigurations; + } else if (SHAPE_GROUP.equals(tagName)) { + VGroup newChildGroup = new VGroup(); + newChildGroup.inflate(res, attrs, theme); + currentGroup.mChildren.add(newChildGroup); + groupStack.push(newChildGroup); + if (newChildGroup.getGroupName() != null) { + pathRenderer.mVGTargetsMap.put(newChildGroup.getGroupName(), + newChildGroup); + } + state.mChangingConfigurations |= newChildGroup.mChangingConfigurations; + } + } else if (eventType == XmlPullParser.END_TAG) { + final String tagName = parser.getName(); + if (SHAPE_GROUP.equals(tagName)) { + groupStack.pop(); + } + } + eventType = parser.next(); + } + + // Print the tree out for debug. + if (DBG_VECTOR_DRAWABLE) { + printGroupTree(pathRenderer.mRootGroup, 0); + } + + if (noPathTag) { + final StringBuffer tag = new StringBuffer(); + + if (tag.length() > 0) { + tag.append(" or "); + } + tag.append(SHAPE_PATH); + + throw new XmlPullParserException("no " + tag + " defined"); + } + } + + private void printGroupTree(VGroup currentGroup, int level) { + String indent = ""; + for (int i = 0; i < level; i++) { + indent += " "; + } + // Print the current node + Log.v(LOGTAG, indent + "current group is :" + currentGroup.getGroupName() + + " rotation is " + currentGroup.mRotate); + Log.v(LOGTAG, indent + "matrix is :" + currentGroup.getLocalMatrix().toString()); + // Then print all the children groups + for (int i = 0; i < currentGroup.mChildren.size(); i++) { + Object child = currentGroup.mChildren.get(i); + if (child instanceof VGroup) { + printGroupTree((VGroup) child, level + 1); + } + } + } + + @Override + public int getChangingConfigurations() { + return super.getChangingConfigurations() | mVectorState.mChangingConfigurations; + } + + void setAllowCaching(boolean allowCaching) { + mAllowCaching = allowCaching; + } + + private boolean needMirroring() { + return isAutoMirrored() && Utils.getLayoutDirection(this) == RTL; + } + + @Override + public void setAutoMirrored(boolean mirrored) { + if (mVectorState.mAutoMirrored != mirrored) { + mVectorState.mAutoMirrored = mirrored; + invalidateSelf(); + } + } + + @Override + public boolean isAutoMirrored() { + return mVectorState.mAutoMirrored; + } + + private static class VectorDrawableState extends ConstantState { + int[] mThemeAttrs; + int mChangingConfigurations; + VPathRenderer mVPathRenderer; + ColorStateList mTint = null; + Mode mTintMode = DEFAULT_TINT_MODE; + boolean mAutoMirrored; + + Bitmap mCachedBitmap; + int[] mCachedThemeAttrs; + ColorStateList mCachedTint; + Mode mCachedTintMode; + int mCachedRootAlpha; + boolean mCachedAutoMirrored; + boolean mCacheDirty; + + /** Temporary paint object used to draw cached bitmaps. */ + Paint mTempPaint; + + // Deep copy for mutate() or implicitly mutate. + public VectorDrawableState(VectorDrawableState copy) { + if (copy != null) { + mThemeAttrs = copy.mThemeAttrs; + mChangingConfigurations = copy.mChangingConfigurations; + mVPathRenderer = new VPathRenderer(copy.mVPathRenderer); + if (copy.mVPathRenderer.mFillPaint != null) { + mVPathRenderer.mFillPaint = new Paint(copy.mVPathRenderer.mFillPaint); + } + if (copy.mVPathRenderer.mStrokePaint != null) { + mVPathRenderer.mStrokePaint = new Paint(copy.mVPathRenderer.mStrokePaint); + } + mTint = copy.mTint; + mTintMode = copy.mTintMode; + mAutoMirrored = copy.mAutoMirrored; + } + } + + public void drawCachedBitmapWithRootAlpha(Canvas canvas, ColorFilter filter) { + // The bitmap's size is the same as the bounds. + final Paint p = getPaint(filter); + canvas.drawBitmap(mCachedBitmap, 0, 0, p); + } + + public boolean hasTranslucentRoot() { + return mVPathRenderer.getRootAlpha() < 255; + } + + /** + * @return null when there is no need for alpha paint. + */ + public Paint getPaint(ColorFilter filter) { + if (!hasTranslucentRoot() && filter == null) { + return null; + } + + if (mTempPaint == null) { + mTempPaint = new Paint(); + mTempPaint.setFilterBitmap(true); + } + mTempPaint.setAlpha(mVPathRenderer.getRootAlpha()); + mTempPaint.setColorFilter(filter); + return mTempPaint; + } + + public void updateCachedBitmap(Rect bounds) { + mCachedBitmap.eraseColor(Color.TRANSPARENT); + Canvas tmpCanvas = new Canvas(mCachedBitmap); + mVPathRenderer.draw(tmpCanvas, bounds.width(), bounds.height(), null); + } + + public void createCachedBitmapIfNeeded(Rect bounds) { + if (mCachedBitmap == null || !canReuseBitmap(bounds.width(), + bounds.height())) { + mCachedBitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), + Bitmap.Config.ARGB_8888); + mCacheDirty = true; + } + + } + + public boolean canReuseBitmap(int width, int height) { + if (width == mCachedBitmap.getWidth() + && height == mCachedBitmap.getHeight()) { + return true; + } + return false; + } + + public boolean canReuseCache() { + if (!mCacheDirty + && mCachedThemeAttrs == mThemeAttrs + && mCachedTint == mTint + && mCachedTintMode == mTintMode + && mCachedAutoMirrored == mAutoMirrored + && mCachedRootAlpha == mVPathRenderer.getRootAlpha()) { + return true; + } + return false; + } + + public void updateCacheStates() { + // Use shallow copy here and shallow comparison in canReuseCache(), + // likely hit cache miss more, but practically not much difference. + mCachedThemeAttrs = mThemeAttrs; + mCachedTint = mTint; + mCachedTintMode = mTintMode; + mCachedRootAlpha = mVPathRenderer.getRootAlpha(); + mCachedAutoMirrored = mAutoMirrored; + mCacheDirty = false; + } + + @Override + @TargetApi(LOLLIPOP) + public boolean canApplyTheme() { + if (!LOLLIPOP_PLUS) { + return false; + } + return super.canApplyTheme() || mThemeAttrs != null + || (mVPathRenderer != null && mVPathRenderer.canApplyTheme()); + } + + boolean canApplyThemeCompat() { + return canApplyTheme(); + } + + public VectorDrawableState() { + mVPathRenderer = new VPathRenderer(); + } + + @Override + public Drawable newDrawable() { + return new VectorDrawable(this, null, null); + } + + @Override + public Drawable newDrawable(Resources res) { + return new VectorDrawable(this, res, null); + } + + @Override + @TargetApi(LOLLIPOP) + public Drawable newDrawable(Resources res, Theme theme) { + return new VectorDrawable(this, res, theme); + } + + @Override + public int getChangingConfigurations() { + return mChangingConfigurations; + } + } + + private static class VPathRenderer { + /* Right now the internal data structure is organized as a tree. + * Each node can be a group node, or a path. + * A group node can have groups or paths as children, but a path node has + * no children. + * One example can be: + * Root Group + * / | \ + * Group Path Group + * / \ | + * Path Path Path + * + */ + // Variables that only used temporarily inside the draw() call, so there + // is no need for deep copying. + private final Path mPath; + private final Path mRenderPath; + private static final Matrix IDENTITY_MATRIX = new Matrix(); + private final Matrix mFinalPathMatrix = new Matrix(); + + private Paint mStrokePaint; + private Paint mFillPaint; + private PathMeasure mPathMeasure; + + ///////////////////////////////////////////////////// + // Variables below need to be copied (deep copy if applicable) for mutation. + private int mChangingConfigurations; + private final VGroup mRootGroup; + float mBaseWidth = 0; + float mBaseHeight = 0; + float mViewportWidth = 0; + float mViewportHeight = 0; + int mRootAlpha = 0xFF; + String mRootName = null; + + final ArrayMap mVGTargetsMap = new ArrayMap(); + + public VPathRenderer() { + mRootGroup = new VGroup(); + mPath = new Path(); + mRenderPath = new Path(); + } + + public void setRootAlpha(int alpha) { + mRootAlpha = alpha; + } + + public int getRootAlpha() { + return mRootAlpha; + } + + // setAlpha() and getAlpha() are used mostly for animation purpose, since + // Animator like to use alpha from 0 to 1. + public void setAlpha(float alpha) { + setRootAlpha((int) (alpha * 255)); + } + + @SuppressWarnings("unused") + public float getAlpha() { + return getRootAlpha() / 255.0f; + } + + public VPathRenderer(VPathRenderer copy) { + mRootGroup = new VGroup(copy.mRootGroup, mVGTargetsMap); + mPath = new Path(copy.mPath); + mRenderPath = new Path(copy.mRenderPath); + mBaseWidth = copy.mBaseWidth; + mBaseHeight = copy.mBaseHeight; + mViewportWidth = copy.mViewportWidth; + mViewportHeight = copy.mViewportHeight; + mChangingConfigurations = copy.mChangingConfigurations; + mRootAlpha = copy.mRootAlpha; + mRootName = copy.mRootName; + if (copy.mRootName != null) { + mVGTargetsMap.put(copy.mRootName, this); + } + } + + public boolean canApplyTheme() { + // If one of the paths can apply theme, then return true; + return recursiveCanApplyTheme(mRootGroup); + } + + private boolean recursiveCanApplyTheme(VGroup currentGroup) { + // We can do a tree traverse here, if there is one path return true, + // then we return true for the whole tree. + final ArrayList children = currentGroup.mChildren; + + for (int i = 0; i < children.size(); i++) { + Object child = children.get(i); + if (child instanceof VGroup) { + VGroup childGroup = (VGroup) child; + if (childGroup.canApplyTheme() + || recursiveCanApplyTheme(childGroup)) { + return true; + } + } else if (child instanceof VPath) { + VPath childPath = (VPath) child; + if (childPath.canApplyTheme()) { + return true; + } + } + } + return false; + } + + public void applyTheme(Theme t) { + // Apply theme to every path of the tree. + recursiveApplyTheme(mRootGroup, t); + } + + private void recursiveApplyTheme(VGroup currentGroup, Theme t) { + // We can do a tree traverse here, apply theme to all paths which + // can apply theme. + final ArrayList children = currentGroup.mChildren; + for (int i = 0; i < children.size(); i++) { + Object child = children.get(i); + if (child instanceof VGroup) { + VGroup childGroup = (VGroup) child; + if (childGroup.canApplyTheme()) { + childGroup.applyTheme(t); + } + recursiveApplyTheme(childGroup, t); + } else if (child instanceof VPath) { + VPath childPath = (VPath) child; + if (childPath.canApplyTheme()) { + childPath.applyTheme(t); + } + } + } + } + + private void drawGroupTree(VGroup currentGroup, Matrix currentMatrix, + Canvas canvas, int w, int h, ColorFilter filter) { + // Calculate current group's matrix by preConcat the parent's and + // and the current one on the top of the stack. + // Basically the Mfinal = Mviewport * M0 * M1 * M2; + // Mi the local matrix at level i of the group tree. + currentGroup.mStackedMatrix.set(currentMatrix); + + currentGroup.mStackedMatrix.preConcat(currentGroup.mLocalMatrix); + + // Draw the group tree in the same order as the XML file. + for (int i = 0; i < currentGroup.mChildren.size(); i++) { + Object child = currentGroup.mChildren.get(i); + if (child instanceof VGroup) { + VGroup childGroup = (VGroup) child; + drawGroupTree(childGroup, currentGroup.mStackedMatrix, + canvas, w, h, filter); + } else if (child instanceof VPath) { + VPath childPath = (VPath) child; + drawPath(currentGroup, childPath, canvas, w, h, filter); + } + } + } + + public void draw(Canvas canvas, int w, int h, ColorFilter filter) { + // Travese the tree in pre-order to draw. + drawGroupTree(mRootGroup, IDENTITY_MATRIX, canvas, w, h, filter); + } + + private void drawPath(VGroup vGroup, VPath vPath, Canvas canvas, int w, int h, + ColorFilter filter) { + final float scaleX = w / mViewportWidth; + final float scaleY = h / mViewportHeight; + final float minScale = Math.min(scaleX, scaleY); + + mFinalPathMatrix.set(vGroup.mStackedMatrix); + mFinalPathMatrix.postScale(scaleX, scaleY); + + vPath.toPath(mPath); + final Path path = mPath; + + mRenderPath.reset(); + + if (vPath.isClipPath()) { + mRenderPath.addPath(path, mFinalPathMatrix); + canvas.clipPath(mRenderPath, Region.Op.REPLACE); + } else { + VFullPath fullPath = (VFullPath) vPath; + if (fullPath.mTrimPathStart != 0.0f || fullPath.mTrimPathEnd != 1.0f) { + float start = (fullPath.mTrimPathStart + fullPath.mTrimPathOffset) % 1.0f; + float end = (fullPath.mTrimPathEnd + fullPath.mTrimPathOffset) % 1.0f; + + if (mPathMeasure == null) { + mPathMeasure = new PathMeasure(); + } + mPathMeasure.setPath(mPath, false); + + float len = mPathMeasure.getLength(); + start = start * len; + end = end * len; + path.reset(); + if (start > end) { + mPathMeasure.getSegment(start, len, path, true); + mPathMeasure.getSegment(0f, end, path, true); + } else { + mPathMeasure.getSegment(start, end, path, true); + } + path.rLineTo(0, 0); // fix bug in measure + } + mRenderPath.addPath(path, mFinalPathMatrix); + + if (fullPath.mFillColor != Color.TRANSPARENT) { + if (mFillPaint == null) { + mFillPaint = new Paint(); + mFillPaint.setStyle(Paint.Style.FILL); + mFillPaint.setAntiAlias(true); + } + + final Paint fillPaint = mFillPaint; + fillPaint.setColor(applyAlpha(fullPath.mFillColor, fullPath.mFillAlpha)); + fillPaint.setColorFilter(filter); + canvas.drawPath(mRenderPath, fillPaint); + } + + if (fullPath.mStrokeColor != Color.TRANSPARENT) { + if (mStrokePaint == null) { + mStrokePaint = new Paint(); + mStrokePaint.setStyle(Paint.Style.STROKE); + mStrokePaint.setAntiAlias(true); + } + + final Paint strokePaint = mStrokePaint; + if (fullPath.mStrokeLineJoin != null) { + strokePaint.setStrokeJoin(fullPath.mStrokeLineJoin); + } + + if (fullPath.mStrokeLineCap != null) { + strokePaint.setStrokeCap(fullPath.mStrokeLineCap); + } + + strokePaint.setStrokeMiter(fullPath.mStrokeMiterlimit); + strokePaint.setColor(applyAlpha(fullPath.mStrokeColor, fullPath.mStrokeAlpha)); + strokePaint.setColorFilter(filter); + strokePaint.setStrokeWidth(fullPath.mStrokeWidth * minScale); + canvas.drawPath(mRenderPath, strokePaint); + } + } + } + } + + private static class VGroup { + // mStackedMatrix is only used temporarily when drawing, it combines all + // the parents' local matrices with the current one. + private final Matrix mStackedMatrix = new Matrix(); + + ///////////////////////////////////////////////////// + // Variables below need to be copied (deep copy if applicable) for mutation. + final ArrayList mChildren = new ArrayList(); + + private float mRotate = 0; + private float mPivotX = 0; + private float mPivotY = 0; + private float mScaleX = 1; + private float mScaleY = 1; + private float mTranslateX = 0; + private float mTranslateY = 0; + + // mLocalMatrix is updated based on the update of transformation information, + // either parsed from the XML or by animation. + private final Matrix mLocalMatrix = new Matrix(); + private int mChangingConfigurations; + private int[] mThemeAttrs; + private String mGroupName = null; + + public VGroup(VGroup copy, ArrayMap targetsMap) { + mRotate = copy.mRotate; + mPivotX = copy.mPivotX; + mPivotY = copy.mPivotY; + mScaleX = copy.mScaleX; + mScaleY = copy.mScaleY; + mTranslateX = copy.mTranslateX; + mTranslateY = copy.mTranslateY; + mThemeAttrs = copy.mThemeAttrs; + mGroupName = copy.mGroupName; + mChangingConfigurations = copy.mChangingConfigurations; + if (mGroupName != null) { + targetsMap.put(mGroupName, this); + } + + mLocalMatrix.set(copy.mLocalMatrix); + + final ArrayList children = copy.mChildren; + for (int i = 0; i < children.size(); i++) { + Object copyChild = children.get(i); + if (copyChild instanceof VGroup) { + VGroup copyGroup = (VGroup) copyChild; + mChildren.add(new VGroup(copyGroup, targetsMap)); + } else { + VPath newPath = null; + if (copyChild instanceof VFullPath) { + newPath = new VFullPath((VFullPath) copyChild); + } else if (copyChild instanceof VClipPath) { + newPath = new VClipPath((VClipPath) copyChild); + } else { + throw new IllegalStateException("Unknown object in the tree!"); + } + mChildren.add(newPath); + if (newPath.mPathName != null) { + targetsMap.put(newPath.mPathName, newPath); + } + } + } + } + + public VGroup() { + } + + public String getGroupName() { + return mGroupName; + } + + public Matrix getLocalMatrix() { + return mLocalMatrix; + } + + public void inflate(Resources res, AttributeSet attrs, Theme theme) { + // TODO TINT THEME Not supported yet + final TypedArray a = res.obtainAttributes(attrs, + R.styleable.VectorDrawableGroup); + updateStateFromTypedArray(a); + a.recycle(); + } + + private void updateStateFromTypedArray(TypedArray a) { + // Account for any configuration changes. + mChangingConfigurations |= Utils.getChangingConfigurations(a); + + // Extract the theme attributes, if any. + mThemeAttrs = null; // TODO TINT THEME Not supported yet a.extractThemeAttrs(); + + mRotate = a.getFloat(R.styleable.VectorDrawableGroup_rotation, mRotate); + mPivotX = a.getFloat(R.styleable.VectorDrawableGroup_pivotX, mPivotX); + mPivotY = a.getFloat(R.styleable.VectorDrawableGroup_pivotY, mPivotY); + mScaleX = a.getFloat(R.styleable.VectorDrawableGroup_scaleX, mScaleX); + mScaleY = a.getFloat(R.styleable.VectorDrawableGroup_scaleY, mScaleY); + mTranslateX = a.getFloat(R.styleable.VectorDrawableGroup_translateX, mTranslateX); + mTranslateY = a.getFloat(R.styleable.VectorDrawableGroup_translateY, mTranslateY); + + final String groupName = a.getString(R.styleable.VectorDrawableGroup_name); + if (groupName != null) { + mGroupName = groupName; + } + + updateLocalMatrix(); + } + + public boolean canApplyTheme() { + return mThemeAttrs != null; + } + + public void applyTheme(Theme t) { + if (mThemeAttrs == null) { + return; + } + /* TODO TINT THEME Not supported yet + final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawableGroup); + updateStateFromTypedArray(a); + a.recycle(); + */ + } + + private void updateLocalMatrix() { + // The order we apply is the same as the + // RenderNode.cpp::applyViewPropertyTransforms(). + mLocalMatrix.reset(); + mLocalMatrix.postTranslate(-mPivotX, -mPivotY); + mLocalMatrix.postScale(mScaleX, mScaleY); + mLocalMatrix.postRotate(mRotate, 0, 0); + mLocalMatrix.postTranslate(mTranslateX + mPivotX, mTranslateY + mPivotY); + } + + /* Setters and Getters, used by animator from AnimatedVectorDrawable. */ + @SuppressWarnings("unused") + public float getRotation() { + return mRotate; + } + + @SuppressWarnings("unused") + public void setRotation(float rotation) { + if (rotation != mRotate) { + mRotate = rotation; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getPivotX() { + return mPivotX; + } + + @SuppressWarnings("unused") + public void setPivotX(float pivotX) { + if (pivotX != mPivotX) { + mPivotX = pivotX; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getPivotY() { + return mPivotY; + } + + @SuppressWarnings("unused") + public void setPivotY(float pivotY) { + if (pivotY != mPivotY) { + mPivotY = pivotY; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getScaleX() { + return mScaleX; + } + + @SuppressWarnings("unused") + public void setScaleX(float scaleX) { + if (scaleX != mScaleX) { + mScaleX = scaleX; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getScaleY() { + return mScaleY; + } + + @SuppressWarnings("unused") + public void setScaleY(float scaleY) { + if (scaleY != mScaleY) { + mScaleY = scaleY; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getTranslateX() { + return mTranslateX; + } + + @SuppressWarnings("unused") + public void setTranslateX(float translateX) { + if (translateX != mTranslateX) { + mTranslateX = translateX; + updateLocalMatrix(); + } + } + + @SuppressWarnings("unused") + public float getTranslateY() { + return mTranslateY; + } + + @SuppressWarnings("unused") + public void setTranslateY(float translateY) { + if (translateY != mTranslateY) { + mTranslateY = translateY; + updateLocalMatrix(); + } + } + } + + /** + * Common Path information for clip path and normal path. + */ + private static class VPath { + protected PathParser.PathDataNode[] mNodes = null; + String mPathName; + int mChangingConfigurations; + + public VPath() { + // Empty constructor. + } + + public VPath(VPath copy) { + mPathName = copy.mPathName; + mChangingConfigurations = copy.mChangingConfigurations; + mNodes = PathParser.deepCopyNodes(copy.mNodes); + } + + public void toPath(Path path) { + path.reset(); + if (mNodes != null) { + PathParser.PathDataNode.nodesToPath(mNodes, path); + } + } + + public String getPathName() { + return mPathName; + } + + public boolean canApplyTheme() { + return false; + } + + public void applyTheme(Theme t) { + } + + public boolean isClipPath() { + return false; + } + + /* Setters and Getters, used by animator from AnimatedVectorDrawable. */ + @SuppressWarnings("unused") + public PathParser.PathDataNode[] getPathData() { + return mNodes; + } + + @SuppressWarnings("unused") + public void setPathData(PathParser.PathDataNode[] nodes) { + if (!PathParser.canMorph(mNodes, nodes)) { + // This should not happen in the middle of animation. + mNodes = PathParser.deepCopyNodes(nodes); + } else { + PathParser.updateNodes(mNodes, nodes); + } + } + } + + /** + * Clip path, which only has name and pathData. + */ + private static class VClipPath extends VPath { + public VClipPath() { + // Empty constructor. + } + + public VClipPath(VClipPath copy) { + super(copy); + } + + public void inflate(Resources r, AttributeSet attrs, Theme theme) { + // TODO TINT THEME Not supported yet + final TypedArray a = r.obtainAttributes(attrs, + R.styleable.VectorDrawableClipPath); + updateStateFromTypedArray(a); + a.recycle(); + } + + private void updateStateFromTypedArray(TypedArray a) { + // Account for any configuration changes. + mChangingConfigurations |= Utils.getChangingConfigurations(a);; + + final String pathName = a.getString(R.styleable.VectorDrawableClipPath_name); + if (pathName != null) { + mPathName = pathName; + } + + final String pathData = a.getString(R.styleable.VectorDrawableClipPath_pathData); + if (pathData != null) { + mNodes = PathParser.createNodesFromPathData(pathData); + } + } + + @Override + public boolean isClipPath() { + return true; + } + } + + /** + * Normal path, which contains all the fill / paint information. + */ + private static class VFullPath extends VPath { + ///////////////////////////////////////////////////// + // Variables below need to be copied (deep copy if applicable) for mutation. + private int[] mThemeAttrs; + + int mStrokeColor = Color.TRANSPARENT; + float mStrokeWidth = 0; + + int mFillColor = Color.TRANSPARENT; + float mStrokeAlpha = 1.0f; + int mFillRule; + float mFillAlpha = 1.0f; + float mTrimPathStart = 0; + float mTrimPathEnd = 1; + float mTrimPathOffset = 0; + + Paint.Cap mStrokeLineCap = Paint.Cap.BUTT; + Paint.Join mStrokeLineJoin = Paint.Join.MITER; + float mStrokeMiterlimit = 4; + + public VFullPath() { + // Empty constructor. + } + + public VFullPath(VFullPath copy) { + super(copy); + mThemeAttrs = copy.mThemeAttrs; + + mStrokeColor = copy.mStrokeColor; + mStrokeWidth = copy.mStrokeWidth; + mStrokeAlpha = copy.mStrokeAlpha; + mFillColor = copy.mFillColor; + mFillRule = copy.mFillRule; + mFillAlpha = copy.mFillAlpha; + mTrimPathStart = copy.mTrimPathStart; + mTrimPathEnd = copy.mTrimPathEnd; + mTrimPathOffset = copy.mTrimPathOffset; + + mStrokeLineCap = copy.mStrokeLineCap; + mStrokeLineJoin = copy.mStrokeLineJoin; + mStrokeMiterlimit = copy.mStrokeMiterlimit; + } + + private Paint.Cap getStrokeLineCap(int id, Paint.Cap defValue) { + switch (id) { + case LINECAP_BUTT: + return Paint.Cap.BUTT; + case LINECAP_ROUND: + return Paint.Cap.ROUND; + case LINECAP_SQUARE: + return Paint.Cap.SQUARE; + default: + return defValue; + } + } + + private Paint.Join getStrokeLineJoin(int id, Paint.Join defValue) { + switch (id) { + case LINEJOIN_MITER: + return Paint.Join.MITER; + case LINEJOIN_ROUND: + return Paint.Join.ROUND; + case LINEJOIN_BEVEL: + return Paint.Join.BEVEL; + default: + return defValue; + } + } + + @Override + public boolean canApplyTheme() { + return mThemeAttrs != null; + } + + public void inflate(Resources r, AttributeSet attrs, Theme theme) { + final TypedArray a = r.obtainAttributes(attrs, + R.styleable.VectorDrawablePath); + updateStateFromTypedArray(a); + a.recycle(); + } + + private void updateStateFromTypedArray(TypedArray a) { + // Account for any configuration changes. + mChangingConfigurations |= Utils.getChangingConfigurations(a); + + // Extract the theme attributes, if any. + mThemeAttrs = null; // TODO TINT THEME Not supported yet a.extractThemeAttrs(); + + final String pathName = a.getString(R.styleable.VectorDrawablePath_name); + if (pathName != null) { + mPathName = pathName; + } + + final String pathData = a.getString(R.styleable.VectorDrawablePath_pathData); + if (pathData != null) { + mNodes = PathParser.createNodesFromPathData(pathData); + } + + mFillColor = a.getColor(R.styleable.VectorDrawablePath_fillColor, + mFillColor); + mFillAlpha = a.getFloat(R.styleable.VectorDrawablePath_fillAlpha, + mFillAlpha); + mStrokeLineCap = getStrokeLineCap(a.getInt( + R.styleable.VectorDrawablePath_strokeLineCap, -1), mStrokeLineCap); + mStrokeLineJoin = getStrokeLineJoin(a.getInt( + R.styleable.VectorDrawablePath_strokeLineJoin, -1), mStrokeLineJoin); + mStrokeMiterlimit = a.getFloat( + R.styleable.VectorDrawablePath_strokeMiterLimit, mStrokeMiterlimit); + mStrokeColor = a.getColor(R.styleable.VectorDrawablePath_strokeColor, + mStrokeColor); + mStrokeAlpha = a.getFloat(R.styleable.VectorDrawablePath_strokeAlpha, + mStrokeAlpha); + mStrokeWidth = a.getFloat(R.styleable.VectorDrawablePath_strokeWidth, + mStrokeWidth); + mTrimPathEnd = a.getFloat(R.styleable.VectorDrawablePath_trimPathEnd, + mTrimPathEnd); + mTrimPathOffset = a.getFloat( + R.styleable.VectorDrawablePath_trimPathOffset, mTrimPathOffset); + mTrimPathStart = a.getFloat( + R.styleable.VectorDrawablePath_trimPathStart, mTrimPathStart); + } + + @Override + public void applyTheme(Theme t) { + if (mThemeAttrs == null) { + return; + } + + /* TODO TINT THEME Not supported yet + final TypedArray a = t.resolveAttributes(mThemeAttrs, R.styleable.VectorDrawablePath); + updateStateFromTypedArray(a); + a.recycle(); + */ + } + + /* Setters and Getters, used by animator from AnimatedVectorDrawable. */ + @SuppressWarnings("unused") + int getStrokeColor() { + return mStrokeColor; + } + + @SuppressWarnings("unused") + void setStrokeColor(int strokeColor) { + mStrokeColor = strokeColor; + } + + @SuppressWarnings("unused") + float getStrokeWidth() { + return mStrokeWidth; + } + + @SuppressWarnings("unused") + void setStrokeWidth(float strokeWidth) { + mStrokeWidth = strokeWidth; + } + + @SuppressWarnings("unused") + float getStrokeAlpha() { + return mStrokeAlpha; + } + + @SuppressWarnings("unused") + void setStrokeAlpha(float strokeAlpha) { + mStrokeAlpha = strokeAlpha; + } + + @SuppressWarnings("unused") + int getFillColor() { + return mFillColor; + } + + @SuppressWarnings("unused") + void setFillColor(int fillColor) { + mFillColor = fillColor; + } + + @SuppressWarnings("unused") + float getFillAlpha() { + return mFillAlpha; + } + + @SuppressWarnings("unused") + void setFillAlpha(float fillAlpha) { + mFillAlpha = fillAlpha; + } + + @SuppressWarnings("unused") + float getTrimPathStart() { + return mTrimPathStart; + } + + @SuppressWarnings("unused") + void setTrimPathStart(float trimPathStart) { + mTrimPathStart = trimPathStart; + } + + @SuppressWarnings("unused") + float getTrimPathEnd() { + return mTrimPathEnd; + } + + @SuppressWarnings("unused") + void setTrimPathEnd(float trimPathEnd) { + mTrimPathEnd = trimPathEnd; + } + + @SuppressWarnings("unused") + float getTrimPathOffset() { + return mTrimPathOffset; + } + + @SuppressWarnings("unused") + void setTrimPathOffset(float trimPathOffset) { + mTrimPathOffset = trimPathOffset; + } + } +} \ No newline at end of file diff --git a/library/src/main/res/values/attrs.xml b/library/src/main/res/values/attrs.xml new file mode 100644 index 0000000..7f6aa87 --- /dev/null +++ b/library/src/main/res/values/attrs.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..70ee5af --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':demo', ':library' \ No newline at end of file