diff options
Diffstat (limited to 'tools')
28 files changed, 1026 insertions, 17 deletions
diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk index d311cd9..10f8150 100644 --- a/tools/aapt2/Android.mk +++ b/tools/aapt2/Android.mk @@ -40,6 +40,7 @@ sources := \ ManifestParser.cpp \ ManifestValidator.cpp \ Png.cpp \ + ProguardRules.cpp \ ResChunkPullParser.cpp \ Resource.cpp \ ResourceParser.cpp \ diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp index de2dafc..41c229d 100644 --- a/tools/aapt2/Main.cpp +++ b/tools/aapt2/Main.cpp @@ -28,6 +28,7 @@ #include "ManifestValidator.h" #include "NameMangler.h" #include "Png.h" +#include "ProguardRules.h" #include "ResourceParser.h" #include "ResourceTable.h" #include "ResourceTableResolver.h" @@ -300,6 +301,9 @@ struct AaptOptions { // Directory to in which to generate R.java. Maybe<Source> generateJavaClass; + // File in which to produce proguard rules. + Maybe<Source> generateProguardRules; + // Whether to output verbose details about // compilation. bool verbose = false; @@ -417,7 +421,8 @@ bool shouldGenerateVersionedResource(const std::shared_ptr<const ResourceTable>& bool linkXml(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, const std::shared_ptr<IResolver>& resolver, const LinkItem& item, - const void* data, size_t dataLen, ZipFile* outApk, std::queue<LinkItem>* outQueue) { + const void* data, size_t dataLen, ZipFile* outApk, std::queue<LinkItem>* outQueue, + proguard::KeepSet* keepSet) { SourceLogger logger(item.source); std::unique_ptr<xml::Node> root = xml::inflate(data, dataLen, &logger); if (!root) { @@ -435,6 +440,10 @@ bool linkXml(const AaptOptions& options, const std::shared_ptr<ResourceTable>& t xmlOptions.maxSdkAttribute = item.config.sdkVersion ? item.config.sdkVersion : 1; } + if (options.generateProguardRules) { + proguard::collectProguardRules(item.name.type, item.source, root.get(), keepSet); + } + BigBuffer outBuffer(1024); Maybe<size_t> minStrippedSdk = xml::flattenAndLink(item.source, root.get(), item.originalPackage, resolver, @@ -509,7 +518,7 @@ bool copyFile(const AaptOptions& options, const CompileItem& item, ZipFile* outA bool compileManifest(const AaptOptions& options, const std::shared_ptr<IResolver>& resolver, const std::map<std::shared_ptr<ResourceTable>, StaticLibraryData>& libApks, - const android::ResTable& table, ZipFile* outApk) { + const android::ResTable& table, ZipFile* outApk, proguard::KeepSet* keepSet) { if (options.verbose) { Logger::note(options.manifest) << "compiling AndroidManifest.xml." << std::endl; } @@ -557,6 +566,11 @@ bool compileManifest(const AaptOptions& options, const std::shared_ptr<IResolver } } + if (options.generateProguardRules) { + proguard::collectProguardRulesForManifest(options.manifest, merger.getMergedXml(), + keepSet); + } + BigBuffer outBuffer(1024); if (!xml::flattenAndLink(options.manifest, merger.getMergedXml(), options.appInfo.package, resolver, {}, &outBuffer)) { @@ -805,8 +819,10 @@ bool link(const AaptOptions& options, const std::shared_ptr<ResourceTable>& outT return false; } + proguard::KeepSet keepSet; + android::ResTable binTable; - if (!compileManifest(options, resolver, apkFiles, binTable, &outApk)) { + if (!compileManifest(options, resolver, apkFiles, binTable, &outApk, &keepSet)) { return false; } @@ -826,7 +842,7 @@ bool link(const AaptOptions& options, const std::shared_ptr<ResourceTable>& outT assert(uncompressedData); if (!linkXml(options, outTable, resolver, item, uncompressedData, - entry->getUncompressedLen(), &outApk, &linkQueue)) { + entry->getUncompressedLen(), &outApk, &linkQueue, &keepSet)) { Logger::error(options.output) << "failed to link '" << item.originalPath << "'." << std::endl; return false; @@ -883,6 +899,26 @@ bool link(const AaptOptions& options, const std::shared_ptr<ResourceTable>& outT } } + // Generate the Proguard rules file. + if (options.generateProguardRules) { + const Source& outPath = options.generateProguardRules.value(); + + if (options.verbose) { + Logger::note(outPath) << "writing proguard rules." << std::endl; + } + + std::ofstream fout(outPath.path); + if (!fout) { + Logger::error(outPath) << strerror(errno) << std::endl; + return false; + } + + if (!proguard::writeKeepSet(&fout, keepSet)) { + Logger::error(outPath) << "failed to write proguard rules." << std::endl; + return false; + } + } + outTable->getValueStringPool().prune(); outTable->getValueStringPool().sort( [](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { @@ -1072,6 +1108,11 @@ static AaptOptions prepareArgs(int argc, char** argv) { options.generateJavaClass = Source{ arg.toString() }; }); + flag::optionalFlag("--proguard", "file in which to output proguard rules", + [&options](const StringPiece& arg) { + options.generateProguardRules = Source{ arg.toString() }; + }); + flag::optionalSwitch("--static-lib", "generate a static Android library", true, &isStaticLib); diff --git a/tools/aapt2/ProguardRules.cpp b/tools/aapt2/ProguardRules.cpp new file mode 100644 index 0000000..e89fb7c --- /dev/null +++ b/tools/aapt2/ProguardRules.cpp @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProguardRules.h" +#include "Util.h" +#include "XmlDom.h" + +#include <memory> +#include <string> + +namespace aapt { +namespace proguard { + +constexpr const char16_t* kSchemaAndroid = u"http://schemas.android.com/apk/res/android"; + +class BaseVisitor : public xml::Visitor { +public: + BaseVisitor(const Source& source, KeepSet* keepSet) : mSource(source), mKeepSet(keepSet) { + } + + virtual void visit(xml::Text*) override {}; + + virtual void visit(xml::Namespace* node) override { + for (const auto& child : node->children) { + child->accept(this); + } + } + + virtual void visit(xml::Element* node) override { + if (!node->namespaceUri.empty()) { + Maybe<std::u16string> maybePackage = util::extractPackageFromNamespace( + node->namespaceUri); + if (maybePackage) { + // This is a custom view, let's figure out the class name from this. + std::u16string package = maybePackage.value() + u"." + node->name; + if (util::isJavaClassName(package)) { + addClass(node->lineNumber, package); + } + } + } else if (util::isJavaClassName(node->name)) { + addClass(node->lineNumber, node->name); + } + + for (const auto& child: node->children) { + child->accept(this); + } + } + +protected: + void addClass(size_t lineNumber, const std::u16string& className) { + mKeepSet->addClass(mSource.line(lineNumber), className); + } + + void addMethod(size_t lineNumber, const std::u16string& methodName) { + mKeepSet->addMethod(mSource.line(lineNumber), methodName); + } + +private: + Source mSource; + KeepSet* mKeepSet; +}; + +struct LayoutVisitor : public BaseVisitor { + LayoutVisitor(const Source& source, KeepSet* keepSet) : BaseVisitor(source, keepSet) { + } + + virtual void visit(xml::Element* node) override { + bool checkClass = false; + bool checkName = false; + if (node->namespaceUri.empty()) { + checkClass = node->name == u"view" || node->name == u"fragment"; + } else if (node->namespaceUri == kSchemaAndroid) { + checkName = node->name == u"fragment"; + } + + for (const auto& attr : node->attributes) { + if (checkClass && attr.namespaceUri.empty() && attr.name == u"class" && + util::isJavaClassName(attr.value)) { + addClass(node->lineNumber, attr.value); + } else if (checkName && attr.namespaceUri == kSchemaAndroid && attr.name == u"name" && + util::isJavaClassName(attr.value)) { + addClass(node->lineNumber, attr.value); + } else if (attr.namespaceUri == kSchemaAndroid && attr.name == u"onClick") { + addMethod(node->lineNumber, attr.value); + } + } + + BaseVisitor::visit(node); + } +}; + +struct XmlResourceVisitor : public BaseVisitor { + XmlResourceVisitor(const Source& source, KeepSet* keepSet) : BaseVisitor(source, keepSet) { + } + + virtual void visit(xml::Element* node) override { + bool checkFragment = false; + if (node->namespaceUri.empty()) { + checkFragment = node->name == u"PreferenceScreen" || node->name == u"header"; + } + + if (checkFragment) { + xml::Attribute* attr = node->findAttribute(kSchemaAndroid, u"fragment"); + if (attr && util::isJavaClassName(attr->value)) { + addClass(node->lineNumber, attr->value); + } + } + + BaseVisitor::visit(node); + } +}; + +struct TransitionVisitor : public BaseVisitor { + TransitionVisitor(const Source& source, KeepSet* keepSet) : BaseVisitor(source, keepSet) { + } + + virtual void visit(xml::Element* node) override { + bool checkClass = node->namespaceUri.empty() && + (node->name == u"transition" || node->name == u"pathMotion"); + if (checkClass) { + xml::Attribute* attr = node->findAttribute({}, u"class"); + if (attr && util::isJavaClassName(attr->value)) { + addClass(node->lineNumber, attr->value); + } + } + + BaseVisitor::visit(node); + } +}; + +struct ManifestVisitor : public BaseVisitor { + ManifestVisitor(const Source& source, KeepSet* keepSet) : BaseVisitor(source, keepSet) { + } + + virtual void visit(xml::Element* node) override { + if (node->namespaceUri.empty()) { + bool getName = false; + if (node->name == u"manifest") { + xml::Attribute* attr = node->findAttribute({}, u"package"); + if (attr) { + mPackage = attr->value; + } + } else if (node->name == u"application") { + getName = true; + xml::Attribute* attr = node->findAttribute(kSchemaAndroid, u"backupAgent"); + if (attr) { + Maybe<std::u16string> result = util::getFullyQualifiedClassName(mPackage, + attr->value); + if (result) { + addClass(node->lineNumber, result.value()); + } + } + } else if (node->name == u"activity" || node->name == u"service" || + node->name == u"receiver" || node->name == u"provider" || + node->name == u"instrumentation") { + getName = true; + } + + if (getName) { + xml::Attribute* attr = node->findAttribute(kSchemaAndroid, u"name"); + if (attr) { + Maybe<std::u16string> result = util::getFullyQualifiedClassName(mPackage, + attr->value); + if (result) { + addClass(node->lineNumber, result.value()); + } + } + } + } + BaseVisitor::visit(node); + } + + std::u16string mPackage; +}; + +bool collectProguardRulesForManifest(const Source& source, xml::Node* node, KeepSet* keepSet) { + ManifestVisitor visitor(source, keepSet); + node->accept(&visitor); + return true; +} + +bool collectProguardRules(ResourceType type, const Source& source, xml::Node* node, + KeepSet* keepSet) { + switch (type) { + case ResourceType::kLayout: { + LayoutVisitor visitor(source, keepSet); + node->accept(&visitor); + break; + } + + case ResourceType::kXml: { + XmlResourceVisitor visitor(source, keepSet); + node->accept(&visitor); + break; + } + + case ResourceType::kTransition: { + TransitionVisitor visitor(source, keepSet); + node->accept(&visitor); + break; + } + + default: + break; + } + return true; +} + +bool writeKeepSet(std::ostream* out, const KeepSet& keepSet) { + for (const auto& entry : keepSet.mKeepSet) { + for (const SourceLine& source : entry.second) { + *out << "// Referenced at " << source << "\n"; + } + *out << "-keep class " << entry.first << " { <init>(...); }\n" << std::endl; + } + + for (const auto& entry : keepSet.mKeepMethodSet) { + for (const SourceLine& source : entry.second) { + *out << "// Referenced at " << source << "\n"; + } + *out << "-keepclassmembers class * { *** " << entry.first << "(...); }\n" << std::endl; + } + return true; +} + +} // namespace proguard +} // namespace aapt diff --git a/tools/aapt2/ProguardRules.h b/tools/aapt2/ProguardRules.h new file mode 100644 index 0000000..bbb3e64 --- /dev/null +++ b/tools/aapt2/ProguardRules.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_PROGUARD_RULES_H +#define AAPT_PROGUARD_RULES_H + +#include "Resource.h" +#include "Source.h" +#include "XmlDom.h" + +#include <map> +#include <ostream> +#include <set> +#include <string> + +namespace aapt { +namespace proguard { + +class KeepSet { +public: + inline void addClass(const SourceLine& source, const std::u16string& className) { + mKeepSet[className].insert(source); + } + + inline void addMethod(const SourceLine& source, const std::u16string& methodName) { + mKeepMethodSet[methodName].insert(source); + } + +private: + friend bool writeKeepSet(std::ostream* out, const KeepSet& keepSet); + + std::map<std::u16string, std::set<SourceLine>> mKeepSet; + std::map<std::u16string, std::set<SourceLine>> mKeepMethodSet; +}; + +bool collectProguardRulesForManifest(const Source& source, xml::Node* node, KeepSet* keepSet); +bool collectProguardRules(ResourceType type, const Source& source, xml::Node* node, + KeepSet* keepSet); + +bool writeKeepSet(std::ostream* out, const KeepSet& keepSet); + +} // namespace proguard +} // namespace aapt + +#endif // AAPT_PROGUARD_RULES_H diff --git a/tools/aapt2/Source.h b/tools/aapt2/Source.h index 10c75aa..3606488 100644 --- a/tools/aapt2/Source.h +++ b/tools/aapt2/Source.h @@ -19,6 +19,7 @@ #include <ostream> #include <string> +#include <tuple> namespace aapt { @@ -80,6 +81,10 @@ inline ::std::ostream& operator<<(::std::ostream& out, const SourceLineColumn& s return out << source.path << ":" << source.line << ":" << source.column; } +inline bool operator<(const SourceLine& lhs, const SourceLine& rhs) { + return std::tie(lhs.path, lhs.line) < std::tie(rhs.path, rhs.line); +} + } // namespace aapt #endif // AAPT_SOURCE_H diff --git a/tools/aapt2/Util.cpp b/tools/aapt2/Util.cpp index 7adaf1e..03ecd1a 100644 --- a/tools/aapt2/Util.cpp +++ b/tools/aapt2/Util.cpp @@ -102,6 +102,51 @@ StringPiece16::const_iterator findNonAlphaNumericAndNotInSet(const StringPiece16 return endIter; } +bool isJavaClassName(const StringPiece16& str) { + size_t pieces = 0; + for (const StringPiece16& piece : tokenize(str, u'.')) { + pieces++; + if (piece.empty()) { + return false; + } + + // Can't have starting or trailing $ character. + if (piece.data()[0] == u'$' || piece.data()[piece.size() - 1] == u'$') { + return false; + } + + if (findNonAlphaNumericAndNotInSet(piece, u"$_") != piece.end()) { + return false; + } + } + return pieces >= 2; +} + +Maybe<std::u16string> getFullyQualifiedClassName(const StringPiece16& package, + const StringPiece16& className) { + if (className.empty()) { + return {}; + } + + if (util::isJavaClassName(className)) { + return className.toString(); + } + + if (package.empty()) { + return {}; + } + + std::u16string result(package.data(), package.size()); + if (className.data()[0] != u'.') { + result += u'.'; + } + result.append(className.data(), className.size()); + if (!isJavaClassName(result)) { + return {}; + } + return result; +} + static Maybe<char16_t> parseUnicodeCodepoint(const char16_t** start, const char16_t* end) { char16_t code = 0; for (size_t i = 0; i < 4 && *start != end; i++, (*start)++) { diff --git a/tools/aapt2/Util.h b/tools/aapt2/Util.h index 6015d82..9cdb152 100644 --- a/tools/aapt2/Util.h +++ b/tools/aapt2/Util.h @@ -78,6 +78,23 @@ StringPiece16::const_iterator findNonAlphaNumericAndNotInSet(const StringPiece16 const StringPiece16& allowedChars); /** + * Tests that the string is a valid Java class name. + */ +bool isJavaClassName(const StringPiece16& str); + +/** + * Converts the class name to a fully qualified class name from the given `package`. Ex: + * + * asdf --> package.asdf + * .asdf --> package.asdf + * .a.b --> package.a.b + * asdf.adsf --> asdf.adsf + */ +Maybe<std::u16string> getFullyQualifiedClassName(const StringPiece16& package, + const StringPiece16& className); + + +/** * Makes a std::unique_ptr<> with the template parameter inferred by the compiler. * This will be present in C++14 and can be removed then. */ diff --git a/tools/aapt2/Util_test.cpp b/tools/aapt2/Util_test.cpp index c16f6bb..0b08d24 100644 --- a/tools/aapt2/Util_test.cpp +++ b/tools/aapt2/Util_test.cpp @@ -93,4 +93,44 @@ TEST(UtilTest, TokenizeInput) { ASSERT_EQ(tokenizer.end(), iter); } +TEST(UtilTest, IsJavaClassName) { + EXPECT_TRUE(util::isJavaClassName(u"android.test.Class")); + EXPECT_TRUE(util::isJavaClassName(u"android.test.Class$Inner")); + EXPECT_TRUE(util::isJavaClassName(u"android_test.test.Class")); + EXPECT_TRUE(util::isJavaClassName(u"_android_.test._Class_")); + EXPECT_FALSE(util::isJavaClassName(u"android.test.$Inner")); + EXPECT_FALSE(util::isJavaClassName(u"android.test.Inner$")); + EXPECT_FALSE(util::isJavaClassName(u".test.Class")); + EXPECT_FALSE(util::isJavaClassName(u"android")); +} + +TEST(UtilTest, FullyQualifiedClassName) { + Maybe<std::u16string> res = util::getFullyQualifiedClassName(u"android", u"asdf"); + ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"android.asdf"); + + res = util::getFullyQualifiedClassName(u"android", u".asdf"); + ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"android.asdf"); + + res = util::getFullyQualifiedClassName(u"android", u".a.b"); + ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"android.a.b"); + + res = util::getFullyQualifiedClassName(u"android", u"a.b"); + ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"a.b"); + + res = util::getFullyQualifiedClassName(u"", u"a.b"); + ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"a.b"); + + res = util::getFullyQualifiedClassName(u"", u""); + ASSERT_FALSE(res); + + res = util::getFullyQualifiedClassName(u"android", u"./Apple"); + ASSERT_FALSE(res); +} + + } // namespace aapt diff --git a/tools/aapt2/data/AndroidManifest.xml b/tools/aapt2/data/AndroidManifest.xml index c017a0d..8533c28 100644 --- a/tools/aapt2/data/AndroidManifest.xml +++ b/tools/aapt2/data/AndroidManifest.xml @@ -1,6 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.app"> - <application> + <application + android:name=".Activity"> </application> </manifest> diff --git a/tools/aapt2/data/Makefile b/tools/aapt2/data/Makefile index ce5201b..3387135 100644 --- a/tools/aapt2/data/Makefile +++ b/tools/aapt2/data/Makefile @@ -15,6 +15,7 @@ LOCAL_RESOURCE_DIR := res LOCAL_LIBS := lib/out/package.apk LOCAL_OUT := out LOCAL_GEN := out/gen +LOCAL_PROGUARD := out/proguard.rule ## # AAPT2 custom rules. @@ -57,7 +58,7 @@ $(foreach d,$(PRIVATE_RESOURCE_TYPES),$(eval $(call make-collect-rule,$d))) # Link: out/package-unaligned.apk <- out/values-v4.apk out/drawable-v4.apk $(PRIVATE_APK_UNALIGNED): $(PRIVATE_INTERMEDIATE_TABLES) $(PRIVATE_INCLUDES) $(LOCAL_LIBS) AndroidManifest.xml - $(AAPT) link --manifest AndroidManifest.xml $(addprefix -I ,$(PRIVATE_INCLUDES)) --java $(LOCAL_GEN) -o $@ $(PRIVATE_INTERMEDIATE_TABLES) $(LOCAL_LIBS) + $(AAPT) link --manifest AndroidManifest.xml $(addprefix -I ,$(PRIVATE_INCLUDES)) --java $(LOCAL_GEN) -o $@ $(PRIVATE_INTERMEDIATE_TABLES) $(LOCAL_LIBS) --proguard $(LOCAL_PROGUARD) -v # R.java: gen/com/android/app/R.java <- out/resources.arsc # No action since R.java is generated when out/resources.arsc is. diff --git a/tools/aapt2/data/res/layout/main.xml b/tools/aapt2/data/res/layout/main.xml index 77ccedb..50a51d9 100644 --- a/tools/aapt2/data/res/layout/main.xml +++ b/tools/aapt2/data/res/layout/main.xml @@ -5,11 +5,14 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> + <fragment class="android.test.sample.App$Inner" /> + <variable name="user" type="com.android.User" /> <View xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/me" android:layout_width="1dp" + android:onClick="doClick" android:text="@{user.name}" android:layout_height="match_parent" app:layout_width="@support:bool/allow" diff --git a/tools/layoutlib/.idea/artifacts/studio_android_widgets_jar.xml b/tools/layoutlib/.idea/artifacts/studio_android_widgets_jar.xml new file mode 100644 index 0000000..0450be3 --- /dev/null +++ b/tools/layoutlib/.idea/artifacts/studio_android_widgets_jar.xml @@ -0,0 +1,8 @@ +<component name="ArtifactManager"> + <artifact type="jar" name="studio-android-widgets:jar"> + <output-path>$PROJECT_DIR$/out/artifacts/studio_android_widgets_jar</output-path> + <root id="archive" name="studio-android-widgets.jar"> + <element id="module-output" name="studio-android-widgets" /> + </root> + </artifact> +</component>
\ No newline at end of file diff --git a/tools/layoutlib/.idea/artifacts/studio_android_widgets_src_jar.xml b/tools/layoutlib/.idea/artifacts/studio_android_widgets_src_jar.xml new file mode 100644 index 0000000..a844ca3 --- /dev/null +++ b/tools/layoutlib/.idea/artifacts/studio_android_widgets_src_jar.xml @@ -0,0 +1,8 @@ +<component name="ArtifactManager"> + <artifact type="jar" name="studio-android-widgets-src:jar"> + <output-path>$PROJECT_DIR$/out/artifacts/studio_android_widgets_src_jar</output-path> + <root id="archive" name="studio-android-widgets-src.jar"> + <element id="dir-copy" path="$PROJECT_DIR$/studio-custom-widgets/src" /> + </root> + </artifact> +</component>
\ No newline at end of file diff --git a/tools/layoutlib/.idea/modules.xml b/tools/layoutlib/.idea/modules.xml index 684f4fd..9bdc381 100644 --- a/tools/layoutlib/.idea/modules.xml +++ b/tools/layoutlib/.idea/modules.xml @@ -4,7 +4,7 @@ <modules> <module fileurl="file://$PROJECT_DIR$/bridge/bridge.iml" filepath="$PROJECT_DIR$/bridge/bridge.iml" /> <module fileurl="file://$PROJECT_DIR$/create/create.iml" filepath="$PROJECT_DIR$/create/create.iml" /> + <module fileurl="file://$PROJECT_DIR$/studio-custom-widgets/studio-android-widgets.iml" filepath="$PROJECT_DIR$/studio-custom-widgets/studio-android-widgets.iml" /> </modules> </component> -</project> - +</project>
\ No newline at end of file diff --git a/tools/layoutlib/bridge/src/android/graphics/LinearGradient_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/LinearGradient_Delegate.java index 703719c..cbc30c3 100644 --- a/tools/layoutlib/bridge/src/android/graphics/LinearGradient_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/LinearGradient_Delegate.java @@ -23,6 +23,8 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import android.graphics.Shader.TileMode; +import java.awt.image.ColorModel; + /** * Delegate implementing the native methods of android.graphics.LinearGradient * @@ -158,7 +160,7 @@ public final class LinearGradient_Delegate extends Gradient_Delegate { java.awt.image.ColorModel colorModel) { mCanvasMatrix = canvasMatrix; mLocalMatrix = localMatrix; - mColorModel = colorModel; + mColorModel = colorModel.hasAlpha() ? colorModel : ColorModel.getRGBdefault(); } @Override diff --git a/tools/layoutlib/bridge/src/android/graphics/RadialGradient_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/RadialGradient_Delegate.java index 04e423b..9881a38 100644 --- a/tools/layoutlib/bridge/src/android/graphics/RadialGradient_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/RadialGradient_Delegate.java @@ -23,6 +23,8 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate; import android.graphics.Shader.TileMode; +import java.awt.image.ColorModel; + /** * Delegate implementing the native methods of android.graphics.RadialGradient * @@ -146,7 +148,7 @@ public class RadialGradient_Delegate extends Gradient_Delegate { java.awt.image.ColorModel colorModel) { mCanvasMatrix = canvasMatrix; mLocalMatrix = localMatrix; - mColorModel = colorModel; + mColorModel = colorModel.hasAlpha() ? colorModel : ColorModel.getRGBdefault(); } @Override diff --git a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java index 32ee9e8..6767a07 100644 --- a/tools/layoutlib/bridge/src/android/view/BridgeInflater.java +++ b/tools/layoutlib/bridge/src/android/view/BridgeInflater.java @@ -22,6 +22,7 @@ import com.android.ide.common.rendering.api.MergeCookie; import com.android.ide.common.rendering.api.ResourceReference; import com.android.ide.common.rendering.api.ResourceValue; import com.android.layoutlib.bridge.Bridge; +import com.android.layoutlib.bridge.BridgeConstants; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; import com.android.layoutlib.bridge.impl.ParserFactory; @@ -234,6 +235,13 @@ public final class BridgeInflater extends LayoutInflater { if (viewKey != null) { bc.addViewKey(view, viewKey); } + String scrollPos = attrs.getAttributeValue(BridgeConstants.NS_RESOURCES, "scrollY"); + if (scrollPos != null) { + if (scrollPos.endsWith("px")) { + int value = Integer.parseInt(scrollPos.substring(0, scrollPos.length() - 2)); + bc.setScrollYPos(view, value); + } + } } } diff --git a/tools/layoutlib/bridge/src/android/view/ShadowPainter.java b/tools/layoutlib/bridge/src/android/view/ShadowPainter.java index c734ea1..f09fffd 100644 --- a/tools/layoutlib/bridge/src/android/view/ShadowPainter.java +++ b/tools/layoutlib/bridge/src/android/view/ShadowPainter.java @@ -34,12 +34,15 @@ public class ShadowPainter { * new image. This method attempts to mimic the same visual characteristics as the rectangular * shadow painting methods in this class, {@link #createRectangularDropShadow(java.awt.image.BufferedImage)} * and {@link #createSmallRectangularDropShadow(java.awt.image.BufferedImage)}. + * <p/> + * If shadowSize is less or equals to 1, no shadow will be painted and the source image will be + * returned instead. * * @param source the source image * @param shadowSize the size of the shadow, normally {@link #SHADOW_SIZE or {@link * #SMALL_SHADOW_SIZE}} * - * @return a new image with the shadow painted in + * @return an image with the shadow painted in or the source image if shadowSize <= 1 */ @NonNull public static BufferedImage createDropShadow(BufferedImage source, int shadowSize) { @@ -60,12 +63,13 @@ public class ShadowPainter { * @param shadowOpacity the opacity of the shadow, with 0=transparent and 1=opaque * @param shadowRgb the RGB int to use for the shadow color * - * @return a new image with the source image on top of its shadow + * @return a new image with the source image on top of its shadow when shadowSize > 0 or the + * source image otherwise */ @SuppressWarnings({"SuspiciousNameCombination", "UnnecessaryLocalVariable"}) // Imported code public static BufferedImage createDropShadow(BufferedImage source, int shadowSize, float shadowOpacity, int shadowRgb) { - if (shadowSize == 0) { + if (shadowSize <= 0) { return source; } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index a2518fa..6be5a95 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -37,6 +37,7 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import android.annotation.Nullable; +import android.annotation.NonNull; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -125,6 +126,7 @@ public final class BridgeContext extends Context { private final LayoutlibCallback mLayoutlibCallback; private final WindowManager mWindowManager; private final DisplayManager mDisplayManager; + private final HashMap<View, Integer> mScrollYPos = new HashMap<View, Integer>(); private Resources.Theme mTheme; @@ -1738,4 +1740,13 @@ public final class BridgeContext extends Context { // pass return new File[0]; } + + public void setScrollYPos(@NonNull View view, int scrollPos) { + mScrollYPos.put(view, scrollPos); + } + + public int getScrollYPos(@NonNull View view) { + Integer pos = mScrollYPos.get(view); + return pos != null ? pos : 0; + } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/DesignLibUtil.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/DesignLibUtil.java new file mode 100644 index 0000000..0426907 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/support/DesignLibUtil.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.android.support; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.view.View; + +import java.lang.reflect.Method; + +import static com.android.layoutlib.bridge.util.ReflectionUtils.ReflectionException; +import static com.android.layoutlib.bridge.util.ReflectionUtils.getMethod; +import static com.android.layoutlib.bridge.util.ReflectionUtils.invoke; + +/** + * Utility class for working with the design support lib. + */ +public class DesignLibUtil { + + private static final String PKG_PREFIX = "android.support.design.widget."; + public static final String CN_COORDINATOR_LAYOUT = PKG_PREFIX + "CoordinatorLayout"; + public static final String CN_APPBAR_LAYOUT = PKG_PREFIX + "AppBarLayout"; + public static final String CN_COLLAPSING_TOOLBAR_LAYOUT = + PKG_PREFIX + "CollapsingToolbarLayout"; + public static final String CN_TOOLBAR = "android.support.v7.widget.Toolbar"; + public static final int SCROLL_AXIS_VERTICAL = 1 << 1; + + /** + * Tries to set the title of a view. This is used to set the title in a + * CollapsingToolbarLayout. + * <p/> + * Any exceptions thrown during the process are logged in {@link Bridge#getLog()} + */ + public static void setTitle(@NonNull View view, @Nullable String title) { + if (title == null) { + return; + } + try { + Method setTitle = getMethod(view.getClass(), "setTitle", CharSequence.class); + if (setTitle != null) { + invoke(setTitle, view, title); + } + } catch (ReflectionException e) { + Bridge.getLog().warning(LayoutLog.TAG_INFO, + "Error occurred while trying to set title.", e); + } + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/GcSnapshot.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/GcSnapshot.java index c34f9b5..a39eb4d 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/GcSnapshot.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/GcSnapshot.java @@ -616,6 +616,8 @@ public class GcSnapshot { return; } + int x = 0; + int y = 0; int width; int height; Rectangle clipBounds = originalGraphics.getClipBounds(); @@ -626,6 +628,8 @@ public class GcSnapshot { } // If we have clipBounds available, use them as they will always be // smaller than the full layer size. + x = clipBounds.x; + y = clipBounds.y; width = clipBounds.width; height = clipBounds.height; } else { @@ -646,13 +650,20 @@ public class GcSnapshot { true /*compositeOnly*/, forceMode); try { // The main draw operation. + // We translate the operation to take into account that the rendering does not + // know about the clipping area. + imageGraphics.translate(-x, -y); drawable.draw(imageGraphics, paint); // Apply the color filter. + // Restore the original coordinates system and apply the filter only to the + // clipped area. + imageGraphics.translate(x, y); filter.applyFilter(imageGraphics, width, height); - // Draw the tinted image on the main layer. - configuredGraphics.drawImage(image, 0, 0, null); + // Draw the tinted image on the main layer using as start point the clipping + // upper left coordinates. + configuredGraphics.drawImage(image, x, y, null); layer.change(); } finally { // dispose Graphics2D objects diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java index ff77b58..d571d35 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java @@ -44,6 +44,7 @@ import com.android.layoutlib.bridge.android.BridgeContext; import com.android.layoutlib.bridge.android.BridgeLayoutParamsMapAttributes; import com.android.layoutlib.bridge.android.BridgeXmlBlockParser; import com.android.layoutlib.bridge.android.RenderParamsFlags; +import com.android.layoutlib.bridge.android.support.DesignLibUtil; import com.android.layoutlib.bridge.android.support.RecyclerViewUtil; import com.android.layoutlib.bridge.bars.AppCompatActionBar; import com.android.layoutlib.bridge.bars.BridgeActionBar; @@ -148,7 +149,6 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { private int mTitleBarSize; private int mActionBarSize; - // information being returned through the API private BufferedImage mImage; private List<ViewInfo> mViewInfoList; @@ -424,6 +424,8 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { // post-inflate process. For now this supports TabHost/TabWidget postInflateProcess(view, params.getLayoutlibCallback(), isPreference ? view : null); + setActiveToolbar(view, context, params); + // get the background drawable if (mWindowBackground != null) { Drawable d = ResourceHelper.getDrawable(mWindowBackground, context); @@ -544,6 +546,8 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { // now do the layout. mViewRoot.layout(0, 0, mMeasuredScreenWidth, mMeasuredScreenHeight); + handleScrolling(mViewRoot); + if (params.isLayoutOnly()) { // delete the canvas and image to reset them on the next full rendering mImage = null; @@ -1350,6 +1354,99 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { } /** + * If the root layout is a CoordinatorLayout with an AppBar: + * Set the title of the AppBar to the title of the activity context. + */ + private void setActiveToolbar(View view, BridgeContext context, SessionParams params) { + View coordinatorLayout = findChildView(view, DesignLibUtil.CN_COORDINATOR_LAYOUT); + if (coordinatorLayout == null) { + return; + } + View appBar = findChildView(coordinatorLayout, DesignLibUtil.CN_APPBAR_LAYOUT); + if (appBar == null) { + return; + } + ViewGroup collapsingToolbar = + (ViewGroup) findChildView(appBar, DesignLibUtil.CN_COLLAPSING_TOOLBAR_LAYOUT); + if (collapsingToolbar == null) { + return; + } + if (!hasToolbar(collapsingToolbar)) { + return; + } + RenderResources res = context.getRenderResources(); + String title = params.getAppLabel(); + ResourceValue titleValue = res.findResValue(title, false); + if (titleValue != null && titleValue.getValue() != null) { + title = titleValue.getValue(); + } + DesignLibUtil.setTitle(collapsingToolbar, title); + } + + private View findChildView(View view, String className) { + if (!(view instanceof ViewGroup)) { + return null; + } + ViewGroup group = (ViewGroup) view; + for (int i = 0; i < group.getChildCount(); i++) { + if (isInstanceOf(group.getChildAt(i), className)) { + return group.getChildAt(i); + } + } + return null; + } + + private boolean hasToolbar(View collapsingToolbar) { + if (!(collapsingToolbar instanceof ViewGroup)) { + return false; + } + ViewGroup group = (ViewGroup) collapsingToolbar; + for (int i = 0; i < group.getChildCount(); i++) { + if (isInstanceOf(group.getChildAt(i), DesignLibUtil.CN_TOOLBAR)) { + return true; + } + } + return false; + } + + /** + * Set the vertical scroll position on all the components with the "scrollY" attribute. If the + * component supports nested scrolling attempt that first, then use the unconsumed scroll part + * to scroll the content in the component. + */ + private void handleScrolling(View view) { + BridgeContext context = getContext(); + int scrollPos = context.getScrollYPos(view); + if (scrollPos != 0) { + if (view.isNestedScrollingEnabled()) { + int[] consumed = new int[2]; + if (view.startNestedScroll(DesignLibUtil.SCROLL_AXIS_VERTICAL)) { + view.dispatchNestedPreScroll(0, scrollPos, consumed, null); + view.dispatchNestedScroll(consumed[0], consumed[1], 0, scrollPos, null); + view.stopNestedScroll(); + scrollPos -= consumed[1]; + } + } + if (scrollPos != 0) { + view.scrollBy(0, scrollPos); + } else { + view.scrollBy(0, scrollPos); + } + } else { + view.scrollBy(0, scrollPos); + } + + if (!(view instanceof ViewGroup)) { + return; + } + ViewGroup group = (ViewGroup) view; + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + handleScrolling(child); + } + } + + /** * Check if the object is an instance of a class named {@code className}. This doesn't work * for interfaces. */ diff --git a/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ErrorCatcher.java b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ErrorCatcher.java new file mode 100644 index 0000000..ecf39b3 --- /dev/null +++ b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ErrorCatcher.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.idea.editors.theme.widgets; + +import com.android.ide.common.rendering.api.LayoutLog; +import com.android.layoutlib.bridge.Bridge; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; + +/** + * {@link ViewGroup} that wraps another view and catches any possible exceptions that the child view + * might generate. + * This is used by the theme editor to stop custom views from breaking the preview. + */ +// TODO: This view is just a temporary solution that will be replaced by adding a try / catch +// for custom views in the ClassConverter +public class ErrorCatcher extends ViewGroup { + public ErrorCatcher(Context context) { + super(context); + } + + public ErrorCatcher(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ErrorCatcher(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public ErrorCatcher(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + assert getChildCount() == 1 : "ErrorCatcher can only have one child"; + + View child = getChildAt(0); + try { + measureChild(child, widthMeasureSpec, heightMeasureSpec); + + setMeasuredDimension(resolveSize(child.getMeasuredWidth(), widthMeasureSpec), + resolveSize(child.getMeasuredHeight(), heightMeasureSpec)); + } catch (Throwable t) { + Bridge.getLog().warning(LayoutLog.TAG_BROKEN, "Failed to do onMeasure for view " + + child.getClass().getCanonicalName(), t); + setMeasuredDimension(resolveSize(0, widthMeasureSpec), + resolveSize(0, heightMeasureSpec)); + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + try { + return super.drawChild(canvas, child, drawingTime); + } catch (Throwable t) { + Bridge.getLog().warning(LayoutLog.TAG_BROKEN, "Failed to draw for view " + + child.getClass().getCanonicalName(), t); + } + + return false; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + assert getChildCount() == 1 : "ErrorCatcher can only have one child"; + + View child = getChildAt(0); + try { + child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); + } catch (Throwable e) { + Bridge.getLog().warning(LayoutLog.TAG_BROKEN, "Failed to do onLayout for view " + + child.getClass().getCanonicalName(), e); + } + } +} diff --git a/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/PressedButton.java b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/PressedButton.java new file mode 100644 index 0000000..4320157 --- /dev/null +++ b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/PressedButton.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.idea.editors.theme.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.Button; + +@SuppressWarnings("unused") +public class PressedButton extends Button { + public PressedButton(Context context, AttributeSet attrs) { + super(context, attrs); + + setPressed(true); + jumpDrawablesToCurrentState(); + } +} diff --git a/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ThemePreviewLayout.java b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ThemePreviewLayout.java new file mode 100644 index 0000000..af89910 --- /dev/null +++ b/tools/layoutlib/studio-custom-widgets/src/com/android/tools/idea/editors/theme/widgets/ThemePreviewLayout.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.tools.idea.editors.theme.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; + +/** + * Custom layout used in the theme editor to display the component preview. It arranges the child + * Views as a grid of cards. + * <p/> + * The Views are measured and the maximum width and height are used to dimension all the child + * components. Any margin attributes from the children are ignored and only the item_margin element + * is used. + */ +@SuppressWarnings("unused") +public class ThemePreviewLayout extends ViewGroup { + private final int mMaxColumns; + private final int mMaxColumnWidth; + private final int mMinColumnWidth; + private final int mItemHorizontalMargin; + private final int mItemVerticalMargin; + + /** Item width to use for every card component. This includes margins. */ + private int mItemWidth; + /** Item height to use for every card component. This includes margins. */ + private int mItemHeight; + + /** Calculated number of columns */ + private int mNumColumns; + + public ThemePreviewLayout(Context context) { + this(context, null); + } + + public ThemePreviewLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ThemePreviewLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + if (attrs == null) { + mMaxColumnWidth = Integer.MAX_VALUE; + mMinColumnWidth = 0; + mMaxColumns = Integer.MAX_VALUE; + mItemHorizontalMargin = 0; + mItemVerticalMargin = 0; + return; + } + + DisplayMetrics dm = getResources().getDisplayMetrics(); + int maxColumnWidth = attrs.getAttributeIntValue(null, "max_column_width", Integer + .MAX_VALUE); + int minColumnWidth = attrs.getAttributeIntValue(null, "min_column_width", 0); + int itemHorizontalMargin = attrs.getAttributeIntValue(null, "item_horizontal_margin", 0); + int itemVerticalMargin = attrs.getAttributeIntValue(null, "item_vertical_margin", 0); + + mMaxColumnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + maxColumnWidth, + dm); + mMinColumnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + minColumnWidth, + dm); + mItemHorizontalMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + itemHorizontalMargin, + dm); + mItemVerticalMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, + itemVerticalMargin, + dm); + mMaxColumns = attrs.getAttributeIntValue(null, "max_columns", Integer.MAX_VALUE); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Measure the column size. + // The column has a minimum width that will be used to calculate the maximum number of + // columns that we can fit in the available space. + // + // Once we have the maximum number of columns, we will span all columns width evenly to fill + // all the available space. + int wSize = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight; + + // Calculate the desired width of all columns and take the maximum. + // This step can be skipped if we have a fixed column height so we do not have to + // dynamically calculate it. + int childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + int childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + int itemWidth = 0; + int itemHeight = 0; + for (int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + + if (v.getVisibility() == GONE) { + continue; + } + + measureChild(v, childWidthSpec, childHeightSpec); + + itemWidth = Math.max(itemWidth, v.getMeasuredWidth()); + itemHeight = Math.max(itemHeight, v.getMeasuredHeight()); + } + + itemWidth = Math.min(Math.max(itemWidth, mMinColumnWidth), mMaxColumnWidth); + mNumColumns = Math.min((int) Math.ceil((double) wSize / itemWidth), mMaxColumns); + + // Check how much space this distribution would take taking into account the margins. + // If it's bigger than what we have, remove one column. + int wSizeNeeded = mNumColumns * itemWidth + (mNumColumns - 1) * mItemHorizontalMargin; + if (wSizeNeeded > wSize && mNumColumns > 1) { + mNumColumns--; + } + + if (getChildCount() < mNumColumns) { + mNumColumns = getChildCount(); + } + if (mNumColumns == 0) { + mNumColumns = 1; + } + + // Inform each child of the measurement + childWidthSpec = MeasureSpec.makeMeasureSpec(itemWidth, MeasureSpec.EXACTLY); + childHeightSpec = MeasureSpec.makeMeasureSpec(itemHeight, MeasureSpec.EXACTLY); + for (int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + + if (v.getVisibility() == GONE) { + continue; + } + + measureChild(v, childWidthSpec, childHeightSpec); + } + + // Calculate the height of the first column to measure our own size + int firstColumnItems = getChildCount() / mNumColumns + ((getChildCount() % mNumColumns) > 0 + ? 1 : 0); + + int horizontalMarginsTotalWidth = (mNumColumns - 1) * mItemHorizontalMargin; + int verticalMarginsTotalHeight = (firstColumnItems - 1) * mItemVerticalMargin; + int totalWidth = mNumColumns * itemWidth + horizontalMarginsTotalWidth + + mPaddingRight + mPaddingLeft; + int totalHeight = firstColumnItems * itemHeight + verticalMarginsTotalHeight + + mPaddingBottom + mPaddingTop; + + setMeasuredDimension(resolveSize(totalWidth, widthMeasureSpec), + resolveSize(totalHeight, heightMeasureSpec)); + + mItemWidth = itemWidth; + mItemHeight = itemHeight; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int itemsPerColumn = getChildCount() / mNumColumns; + // The remainder items are distributed one per column. + int remainderItems = getChildCount() % mNumColumns; + + int x = mPaddingLeft; + int y = mPaddingTop; + int position = 1; + for (int i = 0; i < getChildCount(); i++) { + View v = getChildAt(i); + v.layout(x, + y, + x + mItemWidth, + y + mItemHeight); + + if (position == itemsPerColumn + (remainderItems > 0 ? 1 : 0)) { + // Break column + position = 1; + remainderItems--; + x += mItemWidth + mItemHorizontalMargin; + y = mPaddingTop; + } else { + position++; + y += mItemHeight + mItemVerticalMargin; + } + } + } +} + + diff --git a/tools/layoutlib/studio-custom-widgets/studio-android-widgets.iml b/tools/layoutlib/studio-custom-widgets/studio-android-widgets.iml new file mode 100644 index 0000000..b0363d7 --- /dev/null +++ b/tools/layoutlib/studio-custom-widgets/studio-android-widgets.iml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="JAVA_MODULE" version="4"> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" name="layoutlib_api-prebuilt" level="project" /> + <orderEntry type="library" name="framework.jar" level="project" /> + <orderEntry type="module" module-name="bridge" /> + </component> +</module>
\ No newline at end of file diff --git a/tools/orientationplot/README.txt b/tools/orientationplot/README.txt index d53f65e..958207d 100644 --- a/tools/orientationplot/README.txt +++ b/tools/orientationplot/README.txt @@ -9,6 +9,8 @@ PREREQUISITES 2. numpy 3. matplotlib +eg. sudo apt-get install python-numpy python-matplotlib + USAGE ----- diff --git a/tools/orientationplot/orientationplot.py b/tools/orientationplot/orientationplot.py index 6fc3922..77ed074 100755 --- a/tools/orientationplot/orientationplot.py +++ b/tools/orientationplot/orientationplot.py @@ -440,7 +440,7 @@ class Plotter: # Notice print "Window Orientation Listener plotting tool" print "-----------------------------------------\n" -print "Please turn on the Window Orientation Listener logging in Development Settings." +print "Please turn on the Window Orientation Listener logging. See README.txt." # Start adb. print "Starting adb logcat.\n" |
