diff --git a/tns-core-modules-widgets/.github/ISSUE_TEMPLATE/bug_report.md b/tns-core-modules-widgets/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..5ef9b1bb0 --- /dev/null +++ b/tns-core-modules-widgets/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: 'We really appreciate your effort to provide feedback. Before opening a new + issue, please make sure that this case is not already reported in GitHub as an + issue or in StackOverflow as a question.' + +--- + +**Environment** +Provide version numbers for the following components (information can be retrieved by running `tns info` in your project folder or by inspecting the `package.json` of the project): + - CLI: + - Cross-platform modules: + - Android Runtime: + - iOS Runtime: + - Plugin(s): + +**Describe the bug** + + +**To Reproduce** + + +**Expected behavior** + +**Sample project** + + +**Additional context** + diff --git a/tns-core-modules-widgets/.github/ISSUE_TEMPLATE/feature_request.md b/tns-core-modules-widgets/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..397090987 --- /dev/null +++ b/tns-core-modules-widgets/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** + + +**Describe the solution you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + diff --git a/tns-core-modules-widgets/.github/pull_request_template.md b/tns-core-modules-widgets/.github/pull_request_template.md new file mode 100644 index 000000000..8ac5dee8f --- /dev/null +++ b/tns-core-modules-widgets/.github/pull_request_template.md @@ -0,0 +1,37 @@ + + + + + +## PR Checklist + +- [ ] The PR title follows our guidelines: https://github.com/NativeScript/NativeScript/blob/master/CONTRIBUTING.md#commit-messages. +- [ ] There is an issue for the bug/feature this PR is for. To avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it. +- [ ] You have signed the [CLA](http://www.nativescript.org/cla). +- [ ] All existing tests are passing +- [ ] Tests for the changes are included + +## What is the current behavior? + + +## What is the new behavior? + + +Fixes/Implements/Closes #[Issue Number]. + + + + + diff --git a/tns-core-modules-widgets/.gitignore b/tns-core-modules-widgets/.gitignore new file mode 100644 index 000000000..8daa1f735 --- /dev/null +++ b/tns-core-modules-widgets/.gitignore @@ -0,0 +1,54 @@ +/dist +/build + +*.iml + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Built application files +build/ + +## File-based project format: +*.ipr +*.iws + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +## Directory-based project format: +.idea/ + +# Local configuration file (sdk path, etc) +local.properties + +# Gradle generated files +.gradle/ + +# Signing files +.signing/ + +# OS-specific files +.DS_Store +.DS_Store? +ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/xcuserdata/ +ios/TNSWidgets/TNSWidgets.xcodeproj/xcuserdata/ +ios/TNSWidgets/DerivedData/ + +android/widgets/bin +android/widgets/.settings +android/.project +android/widgets/.project +android/.settings \ No newline at end of file diff --git a/tns-core-modules-widgets/.travis.yml b/tns-core-modules-widgets/.travis.yml new file mode 100644 index 000000000..31cd533a3 --- /dev/null +++ b/tns-core-modules-widgets/.travis.yml @@ -0,0 +1,16 @@ +env: + global: + - DATE=$(date +%Y-%m-%d) + - PACKAGE_VERSION=$DATE-$TRAVIS_BUILD_NUMBER +language: objective-c +osx_image: xcode8.3 +install: + - brew update + - brew cask install android-sdk + # Suppress output of sdkmanager to keep log under the 4MB limit of travis-ci + - yes | sdkmanager "platforms;android-23" >/dev/null + - yes | sdkmanager "build-tools;23.0.3" >/dev/null + - yes | sdkmanager "extras;android;m2repository" >/dev/null +before_script: + - export ANDROID_HOME=/usr/local/share/android-sdk +script: ./build.sh $PACKAGE_VERSION diff --git a/tns-core-modules-widgets/CODE_OF_CONDUCT.md b/tns-core-modules-widgets/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..1c845d0b1 --- /dev/null +++ b/tns-core-modules-widgets/CODE_OF_CONDUCT.md @@ -0,0 +1,83 @@ +# NativeScript Community Code of Conduct + +Our community members come from all walks of life and are all at different stages of their personal and professional journeys. To support everyone, we've prepared a short code of conduct. Our mission is best served in an environment that is friendly, safe, and accepting; free from intimidation or harassment. + +Towards this end, certain behaviors and practices will not be tolerated. + +## tl;dr + +- Be respectful. +- We're here to help. +- Abusive behavior is never tolerated. +- Violations of this code may result in swift and permanent expulsion from the NativeScript community channels. + +## Administrators + +- Dan Wilson (@DanWilson on Slack) +- Jen Looper (@jen.looper on Slack) +- TJ VanToll (@tjvantoll on Slack) + +## Scope + +We expect all members of the NativeScript community, including administrators, users, facilitators, and vendors to abide by this Code of Conduct at all times in our community venues, online and in person, and in one-on-one communications pertaining to NativeScript affairs. + +This policy covers the usage of the NativeScript Slack community, as well as the NativeScript support forums, NativeScript GitHub repositories, the NativeScript website, and any NativeScript-related events. This Code of Conduct is in addition to, and does not in any way nullify or invalidate, any other terms or conditions related to use of NativeScript. + +The definitions of various subjective terms such as "discriminatory", "hateful", or "confusing" will be decided at the sole discretion of the NativeScript administrators. + +## Friendly, Harassment-Free Space + +We are committed to providing a friendly, safe, and welcoming environment for all, regardless of gender identity, sexual orientation, disability, ethnicity, religion, age, physical appearance, body size, race, or similar personal characteristics. + +We ask that you please respect that people have differences of opinion regarding technical choices, and acknowledge that every design or implementation choice carries a trade-off and numerous costs. There is seldom a single right answer. A difference of technology preferences is never a license to be rude. + +Any spamming, trolling, flaming, baiting, or other attention-stealing behaviour is not welcome, and will not be tolerated. + +Harassing other users of NativeScript is never tolerated, whether via public or private media. + +Avoid using offensive or harassing package names, nicknames, or other identifiers that might detract from a friendly, safe, and welcoming environment for all. + +Harassment includes, but is not limited to: harmful or prejudicial verbal or written comments related to gender identity, sexual orientation, disability, ethnicity, religion, age, physical appearance, body size, race, or similar personal characteristics; inappropriate use of nudity, sexual images, and/or sexually explicit language in public spaces; threats of physical or non-physical harm; deliberate intimidation, stalking or following; harassing photography or recording; sustained disruption of talks or other events; inappropriate physical contact; and unwelcome sexual attention. + +## Acceptable Content + +The NativeScript administrators reserve the right to make judgement calls about what is and isn't appropriate in published content. These are guidelines to help you be successful in our community. + +Content must contain something applicable to the previously stated goals of the NativeScript community. "Spamming", that is, publishing any form of content that is not applicable, is not allowed. + +Content must not contain illegal or infringing content. You should only publish content to NativeScript properties if you have the right to do so. This includes complying with all software license agreements or other intellectual property restrictions. For example, redistributing an MIT-licensed module with the copyright notice removed, would not be allowed. You will be responsible for any violation of laws or others’ intellectual property rights. + +Content must not be malware. For example, content (code, video, pictures, words, etc.) which is designed to maliciously exploit or damage computer systems, is not allowed. + +Content name, description, and other visible metadata must not include abusive, inappropriate, or harassing content. + +## Reporting Violations of this Code of Conduct + +If you believe someone is harassing you or has otherwise violated this Code of Conduct, please contact the administrators and send us an abuse report. If this is the initial report of a problem, please include as much detail as possible. It is easiest for us to address issues when we have more context. + +## Consequences + +All content published to the NativeScript community channels is hosted at the sole discretion of the NativeScript administrators. + +Unacceptable behavior from any community member, including sponsors, employees, customers, or others with decision-making authority, will not be tolerated. + +Anyone asked to stop unacceptable behavior is expected to comply immediately. + +If a community member engages in unacceptable behavior, the NativeScript administrators may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event or service). + +## Addressing Grievances + +If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify the administrators. We will do our best to ensure that your grievance is handled appropriately. + +In general, we will choose the course of action that we judge as being most in the interest of fostering a safe and friendly community. + +## Contact Info +Please contact Dan Wilson @DanWilson if you need to report a problem or address a grievance related to an abuse report. + +You are also encouraged to contact us if you are curious about something that might be "on the line" between appropriate and inappropriate content. We are happy to provide guidance to help you be a successful part of our community. + +## Credit and License + +This Code of Conduct borrows heavily from the WADE Code of Conduct, which is derived from the NodeBots Code of Conduct, which in turn borrows from the npm Code of Conduct, which was derived from the Stumptown Syndicate Citizen's Code of Conduct, and the Rust Project Code of Conduct. + +This document may be reused under a Creative Commons Attribution-ShareAlike License. \ No newline at end of file diff --git a/tns-core-modules-widgets/CONTRIBUTING.md b/tns-core-modules-widgets/CONTRIBUTING.md new file mode 100644 index 000000000..1f7aaa709 --- /dev/null +++ b/tns-core-modules-widgets/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# Contributing to NativeScript Core Modules Widgets + +:+1: First of all, thank you for taking the time to contribute! :+1: + +Here are some guides on how to do that: + + + +- [Code of Conduct](#code-of-conduct) +- [Reporting Bugs](#reporting-bugs) +- [Requesting Features](#requesting-features) +- [Submitting a PR](#submitting-a-pr) +- [Where to Start](#where-to-start) + + + +## Code of Conduct +Help us keep a healthy and open community. We expect all participants in this project to adhere to the [NativeScript Code Of Conduct](https://github.com/NativeScript/codeofconduct). + + +## Reporting Bugs + +1. Always update to the most recent master release; the bug may already be resolved. +2. Search for similar issues in the issues list for this repo; it may already be an identified problem. +3. If this is a bug or problem that is clear, simple, and is unlikely to require any discussion -- it is OK to open an issue on GitHub with a reproduction of the bug including workflows and screenshots. If possible, submit a Pull Request with a failing test, entire application or module. If you'd rather take matters into your own hands, fix the bug yourself (jump down to the [Submitting a PR](#submitting-a-pr) section). + +## Requesting Features + +1. Use Github Issues to submit feature requests. +2. First, search for a similar request and extend it if applicable. This way it would be easier for the community to track the features. +3. When requesting a new feature, please provide as much detail as possible about why you need the feature in your apps. We prefer that you explain a need rather than explain a technical solution for it. That might trigger a nice conversation on finding the best and broadest technical solution to a specific need. + +## Submitting a PR + +Before you begin: +* Read and sign the [NativeScript Contribution License Agreement](http://www.nativescript.org/cla). +* Make sure there is an issue for the bug or feature you will be working on. + +Following these steps is the best way to get you code included in the project: + +1. Fork and clone the tns-core-modules-widgets repo: +```bash +git clone https://github.com//tns-core-modules-widgets.git +# Navigate to the newly cloned directory +cd tns-core-modules-widgets +# Add an "upstream" remote pointing to the original repo. +git remote add upstream https://github.com/NativeScript/tns-core-modules-widgets.git +``` + +2. Read our [development workflow guide](DevelopmentWorkflow.md) for local setup: + +3. Create a branch for your PR +```bash +git checkout -b master +``` + +4. The fun part! Make your code changes. Make sure you: + - Follow the [code conventions guide](https://github.com/NativeScript/NativeScript/blob/master/CodingConvention.md). + - Write unit tests for your fix or feature. + +5. Before you submit your PR: + - Rebase your changes to the latest master: `git pull --rebase upstream master`. + - Ensure all unit test are green. Check [running unit tests](DevelopmentWorkflow.md#running-the-tests). + - Ensure your changes pass tslint validation. (run `npm run tslint` in the root of the repo). + +6. Push your fork. If you have rebased you might have to use force-push your branch: +``` +git push origin --force +``` + +7. [Submit your pull request](https://github.com/NativeScript/tns-core-modules-widgets/compare). Please, fill in the Pull Request template - it will help us better understand the PR and increase the chances of it getting merged quickly. + +It's our turn from there on! We will review the PR and discuss changes you might have to make before merging it! Thanks! + + +## Where to Start + +If you want to contribute, but you are not sure where to start - look for issues labeled [`help wanted`](https://github.com/NativeScript/tns-core-modules-widgets/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22). diff --git a/tns-core-modules-widgets/DevelopmentWorkflow.md b/tns-core-modules-widgets/DevelopmentWorkflow.md new file mode 100644 index 000000000..7a21c8bc1 --- /dev/null +++ b/tns-core-modules-widgets/DevelopmentWorkflow.md @@ -0,0 +1,63 @@ +# Development Workflow + + + +- [Prerequisites](#prerequisites) +- [How to Build the Package](#how-to-build-the-package) +- [How to Build Android](#how-to-build-android) +- [How to Build iOS](#how-to-build-ios) +- [How to Use in an Application](#how-to-use-in-an-application) + + + +## Prerequisites + +Install your native toolchain and NativeScript as described in the docs: https://docs.nativescript.org/setup/quick-setup. In order to open the native Android and iOS project, you need Android Studio and Xcode respectively. + +## How to Build the Package + +On macOS you can execute: + +```shell +$ ./build.sh +``` + +This script builds both Android and iOS, assembles the package at `./dist/package` and packs it as `./dist/tns-core-modules-widgets-*.tgz`. + +## How to Build Android + +On Unix-like operating systems you can execute: + +```shell +$ ./build.android.sh +``` +This script builds only the Android project, assembles the package at `./dist/package` and packs it as `./dist/tns-core-modules-widgets-*.tgz`. The output file is available at `./android/widgets/build/outputs/aar/widgets-release.aar`. + +**NOTE:** To run bash script on Windows you can install [GIT SCM](https://git-for-windows.github.io/) and use Git Bash. + +## How to Build iOS + +On macOS you can execute: + +```shell +$ ./build.ios.sh +``` +This script builds only the Xcode project, assembles the package at `./dist/package` and packs it as `./dist/tns-core-modules-widgets-*.tgz`. The output native iOS framework is available at `./ios/TNSWidgets/build/TNSWidgets.framework`. + +## How to Use in an Application + +You could link the `tns-core-modules-widgets` plugin package to your application through the steps listed below. + +In the `./dist/package` folder execute: + +``` +npm link +``` + +In your application project folder execute: + +``` +npm link tns-core-modules-widgets +``` + +Build the plugin with the above-mentioned commands after each change you would like to test. diff --git a/tns-core-modules-widgets/LICENSE b/tns-core-modules-widgets/LICENSE new file mode 100755 index 000000000..061c44028 --- /dev/null +++ b/tns-core-modules-widgets/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2015-2019 Progress Software Corporation + + 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. \ No newline at end of file diff --git a/tns-core-modules-widgets/README.md b/tns-core-modules-widgets/README.md new file mode 100644 index 000000000..dfc75b6d3 --- /dev/null +++ b/tns-core-modules-widgets/README.md @@ -0,0 +1,19 @@ +# NativeScript Core Modules Widgets +[![Build Status](https://travis-ci.org/NativeScript/tns-core-modules-widgets.svg?branch=master)](https://travis-ci.org/NativeScript/tns-core-modules-widgets) + +This repository contains the source code of the `tns-core-modules-widgets` library. This library represents native code (Java and Objective-C) used by the NativeScript [`core modules`](https://github.com/NativeScript/NativeScript/tree/master/tns-core-modules). + +[NativeScript](https://www.nativescript.org/) is a framework which enables developers to write truly native mobile applications for Android and iOS using JavaScript and CSS. + + + +- [Contribute](#contribute) +- [Get Help](#get-help) + + + +## Contribute +We love PRs! Check out the [contributing guidelines](CONTRIBUTING.md) and [development workflow for local setup](DevelopmentWorkflow.md). If you want to contribute, but you are not sure where to start - look for issues labeled [`help wanted`](https://github.com/NativeScript/tns-core-modules-widgets/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22). + +## Get Help +Please, use [github issues](https://github.com/NativeScript/tns-core-modules-widgets/issues) strictly for [reporting bugs](CONTRIBUTING.md#reporting-bugs) or [requesting features](CONTRIBUTING.md#requesting-new-features). For general questions and support, check out the [NativeScript community forum](https://discourse.nativescript.org/) or ask our experts in [NativeScript community Slack channel](http://developer.telerik.com/wp-login.php?action=slack-invitation). diff --git a/tns-core-modules-widgets/android/.gitignore b/tns-core-modules-widgets/android/.gitignore new file mode 100644 index 000000000..c6cbe562a --- /dev/null +++ b/tns-core-modules-widgets/android/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures diff --git a/tns-core-modules-widgets/android/README.md b/tns-core-modules-widgets/android/README.md new file mode 100644 index 000000000..5633091be --- /dev/null +++ b/tns-core-modules-widgets/android/README.md @@ -0,0 +1,14 @@ +### Android + +This directory contains an Android Studio project. + +### How to open? +* In Android Studio choose: File -> Open +* Navigate to `tns-core-modules-widgets/android/` folder +* On the left side of the screen choose the Project tab and select `widgets` + +### How to build? +* On the right side of the screen choose the Gradle tab +* Navigate to `android/widgets/Tasks/build/` +* Execute the `assembleRelease` task +* Output will be in `./android/widgets/build/outputs/` diff --git a/tns-core-modules-widgets/android/build.gradle b/tns-core-modules-widgets/android/build.gradle new file mode 100644 index 000000000..a443fad34 --- /dev/null +++ b/tns-core-modules-widgets/android/build.gradle @@ -0,0 +1,25 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.3' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/tns-core-modules-widgets/android/gradle.properties b/tns-core-modules-widgets/android/gradle.properties new file mode 100644 index 000000000..fab0e33e6 --- /dev/null +++ b/tns-core-modules-widgets/android/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# Due to a known issue with Android Gradle Plugin versions 3.0.* and 3.1.*, +# the configuration on demand should be disabled. +# https://developer.android.com/studio/known-issues +org.gradle.configureondemand=false \ No newline at end of file diff --git a/tns-core-modules-widgets/android/gradle/wrapper/gradle-wrapper.jar b/tns-core-modules-widgets/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..13372aef5 Binary files /dev/null and b/tns-core-modules-widgets/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/tns-core-modules-widgets/android/gradle/wrapper/gradle-wrapper.properties b/tns-core-modules-widgets/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..79d846136 --- /dev/null +++ b/tns-core-modules-widgets/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Sep 02 07:50:15 EEST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip diff --git a/tns-core-modules-widgets/android/gradlew b/tns-core-modules-widgets/android/gradlew new file mode 100755 index 000000000..9d82f7891 --- /dev/null +++ b/tns-core-modules-widgets/android/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/tns-core-modules-widgets/android/gradlew.bat b/tns-core-modules-widgets/android/gradlew.bat new file mode 100644 index 000000000..aec99730b --- /dev/null +++ b/tns-core-modules-widgets/android/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/tns-core-modules-widgets/android/settings.gradle b/tns-core-modules-widgets/android/settings.gradle new file mode 100644 index 000000000..09ee5ce4c --- /dev/null +++ b/tns-core-modules-widgets/android/settings.gradle @@ -0,0 +1 @@ +include ':widgets' diff --git a/tns-core-modules-widgets/android/widgets/.gitignore b/tns-core-modules-widgets/android/widgets/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/.gitignore @@ -0,0 +1 @@ +/build diff --git a/tns-core-modules-widgets/android/widgets/build.gradle b/tns-core-modules-widgets/android/widgets/build.gradle new file mode 100644 index 000000000..ce1451440 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/build.gradle @@ -0,0 +1,81 @@ +import groovy.json.JsonSlurper //used to parse package.json +import groovy.json.JsonBuilder +import groovy.json.JsonOutput + +def isWinOs = System.properties['os.name'].toLowerCase().contains('windows') + +apply plugin: 'com.android.library' + +def computeCompileSdkVersion () { + if(project.hasProperty("compileSdk")) { + return compileSdk + } + else { + return 28 + } +} + +def computeBuildToolsVersion() { + if(project.hasProperty("buildToolsVersion")) { + return buildToolsVersion + } + else { + return "28.0.2" + } +} + +def computeSupportVersion() { + if(project.hasProperty("supportVersion")) { + return supportVersion + } + else { + return "28.0.0" + } +} + +def computeTargetSdkVersion() { + if(project.hasProperty("targetSdk")) { + return targetSdk + } + else { + return 28 + } +} + +android { + compileSdkVersion computeCompileSdkVersion() + buildToolsVersion computeBuildToolsVersion() + + defaultConfig { + minSdkVersion 16 + targetSdkVersion computeTargetSdkVersion() + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.android.support:support-v4:' + computeSupportVersion() +} + +task cleanBuildDir (type: Delete) { + delete "../build/" +} + +task copyAar << { + copy { + from "build/outputs/aar/widgets-release.aar" + into "../build/" + } +} + +assemble.dependsOn(cleanBuildDir) +copyAar.dependsOn(assemble) +build.dependsOn(copyAar) \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/gradle/wrapper/gradle-wrapper.jar b/tns-core-modules-widgets/android/widgets/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..13372aef5 Binary files /dev/null and b/tns-core-modules-widgets/android/widgets/gradle/wrapper/gradle-wrapper.jar differ diff --git a/tns-core-modules-widgets/android/widgets/gradle/wrapper/gradle-wrapper.properties b/tns-core-modules-widgets/android/widgets/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..122a0dca2 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Dec 28 10:00:20 PST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip diff --git a/tns-core-modules-widgets/android/widgets/gradlew b/tns-core-modules-widgets/android/widgets/gradlew new file mode 100644 index 000000000..9d82f7891 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/tns-core-modules-widgets/android/widgets/gradlew.bat b/tns-core-modules-widgets/android/widgets/gradlew.bat new file mode 100644 index 000000000..aec99730b --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/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/tns-core-modules-widgets/android/widgets/proguard-rules.pro b/tns-core-modules-widgets/android/widgets/proguard-rules.pro new file mode 100644 index 000000000..dca5777d1 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/plamen5kov/tools/android-sdk-linux/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/tns-core-modules-widgets/android/widgets/src/main/AndroidManifest.xml b/tns-core-modules-widgets/android/widgets/src/main/AndroidManifest.xml new file mode 100644 index 000000000..803685657 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/AndroidManifest.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/Process.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/Process.java new file mode 100644 index 000000000..cbc42c4ca --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/Process.java @@ -0,0 +1,59 @@ +package org.nativescript; + +import android.os.SystemClock; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.FileReader; + +/** + * Created by cankov on 15/05/2017. + */ +public class Process { + static long startTime = -1; + + public static long getStartTime() { + if (startTime != -1) { + return startTime; + } + + try { + int pid = android.os.Process.myPid(); + final String path = "/proc/" + pid + "/stat"; + final BufferedReader reader = new BufferedReader(new FileReader(path)); + final String stat; + try { + stat = reader.readLine(); + } finally { + reader.close(); + } + final String field2End = ") "; + final String fieldSep = " "; + final int fieldStartTime = 20; + final int msInSec = 1000; + + final String[] fields = stat.substring(stat.lastIndexOf(field2End)).split(fieldSep); + final long t = Long.parseLong(fields[fieldStartTime]); + int tckName; + try { + tckName = Class.forName("android.system.OsConstants").getField("_SC_CLK_TCK").getInt(null); + } catch (ClassNotFoundException e) { + tckName = Class.forName("libcore.io.OsConstants").getField("_SC_CLK_TCK").getInt(null); + } + final Object os = Class.forName("libcore.io.Libcore").getField("os").get(null); + final long tck = (Long) os.getClass().getMethod("sysconf", Integer.TYPE).invoke(os, tckName); + startTime = t * msInSec / tck; + } catch (Exception e) { + Log.v("JS", "Failed to get process start time. Using the current time as start time. Error: " + e); + startTime = SystemClock.elapsedRealtime(); + } + + return startTime; + } + + public static long getUpTime() { + long startTime = getStartTime(); + long currentTime = SystemClock.elapsedRealtime(); + return currentTime - startTime; + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/AbsoluteLayout.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/AbsoluteLayout.java new file mode 100644 index 000000000..fdb4fdf39 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/AbsoluteLayout.java @@ -0,0 +1,81 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class AbsoluteLayout extends LayoutBase { + + public AbsoluteLayout(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int measureWidth = 0; + int measureHeight = 0; + int childMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + for (int i = 0, count = this.getChildCount(); i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.measureChild(child, childMeasureSpec, childMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + measureWidth = Math.max(measureWidth, childLayoutParams.left + childMeasuredWidth); + measureHeight = Math.max(measureHeight, childLayoutParams.top + childMeasuredHeight); + } + + // Add in our padding + measureWidth += this.getPaddingLeft() + this.getPaddingRight(); + measureHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum height + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int leftPadding = this.getPaddingLeft(); + int topPadding = this.getPaddingTop(); + + for (int i = 0, count = this.getChildCount(); i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + + int childLeft = leftPadding + childLayoutParams.left; + int childTop = topPadding + childLayoutParams.top; + int childRight = childLeft + childWidth + childLayoutParams.leftMargin + childLayoutParams.rightMargin; + int childBottom = childTop + childHeight + childLayoutParams.topMargin + childLayoutParams.bottomMargin; + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childRight, childBottom); + } + + CommonLayoutParams.restoreOriginalParams(this); + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/AnimatorHelper.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/AnimatorHelper.java new file mode 100644 index 000000000..3dfaf0708 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/AnimatorHelper.java @@ -0,0 +1,61 @@ +package org.nativescript.widgets; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; + +class AnimatorHelper { + static final int version = android.os.Build.VERSION.SDK_INT; + static final int exitFakeResourceId = -20; + + static Animator createDummyAnimator(long duration) { + float[] alphaValues = new float[2]; + alphaValues[0] = 1; + alphaValues[1] = 1; + + Animator animator = ObjectAnimator.ofFloat(null, "alpha", alphaValues); + if (duration > 0) { + animator.setDuration(duration); + } + + return animator; + } + + static long getTotalDuration(Animator animator) { + if (animator instanceof AnimatorSet) { + return getAnimatorSetTotalDuration((AnimatorSet)animator); + } else { + return getAnimatorTotalDuration(animator); + } + } + + static long getAnimatorTotalDuration(Animator animator) { + long totalDuration; + if (version >= 24) { + totalDuration = animator.getTotalDuration(); + } else { + long duration = animator.getDuration(); + if (duration == Animator.DURATION_INFINITE) { + totalDuration = Animator.DURATION_INFINITE; + } else { + totalDuration = animator.getStartDelay() + duration; + } + } + + return totalDuration; + } + + static long getAnimatorSetTotalDuration(AnimatorSet animatorSet) { + long totalDuration = 0; + if (version >= 24) { + totalDuration = animatorSet.getTotalDuration(); + } else { + // TODO: this is only meaningful for "playTogether" animators + for (Animator animator: animatorSet.getChildAnimations()) { + totalDuration = Math.max(totalDuration, getTotalDuration(animator)); + } + } + + return totalDuration; + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Async.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Async.java new file mode 100644 index 000000000..5ed78364f --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Async.java @@ -0,0 +1,587 @@ +package org.nativescript.widgets; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.util.Base64; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Stack; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; + +public class Async { + static final String TAG = "Async"; + static ThreadPoolExecutor executor = null; + + static ThreadPoolExecutor threadPoolExecutor() { + if (executor == null) { + int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(); + ThreadFactory backgroundPriorityThreadFactory = new PriorityThreadFactory(android.os.Process.THREAD_PRIORITY_BACKGROUND); + + executor = new ThreadPoolExecutor( + NUMBER_OF_CORES * 2, + NUMBER_OF_CORES * 2, + 60L, + TimeUnit.SECONDS, + new LinkedBlockingQueue(), + backgroundPriorityThreadFactory + ); + } + + return executor; + } + + public interface CompleteCallback { + void onComplete(Object result, Object tag); + + void onError(String error, Object tag); + } + + static class PriorityThreadFactory implements ThreadFactory { + private final int mThreadPriority; + + public PriorityThreadFactory(int threadPriority) { + mThreadPriority = threadPriority; + } + + @Override + public Thread newThread(final Runnable runnable) { + Runnable wrapperRunnable = new Runnable() { + @Override + public void run() { + try { + android.os.Process.setThreadPriority(mThreadPriority); + } catch (Throwable t) { + + } + runnable.run(); + } + }; + return new Thread(wrapperRunnable); + } + } + + public static class Image { + /* + * The request id parameter is needed for the sake of the JavaScript implementation. + * Because we want to use only one extend of the CompleteCallback interface (for the sake of better performance) + * we use this id to detect the initial request, which result is currently received in the complete callback. + * When the async task completes it will pass back this id to JavaScript. + */ + public static void fromResource(final String name, final Context context, final int requestId, final CompleteCallback callback) { + final android.os.Handler mHandler = new android.os.Handler(); + threadPoolExecutor().execute(new Runnable() { + @Override + public void run() { + final LoadImageFromResourceTask task = new LoadImageFromResourceTask(context, requestId, callback); + final Bitmap result = task.doInBackground(name); + mHandler.post(new Runnable() { + @Override + public void run() { + task.onPostExecute(result); + } + }); + } + }); + } + + public static void fromFile(final String fileName, final int requestId, final CompleteCallback callback) { + final android.os.Handler mHandler = new android.os.Handler(); + threadPoolExecutor().execute(new Runnable() { + @Override + public void run() { + final LoadImageFromFileTask task = new LoadImageFromFileTask(requestId, callback); + final Bitmap result = task.doInBackground(fileName); + mHandler.post(new Runnable() { + @Override + public void run() { + task.onPostExecute(result); + } + }); + } + }); + } + + public static void fromBase64(final String source, final int requestId, final CompleteCallback callback) { + final android.os.Handler mHandler = new android.os.Handler(); + threadPoolExecutor().execute(new Runnable() { + @Override + public void run() { + final LoadImageFromBase64StringTask task = new LoadImageFromBase64StringTask(requestId, callback); + final Bitmap result = task.doInBackground(source); + mHandler.post(new Runnable() { + @Override + public void run() { + task.onPostExecute(result); + } + }); + } + }); + } + + public static void download(final String url, final CompleteCallback callback, final Object context) { + final android.os.Handler mHandler = new android.os.Handler(); + threadPoolExecutor().execute(new Runnable() { + @Override + public void run() { + final DownloadImageTask task = new DownloadImageTask(callback, context); + final Bitmap result = task.doInBackground(url); + mHandler.post(new Runnable() { + @Override + public void run() { + task.onPostExecute(result); + } + }); + } + }); + } + + static class DownloadImageTask { + private CompleteCallback callback; + private Object context; + + public DownloadImageTask(CompleteCallback callback, Object context) { + this.callback = callback; + this.context = context; + } + + protected Bitmap doInBackground(String... params) { + InputStream stream = null; + try { + stream = new java.net.URL(params[0]).openStream(); + Bitmap bmp = BitmapFactory.decodeStream(stream); + return bmp; + } catch (MalformedURLException e) { + Log.e(TAG, "Failed to decode stream, MalformedURLException: " + e.getMessage()); + return null; + } catch (IOException e) { + Log.e(TAG, "Failed to decode stream, IOException: " + e.getMessage()); + return null; + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to close stream, IOException: " + e.getMessage()); + } + } + } + } + + protected void onPostExecute(final Bitmap result) { + if (result != null) { + this.callback.onComplete(result, this.context); + } else { + this.callback.onError("DownloadImageTask returns no result.", this.context); + } + } + } + + static class LoadImageFromResourceTask { + private CompleteCallback callback; + private Context context; + private int requestId; + + public LoadImageFromResourceTask(Context context, int requestId, CompleteCallback callback) { + this.callback = callback; + this.context = context; + this.requestId = requestId; + } + + protected Bitmap doInBackground(String... params) { + String name = params[0]; + Resources res = this.context.getResources(); + int id = res.getIdentifier(name, "drawable", context.getPackageName()); + + if (id > 0) { + BitmapDrawable result = (BitmapDrawable) res.getDrawable(id); + return result.getBitmap(); + } + + return null; + } + + protected void onPostExecute(final Bitmap result) { + if (result != null) { + this.callback.onComplete(result, this.requestId); + } else { + this.callback.onError("LoadImageFromResourceTask returns no result.", this.requestId); + } + } + } + + static class LoadImageFromFileTask { + private CompleteCallback callback; + private int requestId; + + public LoadImageFromFileTask(int requestId, CompleteCallback callback) { + this.callback = callback; + this.requestId = requestId; + } + + protected Bitmap doInBackground(String... params) { + String fileName = params[0]; + return BitmapFactory.decodeFile(fileName); + } + + protected void onPostExecute(final Bitmap result) { + if (result != null) { + this.callback.onComplete(result, this.requestId); + } else { + this.callback.onError("LoadImageFromFileTask returns no result.", this.requestId); + } + } + } + + static class LoadImageFromBase64StringTask { + private CompleteCallback callback; + private int requestId; + + public LoadImageFromBase64StringTask(int requestId, CompleteCallback callback) { + this.callback = callback; + this.requestId = requestId; + } + + protected Bitmap doInBackground(String... params) { + String source = params[0]; + byte[] bytes = Base64.decode(source, Base64.DEFAULT); + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + } + + protected void onPostExecute(final Bitmap result) { + if (result != null) { + this.callback.onComplete(result, this.requestId); + } else { + this.callback.onError("LoadImageFromBase64StringTask returns no result.", this.requestId); + } + } + } + } + + public static class Http { + private static final String DELETE_METHOD = "DELETE"; + private static final String GET_METHOD = "GET"; + private static final String HEAD_METHOD = "HEAD"; + private static CookieManager cookieManager; + + public static void MakeRequest(final RequestOptions options, final CompleteCallback callback, final Object context) { + if (cookieManager == null) { + cookieManager = new CookieManager(); + CookieHandler.setDefault(cookieManager); + } + + final android.os.Handler mHandler = new android.os.Handler(); + threadPoolExecutor().execute(new Runnable() { + @Override + public void run() { + final HttpRequestTask task = new HttpRequestTask(callback, context); + final RequestResult result = task.doInBackground(options); + mHandler.post(new Runnable() { + @Override + public void run() { + task.onPostExecute(result); + } + }); + } + }); + } + + public static class KeyValuePair { + public String key; + public String value; + + public KeyValuePair(String key, String value) { + this.key = key; + this.value = value; + } + } + + public static class RequestOptions { + public String url; + public String method; + public ArrayList headers; + public String content; + public int timeout = -1; + public int screenWidth = -1; + public int screenHeight = -1; + public boolean dontFollowRedirects = false; + + public void addHeaders(HttpURLConnection connection) { + if (this.headers == null) { + return; + } + boolean hasAcceptHeader = false; + + for (KeyValuePair pair : this.headers) { + String key = pair.key.toString(); + connection.addRequestProperty(key, pair.value.toString()); + if (key.toLowerCase().contentEquals("accept-encoding")) { + hasAcceptHeader = true; + } + } + + // If the user hasn't added an Accept-Encoding header, we add gzip as something we accept + if (!hasAcceptHeader) { + connection.addRequestProperty("Accept-Encoding", "gzip"); + } + } + + public void writeContent(HttpURLConnection connection, Stack openedStreams) throws IOException { + if (this.content == null || this.content.getClass() != String.class) { + return; + } + + OutputStream outStream = connection.getOutputStream(); + openedStreams.push(outStream); + + OutputStreamWriter writer = new OutputStreamWriter(outStream); + openedStreams.push(writer); + + writer.write((String) this.content); + } + } + + public static class RequestResult { + public ByteArrayOutputStream raw; + public ArrayList headers = new ArrayList(); + public int statusCode; + public String responseAsString; + public Bitmap responseAsImage; + public Exception error; + public String url; + public String statusText; + + public void getHeaders(HttpURLConnection connection) { + Map> headers = connection.getHeaderFields(); + if (headers == null) { + // no headers, this may happen if there is no internet connection currently available + return; + } + + int size = headers.size(); + if (size == 0) { + return; + } + + for (Map.Entry> entry : headers.entrySet()) { + String key = entry.getKey(); + for (String value : entry.getValue()) { + this.headers.add(new KeyValuePair(key, value)); + } + } + } + + public void readResponseStream(HttpURLConnection connection, Stack openedStreams, RequestOptions options) throws IOException { + int contentLength = connection.getContentLength(); + + InputStream inStream = + this.statusCode >= 400 + ? connection.getErrorStream() + : connection.getInputStream(); + + if (inStream == null) { + // inStream is null when receiving status code 401 or 407 + // see this thread for more information http://stackoverflow.com/a/24986433 + return; + } + + // In the event we don't have a null stream, and we have gzip as part of the encoding + // then we will use gzip to decode the stream + String encodingHeader = connection.getHeaderField("Content-Encoding"); + if (encodingHeader != null && encodingHeader.toLowerCase().contains("gzip")) { + inStream = new GZIPInputStream(inStream); + } + + openedStreams.push(inStream); + + BufferedInputStream buffer = new BufferedInputStream(inStream, 4096); + openedStreams.push(buffer); + + ByteArrayOutputStream2 responseStream = contentLength != -1 ? new ByteArrayOutputStream2(contentLength) : new ByteArrayOutputStream2(); + openedStreams.push(responseStream); + + byte[] buff = new byte[4096]; + int read = -1; + while ((read = buffer.read(buff, 0, buff.length)) != -1) { + responseStream.write(buff, 0, read); + } + + this.raw = responseStream; + buff = null; + + // make the byte array conversion here, not in the JavaScript + // world for better performance + // since we do not have some explicit way to determine whether + // the content-type is image + try { + // TODO: Generally this approach will not work for very + // large files + BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); + bitmapOptions.inJustDecodeBounds = true; + + // check the size of the bitmap first + BitmapFactory.decodeByteArray(responseStream.buf(), 0, responseStream.size(), bitmapOptions); + if (bitmapOptions.outWidth > 0 && bitmapOptions.outHeight > 0) { + int scale = 1; + final int height = bitmapOptions.outHeight; + final int width = bitmapOptions.outWidth; + + if ((options.screenWidth > 0 && bitmapOptions.outWidth > options.screenWidth) || + (options.screenHeight > 0 && bitmapOptions.outHeight > options.screenHeight)) { + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + // scale down the image since it is larger than the + // screen resolution + while ((halfWidth / scale) > options.screenWidth && (halfHeight / scale) > options.screenHeight) { + scale *= 2; + } + } + + bitmapOptions.inJustDecodeBounds = false; + bitmapOptions.inSampleSize = scale; + this.responseAsImage = BitmapFactory.decodeByteArray(responseStream.buf(), 0, responseStream.size(), bitmapOptions); + } + } catch (Exception e) { + Log.e(TAG, "Failed to decode byte array, Exception: " + e.getMessage()); + } + + if (this.responseAsImage == null) { + // convert to string + this.responseAsString = responseStream.toString(); + } + } + + public static final class ByteArrayOutputStream2 extends ByteArrayOutputStream { + public ByteArrayOutputStream2() { + super(); + } + + public ByteArrayOutputStream2(int size) { + super(size); + } + + /** + * Returns the internal buffer of this ByteArrayOutputStream, without copying. + */ + public synchronized byte[] buf() { + return this.buf; + } + } + } + + static class HttpRequestTask { + private CompleteCallback callback; + private Object context; + + public HttpRequestTask(CompleteCallback callback, Object context) { + this.callback = callback; + this.context = context; + } + + protected RequestResult doInBackground(RequestOptions... params) { + RequestResult result = new RequestResult(); + Stack openedStreams = new Stack(); + + try { + RequestOptions options = params[0]; + URL url = new URL(options.url); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + // set the request method + String requestMethod = options.method != null ? options.method.toUpperCase(Locale.ENGLISH) : GET_METHOD; + connection.setRequestMethod(requestMethod); + + // add the headers + options.addHeaders(connection); + + // apply timeout + if (options.timeout > 0) { + connection.setConnectTimeout(options.timeout); + } + + // don't follow redirect (30x) responses; by default, HttpURLConnection follows them. + if (options.dontFollowRedirects) { + connection.setInstanceFollowRedirects(false); + } + + // Do not attempt to write the content (body) for DELETE method, Java will throw directly + if (!requestMethod.equals(DELETE_METHOD)) { + options.writeContent(connection, openedStreams); + } + + // close the opened streams (saves copy-paste implementation + // in each method that throws IOException) + this.closeOpenedStreams(openedStreams); + + connection.connect(); + + // build the result + result.getHeaders(connection); + result.url = options.url; + result.statusCode = connection.getResponseCode(); + result.statusText = connection.getResponseMessage(); + if (!requestMethod.equals(HEAD_METHOD)) { + result.readResponseStream(connection, openedStreams, options); + } + + // close the opened streams (saves copy-paste implementation + // in each method that throws IOException) + this.closeOpenedStreams(openedStreams); + + connection.disconnect(); + + return result; + } catch (Exception e) // TODO: Catch all exceptions? + { + result.error = e; + + return result; + } finally { + try { + this.closeOpenedStreams(openedStreams); + } catch (IOException e) { + Log.e(TAG, "Failed to close opened streams, IOException: " + e.getMessage()); + } + } + } + + protected void onPostExecute(final RequestResult result) { + if (result != null) { + this.callback.onComplete(result, this.context); + } else { + this.callback.onError("HttpRequestTask returns no result.", this.context); + } + } + + private void closeOpenedStreams(Stack streams) throws IOException { + while (streams.size() > 0) { + Closeable stream = streams.pop(); + stream.close(); + } + } + } + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java new file mode 100644 index 000000000..3608a195d --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/BorderDrawable.java @@ -0,0 +1,815 @@ +package org.nativescript.widgets; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.LinearGradient; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.Shader; + +import org.nativescript.widgets.image.BitmapOwner; +import org.nativescript.widgets.image.Fetcher; + +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Created by hristov on 6/15/2016. + */ +public class BorderDrawable extends ColorDrawable implements BitmapOwner { + private float density; + private String id; + + private int borderTopColor; + private int borderRightColor; + private int borderBottomColor; + private int borderLeftColor; + + private float borderTopWidth; + private float borderRightWidth; + private float borderBottomWidth; + private float borderLeftWidth; + + private float borderTopLeftRadius; + private float borderTopRightRadius; + private float borderBottomRightRadius; + private float borderBottomLeftRadius; + + private String clipPath; + + private int backgroundColor; + private String backgroundImage; + private Bitmap backgroundBitmap; + private LinearGradientDefinition backgroundGradient; + private String backgroundRepeat; + private String backgroundPosition; + private CSSValue[] backgroundPositionParsedCSSValues; + private String backgroundSize; + private CSSValue[] backgroundSizeParsedCSSValues; + + private Drawable drawable; + + public float getDensity() { + return density; + } + + public int getBorderTopColor() { + return borderTopColor; + } + + public int getBorderRightColor() { + return borderRightColor; + } + + public int getBorderBottomColor() { + return borderBottomColor; + } + + public int getBorderLeftColor() { + return borderLeftColor; + } + + public int getUniformBorderColor() { + if (this.hasUniformBorderColor()) { + return this.borderTopColor; + } + + return 0; + } + + public float getBorderTopWidth() { + return borderTopWidth; + } + + public float getBorderRightWidth() { + return borderRightWidth; + } + + public float getBorderBottomWidth() { + return borderBottomWidth; + } + + public float getBorderLeftWidth() { + return borderLeftWidth; + } + + public float getUniformBorderWidth() { + if (this.hasUniformBorderWidth()) { + return this.borderTopWidth; + } + + return 0; + } + + public float getBorderTopLeftRadius() { + return borderTopLeftRadius; + } + + public float getBorderTopRightRadius() { + return borderTopRightRadius; + } + + public float getBorderBottomRightRadius() { + return borderBottomRightRadius; + } + + public float getBorderBottomLeftRadius() { + return borderBottomLeftRadius; + } + + public float getUniformBorderRadius() { + if (this.hasUniformBorderRadius()) { + return this.borderTopLeftRadius; + } + + return 0; + } + + public String getClipPath() { + return clipPath; + } + + public int getBackgroundColor() { + return backgroundColor; + } + + public String getBackgroundImage() { + return backgroundImage; + } + + public Bitmap getBackgroundBitmap() { + return backgroundBitmap; + } + + public LinearGradientDefinition getBackgroundGradient() { return backgroundGradient; } + + public String getBackgroundRepeat() { + return backgroundRepeat; + } + + public String getBackgroundPosition() { + return backgroundPosition; + } + + public String getBackgroundSize() { + return backgroundSize; + } + + public boolean hasBorderWidth() { + return this.borderTopWidth != 0 + || this.borderRightWidth != 0 + || this.borderBottomWidth != 0 + || this.borderLeftWidth != 0; + } + + public boolean hasUniformBorderColor() { + return this.borderTopColor == this.borderRightColor && + this.borderTopColor == this.borderBottomColor && + this.borderTopColor == this.borderLeftColor; + } + + public boolean hasUniformBorderWidth() { + return this.borderTopWidth == this.borderRightWidth && + this.borderTopWidth == this.borderBottomWidth && + this.borderTopWidth == this.borderLeftWidth; + } + + public boolean hasUniformBorderRadius() { + return this.borderTopLeftRadius == this.borderTopRightRadius && + this.borderTopLeftRadius == this.borderBottomRightRadius && + this.borderTopLeftRadius == this.borderBottomLeftRadius; + } + + public boolean hasUniformBorder() { + return this.hasUniformBorderColor() && + this.hasUniformBorderWidth() && + this.hasUniformBorderRadius(); + } + + public BorderDrawable(float density) { + super(); + this.density = density; + } + + public BorderDrawable(float density, String id) { + super(); + this.density = density; + this.id = id; + } + + public void refresh(int borderTopColor, + int borderRightColor, + int borderBottomColor, + int borderLeftColor, + + float borderTopWidth, + float borderRightWidth, + float borderBottomWidth, + float borderLeftWidth, + + float borderTopLeftRadius, + float borderTopRightRadius, + float borderBottomRightRadius, + float borderBottomLeftRadius, + + String clipPath, + + int backgroundColor, + String backgroundImageUri, + Bitmap backgroundBitmap, + LinearGradientDefinition backgroundGradient, + Context context, + String backgroundRepeat, + String backgroundPosition, + CSSValue[] backgroundPositionParsedCSSValues, + String backgroundSize, + CSSValue[] backgroundSizeParsedCSSValues) { + + this.borderTopColor = borderTopColor; + this.borderRightColor = borderRightColor; + this.borderBottomColor = borderBottomColor; + this.borderLeftColor = borderLeftColor; + + this.borderTopWidth = borderTopWidth; + this.borderRightWidth = borderRightWidth; + this.borderBottomWidth = borderBottomWidth; + this.borderLeftWidth = borderLeftWidth; + + this.borderTopLeftRadius = borderTopLeftRadius; + this.borderTopRightRadius = borderTopRightRadius; + this.borderBottomRightRadius = borderBottomRightRadius; + this.borderBottomLeftRadius = borderBottomLeftRadius; + + this.clipPath = clipPath; + + this.backgroundColor = backgroundColor; + this.backgroundImage = backgroundImageUri; + this.backgroundBitmap = backgroundBitmap; + this.backgroundGradient = backgroundGradient; + this.backgroundRepeat = backgroundRepeat; + this.backgroundPosition = backgroundPosition; + this.backgroundPositionParsedCSSValues = backgroundPositionParsedCSSValues; + this.backgroundSize = backgroundSize; + this.backgroundSizeParsedCSSValues = backgroundSizeParsedCSSValues; + + this.invalidateSelf(); + if (backgroundImageUri != null) { + Fetcher fetcher = Fetcher.getInstance(context); + // TODO: Implement option to pass load-mode like in ImageView class. + boolean loadAsync = backgroundImageUri.startsWith("http"); + fetcher.loadImage(backgroundImageUri, this, 0, 0, false, true, loadAsync, null); + } + } + + @Override + public void draw(Canvas canvas) { + Rect bounds = this.getBounds(); + float width = (float)bounds.width(); + float height = (float)bounds.height(); + + if (width <= 0 || height <= 0) { + // When the view is off-screen the bounds might be empty and we don't have anything to draw. + return; + } + + RectF backgroundBoundsF = new RectF(bounds.left, bounds.top, bounds.right, bounds.bottom); + + float topBackoffAntialias = calculateBackoffAntialias(this.borderTopColor, this.borderTopWidth); + float rightBackoffAntialias = calculateBackoffAntialias(this.borderRightColor, this.borderRightWidth); + float bottomBackoffAntialias = calculateBackoffAntialias(this.borderBottomColor, this.borderBottomWidth); + float leftBackoffAntialias = calculateBackoffAntialias(this.borderLeftColor, this.borderLeftWidth); + + float[] backgroundRadii = { + Math.max(0, borderTopLeftRadius + leftBackoffAntialias), Math.max(0, borderTopLeftRadius + topBackoffAntialias), + Math.max(0, borderTopRightRadius + rightBackoffAntialias), Math.max(0, borderTopRightRadius + topBackoffAntialias), + Math.max(0, borderBottomRightRadius + rightBackoffAntialias), Math.max(0, borderBottomRightRadius + bottomBackoffAntialias), + Math.max(0, borderBottomLeftRadius + leftBackoffAntialias), Math.max(0, borderBottomLeftRadius + bottomBackoffAntialias) + }; + + Path backgroundPath = new Path(); + RectF backgroundRect = new RectF( + leftBackoffAntialias, + topBackoffAntialias, + width - rightBackoffAntialias, + height - bottomBackoffAntialias + ); + backgroundPath.addRoundRect(backgroundRect, backgroundRadii, Path.Direction.CW); + + // draw background + if (this.backgroundColor != 0) { + Paint backgroundColorPaint = new Paint(); + backgroundColorPaint.setStyle(Paint.Style.FILL); + backgroundColorPaint.setColor(this.backgroundColor); + backgroundColorPaint.setAntiAlias(true); + + if (this.clipPath != null && !this.clipPath.isEmpty()) { + drawClipPath(this.clipPath, canvas, backgroundColorPaint, backgroundBoundsF, this.density); + } else { + canvas.drawPath(backgroundPath, backgroundColorPaint); + } + } + + if (this.backgroundBitmap != null) { + BackgroundDrawParams params = this.getDrawParams(width, height); + Matrix transform = new Matrix(); + if (params.sizeX > 0 && params.sizeY > 0) { + float scaleX = params.sizeX / this.backgroundBitmap.getWidth(); + float scaleY = params.sizeY / this.backgroundBitmap.getHeight(); + transform.setScale(scaleX, scaleY, 0, 0); + } else { + params.sizeX = this.backgroundBitmap.getWidth(); + params.sizeY = this.backgroundBitmap.getHeight(); + } + transform.postTranslate(params.posX, params.posY); + + Paint backgroundImagePaint = new Paint(); + BitmapShader shader = new BitmapShader( + this.backgroundBitmap, + params.repeatX ? Shader.TileMode.REPEAT : Shader.TileMode.CLAMP, + params.repeatY ? Shader.TileMode.REPEAT : Shader.TileMode.CLAMP + ); + shader.setLocalMatrix(transform); + backgroundImagePaint.setAntiAlias(true); + backgroundImagePaint.setFilterBitmap(true); + backgroundImagePaint.setShader(shader); + + float imageWidth = params.repeatX ? width : params.sizeX; + float imageHeight = params.repeatY ? height : params.sizeY; + params.posX = params.repeatX ? 0 : params.posX; + params.posY = params.repeatY ? 0 : params.posY; + + if (this.clipPath != null && !this.clipPath.isEmpty()) { + drawClipPath(this.clipPath, canvas, backgroundImagePaint, backgroundBoundsF, this.density); + } else { + boolean supportsPathOp = android.os.Build.VERSION.SDK_INT >= 19; + if (supportsPathOp) { + Path backgroundNoRepeatPath = new Path(); + backgroundNoRepeatPath.addRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight, Path.Direction.CCW); + intersect(backgroundNoRepeatPath, backgroundPath); + canvas.drawPath(backgroundNoRepeatPath, backgroundImagePaint); + } else { + // Clipping here will not be anti-aliased but at least it won't shine through the rounded corners. + canvas.save(); + canvas.clipRect(params.posX, params.posY, params.posX + imageWidth, params.posY + imageHeight); + canvas.drawPath(backgroundPath, backgroundImagePaint); + canvas.restore(); + } + } + } + + if (this.backgroundGradient != null) { + LinearGradientDefinition def = this.backgroundGradient; + Paint backgroundGradientPaint = new Paint(); + LinearGradient shader = new LinearGradient( + def.getStartX() * width, def.getStartY() * height, + def.getEndX() * width, def.getEndY() * height, + def.getColors(), def.getStops(), Shader.TileMode.MIRROR); + backgroundGradientPaint.setAntiAlias(true); + backgroundGradientPaint.setFilterBitmap(true); + backgroundGradientPaint.setShader(shader); + + if (this.clipPath != null && !this.clipPath.isEmpty()) { + drawClipPath(this.clipPath, canvas, backgroundGradientPaint, backgroundBoundsF, this.density); + } else { + canvas.drawPath(backgroundPath, backgroundGradientPaint); + } + } + + // draw border + if (this.clipPath != null && !this.clipPath.isEmpty()) { + float borderWidth = this.getUniformBorderWidth(); + if (borderWidth > 0) { + Paint borderPaint = new Paint(); + borderPaint.setColor(this.getUniformBorderColor()); + borderPaint.setStyle(Paint.Style.STROKE); + borderPaint.setStrokeWidth(borderWidth); + drawClipPath(this.clipPath, canvas, borderPaint, backgroundBoundsF, this.density); + } + } else if (!this.hasBorderWidth()) { + // No borders trap. + } else if (this.hasUniformBorderColor()) { + // iOS and browsers use black when no color is specified. + if (borderLeftWidth > 0 || borderTopWidth > 0 || borderRightWidth > 0 || borderBottomWidth > 0) { + Paint borderPaint = new Paint(); + borderPaint.setColor(this.getUniformBorderColor()); + borderPaint.setStyle(Paint.Style.FILL); + borderPaint.setAntiAlias(true); + Path borderPath = new Path(); + + RectF borderOuterRect = new RectF(0, 0, width, height); + float[] borderOuterRadii = { + borderTopLeftRadius, borderTopLeftRadius, + borderTopRightRadius, borderTopRightRadius, + borderBottomRightRadius, borderBottomRightRadius, + borderBottomLeftRadius, borderBottomLeftRadius + }; + borderPath.addRoundRect(borderOuterRect, borderOuterRadii, Path.Direction.CW); + + RectF borderInnerRect = new RectF( + borderLeftWidth, + borderTopWidth, + width - borderRightWidth, + height - borderBottomWidth + ); + float[] borderInnerRadii = { + Math.max(0, borderTopLeftRadius - borderLeftWidth), Math.max(0, borderTopLeftRadius - borderTopWidth), + Math.max(0, borderTopRightRadius - borderRightWidth), Math.max(0, borderTopRightRadius - borderTopWidth), + Math.max(0, borderBottomRightRadius - borderRightWidth), Math.max(0, borderBottomRightRadius - borderBottomWidth), + Math.max(0, borderBottomLeftRadius - borderLeftWidth), Math.max(0, borderBottomLeftRadius - borderBottomWidth) + }; + borderPath.addRoundRect(borderInnerRect, borderInnerRadii, Path.Direction.CCW); + + canvas.drawPath(borderPath, borderPaint); + } + } else { + float top = this.borderTopWidth; + float right = this.borderRightWidth; + float bottom = this.borderBottomWidth; + float left = this.borderLeftWidth; + + //lto rto + // +---------------------+ + // |lti rti| + // | | + // | | + // | | + // | | + // |lbi rbi| + // +---------------------+ + //lbo rbo + + PointF lto = new PointF(0, 0); // left-top-outside + PointF lti = new PointF(left, top); // left-top-inside + + PointF rto = new PointF(bounds.right, 0); // right-top-outside + PointF rti = new PointF(bounds.right - right, top); // right-top-outside + + PointF rbo = new PointF(bounds.right, bounds.bottom); // right-bottom-outside + PointF rbi = new PointF(bounds.right - right, bounds.bottom - bottom); // right-bottom-inside + + PointF lbo = new PointF(0, bounds.bottom); // left-bottom-outside + PointF lbi = new PointF(left, bounds.bottom - bottom); // left-bottom-inside + + if (this.borderTopWidth > 0) { + Paint topBorderPaint = new Paint(); + topBorderPaint.setColor(this.borderTopColor); + topBorderPaint.setAntiAlias(true); + Path topBorderPath = new Path(); + topBorderPath.setFillType(Path.FillType.EVEN_ODD); + topBorderPath.moveTo(lto.x, lto.y); + topBorderPath.lineTo(rto.x, rto.y); + topBorderPath.lineTo(rti.x, rti.y); + topBorderPath.lineTo(lti.x, lti.y); + topBorderPath.close(); + canvas.drawPath(topBorderPath, topBorderPaint); + } + + if (this.borderRightWidth > 0) { + Paint rightBorderPaint = new Paint(); + rightBorderPaint.setColor(this.borderRightColor); + rightBorderPaint.setAntiAlias(true); + Path rightBorderPath = new Path(); + rightBorderPath.setFillType(Path.FillType.EVEN_ODD); + rightBorderPath.moveTo(rto.x, rto.y); + rightBorderPath.lineTo(rbo.x, rbo.y); + rightBorderPath.lineTo(rbi.x, rbi.y); + rightBorderPath.lineTo(rti.x, rti.y); + rightBorderPath.close(); + canvas.drawPath(rightBorderPath, rightBorderPaint); + } + + if (this.borderBottomWidth > 0) { + Paint bottomBorderPaint = new Paint(); + bottomBorderPaint.setColor(this.borderBottomColor); + bottomBorderPaint.setAntiAlias(true); + Path bottomBorderPath = new Path(); + bottomBorderPath.setFillType(Path.FillType.EVEN_ODD); + bottomBorderPath.moveTo(rbo.x, rbo.y); + bottomBorderPath.lineTo(lbo.x, lbo.y); + bottomBorderPath.lineTo(lbi.x, lbi.y); + bottomBorderPath.lineTo(rbi.x, rbi.y); + bottomBorderPath.close(); + canvas.drawPath(bottomBorderPath, bottomBorderPaint); + } + + if (this.borderLeftWidth > 0) { + Paint leftBorderPaint = new Paint(); + leftBorderPaint.setColor(this.borderLeftColor); + leftBorderPaint.setAntiAlias(true); + Path leftBorderPath = new Path(); + leftBorderPath.setFillType(Path.FillType.EVEN_ODD); + leftBorderPath.moveTo(lbo.x, lbo.y); + leftBorderPath.lineTo(lto.x, lto.y); + leftBorderPath.lineTo(lti.x, lti.y); + leftBorderPath.lineTo(lbi.x, lbi.y); + leftBorderPath.close(); + canvas.drawPath(leftBorderPath, leftBorderPaint); + } + } + } + + private static float calculateBackoffAntialias(int borderColor, float borderWidth) { + // We will inset background colors and images so antialiasing will not color pixels outside the border. + // If the border is transparent we will backoff less, and we will not backoff more than half a pixel or half the border width. + float halfBorderWidth = borderWidth / 2.0f; + float normalizedBorderAlpha = ((float) Color.alpha(borderColor)) / 255.0f; + return Math.min(1f, halfBorderWidth) * normalizedBorderAlpha; + } + + @TargetApi(19) + private static void intersect(Path path1, Path path2) { + path1.op(path2, Path.Op.INTERSECT); + } + + private static Pattern spaceAndComma = Pattern.compile("[\\s,]+"); + private static Pattern space = Pattern.compile("\\s+"); + + private static void drawClipPath(String clipPath, Canvas canvas, Paint paint, RectF bounds, float density) { + // Sample string is polygon(20% 0%, 0% 20%, 30% 50%, 0% 80%, 20% 100%, 50% 70%, 80% 100%, 100% 80%, 70% 50%, 100% 20%, 80% 0%, 50% 30%); + String functionName = clipPath.substring(0, clipPath.indexOf("(")); + String value = clipPath.substring(clipPath.indexOf("(") + 1, clipPath.indexOf(")")); + + String[] arr; + float top; + float right; + float bottom; + float left; + switch (functionName) { + case "rect": + arr = spaceAndComma.split(value); + + top = cssValueToDevicePixels(arr[0], bounds.bottom, density); + right = cssValueToDevicePixels(arr[1], bounds.right, density); + bottom = cssValueToDevicePixels(arr[2], bounds.bottom, density); + left = cssValueToDevicePixels(arr[3], bounds.right, density); + + canvas.drawRect(left, top, right, bottom, paint); + break; + case "inset": + arr = spaceAndComma.split(value); + String topString = "0"; + String rightString = "0"; + String bottomString = "0"; + String leftString = "0"; + if (arr.length == 1) { + topString = rightString = bottomString = leftString = arr[0]; + } else if (arr.length == 2) { + topString = bottomString = arr[0]; + rightString = leftString = arr[1]; + } else if (arr.length == 3) { + topString = arr[0]; + rightString = leftString = arr[1]; + bottomString = arr[2]; + } else if (arr.length == 4) { + topString = arr[0]; + rightString = arr[1]; + bottomString = arr[2]; + leftString = arr[3]; + } + + top = cssValueToDevicePixels(topString, bounds.bottom, density); + right = cssValueToDevicePixels("100%", bounds.right, density) - cssValueToDevicePixels(rightString, bounds.right, density); + bottom = cssValueToDevicePixels("100%", bounds.bottom, density) - cssValueToDevicePixels(bottomString, bounds.bottom, density); + left = cssValueToDevicePixels(leftString, bounds.right, density); + + canvas.drawRect(left, top, right, bottom, paint); + break; + case "circle": + arr = space.split(value); + float radius = cssValueToDevicePixels(arr[0], (bounds.width() > bounds.height() ? bounds.height() : bounds.width()) / 2, density); + float y = cssValueToDevicePixels(arr[2], bounds.height(), density); + float x = cssValueToDevicePixels(arr[3], bounds.width(), density); + canvas.drawCircle(x, y, radius, paint); + break; + case "ellipse": + arr = space.split(value); + float rX = cssValueToDevicePixels(arr[0], bounds.right, density); + float rY = cssValueToDevicePixels(arr[1], bounds.bottom, density); + float cX = cssValueToDevicePixels(arr[3], bounds.right, density); + float cY = cssValueToDevicePixels(arr[4], bounds.bottom, density); + left = cX - rX; + top = cY - rY; + right = (rX * 2) + left; + bottom = (rY * 2) + top; + canvas.drawOval(new RectF(left, top, right, bottom), paint); + break; + case "polygon": + Path path = new Path(); + PointF firstPoint = null; + arr = value.split(","); + for (String s : arr) { + String[] xy = space.split(s.trim()); + PointF point = new PointF(cssValueToDevicePixels(xy[0], bounds.width(), density), cssValueToDevicePixels(xy[1], bounds.height(), density)); + + if (firstPoint == null) { + firstPoint = point; + path.moveTo(point.x, point.y); + } + + path.lineTo(point.x, point.y); + } + if (firstPoint != null) { + path.lineTo(firstPoint.x, firstPoint.y); + } + canvas.drawPath(path, paint); + break; + } + } + + private BackgroundDrawParams getDrawParams(float width, float height) { + BackgroundDrawParams res = new BackgroundDrawParams(); + + // repeat + if (this.backgroundRepeat != null && !this.backgroundRepeat.isEmpty()) { + switch (this.backgroundRepeat.toLowerCase(Locale.ENGLISH)) { + case "no-repeat": + res.repeatX = false; + res.repeatY = false; + break; + + case "repeat-x": + res.repeatY = false; + break; + + case "repeat-y": + res.repeatX = false; + break; + } + } + + float imageWidth = this.backgroundBitmap.getWidth(); + float imageHeight = this.backgroundBitmap.getHeight(); + + // size + if (this.backgroundSize != null && !this.backgroundSize.isEmpty()) { + if (this.backgroundSizeParsedCSSValues.length == 2) { + CSSValue vx = this.backgroundSizeParsedCSSValues[0]; + CSSValue vy = this.backgroundSizeParsedCSSValues[1]; + if ("%".equals(vx.getUnit()) && "%".equals(vy.getUnit())) { + imageWidth = width * vx.getValue() / 100; + imageHeight = height * vy.getValue() / 100; + + res.sizeX = imageWidth; + res.sizeY = imageHeight; + } else if ("number".equals(vx.getType()) && "number".equals(vy.getType()) && + (("px".equals(vx.getUnit()) && "px".equals(vy.getUnit())) || ((vx.getUnit() == null || vx.getUnit().isEmpty()) && (vy.getUnit() == null || vy.getUnit().isEmpty())))) { + imageWidth = vx.getValue(); + imageHeight = vy.getValue(); + + res.sizeX = imageWidth; + res.sizeY = imageHeight; + } + } else if (this.backgroundSizeParsedCSSValues.length == 1 && "ident".equals(this.backgroundSizeParsedCSSValues[0].getType())) { + float scale = 0; + + if ("cover".equals(this.backgroundSizeParsedCSSValues[0].getString())) { + scale = Math.max(width / imageWidth, height / imageHeight); + } else if ("contain".equals(this.backgroundSizeParsedCSSValues[0].getString())) { + scale = Math.min(width / imageWidth, height / imageHeight); + } + + if (scale > 0) { + imageWidth *= scale; + imageHeight *= scale; + + res.sizeX = imageWidth; + res.sizeY = imageHeight; + } + } + } + + // position + if (this.backgroundPosition != null && !this.backgroundPosition.isEmpty()) { + CSSValue[] xy = parsePosition(this.backgroundPositionParsedCSSValues); + if (xy != null) { + CSSValue vx = xy[0]; + CSSValue vy = xy[1]; + float spaceX = width - imageWidth; + float spaceY = height - imageHeight; + + if ("%".equals(vx.getUnit()) && "%".equals(vy.getUnit())) { + res.posX = spaceX * vx.getValue() / 100; + res.posY = spaceY * vy.getValue() / 100; + } else if ("number".equals(vx.getType()) && "number".equals(vy.getType()) && + (("px".equals(vx.getUnit()) && "px".equals(vy.getUnit())) || ((vx.getUnit() == null || vx.getUnit().isEmpty()) && (vy.getUnit() == null || vy.getUnit().isEmpty())))) { + res.posX = vx.getValue(); + res.posY = vy.getValue(); + } else if ("ident".equals(vx.getType()) && "ident".equals(vy.getType())) { + if ("center".equals(vx.getString().toLowerCase(Locale.ENGLISH))) { + res.posX = spaceX / 2; + } else if ("right".equals(vx.getString().toLowerCase(Locale.ENGLISH))) { + res.posX = spaceX; + } + + if ("center".equals(vy.getString().toLowerCase(Locale.ENGLISH))) { + res.posY = spaceY / 2; + } else if ("bottom".equals(vy.getString().toLowerCase(Locale.ENGLISH))) { + res.posY = spaceY; + } + } + } + } + + return res; + } + + private static CSSValue[] parsePosition(CSSValue[] values) { + if (values.length == 2) { + return values; + } + + CSSValue[] result = null; + if (values.length == 1 && "ident".equals(values[0].getType())) { + String val = values[0].getString().toLowerCase(Locale.ENGLISH); + CSSValue center = new CSSValue("ident", "center", null, 0); + + // If you only one keyword is specified, the other value is "center" + if ("left".equals(val) || "right".equals(val)) { + result = new CSSValue[]{values[0], center}; + } else if ("top".equals(val) || "bottom".equals(val)) { + result = new CSSValue[]{center, values[0]}; + } else if ("center".equals(val)) { + result = new CSSValue[]{center, center}; + } + } + + return result; + } + + private static float cssValueToDevicePixels(String source, float total, float density) { + source = source.trim(); + if (source.contains("%")) { + return Float.parseFloat(source.replace("%", "")) * total / 100; + } else if (source.contains("px")) { + return Float.parseFloat(source.replace("px", "")) * density; + } else { + return Float.parseFloat(source) * density; + } + } + + public String toDebugString() { + return getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + "; " + + + "id: " + this.id + "; " + + + "borderTopColor: " + this.borderTopColor + "; " + + "borderRightColor: " + this.borderRightColor + "; " + + "borderBottomColor: " + this.borderBottomColor + "; " + + "borderLeftColor: " + this.borderLeftColor + "; " + + + "borderTopWidth: " + this.borderTopWidth + "; " + + "borderRightWidth: " + this.borderRightWidth + "; " + + "borderBottomWidth: " + this.borderBottomWidth + "; " + + "borderLeftWidth: " + this.borderLeftWidth + "; " + + + "borderTopLeftRadius: " + this.borderTopLeftRadius + "; " + + "borderTopRightRadius: " + this.borderTopRightRadius + "; " + + "borderBottomRightRadius: " + this.borderBottomRightRadius + "; " + + "borderBottomLeftRadius: " + this.borderBottomLeftRadius + "; " + + + "clipPath: " + this.clipPath + "; " + + "backgroundColor: " + this.backgroundColor + "; " + + "backgroundImage: " + this.backgroundImage + "; " + + "backgroundBitmap: " + this.backgroundBitmap + "; " + + "backgroundRepeat: " + this.backgroundRepeat + "; " + + "backgroundPosition: " + this.backgroundPosition + "; " + + "backgroundSize: " + this.backgroundSize + "; " + ; + } + + @Override + public void setBitmap(Bitmap value) { + backgroundBitmap = value; + invalidateSelf(); + drawable = null; + } + + @Override + public void setDrawable(Drawable asyncDrawable) { + drawable = asyncDrawable; + } + + @Override + public Drawable getDrawable() { + return drawable; + } + + private class BackgroundDrawParams { + private boolean repeatX = true; + private boolean repeatY = true; + private float posX; + private float posY; + private float sizeX; + private float sizeY; + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CSSValue.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CSSValue.java new file mode 100644 index 000000000..71152a967 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CSSValue.java @@ -0,0 +1,34 @@ +package org.nativescript.widgets; + +/** + * Created by hristov on 6/16/2016. + */ +public class CSSValue { + private String type; + private String str; + private String unit; + private float value; + + public String getType() { + return type; + } + + public String getString() { + return str; + } + + public String getUnit() { + return unit; + } + + public float getValue() { + return value; + } + + public CSSValue(String type, String str, String unit, float value) { + this.type = type; + this.str = str; + this.unit = unit; + this.value = value; + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CommonLayoutParams.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CommonLayoutParams.java new file mode 100644 index 000000000..4ada4c16f --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CommonLayoutParams.java @@ -0,0 +1,462 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.widget.FrameLayout; + +/** + * @author hhristov + */ +public class CommonLayoutParams extends FrameLayout.LayoutParams { + + static final String TAG = "NSLayout"; + static int debuggable = -1; + private static final int NOT_SET = Integer.MIN_VALUE; + private static final StringBuilder sb = new StringBuilder(); + + public CommonLayoutParams() { + super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.FILL); + } + + public CommonLayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public CommonLayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + + public CommonLayoutParams(FrameLayout.LayoutParams source) { + super((ViewGroup.MarginLayoutParams) source); + this.gravity = source.gravity; + } + + public CommonLayoutParams(CommonLayoutParams source) { + this((FrameLayout.LayoutParams) source); + + this.widthPercent = source.widthPercent; + this.heightPercent = source.heightPercent; + + this.topMargin = source.topMargin; + this.leftMargin = source.leftMargin; + this.bottomMargin = source.bottomMargin; + this.rightMargin = source.rightMargin; + + this.left = source.left; + this.top = source.top; + this.row = source.row; + this.column = source.column; + this.rowSpan = source.rowSpan; + this.columnSpan = source.columnSpan; + this.dock = source.dock; + } + + public float widthPercent = 0; + public float heightPercent = 0; + + public float topMarginPercent = 0; + public float leftMarginPercent = 0; + public float bottomMarginPercent = 0; + public float rightMarginPercent = 0; + + public int widthOriginal = NOT_SET; + public int heightOriginal = NOT_SET; + + public int topMarginOriginal = NOT_SET; + public int leftMarginOriginal = NOT_SET; + public int bottomMarginOriginal = NOT_SET; + public int rightMarginOriginal = NOT_SET; + + public int left = 0; + public int top = 0; + public int row = 0; + public int column = 0; + public int rowSpan = 1; + public int columnSpan = 1; + public Dock dock = Dock.left; + + protected static int getDesiredWidth(View view) { + CommonLayoutParams lp = (CommonLayoutParams) view.getLayoutParams(); + return view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + } + + protected static int getDesiredHeight(View view) { + CommonLayoutParams lp = (CommonLayoutParams) view.getLayoutParams(); + return view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + } + + // We use our own layout method because the one in FrameLayout is broken when margins are set and gravity is CENTER_VERTICAL or CENTER_HORIZONTAL. + @SuppressLint("RtlHardcoded") + protected static void layoutChild(View child, int left, int top, int right, int bottom) { + if (child.getVisibility() == View.GONE) { + return; + } + + int childTop = 0; + int childLeft = 0; + + int childWidth = child.getMeasuredWidth(); + int childHeight = child.getMeasuredHeight(); + + CommonLayoutParams lp = (CommonLayoutParams) child.getLayoutParams(); + int gravity = lp.gravity; + if (gravity == -1) { + gravity = Gravity.FILL; + } + + int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + // If we have explicit height and gravity is FILL we need to be centered otherwise our explicit height won't be taken into account. + if ((lp.height >= 0 || lp.heightPercent > 0) && verticalGravity == Gravity.FILL_VERTICAL) { + verticalGravity = Gravity.CENTER_VERTICAL; + } + + switch (verticalGravity) { + case Gravity.TOP: + childTop = top + lp.topMargin; + break; + + case Gravity.CENTER_VERTICAL: + childTop = top + (bottom - top - childHeight + lp.topMargin - lp.bottomMargin) / 2; + break; + + case Gravity.BOTTOM: + childTop = bottom - childHeight - lp.bottomMargin; + break; + + case Gravity.FILL_VERTICAL: + default: + childTop = top + lp.topMargin; + childHeight = bottom - top - (lp.topMargin + lp.bottomMargin); + break; + } + + int horizontalGravity = Gravity.getAbsoluteGravity(gravity, child.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; + + // If we have explicit width and gravity is FILL we need to be centered otherwise our explicit width won't be taken into account. + if ((lp.width >= 0 || lp.widthPercent > 0) && horizontalGravity == Gravity.FILL_HORIZONTAL) { + horizontalGravity = Gravity.CENTER_HORIZONTAL; + } + + switch (horizontalGravity) { + case Gravity.LEFT: + childLeft = left + lp.leftMargin; + break; + + case Gravity.CENTER_HORIZONTAL: + childLeft = left + (right - left - childWidth + lp.leftMargin - lp.rightMargin) / 2; + break; + + case Gravity.RIGHT: + childLeft = right - childWidth - lp.rightMargin; + break; + + case Gravity.FILL_HORIZONTAL: + default: + childLeft = left + lp.leftMargin; + childWidth = right - left - (lp.leftMargin + lp.rightMargin); + break; + } + + int childRight = Math.round(childLeft + childWidth); + int childBottom = Math.round(childTop + childHeight); + childLeft = Math.round(childLeft); + childTop = Math.round(childTop); + + // Re-measure TextView because it is not centered if layout width is larger than measure width. + if (child instanceof android.widget.TextView) { + + boolean canChangeWidth = lp.width < 0; + boolean canChangeHeight = lp.height < 0; + + int measuredWidth = child.getMeasuredWidth(); + int measuredHeight = child.getMeasuredHeight(); + + int width = childRight - childLeft; + int height = childBottom - childTop; + if ((Math.abs(measuredWidth - width) > 1 && canChangeWidth) || (Math.abs(measuredHeight - height) > 1 && canChangeHeight)) { + int widthMeasureSpec = MeasureSpec.makeMeasureSpec(canChangeWidth ? width : lp.width, MeasureSpec.EXACTLY); + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(canChangeHeight ? height : lp.height, MeasureSpec.EXACTLY); + if (debuggable > 0) { + sb.setLength(0); + sb.append("remeasure "); + sb.append(child); + sb.append(" with "); + sb.append(MeasureSpec.toString(widthMeasureSpec)); + sb.append(", "); + sb.append(MeasureSpec.toString(heightMeasureSpec)); + log(TAG, sb.toString()); + } + + child.measure(widthMeasureSpec, heightMeasureSpec); + } + } + + if (debuggable > 0) { + sb.setLength(0); + sb.append(child.getParent().toString()); + sb.append(" :layoutChild: "); + sb.append(child.toString()); + sb.append(" "); + sb.append(childLeft); + sb.append(", "); + sb.append(childTop); + sb.append(", "); + sb.append(childRight); + sb.append(", "); + sb.append(childBottom); + log(TAG, sb.toString()); + } + + child.layout(childLeft, childTop, childRight, childBottom); + } + + protected static void measureChild(View child, int widthMeasureSpec, int heightMeasureSpec) { + if (child.getVisibility() == View.GONE) { + return; + } + + // Negative means not initialized. + if (debuggable < 0) { + try { + Context context = child.getContext(); + ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), android.content.pm.PackageManager.GET_META_DATA); + android.os.Bundle bundle = ai.metaData; + Boolean debugLayouts = bundle != null ? bundle.getBoolean("debugLayouts", false) : false; + debuggable = debugLayouts ? 1 : 0; + } catch (NameNotFoundException e) { + debuggable = 0; + Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage()); + } catch (NullPointerException e) { + debuggable = 0; + Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage()); + } + } + + int childWidthMeasureSpec = getMeasureSpec(child, widthMeasureSpec, true); + int childHeightMeasureSpec = getMeasureSpec(child, heightMeasureSpec, false); + + if (debuggable > 0) { + sb.setLength(0); + sb.append(child.getParent().toString()); + sb.append(" :measureChild: "); + sb.append(child.toString()); + sb.append(" "); + sb.append(MeasureSpec.toString(childWidthMeasureSpec)); + sb.append(", "); + sb.append(MeasureSpec.toString(childHeightMeasureSpec)); + log(TAG, sb.toString()); + } + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + /** + * Iterates over children and changes their width and height to one calculated from percentage + * values. + * + * @param viewGroup The parent ViewGroup. + * @param widthMeasureSpec Width MeasureSpec of the parent ViewGroup. + * @param heightMeasureSpec Height MeasureSpec of the parent ViewGroup. + */ + protected static void adjustChildrenLayoutParams(ViewGroup viewGroup, int widthMeasureSpec, int heightMeasureSpec) { + + int availableWidth = MeasureSpec.getSize(widthMeasureSpec); + int widthSpec = MeasureSpec.getMode(widthMeasureSpec); + + int availableHeight = MeasureSpec.getSize(heightMeasureSpec); + int heightSpec = MeasureSpec.getMode(heightMeasureSpec); + + for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { + View child = viewGroup.getChildAt(i); + LayoutParams params = child.getLayoutParams(); + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) child.getLayoutParams(); + if (widthSpec != MeasureSpec.UNSPECIFIED) { + if (lp.widthPercent > 0) { + // If we get measured twice we will override the original value with the one calculated from percentValue from the first measure. + // So we set originalValue only the first time. + if (lp.widthOriginal == NOT_SET) { + lp.widthOriginal = lp.width; + } + lp.width = (int) (availableWidth * lp.widthPercent); + } + else { + lp.widthOriginal = NOT_SET; + } + + if (lp.leftMarginPercent > 0) { + if (lp.leftMarginOriginal == NOT_SET) { + lp.leftMarginOriginal = lp.leftMargin; + } + lp.leftMargin = (int) (availableWidth * lp.leftMarginPercent); + } + else { + lp.leftMarginOriginal = NOT_SET; + } + + if (lp.rightMarginPercent > 0) { + if (lp.rightMarginOriginal == NOT_SET) { + lp.rightMarginOriginal = lp.rightMargin; + } + lp.rightMargin = (int) (availableWidth * lp.rightMarginPercent); + } + else { + lp.rightMarginOriginal = NOT_SET; + } + } + + if (heightSpec != MeasureSpec.UNSPECIFIED) { + if (lp.heightPercent > 0) { + if (lp.heightOriginal == NOT_SET) { + lp.heightOriginal = lp.height; + } + lp.height = (int) (availableHeight * lp.heightPercent); + } + else { + lp.heightOriginal = NOT_SET; + } + + if (lp.topMarginPercent > 0) { + if (lp.topMarginOriginal == NOT_SET) { + lp.topMarginOriginal = lp.topMargin; + } + lp.topMargin = (int) (availableHeight * lp.topMarginPercent); + } + else { + lp.topMarginOriginal = NOT_SET; + } + + if (lp.bottomMarginPercent > 0) { + if (lp.bottomMarginOriginal == NOT_SET) { + lp.bottomMarginOriginal = lp.bottomMargin; + } + lp.bottomMargin = (int) (availableHeight * lp.bottomMarginPercent); + } + else { + lp.bottomMarginOriginal = NOT_SET; + } + } + } + } + } + + /** + * Iterates over children and restores their original dimensions that were changed for + * percentage values. + */ + protected static void restoreOriginalParams(ViewGroup viewGroup) { + for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { + View view = viewGroup.getChildAt(i); + LayoutParams params = view.getLayoutParams(); + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + if (lp.widthPercent > 0) { + lp.width = lp.widthOriginal; + } + if (lp.heightPercent > 0) { + lp.height = lp.heightOriginal; + } + if (lp.leftMarginPercent > 0) { + lp.leftMargin = lp.leftMarginOriginal; + } + if (lp.topMarginPercent > 0) { + lp.topMargin = lp.topMarginOriginal; + } + if (lp.rightMarginPercent > 0) { + lp.rightMargin = lp.rightMarginOriginal; + } + if (lp.bottomMarginPercent > 0) { + lp.bottomMargin = lp.bottomMarginOriginal; + } + + lp.widthOriginal = NOT_SET; + lp.heightOriginal = NOT_SET; + lp.leftMarginOriginal = NOT_SET; + lp.topMarginOriginal = NOT_SET; + lp.rightMarginOriginal = NOT_SET; + lp.bottomMarginOriginal = NOT_SET; + } + } + } + + static void log(String tag, String message) { + Log.v(tag, message); + } + + static StringBuilder getStringBuilder() { + sb.setLength(0); + return sb; + } + + private static int getMeasureSpec(View view, int parentMeasureSpec, boolean horizontal) { + + int parentLength = MeasureSpec.getSize(parentMeasureSpec); + int parentSpecMode = MeasureSpec.getMode(parentMeasureSpec); + + CommonLayoutParams lp = (CommonLayoutParams) view.getLayoutParams(); + final int margins = horizontal ? lp.leftMargin + lp.rightMargin : lp.topMargin + lp.bottomMargin; + + int resultSize = 0; + int resultMode = MeasureSpec.UNSPECIFIED; + + int measureLength = Math.max(0, parentLength - margins); + int childLength = horizontal ? lp.width : lp.height; + + // We want a specific size... let be it. + if (childLength >= 0) { + if (parentSpecMode != MeasureSpec.UNSPECIFIED) { + resultSize = Math.min(parentLength, childLength); + } else { + resultSize = childLength; + } + + resultMode = MeasureSpec.EXACTLY; + } else { + switch (parentSpecMode) { + // Parent has imposed an exact size on us + case MeasureSpec.EXACTLY: + resultSize = measureLength; + int gravity = LayoutBase.getGravity(view); + boolean stretched; + if (horizontal) { + final int horizontalGravity = Gravity.getAbsoluteGravity(gravity, view.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; + stretched = horizontalGravity == Gravity.FILL_HORIZONTAL; + } else { + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + stretched = verticalGravity == Gravity.FILL_VERTICAL; + } + + // if stretched - view wants to be our size. So be it. + // else - view wants to determine its own size. It can't be bigger than us. + resultMode = stretched ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST; + break; + + // Parent has imposed a maximum size on us + case MeasureSpec.AT_MOST: + resultSize = measureLength; + resultMode = MeasureSpec.AT_MOST; + break; + + case MeasureSpec.UNSPECIFIED: + resultSize = 0; + resultMode = MeasureSpec.UNSPECIFIED; + break; + } + } + + return MeasureSpec.makeMeasureSpec(resultSize, resultMode); + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ContentLayout.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ContentLayout.java new file mode 100644 index 000000000..30bf965a2 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ContentLayout.java @@ -0,0 +1,78 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class ContentLayout extends LayoutBase { + + public ContentLayout(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int measureWidth = 0; + int measureHeight = 0; + + for (int i = 0, count = this.getChildCount(); i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.measureChild(child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + measureWidth = Math.max(measureWidth, childMeasuredWidth); + measureHeight = Math.max(measureHeight, childMeasuredHeight); + } + + // Add in our padding + measureWidth += this.getPaddingLeft() + this.getPaddingRight(); + measureHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childLeft = paddingLeft; + int childTop = paddingTop; + + int childRight = right - left - (paddingLeft + paddingRight); + int childBottom = bottom - top - (paddingRight + paddingBottom); + + for (int i = 0, count = this.getChildCount(); i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childRight, childBottom); + } + + CommonLayoutParams.restoreOriginalParams(this); + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTypefaceSpan.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTypefaceSpan.java new file mode 100644 index 000000000..a2ec9c1e1 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/CustomTypefaceSpan.java @@ -0,0 +1,46 @@ +package org.nativescript.widgets; + +import android.annotation.SuppressLint; +import android.graphics.Paint; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.text.style.TypefaceSpan; + +/** + * Created by hhristov on 2/27/17. + */ + +@SuppressLint("ParcelCreator") +public class CustomTypefaceSpan extends TypefaceSpan { + private Typeface typeface; + + public CustomTypefaceSpan(String family, Typeface typeface) { + super(family); + this.typeface = typeface; + } + + public void updateDrawState(TextPaint ds) { + this.applyCustomTypeFace(ds); + } + + public void updateMeasureState(TextPaint paint) { + this.applyCustomTypeFace(paint); + } + + private void applyCustomTypeFace(TextPaint paint) { + final Typeface old = paint.getTypeface(); + final int oldStyle = (old == null) ? 0 : old.getStyle(); + + Typeface typeface = this.typeface; + int fake = oldStyle & ~typeface.getStyle(); + if ((fake & android.graphics.Typeface.BOLD) != 0) { + paint.setFakeBoldText(true); + } + + if ((fake & android.graphics.Typeface.ITALIC) != 0) { + paint.setTextSkewX(-0.25f); + } + + paint.setTypeface(typeface); + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Dock.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Dock.java new file mode 100644 index 000000000..e22fdf140 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Dock.java @@ -0,0 +1,15 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public enum Dock { + left, + top, + right, + bottom +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/DockLayout.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/DockLayout.java new file mode 100644 index 000000000..979c4f702 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/DockLayout.java @@ -0,0 +1,178 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class DockLayout extends LayoutBase { + + private boolean _stretchLastChild = true; + + public DockLayout(Context context) { + super(context); + } + + public boolean getStretchLastChild() { + return this._stretchLastChild; + } + public void setStretchLastChild(boolean value) { + this._stretchLastChild = value; + this.requestLayout(); + } + + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int measureWidth = 0; + int measureHeight = 0; + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + int remainingWidth = widthMode == MeasureSpec.UNSPECIFIED ? 0 : width - horizontalPadding; + int remainingHeight = heightMode == MeasureSpec.UNSPECIFIED ? 0 : height - verticalPadding; + + int tempHeight = 0; + int tempWidth = 0; + int childWidthMeasureSpec = 0; + int childHeightMeasureSpec = 0; + int count = this.getChildCount(); + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + if (this._stretchLastChild && (i == (count - 1))) { + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(remainingWidth, widthMode); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(remainingHeight, heightMode); + } + else { + // Measure children with AT_MOST even if our mode is EXACT + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(remainingWidth, widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(remainingHeight, heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode); + } + + CommonLayoutParams.measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + Dock dock = childLayoutParams.dock; + switch (dock) { + case top: + case bottom: + remainingHeight = Math.max(0, remainingHeight - childMeasuredHeight); + tempHeight += childMeasuredHeight; + measureWidth = Math.max(measureWidth, tempWidth + childMeasuredWidth); + measureHeight = Math.max(measureHeight, tempHeight); + break; + + case left: + case right: + default: + remainingWidth = Math.max(0, remainingWidth - childMeasuredWidth); + tempWidth += childMeasuredWidth; + measureWidth = Math.max(measureWidth, tempWidth); + measureHeight = Math.max(measureHeight, tempHeight + childMeasuredHeight); + break; + } + } + + // Add in our padding + measureWidth += horizontalPadding; + measureHeight += verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childLeft = this.getPaddingLeft(); + int childTop = this.getPaddingTop(); + + int x = childLeft; + int y = childTop; + + int remainingWidth = Math.max(0, right - left - (this.getPaddingLeft() + this.getPaddingRight())); + int remainingHeight = Math.max(0, bottom - top - (this.getPaddingTop() + this.getPaddingBottom())); + + int count = this.getChildCount(); + View childToStretch = null; + if (count > 0 && this._stretchLastChild) { + count--; + childToStretch = this.getChildAt(count); + } + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams childLayoutParams = (CommonLayoutParams)child.getLayoutParams(); + int childWidth = CommonLayoutParams.getDesiredWidth(child); + int childHeight = CommonLayoutParams.getDesiredHeight(child); + + switch (childLayoutParams.dock) { + case top: + childLeft = x; + childTop = y; + childWidth = remainingWidth; + y += childHeight; + remainingHeight = Math.max(0, remainingHeight - childHeight); + break; + + case bottom: + childLeft = x; + childTop = y + remainingHeight - childHeight; + childWidth = remainingWidth; + remainingHeight = Math.max(0, remainingHeight - childHeight); + break; + + case right: + childLeft = x + remainingWidth - childWidth; + childTop = y; + childHeight = remainingHeight; + remainingWidth = Math.max(0, remainingWidth - childWidth); + break; + + case left: + default: + childLeft = x; + childTop = y; + childHeight = remainingHeight; + x += childWidth; + remainingWidth = Math.max(0, remainingWidth - childWidth); + break; + } + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childLeft + childWidth, childTop + childHeight); + } + + if (childToStretch != null) { + CommonLayoutParams.layoutChild(childToStretch, x, y, x + remainingWidth, y + remainingHeight); + } + + CommonLayoutParams.restoreOriginalParams(this); + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FlexLine.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FlexLine.java new file mode 100644 index 000000000..2864a569f --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FlexLine.java @@ -0,0 +1,149 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nativescript.widgets; + +import java.util.ArrayList; +import java.util.List; + +/** + * Holds properties related to a single flex line. This class is not expected to be changed outside + * of the {@link FlexboxLayout}, thus only exposing the getter methods that may be useful for + * other classes using the {@link FlexboxLayout}. + */ +public class FlexLine { + + FlexLine() { + } + + /** @see {@link #getLeft()} */ + int mLeft = Integer.MAX_VALUE; + + /** @see {@link #getTop()} */ + int mTop = Integer.MAX_VALUE; + + /** @see {@link #getRight()} */ + int mRight = Integer.MIN_VALUE; + + /** @see {@link #getBottom()} */ + int mBottom = Integer.MIN_VALUE; + + /** @see {@link #getMainSize()} */ + int mMainSize; + + /** + * The sum of the lengths of dividers along the main axis. This value should be lower or + * than than the value of {@link #mMainSize}. + */ + int mDividerLengthInMainSize; + + /** @see {@link #getCrossSize()} */ + int mCrossSize; + + /** @see {@link #getItemCount()} */ + int mItemCount; + + /** @see {@link #getTotalFlexGrow()} */ + float mTotalFlexGrow; + + /** @see {@link #getTotalFlexShrink()} */ + float mTotalFlexShrink; + + /** + * The largest value of the individual child's baseline (obtained by View#getBaseline() + * if the {@link FlexboxLayout#mAlignItems} value is not {@link FlexboxLayout#ALIGN_ITEMS_BASELINE} + * or the flex direction is vertical, this value is not used. + * If the alignment direction is from the bottom to top, + * (e.g. flexWrap == FLEX_WRAP_WRAP_REVERSE and flexDirection == FLEX_DIRECTION_ROW) + * store this value from the distance from the bottom of the view minus baseline. + * (Calculated as view.getMeasuredHeight() - view.getBaseline - LayoutParams.bottomMargin) + */ + int mMaxBaseline; + + /** + * Store the indices of the children views whose alignSelf property is stretch. + * The stored indices are the absolute indices including all children in the Flexbox, + * not the relative indices in this flex line. + */ + List mIndicesAlignSelfStretch = new ArrayList<>(); + + /** + * @return the distance in pixels from the top edge of this view's parent + * to the top edge of this FlexLine. + */ + public int getLeft() { + return mLeft; + } + + /** + * @return the distance in pixels from the top edge of this view's parent + * to the top edge of this FlexLine. + */ + public int getTop() { + return mTop; + } + + /** + * @return the distance in pixels from the right edge of this view's parent + * to the right edge of this FlexLine. + */ + public int getRight() { + return mRight; + } + + /** + * @return the distance in pixels from the bottom edge of this view's parent + * to the bottom edge of this FlexLine. + */ + public int getBottom() { + return mBottom; + } + + /** + * @return the size of the flex line in pixels along the main axis of the flex container. + */ + public int getMainSize() { + return mMainSize; + } + + /** + * @return the size of the flex line in pixels along the cross axis of the flex container. + */ + public int getCrossSize() { + return mCrossSize; + } + + /** + * @return the count of the views contained in this flex line. + */ + public int getItemCount() { + return mItemCount; + } + + /** + * @return the sum of the flexGrow properties of the children included in this flex line + */ + public float getTotalFlexGrow() { + return mTotalFlexGrow; + } + + /** + * @return the sum of the flexShrink properties of the children included in this flex line + */ + public float getTotalFlexShrink() { + return mTotalFlexShrink; + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FlexboxLayout.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FlexboxLayout.java new file mode 100644 index 000000000..5316086c3 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FlexboxLayout.java @@ -0,0 +1,2718 @@ +/* + * Copyright 2016 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nativescript.widgets; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.util.SparseIntArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * A layout that arranges its children in a way its attributes can be specified like the + * CSS Flexible Box Layout Module. + * This class extends the {@link ViewGroup} like other layout classes such as {@link LinearLayout} + * or {@link RelativeLayout}, the attributes can be specified from a layout XML or from code. + * + * The supported attributes that you can use are: + *
    + *
  • {@code flexDirection}
  • + *
  • {@code flexWrap}
  • + *
  • {@code justifyContent}
  • + *
  • {@code alignItems}
  • + *
  • {@code alignContent}
  • + *
  • {@code showDivider}
  • + *
  • {@code showDividerHorizontal}
  • + *
  • {@code showDividerVertical}
  • + *
  • {@code dividerDrawable}
  • + *
  • {@code dividerDrawableHorizontal}
  • + *
  • {@code dividerDrawableVertical}
  • + *
+ * for the FlexboxLayout. + * + * And for the children of the FlexboxLayout, you can use: + *
    + *
  • {@code layout_order}
  • + *
  • {@code layout_flexGrow}
  • + *
  • {@code layout_flexShrink}
  • + *
  • {@code layout_flexBasisPercent}
  • + *
  • {@code layout_alignSelf}
  • + *
  • {@code layout_minWidth}
  • + *
  • {@code layout_minHeight}
  • + *
  • {@code layout_maxWidth}
  • + *
  • {@code layout_maxHeight}
  • + *
  • {@code layout_wrapBefore}
  • + *
+ */ +public class FlexboxLayout extends ViewGroup { + + @IntDef({FLEX_DIRECTION_ROW, FLEX_DIRECTION_ROW_REVERSE, FLEX_DIRECTION_COLUMN, + FLEX_DIRECTION_COLUMN_REVERSE}) + @Retention(RetentionPolicy.SOURCE) + public @interface FlexDirection { + + } + + public static final int FLEX_DIRECTION_ROW = 0; + + public static final int FLEX_DIRECTION_ROW_REVERSE = 1; + + public static final int FLEX_DIRECTION_COLUMN = 2; + + public static final int FLEX_DIRECTION_COLUMN_REVERSE = 3; + + /** + * The direction children items are placed inside the Flexbox layout, it determines the + * direction of the main axis (and the cross axis, perpendicular to the main axis). + *
    + *
  • + * {@link #FLEX_DIRECTION_ROW}: Main axis direction -> horizontal. Main start to + * main end -> Left to right (in LTR languages). + * Cross start to cross end -> Top to bottom + *
  • + *
  • + * {@link #FLEX_DIRECTION_ROW_REVERSE}: Main axis direction -> horizontal. Main start + * to main end -> Right to left (in LTR languages). Cross start to cross end -> + * Top to bottom. + *
  • + *
  • + * {@link #FLEX_DIRECTION_COLUMN}: Main axis direction -> vertical. Main start + * to main end -> Top to bottom. Cross start to cross end -> + * Left to right (In LTR languages). + *
  • + *
  • + * {@link #FLEX_DIRECTION_COLUMN_REVERSE}: Main axis direction -> vertical. Main start + * to main end -> Bottom to top. Cross start to cross end -> Left to right + * (In LTR languages) + *
  • + *
+ * The default value is {@link #FLEX_DIRECTION_ROW}. + */ + private int mFlexDirection = FLEX_DIRECTION_ROW; + + + @IntDef({FLEX_WRAP_NOWRAP, FLEX_WRAP_WRAP, FLEX_WRAP_WRAP_REVERSE}) + @Retention(RetentionPolicy.SOURCE) + public @interface FlexWrap { + + } + + public static final int FLEX_WRAP_NOWRAP = 0; + + public static final int FLEX_WRAP_WRAP = 1; + + public static final int FLEX_WRAP_WRAP_REVERSE = 2; + + /** + * This attribute controls whether the flex container is single-line or multi-line, and the + * direction of the cross axis. + *
    + *
  • {@link #FLEX_WRAP_NOWRAP}: The flex container is single-line.
  • + *
  • {@link #FLEX_WRAP_WRAP}: The flex container is multi-line.
  • + *
  • {@link #FLEX_WRAP_WRAP_REVERSE}: The flex container is multi-line. The direction of the + * cross axis is opposed to the direction as the {@link #FLEX_WRAP_WRAP}
  • + *
+ * The default value is {@link #FLEX_WRAP_NOWRAP}. + */ + private int mFlexWrap = FLEX_WRAP_NOWRAP; + + + @IntDef({JUSTIFY_CONTENT_FLEX_START, JUSTIFY_CONTENT_FLEX_END, JUSTIFY_CONTENT_CENTER, + JUSTIFY_CONTENT_SPACE_BETWEEN, JUSTIFY_CONTENT_SPACE_AROUND}) + @Retention(RetentionPolicy.SOURCE) + public @interface JustifyContent { + + } + + public static final int JUSTIFY_CONTENT_FLEX_START = 0; + + public static final int JUSTIFY_CONTENT_FLEX_END = 1; + + public static final int JUSTIFY_CONTENT_CENTER = 2; + + public static final int JUSTIFY_CONTENT_SPACE_BETWEEN = 3; + + public static final int JUSTIFY_CONTENT_SPACE_AROUND = 4; + + /** + * This attribute controls the alignment along the main axis. + * The default value is {@link #JUSTIFY_CONTENT_FLEX_START}. + */ + private int mJustifyContent = JUSTIFY_CONTENT_FLEX_START; + + + @IntDef({ALIGN_ITEMS_FLEX_START, ALIGN_ITEMS_FLEX_END, ALIGN_ITEMS_CENTER, + ALIGN_ITEMS_BASELINE, ALIGN_ITEMS_STRETCH}) + @Retention(RetentionPolicy.SOURCE) + public @interface AlignItems { + + } + + public static final int ALIGN_ITEMS_FLEX_START = 0; + + public static final int ALIGN_ITEMS_FLEX_END = 1; + + public static final int ALIGN_ITEMS_CENTER = 2; + + public static final int ALIGN_ITEMS_BASELINE = 3; + + public static final int ALIGN_ITEMS_STRETCH = 4; + + /** + * This attribute controls the alignment along the cross axis. + * The default value is {@link #ALIGN_ITEMS_STRETCH}. + */ + private int mAlignItems = ALIGN_ITEMS_STRETCH; + + + @IntDef({ALIGN_CONTENT_FLEX_START, ALIGN_CONTENT_FLEX_END, ALIGN_CONTENT_CENTER, + ALIGN_CONTENT_SPACE_BETWEEN, ALIGN_CONTENT_SPACE_AROUND, ALIGN_CONTENT_STRETCH}) + @Retention(RetentionPolicy.SOURCE) + public @interface AlignContent { + + } + + public static final int ALIGN_CONTENT_FLEX_START = 0; + + public static final int ALIGN_CONTENT_FLEX_END = 1; + + public static final int ALIGN_CONTENT_CENTER = 2; + + public static final int ALIGN_CONTENT_SPACE_BETWEEN = 3; + + public static final int ALIGN_CONTENT_SPACE_AROUND = 4; + + public static final int ALIGN_CONTENT_STRETCH = 5; + + /** + * This attribute controls the alignment of the flex lines in the flex container. + * The default value is {@link #ALIGN_CONTENT_STRETCH}. + */ + private int mAlignContent = ALIGN_CONTENT_STRETCH; + + /** + * The int definition to be used as the arguments for the {@link #setShowDivider(int)}, + * {@link #setShowDividerHorizontal(int)} or {@link #setShowDividerVertical(int)}. + * One or more of the values (such as + * {@link #SHOW_DIVIDER_BEGINNING} | {@link #SHOW_DIVIDER_MIDDLE}) can be passed to those set + * methods. + */ + @IntDef(flag = true, + value = { + SHOW_DIVIDER_NONE, + SHOW_DIVIDER_BEGINNING, + SHOW_DIVIDER_MIDDLE, + SHOW_DIVIDER_END + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DividerMode { + + } + + /** Constant to how no dividers */ + public static final int SHOW_DIVIDER_NONE = 0; + + /** Constant to show a divider at the beginning of the flex lines (or flex items). */ + public static final int SHOW_DIVIDER_BEGINNING = 1; + + /** Constant to show dividers between flex lines or flex items. */ + public static final int SHOW_DIVIDER_MIDDLE = 1 << 1; + + /** Constant to show a divider at the end of the flex lines or flex items. */ + public static final int SHOW_DIVIDER_END = 1 << 2; + + /** The drawable to be drawn for the horizontal dividers. */ + private Drawable mDividerDrawableHorizontal; + + /** The drawable to be drawn for the vertical dividers. */ + private Drawable mDividerDrawableVertical; + + /** + * Indicates the divider mode for the {@link #mDividerDrawableHorizontal}. The value needs to + * be the combination of the value of {@link #SHOW_DIVIDER_NONE}, + * {@link #SHOW_DIVIDER_BEGINNING}, {@link #SHOW_DIVIDER_MIDDLE} and {@link #SHOW_DIVIDER_END} + */ + private int mShowDividerHorizontal; + + /** + * Indicates the divider mode for the {@link #mDividerDrawableVertical}. The value needs to + * be the combination of the value of {@link #SHOW_DIVIDER_NONE}, + * {@link #SHOW_DIVIDER_BEGINNING}, {@link #SHOW_DIVIDER_MIDDLE} and {@link #SHOW_DIVIDER_END} + */ + private int mShowDividerVertical; + + /** The height of the {@link #mDividerDrawableHorizontal}. */ + private int mDividerHorizontalHeight; + + /** The width of the {@link #mDividerDrawableVertical}. */ + private int mDividerVerticalWidth; + + /** + * Holds reordered indices, which {@link LayoutParams#order} parameters are taken into account + */ + private int[] mReorderedIndices; + + /** + * Caches the {@link LayoutParams#order} attributes for children views. + * Key: the index of the view ({@link #mReorderedIndices} isn't taken into account) + * Value: the value for the order attribute + */ + private SparseIntArray mOrderCache; + + private List mFlexLines = new ArrayList<>(); + + /** + * Holds the 'frozen' state of children during measure. If a view is frozen it will no longer + * expand or shrink regardless of flexGrow/flexShrink. Items are indexed by the child's + * reordered index. + */ + private boolean[] mChildrenFrozen; + + public FlexboxLayout(Context context) { + this(context, null); + } + + public FlexboxLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FlexboxLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + // NOTE: We do not support android xml. + // TypedArray a = context.obtainStyledAttributes( + // attrs, R.styleable.FlexboxLayout, defStyleAttr, 0); + // mFlexDirection = a.getInt(R.styleable.FlexboxLayout_flexDirection, FLEX_DIRECTION_ROW); + // mFlexWrap = a.getInt(R.styleable.FlexboxLayout_flexWrap, FLEX_WRAP_NOWRAP); + // mJustifyContent = a + // .getInt(R.styleable.FlexboxLayout_justifyContent, JUSTIFY_CONTENT_FLEX_START); + // mAlignItems = a.getInt(R.styleable.FlexboxLayout_alignItems, ALIGN_ITEMS_STRETCH); + // mAlignContent = a.getInt(R.styleable.FlexboxLayout_alignContent, ALIGN_CONTENT_STRETCH); + // Drawable drawable = a.getDrawable(R.styleable.FlexboxLayout_dividerDrawable); + // if (drawable != null) { + // setDividerDrawableHorizontal(drawable); + // setDividerDrawableVertical(drawable); + // } + // Drawable drawableHorizontal = a + // .getDrawable(R.styleable.FlexboxLayout_dividerDrawableHorizontal); + // if (drawableHorizontal != null) { + // setDividerDrawableHorizontal(drawableHorizontal); + // } + // Drawable drawableVertical = a + // .getDrawable(R.styleable.FlexboxLayout_dividerDrawableVertical); + // if (drawableVertical != null) { + // setDividerDrawableVertical(drawableVertical); + // } + // int dividerMode = a.getInt(R.styleable.FlexboxLayout_showDivider, SHOW_DIVIDER_NONE); + // if (dividerMode != SHOW_DIVIDER_NONE) { + // mShowDividerVertical = dividerMode; + // mShowDividerHorizontal = dividerMode; + // } + // int dividerModeVertical = a + // .getInt(R.styleable.FlexboxLayout_showDividerVertical, SHOW_DIVIDER_NONE); + // if (dividerModeVertical != SHOW_DIVIDER_NONE) { + // mShowDividerVertical = dividerModeVertical; + // } + // int dividerModeHorizontal = a + // .getInt(R.styleable.FlexboxLayout_showDividerHorizontal, SHOW_DIVIDER_NONE); + // if (dividerModeHorizontal != SHOW_DIVIDER_NONE) { + // mShowDividerHorizontal = dividerModeHorizontal; + // } + // a.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (isOrderChangedFromLastMeasurement()) { + mReorderedIndices = createReorderedIndices(); + } + if (mChildrenFrozen == null || mChildrenFrozen.length < getChildCount()) { + mChildrenFrozen = new boolean[getChildCount()]; + } + + // TODO: Only calculate the children views which are affected from the last measure. + + switch (mFlexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + measureHorizontal(widthMeasureSpec, heightMeasureSpec); + break; + case FLEX_DIRECTION_COLUMN: // Intentional fall through + case FLEX_DIRECTION_COLUMN_REVERSE: + measureVertical(widthMeasureSpec, heightMeasureSpec); + break; + default: + throw new IllegalStateException( + "Invalid value for the flex direction is set: " + mFlexDirection); + } + + Arrays.fill(mChildrenFrozen, false); + } + + /** + * Returns a View, which is reordered by taking {@link LayoutParams#order} parameters + * into account. + * + * @param index the index of the view + * @return the reordered view, which {@link LayoutParams@order} is taken into account. + * If the index is negative or out of bounds of the number of contained views, + * returns {@code null}. + */ + public View getReorderedChildAt(int index) { + if (index < 0 || index >= mReorderedIndices.length) { + return null; + } + return getChildAt(mReorderedIndices[index]); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + // Create an array for the reordered indices before the View is added in the parent + // ViewGroup since otherwise reordered indices won't be in effect before the + // FlexboxLayout's onMeasure is called. + // Because requestLayout is requested in the super.addView method. + mReorderedIndices = createReorderedIndices(child, index, params); + super.addView(child, index, params); + } + + /** + * Create an array, which indicates the reordered indices that {@link LayoutParams#order} + * attributes are taken into account. This method takes a View before that is added as the + * parent ViewGroup's children. + * + * @param viewBeforeAdded the View instance before added to the array of children + * Views of the parent ViewGroup + * @param indexForViewBeforeAdded the index for the View before added to the array of the + * parent ViewGroup + * @param paramsForViewBeforeAdded the layout parameters for the View before added to the array + * of the parent ViewGroup + * @return an array which have the reordered indices + */ + private int[] createReorderedIndices(View viewBeforeAdded, int indexForViewBeforeAdded, + ViewGroup.LayoutParams paramsForViewBeforeAdded) { + int childCount = getChildCount(); + List orders = createOrders(childCount); + Order orderForViewToBeAdded = new Order(); + if (viewBeforeAdded != null + && paramsForViewBeforeAdded instanceof FlexboxLayout.LayoutParams) { + orderForViewToBeAdded.order = ((LayoutParams) paramsForViewBeforeAdded).order; + } else { + orderForViewToBeAdded.order = LayoutParams.ORDER_DEFAULT; + } + + if (indexForViewBeforeAdded == -1 || indexForViewBeforeAdded == childCount) { + orderForViewToBeAdded.index = childCount; + } else if (indexForViewBeforeAdded < getChildCount()) { + orderForViewToBeAdded.index = indexForViewBeforeAdded; + for (int i = indexForViewBeforeAdded; i < childCount; i++) { + orders.get(i).index++; + } + } else { + // This path is not expected since OutOfBoundException will be thrown in the ViewGroup + // But setting the index for fail-safe + orderForViewToBeAdded.index = childCount; + } + orders.add(orderForViewToBeAdded); + + return sortOrdersIntoReorderedIndices(childCount + 1, orders); + } + + /** + * Create an array, which indicates the reordered indices that {@link LayoutParams#order} + * attributes are taken into account. + * + * @return @return an array which have the reordered indices + */ + private int[] createReorderedIndices() { + int childCount = getChildCount(); + List orders = createOrders(childCount); + return sortOrdersIntoReorderedIndices(childCount, orders); + } + + private int[] sortOrdersIntoReorderedIndices(int childCount, List orders) { + Collections.sort(orders); + if (mOrderCache == null) { + mOrderCache = new SparseIntArray(childCount); + } + mOrderCache.clear(); + int[] reorderedIndices = new int[childCount]; + int i = 0; + for (Order order : orders) { + reorderedIndices[i] = order.index; + mOrderCache.append(i, order.order); + i++; + } + return reorderedIndices; + } + + @NonNull + private List createOrders(int childCount) { + List orders = new ArrayList<>(); + for (int i = 0; i < childCount; i++) { + View child = getChildAt(i); + LayoutParams params = (LayoutParams) child.getLayoutParams(); + Order order = new Order(); + order.order = params.order; + order.index = i; + orders.add(order); + } + return orders; + } + + /** + * Returns if any of the children's {@link LayoutParams#order} attributes are changed + * from the last measurement. + * + * @return {@code true} if changed from the last measurement, {@code false} otherwise. + */ + private boolean isOrderChangedFromLastMeasurement() { + int childCount = getChildCount(); + if (mOrderCache == null) { + mOrderCache = new SparseIntArray(childCount); + } + if (mOrderCache.size() != childCount) { + return true; + } + for (int i = 0; i < childCount; i++) { + View view = getChildAt(i); + if (view == null) { + continue; + } + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.order != mOrderCache.get(i)) { + return true; + } + } + return false; + } + + /** + * Invalidates the cache of the orders so that they are recalculated. + */ + public void invalidateOrdersCache() { + if (this.mOrderCache != null) { + this.mOrderCache.clear(); + } + } + + /** + * Sub method for {@link #onMeasure(int, int)}, when the main axis direction is horizontal + * (either left to right or right to left). + * + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @see #onMeasure(int, int) + * @see #setFlexDirection(int) + * @see #setFlexWrap(int) + * @see #setAlignItems(int) + * @see #setAlignContent(int) + */ + private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int childState = 0; + + mFlexLines.clear(); + + // Determine how many flex lines are needed in this layout by measuring each child. + // (Expand or shrink the view depending on the flexGrow and flexShrink attributes in a later + // loop) + { + int childCount = getChildCount(); + int paddingStart = ViewCompat.getPaddingStart(this); + int paddingEnd = ViewCompat.getPaddingEnd(this); + int largestHeightInRow = Integer.MIN_VALUE; + FlexLine flexLine = new FlexLine(); + + // The index of the view in a same flex line. + int indexInFlexLine = 0; + flexLine.mMainSize = paddingStart + paddingEnd; + for (int i = 0; i < childCount; i++) { + View child = getReorderedChildAt(i); + if (child == null) { + addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } else if (child.getVisibility() == View.GONE) { + flexLine.mItemCount++; + addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } + + FlexboxLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.alignSelf == LayoutParams.ALIGN_SELF_STRETCH) { + flexLine.mIndicesAlignSelfStretch.add(i); + } + + int childWidth = lp.width; + if (lp.flexBasisPercent != LayoutParams.FLEX_BASIS_PERCENT_DEFAULT + && widthMode == MeasureSpec.EXACTLY) { + childWidth = Math.round(widthSize * lp.flexBasisPercent); + // Use the dimension from the layout_width attribute if the widthMode is not + // MeasureSpec.EXACTLY even if any fraction value is set to + // layout_flexBasisPercent. + // There are likely quite few use cases where assigning any fraction values + // with widthMode is not MeasureSpec.EXACTLY (e.g. FlexboxLayout's layout_width + // is set to wrap_content) + } + int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight() + lp.leftMargin + + lp.rightMargin, childWidth < 0 ? LayoutParams.WRAP_CONTENT : childWidth); + int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingTop() + getPaddingBottom() + lp.topMargin + + lp.bottomMargin, lp.height < 0 ? LayoutParams.WRAP_CONTENT : lp.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + // Check the size constraint after the first measurement for the child + // To prevent the child's width/height violate the size constraints imposed by the + // {@link LayoutParams#minWidth}, {@link LayoutParams#minHeight}, + // {@link LayoutParams#maxWidth} and {@link LayoutParams#maxHeight} attributes. + // E.g. When the child's layout_width is wrap_content the measured width may be + // less than the min width after the first measurement. + checkSizeConstraints(child); + + childState = ViewCompat + .combineMeasuredStates(childState, ViewCompat.getMeasuredState(child)); + largestHeightInRow = Math.max(largestHeightInRow, + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); + + if (isWrapRequired(widthMode, widthSize, flexLine.mMainSize, + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin, lp, + i, indexInFlexLine)) { + if (flexLine.mItemCount > 0) { + addFlexLine(flexLine); + } + + flexLine = new FlexLine(); + flexLine.mItemCount = 1; + flexLine.mMainSize = paddingStart + paddingEnd; + largestHeightInRow = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + indexInFlexLine = 0; + } else { + flexLine.mItemCount++; + indexInFlexLine++; + } + flexLine.mMainSize += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + flexLine.mTotalFlexGrow += lp.flexGrow; + flexLine.mTotalFlexShrink += lp.flexShrink; + // Temporarily set the cross axis length as the largest child in the row + // Expand along the cross axis depending on the mAlignContent property if needed + // later + flexLine.mCrossSize = Math.max(flexLine.mCrossSize, largestHeightInRow); + + // Check if the beginning or middle divider is required for the flex item + if (hasDividerBeforeChildAtAlongMainAxis(i, indexInFlexLine)) { + flexLine.mMainSize += mDividerVerticalWidth; + flexLine.mDividerLengthInMainSize += mDividerVerticalWidth; + } + + if (mFlexWrap != FLEX_WRAP_WRAP_REVERSE) { + flexLine.mMaxBaseline = Math + .max(flexLine.mMaxBaseline, child.getBaseline() + lp.topMargin); + } else { + // if the flex wrap property is FLEX_WRAP_WRAP_REVERSE, calculate the + // baseline as the distance from the cross end and the baseline + // since the cross size calculation is based on the distance from the cross end + flexLine.mMaxBaseline = Math + .max(flexLine.mMaxBaseline, + child.getMeasuredHeight() - child.getBaseline() + + lp.bottomMargin); + } + addFlexLineIfLastFlexItem(i, childCount, flexLine); + } + } + + determineMainSize(mFlexDirection, widthMeasureSpec, heightMeasureSpec); + + // TODO: Consider the case any individual child's alignSelf is set to ALIGN_SELF_BASELINE + if (mAlignItems == ALIGN_ITEMS_BASELINE) { + int viewIndex = 0; + for (FlexLine flexLine : mFlexLines) { + // The largest height value that also take the baseline shift into account + int largestHeightInLine = Integer.MIN_VALUE; + for (int i = viewIndex; i < viewIndex + flexLine.mItemCount; i++) { + View child = getReorderedChildAt(i); + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (mFlexWrap != FLEX_WRAP_WRAP_REVERSE) { + int marginTop = flexLine.mMaxBaseline - child.getBaseline(); + marginTop = Math.max(marginTop, lp.topMargin); + largestHeightInLine = Math.max(largestHeightInLine, + child.getHeight() + marginTop + lp.bottomMargin); + } else { + int marginBottom = flexLine.mMaxBaseline - child.getMeasuredHeight() + + child.getBaseline(); + marginBottom = Math.max(marginBottom, lp.bottomMargin); + largestHeightInLine = Math.max(largestHeightInLine, + child.getHeight() + lp.topMargin + marginBottom); + } + } + flexLine.mCrossSize = largestHeightInLine; + viewIndex += flexLine.mItemCount; + } + } + + determineCrossSize(mFlexDirection, widthMeasureSpec, heightMeasureSpec, + getPaddingTop() + getPaddingBottom()); + // Now cross size for each flex line is determined. + // Expand the views if alignItems (or alignSelf in each child view) is set to stretch + stretchViews(mFlexDirection, mAlignItems); + setMeasuredDimensionForFlex(mFlexDirection, widthMeasureSpec, heightMeasureSpec, + childState); + } + + /** + * Sub method for {@link #onMeasure(int, int)} when the main axis direction is vertical + * (either from top to bottom or bottom to top). + * + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @see #onMeasure(int, int) + * @see #setFlexDirection(int) + * @see #setFlexWrap(int) + * @see #setAlignItems(int) + * @see #setAlignContent(int) + */ + private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int childState = 0; + + mFlexLines.clear(); + + // Determine how many flex lines are needed in this layout by measuring each child. + // (Expand or shrink the view depending on the flexGrow and flexShrink attributes in a later + // loop) + int childCount = getChildCount(); + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + int largestWidthInColumn = Integer.MIN_VALUE; + FlexLine flexLine = new FlexLine(); + flexLine.mMainSize = paddingTop + paddingBottom; + // The index of the view in a same flex line. + int indexInFlexLine = 0; + for (int i = 0; i < childCount; i++) { + View child = getReorderedChildAt(i); + if (child == null) { + addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } else if (child.getVisibility() == View.GONE) { + flexLine.mItemCount++; + addFlexLineIfLastFlexItem(i, childCount, flexLine); + continue; + } + + FlexboxLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.alignSelf == LayoutParams.ALIGN_SELF_STRETCH) { + flexLine.mIndicesAlignSelfStretch.add(i); + } + + int childHeight = lp.height; + if (lp.flexBasisPercent != LayoutParams.FLEX_BASIS_PERCENT_DEFAULT + && heightMode == MeasureSpec.EXACTLY) { + childHeight = Math.round(heightSize * lp.flexBasisPercent); + // Use the dimension from the layout_height attribute if the heightMode is not + // MeasureSpec.EXACTLY even if any fraction value is set to layout_flexBasisPercent. + // There are likely quite few use cases where assigning any fraction values + // with heightMode is not MeasureSpec.EXACTLY (e.g. FlexboxLayout's layout_height + // is set to wrap_content) + } + + int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight() + lp.leftMargin + + lp.rightMargin, lp.width < 0 ? LayoutParams.WRAP_CONTENT : lp.width); + int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingTop() + getPaddingBottom() + lp.topMargin + + lp.bottomMargin, childHeight < 0 ? LayoutParams.WRAP_CONTENT : childHeight); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + // Check the size constraint after the first measurement for the child + // To prevent the child's width/height violate the size constraints imposed by the + // {@link LayoutParams#minWidth}, {@link LayoutParams#minHeight}, + // {@link LayoutParams#maxWidth} and {@link LayoutParams#maxHeight} attributes. + // E.g. When the child's layout_height is wrap_content the measured height may be + // less than the min height after the first measurement. + checkSizeConstraints(child); + + childState = ViewCompat + .combineMeasuredStates(childState, ViewCompat.getMeasuredState(child)); + largestWidthInColumn = Math.max(largestWidthInColumn, + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); + + if (isWrapRequired(heightMode, heightSize, flexLine.mMainSize, + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin, lp, + i, indexInFlexLine)) { + if (flexLine.mItemCount > 0) { + addFlexLine(flexLine); + } + + flexLine = new FlexLine(); + flexLine.mItemCount = 1; + flexLine.mMainSize = paddingTop + paddingBottom; + largestWidthInColumn = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + indexInFlexLine = 0; + } else { + flexLine.mItemCount++; + indexInFlexLine++; + } + flexLine.mMainSize += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + flexLine.mTotalFlexGrow += lp.flexGrow; + flexLine.mTotalFlexShrink += lp.flexShrink; + // Temporarily set the cross axis length as the largest child width in the column + // Expand along the cross axis depending on the mAlignContent property if needed + // later + flexLine.mCrossSize = Math.max(flexLine.mCrossSize, largestWidthInColumn); + + if (hasDividerBeforeChildAtAlongMainAxis(i, indexInFlexLine)) { + flexLine.mMainSize += mDividerHorizontalHeight; + } + addFlexLineIfLastFlexItem(i, childCount, flexLine); + } + + determineMainSize(mFlexDirection, widthMeasureSpec, heightMeasureSpec); + determineCrossSize(mFlexDirection, widthMeasureSpec, heightMeasureSpec, + getPaddingLeft() + getPaddingRight()); + // Now cross size for each flex line is determined. + // Expand the views if alignItems (or alignSelf in each child view) is set to stretch + stretchViews(mFlexDirection, mAlignItems); + setMeasuredDimensionForFlex(mFlexDirection, widthMeasureSpec, heightMeasureSpec, + childState); + } + + /** + * Checks if the view's width/height don't violate the minimum/maximum size constraints imposed + * by the {@link LayoutParams#minWidth}, {@link LayoutParams#minHeight}, + * {@link LayoutParams#maxWidth} and {@link LayoutParams#maxHeight} attributes. + * + * @param view the view to be checked + */ + private void checkSizeConstraints(View view) { + boolean needsMeasure = false; + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + int childWidth = view.getMeasuredWidth(); + int childHeight = view.getMeasuredHeight(); + + if (view.getMeasuredWidth() < lp.minWidth) { + needsMeasure = true; + childWidth = lp.minWidth; + } else if (view.getMeasuredWidth() > lp.maxWidth) { + needsMeasure = true; + childWidth = lp.maxWidth; + } + + if (childHeight < lp.minHeight) { + needsMeasure = true; + childHeight = lp.minHeight; + } else if (childHeight > lp.maxHeight) { + needsMeasure = true; + childHeight = lp.maxHeight; + } + if (needsMeasure) { + view.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY)); + } + } + + private void addFlexLineIfLastFlexItem(int childIndex, int childCount, FlexLine flexLine) { + if (childIndex == childCount - 1 && flexLine.mItemCount != 0) { + // Add the flex line if this item is the last item + addFlexLine(flexLine); + } + } + + private void addFlexLine(FlexLine flexLine) { + // The size of the end divider isn't added until the flexLine is added to the flex container + // take the divider width (or height) into account when adding the flex line. + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + if ((mShowDividerVertical & SHOW_DIVIDER_END) > 0) { + flexLine.mMainSize += mDividerVerticalWidth; + flexLine.mDividerLengthInMainSize += mDividerVerticalWidth; + } + } else { + if ((mShowDividerHorizontal & SHOW_DIVIDER_END) > 0) { + flexLine.mMainSize += mDividerHorizontalHeight; + flexLine.mDividerLengthInMainSize += mDividerHorizontalHeight; + } + } + mFlexLines.add(flexLine); + } + + /** + * Determine the main size by expanding (shrinking if negative remaining free space is given) + * an individual child in each flex line if any children's flexGrow (or flexShrink if remaining + * space is negative) properties are set to non-zero. + * + * @param flexDirection the value of the flex direction + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @see #setFlexDirection(int) + * @see #getFlexDirection() + */ + private void determineMainSize(@FlexDirection int flexDirection, int widthMeasureSpec, + int heightMeasureSpec) { + int mainSize; + int paddingAlongMainAxis; + switch (flexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + if (widthMode == MeasureSpec.EXACTLY) { + mainSize = widthSize; + } else { + mainSize = getLargestMainSize(); + } + paddingAlongMainAxis = getPaddingLeft() + getPaddingRight(); + break; + case FLEX_DIRECTION_COLUMN: // Intentional fall through + case FLEX_DIRECTION_COLUMN_REVERSE: + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + if (heightMode == MeasureSpec.EXACTLY) { + mainSize = heightSize; + } else { + mainSize = getLargestMainSize(); + } + paddingAlongMainAxis = getPaddingTop() + getPaddingBottom(); + break; + default: + throw new IllegalArgumentException("Invalid flex direction: " + flexDirection); + } + + int childIndex = 0; + for (FlexLine flexLine : mFlexLines) { + if (flexLine.mMainSize < mainSize) { + childIndex = expandFlexItems(flexLine, flexDirection, mainSize, + paddingAlongMainAxis, childIndex); + } else { + childIndex = shrinkFlexItems(flexLine, flexDirection, mainSize, + paddingAlongMainAxis, childIndex); + } + } + } + + /** + * Expand the flex items along the main axis based on the individual flexGrow attribute. + * + * @param flexLine the flex line to which flex items belong + * @param flexDirection the flexDirection value for this FlexboxLayout + * @param maxMainSize the maximum main size. Expanded main size will be this size + * @param paddingAlongMainAxis the padding value along the main axis + * @param startIndex the start index of the children views to be expanded. This index + * needs to + * be an absolute index in the flex container (FlexboxLayout), + * not the relative index in the flex line. + * @return the next index, the next flex line's first flex item starts from the returned index + * @see #getFlexDirection() + * @see #setFlexDirection(int) + * @see LayoutParams#flexGrow + */ + private int expandFlexItems(FlexLine flexLine, @FlexDirection int flexDirection, + int maxMainSize, int paddingAlongMainAxis, int startIndex) { + int childIndex = startIndex; + if (flexLine.mTotalFlexGrow <= 0 || maxMainSize < flexLine.mMainSize) { + childIndex += flexLine.mItemCount; + return childIndex; + } + int sizeBeforeExpand = flexLine.mMainSize; + boolean needsReexpand = false; + float unitSpace = (maxMainSize - flexLine.mMainSize) / flexLine.mTotalFlexGrow; + flexLine.mMainSize = paddingAlongMainAxis + flexLine.mDividerLengthInMainSize; + float accumulatedRoundError = 0; + for (int i = 0; i < flexLine.mItemCount; i++) { + View child = getReorderedChildAt(childIndex); + if (child == null) { + continue; + } else if (child.getVisibility() == View.GONE) { + childIndex++; + continue; + } + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (isMainAxisDirectionHorizontal(flexDirection)) { + // The direction of the main axis is horizontal + if (!mChildrenFrozen[childIndex]) { + float rawCalculatedWidth = child.getMeasuredWidth() + unitSpace * lp.flexGrow + accumulatedRoundError; + int roundedCalculatedWidth = Math.round(rawCalculatedWidth); + if (roundedCalculatedWidth > lp.maxWidth) { + // This means the child can't expand beyond the value of the maxWidth attribute. + // To adjust the flex line length to the size of maxMainSize, remaining + // positive free space needs to be re-distributed to other flex items + // (children views). In that case, invoke this method again with the same + // startIndex. + needsReexpand = true; + roundedCalculatedWidth = lp.maxWidth; + mChildrenFrozen[childIndex] = true; + flexLine.mTotalFlexGrow -= lp.flexGrow; + } else { + accumulatedRoundError = (rawCalculatedWidth - roundedCalculatedWidth); + } + child.measure(MeasureSpec.makeMeasureSpec(roundedCalculatedWidth, MeasureSpec.EXACTLY), + MeasureSpec + .makeMeasureSpec(child.getMeasuredHeight(), + MeasureSpec.EXACTLY)); + } + flexLine.mMainSize += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + } else { + // The direction of the main axis is vertical + if (!mChildrenFrozen[childIndex]) { + float rawCalculatedHeight = child.getMeasuredHeight() + unitSpace * lp.flexGrow; + int roundedCalculatedHeight = Math.round(rawCalculatedHeight); + if (roundedCalculatedHeight > lp.maxHeight) { + // This means the child can't expand beyond the value of the maxHeight + // attribute. + // To adjust the flex line length to the size of maxMainSize, remaining + // positive free space needs to be re-distributed to other flex items + // (children views). In that case, invoke this method again with the same + // startIndex. + needsReexpand = true; + roundedCalculatedHeight = lp.maxHeight; + mChildrenFrozen[childIndex] = true; + flexLine.mTotalFlexGrow -= lp.flexGrow; + } else { + accumulatedRoundError = rawCalculatedHeight - roundedCalculatedHeight; + } + child.measure(MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(roundedCalculatedHeight, MeasureSpec.EXACTLY)); + } + flexLine.mMainSize += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + } + childIndex++; + } + + if (needsReexpand && sizeBeforeExpand != flexLine.mMainSize) { + // Re-invoke the method with the same startIndex to distribute the positive free space + // that wasn't fully distributed (because of maximum length constraint) + expandFlexItems(flexLine, flexDirection, maxMainSize, paddingAlongMainAxis, startIndex); + } + return childIndex; + } + + /** + * Shrink the flex items along the main axis based on the individual flexShrink attribute. + * + * @param flexLine the flex line to which flex items belong + * @param flexDirection the flexDirection value for this FlexboxLayout + * @param maxMainSize the maximum main size. Shrank main size will be this size + * @param paddingAlongMainAxis the padding value along the main axis + * @param startIndex the start index of the children views to be shrank. This index + * needs to + * be an absolute index in the flex container (FlexboxLayout), + * not the relative index in the flex line. + * @return the next index, the next flex line's first flex item starts from the returned index + * @see #getFlexDirection() + * @see #setFlexDirection(int) + * @see LayoutParams#flexShrink + */ + private int shrinkFlexItems(FlexLine flexLine, @FlexDirection int flexDirection, + int maxMainSize, int paddingAlongMainAxis, int startIndex) { + int childIndex = startIndex; + int sizeBeforeShrink = flexLine.mMainSize; + if (flexLine.mTotalFlexShrink <= 0 || maxMainSize > flexLine.mMainSize) { + childIndex += flexLine.mItemCount; + return childIndex; + } + boolean needsReshrink = false; + float unitShrink = (flexLine.mMainSize - maxMainSize) / flexLine.mTotalFlexShrink; + float accumulatedRoundError = 0; + flexLine.mMainSize = paddingAlongMainAxis + flexLine.mDividerLengthInMainSize; + for (int i = 0; i < flexLine.mItemCount; i++) { + View child = getReorderedChildAt(childIndex); + if (child == null) { + continue; + } else if (child.getVisibility() == View.GONE) { + childIndex++; + continue; + } + LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (isMainAxisDirectionHorizontal(flexDirection)) { + // The direction of main axis is horizontal + if (!mChildrenFrozen[childIndex]) { + float rawCalculatedWidth = child.getMeasuredWidth() - unitShrink * lp.flexShrink + accumulatedRoundError; + int roundedCalculatedWidth = Math.round(rawCalculatedWidth); + if (roundedCalculatedWidth < lp.minWidth) { + needsReshrink = true; + roundedCalculatedWidth = lp.minWidth; + mChildrenFrozen[childIndex] = true; + flexLine.mTotalFlexShrink -= lp.flexShrink; + } else { + accumulatedRoundError = rawCalculatedWidth - roundedCalculatedWidth; + } + + int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(roundedCalculatedWidth, MeasureSpec.EXACTLY); + + // NOTE: for controls that support internal content wrapping (e.g. TextView) reducing the width + // might result in increased height e.g. text that could be shown on one line for larger + // width needs to be wrapped in two when width is reduced. + // As a result we cannot unconditionally measure with EXACTLY the current measured height + int childHeightMeasureSpec = getChildMeasureSpec(this.getMeasuredHeightAndState(), + getPaddingTop() + getPaddingBottom() + lp.topMargin + + lp.bottomMargin, lp.height < 0 ? LayoutParams.WRAP_CONTENT : lp.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + + // make sure crossSize is up-to-date as child calculated height might have increased + flexLine.mCrossSize = Math.max( + flexLine.mCrossSize, + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin + ); + } + flexLine.mMainSize += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; + } else { + // The direction of main axis is vertical + if (!mChildrenFrozen[childIndex]) { + float rawCalculatedHeight = child.getMeasuredHeight() - unitShrink * lp.flexShrink + accumulatedRoundError; + int roundedCalculatedHeight = Math.round(rawCalculatedHeight); + if (roundedCalculatedHeight < lp.minHeight) { + needsReshrink = true; + roundedCalculatedHeight = lp.minHeight; + mChildrenFrozen[childIndex] = true; + flexLine.mTotalFlexShrink -= lp.flexShrink; + } else { + accumulatedRoundError = rawCalculatedHeight - roundedCalculatedHeight; + } + child.measure(MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(roundedCalculatedHeight, MeasureSpec.EXACTLY)); + } + flexLine.mMainSize += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; + } + childIndex++; + } + + if (needsReshrink && sizeBeforeShrink != flexLine.mMainSize) { + // Re-invoke the method with the same startIndex to distribute the negative free space + // that wasn't fully distributed (because some views length were not enough) + shrinkFlexItems(flexLine, flexDirection, maxMainSize, paddingAlongMainAxis, startIndex); + } + return childIndex; + } + + /** + * Determines the cross size (Calculate the length along the cross axis). + * Expand the cross size only if the height mode is MeasureSpec.EXACTLY, otherwise + * use the sum of cross sizes of all flex lines. + * + * @param flexDirection the flex direction attribute + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @param paddingAlongCrossAxis the padding value for the FlexboxLayout along the cross axis + * @see #getFlexDirection() + * @see #setFlexDirection(int) + * @see #getAlignContent() + * @see #setAlignContent(int) + */ + private void determineCrossSize(int flexDirection, int widthMeasureSpec, + int heightMeasureSpec, int paddingAlongCrossAxis) { + // The MeasureSpec mode along the cross axis + int mode; + // The MeasureSpec size along the cross axis + int size; + switch (flexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + mode = MeasureSpec.getMode(heightMeasureSpec); + size = MeasureSpec.getSize(heightMeasureSpec); + break; + case FLEX_DIRECTION_COLUMN: // Intentional fall through + case FLEX_DIRECTION_COLUMN_REVERSE: + mode = MeasureSpec.getMode(widthMeasureSpec); + size = MeasureSpec.getSize(widthMeasureSpec); + break; + default: + throw new IllegalArgumentException("Invalid flex direction: " + flexDirection); + } + if (mode == MeasureSpec.EXACTLY) { + int totalCrossSize = getSumOfCrossSize() + paddingAlongCrossAxis; + if (mFlexLines.size() == 1) { + mFlexLines.get(0).mCrossSize = size - paddingAlongCrossAxis; + // alignContent property is valid only if the Flexbox has at least two lines + } else if (mFlexLines.size() >= 2 && totalCrossSize < size) { + switch (mAlignContent) { + case ALIGN_CONTENT_STRETCH: { + float freeSpaceUnit = (size - totalCrossSize) / (float) mFlexLines.size(); + float accumulatedError = 0; + for (int i = 0, flexLinesSize = mFlexLines.size(); i < flexLinesSize; i++) { + FlexLine flexLine = mFlexLines.get(i); + float newCrossSizeAsFloat = flexLine.mCrossSize + freeSpaceUnit; + if (i == mFlexLines.size() - 1) { + newCrossSizeAsFloat += accumulatedError; + accumulatedError = 0; + } + int newCrossSize = Math.round(newCrossSizeAsFloat); + accumulatedError += (newCrossSizeAsFloat - newCrossSize); + if (accumulatedError > 1) { + newCrossSize += 1; + accumulatedError -= 1; + } else if (accumulatedError < -1) { + newCrossSize -= 1; + accumulatedError += 1; + } + flexLine.mCrossSize = newCrossSize; + } + break; + } + case ALIGN_CONTENT_SPACE_AROUND: { + // The value of free space along the cross axis which needs to be put on top + // and below the bottom of each flex line. + int spaceTopAndBottom = size - totalCrossSize; + // The number of spaces along the cross axis + int numberOfSpaces = mFlexLines.size() * 2; + spaceTopAndBottom = spaceTopAndBottom / numberOfSpaces; + List newFlexLines = new ArrayList<>(); + FlexLine dummySpaceFlexLine = new FlexLine(); + dummySpaceFlexLine.mCrossSize = spaceTopAndBottom; + for (FlexLine flexLine : mFlexLines) { + newFlexLines.add(dummySpaceFlexLine); + newFlexLines.add(flexLine); + newFlexLines.add(dummySpaceFlexLine); + } + mFlexLines = newFlexLines; + break; + } + case ALIGN_CONTENT_SPACE_BETWEEN: { + // The value of free space along the cross axis between each flex line. + float spaceBetweenFlexLine = size - totalCrossSize; + int numberOfSpaces = mFlexLines.size() - 1; + spaceBetweenFlexLine = spaceBetweenFlexLine / (float) numberOfSpaces; + float accumulatedError = 0; + List newFlexLines = new ArrayList<>(); + for (int i = 0, flexLineSize = mFlexLines.size(); i < flexLineSize; i++) { + FlexLine flexLine = mFlexLines.get(i); + newFlexLines.add(flexLine); + + if (i != mFlexLines.size() - 1) { + FlexLine dummySpaceFlexLine = new FlexLine(); + if (i == mFlexLines.size() - 2) { + // The last dummy space block in the flex container. + // Adjust the cross size by the accumulated error. + dummySpaceFlexLine.mCrossSize = Math + .round(spaceBetweenFlexLine + accumulatedError); + accumulatedError = 0; + } else { + dummySpaceFlexLine.mCrossSize = Math + .round(spaceBetweenFlexLine); + } + accumulatedError += (spaceBetweenFlexLine + - dummySpaceFlexLine.mCrossSize); + if (accumulatedError > 1) { + dummySpaceFlexLine.mCrossSize += 1; + accumulatedError -= 1; + } else if (accumulatedError < -1) { + dummySpaceFlexLine.mCrossSize -= 1; + accumulatedError += 1; + } + newFlexLines.add(dummySpaceFlexLine); + } + } + mFlexLines = newFlexLines; + break; + } + case ALIGN_CONTENT_CENTER: { + int spaceAboveAndBottom = size - totalCrossSize; + spaceAboveAndBottom = spaceAboveAndBottom / 2; + List newFlexLines = new ArrayList<>(); + FlexLine dummySpaceFlexLine = new FlexLine(); + dummySpaceFlexLine.mCrossSize = spaceAboveAndBottom; + for (int i = 0, flexLineSize = mFlexLines.size(); i < flexLineSize; i++) { + if (i == 0) { + newFlexLines.add(dummySpaceFlexLine); + } + FlexLine flexLine = mFlexLines.get(i); + newFlexLines.add(flexLine); + if (i == mFlexLines.size() - 1) { + newFlexLines.add(dummySpaceFlexLine); + } + } + mFlexLines = newFlexLines; + break; + } + case ALIGN_CONTENT_FLEX_END: { + int spaceTop = size - totalCrossSize; + FlexLine dummySpaceFlexLine = new FlexLine(); + dummySpaceFlexLine.mCrossSize = spaceTop; + mFlexLines.add(0, dummySpaceFlexLine); + break; + } + } + } + } + } + + /** + * Expand the view if the {@link #mAlignItems} attribute is set to {@link #ALIGN_ITEMS_STRETCH} + * or {@link LayoutParams#ALIGN_SELF_STRETCH} is set to an individual child view. + * + * @param flexDirection the flex direction attribute + * @param alignItems the align items attribute + * @see #getFlexDirection() + * @see #setFlexDirection(int) + * @see #getAlignItems() + * @see #setAlignItems(int) + * @see LayoutParams#alignSelf + */ + private void stretchViews(int flexDirection, int alignItems) { + if (alignItems == ALIGN_ITEMS_STRETCH) { + int viewIndex = 0; + for (FlexLine flexLine : mFlexLines) { + for (int i = 0; i < flexLine.mItemCount; i++, viewIndex++) { + View view = getReorderedChildAt(viewIndex); + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.alignSelf != LayoutParams.ALIGN_SELF_AUTO && + lp.alignSelf != LayoutParams.ALIGN_SELF_STRETCH) { + continue; + } + switch (flexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + stretchViewVertically(view, flexLine.mCrossSize); + break; + case FLEX_DIRECTION_COLUMN: + case FLEX_DIRECTION_COLUMN_REVERSE: + stretchViewHorizontally(view, flexLine.mCrossSize); + break; + default: + throw new IllegalArgumentException( + "Invalid flex direction: " + flexDirection); + } + } + } + } else { + for (FlexLine flexLine : mFlexLines) { + for (Integer index : flexLine.mIndicesAlignSelfStretch) { + View view = getReorderedChildAt(index); + switch (flexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + stretchViewVertically(view, flexLine.mCrossSize); + break; + case FLEX_DIRECTION_COLUMN: + case FLEX_DIRECTION_COLUMN_REVERSE: + stretchViewHorizontally(view, flexLine.mCrossSize); + break; + default: + throw new IllegalArgumentException( + "Invalid flex direction: " + flexDirection); + } + } + } + } + } + + /** + * Expand the view vertically to the size of the crossSize (considering the view margins) + * + * @param view the View to be stretched + * @param crossSize the cross size + */ + private void stretchViewVertically(View view, int crossSize) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + int newHeight = crossSize - lp.topMargin - lp.bottomMargin; + newHeight = Math.max(newHeight, 0); + view.measure(MeasureSpec + .makeMeasureSpec(view.getMeasuredWidth(), MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY)); + } + + /** + * Expand the view horizontally to the size of the crossSize (considering the view margins) + * + * @param view the View to be stretched + * @param crossSize the cross size + */ + private void stretchViewHorizontally(View view, int crossSize) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + int newWidth = crossSize - lp.leftMargin - lp.rightMargin; + newWidth = Math.max(newWidth, 0); + view.measure(MeasureSpec + .makeMeasureSpec(newWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(view.getMeasuredHeight(), MeasureSpec.EXACTLY)); + } + + /** + * Set this FlexboxLayouts' width and height depending on the calculated size of main axis and + * cross axis. + * + * @param flexDirection the value of the flex direction + * @param widthMeasureSpec horizontal space requirements as imposed by the parent + * @param heightMeasureSpec vertical space requirements as imposed by the parent + * @param childState the child state of the View + * @see #getFlexDirection() + * @see #setFlexDirection(int) + */ + private void setMeasuredDimensionForFlex(@FlexDirection int flexDirection, int widthMeasureSpec, + int heightMeasureSpec, int childState) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int calculatedMaxHeight; + int calculatedMaxWidth; + switch (flexDirection) { + case FLEX_DIRECTION_ROW: // Intentional fall through + case FLEX_DIRECTION_ROW_REVERSE: + calculatedMaxHeight = getSumOfCrossSize() + getPaddingTop() + + getPaddingBottom(); + calculatedMaxWidth = getLargestMainSize(); + break; + case FLEX_DIRECTION_COLUMN: // Intentional fall through + case FLEX_DIRECTION_COLUMN_REVERSE: + calculatedMaxHeight = getLargestMainSize(); + calculatedMaxWidth = getSumOfCrossSize() + getPaddingLeft() + getPaddingRight(); + break; + default: + throw new IllegalArgumentException("Invalid flex direction: " + flexDirection); + } + + int widthSizeAndState; + switch (widthMode) { + case MeasureSpec.EXACTLY: + if (widthSize < calculatedMaxWidth) { + childState = ViewCompat + .combineMeasuredStates(childState, ViewCompat.MEASURED_STATE_TOO_SMALL); + } + widthSizeAndState = ViewCompat.resolveSizeAndState(widthSize, widthMeasureSpec, + childState); + break; + case MeasureSpec.AT_MOST: { + if (widthSize < calculatedMaxWidth) { + childState = ViewCompat + .combineMeasuredStates(childState, ViewCompat.MEASURED_STATE_TOO_SMALL); + } else { + widthSize = calculatedMaxWidth; + } + widthSizeAndState = ViewCompat.resolveSizeAndState(widthSize, widthMeasureSpec, + childState); + break; + } + case MeasureSpec.UNSPECIFIED: { + widthSizeAndState = ViewCompat + .resolveSizeAndState(calculatedMaxWidth, widthMeasureSpec, childState); + break; + } + default: + throw new IllegalStateException("Unknown width mode is set: " + widthMode); + } + int heightSizeAndState; + switch (heightMode) { + case MeasureSpec.EXACTLY: + if (heightSize < calculatedMaxHeight) { + childState = ViewCompat.combineMeasuredStates(childState, + ViewCompat.MEASURED_STATE_TOO_SMALL + >> ViewCompat.MEASURED_HEIGHT_STATE_SHIFT); + } + heightSizeAndState = ViewCompat.resolveSizeAndState(heightSize, heightMeasureSpec, + childState); + break; + case MeasureSpec.AT_MOST: { + if (heightSize < calculatedMaxHeight) { + childState = ViewCompat.combineMeasuredStates(childState, + ViewCompat.MEASURED_STATE_TOO_SMALL + >> ViewCompat.MEASURED_HEIGHT_STATE_SHIFT); + } else { + heightSize = calculatedMaxHeight; + } + heightSizeAndState = ViewCompat.resolveSizeAndState(heightSize, heightMeasureSpec, + childState); + break; + } + case MeasureSpec.UNSPECIFIED: { + heightSizeAndState = ViewCompat.resolveSizeAndState(calculatedMaxHeight, + heightMeasureSpec, childState); + break; + } + default: + throw new IllegalStateException("Unknown height mode is set: " + heightMode); + } + setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + /** + * Determine if a wrap is required (add a new flex line). + * + * @param mode the width or height mode along the main axis direction + * @param maxSize the max size along the main axis direction + * @param currentLength the accumulated current length + * @param childLength the length of a child view which is to be collected to the flex line + * @param lp the LayoutParams for the view being determined whether a new flex line + * is needed + * @return {@code true} if a wrap is required, {@code false} otherwise + * @see #getFlexWrap() + * @see #setFlexWrap(int) + */ + private boolean isWrapRequired(int mode, int maxSize, int currentLength, int childLength, + LayoutParams lp, int childAbsoluteIndex, int childRelativeIndexInFlexLine) { + if (mFlexWrap == FLEX_WRAP_NOWRAP) { + return false; + } + if (lp.wrapBefore) { + return true; + } + if (mode == MeasureSpec.UNSPECIFIED) { + return false; + } + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + if (hasDividerBeforeChildAtAlongMainAxis(childAbsoluteIndex, + childRelativeIndexInFlexLine)) { + childLength += mDividerVerticalWidth; + } + if ((mShowDividerVertical & SHOW_DIVIDER_END) > 0) { + childLength += mDividerVerticalWidth; + } + } else { + if (hasDividerBeforeChildAtAlongMainAxis(childAbsoluteIndex, + childRelativeIndexInFlexLine)) { + childLength += mDividerHorizontalHeight; + } + if ((mShowDividerHorizontal & SHOW_DIVIDER_END) > 0) { + childLength += mDividerHorizontalHeight; + } + } + return maxSize < currentLength + childLength; + } + + /** + * Retrieve the largest main size of all flex lines. + * + * @return the largest main size + */ + private int getLargestMainSize() { + int largestSize = Integer.MIN_VALUE; + for (FlexLine flexLine : mFlexLines) { + largestSize = Math.max(largestSize, flexLine.mMainSize); + } + return largestSize; + } + + /** + * Retrieve the sum of the cross sizes of all flex lines including divider lengths. + * + * @return the sum of the cross sizes + */ + private int getSumOfCrossSize() { + int sum = 0; + for (int i = 0, size = mFlexLines.size(); i < size; i++) { + FlexLine flexLine = mFlexLines.get(i); + + // Judge if the beginning or middle dividers are required + if (hasDividerBeforeFlexLine(i)) { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + sum += mDividerHorizontalHeight; + } else { + sum += mDividerVerticalWidth; + } + } + + // Judge if the end divider is required + if (hasEndDividerAfterFlexLine(i)) { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + sum += mDividerHorizontalHeight; + } else { + sum += mDividerVerticalWidth; + } + } + sum += flexLine.mCrossSize; + } + return sum; + } + + private boolean isMainAxisDirectionHorizontal(@FlexDirection int flexDirection) { + return flexDirection == FLEX_DIRECTION_ROW + || flexDirection == FLEX_DIRECTION_ROW_REVERSE; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int layoutDirection = ViewCompat.getLayoutDirection(this); + boolean isRtl; + switch (mFlexDirection) { + case FLEX_DIRECTION_ROW: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + layoutHorizontal(isRtl, left, top, right, bottom); + break; + case FLEX_DIRECTION_ROW_REVERSE: + isRtl = layoutDirection != ViewCompat.LAYOUT_DIRECTION_RTL; + layoutHorizontal(isRtl, left, top, right, bottom); + break; + case FLEX_DIRECTION_COLUMN: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + isRtl = !isRtl; + } + layoutVertical(isRtl, false, left, top, right, bottom); + break; + case FLEX_DIRECTION_COLUMN_REVERSE: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + isRtl = !isRtl; + } + layoutVertical(isRtl, true, left, top, right, bottom); + break; + default: + throw new IllegalStateException("Invalid flex direction is set: " + mFlexDirection); + } + + CommonLayoutParams.restoreOriginalParams(this); + } + + /** + * Sub method for {@link #onLayout(boolean, int, int, int, int)} when the + * {@link #mFlexDirection} is either {@link #FLEX_DIRECTION_ROW} or + * {@link #FLEX_DIRECTION_ROW_REVERSE}. + * + * @param isRtl {@code true} if the horizontal layout direction is right to left, {@code + * false} otherwise. + * @param left the left position of this View + * @param top the top position of this View + * @param right the right position of this View + * @param bottom the bottom position of this View + * @see #getFlexWrap() + * @see #setFlexWrap(int) + * @see #getJustifyContent() + * @see #setJustifyContent(int) + * @see #getAlignItems() + * @see #setAlignItems(int) + * @see LayoutParams#alignSelf + */ + private void layoutHorizontal(boolean isRtl, int left, int top, int right, int bottom) { + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + // Use float to reduce the round error that may happen in when justifyContent == + // SPACE_BETWEEN or SPACE_AROUND + float childLeft; + int currentViewIndex = 0; + + int height = bottom - top; + int width = right - left; + // childBottom is used if the mFlexWrap is FLEX_WRAP_WRAP_REVERSE otherwise + // childTop is used to align the vertical position of the children views. + int childBottom = height - getPaddingBottom(); + int childTop = getPaddingTop(); + + // Used only for RTL layout + // Use float to reduce the round error that may happen in when justifyContent == + // SPACE_BETWEEN or SPACE_AROUND + float childRight; + for (int i = 0, size = mFlexLines.size(); i < size; i++) { + FlexLine flexLine = mFlexLines.get(i); + if (hasDividerBeforeFlexLine(i)) { + childBottom -= mDividerHorizontalHeight; + childTop += mDividerHorizontalHeight; + } + float spaceBetweenItem = 0f; + switch (mJustifyContent) { + case JUSTIFY_CONTENT_FLEX_START: + childLeft = paddingLeft; + childRight = width - paddingRight; + break; + case JUSTIFY_CONTENT_FLEX_END: + childLeft = width - flexLine.mMainSize + paddingRight; + childRight = flexLine.mMainSize - paddingLeft; + break; + case JUSTIFY_CONTENT_CENTER: + childLeft = paddingLeft + (width - flexLine.mMainSize) / 2f; + childRight = width - paddingRight - (width - flexLine.mMainSize) / 2f; + break; + case JUSTIFY_CONTENT_SPACE_AROUND: + if (flexLine.mItemCount != 0) { + spaceBetweenItem = (width - flexLine.mMainSize) + / (float) flexLine.mItemCount; + } + childLeft = paddingLeft + spaceBetweenItem / 2f; + childRight = width - paddingRight - spaceBetweenItem / 2f; + break; + case JUSTIFY_CONTENT_SPACE_BETWEEN: + childLeft = paddingLeft; + float denominator = flexLine.mItemCount != 1 ? flexLine.mItemCount - 1 : 1f; + spaceBetweenItem = (width - flexLine.mMainSize) / denominator; + childRight = width - paddingRight; + break; + default: + throw new IllegalStateException( + "Invalid justifyContent is set: " + mJustifyContent); + } + spaceBetweenItem = Math.max(spaceBetweenItem, 0); + + for (int j = 0; j < flexLine.mItemCount; j++) { + View child = getReorderedChildAt(currentViewIndex); + if (child == null) { + continue; + } else if (child.getVisibility() == View.GONE) { + currentViewIndex++; + continue; + } + LayoutParams lp = ((LayoutParams) child.getLayoutParams()); + childLeft += lp.leftMargin; + childRight -= lp.rightMargin; + if (hasDividerBeforeChildAtAlongMainAxis(currentViewIndex, j)) { + childLeft += mDividerVerticalWidth; + childRight -= mDividerVerticalWidth; + } + + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + if (isRtl) { + layoutSingleChildHorizontal(child, flexLine, mFlexWrap, mAlignItems, + Math.round(childRight) - child.getMeasuredWidth(), + childBottom - child.getMeasuredHeight(), Math.round(childRight), + childBottom); + } else { + layoutSingleChildHorizontal(child, flexLine, mFlexWrap, mAlignItems, + Math.round(childLeft), childBottom - child.getMeasuredHeight(), + Math.round(childLeft) + child.getMeasuredWidth(), + childBottom); + } + } else { + if (isRtl) { + layoutSingleChildHorizontal(child, flexLine, mFlexWrap, mAlignItems, + Math.round(childRight) - child.getMeasuredWidth(), childTop, + Math.round(childRight), childTop + child.getMeasuredHeight()); + } else { + layoutSingleChildHorizontal(child, flexLine, mFlexWrap, mAlignItems, + Math.round(childLeft), childTop, + Math.round(childLeft) + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + } + } + childLeft += child.getMeasuredWidth() + spaceBetweenItem + lp.rightMargin; + childRight -= child.getMeasuredWidth() + spaceBetweenItem + lp.leftMargin; + currentViewIndex++; + + flexLine.mLeft = Math.min(flexLine.mLeft, child.getLeft() - lp.leftMargin); + flexLine.mTop = Math.min(flexLine.mTop, child.getTop() - lp.topMargin); + flexLine.mRight = Math.max(flexLine.mRight, child.getRight() + lp.rightMargin); + flexLine.mBottom = Math.max(flexLine.mBottom, child.getBottom() + lp.bottomMargin); + } + childTop += flexLine.mCrossSize; + childBottom -= flexLine.mCrossSize; + } + } + + /** + * Place a single View when the layout direction is horizontal ({@link #mFlexDirection} is + * either {@link #FLEX_DIRECTION_ROW} or {@link #FLEX_DIRECTION_ROW_REVERSE}). + * + * @param view the View to be placed + * @param flexLine the {@link FlexLine} where the View belongs to + * @param flexWrap the flex wrap attribute of this FlexboxLayout + * @param alignItems the align items attribute of this FlexboxLayout + * @param left the left position of the View, which the View's margin is already taken + * into account + * @param top the top position of the flex line where the View belongs to. The actual + * View's top position is shifted depending on the flexWrap and alignItems + * attributes + * @param right the right position of the View, which the View's margin is already taken + * into account + * @param bottom the bottom position of the flex line where the View belongs to. The actual + * View's bottom position is shifted depending on the flexWrap and alignItems + * attributes + * @see #getAlignItems() + * @see #setAlignItems(int) + * @see LayoutParams#alignSelf + */ + private void layoutSingleChildHorizontal(View view, FlexLine flexLine, @FlexWrap int flexWrap, + int alignItems, int left, int top, int right, int bottom) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.alignSelf != LayoutParams.ALIGN_SELF_AUTO) { + // Expecting the values for alignItems and alignSelf match except for ALIGN_SELF_AUTO. + // Assigning the alignSelf value as alignItems should work. + alignItems = lp.alignSelf; + } + int crossSize = flexLine.mCrossSize; + switch (alignItems) { + case ALIGN_ITEMS_FLEX_START: // Intentional fall through + case ALIGN_ITEMS_STRETCH: + if (flexWrap != FLEX_WRAP_WRAP_REVERSE) { + view.layout(left, top + lp.topMargin, right, bottom + lp.topMargin); + } else { + view.layout(left, top - lp.bottomMargin, right, bottom - lp.bottomMargin); + } + break; + case ALIGN_ITEMS_BASELINE: + if (flexWrap != FLEX_WRAP_WRAP_REVERSE) { + int marginTop = flexLine.mMaxBaseline - view.getBaseline(); + marginTop = Math.max(marginTop, lp.topMargin); + view.layout(left, top + marginTop, right, bottom + marginTop); + } else { + int marginBottom = flexLine.mMaxBaseline - view.getMeasuredHeight() + view + .getBaseline(); + marginBottom = Math.max(marginBottom, lp.bottomMargin); + view.layout(left, top - marginBottom, right, bottom - marginBottom); + } + break; + case ALIGN_ITEMS_FLEX_END: + if (flexWrap != FLEX_WRAP_WRAP_REVERSE) { + view.layout(left, + top + crossSize - view.getMeasuredHeight() - lp.bottomMargin, + right, top + crossSize - lp.bottomMargin); + } else { + // If the flexWrap == FLEX_WRAP_WRAP_REVERSE, the direction of the + // flexEnd is flipped (from top to bottom). + view.layout(left, top - crossSize + view.getMeasuredHeight() + lp.topMargin, + right, bottom - crossSize + view.getMeasuredHeight() + lp.topMargin); + } + break; + case ALIGN_ITEMS_CENTER: + int topFromCrossAxis = (crossSize - view.getMeasuredHeight()) / 2; + if (flexWrap != FLEX_WRAP_WRAP_REVERSE) { + view.layout(left, top + topFromCrossAxis + lp.topMargin - lp.bottomMargin, + right, top + topFromCrossAxis + view.getMeasuredHeight() + lp.topMargin + - lp.bottomMargin); + } else { + view.layout(left, top - topFromCrossAxis + lp.topMargin - lp.bottomMargin, + right, top - topFromCrossAxis + view.getMeasuredHeight() + lp.topMargin + - lp.bottomMargin); + } + break; + } + } + + /** + * Sub method for {@link #onLayout(boolean, int, int, int, int)} when the + * {@link #mFlexDirection} is either {@link #FLEX_DIRECTION_COLUMN} or + * {@link #FLEX_DIRECTION_COLUMN_REVERSE}. + * + * @param isRtl {@code true} if the horizontal layout direction is right to left, + * {@code false} + * otherwise + * @param fromBottomToTop {@code true} if the layout direction is bottom to top, {@code false} + * otherwise + * @param left the left position of this View + * @param top the top position of this View + * @param right the right position of this View + * @param bottom the bottom position of this View + * @see #getFlexWrap() + * @see #setFlexWrap(int) + * @see #getJustifyContent() + * @see #setJustifyContent(int) + * @see #getAlignItems() + * @see #setAlignItems(int) + * @see LayoutParams#alignSelf + */ + private void layoutVertical(boolean isRtl, boolean fromBottomToTop, int left, int top, + int right, int bottom) { + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + + int paddingRight = getPaddingRight(); + int childLeft = getPaddingLeft(); + int currentViewIndex = 0; + + int width = right - left; + int height = bottom - top; + // childRight is used if the mFlexWrap is FLEX_WRAP_WRAP_REVERSE otherwise + // childLeft is used to align the horizontal position of the children views. + int childRight = width - paddingRight; + + // Use float to reduce the round error that may happen in when justifyContent == + // SPACE_BETWEEN or SPACE_AROUND + float childTop; + + // Used only for if the direction is from bottom to top + float childBottom; + + for (int i = 0, size = mFlexLines.size(); i < size; i++) { + FlexLine flexLine = mFlexLines.get(i); + if (hasDividerBeforeFlexLine(i)) { + childLeft += mDividerVerticalWidth; + childRight -= mDividerVerticalWidth; + } + float spaceBetweenItem = 0f; + switch (mJustifyContent) { + case JUSTIFY_CONTENT_FLEX_START: + childTop = paddingTop; + childBottom = height - paddingBottom; + break; + case JUSTIFY_CONTENT_FLEX_END: + childTop = height - flexLine.mMainSize + paddingBottom; + childBottom = flexLine.mMainSize - paddingTop; + break; + case JUSTIFY_CONTENT_CENTER: + childTop = paddingTop + (height - flexLine.mMainSize) / 2f; + childBottom = height - paddingBottom - (height - flexLine.mMainSize) / 2f; + break; + case JUSTIFY_CONTENT_SPACE_AROUND: + if (flexLine.mItemCount != 0) { + spaceBetweenItem = (height - flexLine.mMainSize) + / (float) flexLine.mItemCount; + } + childTop = paddingTop + spaceBetweenItem / 2f; + childBottom = height - paddingBottom - spaceBetweenItem / 2f; + break; + case JUSTIFY_CONTENT_SPACE_BETWEEN: + childTop = paddingTop; + float denominator = flexLine.mItemCount != 1 ? flexLine.mItemCount - 1 : 1f; + spaceBetweenItem = (height - flexLine.mMainSize) / denominator; + childBottom = height - paddingBottom; + break; + default: + throw new IllegalStateException( + "Invalid justifyContent is set: " + mJustifyContent); + } + spaceBetweenItem = Math.max(spaceBetweenItem, 0); + + for (int j = 0; j < flexLine.mItemCount; j++) { + View child = getReorderedChildAt(currentViewIndex); + if (child == null) { + continue; + } else if (child.getVisibility() == View.GONE) { + currentViewIndex++; + continue; + } + LayoutParams lp = ((LayoutParams) child.getLayoutParams()); + childTop += lp.topMargin; + childBottom -= lp.bottomMargin; + if (hasDividerBeforeChildAtAlongMainAxis(currentViewIndex, j)) { + childTop += mDividerHorizontalHeight; + childBottom -= mDividerHorizontalHeight; + } + if (isRtl) { + if (fromBottomToTop) { + layoutSingleChildVertical(child, flexLine, true, mAlignItems, + childRight - child.getMeasuredWidth(), + Math.round(childBottom) - child.getMeasuredHeight(), childRight, + Math.round(childBottom)); + } else { + layoutSingleChildVertical(child, flexLine, true, mAlignItems, + childRight - child.getMeasuredWidth(), Math.round(childTop), + childRight, Math.round(childTop) + child.getMeasuredHeight()); + } + } else { + if (fromBottomToTop) { + layoutSingleChildVertical(child, flexLine, false, mAlignItems, + childLeft, Math.round(childBottom) - child.getMeasuredHeight(), + childLeft + child.getMeasuredWidth(), Math.round(childBottom)); + } else { + layoutSingleChildVertical(child, flexLine, false, mAlignItems, + childLeft, Math.round(childTop), + childLeft + child.getMeasuredWidth(), + Math.round(childTop) + child.getMeasuredHeight()); + } + } + childTop += child.getMeasuredHeight() + spaceBetweenItem + lp.bottomMargin; + childBottom -= child.getMeasuredHeight() + spaceBetweenItem + lp.topMargin; + currentViewIndex++; + + flexLine.mLeft = Math.min(flexLine.mLeft, child.getLeft() - lp.leftMargin); + flexLine.mTop = Math.min(flexLine.mTop, child.getTop() - lp.topMargin); + flexLine.mRight = Math.max(flexLine.mRight, child.getRight() + lp.rightMargin); + flexLine.mBottom = Math.max(flexLine.mBottom, child.getBottom() + lp.bottomMargin); + } + childLeft += flexLine.mCrossSize; + childRight -= flexLine.mCrossSize; + } + } + + /** + * Place a single View when the layout direction is vertical ({@link #mFlexDirection} is + * either {@link #FLEX_DIRECTION_COLUMN} or {@link #FLEX_DIRECTION_COLUMN_REVERSE}). + * + * @param view the View to be placed + * @param flexLine the {@link FlexLine} where the View belongs to + * @param isRtl {@code true} if the layout direction is right to left, {@code false} + * otherwise + * @param alignItems the align items attribute of this FlexboxLayout + * @param left the left position of the flex line where the View belongs to. The actual + * View's left position is shifted depending on the isRtl and alignItems + * attributes + * @param top the top position of the View, which the View's margin is already taken + * into account + * @param right the right position of the flex line where the View belongs to. The actual + * View's right position is shifted depending on the isRtl and alignItems + * attributes + * @param bottom the bottom position of the View, which the View's margin is already taken + * into account + * @see #getAlignItems() + * @see #setAlignItems(int) + * @see LayoutParams#alignSelf + */ + private void layoutSingleChildVertical(View view, FlexLine flexLine, boolean isRtl, + int alignItems, int left, int top, int right, int bottom) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.alignSelf != LayoutParams.ALIGN_SELF_AUTO) { + // Expecting the values for alignItems and alignSelf match except for ALIGN_SELF_AUTO. + // Assigning the alignSelf value as alignItems should work. + alignItems = lp.alignSelf; + } + int crossSize = flexLine.mCrossSize; + switch (alignItems) { + case ALIGN_ITEMS_FLEX_START: // Intentional fall through + case ALIGN_ITEMS_STRETCH: // Intentional fall through + case ALIGN_ITEMS_BASELINE: + if (!isRtl) { + view.layout(left + lp.leftMargin, top, right + lp.leftMargin, bottom); + } else { + view.layout(left - lp.rightMargin, top, right - lp.rightMargin, bottom); + } + break; + case ALIGN_ITEMS_FLEX_END: + if (!isRtl) { + view.layout(left + crossSize - view.getMeasuredWidth() - lp.rightMargin, + top, right + crossSize - view.getMeasuredWidth() - lp.rightMargin, + bottom); + } else { + // If the flexWrap == FLEX_WRAP_WRAP_REVERSE, the direction of the + // flexEnd is flipped (from left to right). + view.layout(left - crossSize + view.getMeasuredWidth() + lp.leftMargin, top, + right - crossSize + view.getMeasuredWidth() + lp.leftMargin, + bottom); + } + break; + case ALIGN_ITEMS_CENTER: + int leftFromCrossAxis = (crossSize - view.getMeasuredWidth()) / 2; + if (!isRtl) { + view.layout(left + leftFromCrossAxis + lp.leftMargin - lp.rightMargin, + top, right + leftFromCrossAxis + lp.leftMargin - lp.rightMargin, + bottom); + } else { + view.layout(left - leftFromCrossAxis + lp.leftMargin - lp.rightMargin, + top, right - leftFromCrossAxis + lp.leftMargin - lp.rightMargin, + bottom); + } + break; + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (mDividerDrawableVertical == null && mDividerDrawableHorizontal == null) { + return; + } + if (mShowDividerHorizontal == SHOW_DIVIDER_NONE + && mShowDividerVertical == SHOW_DIVIDER_NONE) { + return; + } + + int layoutDirection = ViewCompat.getLayoutDirection(this); + boolean isRtl; + boolean fromBottomToTop = false; + switch (mFlexDirection) { + case FLEX_DIRECTION_ROW: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + fromBottomToTop = true; + } + drawDividersHorizontal(canvas, isRtl, fromBottomToTop); + break; + case FLEX_DIRECTION_ROW_REVERSE: + isRtl = layoutDirection != ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + fromBottomToTop = true; + } + drawDividersHorizontal(canvas, isRtl, fromBottomToTop); + break; + case FLEX_DIRECTION_COLUMN: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + isRtl = !isRtl; + } + fromBottomToTop = false; + drawDividersVertical(canvas, isRtl, fromBottomToTop); + break; + case FLEX_DIRECTION_COLUMN_REVERSE: + isRtl = layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL; + if (mFlexWrap == FLEX_WRAP_WRAP_REVERSE) { + isRtl = !isRtl; + } + fromBottomToTop = true; + drawDividersVertical(canvas, isRtl, fromBottomToTop); + break; + } + } + + /** + * Sub method for {@link #onDraw(Canvas)} when the main axis direction is horizontal + * ({@link #mFlexDirection} is either of {@link #FLEX_DIRECTION_ROW} or + * {@link #FLEX_DIRECTION_ROW_REVERSE}. + * + * @param canvas the canvas on which the background will be drawn + * @param isRtl {@code true} when the horizontal layout direction is right to left, + * {@code false} otherwise + * @param fromBottomToTop {@code true} when the vertical layout direction is bottom to top, + * {@code false} otherwise + */ + private void drawDividersHorizontal(Canvas canvas, boolean isRtl, boolean fromBottomToTop) { + int currentViewIndex = 0; + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + int horizontalDividerLength = Math.max(0, getWidth() - paddingRight - paddingLeft); + for (int i = 0, size = mFlexLines.size(); i < size; i++) { + FlexLine flexLine = mFlexLines.get(i); + for (int j = 0; j < flexLine.mItemCount; j++) { + View view = getReorderedChildAt(currentViewIndex); + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + + // Judge if the beginning or middle divider is needed + if (hasDividerBeforeChildAtAlongMainAxis(currentViewIndex, j)) { + int dividerLeft; + if (isRtl) { + dividerLeft = view.getRight() + lp.rightMargin; + } else { + dividerLeft = view.getLeft() - lp.leftMargin - mDividerVerticalWidth; + } + + drawVerticalDivider(canvas, dividerLeft, flexLine.mTop, flexLine.mCrossSize); + } + + // Judge if the end divider is needed + if (j == flexLine.mItemCount - 1) { + if ((mShowDividerVertical & SHOW_DIVIDER_END) > 0) { + int dividerLeft; + if (isRtl) { + dividerLeft = view.getLeft() - lp.leftMargin - mDividerVerticalWidth; + } else { + dividerLeft = view.getRight() + lp.rightMargin; + } + + drawVerticalDivider(canvas, dividerLeft, flexLine.mTop, + flexLine.mCrossSize); + } + } + currentViewIndex++; + } + + // Judge if the beginning or middle dividers are needed before the flex line + if (hasDividerBeforeFlexLine(i)) { + int horizontalDividerTop; + if (fromBottomToTop) { + horizontalDividerTop = flexLine.mBottom; + } else { + horizontalDividerTop = flexLine.mTop - mDividerHorizontalHeight; + } + drawHorizontalDivider(canvas, paddingLeft, horizontalDividerTop, + horizontalDividerLength); + } + // Judge if the end divider is needed before the flex line + if (hasEndDividerAfterFlexLine(i)) { + if ((mShowDividerHorizontal & SHOW_DIVIDER_END) > 0) { + int horizontalDividerTop; + if (fromBottomToTop) { + horizontalDividerTop = flexLine.mTop - mDividerHorizontalHeight; + } else { + horizontalDividerTop = flexLine.mBottom; + } + drawHorizontalDivider(canvas, paddingLeft, horizontalDividerTop, + horizontalDividerLength); + } + } + } + } + + /** + * Sub method for {@link #onDraw(Canvas)} when the main axis direction is vertical + * ({@link #mFlexDirection} is either of {@link #FLEX_DIRECTION_COLUMN} or + * {@link #FLEX_DIRECTION_COLUMN_REVERSE}. + * + * @param canvas the canvas on which the background will be drawn + * @param isRtl {@code true} when the horizontal layout direction is right to left, + * {@code false} otherwise + * @param fromBottomToTop {@code true} when the vertical layout direction is bottom to top, + * {@code false} otherwise + */ + private void drawDividersVertical(Canvas canvas, boolean isRtl, boolean fromBottomToTop) { + int currentViewIndex = 0; + int paddingTop = getPaddingTop(); + int paddingBottom = getPaddingBottom(); + int verticalDividerLength = Math.max(0, getHeight() - paddingBottom - paddingTop); + for (int i = 0, size = mFlexLines.size(); i < size; i++) { + FlexLine flexLine = mFlexLines.get(i); + + // Draw horizontal dividers if needed + for (int j = 0; j < flexLine.mItemCount; j++) { + View view = getReorderedChildAt(currentViewIndex); + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + + // Judge if the beginning or middle divider is needed + if (hasDividerBeforeChildAtAlongMainAxis(currentViewIndex, j)) { + int dividerTop; + if (fromBottomToTop) { + dividerTop = view.getBottom() + lp.bottomMargin; + } else { + dividerTop = view.getTop() - lp.topMargin - mDividerHorizontalHeight; + } + + drawHorizontalDivider(canvas, flexLine.mLeft, dividerTop, flexLine.mCrossSize); + } + + // Judge if the end divider is needed + if (j == flexLine.mItemCount - 1) { + if ((mShowDividerHorizontal & SHOW_DIVIDER_END) > 0) { + int dividerTop; + if (fromBottomToTop) { + dividerTop = view.getTop() - lp.topMargin - mDividerHorizontalHeight; + } else { + dividerTop = view.getBottom() + lp.bottomMargin; + } + + drawHorizontalDivider(canvas, flexLine.mLeft, dividerTop, + flexLine.mCrossSize); + } + } + currentViewIndex++; + } + + // Judge if the beginning or middle dividers are needed before the flex line + if (hasDividerBeforeFlexLine(i)) { + int verticalDividerLeft; + if (isRtl) { + verticalDividerLeft = flexLine.mRight; + } else { + verticalDividerLeft = flexLine.mLeft - mDividerVerticalWidth; + } + drawVerticalDivider(canvas, verticalDividerLeft, paddingTop, + verticalDividerLength); + } + if (hasEndDividerAfterFlexLine(i)) { + if ((mShowDividerVertical & SHOW_DIVIDER_END) > 0) { + int verticalDividerLeft; + if (isRtl) { + verticalDividerLeft = flexLine.mLeft - mDividerVerticalWidth; + } else { + verticalDividerLeft = flexLine.mRight; + } + drawVerticalDivider(canvas, verticalDividerLeft, paddingTop, + verticalDividerLength); + } + } + } + } + + private void drawVerticalDivider(Canvas canvas, int left, int top, int length) { + if (mDividerDrawableVertical == null) { + return; + } + mDividerDrawableVertical.setBounds(left, top, left + mDividerVerticalWidth, top + length); + mDividerDrawableVertical.draw(canvas); + } + + private void drawHorizontalDivider(Canvas canvas, int left, int top, int length) { + if (mDividerDrawableHorizontal == null) { + return; + } + mDividerDrawableHorizontal + .setBounds(left, top, left + length, top + mDividerHorizontalHeight); + mDividerDrawableHorizontal.draw(canvas); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof FlexboxLayout.LayoutParams; + } + + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new FlexboxLayout.LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams from) { + if (from instanceof FlexboxLayout.LayoutParams) + return new FlexboxLayout.LayoutParams((FlexboxLayout.LayoutParams)from); + + if (from instanceof CommonLayoutParams) + return new FlexboxLayout.LayoutParams((CommonLayoutParams)from); + + if (from instanceof FrameLayout.LayoutParams) + return new FlexboxLayout.LayoutParams((FrameLayout.LayoutParams)from); + + if (from instanceof ViewGroup.MarginLayoutParams) + return new FlexboxLayout.LayoutParams((ViewGroup.MarginLayoutParams)from); + + return new FlexboxLayout.LayoutParams(from); + } + + @FlexDirection + public int getFlexDirection() { + return mFlexDirection; + } + + public void setFlexDirection(@FlexDirection int flexDirection) { + if (mFlexDirection != flexDirection) { + mFlexDirection = flexDirection; + requestLayout(); + } + } + + @FlexWrap + public int getFlexWrap() { + return mFlexWrap; + } + + public void setFlexWrap(@FlexWrap int flexWrap) { + if (mFlexWrap != flexWrap) { + mFlexWrap = flexWrap; + requestLayout(); + } + } + + @JustifyContent + public int getJustifyContent() { + return mJustifyContent; + } + + public void setJustifyContent(@JustifyContent int justifyContent) { + if (mJustifyContent != justifyContent) { + mJustifyContent = justifyContent; + requestLayout(); + } + } + + @AlignItems + public int getAlignItems() { + return mAlignItems; + } + + public void setAlignItems(@AlignItems int alignItems) { + if (mAlignItems != alignItems) { + mAlignItems = alignItems; + requestLayout(); + } + } + + @AlignContent + public int getAlignContent() { + return mAlignContent; + } + + public void setAlignContent(@AlignContent int alignContent) { + if (mAlignContent != alignContent) { + mAlignContent = alignContent; + requestLayout(); + } + } + + /** + * @return the flex lines composing this flex container. This method returns an unmodifiable + * list. Thus any changes of the returned list are not supported. + */ + public List getFlexLines() { + return Collections.unmodifiableList(mFlexLines); + } + + /** + * @return the horizontal divider drawable that will divide each item. + * @see #setDividerDrawable(Drawable) + * @see #setDividerDrawableHorizontal(Drawable) + */ + public Drawable getDividerDrawableHorizontal() { + return mDividerDrawableHorizontal; + } + + /** + * @return the vertical divider drawable that will divide each item. + * @see #setDividerDrawable(Drawable) + * @see #setDividerDrawableVertical(Drawable) + */ + public Drawable getDividerDrawableVertical() { + return mDividerDrawableVertical; + } + + /** + * Set a drawable to be used as a divider between items. The drawable is used for both + * horizontal and vertical dividers. + * + * @param divider Drawable that will divide each item for both horizontally and vertically. + * @see #setShowDivider(int) + */ + public void setDividerDrawable(Drawable divider) { + setDividerDrawableHorizontal(divider); + setDividerDrawableVertical(divider); + } + + /** + * Set a drawable to be used as a horizontal divider between items. + * + * @param divider Drawable that will divide each item. + * @see #setDividerDrawable(Drawable) + * @see #setShowDivider(int) + * @see #setShowDividerHorizontal(int) + */ + public void setDividerDrawableHorizontal(Drawable divider) { + if (divider == mDividerDrawableHorizontal) { + return; + } + mDividerDrawableHorizontal = divider; + if (divider != null) { + mDividerHorizontalHeight = divider.getIntrinsicHeight(); + } else { + mDividerHorizontalHeight = 0; + } + setWillNotDrawFlag(); + requestLayout(); + } + + /** + * Set a drawable to be used as a vertical divider between items. + * + * @param divider Drawable that will divide each item. + * @see #setDividerDrawable(Drawable) + * @see #setShowDivider(int) + * @see #setShowDividerVertical(int) + */ + public void setDividerDrawableVertical(Drawable divider) { + if (divider == mDividerDrawableVertical) { + return; + } + mDividerDrawableVertical = divider; + if (divider != null) { + mDividerVerticalWidth = divider.getIntrinsicWidth(); + } else { + mDividerVerticalWidth = 0; + } + setWillNotDrawFlag(); + requestLayout(); + } + + @FlexboxLayout.DividerMode + public int getShowDividerVertical() { + return mShowDividerVertical; + } + + @FlexboxLayout.DividerMode + public int getShowDividerHorizontal() { + return mShowDividerHorizontal; + } + + /** + * Set how dividers should be shown between items in this layout. This method sets the + * divider mode for both horizontally and vertically. + * + * @param dividerMode One or more of {@link #SHOW_DIVIDER_BEGINNING}, + * {@link #SHOW_DIVIDER_MIDDLE}, or {@link #SHOW_DIVIDER_END}, + * or {@link #SHOW_DIVIDER_NONE} to show no dividers. + * @see #setShowDividerVertical(int) + * @see #setShowDividerHorizontal(int) + */ + public void setShowDivider(@DividerMode int dividerMode) { + setShowDividerVertical(dividerMode); + setShowDividerHorizontal(dividerMode); + } + + /** + * Set how vertical dividers should be shown between items in this layout + * + * @param dividerMode One or more of {@link #SHOW_DIVIDER_BEGINNING}, + * {@link #SHOW_DIVIDER_MIDDLE}, or {@link #SHOW_DIVIDER_END}, + * or {@link #SHOW_DIVIDER_NONE} to show no dividers. + * @see #setShowDivider(int) + */ + public void setShowDividerVertical(@DividerMode int dividerMode) { + if (dividerMode != mShowDividerVertical) { + mShowDividerVertical = dividerMode; + requestLayout(); + } + } + + /** + * Set how horizontal dividers should be shown between items in this layout. + * + * @param dividerMode One or more of {@link #SHOW_DIVIDER_BEGINNING}, + * {@link #SHOW_DIVIDER_MIDDLE}, or {@link #SHOW_DIVIDER_END}, + * or {@link #SHOW_DIVIDER_NONE} to show no dividers. + * @see #setShowDivider(int) + */ + public void setShowDividerHorizontal(@DividerMode int dividerMode) { + if (dividerMode != mShowDividerHorizontal) { + mShowDividerHorizontal = dividerMode; + requestLayout(); + } + } + + private void setWillNotDrawFlag() { + if (mDividerDrawableHorizontal == null && mDividerDrawableVertical == null) { + setWillNotDraw(true); + } else { + setWillNotDraw(false); + } + } + + /** + * Check if a divider is needed before the view whose indices are passed as arguments. + * + * @param childAbsoluteIndex the absolute index of the view to be judged + * @param childRelativeIndexInFlexLine the relative index in the flex line where the view + * belongs + * @return {@code true} if a divider is needed, {@code false} otherwise + */ + private boolean hasDividerBeforeChildAtAlongMainAxis(int childAbsoluteIndex, + int childRelativeIndexInFlexLine) { + if (allViewsAreGoneBefore(childAbsoluteIndex, childRelativeIndexInFlexLine)) { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + return (mShowDividerVertical & SHOW_DIVIDER_BEGINNING) != 0; + } else { + return (mShowDividerHorizontal & SHOW_DIVIDER_BEGINNING) != 0; + } + } else { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + return (mShowDividerVertical & SHOW_DIVIDER_MIDDLE) != 0; + } else { + return (mShowDividerHorizontal & SHOW_DIVIDER_MIDDLE) != 0; + } + } + } + + private boolean allViewsAreGoneBefore(int childAbsoluteIndex, + int childRelativeIndexInFlexLine) { + for (int i = 1; i <= childRelativeIndexInFlexLine; i++) { + View view = getReorderedChildAt(childAbsoluteIndex - i); + if (view != null && view.getVisibility() != View.GONE) { + return false; + } + } + return true; + } + + /** + * Check if a divider is needed before the flex line whose index is passed as an argument. + * + * @param flexLineIndex the index of the flex line to be checked + * @return {@code true} if a divider is needed, {@code false} otherwise + */ + private boolean hasDividerBeforeFlexLine(int flexLineIndex) { + if (flexLineIndex < 0 || flexLineIndex >= mFlexLines.size()) { + return false; + } + if (allFlexLinesAreDummyBefore(flexLineIndex)) { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + return (mShowDividerHorizontal & SHOW_DIVIDER_BEGINNING) != 0; + } else { + return (mShowDividerVertical & SHOW_DIVIDER_BEGINNING) != 0; + } + } else { + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + return (mShowDividerHorizontal & SHOW_DIVIDER_MIDDLE) != 0; + } else { + return (mShowDividerVertical & SHOW_DIVIDER_MIDDLE) != 0; + } + } + } + + private boolean allFlexLinesAreDummyBefore(int flexLineIndex) { + for (int i = 0; i < flexLineIndex; i++) { + if (mFlexLines.get(i).mItemCount > 0) { + return false; + } + } + return true; + } + + /** + * Check if a end divider is needed after the flex line whose index is passed as an argument. + * + * @param flexLineIndex the index of the flex line to be checked + * @return {@code true} if a divider is needed, {@code false} otherwise + */ + private boolean hasEndDividerAfterFlexLine(int flexLineIndex) { + if (flexLineIndex < 0 || flexLineIndex >= mFlexLines.size()) { + return false; + } + + for (int i = flexLineIndex + 1; i < mFlexLines.size(); i++) { + if (mFlexLines.get(i).mItemCount > 0) { + return false; + } + } + if (isMainAxisDirectionHorizontal(mFlexDirection)) { + return (mShowDividerHorizontal & SHOW_DIVIDER_END) != 0; + } else { + return (mShowDividerVertical & SHOW_DIVIDER_END) != 0; + } + + } + + /** + * Per child parameters for children views of the {@link FlexboxLayout}. + */ + public static class LayoutParams extends CommonLayoutParams { + + private static final int ORDER_DEFAULT = 1; + + private static final float FLEX_GROW_DEFAULT = 0f; + + private static final float FLEX_SHRINK_DEFAULT = 1f; + + public static final float FLEX_BASIS_PERCENT_DEFAULT = -1f; + + public static final int ALIGN_SELF_AUTO = -1; + + public static final int ALIGN_SELF_FLEX_START = ALIGN_ITEMS_FLEX_START; + + public static final int ALIGN_SELF_FLEX_END = ALIGN_ITEMS_FLEX_END; + + public static final int ALIGN_SELF_CENTER = ALIGN_ITEMS_CENTER; + + public static final int ALIGN_SELF_BASELINE = ALIGN_ITEMS_BASELINE; + + public static final int ALIGN_SELF_STRETCH = ALIGN_ITEMS_STRETCH; + + private static final int MAX_SIZE = Integer.MAX_VALUE & ViewCompat.MEASURED_SIZE_MASK; + + /** + * This attribute can change the ordering of the children views are laid out. + * By default, children are displayed and laid out in the same order as they appear in the + * layout XML. If not specified, {@link #ORDER_DEFAULT} is set as a default value. + */ + public int order = ORDER_DEFAULT; + + /** + * This attribute determines how much this child will grow if positive free space is + * distributed relative to the rest of other flex items included in the same flex line. + * If not specified, {@link #FLEX_GROW_DEFAULT} is set as a default value. + */ + public float flexGrow = FLEX_GROW_DEFAULT; + + /** + * This attributes determines how much this child will shrink is negative free space is + * distributed relative to the rest of other flex items included in the same flex line. + * If not specified, {@link #FLEX_SHRINK_DEFAULT} is set as a default value. + */ + public float flexShrink = FLEX_SHRINK_DEFAULT; + + /** + * This attributes determines the alignment along the cross axis (perpendicular to the + * main axis). The alignment in the same direction can be determined by the + * {@link #mAlignItems} in the parent, but if this is set to other than + * {@link #ALIGN_SELF_AUTO}, the cross axis alignment is overridden for this child. + * The value needs to be one of the values in ({@link #ALIGN_SELF_AUTO}, + * {@link #ALIGN_SELF_STRETCH}, {@link #ALIGN_SELF_FLEX_START}, {@link + * #ALIGN_SELF_FLEX_END}, {@link #ALIGN_SELF_CENTER}, or {@link #ALIGN_SELF_BASELINE}). + * If not specified, {@link #ALIGN_SELF_AUTO} is set as a default value. + */ + public int alignSelf = ALIGN_SELF_AUTO; + + /** + * The initial flex item length in a fraction format relative to its parent. + * The initial main size of this child View is trying to be expanded as the specified + * fraction against the parent main size. + * If this value is set, the length specified from layout_width + * (or layout_height) is overridden by the calculated value from this attribute. + * This attribute is only effective when the parent's MeasureSpec mode is + * MeasureSpec.EXACTLY. The default value is {@link #FLEX_BASIS_PERCENT_DEFAULT}, which + * means not set. + */ + public float flexBasisPercent = FLEX_BASIS_PERCENT_DEFAULT; + + /** + * This attribute determines the minimum width the child can shrink to. + */ + public int minWidth; + + /** + * This attribute determines the minimum height the child can shrink to. + */ + public int minHeight; + + /** + * This attribute determines the maximum width the child can expand to. + */ + public int maxWidth = MAX_SIZE; + + /** + * This attribute determines the maximum height the child can expand to. + */ + public int maxHeight = MAX_SIZE; + + /** + * This attribute forces a flex line wrapping. i.e. if this is set to {@code true} for a + * flex item, the item will become the first item of the new flex line. (A wrapping happens + * regardless of the flex items being processed in the the previous flex line) + * This attribute is ignored if the flex_wrap attribute is set as nowrap. + * The equivalent attribute isn't defined in the original CSS Flexible Box Module + * specification, but having this attribute is useful for Android developers to flatten + * the layouts when building a grid like layout or for a situation where developers want + * to put a new flex line to make a semantic difference from the previous one, etc. + */ + public boolean wrapBefore; + + public LayoutParams() { + super(); + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + + public LayoutParams(FrameLayout.LayoutParams source) { + super(source); + } + + public LayoutParams(CommonLayoutParams source) { + super(source); + } + + public LayoutParams(LayoutParams source) { + super(source); + + this.order = source.order; + this.flexGrow = source.flexGrow; + this.flexShrink = source.flexShrink; + this.wrapBefore = source.wrapBefore; + this.alignSelf = source.alignSelf; + } + } + + /** + * A class that is used for calculating the view order which view's indices and order + * properties from Flexbox are taken into account. + */ + private static class Order implements Comparable { + + /** {@link View}'s index */ + int index; + + /** order property in the Flexbox */ + int order; + + @Override + public int compareTo(@NonNull Order another) { + if (order != another.order) { + return order - another.order; + } + return index - another.index; + } + + @Override + public String toString() { + return "Order{" + + "order=" + order + + ", index=" + index + + '}'; + } + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java new file mode 100644 index 000000000..66873bde9 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/FragmentBase.java @@ -0,0 +1,36 @@ +package org.nativescript.widgets; + +import android.animation.Animator; +import android.support.v4.app.Fragment; + +public abstract class FragmentBase extends Fragment { + + @Override + public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { + // [nested frames / fragments] apply dummy animator to the nested fragment with + // the same duration as the exit animator of the removing parent fragment to work around + // https://code.google.com/p/android/issues/detail?id=55228 (child fragments disappear + // when parent fragment is removed as all children are first removed from parent) + if (!enter) { + Fragment removingParentFragment = this.getRemovingParentFragment(); + if (removingParentFragment != null) { + Animator parentAnimator = removingParentFragment.onCreateAnimator(transit, enter, AnimatorHelper.exitFakeResourceId); + if (parentAnimator != null) { + long duration = AnimatorHelper.getTotalDuration(parentAnimator); + return AnimatorHelper.createDummyAnimator(duration); + } + } + } + + return super.onCreateAnimator(transit, enter, nextAnim); + } + + public Fragment getRemovingParentFragment() { + Fragment parentFragment = this.getParentFragment(); + while (parentFragment != null && !parentFragment.isRemoving()) { + parentFragment = parentFragment.getParentFragment(); + } + + return parentFragment; + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/GridLayout.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/GridLayout.java new file mode 100644 index 000000000..1dde9c277 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/GridLayout.java @@ -0,0 +1,1102 @@ +/** + * + */ +package org.nativescript.widgets; + +import java.util.ArrayList; +import java.util.HashMap; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.View.MeasureSpec; + +/** + * @author hhristov + */ +public class GridLayout extends LayoutBase { + + private MeasureHelper helper = new MeasureHelper(this); + + private ArrayList _rows = new ArrayList(); + private ArrayList _cols = new ArrayList(); + private ArrayList columnOffsets = new ArrayList(); + private ArrayList rowOffsets = new ArrayList(); + private HashMap map = new HashMap(); + + public GridLayout(Context context) { + super(context); + } + + private static void validateItemSpec(ItemSpec itemSpec) { + if (itemSpec == null) { + throw new Error("itemSpec is null."); + } + + if (itemSpec.owner != null) { + throw new Error("itemSpec is already added to GridLayout."); + } + } + + public void addRow(ItemSpec itemSpec) { + validateItemSpec(itemSpec); + itemSpec.owner = this; + this._rows.add(itemSpec); + + ItemGroup rowGroup = new ItemGroup(itemSpec); + this.helper.rows.add(rowGroup); + + this.requestLayout(); + } + + public void addColumn(ItemSpec itemSpec) { + validateItemSpec(itemSpec); + itemSpec.owner = this; + this._cols.add(itemSpec); + + ItemGroup columnGroup = new ItemGroup(itemSpec); + this.helper.columns.add(columnGroup); + + this.requestLayout(); + } + + public void removeColumn(ItemSpec itemSpec) { + if (itemSpec == null) { + throw new Error("itemSpec is null."); + } + + int index = this._cols.indexOf(itemSpec); + if (itemSpec.owner != this || index < 0) { + throw new Error("itemSpec is not child of this GridLayout"); + } + + this.removeColumnAt(index); + } + + public void removeColumnAt(int index) { + this._cols.remove(index); + this.helper.columns.get(index).children.clear(); + this.helper.columns.remove(index); + this.requestLayout(); + } + + public void removeRow(ItemSpec itemSpec) { + if (itemSpec == null) { + throw new Error("itemSpec is null."); + } + + int index = this._rows.indexOf(itemSpec); + if (itemSpec.owner != this || index < 0) { + throw new Error("itemSpec is not child of this GridLayout"); + } + + this.removeRowAt(index); + } + + public void removeRowAt(int index) { + this._rows.remove(index); + this.helper.rows.get(index).children.clear(); + this.helper.rows.remove(index); + this.requestLayout(); + } + + public ItemSpec[] getColumns() { + ItemSpec copy[] = new ItemSpec[this._cols.size()]; + copy = this._cols.toArray(copy); + return copy; + } + + public ItemSpec[] getRows() { + ItemSpec copy[] = new ItemSpec[this._rows.size()]; + copy = this._rows.toArray(copy); + return copy; + } + + @Override + public void addView(View child) { + super.addView(child); + this.addToMap(child); + } + + @Override + public void addView(View child, int index) { + super.addView(child, index); + this.addToMap(child); + } + + @Override + public void addView(View child, LayoutParams params) { + super.addView(child, params); + this.addToMap(child); + } + + @Override + public void removeView(View view) { + this.removeFromMap(view); + super.removeView(view); + } + + @Override + public void removeViewAt(int index) { + View view = this.getChildAt(index); + this.removeFromMap(view); + super.removeViewAt(index); + } + + @Override + public void removeViews(int start, int count) { + int end = start + count; + for (int i = start; i < end; i++) { + View view = this.getChildAt(i); + this.removeFromMap(view); + } + + super.removeViews(start, count); + } + + private int getColumnIndex(CommonLayoutParams lp) { + return Math.max(0, Math.min(lp.column, this._cols.size() - 1)); + } + + private int getRowIndex(CommonLayoutParams lp) { + return Math.max(0, Math.min(lp.row, this._rows.size() - 1)); + } + + private ItemSpec getColumnSpec(CommonLayoutParams lp) { + if (this._cols.size() == 0) { + return this.helper.singleColumn; + } + + int columnIndex = Math.min(lp.column, this._cols.size() - 1); + return this._cols.get(columnIndex); + } + + private ItemSpec getRowSpec(CommonLayoutParams lp) { + if (this._rows.size() == 0) { + return this.helper.singleRow; + } + + int rowIndex = Math.min(lp.row, this._rows.size() - 1); + return this._rows.get(rowIndex); + } + + private int getColumnSpan(CommonLayoutParams lp, int columnIndex) { + if (this._cols.size() == 0) { + return 1; + } + + return Math.min(lp.columnSpan, this._cols.size() - columnIndex); + } + + private int getRowSpan(CommonLayoutParams lp, int rowIndex) { + if (this._rows.size() == 0) { + return 1; + } + + return Math.min(lp.rowSpan, this._rows.size() - rowIndex); + } + + private void updateMeasureSpecs(View child, MeasureSpecs measureSpec) { + CommonLayoutParams lp = (CommonLayoutParams) child.getLayoutParams(); + int columnIndex = this.getColumnIndex(lp); + ItemSpec column = this.getColumnSpec(lp); + int rowIndex = this.getRowIndex(lp); + ItemSpec row = this.getRowSpec(lp); + int columnSpan = this.getColumnSpan(lp, columnIndex); + int rowSpan = this.getRowSpan(lp, rowIndex); + + measureSpec.setColumn(column); + measureSpec.setRow(row); + measureSpec.setColumnIndex(columnIndex); + measureSpec.setRowIndex(rowIndex); + measureSpec.setColumnSpan(columnSpan); + measureSpec.setRowSpan(rowSpan); + measureSpec.autoColumnsCount = 0; + measureSpec.autoRowsCount = 0; + measureSpec.measured = false; + measureSpec.pixelHeight = 0; + measureSpec.pixelWidth = 0; + measureSpec.starColumnsCount = 0; + measureSpec.starRowsCount = 0; + } + + private void addToMap(View child) { + MeasureSpecs measureSpec = new MeasureSpecs(child); + this.map.put(child, measureSpec); + } + + private void removeFromMap(View child) { + this.map.get(child).child = null; + this.map.remove(child); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int measureWidth = 0; + int measureHeight = 0; + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + boolean infinityWidth = widthMode == MeasureSpec.UNSPECIFIED; + boolean infinityHeight = heightMode == MeasureSpec.UNSPECIFIED; + + this.helper.width = Math.max(0, width - horizontalPadding); + this.helper.height = Math.max(0, height - verticalPadding); + + int gravity = LayoutBase.getGravity(this); + int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + final int layoutDirection = this.getLayoutDirection(); + final int horizontalGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection) & Gravity.HORIZONTAL_GRAVITY_MASK; + + this.helper.stretchedHorizontally = widthMode == MeasureSpec.EXACTLY || (horizontalGravity == Gravity.FILL_HORIZONTAL && !infinityWidth); + this.helper.stretchedVertically = heightMode == MeasureSpec.EXACTLY || (verticalGravity == Gravity.FILL_VERTICAL && !infinityHeight); + + this.helper.setInfinityWidth(infinityWidth); + this.helper.setInfinityHeight(infinityHeight); + + this.helper.clearMeasureSpecs(); + this.helper.init(); + + for (int i = 0, count = this.getChildCount(); i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + MeasureSpecs measureSpecs = this.map.get(child); + this.updateMeasureSpecs(child, measureSpecs); + this.helper.addMeasureSpec(measureSpecs); + } + + this.helper.measure(); + + // Add in our padding + measureWidth = this.helper.measuredWidth + horizontalPadding; + measureHeight = this.helper.measuredHeight + verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + int paddingLeft = this.getPaddingLeft(); + int paddingTop = this.getPaddingTop(); + + this.columnOffsets.clear(); + this.rowOffsets.clear(); + + this.columnOffsets.add(paddingLeft); + this.rowOffsets.add(paddingTop); + + float offset = this.columnOffsets.get(0); + int roundedOffset = paddingLeft; + int roundedLength = 0; + float actualLength = 0; + int size = this.helper.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.helper.columns.get(i); + offset += columnGroup.length; + + actualLength = offset - roundedOffset; + roundedLength = Math.round(actualLength); + columnGroup.rowOrColumn._actualLength = roundedLength; + roundedOffset += roundedLength; + + this.columnOffsets.add(roundedOffset); + } + + offset = this.rowOffsets.get(0); + roundedOffset = this.getPaddingTop(); + roundedLength = 0; + actualLength = 0; + size = this.helper.rows.size(); + for (int i = 0; i < size; i++) { + ItemGroup rowGroup = this.helper.rows.get(i); + offset += rowGroup.length; + + actualLength = offset - roundedOffset; + roundedLength = Math.round(actualLength); + rowGroup.rowOrColumn._actualLength = roundedLength; + roundedOffset += roundedLength; + + this.rowOffsets.add(roundedOffset); + } + + int columns = this.helper.columns.size(); + for (int i = 0; i < columns; i++) { + ItemGroup columnGroup = this.helper.columns.get(i); + int children = columnGroup.children.size(); + for (int j = 0; j < children; j++) { + + MeasureSpecs measureSpec = columnGroup.children.get(j); + int childLeft = this.columnOffsets.get(measureSpec.getColumnIndex()); + int childRight = this.columnOffsets.get(measureSpec.getColumnIndex() + measureSpec.getColumnSpan()); + int childTop = this.rowOffsets.get(measureSpec.getRowIndex()); + int childBottom = this.rowOffsets.get(measureSpec.getRowIndex() + measureSpec.getRowSpan()); + + // No need to include margins in the width, height + CommonLayoutParams.layoutChild(measureSpec.child, childLeft, childTop, childRight, childBottom); + } + } + + CommonLayoutParams.restoreOriginalParams(this); + } +} + +class MeasureSpecs { + + private int _columnSpan = 1; + private int _rowSpan = 1; + + public int pixelWidth = 0; + public int pixelHeight = 0; + + public int starColumnsCount = 0; + public int starRowsCount = 0; + + public int autoColumnsCount = 0; + public int autoRowsCount = 0; + + public boolean measured = false; + + public View child; + private ItemSpec column; + private ItemSpec row; + private int columnIndex; + private int rowIndex; + + MeasureSpecs(View child) { + this.child = child; + } + + public boolean getSpanned() { + return this._columnSpan > 1 || this._rowSpan > 1; + } + + public boolean getIsStar() { + return this.starRowsCount > 0 || this.starColumnsCount > 0; + } + + public int getColumnSpan() { + return this._columnSpan; + } + + public int getRowSpan() { + return this._rowSpan; + } + + public void setRowSpan(int value) { + // cannot have zero rowSpan. + this._rowSpan = Math.max(1, value); + } + + public void setColumnSpan(int value) { + // cannot have zero colSpan. + this._columnSpan = Math.max(1, value); + } + + public int getRowIndex() { + return this.rowIndex; + } + + public int getColumnIndex() { + return this.columnIndex; + } + + public void setRowIndex(int value) { + this.rowIndex = value; + } + + public void setColumnIndex(int value) { + this.columnIndex = value; + } + + public ItemSpec getRow() { + return this.row; + } + + public ItemSpec getColumn() { + return this.column; + } + + public void setRow(ItemSpec value) { + this.row = value; + } + + public void setColumn(ItemSpec value) { + this.column = value; + } +} + +class ItemGroup { + int length = 0; + int measuredCount = 0; + ItemSpec rowOrColumn; + ArrayList children = new ArrayList(); + + public int measureToFix = 0; + public int currentMeasureToFixCount = 0; + private boolean infinityLength = false; + + ItemGroup(ItemSpec spec) { + this.rowOrColumn = spec; + } + + public void setIsLengthInfinity(boolean infinityLength) { + this.infinityLength = infinityLength; + } + + public void init() { + this.measuredCount = 0; + this.currentMeasureToFixCount = 0; + this.length = this.rowOrColumn.getIsAbsolute() ? this.rowOrColumn.getValue() : 0; + } + + public boolean getAllMeasured() { + return this.measuredCount == this.children.size(); + } + + public boolean getCanBeFixed() { + return this.currentMeasureToFixCount == this.measureToFix; + } + + public boolean getIsAuto() { + return this.rowOrColumn.getIsAuto() || (this.rowOrColumn.getIsStar() && this.infinityLength); + } + + public boolean getIsStar() { + return this.rowOrColumn.getIsStar() && !this.infinityLength; + } + + public boolean getIsAbsolute() { + return this.rowOrColumn.getIsAbsolute(); + } +} + +class MeasureHelper { + public final ItemSpec singleRow = new ItemSpec(); + public final ItemSpec singleColumn = new ItemSpec(); + public final GridLayout grid; + + static int infinity = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + ArrayList rows = new ArrayList(); + ArrayList columns = new ArrayList(); + + public int width; + public int height; + public boolean stretchedHorizontally = false; + public boolean stretchedVertically = false; + + private boolean infinityWidth = false; + private boolean infinityHeight = false; + + private float minColumnStarValue; + private float maxColumnStarValue; + + private float minRowStarValue; + private float maxRowStarValue; + + int measuredWidth; + int measuredHeight; + + private boolean fakeRowAdded = false; + private boolean fakeColumnAdded = false; + + MeasureHelper(GridLayout grid) { + this.grid = grid; + } + + public void setInfinityWidth(boolean value) { + this.infinityWidth = value; + + for (int i = 0, size = this.columns.size(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + columnGroup.setIsLengthInfinity(value); + } + } + + public void setInfinityHeight(boolean value) { + this.infinityHeight = value; + + for (int i = 0, size = this.rows.size(); i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + rowGroup.setIsLengthInfinity(value); + } + } + + public void addMeasureSpec(MeasureSpecs measureSpec) { + // Get column stats + int size = measureSpec.getColumnIndex() + measureSpec.getColumnSpan(); + for (int i = measureSpec.getColumnIndex(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + measureSpec.autoColumnsCount++; + } else if (columnGroup.getIsStar()) { + measureSpec.starColumnsCount += columnGroup.rowOrColumn.getValue(); + } else if (columnGroup.getIsAbsolute()) { + measureSpec.pixelWidth += columnGroup.rowOrColumn.getValue(); + } + } + + if (measureSpec.autoColumnsCount > 0 && measureSpec.starColumnsCount == 0) { + // Determine which auto columns are affected by this element + for (int i = measureSpec.getColumnIndex(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + columnGroup.measureToFix++; + } + } + } + + // Get row stats + size = measureSpec.getRowIndex() + measureSpec.getRowSpan(); + for (int i = measureSpec.getRowIndex(); i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + measureSpec.autoRowsCount++; + } else if (rowGroup.getIsStar()) { + measureSpec.starRowsCount += rowGroup.rowOrColumn.getValue(); + } else if (rowGroup.getIsAbsolute()) { + measureSpec.pixelHeight += rowGroup.rowOrColumn.getValue(); + } + } + + if (measureSpec.autoRowsCount > 0 && measureSpec.starRowsCount == 0) { + // Determine which auto rows are affected by this element + for (int i = measureSpec.getRowIndex(); i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.measureToFix++; + } + } + } + + this.columns.get(measureSpec.getColumnIndex()).children.add(measureSpec); + this.rows.get(measureSpec.getRowIndex()).children.add(measureSpec); + } + + public void clearMeasureSpecs() { + for (int i = 0, size = this.columns.size(); i < size; i++) { + this.columns.get(i).children.clear(); + } + + for (int i = 0, size = this.rows.size(); i < size; i++) { + this.rows.get(i).children.clear(); + } + } + + private static void initList(ArrayList list) { + for (int i = 0, size = list.size(); i < size; i++) { + ItemGroup item = list.get(i); + item.init(); + } + } + + private ItemGroup singleRowGroup = new ItemGroup(singleRow); + private ItemGroup singleColumnGroup = new ItemGroup(singleColumn); + + void init() { + + int rows = this.rows.size(); + if (rows == 0) { + singleRowGroup.setIsLengthInfinity(this.infinityHeight); + this.rows.add(singleRowGroup); + this.fakeRowAdded = true; + } else if (rows > 1 && this.fakeRowAdded) { + this.rows.remove(0); + this.fakeRowAdded = false; + } + + int cols = this.columns.size(); + if (cols == 0) { + this.fakeColumnAdded = true; + singleColumnGroup.setIsLengthInfinity(this.infinityWidth); + this.columns.add(singleColumnGroup); + } else if (cols > 1 && this.fakeColumnAdded) { + this.columns.remove(0); + this.fakeColumnAdded = false; + } + + initList(this.rows); + initList(this.columns); + + this.minColumnStarValue = -1; + this.minRowStarValue = -1; + this.maxColumnStarValue = -1; + this.maxRowStarValue = -1; + } + + private void itemMeasured(MeasureSpecs measureSpec, boolean isFakeMeasure) { + if (!isFakeMeasure) { + this.columns.get(measureSpec.getColumnIndex()).measuredCount++; + this.rows.get(measureSpec.getRowIndex()).measuredCount++; + measureSpec.measured = true; + } + + if (measureSpec.autoColumnsCount > 0 && measureSpec.starColumnsCount == 0) { + int size = measureSpec.getColumnIndex() + measureSpec.getColumnSpan(); + for (int i = measureSpec.getColumnIndex(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + columnGroup.currentMeasureToFixCount++; + } + } + } + + if (measureSpec.autoRowsCount > 0 && measureSpec.starRowsCount == 0) { + int size = measureSpec.getRowIndex() + measureSpec.getRowSpan(); + for (int i = measureSpec.getRowIndex(); i < size; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.currentMeasureToFixCount++; + } + } + } + } + + private void fixColumns() { + int currentColumnWidth = 0; + int columnStarCount = 0; + + int columnCount = this.columns.size(); + for (int i = 0; i < columnCount; i++) { + ItemGroup item = this.columns.get(i); + if (item.rowOrColumn.getIsStar()) { + columnStarCount += item.rowOrColumn.getValue(); + } else { + // Star columns are still zeros (not calculated). + currentColumnWidth += item.length; + } + } + + float widthForStarColumns = Math.max(0, this.width - currentColumnWidth); + this.maxColumnStarValue = columnStarCount > 0 ? widthForStarColumns / columnStarCount : 0; + + updateStarLength(this.columns, this.maxColumnStarValue); + } + + private void fixRows() { + int currentRowHeight = 0; + int rowStarCount = 0; + + int rowCount = this.rows.size(); + for (int i = 0; i < rowCount; i++) { + ItemGroup item = this.rows.get(i); + if (item.rowOrColumn.getIsStar()) { + rowStarCount += item.rowOrColumn.getValue(); + } else { + // Star rows are still zeros (not calculated). + currentRowHeight += item.length; + } + } + + float heightForStarRows = Math.max(0, this.height - currentRowHeight); + this.maxRowStarValue = rowStarCount > 0 ? heightForStarRows / rowStarCount : 0; + + updateStarLength(this.rows, this.maxRowStarValue); + } + + private static void updateStarLength(ArrayList list, float starValue) { + float offset = 0; + int roundedOffset = 0; + for (int i = 0, rowCount = list.size(); i < rowCount; i++) { + ItemGroup item = list.get(i); + if (item.getIsStar()) { + offset += item.rowOrColumn.getValue() * starValue; + + float actualLength = offset - roundedOffset; + int roundedLength = Math.round(actualLength); + item.length = roundedLength; + roundedOffset += roundedLength; + } + } + } + + private void fakeMeasure() { + // Fake measure - measure all elements that have star rows and auto columns - with infinity Width and infinity Height + for (int i = 0, size = this.columns.size(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getAllMeasured()) { + continue; + } + + for (int j = 0, childrenCount = columnGroup.children.size(); j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.starRowsCount > 0 && measureSpec.autoColumnsCount > 0 && measureSpec.starColumnsCount == 0) { + this.measureChild(measureSpec, true); + } + } + } + } + + private void measureFixedColumnsNoStarRows() { + for (int i = 0, size = this.columns.size(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + for (int j = 0, childrenCount = columnGroup.children.size(); j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.starColumnsCount > 0 && measureSpec.starRowsCount == 0) { + this.measureChildFixedColumns(measureSpec); + } + } + } + } + + private void measureNoStarColumnsFixedRows() { + for (int i = 0, size = this.columns.size(); i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + for (int j = 0, childrenCount = columnGroup.children.size(); j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.starRowsCount > 0 && measureSpec.starColumnsCount == 0) { + this.measureChildFixedRows(measureSpec); + } + } + } + } + + private static boolean canFix(ArrayList list) { + for (int i = 0, size = list.size(); i < size; i++) { + ItemGroup item = list.get(i); + if (!item.getCanBeFixed()) { + return false; + } + } + + return true; + } + + private static int getMeasureLength(ArrayList list) { + int result = 0; + for (int i = 0, size = list.size(); i < size; i++) { + ItemGroup item = list.get(i); + result += item.length; + } + + return result; + } + + public void measure() { + + // Measure auto & pixel columns and rows (no spans). + int size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + for (int j = 0, childrenCount = columnGroup.children.size(); j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.getIsStar() || measureSpec.getSpanned()) { + continue; + } + + this.measureChild(measureSpec, false); + } + } + + // Measure auto & pixel columns and rows (with spans). + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + for (int j = 0, childrenCount = columnGroup.children.size(); j < childrenCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (measureSpec.getIsStar() || !measureSpec.getSpanned()) { + continue; + } + + this.measureChild(measureSpec, false); + } + } + + // try fix stars! + boolean fixColumns = canFix(this.columns); + boolean fixRows = canFix(this.rows); + + if (fixColumns) { + this.fixColumns(); + } + + if (fixRows) { + this.fixRows(); + } + + if (!fixColumns && !fixRows) { + // Fake measure - measure all elements that have star rows and auto columns - with infinity Width and infinity Height + // should be able to fix rows after that + this.fakeMeasure(); + + this.fixColumns(); + + // Measure all elements that have star columns(already fixed), but no stars at the rows + this.measureFixedColumnsNoStarRows(); + + this.fixRows(); + } else if (fixColumns && !fixRows) { + // Measure all elements that have star columns(already fixed) but no stars at the rows + this.measureFixedColumnsNoStarRows(); + + // Then + this.fixRows(); + } else if (!fixColumns && fixRows) { + // Measure all elements that have star rows(already fixed) but no star at the columns + this.measureNoStarColumnsFixedRows(); + + // Then + this.fixColumns(); + } + + // Rows and columns are fixed here - measure remaining elements + size = this.columns.size(); + for (int i = 0; i < size; i++) { + ItemGroup columnGroup = this.columns.get(i); + for (int j = 0, childCount = columnGroup.children.size(); j < childCount; j++) { + MeasureSpecs measureSpec = columnGroup.children.get(j); + if (!measureSpec.measured) { + this.measureChildFixedColumnsAndRows(measureSpec); + } + } + } + + // If we are not stretched and minColumnStarValue is less than maxColumnStarValue + // we need to reduce the length of star columns. + if (!this.stretchedHorizontally && this.minColumnStarValue != -1 && this.minColumnStarValue < this.maxColumnStarValue) { + updateStarLength(this.columns, this.minColumnStarValue); + } + + // If we are not stretched and minRowStarValue is less than maxRowStarValue + // we need to reduce the height of star maxRowStarValue. + if (!this.stretchedVertically && this.minRowStarValue != -1 && this.minRowStarValue < this.maxRowStarValue) { + updateStarLength(this.rows, this.minRowStarValue); + } + + this.measuredWidth = getMeasureLength(this.columns); + this.measuredHeight = getMeasureLength(this.rows); + } + + private void measureChild(MeasureSpecs measureSpec, boolean isFakeMeasure) { + int widthMeasureSpec = (measureSpec.autoColumnsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelWidth, MeasureSpec.EXACTLY); + int heightMeasureSpec = (isFakeMeasure || measureSpec.autoRowsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelHeight, MeasureSpec.EXACTLY); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + int columnSpanEnd = measureSpec.getColumnIndex() + measureSpec.getColumnSpan(); + int rowSpanEnd = measureSpec.getRowIndex() + measureSpec.getRowSpan(); + + if (measureSpec.autoColumnsCount > 0) { + int remainingSpace = childMeasuredWidth; + + for (int i = measureSpec.getColumnIndex(); i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + remainingSpace -= columnGroup.length; + } + + if (remainingSpace > 0) { + int growSize = remainingSpace / measureSpec.autoColumnsCount; + for (int i = measureSpec.getColumnIndex(); i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + columnGroup.length += growSize; + } + } + } + } + + if (!isFakeMeasure && measureSpec.autoRowsCount > 0) { + int remainingSpace = childMeasuredHeight; + + for (int i = measureSpec.getRowIndex(); i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + remainingSpace -= rowGroup.length; + } + + if (remainingSpace > 0) { + int growSize = remainingSpace / measureSpec.autoRowsCount; + for (int i = measureSpec.getRowIndex(); i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.length += growSize; + } + } + } + } + + this.itemMeasured(measureSpec, isFakeMeasure); + } + + private void measureChildFixedColumns(MeasureSpecs measureSpec) { + int columnIndex = measureSpec.getColumnIndex(); + int columnSpanEnd = columnIndex + measureSpec.getColumnSpan(); + int rowIndex = measureSpec.getRowIndex(); + int rowSpanEnd = rowIndex + measureSpec.getRowSpan(); + + int measureWidth = 0; + int growSize = 0; + + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + measureWidth += columnGroup.length; + } + + int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measureWidth, this.stretchedHorizontally ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST); + int heightMeasureSpec = (measureSpec.autoRowsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelHeight, MeasureSpec.EXACTLY); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + this.updateMinColumnStarValueIfNeeded(measureSpec, childMeasuredWidth); + + // Distribute height over auto rows + if (measureSpec.autoRowsCount > 0) { + int remainingSpace = childMeasuredHeight; + + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + remainingSpace -= rowGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.autoRowsCount; + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (rowGroup.getIsAuto()) { + rowGroup.length += growSize; + } + } + } + } + + this.itemMeasured(measureSpec, false); + } + + private void measureChildFixedRows(MeasureSpecs measureSpec) { + int columnIndex = measureSpec.getColumnIndex(); + int columnSpanEnd = columnIndex + measureSpec.getColumnSpan(); + int rowIndex = measureSpec.getRowIndex(); + int rowSpanEnd = rowIndex + measureSpec.getRowSpan(); + int measureHeight = 0; + + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + measureHeight += rowGroup.length; + } + + int widthMeasureSpec = (measureSpec.autoColumnsCount > 0) ? infinity : MeasureSpec.makeMeasureSpec(measureSpec.pixelWidth, MeasureSpec.EXACTLY); + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(measureHeight, this.stretchedVertically ? MeasureSpec.EXACTLY : MeasureSpec.AT_MOST); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + int remainingSpace = 0; + int growSize = 0; + + // Distribute width over auto columns + if (measureSpec.autoColumnsCount > 0) { + remainingSpace = childMeasuredWidth; + + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + remainingSpace -= columnGroup.length; + } + + if (remainingSpace > 0) { + growSize = remainingSpace / measureSpec.autoColumnsCount; + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (columnGroup.getIsAuto()) { + columnGroup.length += growSize; + } + } + } + } + + this.updateMinRowStarValueIfNeeded(measureSpec, childMeasuredHeight); + this.itemMeasured(measureSpec, false); + } + + private void measureChildFixedColumnsAndRows(MeasureSpecs measureSpec) { + int columnIndex = measureSpec.getColumnIndex(); + int columnSpanEnd = columnIndex + measureSpec.getColumnSpan(); + int rowIndex = measureSpec.getRowIndex(); + int rowSpanEnd = rowIndex + measureSpec.getRowSpan(); + + int measureWidth = 0; + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + measureWidth += columnGroup.length; + } + + int measureHeight = 0; + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + measureHeight += rowGroup.length; + } + + // if (have stars) & (not stretch) - at most + int widthMeasureSpec = MeasureSpec.makeMeasureSpec(measureWidth, + (measureSpec.starColumnsCount > 0 && !this.stretchedHorizontally) ? MeasureSpec.AT_MOST : MeasureSpec.EXACTLY); + + int heightMeasureSpec = MeasureSpec.makeMeasureSpec(measureHeight, + (measureSpec.starRowsCount > 0 && !this.stretchedVertically) ? MeasureSpec.AT_MOST : MeasureSpec.EXACTLY); + + CommonLayoutParams.measureChild(measureSpec.child, widthMeasureSpec, heightMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(measureSpec.child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(measureSpec.child); + + this.updateMinColumnStarValueIfNeeded(measureSpec, childMeasuredWidth); + this.updateMinRowStarValueIfNeeded(measureSpec, childMeasuredHeight); + this.itemMeasured(measureSpec, false); + } + + private void updateMinRowStarValueIfNeeded(MeasureSpecs measureSpec, int childMeasuredHeight) { + if (!this.stretchedVertically && measureSpec.starRowsCount > 0) { + int remainingSpace = childMeasuredHeight; + int rowIndex = measureSpec.getRowIndex(); + int rowSpanEnd = rowIndex + measureSpec.getRowSpan(); + for (int i = rowIndex; i < rowSpanEnd; i++) { + ItemGroup rowGroup = this.rows.get(i); + if (!rowGroup.getIsStar()) { + remainingSpace -= rowGroup.length; + } + } + + if (remainingSpace > 0) { + this.minRowStarValue = Math.max(remainingSpace / measureSpec.starRowsCount, this.minRowStarValue); + } + } + } + + private void updateMinColumnStarValueIfNeeded(MeasureSpecs measureSpec, int childMeasuredWidth) { + // When not stretched star columns are not fixed so we may grow them here + // if there is an element that spans on multiple columns + if (!this.stretchedHorizontally && measureSpec.starColumnsCount > 0) { + int remainingSpace = childMeasuredWidth; + int columnIndex = measureSpec.getColumnIndex(); + int columnSpanEnd = columnIndex + measureSpec.getColumnSpan(); + for (int i = columnIndex; i < columnSpanEnd; i++) { + ItemGroup columnGroup = this.columns.get(i); + if (!columnGroup.getIsStar()) { + remainingSpace -= columnGroup.length; + } + } + + if (remainingSpace > 0) { + this.minColumnStarValue = Math.max(remainingSpace / measureSpec.starColumnsCount, this.minColumnStarValue); + } + } + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/GridUnitType.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/GridUnitType.java new file mode 100644 index 000000000..f5293ad84 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/GridUnitType.java @@ -0,0 +1,14 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public enum GridUnitType { + auto, + pixel, + star +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/HorizontalScrollView.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/HorizontalScrollView.java new file mode 100644 index 000000000..c63489719 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/HorizontalScrollView.java @@ -0,0 +1,317 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.FrameLayout; + +/** + * @author hhristov + * + */ +public class HorizontalScrollView extends android.widget.HorizontalScrollView { + + private final Rect mTempRect = new Rect(); + + private int contentMeasuredWidth = 0; + private int contentMeasuredHeight = 0; + private int scrollableLength = 0; + private SavedState mSavedState; + private boolean isFirstLayout = true; + private boolean scrollEnabled = true; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + public HorizontalScrollView(Context context) { + super(context); + } + + public int getScrollableLength() { + return this.scrollableLength; + } + + public boolean getScrollEnabled() { + return this.scrollEnabled; + } + + public void setScrollEnabled(boolean value) { + this.scrollEnabled = value; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // Do nothing with intercepted touch events if we are not scrollable + if (!this.scrollEnabled) { + return false; + } + + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!this.scrollEnabled && ev.getAction() == MotionEvent.ACTION_DOWN) { + return false; + } + + return super.onTouchEvent(ev); + } + + @Override + public void requestLayout() { + this.mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected CommonLayoutParams generateDefaultLayoutParams() { + return new CommonLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + public CommonLayoutParams generateLayoutParams(AttributeSet attrs) { + return new CommonLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof CommonLayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams from) { + if (from instanceof CommonLayoutParams) + return new CommonLayoutParams((CommonLayoutParams)from); + + if (from instanceof FrameLayout.LayoutParams) + return new CommonLayoutParams((FrameLayout.LayoutParams)from); + + if (from instanceof ViewGroup.MarginLayoutParams) + return new CommonLayoutParams((ViewGroup.MarginLayoutParams)from); + + return new CommonLayoutParams(from); + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mIsLayoutDirty) { + this.scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + // Don't call measure because it will measure content twice. + // ScrollView is expected to have single child so we measure only the first child. + View child = this.getChildCount() > 0 ? this.getChildAt(0) : null; + if (child == null) { + this.scrollableLength = 0; + this.contentMeasuredWidth = 0; + this.contentMeasuredHeight = 0; + } + else { + CommonLayoutParams.measureChild(child, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightMeasureSpec); + this.contentMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + this.contentMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + // Android ScrollView does not account to child margins so we set them as paddings. Otherwise you can never scroll to bottom. + CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); + this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); + } + + // Don't add in our paddings because they are already added as child margins. (we will include them twice if we add them). + // Check the previous line - this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); + //this.contentMeasuredWidth += this.getPaddingLeft() + this.getPaddingRight(); + //this.contentMeasuredHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum height + this.contentMeasuredWidth = Math.max(this.contentMeasuredWidth, this.getSuggestedMinimumWidth()); + this.contentMeasuredHeight = Math.max(this.contentMeasuredHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(this.contentMeasuredWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(this.contentMeasuredHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childWidth = 0; + if (this.getChildCount() > 0) { + View child = this.getChildAt(0); + childWidth = child.getMeasuredWidth(); + + int width = right - left; + int height = bottom - top; + + this.scrollableLength = this.contentMeasuredWidth - width; + CommonLayoutParams.layoutChild(child, 0, 0, Math.max(this.contentMeasuredWidth, width), height); + this.scrollableLength = Math.max(0, this.scrollableLength); + } + + this.mIsLayoutDirty = false; + // Give a child focus if it needs it + if (this.mChildToScrollTo != null && isViewDescendantOf(this.mChildToScrollTo, this)) { + this.scrollToChild(this.mChildToScrollTo); + } + + this.mChildToScrollTo = null; + + int scrollX = this.getScrollX(); + int scrollY = this.getScrollY(); + + if (this.isFirstLayout) { + this.isFirstLayout = false; + + final int scrollRange = Math.max(0, childWidth - (right - left - this.getPaddingLeft() - this.getPaddingRight())); + if (this.mSavedState != null) { + scrollX = (this.isLayoutRtl() == mSavedState.isLayoutRtl) ? mSavedState.scrollPosition : (scrollRange - this.mSavedState.scrollPosition); + mSavedState = null; + } else { + if (this.isLayoutRtl()) { + scrollX = scrollRange - scrollX; + } // mScrollX default value is "0" for LTR + } + // Don't forget to clamp + if (scrollX > scrollRange) { + scrollX = scrollRange; + } else if (scrollX < 0) { + scrollX = 0; + } + } + + // Calling this with the present values causes it to re-claim them + this.scrollTo(scrollX, scrollY); + CommonLayoutParams.restoreOriginalParams(this); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + this.mSavedState = ss; + this.requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = this.getScrollX(); + ss.isLayoutRtl = this.isLayoutRtl(); + return ss; + } + + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) { + this.scrollBy(scrollDelta, 0); + } + } + + private boolean isLayoutRtl() { + return (this.getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } + + /** + * Return true if child is a descendant of parent, (or equal to the parent). + */ + static boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + static class SavedState extends BaseSavedState { + public int scrollPosition; + public boolean isLayoutRtl; + + SavedState(Parcelable superState) { + super(superState); + } + + public SavedState(Parcel source) { + super(source); + scrollPosition = source.readInt(); + isLayoutRtl = (source.readInt() == 0) ? true : false; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(scrollPosition); + dest.writeInt(isLayoutRtl ? 1 : 0); + } + + @Override + public String toString() { + return "HorizontalScrollView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " scrollPosition=" + scrollPosition + + " isLayoutRtl=" + isLayoutRtl + "}"; + } + + public static final Creator CREATOR + = new Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/AsyncTask.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/AsyncTask.java new file mode 100644 index 000000000..81ab98bf9 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/AsyncTask.java @@ -0,0 +1,693 @@ +/* + * Copyright (C) 2008 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 org.nativescript.widgets.image; + +import android.annotation.TargetApi; +import android.os.Handler; +import android.os.Message; +import android.os.Process; + +import java.util.ArrayDeque; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * ************************************* + * Copied from JB release framework: + * https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/AsyncTask.java + * + * so that threading behavior on all OS versions is the same and we can tweak behavior by using + * executeOnExecutor() if needed. + * + * There are 3 changes in this copy of AsyncTask: + * -pre-HC a single thread executor is used for serial operation + * (Executors.newSingleThreadExecutor) and is the default + * -the default THREAD_POOL_EXECUTOR was changed to use DiscardOldestPolicy + * -a new fixed thread pool called DUAL_THREAD_EXECUTOR was added + * ************************************* + * + *

AsyncTask enables proper and easy use of the UI thread. This class allows to + * perform background operations and publish results on the UI thread without + * having to manipulate threads and/or handlers.

+ * + *

AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler} + * and does not constitute a generic threading framework. AsyncTasks should ideally be + * used for short operations (a few seconds at the most.) If you need to keep threads + * running for long periods of time, it is highly recommended you use the various APIs + * provided by the java.util.concurrent pacakge such as {@link Executor}, + * {@link ThreadPoolExecutor} and {@link FutureTask}.

+ * + *

An asynchronous task is defined by a computation that runs on a background thread and + * whose result is published on the UI thread. An asynchronous task is defined by 3 generic + * types, called Params, Progress and Result, + * and 4 steps, called onPreExecute, doInBackground, + * onProgressUpdate and onPostExecute.

+ * + *
+ *

Developer Guides

+ *

For more information about using tasks and threads, read the + * Processes and + * Threads developer guide.

+ *
+ * + *

Usage

+ *

AsyncTask must be subclassed to be used. The subclass will override at least + * one method ({@link #doInBackground}), and most often will override a + * second one ({@link #onPostExecute}.)

+ * + *

Here is an example of subclassing:

+ *
+ * private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
+ *     protected Long doInBackground(URL... urls) {
+ *         int count = urls.length;
+ *         long totalSize = 0;
+ *         for (int i = 0; i < count; i++) {
+ *             totalSize += Downloader.downloadFile(urls[i]);
+ *             publishProgress((int) ((i / (float) count) * 100));
+ *             // Escape early if cancel() is called
+ *             if (isCancelled()) break;
+ *         }
+ *         return totalSize;
+ *     }
+ *
+ *     protected void onProgressUpdate(Integer... progress) {
+ *         setProgressPercent(progress[0]);
+ *     }
+ *
+ *     protected void onPostExecute(Long result) {
+ *         showDialog("Downloaded " + result + " bytes");
+ *     }
+ * }
+ * 
+ * + *

Once created, a task is executed very simply:

+ *
+ * new DownloadFilesTask().execute(url1, url2, url3);
+ * 
+ * + *

AsyncTask's generic types

+ *

The three types used by an asynchronous task are the following:

+ *
    + *
  1. Params, the type of the parameters sent to the task upon + * execution.
  2. + *
  3. Progress, the type of the progress units published during + * the background computation.
  4. + *
  5. Result, the type of the result of the background + * computation.
  6. + *
+ *

Not all types are always used by an asynchronous task. To mark a type as unused, + * simply use the type {@link Void}:

+ *
+ * private class MyTask extends AsyncTask<Void, Void, Void> { ... }
+ * 
+ * + *

The 4 steps

+ *

When an asynchronous task is executed, the task goes through 4 steps:

+ *
    + *
  1. {@link #onPreExecute()}, invoked on the UI thread immediately after the task + * is executed. This step is normally used to setup the task, for instance by + * showing a progress bar in the user interface.
  2. + *
  3. {@link #doInBackground}, invoked on the background thread + * immediately after {@link #onPreExecute()} finishes executing. This step is used + * to perform background computation that can take a long time. The parameters + * of the asynchronous task are passed to this step. The result of the computation must + * be returned by this step and will be passed back to the last step. This step + * can also use {@link #publishProgress} to publish one or more units + * of progress. These values are published on the UI thread, in the + * {@link #onProgressUpdate} step.
  4. + *
  5. {@link #onProgressUpdate}, invoked on the UI thread after a + * call to {@link #publishProgress}. The timing of the execution is + * undefined. This method is used to display any form of progress in the user + * interface while the background computation is still executing. For instance, + * it can be used to animate a progress bar or show logs in a text field.
  6. + *
  7. {@link #onPostExecute}, invoked on the UI thread after the background + * computation finishes. The result of the background computation is passed to + * this step as a parameter.
  8. + *
+ * + *

Cancelling a task

+ *

A task can be cancelled at any time by invoking {@link #cancel(boolean)}. Invoking + * this method will cause subsequent calls to {@link #isCancelled()} to return true. + * After invoking this method, {@link #onCancelled(Object)}, instead of + * {@link #onPostExecute(Object)} will be invoked after {@link #doInBackground(Object[])} + * returns. To ensure that a task is cancelled as quickly as possible, you should always + * check the return value of {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])}, if possible (inside a loop for instance.)

+ * + *

Threading rules

+ *

There are a few threading rules that must be followed for this class to + * work properly:

+ *
    + *
  • The AsyncTask class must be loaded on the UI thread. This is done + * automatically as of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}.
  • + *
  • The task instance must be created on the UI thread.
  • + *
  • {@link #execute} must be invoked on the UI thread.
  • + *
  • Do not call {@link #onPreExecute()}, {@link #onPostExecute}, + * {@link #doInBackground}, {@link #onProgressUpdate} manually.
  • + *
  • The task can be executed only once (an exception will be thrown if + * a second execution is attempted.)
  • + *
+ * + *

Memory observability

+ *

AsyncTask guarantees that all callback calls are synchronized in such a way that the following + * operations are safe without explicit synchronizations.

+ *
    + *
  • Set member fields in the constructor or {@link #onPreExecute}, and refer to them + * in {@link #doInBackground}. + *
  • Set member fields in {@link #doInBackground}, and refer to them in + * {@link #onProgressUpdate} and {@link #onPostExecute}. + *
+ * + *

Order of execution

+ *

When first introduced, AsyncTasks were executed serially on a single background + * thread. Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. Starting with + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are executed on a single + * thread to avoid common application errors caused by parallel execution.

+ *

If you truly want parallel execution, you can invoke + * {@link #executeOnExecutor(Executor, Object[])} with + * {@link #THREAD_POOL_EXECUTOR}.

+ */ +public abstract class AsyncTask { + private static final String LOG_TAG = "AsyncTask"; + + private static final int CORE_POOL_SIZE = 5; + private static final int MAXIMUM_POOL_SIZE = 128; + private static final int KEEP_ALIVE = 1; + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(r, "AsyncTask #" + mCount.getAndIncrement()); + } + }; + + private static final BlockingQueue sPoolWorkQueue = + new LinkedBlockingQueue(10); + + /** + * An {@link Executor} that can be used to execute tasks in parallel. + */ + public static final Executor THREAD_POOL_EXECUTOR + = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory, + new ThreadPoolExecutor.DiscardOldestPolicy()); + + /** + * An {@link Executor} that executes tasks one at a time in serial + * order. This serialization is global to a particular process. + */ + public static final Executor SERIAL_EXECUTOR = Utils.hasHoneycomb() ? new SerialExecutor() : + Executors.newSingleThreadExecutor(sThreadFactory); + + public static final Executor DUAL_THREAD_EXECUTOR = + Executors.newFixedThreadPool(2, sThreadFactory); + + private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; + + private static final InternalHandler sHandler = new InternalHandler(); + + private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; + private final WorkerRunnable mWorker; + private final FutureTask mFuture; + + private volatile Status mStatus = Status.PENDING; + + private final AtomicBoolean mCancelled = new AtomicBoolean(); + private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); + + @TargetApi(11) + private static class SerialExecutor implements Executor { + final ArrayDeque mTasks = new ArrayDeque(); + Runnable mActive; + + public synchronized void execute(final Runnable r) { + mTasks.offer(new Runnable() { + public void run() { + try { + r.run(); + } finally { + scheduleNext(); + } + } + }); + if (mActive == null) { + scheduleNext(); + } + } + + protected synchronized void scheduleNext() { + if ((mActive = mTasks.poll()) != null) { + THREAD_POOL_EXECUTOR.execute(mActive); + } + } + } + + /** + * Indicates the current status of the task. Each status will be set only once + * during the lifetime of a task. + */ + public enum Status { + /** + * Indicates that the task has not been executed yet. + */ + PENDING, + /** + * Indicates that the task is running. + */ + RUNNING, + /** + * Indicates that {@link AsyncTask#onPostExecute} has finished. + */ + FINISHED, + } + + /** @hide Used to force static handler to be created. */ + public static void init() { + sHandler.getLooper(); + } + + /** @hide */ + public static void setDefaultExecutor(Executor exec) { + sDefaultExecutor = exec; + } + + /** + * Creates a new asynchronous task. This constructor must be invoked on the UI thread. + */ + public AsyncTask() { + mWorker = new WorkerRunnable() { + public Result call() throws Exception { + mTaskInvoked.set(true); + + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + //noinspection unchecked + return postResult(doInBackground(mParams)); + } + }; + + mFuture = new FutureTask(mWorker) { + @Override + protected void done() { + try { + postResultIfNotInvoked(get()); + } catch (InterruptedException e) { + android.util.Log.w(LOG_TAG, e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + postResultIfNotInvoked(null); + } + } + }; + } + + private void postResultIfNotInvoked(Result result) { + final boolean wasTaskInvoked = mTaskInvoked.get(); + if (!wasTaskInvoked) { + postResult(result); + } + } + + private Result postResult(Result result) { + @SuppressWarnings("unchecked") + Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, + new AsyncTaskResult(this, result)); + message.sendToTarget(); + return result; + } + + /** + * Returns the current status of this task. + * + * @return The current status. + */ + public final Status getStatus() { + return mStatus; + } + + /** + * Override this method to perform a computation on a background thread. The + * specified parameters are the parameters passed to {@link #execute} + * by the caller of this task. + * + * This method can call {@link #publishProgress} to publish updates + * on the UI thread. + * + * @param params The parameters of the task. + * + * @return A result, defined by the subclass of this task. + * + * @see #onPreExecute() + * @see #onPostExecute + * @see #publishProgress + */ + protected abstract Result doInBackground(Params... params); + + /** + * Runs on the UI thread before {@link #doInBackground}. + * + * @see #onPostExecute + * @see #doInBackground + */ + protected void onPreExecute() { + } + + /** + *

Runs on the UI thread after {@link #doInBackground}. The + * specified result is the value returned by {@link #doInBackground}.

+ * + *

This method won't be invoked if the task was cancelled.

+ * + * @param result The result of the operation computed by {@link #doInBackground}. + * + * @see #onPreExecute + * @see #doInBackground + * @see #onCancelled(Object) + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onPostExecute(Result result) { + } + + /** + * Runs on the UI thread after {@link #publishProgress} is invoked. + * The specified values are the values passed to {@link #publishProgress}. + * + * @param values The values indicating progress. + * + * @see #publishProgress + * @see #doInBackground + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onProgressUpdate(Progress... values) { + } + + /** + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + *

The default implementation simply invokes {@link #onCancelled()} and + * ignores the result. If you write your own implementation, do not call + * super.onCancelled(result).

+ * + * @param result The result, if any, computed in + * {@link #doInBackground(Object[])}, can be null + * + * @see #cancel(boolean) + * @see #isCancelled() + */ + @SuppressWarnings({"UnusedParameters"}) + protected void onCancelled(Result result) { + onCancelled(); + } + + /** + *

Applications should preferably override {@link #onCancelled(Object)}. + * This method is invoked by the default implementation of + * {@link #onCancelled(Object)}.

+ * + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + * @see #onCancelled(Object) + * @see #cancel(boolean) + * @see #isCancelled() + */ + protected void onCancelled() { + } + + /** + * Returns true if this task was cancelled before it completed + * normally. If you are calling {@link #cancel(boolean)} on the task, + * the value returned by this method should be checked periodically from + * {@link #doInBackground(Object[])} to end the task as soon as possible. + * + * @return true if task was cancelled before it completed + * + * @see #cancel(boolean) + */ + public final boolean isCancelled() { + return mCancelled.get(); + } + + /** + *

Attempts to cancel execution of this task. This attempt will + * fail if the task has already completed, already been cancelled, + * or could not be cancelled for some other reason. If successful, + * and this task has not started when cancel is called, + * this task should never run. If the task has already started, + * then the mayInterruptIfRunning parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task.

+ * + *

Calling this method will result in {@link #onCancelled(Object)} being + * invoked on the UI thread after {@link #doInBackground(Object[])} + * returns. Calling this method guarantees that {@link #onPostExecute(Object)} + * is never invoked. After invoking this method, you should check the + * value returned by {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])} to finish the task as early as + * possible.

+ * + * @param mayInterruptIfRunning true if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + * + * @return false if the task could not be cancelled, + * typically because it has already completed normally; + * true otherwise + * + * @see #isCancelled() + * @see #onCancelled(Object) + */ + public final boolean cancel(boolean mayInterruptIfRunning) { + mCancelled.set(true); + return mFuture.cancel(mayInterruptIfRunning); + } + + /** + * Waits if necessary for the computation to complete, and then + * retrieves its result. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + */ + public final Result get() throws InterruptedException, ExecutionException { + return mFuture.get(); + } + + /** + * Waits if necessary for at most the given time for the computation + * to complete, and then retrieves its result. + * + * @param timeout Time to wait before cancelling the operation. + * @param unit The time unit for the timeout. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + * @throws TimeoutException If the wait timed out. + */ + public final Result get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + return mFuture.get(timeout, unit); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + *

Note: this function schedules the task on a queue for a single background + * thread or pool of threads depending on the platform version. When first + * introduced, AsyncTasks were executed serially on a single background thread. + * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. Starting + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, tasks are back to being + * executed on a single thread to avoid common application errors caused + * by parallel execution. If you truly want parallel execution, you can use + * the {@link #executeOnExecutor} version of this method + * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings + * on its use. + * + *

This method must be invoked on the UI thread. + * + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + * + * @see #executeOnExecutor(Executor, Object[]) + * @see #execute(Runnable) + */ + public final AsyncTask execute(Params... params) { + return executeOnExecutor(sDefaultExecutor, params); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + *

This method is typically used with {@link #THREAD_POOL_EXECUTOR} to + * allow multiple tasks to run in parallel on a pool of threads managed by + * AsyncTask, however you can also use your own {@link Executor} for custom + * behavior. + * + *

Warning: Allowing multiple tasks to run in parallel from + * a thread pool is generally not what one wants, because the order + * of their operation is not defined. For example, if these tasks are used + * to modify any state in common (such as writing a file due to a button click), + * there are no guarantees on the order of the modifications. + * Without careful work it is possible in rare cases for the newer version + * of the data to be over-written by an older one, leading to obscure data + * loss and stability issues. Such changes are best + * executed in serial; to guarantee such work is serialized regardless of + * platform version you can use this function with {@link #SERIAL_EXECUTOR}. + * + *

This method must be invoked on the UI thread. + * + * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a + * convenient process-wide thread pool for tasks that are loosely coupled. + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link AsyncTask.Status#RUNNING} or {@link AsyncTask.Status#FINISHED}. + * + * @see #execute(Object[]) + */ + public final AsyncTask executeOnExecutor(Executor exec, + Params... params) { + if (mStatus != Status.PENDING) { + switch (mStatus) { + case RUNNING: + throw new IllegalStateException("Cannot execute task:" + + " the task is already running."); + case FINISHED: + throw new IllegalStateException("Cannot execute task:" + + " the task has already been executed " + + "(a task can be executed only once)"); + } + } + + mStatus = Status.RUNNING; + + onPreExecute(); + + mWorker.mParams = params; + exec.execute(mFuture); + + return this; + } + + /** + * Convenience version of {@link #execute(Object...)} for use with + * a simple Runnable object. See {@link #execute(Object[])} for more + * information on the order of execution. + * + * @see #execute(Object[]) + * @see #executeOnExecutor(Executor, Object[]) + */ + public static void execute(Runnable runnable) { + sDefaultExecutor.execute(runnable); + } + + /** + * This method can be invoked from {@link #doInBackground} to + * publish updates on the UI thread while the background computation is + * still running. Each call to this method will trigger the execution of + * {@link #onProgressUpdate} on the UI thread. + * + * {@link #onProgressUpdate} will note be called if the task has been + * canceled. + * + * @param values The progress values to update the UI with. + * + * @see #onProgressUpdate + * @see #doInBackground + */ + protected final void publishProgress(Progress... values) { + if (!isCancelled()) { + sHandler.obtainMessage(MESSAGE_POST_PROGRESS, + new AsyncTaskResult(this, values)).sendToTarget(); + } + } + + private void finish(Result result) { + if (isCancelled()) { + onCancelled(result); + } else { + onPostExecute(result); + } + mStatus = Status.FINISHED; + } + + private static class InternalHandler extends Handler { + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) + @Override + public void handleMessage(Message msg) { + AsyncTaskResult result = (AsyncTaskResult) msg.obj; + switch (msg.what) { + case MESSAGE_POST_RESULT: + // There is only one result + result.mTask.finish(result.mData[0]); + break; + case MESSAGE_POST_PROGRESS: + result.mTask.onProgressUpdate(result.mData); + break; + } + } + } + + private static abstract class WorkerRunnable implements Callable { + Params[] mParams; + } + + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class AsyncTaskResult { + final AsyncTask mTask; + final Data[] mData; + + AsyncTaskResult(AsyncTask task, Data... data) { + mTask = task; + mData = data; + } + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Cache.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Cache.java new file mode 100644 index 000000000..0e1b327b8 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Cache.java @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2012 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 org.nativescript.widgets.image; + +import android.Manifest; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.os.Build.VERSION_CODES; +import android.os.Environment; +import android.os.StatFs; +import android.support.v4.util.LruCache; +import android.util.Log; + +import java.io.File; +import java.lang.ref.SoftReference; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * This class handles disk and memory caching of bitmaps in conjunction with the + * {@link Worker} class and its subclasses. Use + * {@link Cache#getInstance(CacheParams)} to get an instance of this + * class, although usually a cache should be added directly to an {@link Worker} by calling + * {@link Worker#addImageCache(Cache)}. + */ +public class Cache { + private static final String TAG = "JS"; + + // Default memory cache size in kilobytes + private static final int DEFAULT_MEM_CACHE_SIZE = 1024 * 5; // 5MB + + // Constants to easily toggle various caches + private static final boolean DEFAULT_MEM_CACHE_ENABLED = true; + private static final boolean DEFAULT_DISK_CACHE_ENABLED = true; + + private static Cache instance; + private HashMap mMemoryCacheUsage; + private LruCache mMemoryCache; + private CacheParams mParams; + + private Set> mReusableBitmaps; + + /** + * Create a new Cache object using the specified parameters. This should not be + * called directly by other classes, instead use + * {@link Cache#getInstance(CacheParams)} to fetch an Cache + * instance. + */ + private Cache() { + } + + /** + * Return an {@link Cache} instance. + * + * @return An existing retained Cache object or a new one if one did not exist + */ + public static Cache getInstance(CacheParams cacheParams) { + if (instance == null) { + instance = new Cache(); + } + + if (instance.mParams != cacheParams) { + instance.init(cacheParams); + } + + return instance; + } + + /** + * Initialize the cache, providing all parameters. + * + * @param cacheParams The cache parameters to initialize the cache + */ + private void init(CacheParams cacheParams) { + clearCache(); + if (mReusableBitmaps != null) { + mReusableBitmaps.clear(); + mReusableBitmaps = null; + } + + mParams = cacheParams; + + // Set up memory cache + if (mParams.memoryCacheEnabled) { + if (Worker.debuggable > 0) { + Log.v(TAG, "Memory cache created (size = " + mParams.memCacheSize + ")"); + } + + // If we're running on Honeycomb or newer, create a set of reusable bitmaps that can be + // populated into the inBitmap field of BitmapFactory.Options. Note that the set is + // of SoftReferences which will actually not be very effective due to the garbage + // collector being aggressive clearing Soft/WeakReferences. A better approach + // would be to use a strongly references bitmaps, however this would require some + // balancing of memory usage between this set and the bitmap LruCache. It would also + // require knowledge of the expected size of the bitmaps. From Honeycomb to JellyBean + // the size would need to be precise, from KitKat onward the size would just need to + // be the upper bound (due to changes in how inBitmap can re-use bitmaps). + if (Utils.hasHoneycomb()) { + mReusableBitmaps = + Collections.synchronizedSet(new HashSet>()); + } + + mMemoryCacheUsage = new HashMap(); + mMemoryCache = new LruCache(mParams.memCacheSize) { + + /** + * Notify the removed entry that is no longer being cached + */ + @Override + protected void entryRemoved(boolean evicted, String key, + Bitmap oldValue, Bitmap newValue) { + Integer count = mMemoryCacheUsage.get(key); + if (Utils.hasHoneycomb() && (count == null || count == 0)) { + // We're running on Honeycomb or later, so add the bitmap + // to a SoftReference set for possible use with inBitmap later + mReusableBitmaps.add(new SoftReference(oldValue)); + } + } + + /** + * Measure item size in kilobytes rather than units which is more practical + * for a bitmap cache + */ + @Override + protected int sizeOf(String key, Bitmap value) { + final int bitmapSize = getBitmapSize(value) / 1024; + return bitmapSize == 0 ? 1 : bitmapSize; + } + }; + } + } + + /** + * Adds a bitmap to both memory and disk cache. + * + * @param data Unique identifier for the bitmap to store + * @param value The bitmap drawable to store + */ + public void addBitmapToCache(String data, Bitmap value) { + if (data == null || value == null) { + return; + } + + // Add to memory cache + if (mMemoryCache != null) { + Bitmap currentValue = mMemoryCache.get(data); + // NOTE: If we have existing we probably loaded it sync so we don't want to add the new one, + // because this will make the previous bitmap free for reuse but it is used somewhere. + // Probably won't happen often. + if (currentValue == null) { + Integer count = mMemoryCacheUsage.get(data); + // NOTE: count should be null here. + mMemoryCacheUsage.put(data, count == null ? 1 : count + 1); + mMemoryCache.put(data, value); + } + } + } + + /** + * Get from memory cache. + * + * @param data Unique identifier for which item to get + * @return The bitmap if found in cache, null otherwise + */ + public Bitmap getBitmapFromMemCache(String data) { + Bitmap memValue = null; + + if (mMemoryCache != null) { + memValue = mMemoryCache.get(data); + if (memValue != null) { + Integer count = mMemoryCacheUsage.get(data); + mMemoryCacheUsage.put(data, count == null ? 1 : count + 1); + } + } + + if (Worker.debuggable > 0 && memValue != null) { + Log.v(TAG, "Memory cache hit"); + } + + return memValue; + } + + public void reduceDisplayedCounter(String uri) { + if (mMemoryCache != null) { + Integer count = mMemoryCacheUsage.get(uri); + if (count != null) { + if (count == 1) { + mMemoryCacheUsage.remove(uri); + } else { + mMemoryCacheUsage.put(uri, count - 1); + } + } + } + } + + /** + * @param options - BitmapFactory.Options with out* options populated + * @return Bitmap that case be used for inBitmap + */ + protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) { + //BEGIN_INCLUDE(get_bitmap_from_reusable_set) + Bitmap bitmap = null; + + if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) { + synchronized (mReusableBitmaps) { + final Iterator> iterator = mReusableBitmaps.iterator(); + Bitmap item; + + while (iterator.hasNext()) { + item = iterator.next().get(); + + if (null != item && item.isMutable()) { + // Check to see it the item can be used for inBitmap + if (canUseForInBitmap(item, options)) { + bitmap = item; + + // Remove from reusable set so it can't be used again + iterator.remove(); + break; + } + } else { + // Remove from the set if the reference has been cleared. + iterator.remove(); + } + } + } + } + + return bitmap; + //END_INCLUDE(get_bitmap_from_reusable_set) + } + + /** + * Clears both the memory and disk cache associated with this Cache object. Note that + * this includes disk access so this should not be executed on the main/UI thread. + */ + public void clearCache() { + if (mMemoryCache != null) { + mMemoryCache.evictAll(); + if (Worker.debuggable > 0) { + Log.v(TAG, "Memory cache cleared"); + } + } + if (mMemoryCacheUsage != null) { + mMemoryCacheUsage.clear(); + } + + mMemoryCacheUsage = null; + mMemoryCache = null; + } + + /** + * A holder class that contains cache parameters. + */ + public static class CacheParams { + public int memCacheSize = DEFAULT_MEM_CACHE_SIZE; + public boolean memoryCacheEnabled = DEFAULT_MEM_CACHE_ENABLED; + public boolean diskCacheEnabled = DEFAULT_DISK_CACHE_ENABLED; + + /** + * Sets the memory cache size based on a percentage of the max available VM memory. + * Eg. setting percent to 0.2 would set the memory cache to one fifth of the available + * memory. Throws {@link IllegalArgumentException} if percent is < 0.01 or > .8. + * memCacheSize is stored in kilobytes instead of bytes as this will eventually be passed + * to construct a LruCache which takes an int in its constructor. + *

+ * This value should be chosen carefully based on a number of factors + * Refer to the corresponding Android Training class for more discussion: + * http://developer.android.com/training/displaying-bitmaps/ + * + * @param percent Percent of available app memory to use to size memory cache + */ + public void setMemCacheSizePercent(float percent) { + if (percent < 0.01f || percent > 0.8f) { + throw new IllegalArgumentException("setMemCacheSizePercent - percent must be " + + "between 0.01 and 0.8 (inclusive)"); + } + memCacheSize = Math.round(percent * Runtime.getRuntime().maxMemory() / 1024); + } + } + + /** + * @param candidate - Bitmap to check + * @param targetOptions - Options that have the out* value populated + * @return true if candidate can be used for inBitmap re-use with + * targetOptions + */ + @TargetApi(VERSION_CODES.KITKAT) + private static boolean canUseForInBitmap( + Bitmap candidate, BitmapFactory.Options targetOptions) { + //BEGIN_INCLUDE(can_use_for_inbitmap) + if (!Utils.hasKitKat()) { + // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1 + return candidate.getWidth() == targetOptions.outWidth + && candidate.getHeight() == targetOptions.outHeight + && targetOptions.inSampleSize == 1; + } + + // From Android 4.4 (KitKat) onward we can re-use if the byte size of the new bitmap + // is smaller than the reusable bitmap candidate allocation byte count. + int width = targetOptions.outWidth / targetOptions.inSampleSize; + int height = targetOptions.outHeight / targetOptions.inSampleSize; + int byteCount = width * height * getBytesPerPixel(candidate.getConfig()); + return byteCount <= candidate.getAllocationByteCount(); + //END_INCLUDE(can_use_for_inbitmap) + } + + /** + * Return the byte usage per pixel of a bitmap based on its configuration. + * + * @param config The bitmap configuration. + * @return The byte usage per pixel. + */ + private static int getBytesPerPixel(Config config) { + if (config == Config.ARGB_8888) { + return 4; + } else if (config == Config.RGB_565) { + return 2; + } else if (config == Config.ARGB_4444) { + return 2; + } else if (config == Config.ALPHA_8) { + return 1; + } + return 1; + } + + /** + * Get a usable cache directory (external if available, internal otherwise). + * + * @param context The context to use + * @param uniqueName A unique directory name to append to the cache dir + * @return The cache dir + */ + public static File getDiskCacheDir(Context context, String uniqueName) { + // Check if media is mounted or storage is built-in, if so, try and use external cache dir + // otherwise use internal cache dir + final File cacheFilePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || + !isExternalStorageRemovable() ? getExternalCacheDir(context) : context.getCacheDir(); + // In case there is no external storage isExternalStorageRemovable returns False and we get Null from getExternalCacheDir. + // If this is the case - fall back to internal cache dir. + final String cachePath = cacheFilePath != null ? cacheFilePath.getPath() : context.getCacheDir().getPath(); + return new File(cachePath + File.separator + uniqueName); + } + + /** + * A hashing method that changes a string (like a URL) into a hash suitable for using as a + * disk filename. + */ + public static String hashKeyForDisk(String key) { + String cacheKey; + try { + final MessageDigest mDigest = MessageDigest.getInstance("MD5"); + mDigest.update(key.getBytes()); + cacheKey = bytesToHexString(mDigest.digest()); + } catch (NoSuchAlgorithmException e) { + cacheKey = String.valueOf(key.hashCode()); + } + return cacheKey; + } + + private static String bytesToHexString(byte[] bytes) { + // http://stackoverflow.com/questions/332079 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < bytes.length; i++) { + String hex = Integer.toHexString(0xFF & bytes[i]); + if (hex.length() == 1) { + sb.append('0'); + } + sb.append(hex); + } + return sb.toString(); + } + + /** + * Get the size in bytes of a bitmap in a BitmapDrawable. Note that from Android 4.4 (KitKat) + * onward this returns the allocated memory size of the bitmap which can be larger than the + * actual bitmap data byte count (in the case it was re-used). + * + * @param bitmap + * @return size in bytes + */ + @TargetApi(VERSION_CODES.KITKAT) + public static int getBitmapSize(Bitmap bitmap) { + // From KitKat onward use getAllocationByteCount() as allocated bytes can potentially be + // larger than bitmap byte count. + if (Utils.hasKitKat()) { + return bitmap.getAllocationByteCount(); + } + + if (Utils.hasHoneycombMR1()) { + return bitmap.getByteCount(); + } + + // Pre HC-MR1 + return bitmap.getRowBytes() * bitmap.getHeight(); + } + + /** + * Check if external storage is built-in or removable. + * + * @return True if external storage is removable (like an SD card), false + * otherwise. + */ + @TargetApi(VERSION_CODES.GINGERBREAD) + public static boolean isExternalStorageRemovable() { + if (Utils.hasGingerbread()) { + return Environment.isExternalStorageRemovable(); + } + return true; + } + + /** + * Get the external app cache directory. + * + * @param context The context to use + * @return The external cache dir + */ + @TargetApi(VERSION_CODES.FROYO) + public static File getExternalCacheDir(Context context) { + if (Utils.hasFroyo()) { + if (Utils.hasKitKat() || + context.checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, android.os.Process.myPid(), android.os.Process.myUid()) == PackageManager.PERMISSION_GRANTED) { + return context.getExternalCacheDir(); + } + + return null; + } + + // Before Froyo we need to construct the external cache dir ourselves + final String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/"; + return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir); + } + + /** + * Check how much usable space is available at a given path. + * + * @param path The path to check + * @return The space available in bytes + */ + @TargetApi(VERSION_CODES.GINGERBREAD) + public static long getUsableSpace(File path) { + if (Utils.hasGingerbread()) { + return path.getUsableSpace(); + } + final StatFs stats = new StatFs(path.getPath()); + return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks(); + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/DiskLruCache.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/DiskLruCache.java new file mode 100644 index 000000000..7d89176fe --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/DiskLruCache.java @@ -0,0 +1,953 @@ +/* + * Copyright (C) 2011 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 org.nativescript.widgets.image; + +import java.io.BufferedInputStream; +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + ****************************************************************************** + * Taken from the JB source code, can be found in: + * libcore/luni/src/main/java/libcore/io/DiskLruCache.java + * or direct link: + * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java + ****************************************************************************** + * + * A cache that uses a bounded amount of space on a filesystem. Each cache + * entry has a string key and a fixed number of values. Values are byte + * sequences, accessible as streams or files. Each value must be between {@code + * 0} and {@code Integer.MAX_VALUE} bytes in length. + * + *

The cache stores its data in a directory on the filesystem. This + * directory must be exclusive to the cache; the cache may delete or overwrite + * files from its directory. It is an error for multiple processes to use the + * same cache directory at the same time. + * + *

This cache limits the number of bytes that it will store on the + * filesystem. When the number of stored bytes exceeds the limit, the cache will + * remove entries in the background until the limit is satisfied. The limit is + * not strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache + * journal so space-sensitive applications should set a conservative limit. + * + *

Clients call {@link #edit} to create or update the values of an entry. An + * entry may have only one editor at one time; if a value is not available to be + * edited then {@link #edit} will return null. + *

    + *
  • When an entry is being created it is necessary to + * supply a full set of values; the empty value should be used as a + * placeholder if necessary. + *
  • When an entry is being edited, it is not necessary + * to supply data for every value; values default to their previous + * value. + *
+ * Every {@link #edit} call must be matched by a call to {@link Editor#commit} + * or {@link Editor#abort}. Committing is atomic: a read observes the full set + * of values as they were before or after the commit, but never a mix of values. + * + *

Clients call {@link #get} to read a snapshot of an entry. The read will + * observe the value at the time that {@link #get} was called. Updates and + * removals after the call do not impact ongoing reads. + * + *

This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If + * an error occurs while writing a cache value, the edit will fail silently. + * Callers should handle other problems by catching {@code IOException} and + * responding appropriately. + */ +public final class DiskLruCache implements Closeable { + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TMP = "journal.tmp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private static final int IO_BUFFER_SIZE = 8 * 1024; + + /* + * This cache uses a journal file named "journal". A typical journal file + * looks like this: + * libcore.io.DiskLruCache + * 1 + * 100 + * 2 + * + * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 + * DIRTY 335c4c6028171cfddfbaae1a9c313c52 + * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 + * REMOVE 335c4c6028171cfddfbaae1a9c313c52 + * DIRTY 1ab96a171faeeee38496d8b330771a7a + * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 + * READ 335c4c6028171cfddfbaae1a9c313c52 + * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 + * + * The first five lines of the journal form its header. They are the + * constant string "libcore.io.DiskLruCache", the disk cache's version, + * the application's version, the value count, and a blank line. + * + * Each of the subsequent lines in the file is a record of the state of a + * cache entry. Each line contains space-separated values: a state, a key, + * and optional state-specific values. + * o DIRTY lines track that an entry is actively being created or updated. + * Every successful DIRTY action should be followed by a CLEAN or REMOVE + * action. DIRTY lines without a matching CLEAN or REMOVE indicate that + * temporary files may need to be deleted. + * o CLEAN lines track a cache entry that has been successfully published + * and may be read. A publish line is followed by the lengths of each of + * its values. + * o READ lines track accesses for LRU. + * o REMOVE lines track entries that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal may + * occasionally be compacted by dropping redundant lines. A temporary file named + * "journal.tmp" will be used during compaction; that file should be deleted if + * it exists when the cache is opened. + */ + + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final int appVersion; + private final long maxSize; + private final int valueCount; + private long size = 0; + private Writer journalWriter; + private final LinkedHashMap lruEntries + = new LinkedHashMap(0, 0.75f, true); + private int redundantOpCount; + + /** + * To differentiate between old and current snapshots, each entry is given + * a sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; + + /* From java.util.Arrays */ + @SuppressWarnings("unchecked") + private static T[] copyOfRange(T[] original, int start, int end) { + final int originalLength = original.length; // For exception priority compatibility. + if (start > end) { + throw new IllegalArgumentException(); + } + if (start < 0 || start > originalLength) { + throw new ArrayIndexOutOfBoundsException(); + } + final int resultLength = end - start; + final int copyLength = Math.min(resultLength, originalLength - start); + final T[] result = (T[]) Array + .newInstance(original.getClass().getComponentType(), resultLength); + System.arraycopy(original, start, result, 0, copyLength); + return result; + } + + /** + * Returns the remainder of 'reader' as a string, closing it when done. + */ + public static String readFully(Reader reader) throws IOException { + try { + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); + } + return writer.toString(); + } finally { + reader.close(); + } + } + + /** + * Returns the ASCII characters up to but not including the next "\r\n", or + * "\n". + * + * @throws EOFException if the stream is exhausted before the next newline + * character. + */ + public static String readAsciiLine(InputStream in) throws IOException { + // TODO: support UTF-8 here instead + + StringBuilder result = new StringBuilder(80); + while (true) { + int c = in.read(); + if (c == -1) { + throw new EOFException(); + } else if (c == '\n') { + break; + } + + result.append((char) c); + } + int length = result.length(); + if (length > 0 && result.charAt(length - 1) == '\r') { + result.setLength(length - 1); + } + return result.toString(); + } + + /** + * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Recursively delete everything in {@code dir}. + */ + // TODO: this should specify paths as Strings rather than as Files + public static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + /** This cache uses a single background thread to evict entries. */ + private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, + 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + private final Callable cleanupCallable = new Callable() { + @Override public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // closed + } + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } + return null; + } + }; + + private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory a writable directory + * @param appVersion + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws IOException if reading or writing the cache directory fails + */ + public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) + throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); + } + + // prefer to pick up where we left off + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), + IO_BUFFER_SIZE); + return cache; + } catch (IOException journalIsCorrupt) { +// System.logW("DiskLruCache " + directory + " is corrupt: " +// + journalIsCorrupt.getMessage() + ", removing"); + cache.delete(); + } + } + + // create a new empty cache + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private void readJournal() throws IOException { + InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE); + try { + String magic = readAsciiLine(in); + String version = readAsciiLine(in); + String appVersionString = readAsciiLine(in); + String valueCountString = readAsciiLine(in); + String blank = readAsciiLine(in); + if (!MAGIC.equals(magic) + || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) + || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + + magic + ", " + version + ", " + valueCountString + ", " + blank + "]"); + } + + while (true) { + try { + readJournalLine(readAsciiLine(in)); + } catch (EOFException endOfJournal) { + break; + } + } + } finally { + closeQuietly(in); + } + } + + private void readJournalLine(String line) throws IOException { + String[] parts = line.split(" "); + if (parts.length < 2) { + throw new IOException("unexpected journal line: " + line); + } + + String key = parts[1]; + if (parts[0].equals(REMOVE) && parts.length == 2) { + lruEntries.remove(key); + return; + } + + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } + + if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) { + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(copyOfRange(parts, 2, parts.length)); + } else if (parts[0].equals(DIRTY) && parts.length == 2) { + entry.currentEditor = new Editor(entry); + } else if (parts[0].equals(READ) && parts.length == 2) { + // this work was already done by calling lruEntries.get() + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); + } + i.remove(); + } + } + } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE); + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } + } + + writer.close(); + journalFileTmp.renameTo(journalFile); + journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE); + } + + private static void deleteIfExists(File file) throws IOException { +// try { +// Libcore.os.remove(file.getPath()); +// } catch (ErrnoException errnoException) { +// if (errnoException.errno != OsConstants.ENOENT) { +// throw errnoException.rethrowAsIOException(); +// } +// } + if (file.exists() && !file.delete()) { + throw new IOException(); + } + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + /* + * Open all streams eagerly to guarantee that we see a single published + * snapshot. If we opened streams lazily then the streams could come + * from different edits. + */ + InputStream[] ins = new InputStream[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + ins[i] = new FileInputStream(entry.getCleanFile(i)); + } + } catch (FileNotFoundException e) { + // a file must have been deleted manually! + return null; + } + + redundantOpCount++; + journalWriter.append(READ + ' ' + key + '\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Snapshot(key, entry.sequenceNumber, ins); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER + && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // snapshot is stale + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // another edit is in progress + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // flush the journal before creating files to prevent file leaks + journalWriter.write(DIRTY + ' ' + key + '\n'); + journalWriter.flush(); + return editor; + } + + /** + * Returns the directory where this cache stores its data. + */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public long maxSize() { + return maxSize; + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // if this edit is creating the entry for the first time, every index must have a value + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + throw new IllegalStateException("edit didn't create file " + i); + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.write(REMOVE + ' ' + entry.key + '\n'); + } + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000; + return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.getCleanFile(i); + if (!file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE + ' ' + key + '\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** + * Returns true if this cache has been closed. + */ + public boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** + * Force buffered operations to the filesystem. + */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** + * Closes this cache. Stored values will remain on the filesystem. + */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // already closed + } + for (Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { +// Map.Entry toEvict = lruEntries.eldest(); + final Map.Entry toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + deleteContents(directory); + } + + private void validateKey(String key) { + if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { + throw new IllegalArgumentException( + "keys must not contain spaces or newlines: \"" + key + "\""); + } + } + + private static String inputStreamToString(InputStream in) throws IOException { + return readFully(new InputStreamReader(in, UTF_8)); + } + + /** + * A snapshot of the values for an entry. + */ + public final class Snapshot implements Closeable { + private final String key; + private final long sequenceNumber; + private final InputStream[] ins; + + private Snapshot(String key, long sequenceNumber, InputStream[] ins) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.ins = ins; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + /** + * Returns the unbuffered stream with the value for {@code index}. + */ + public InputStream getInputStream(int index) { + return ins[index]; + } + + /** + * Returns the string value for {@code index}. + */ + public String getString(int index) throws IOException { + return inputStreamToString(getInputStream(index)); + } + + @Override public void close() { + for (InputStream in : ins) { + closeQuietly(in); + } + } + } + + /** + * Edits the values for an entry. + */ + public final class Editor { + private final Entry entry; + private boolean hasErrors; + + private Editor(Entry entry) { + this.entry = entry; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public InputStream newInputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + return new FileInputStream(entry.getCleanFile(index)); + } + } + + /** + * Returns the last committed value as a string, or null if no value + * has been committed. + */ + public String getString(int index) throws IOException { + InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors + * when writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public OutputStream newOutputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); + } + } + + /** + * Sets the value at {@code index} to {@code value}. + */ + public void set(int index, String value) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(newOutputStream(index), UTF_8); + writer.write(value); + } finally { + closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the + * edit lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // the previous entry is stale + } else { + completeEdit(this, true); + } + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + private class FaultHidingOutputStream extends FilterOutputStream { + private FaultHidingOutputStream(OutputStream out) { + super(out); + } + + @Override public void write(int oneByte) { + try { + out.write(oneByte); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void write(byte[] buffer, int offset, int length) { + try { + out.write(buffer, offset, length); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void close() { + try { + out.close(); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void flush() { + try { + out.flush(); + } catch (IOException e) { + hasErrors = true; + } + } + } + } + + private final class Entry { + private final String key; + + /** Lengths of this entry's files. */ + private final long[] lengths; + + /** True if this entry has ever been published */ + private boolean readable; + + /** The ongoing edit or null if this entry is not being edited. */ + private Editor currentEditor; + + /** The sequence number of the most recently committed edit to this entry. */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + this.lengths = new long[valueCount]; + } + + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** + * Set lengths using decimal numbers like "10123". + */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + Arrays.toString(strings)); + } + + public File getCleanFile(int i) { + return new File(directory, key + "." + i); + } + + public File getDirtyFile(int i) { + return new File(directory, key + "." + i + ".tmp"); + } + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Fetcher.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Fetcher.java new file mode 100644 index 000000000..19878b6e4 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Fetcher.java @@ -0,0 +1,672 @@ +/* + * Copyright (C) 2012 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 org.nativescript.widgets.image; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Matrix; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.ExifInterface; +import android.os.Build; +import android.util.Log; +import android.util.TypedValue; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * A simple subclass of {@link Worker} that fetch and resize images from a file, resource or URL. + */ +public class Fetcher extends Worker { + private static final int HTTP_CACHE_SIZE = 10 * 1024 * 1024; // 10MB + private static final String HTTP_CACHE_DIR = "http"; + private static final int IO_BUFFER_SIZE = 8 * 1024; + + private static int mDeviceWidthPixels; + private static int mDeviceHeightPixels; + + private File mHttpCacheDir; + private DiskLruCache mHttpDiskCache; + private boolean mHttpDiskCacheStarting = true; + private final Object mHttpDiskCacheLock = new Object(); + private static final int DISK_CACHE_INDEX = 0; + + private final String mPackageName; + private static Fetcher instance; + + public static Fetcher getInstance(Context context) { + if (instance == null) { + instance = new Fetcher(context); + } + + return instance; + } + + /** + * Initialize providing a target image width and height for the processing images. + * + * @param context + */ + private Fetcher(Context context) { + super(context); + mHttpCacheDir = Cache.getDiskCacheDir(context, HTTP_CACHE_DIR); + mPackageName = context.getPackageName(); + mDeviceWidthPixels = (int) context.getResources().getDisplayMetrics().widthPixels; + mDeviceHeightPixels = (int) context.getResources().getDisplayMetrics().heightPixels; + } + + @Override + protected void initDiskCacheInternal() { + if (!mHttpCacheDir.exists()) { + mHttpCacheDir.mkdirs(); + } + synchronized (mHttpDiskCacheLock) { + if (Cache.getUsableSpace(mHttpCacheDir) > HTTP_CACHE_SIZE) { + try { + mHttpDiskCache = DiskLruCache.open(mHttpCacheDir, 1, 1, HTTP_CACHE_SIZE); + if (debuggable > 0) { + Log.v(TAG, "HTTP cache initialized"); + } + } catch (IOException e) { + mHttpDiskCache = null; + } + } + mHttpDiskCacheStarting = false; + mHttpDiskCacheLock.notifyAll(); + } + } + + @Override + protected void clearCacheInternal() { + super.clearCacheInternal(); + synchronized (mHttpDiskCacheLock) { + if (mHttpDiskCache != null && !mHttpDiskCache.isClosed()) { + try { + mHttpDiskCache.delete(); + if (debuggable > 0) { + Log.v(TAG, "HTTP cache cleared"); + } + } catch (IOException e) { + Log.e(TAG, "clearCacheInternal - " + e); + } + mHttpDiskCache = null; + mHttpDiskCacheStarting = true; + } + } + } + + @Override + protected void flushCacheInternal() { + synchronized (mHttpDiskCacheLock) { + if (mHttpDiskCache != null) { + try { + mHttpDiskCache.flush(); + if (debuggable > 0) { + Log.v(TAG, "HTTP cache flushed"); + } + } catch (IOException e) { + Log.e(TAG, "flush - " + e); + } + } + } + } + + @Override + protected void closeCacheInternal() { + synchronized (mHttpDiskCacheLock) { + if (mHttpDiskCache != null) { + try { + if (!mHttpDiskCache.isClosed()) { + mHttpDiskCache.close(); + mHttpDiskCache = null; + if (debuggable > 0) { + Log.v(TAG, "HTTP cache closed"); + } + } + } catch (IOException e) { + Log.e(TAG, "closeCacheInternal - " + e); + } + } + } + } + + /** + * The main process method, which will be called by the Worker in the AsyncTask background + * thread. + * + * @param data The data to load the bitmap, in this case, a regular http URL + * @return The downloaded and resized bitmap + */ + private Bitmap processHttp(String data, int decodeWidth, int decodeHeight, boolean keepAspectRatio) { + final String key = Cache.hashKeyForDisk(data); + FileDescriptor fileDescriptor = null; + FileInputStream fileInputStream = null; + DiskLruCache.Snapshot snapshot; + synchronized (mHttpDiskCacheLock) { + // Wait for disk cache to initialize + while (mHttpDiskCacheStarting) { + try { + mHttpDiskCacheLock.wait(); + } catch (InterruptedException e) { + } + } + + if (mHttpDiskCache != null) { + try { + snapshot = mHttpDiskCache.get(key); + if (snapshot == null) { + if (debuggable > 0) { + Log.v(TAG, "processBitmap, not found in http cache, downloading..."); + } + DiskLruCache.Editor editor = mHttpDiskCache.edit(key); + if (editor != null) { + if (downloadUrlToStream(data, editor.newOutputStream(DISK_CACHE_INDEX))) { + editor.commit(); + } else { + editor.abort(); + } + } + snapshot = mHttpDiskCache.get(key); + } + if (snapshot != null) { + fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX); + fileDescriptor = fileInputStream.getFD(); + } + } catch (IOException e) { + Log.e(TAG, "processHttp - " + e); + } catch (IllegalStateException e) { + Log.e(TAG, "processHttp - " + e); + } finally { + if (fileDescriptor == null && fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) { + } + } + } + } + } + + Bitmap bitmap = null; + if (fileDescriptor != null) { + bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, decodeWidth, decodeHeight, keepAspectRatio, + getCache()); + } + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (IOException e) { + } + } + return bitmap; + } + + private Bitmap processHttpNoCache(String data, int decodeWidth, int decodeHeight, boolean keepAspectRatio) { + ByteArrayOutputStreamInternal outputStream = null; + Bitmap bitmap = null; + + try { + outputStream = new ByteArrayOutputStreamInternal(); + if (downloadUrlToStream(data, outputStream)) { + bitmap = decodeSampledBitmapFromByteArray(outputStream.getBuffer(), decodeWidth, decodeHeight, + keepAspectRatio, getCache()); + } + } catch (IllegalStateException e) { + Log.e(TAG, "processHttpNoCache - " + e); + } finally { + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + } + } + } + + return bitmap; + } + + @Override + protected Bitmap processBitmap(String uri, int decodeWidth, int decodeHeight, boolean keepAspectRatio, + boolean useCache) { + if (debuggable > 0) { + Log.v(TAG, "process: " + uri); + } + + if (uri.startsWith(FILE_PREFIX)) { + String filename = uri.substring(FILE_PREFIX.length()); + return decodeSampledBitmapFromFile(filename, decodeWidth, decodeHeight, keepAspectRatio, getCache()); + } else if (uri.startsWith(RESOURCE_PREFIX)) { + String resPath = uri.substring(RESOURCE_PREFIX.length()); + int resId = mResources.getIdentifier(resPath, "drawable", mPackageName); + if (resId > 0) { + return decodeSampledBitmapFromResource(mResources, resId, decodeWidth, decodeHeight, keepAspectRatio, + getCache()); + } else { + Log.v(TAG, "Missing Image with resourceID: " + uri); + return null; + } + } else { + if (useCache && mHttpDiskCache != null) { + return processHttp(uri, decodeWidth, decodeHeight, keepAspectRatio); + } else { + return processHttpNoCache(uri, decodeWidth, decodeHeight, keepAspectRatio); + } + } + } + + /** + * Download a bitmap from a URL and write the content to an output stream. + * + * @param urlString The URL to fetch + * @return true if successful, false otherwise + */ + public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { + disableConnectionReuseIfNecessary(); + HttpURLConnection urlConnection = null; + BufferedOutputStream out = null; + BufferedInputStream in = null; + + try { + final URL url = new URL(urlString); + urlConnection = (HttpURLConnection) url.openConnection(); + in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE); + out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); + + int b; + while ((b = in.read()) != -1) { + out.write(b); + } + return true; + } catch (final IOException e) { + Log.e(TAG, "Error in downloadBitmap - " + e); + } finally { + if (urlConnection != null) { + urlConnection.disconnect(); + } + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (final IOException e) { + } + } + return false; + } + + /** + * Workaround for bug pre-Froyo, see here for more info: + * http://android-developers.blogspot.com/2011/09/androids-http-clients.html + */ + public static void disableConnectionReuseIfNecessary() { + // HTTP connection reuse which was buggy pre-froyo + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO) { + System.setProperty("http.keepAlive", "false"); + } + } + + private static class ByteArrayOutputStreamInternal extends ByteArrayOutputStream { + public byte[] getBuffer() { + return buf; + } + } + + /** + * Decode and sample down a bitmap from resources to the requested width and height. + * + * @param res The resources object containing the image data + * @param resId The resource id of the image data + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The Cache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight, + boolean keepAspectRatio, Cache cache) { + + // BEGIN_INCLUDE (read_bitmap_dimensions) + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, options); + + options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight); + + // END_INCLUDE (read_bitmap_dimensions) + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + Bitmap bitmap = null; + InputStream is = null; + + try { + final TypedValue value = new TypedValue(); + is = res.openRawResource(resId, value); + + bitmap = BitmapFactory.decodeResourceStream(res, value, is, null, options); + } catch (Exception e) { + /* do nothing. + If the exception happened on open, bm will be null. + If it happened on close, bm is still valid. + */ + } + + if (bitmap == null && options != null && options.inBitmap != null) { + throw new IllegalArgumentException("Problem decoding into existing bitmap"); + } + + ExifInterface ei = getExifInterface(is); + + return scaleAndRotateBitmap(bitmap, ei, reqWidth, reqHeight, keepAspectRatio); + } + + @TargetApi(Build.VERSION_CODES.N) + private static ExifInterface getExifInterface(InputStream is) { + ExifInterface ei = null; + try { + if (Utils.hasN()) { + ei = new ExifInterface(is); + } + } catch (final Exception e) { + Log.e(TAG, "Error in reading bitmap - " + e); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + } + } + } + + return ei; + } + + @TargetApi(Build.VERSION_CODES.N) + private static ExifInterface getExifInterface(FileDescriptor fd) { + ExifInterface ei = null; + try { + if (Utils.hasN()) { + ei = new ExifInterface(fd); + } + } catch (final Exception e) { + Log.e(TAG, "Error in reading bitmap - " + e); + } + + return ei; + } + + private static ExifInterface getExifInterface(String fileName) { + ExifInterface ei = null; + try { + ei = new ExifInterface(fileName); + } catch (final Exception e) { + Log.e(TAG, "Error in reading bitmap - " + e); + } + + return ei; + } + + /** + * Decode and sample down a bitmap from a file to the requested width and height. + * + * @param filename The full path of the file to decode + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The Cache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromFile(String fileName, int reqWidth, int reqHeight, + boolean keepAspectRatio, Cache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(fileName, options); + + options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight); + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + + final Bitmap bitmap = BitmapFactory.decodeFile(fileName, options); + ExifInterface ei = getExifInterface(fileName); + + return scaleAndRotateBitmap(bitmap, ei, reqWidth, reqHeight, keepAspectRatio); + } + + private static Bitmap scaleAndRotateBitmap(Bitmap bitmap, ExifInterface ei, int reqWidth, int reqHeight, + boolean keepAspectRatio) { + if (bitmap == null) { + return null; + } + + int sourceWidth = bitmap.getWidth(); + int sourceHeight = bitmap.getHeight(); + reqWidth = reqWidth > 0 ? reqWidth : Math.min(sourceWidth, mDeviceWidthPixels); + reqHeight = reqHeight > 0 ? reqHeight : Math.min(sourceHeight, mDeviceHeightPixels); + + // scale + if (reqWidth != sourceWidth || reqHeight != sourceHeight) { + if (keepAspectRatio) { + double widthCoef = (double) sourceWidth / (double) reqWidth; + double heightCoef = (double) sourceHeight / (double) reqHeight; + double aspectCoef = Math.min(widthCoef, heightCoef); + + reqWidth = (int) Math.floor(sourceWidth / aspectCoef); + reqHeight = (int) Math.floor(sourceHeight / aspectCoef); + } + + bitmap = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, true); + } + + // rotate + if (ei != null) { + final Matrix matrix = new Matrix(); + final int rotationAngle = calculateRotationAngle(ei); + if (rotationAngle != 0) { + matrix.postRotate(rotationAngle); + bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + } + + return bitmap; + } + + private static int calculateRotationAngle(ExifInterface ei) { + int rotationAngle = 0; + final int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + rotationAngle = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + rotationAngle = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + rotationAngle = 270; + break; + } + + return rotationAngle; + } + + /** + * Decode and sample down a bitmap from a file input stream to the requested width and height. + * + * @param fileDescriptor The file descriptor to read from + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @param cache The Cache used to find candidate bitmaps for use with inBitmap + * @return A bitmap sampled down from the original with the same aspect ratio and dimensions + * that are equal to or greater than the requested width and height + */ + public static Bitmap decodeSampledBitmapFromDescriptor(FileDescriptor fileDescriptor, int reqWidth, int reqHeight, + boolean keepAspectRatio, Cache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + + options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight); + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + Bitmap results = null; + try { + // This can throw an error on a corrupted image when using an inBitmap + results = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + } catch (Exception e) { + // clear the inBitmap and try again + options.inBitmap = null; + results = BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); + // If image is broken, rather than an issue with the inBitmap, we will get a NULL out in this case... + } + + ExifInterface ei = getExifInterface(fileDescriptor); + + return scaleAndRotateBitmap(results, ei, reqWidth, reqHeight, keepAspectRatio); + } + + public static Bitmap decodeSampledBitmapFromByteArray(byte[] buffer, int reqWidth, int reqHeight, + boolean keepAspectRatio, Cache cache) { + + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options); + + options.inSampleSize = calculateInSampleSize(options.outWidth, options.outHeight, reqWidth, reqHeight); + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; + + // If we're running on Honeycomb or newer, try to use inBitmap + if (Utils.hasHoneycomb()) { + addInBitmapOptions(options, cache); + } + + final Bitmap bitmap = BitmapFactory.decodeByteArray(buffer, 0, buffer.length, options); + + InputStream is = new ByteArrayInputStream(buffer); + ExifInterface ei = getExifInterface(is); + + return scaleAndRotateBitmap(bitmap, ei, reqWidth, reqHeight, keepAspectRatio); + } + + /** + * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding + * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates + * the closest inSampleSize that is a power of 2 and will result in the final decoded bitmap + * having a width and height equal to or larger than the requested width and height. + * + * @param imageWidth The original width of the resulting bitmap + * @param imageHeight The original height of the resulting bitmap + * @param reqWidth The requested width of the resulting bitmap + * @param reqHeight The requested height of the resulting bitmap + * @return The value to be used for inSampleSize + */ + public static int calculateInSampleSize(int imageWidth, int imageHeight, int reqWidth, int reqHeight) { + // BEGIN_INCLUDE (calculate_sample_size) + // Raw height and width of image + final int height = imageHeight; + final int width = imageWidth; + reqWidth = reqWidth > 0 ? reqWidth : width; + reqHeight = reqHeight > 0 ? reqHeight : height; + int inSampleSize = 1; + if (height > reqHeight || width > reqWidth) { + + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2; + } + + // This offers some additional logic in case the image has a strange + // aspect ratio. For example, a panorama may have a much larger + // width than height. In these cases the total pixels might still + // end up being too large to fit comfortably in memory, so we should + // be more aggressive with sample down the image (=larger inSampleSize). + + long totalPixels = (width / inSampleSize) * (height / inSampleSize); + + // Anything more than 2x the requested pixels we'll sample down further + final long totalReqPixelsCap = reqWidth * reqHeight * 2; + + while (totalPixels > totalReqPixelsCap) { + inSampleSize *= 2; + totalPixels = (width / inSampleSize) * (height / inSampleSize); + } + } + return inSampleSize; + // END_INCLUDE (calculate_sample_size) + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + private static void addInBitmapOptions(BitmapFactory.Options options, Cache cache) { + //BEGIN_INCLUDE(add_bitmap_options) + // inBitmap only works with mutable bitmaps so force the decoder to + // return mutable bitmaps. + options.inMutable = true; + + if (cache != null) { + // Try and find a bitmap to use for inBitmap + Bitmap inBitmap = cache.getBitmapFromReusableSet(options); + + if (inBitmap != null) { + options.inBitmap = inBitmap; + } + } + //END_INCLUDE(add_bitmap_options) + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Utils.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Utils.java new file mode 100644 index 000000000..c1054224f --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Utils.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2012 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 org.nativescript.widgets.image; + +import android.annotation.TargetApi; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.StrictMode; + +/** + * Class containing some static utility methods. + */ +public class Utils { + private Utils() {}; + + public static boolean hasFroyo() { + // Can use static final constants like FROYO, declared in later versions + // of the OS since they are inlined at compile time. This is guaranteed behavior. + return Build.VERSION.SDK_INT >= VERSION_CODES.FROYO; + } + + public static boolean hasGingerbread() { + return Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD; + } + + public static boolean hasHoneycomb() { + return Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB; + } + + public static boolean hasHoneycombMR1() { + return Build.VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1; + } + + public static boolean hasJellyBean() { + return Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN; + } + + public static boolean hasKitKat() { + return Build.VERSION.SDK_INT >= VERSION_CODES.KITKAT; + } + + public static boolean hasN() { + return Build.VERSION.SDK_INT >= VERSION_CODES.N; + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Worker.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Worker.java new file mode 100644 index 000000000..2d940d597 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Image/Worker.java @@ -0,0 +1,542 @@ +/* + * Copyright (C) 2012 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 org.nativescript.widgets.image; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.Log; +import android.widget.ImageView; +import java.lang.ref.WeakReference; + +/** + * This class wraps up completing some arbitrary long running work when loading a bitmap to an + * ImageView. It handles things like using a memory and disk cache, running the work in a background + * thread and setting a placeholder image. + */ +public abstract class Worker { + + protected static final String RESOURCE_PREFIX = "res://"; + protected static final String FILE_PREFIX = "file:///"; + + static final String TAG = "JS"; + private static final int FADE_IN_TIME = 200; + + private Cache mCache; + private Bitmap mLoadingBitmap; + private boolean mFadeInBitmap = true; + private boolean mExitTasksEarly = false; + private final Object mPauseWorkLock = new Object(); + + protected boolean mPauseWork = false; + protected Resources mResources; + + private static final int MESSAGE_CLEAR = 0; + private static final int MESSAGE_INIT_DISK_CACHE = 1; + private static final int MESSAGE_FLUSH = 2; + private static final int MESSAGE_CLOSE = 3; + + protected static int debuggable = -1; + + protected Worker(Context context) { + mResources = context.getResources(); + + // Negative means not initialized. + if (debuggable < 0) { + try { + ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), android.content.pm.PackageManager.GET_META_DATA); + android.os.Bundle bundle = ai.metaData; + Boolean debugLayouts = bundle != null ? bundle.getBoolean("debugImageCache", false) : false; + debuggable = debugLayouts ? 1 : 0; + } catch (PackageManager.NameNotFoundException e) { + debuggable = 0; + Log.e(TAG, "Failed to load meta-data, NameNotFound: " + e.getMessage()); + } catch (NullPointerException e) { + debuggable = 0; + Log.e(TAG, "Failed to load meta-data, NullPointer: " + e.getMessage()); + } + } + } + + public void removeBitmap(String uri) { + if (mCache != null) { + mCache.reduceDisplayedCounter(uri); + } + } + + /** + * Load an image specified by the data parameter into an ImageView (override + * {@link Worker#processBitmap(String, int, int, boolean)} to define the processing logic). A memory and + * disk cache will be used if an {@link Cache} has been added using + * {@link Worker#addImageCache(Cache)}. If the + * image is found in the memory cache, it is set immediately, otherwise an {@link AsyncTask} + * will be created to asynchronously load the bitmap. + * + * @param uri The URI of the image to download. + * @param owner The owner to bind the downloaded image to. + * @param listener A listener that will be called back once the image has been loaded. + */ + public void loadImage(String uri, BitmapOwner owner, int decodeWidth, int decodeHeight, boolean keepAspectRatio, boolean useCache, boolean async, OnImageLoadedListener listener) { + if (uri == null) { + return; + } + + Bitmap value = null; + String cacheUri = uri; + + if (debuggable > 0) { + Log.v(TAG, "loadImage on: " + owner + " to: " + uri); + } + + if (mCache != null && useCache) { + // Create new image cache for images with different decodeHeight/decodeWidth. + cacheUri = createCacheUri(uri, decodeHeight, decodeWidth); + + value = mCache.getBitmapFromMemCache(cacheUri); + } + + if (value == null && !async) { + // Decode sync. + value = processBitmap(uri, decodeWidth, decodeHeight, keepAspectRatio, useCache); + if (value != null) { + if (mCache != null && useCache) { + if (debuggable > 0) { + Log.v(TAG, "loadImage.addBitmapToCache: " + owner + ", src: " + cacheUri); + } + mCache.addBitmapToCache(cacheUri, value); + } + } + } + + if (value != null) { + // Bitmap found in memory cache or loaded sync. + if (debuggable > 0) { + Log.v(TAG, "Set ImageBitmap on: " + owner + " to: " + uri); + } + owner.setBitmap(value); + if (listener != null) { + if (debuggable > 0) { + Log.v(TAG, "OnImageLoadedListener on: " + owner + " to: " + uri); + } + listener.onImageLoaded(true); + } + } else if (cancelPotentialWork(uri, owner)) { + final BitmapWorkerTask task = new BitmapWorkerTask(uri, owner, decodeWidth, decodeHeight, keepAspectRatio, useCache, listener); + final AsyncDrawable asyncDrawable = + new AsyncDrawable(mResources, mLoadingBitmap, task); + owner.setDrawable(asyncDrawable); + + // NOTE: This uses a custom version of AsyncTask that has been pulled from the + // framework and slightly modified. Refer to the docs at the top of the class + // for more info on what was changed. + task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR); + } + } + + /** + * Set placeholder bitmap that shows when the the background thread is running. + * + * @param bitmap + */ + public void setLoadingImage(Bitmap bitmap) { + mLoadingBitmap = bitmap; + } + + /** + * Set placeholder bitmap that shows when the the background thread is running. + * + * @param resId + */ + public void setLoadingImage(int resId) { + mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId); + } + + /** + * Adds an {@link Cache} to this {@link Worker} to handle disk and memory bitmap + * caching. ImageCahce should be initialized before it is passed as parameter. + */ + public void addImageCache(Cache imageCache) { + this.mCache = imageCache; + } + + /** + * If set to true, the image will fade-in once it has been loaded by the background thread. + */ + public void setImageFadeIn(boolean fadeIn) { + mFadeInBitmap = fadeIn; + } + + public void setExitTasksEarly(boolean exitTasksEarly) { + mExitTasksEarly = exitTasksEarly; + setPauseWork(false); + } + + /** + * Subclasses should override this to define any processing or work that must happen to produce + * the final bitmap. This will be executed in a background thread and be long running. For + * example, you could resize a large bitmap here, or pull down an image from the network. + * + * @param uri The URI to identify which image to process, as provided by + * {@link Worker#loadImage(String, BitmapOwner, int, int, boolean, boolean, OnImageLoadedListener)} + * @return The processed bitmap + */ + protected abstract Bitmap processBitmap(String uri, int decodeWidth, int decodeHeight, boolean keepAspectRatio, boolean useCache); + + /** + * @return The {@link Cache} object currently being used by this Worker. + */ + protected Cache getCache() { + return mCache; + } + + /** + * Cancels any pending work attached to the provided ImageView. + * @param owner + */ + public static void cancelWork(BitmapOwner owner) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(owner); + if (bitmapWorkerTask != null) { + bitmapWorkerTask.cancel(true); + if (debuggable > 0) { + Log.v(TAG, "cancelWork - cancelled work for " + bitmapWorkerTask.mUri); + } + } + } + + /** + * Returns true if the current work has been canceled or if there was no work in + * progress on this image view. + * Returns false if the work in progress deals with the same data. The work is not + * stopped in that case. + */ + public static boolean cancelPotentialWork(String uri, BitmapOwner owner) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(owner); + + if (bitmapWorkerTask != null) { + final String mUri = bitmapWorkerTask.mUri; + if (mUri == null || !mUri.equals(uri)) { + bitmapWorkerTask.cancel(true); + if (debuggable > 0) { + Log.v(TAG, "cancelPotentialWork - cancelled work for " + uri); + } + } else { + // The same work is already in progress. + return false; + } + } + return true; + } + + /** + * @param owner The owner that requested the bitmap; + * @return Retrieve the currently active work task (if any) associated with this imageView. + * null if there is no such task. + */ + private static BitmapWorkerTask getBitmapWorkerTask(BitmapOwner owner) { + if (owner != null) { + final Drawable drawable = owner.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + /** + * Create cache key depending on image uri and decode properties. + */ + private static String createCacheUri(String uri, int decodeHeight, int decodeWidth) { + uri += decodeHeight != 0 ? "height%%" + String.valueOf(decodeHeight): ""; + uri += decodeWidth != 0 ? "width%%" + String.valueOf(decodeWidth): ""; + + return uri; + } + + /** + * The actual AsyncTask that will asynchronously process the image. + */ + private class BitmapWorkerTask extends AsyncTask { + private int mDecodeWidth; + private int mDecodeHeight; + private boolean mKeepAspectRatio; + private String mUri; + private String mCacheUri; + private boolean mCacheImage; + private final WeakReference imageViewReference; + private final OnImageLoadedListener mOnImageLoadedListener; + + public BitmapWorkerTask(String uri, BitmapOwner owner, int decodeWidth, int decodeHeight, boolean keepAspectRatio, boolean cacheImage) { + this(uri, owner, decodeWidth, decodeHeight, keepAspectRatio, cacheImage, null); + } + + public BitmapWorkerTask(String uri, BitmapOwner owner, int decodeWidth, int decodeHeight, boolean keepAspectRatio, boolean cacheImage, OnImageLoadedListener listener) { + mDecodeWidth = decodeWidth; + mDecodeHeight = decodeHeight; + mKeepAspectRatio = keepAspectRatio; + mCacheImage = cacheImage; + mUri = uri; + mCacheUri = createCacheUri(uri, decodeHeight, decodeWidth); + imageViewReference = new WeakReference(owner); + mOnImageLoadedListener = listener; + } + + /** + * Background processing. + */ + @Override + protected Bitmap doInBackground(Void... params) { + if (debuggable > 0) { + Log.v(TAG, "doInBackground - starting work: " + imageViewReference.get() + ", on: " + mUri); + } + + + Bitmap bitmap = null; + + // Wait here if work is paused and the task is not cancelled + synchronized (mPauseWorkLock) { + while (mPauseWork && !isCancelled()) { + try { + mPauseWorkLock.wait(); + } catch (InterruptedException e) {} + } + } + + // If the bitmap was not found in the cache and this task has not been cancelled by + // another thread and the ImageView that was originally bound to this task is still + // bound back to this task and our "exit early" flag is not set, then call the main + // process method (as implemented by a subclass) + if (bitmap == null && !isCancelled() && getAttachedOwner() != null + && !mExitTasksEarly) { + bitmap = processBitmap(mUri, mDecodeWidth, mDecodeHeight, mKeepAspectRatio, mCacheImage); + } + + // If the bitmap was processed and the image cache is available, then add the processed + // bitmap to the cache for future use. Note we don't check if the task was cancelled + // here, if it was, and the thread is still running, we may as well add the processed + // bitmap to our cache as it might be used again in the future + if (bitmap != null) { + if (mCache != null && mCacheImage) { + if (debuggable > 0) { + Log.v(TAG, "addBitmapToCache: " + imageViewReference.get() + ", src: " + mCacheUri); + } + mCache.addBitmapToCache(mCacheUri, bitmap); + } + } + + if (debuggable > 0) { + Log.v(TAG, "doInBackground - finished work"); + } + + return bitmap; + } + + /** + * Once the image is processed, associates it to the imageView + */ + @Override + protected void onPostExecute(Bitmap value) { + boolean success = false; + // if cancel was called on this task or the "exit early" flag is set then we're done + if (isCancelled() || mExitTasksEarly) { + value = null; + } + + if (debuggable > 0) { + Log.v(TAG, "onPostExecute - setting bitmap for: " + imageViewReference.get() + " src: " + mUri); + } + + final BitmapOwner owner = getAttachedOwner(); + if (debuggable > 0) { + Log.v(TAG, "onPostExecute - current ImageView: " + owner); + } + + if (value != null && owner != null) { + success = true; + if (debuggable > 0) { + Log.v(TAG, "Set ImageDrawable on: " + owner + " to: " + mUri); + } + owner.setBitmap(value); + } + + if (mOnImageLoadedListener != null) { + if (debuggable > 0) { + Log.v(TAG, "OnImageLoadedListener on: " + owner + " to: " + mUri); + } + mOnImageLoadedListener.onImageLoaded(success); + } + } + + @Override + protected void onCancelled(Bitmap value) { + super.onCancelled(value); + synchronized (mPauseWorkLock) { + mPauseWorkLock.notifyAll(); + } + } + + /** + * Returns the ImageView associated with this task as long as the ImageView's task still + * points to this task as well. Returns null otherwise. + */ + private BitmapOwner getAttachedOwner() { + final BitmapOwner owner = imageViewReference.get(); + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(owner); + + if (this == bitmapWorkerTask) { + return owner; + } + + return null; + } + } + + /** + * Interface definition for callback on image loaded successfully. + */ + public interface OnImageLoadedListener { + + /** + * Called once the image has been loaded. + * @param success True if the image was loaded successfully, false if + * there was an error. + */ + void onImageLoaded(boolean success); + } + + /** + * A custom Drawable that will be attached to the imageView while the work is in progress. + * Contains a reference to the actual worker task, so that it can be stopped if a new binding is + * required, and makes sure that only the last started worker process can bind its result, + * independently of the finish order. + */ + private static class AsyncDrawable extends BitmapDrawable { + private final WeakReference bitmapWorkerTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = + new WeakReference(bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + +// /** +// * Called when the processing is complete and the final drawable should be +// * set on the ImageView. +// * +// * @param imageView +// * @param bitmap +// */ +// private void setImageDrawable(ImageView imageView, Bitmap bitmap) { +// if (mFadeInBitmap) { +// // Transition drawable with a transparent drawable and the final drawable +// final TransitionDrawable td = +// new TransitionDrawable(new Drawable[] { +// new ColorDrawable(0), +// new BitmapDrawable(bitmap) +// }); +// // Set background to loading bitmap +// imageView.setBackgroundDrawable( +// new BitmapDrawable(mResources, mLoadingBitmap)); +// +// imageView.setImageDrawable(td); +// td.startTransition(FADE_IN_TIME); +// } else { +// imageView.setImageBitmap(bitmap); +// } +// } + + /** + * Pause any ongoing background work. This can be used as a temporary + * measure to improve performance. For example background work could + * be paused when a ListView or GridView is being scrolled using a + * {@link android.widget.AbsListView.OnScrollListener} to keep + * scrolling smooth. + *

+ * If work is paused, be sure setPauseWork(false) is called again + * before your fragment or activity is destroyed (for example during + * {@link android.app.Activity#onPause()}), or there is a risk the + * background thread will never finish. + */ + public void setPauseWork(boolean pauseWork) { + synchronized (mPauseWorkLock) { + mPauseWork = pauseWork; + if (!mPauseWork) { + mPauseWorkLock.notifyAll(); + } + } + } + + class CacheAsyncTask extends AsyncTask { + + @Override + protected Void doInBackground(Object... params) { + switch ((Integer)params[0]) { + case MESSAGE_CLEAR: + clearCacheInternal(); + break; + case MESSAGE_INIT_DISK_CACHE: + initDiskCacheInternal(); + break; + case MESSAGE_FLUSH: + flushCacheInternal(); + break; + case MESSAGE_CLOSE: + closeCacheInternal(); + break; + } + return null; + } + } + + protected void clearCacheInternal() { + if (mCache != null) { + mCache.clearCache(); + } + } + + protected abstract void initDiskCacheInternal(); + + protected abstract void flushCacheInternal(); + + protected abstract void closeCacheInternal(); + + public void initCache() { + new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); + } + + public void clearCache() { + new CacheAsyncTask().execute(MESSAGE_CLEAR); + } + + public void flushCache() { + new CacheAsyncTask().execute(MESSAGE_FLUSH); + } + + public void closeCache() { + new CacheAsyncTask().execute(MESSAGE_CLOSE); + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java new file mode 100644 index 000000000..492391870 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ImageView.java @@ -0,0 +1,335 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.graphics.*; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.shapes.RoundRectShape; + +import org.nativescript.widgets.image.BitmapOwner; +import org.nativescript.widgets.image.Fetcher; +import org.nativescript.widgets.image.Worker; +/** + * @author hhristov + */ +public class ImageView extends android.widget.ImageView implements BitmapOwner { + private static final double EPSILON = 1E-05; + + private Path path = new Path(); + private RectF rect = new RectF(); + + private double scaleW = 1; + private double scaleH = 1; + + private float rotationAngle; + + private Matrix mMatrix; + private Bitmap mBitmap; + private String mUri; + private int mDecodeWidth; + private int mDecodeHeight; + private boolean mKeepAspectRatio; + private boolean mUseCache; + private boolean mAsync; + private Worker.OnImageLoadedListener mListener; + private boolean mAttachedToWindow = false; + + public float getRotationAngle() { + return rotationAngle; + } + + public void setRotationAngle(float rotationAngle) { + this.rotationAngle = rotationAngle; + invalidate(); + } + + public ImageView(Context context) { + super(context); + this.mMatrix = new Matrix(); + this.setScaleType(ScaleType.FIT_CENTER); + } + + @Override + protected void onAttachedToWindow() { + mAttachedToWindow = true; + super.onAttachedToWindow(); + this.loadImage(); + } + + @Override + protected void onDetachedFromWindow() { + mAttachedToWindow = false; + super.onDetachedFromWindow(); + if (mUri != null) { + // Clear the bitmap as we are not in the visual tree. + this.setImageBitmap(null); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + Drawable drawable = this.getDrawable(); + int measureWidth; + int measureHeight; + if (drawable != null) { + measureWidth = drawable.getIntrinsicWidth(); + measureHeight = drawable.getIntrinsicHeight(); + } else { + measureWidth = 0; + measureHeight = 0; + } + + boolean finiteWidth = widthMode != MeasureSpec.UNSPECIFIED; + boolean finiteHeight = heightMode != MeasureSpec.UNSPECIFIED; + + if (measureWidth != 0 && measureHeight != 0 && (finiteWidth || finiteHeight)) { + this.computeScaleFactor(width, height, finiteWidth, finiteHeight, measureWidth, measureHeight); + int resultW = (int) Math.round(measureWidth * this.scaleW); + int resultH = (int) Math.round(measureHeight * this.scaleH); + + measureWidth = finiteWidth ? Math.min(resultW, width) : resultW; + measureHeight = finiteHeight ? Math.min(resultH, height) : resultH; + } + + measureWidth += this.getPaddingLeft() + this.getPaddingRight(); + measureHeight += this.getPaddingTop() + this.getPaddingBottom(); + + measureWidth = Math.max(measureWidth, getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, getSuggestedMinimumHeight()); + + if (CommonLayoutParams.debuggable > 0) { + StringBuilder sb = CommonLayoutParams.getStringBuilder(); + sb.append("ImageView onMeasure: "); + sb.append(MeasureSpec.toString(widthMeasureSpec)); + sb.append(", "); + sb.append(MeasureSpec.toString(heightMeasureSpec)); + sb.append(", stretch: "); + sb.append(this.getScaleType()); + sb.append(", measureWidth: "); + sb.append(measureWidth); + sb.append(", measureHeight: "); + sb.append(measureHeight); + + CommonLayoutParams.log(CommonLayoutParams.TAG, sb.toString()); + } + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + private void computeScaleFactor(int measureWidth, int measureHeight, boolean widthIsFinite, boolean heightIsFinite, double nativeWidth, double nativeHeight) { + + this.scaleW = 1; + this.scaleH = 1; + + ScaleType scale = this.getScaleType(); + if ((scale == ScaleType.CENTER_CROP || scale == ScaleType.FIT_CENTER || scale == ScaleType.FIT_XY) && + (widthIsFinite || heightIsFinite)) { + + this.scaleW = (nativeWidth > 0) ? measureWidth / nativeWidth : 0d; + this.scaleH = (nativeHeight > 0) ? measureHeight / nativeHeight : 0d; + + if (!widthIsFinite) { + this.scaleW = scaleH; + } else if (!heightIsFinite) { + this.scaleH = scaleW; + } else { + // No infinite dimensions. + switch (scale) { + case FIT_CENTER: + this.scaleH = this.scaleW < this.scaleH ? this.scaleW : this.scaleH; + this.scaleW = this.scaleH; + break; + case CENTER_CROP: + this.scaleH = this.scaleW > this.scaleH ? this.scaleW : this.scaleH; + this.scaleW = this.scaleH; + break; + default: + break; + } + } + } + } + + public void setUri(String uri, int decodeWidth, int decodeHeight, boolean useCache, boolean async) { + this.setUri(uri, decodeWidth, decodeHeight, false, useCache, async); + } + + public void setUri(String uri, int decodeWidth, int decodeHeight, boolean keepAspectRatio, boolean useCache, boolean async) { + mUri = uri; + mDecodeWidth = decodeWidth; + mDecodeHeight = decodeHeight; + mKeepAspectRatio = keepAspectRatio; + mUseCache = useCache; + mAsync = async; + + // Clear current bitmap only if we set empty URI. + // We support setting bitmap through ImageSource (e.g. Bitmap). + if (uri == null || uri.trim() == "") { + this.setImageBitmap(null); + } + + // Begin loading image only if we are attached to window. + if (mAttachedToWindow) { + loadImage(); + } + } + + public void setImageLoadedListener(Worker.OnImageLoadedListener listener) { + mListener = listener; + } + + private void loadImage() { + Fetcher fetcher = Fetcher.getInstance(this.getContext()); + if (mUri != null && fetcher != null) { + // Get the Bitmap from cache. + fetcher.loadImage(mUri, this, mDecodeWidth, mDecodeHeight, mKeepAspectRatio, mUseCache, mAsync, mListener); + } + } + + @Override + public void setImageBitmap(Bitmap bm) { + Fetcher fetcher = Fetcher.getInstance(this.getContext()); + // if we have existing bitmap from uri notify fetcher that this bitmap is not shown in this ImageView instance. + // This is needed so that fetcher inner cache could reuse the bitmap only when no other ImageView shows it. + if (mUseCache && mUri != null && mBitmap != null && fetcher != null) { + fetcher.removeBitmap(mUri); + } + + super.setImageBitmap(bm); + this.mBitmap = bm; + } + + @Override + protected void onDraw(Canvas canvas) { + BorderDrawable background = this.getBackground() instanceof BorderDrawable ? (BorderDrawable) this.getBackground() : null; + + if (this.mBitmap != null) { + float borderTopLeftRadius, borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius; + + if (background != null) { + background.draw(canvas); + + borderTopLeftRadius = background.getBorderTopLeftRadius(); + borderTopRightRadius = background.getBorderTopRightRadius(); + borderBottomRightRadius = background.getBorderBottomRightRadius(); + borderBottomLeftRadius = background.getBorderBottomLeftRadius(); + } else { + borderTopLeftRadius = borderTopRightRadius = borderBottomRightRadius = borderBottomLeftRadius = 0; + } + + // Padding? + float borderTopWidth = this.getPaddingTop(); + float borderRightWidth = this.getPaddingRight(); + float borderBottomWidth = this.getPaddingBottom(); + float borderLeftWidth = this.getPaddingLeft(); + + float innerWidth, innerHeight; + + float rotationDegree = this.getRotationAngle(); + boolean swap = Math.abs(rotationDegree % 180) > 45 && Math.abs(rotationDegree % 180) < 135; + + innerWidth = this.getWidth() - borderLeftWidth - borderRightWidth; + innerHeight = this.getHeight() - borderTopWidth - borderBottomWidth; + + // TODO: Capture all created objects here in locals and update them instead... + Path path = new Path(); + float[] radii = { + Math.max(0, borderTopLeftRadius - borderLeftWidth), Math.max(0, borderTopLeftRadius - borderTopWidth), + Math.max(0, borderTopRightRadius - borderRightWidth), Math.max(0, borderTopRightRadius - borderTopWidth), + Math.max(0, borderBottomRightRadius - borderRightWidth), Math.max(0, borderBottomRightRadius - borderBottomWidth), + Math.max(0, borderBottomLeftRadius - borderLeftWidth), Math.max(0, borderBottomLeftRadius - borderBottomWidth) + }; + path.addRoundRect(new RectF(borderLeftWidth, borderTopWidth, borderLeftWidth + innerWidth, borderTopWidth + innerHeight), radii, Path.Direction.CW); + + Paint paint = new Paint(); + BitmapShader bitmapShader = new BitmapShader(this.mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + + float bitmapWidth = (float) mBitmap.getWidth(); + float bitmapHeight = (float) mBitmap.getHeight(); + + Matrix matrix = this.mMatrix; + matrix.reset(); + + matrix.postRotate(rotationDegree, bitmapWidth / 2, bitmapHeight / 2); + if (swap) { + matrix.postTranslate((bitmapHeight - bitmapWidth) / 2, (bitmapWidth - bitmapHeight) / 2); + float temp = bitmapWidth; + bitmapWidth = bitmapHeight; + bitmapHeight = temp; + } + + float fittingScaleX = innerWidth / bitmapWidth; + float fittingScaleY = innerHeight / bitmapHeight; + + float uniformScale; + float pivotX, pivotY; + switch(this.getScaleType()) { + case FIT_CENTER: // aspectFit + uniformScale = Math.min(fittingScaleX, fittingScaleY); + matrix.postTranslate((innerWidth - bitmapWidth) / 2, (innerHeight - bitmapHeight) / 2); + matrix.postScale(uniformScale, uniformScale, innerWidth / 2, innerHeight / 2); + canvas.clipRect( + borderLeftWidth + (innerWidth - bitmapWidth * uniformScale) / 2, + borderTopWidth + (innerHeight - bitmapHeight * uniformScale) / 2, + borderLeftWidth + (innerWidth + bitmapWidth * uniformScale) / 2, + borderTopWidth + (innerHeight + bitmapHeight * uniformScale) / 2 + ); + break; + case CENTER_CROP: // aspectFill + uniformScale = Math.max(fittingScaleX, fittingScaleY); + matrix.postTranslate((innerWidth - bitmapWidth) / 2, (innerHeight - bitmapHeight) / 2); + matrix.postScale(uniformScale, uniformScale, innerWidth / 2, innerHeight / 2); + canvas.clipRect( + borderLeftWidth + (innerWidth - bitmapWidth * uniformScale) / 2, + borderTopWidth + (innerHeight - bitmapHeight * uniformScale) / 2, + borderLeftWidth + (innerWidth + bitmapWidth * uniformScale) / 2, + borderTopWidth + (innerHeight + bitmapHeight * uniformScale) / 2 + ); + break; + case FIT_XY: // fill + matrix.postScale(fittingScaleX, fittingScaleY); + break; + case MATRIX: // none + canvas.clipRect( + borderLeftWidth, + borderTopWidth, + borderLeftWidth + bitmapWidth, + borderTopWidth + bitmapHeight + ); + break; + } + matrix.postTranslate(borderLeftWidth, borderTopWidth); + bitmapShader.setLocalMatrix(matrix); + paint.setAntiAlias(true); + paint.setFilterBitmap(true); + paint.setShader(bitmapShader); + ColorFilter filter = this.getColorFilter(); + if (filter != null) { + paint.setColorFilter(filter); + } + canvas.drawPath(path, paint); + } + } + + @Override + public void setBitmap(Bitmap value) { + this.setImageBitmap(value); + } + + @Override + public void setDrawable(Drawable asyncDrawable) { + this.setImageDrawable(asyncDrawable); + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ItemSpec.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ItemSpec.java new file mode 100644 index 000000000..5c923049b --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ItemSpec.java @@ -0,0 +1,60 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public class ItemSpec { + + private int _value; + private GridUnitType _unitType; + + public ItemSpec() { + this(1, GridUnitType.star); + } + + public ItemSpec(int value, GridUnitType unitType) { + this._value = value; + this._unitType = unitType; + } + + GridLayout owner; + int _actualLength = 0; + + @Override + public boolean equals(Object o) { + if (!(o instanceof ItemSpec)) { + return false; + } + + ItemSpec other = (ItemSpec)o; + return (this._unitType == other._unitType) && (this._value == other._value) && (this.owner == other.owner); + } + + public GridUnitType getGridUnitType() { + return this._unitType; + } + + public boolean getIsAbsolute() { + return this._unitType == GridUnitType.pixel; + } + + public boolean getIsAuto() { + return this._unitType == GridUnitType.auto; + } + + public boolean getIsStar() { + return this._unitType == GridUnitType.star; + } + + public int getValue() { + return this._value; + } + + public int getActualLength() { + return this._actualLength; + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java new file mode 100644 index 000000000..ee9e3227c --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/LayoutBase.java @@ -0,0 +1,94 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * @author hhristov + * + */ +public abstract class LayoutBase extends ViewGroup { + + public LayoutBase(Context context) { + super(context); + } + + private boolean passThroughParent; + + public boolean getPassThroughParent() { return this.passThroughParent; } + public void setPassThroughParent(boolean value) { this.passThroughParent = value; } + + @Override + protected LayoutParams generateDefaultLayoutParams() { + return new CommonLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new CommonLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkLayoutParams(LayoutParams p) { + return p instanceof CommonLayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams from) { + if (from instanceof CommonLayoutParams) + return new CommonLayoutParams((CommonLayoutParams)from); + + if (from instanceof FrameLayout.LayoutParams) + return new CommonLayoutParams((FrameLayout.LayoutParams)from); + + if (from instanceof ViewGroup.MarginLayoutParams) + return new CommonLayoutParams((ViewGroup.MarginLayoutParams)from); + + return new CommonLayoutParams(from); + } + + @Override + public boolean shouldDelayChildPressedState() { + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (!this.passThroughParent) { + return super.onTouchEvent(event); + } + + // LayoutBase.onTouchEvent(ev) execution means no interactive child view handled + // the event so we let the event pass through to parent view of the layout container + // because passThroughParent is set to true + return false; + } + + protected static int getGravity(View view) { + int gravity = -1; + LayoutParams params = view.getLayoutParams(); + if (params instanceof FrameLayout.LayoutParams) { + gravity = ((FrameLayout.LayoutParams)params).gravity; + } + + if (gravity == -1) { + gravity = Gravity.FILL; + } + + return gravity; + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/LinearGradientDefinition.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/LinearGradientDefinition.java new file mode 100644 index 000000000..46d818680 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/LinearGradientDefinition.java @@ -0,0 +1,29 @@ +package org.nativescript.widgets; + +/** + * Created by Vultix on 3/12/2018. + */ +public class LinearGradientDefinition { + private float startX; + private float startY; + private float endX; + private float endY; + private int[] colors; + private float[] stops; + + public float getStartX() { return startX; } + public float getStartY() { return startY; } + public float getEndX() { return endX; } + public float getEndY() { return endY; } + public int[] getColors() { return colors; } + public float[] getStops() { return stops; } + + public LinearGradientDefinition(float startX, float startY, float endX, float endY, int[] colors, float[] stops) { + this.startX = startX; + this.startY = startY; + this.endX = endX; + this.endY = endY; + this.colors = colors; + this.stops = stops; + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Orientation.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Orientation.java new file mode 100644 index 000000000..273fff8c0 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/Orientation.java @@ -0,0 +1,13 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author hhristov + * + */ +public enum Orientation { + horizontal, + vertical +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/OriginPoint.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/OriginPoint.java new file mode 100644 index 000000000..6ea55395a --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/OriginPoint.java @@ -0,0 +1,73 @@ +package org.nativescript.widgets; + +import android.view.View; +import java.util.WeakHashMap; + +/** + * A class encapsulating the logic of setting an origin point. + * The origin acts as a pivot point but is relative to the View size, + * Where 0 is the left or top of the view, 0.5 is at the middle and 1 is right or bottom. + * Under the hood the pivot point is set but is updated when the View's layout is changed. + */ +public class OriginPoint { + private static WeakHashMap layoutListeners; + + public static void setX(View view, float x) { + getSetter(view).setOriginX(view, x); + } + + public static void setY(View view, float y) { + getSetter(view).setOriginY(view, y); + } + + private static PivotSetter getSetter(View view) { + PivotSetter setter = null; + + if (layoutListeners == null) { + layoutListeners = new WeakHashMap<>(); + } else { + setter = layoutListeners.get(view); + } + + if (setter == null) { + setter = new PivotSetter(); + view.addOnLayoutChangeListener(setter); + layoutListeners.put(view, setter); + } + + return setter; + } + + private static class PivotSetter implements View.OnLayoutChangeListener { + private float originX; + private float originY; + + public PivotSetter() { + originX = 0.5f; + originY = 0.5f; + } + + public void setOriginX(View view, float x) { + originX = x; + updateX(view, view.getWidth()); + } + + public void setOriginY(View view, float y) { + originY = y; + updateY(view, view.getHeight()); + } + + public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { + updateX(view, right - left); + updateY(view, bottom - top); + } + + private void updateX(View view, int width) { + view.setPivotX(originX * width); + } + + private void updateY(View view, int height) { + view.setPivotY(originY * height); + } + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/SegmentedBarColorDrawable.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/SegmentedBarColorDrawable.java new file mode 100644 index 000000000..525a7282d --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/SegmentedBarColorDrawable.java @@ -0,0 +1,26 @@ +package org.nativescript.widgets; + +import android.graphics.Paint; +import android.graphics.drawable.ColorDrawable; +import android.support.annotation.ColorInt; + +/** + * Created by hhristov on 2/23/17. + */ + +public class SegmentedBarColorDrawable extends ColorDrawable { + + private float thickness; + + public SegmentedBarColorDrawable(@ColorInt int color, float thickness) { + super(color); + this.thickness = thickness; + } + + public void draw(android.graphics.Canvas canvas) { + Paint p = new Paint(); + p.setColor(this.getColor()); + p.setStyle(android.graphics.Paint.Style.FILL); + canvas.drawRect(0, this.getBounds().height() - thickness, this.getBounds().width(), this.getBounds().height(), p); + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/StackLayout.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/StackLayout.java new file mode 100644 index 000000000..9a4b1498a --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/StackLayout.java @@ -0,0 +1,235 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.util.Log; + +/** + * @author hhristov + * + */ +public class StackLayout extends LayoutBase { + static final String TAG = "JS"; + private int _totalLength = 0; + private Orientation _orientation = Orientation.vertical; + + public StackLayout(Context context) { + super(context); + } + + public Orientation getOrientation() { + return this._orientation; + } + public void setOrientation(Orientation value) { + this._orientation = value; + this.requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int childState = 0; + int measureWidth = 0; + int measureHeight = 0; + + int width = MeasureSpec.getSize(widthMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + + int height = MeasureSpec.getSize(heightMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + boolean isVertical = this._orientation == Orientation.vertical; + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + int count = this.getChildCount(); + int measureSpecMode; + int remainingLength; + + int mode = isVertical ? heightMode : widthMode; + if (mode == MeasureSpec.UNSPECIFIED) { + measureSpecMode = MeasureSpec.UNSPECIFIED; + remainingLength = 0; + } + else { + measureSpecMode = MeasureSpec.AT_MOST; + remainingLength = isVertical ? height - verticalPadding : width - horizontalPadding; + } + + int childMeasureSpec; + if (isVertical) { + int childWidth = (widthMode == MeasureSpec.UNSPECIFIED) ? 0 : width - horizontalPadding; + childWidth = Math.max(0, childWidth); + childMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, widthMode); + } + else { + int childHeight = (heightMode == MeasureSpec.UNSPECIFIED) ? 0 : height - verticalPadding; + childHeight = Math.max(0, childHeight); + childMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, heightMode); + } + + for (int i = 0; i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + if (isVertical) { + // Measuring android.widget.ListView, with no height property set, with MeasureSpec.AT_MOST will + // result in height required for all list view items or the maximum available space for the StackLayout. + // Any following controls will be visible only if enough space left. + CommonLayoutParams.measureChild(child, childMeasureSpec, MeasureSpec.makeMeasureSpec(remainingLength, measureSpecMode)); + + if(measureSpecMode == MeasureSpec.AT_MOST && this.isUnsizedScrollableView(child)){ + Log.e(TAG, "Avoid using ListView or ScrollView with no explicit height set inside StackLayout. Doing so might results in poor user interface performance and a poor user experience."); + } + + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + measureWidth = Math.max(measureWidth, childMeasuredWidth); + measureHeight += childMeasuredHeight; + remainingLength = Math.max(0, remainingLength - childMeasuredHeight); + } + else { + CommonLayoutParams.measureChild(child, MeasureSpec.makeMeasureSpec(remainingLength, measureSpecMode), childMeasureSpec); + final int childMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + final int childMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + measureHeight = Math.max(measureHeight, childMeasuredHeight); + measureWidth += childMeasuredWidth; + remainingLength = Math.max(0, remainingLength - childMeasuredWidth); + } + + childState = combineMeasuredStates(childState, child.getMeasuredState()); + } + + // Add in our padding + measureWidth += horizontalPadding; + measureHeight += verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + this._totalLength = isVertical ? measureHeight : measureWidth; + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, isVertical ? childState : 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, isVertical ? 0 : childState); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (this._orientation == Orientation.vertical) { + this.layoutVertical(l, t, r, b); + } + else { + this.layoutHorizontal(l, t, r, b); + } + + CommonLayoutParams.restoreOriginalParams(this); + } + + private void layoutVertical(int left, int top, int right, int bottom) { + + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childTop = 0; + int childLeft = paddingLeft; + int childRight = right - left - paddingRight; + + int gravity = LayoutBase.getGravity(this); + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (verticalGravity) { + case Gravity.CENTER_VERTICAL: + childTop = (bottom - top - this._totalLength) / 2 + paddingTop - paddingBottom; + break; + + case Gravity.BOTTOM: + childTop = bottom - top - this._totalLength + paddingTop - paddingBottom; + break; + + case Gravity.TOP: + case Gravity.FILL_VERTICAL: + default: + childTop = paddingTop; + break; + } + + for (int i = 0, count = this.getChildCount(); i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + int childHeight = CommonLayoutParams.getDesiredHeight(child); + CommonLayoutParams.layoutChild(child, childLeft, childTop, childRight, childTop + childHeight); + childTop += childHeight; + } + } + + @SuppressLint("RtlHardcoded") + private void layoutHorizontal(int left, int top, int right, int bottom) { + + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childTop = paddingTop; + int childLeft = 0; + int childBottom = bottom - top - paddingBottom; + + int gravity = LayoutBase.getGravity(this); + final int horizontalGravity = Gravity.getAbsoluteGravity(gravity, this.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK; + + switch (horizontalGravity) { + case Gravity.CENTER_HORIZONTAL: + childLeft = (right - left - this._totalLength) / 2 + paddingLeft - paddingRight; + break; + + case Gravity.RIGHT: + childLeft = right - left - this._totalLength + paddingLeft - paddingRight; + break; + + case Gravity.LEFT: + case Gravity.FILL_HORIZONTAL: + default: + childLeft = paddingLeft; + break; + } + + for (int i = 0, count = this.getChildCount(); i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + int childWidth = CommonLayoutParams.getDesiredWidth(child); + CommonLayoutParams.layoutChild(child, childLeft, childTop, childLeft + childWidth, childBottom); + childLeft += childWidth; + } + } + + private boolean isUnsizedScrollableView(View child) { + LayoutParams childLayoutParams = child.getLayoutParams(); + + if (childLayoutParams.height == -1 && (child instanceof android.widget.ListView || child instanceof org.nativescript.widgets.VerticalScrollView)) { + return true; + } + + return false; + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabItemSpec.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabItemSpec.java new file mode 100644 index 000000000..dc6881f71 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabItemSpec.java @@ -0,0 +1,9 @@ +package org.nativescript.widgets; + +import android.graphics.drawable.Drawable; + +public class TabItemSpec { + public String title; + public int iconId; + public Drawable iconDrawable; +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabLayout.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabLayout.java new file mode 100644 index 000000000..285bec277 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabLayout.java @@ -0,0 +1,420 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nativescript.widgets; + +import android.content.Context; +import android.graphics.Typeface; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.HorizontalScrollView; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * To be used with ViewPager to provide a tab indicator component which give + * constant feedback as to the user's scroll progress. + *

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

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

+ */ +public class TabLayout extends HorizontalScrollView { + /** + * Allows complete control over the colors drawn in the tab layout. Set with + * {@link #setCustomTabColorizer(TabColorizer)}. + */ + public interface TabColorizer { + + /** + * @return return the color of the indicator used when {@code position} + * is selected. + */ + int getIndicatorColor(int position); + + } + + private static final int TITLE_OFFSET_DIPS = 24; + private static final int TAB_VIEW_PADDING_DIPS = 16; + private static final int TAB_VIEW_TEXT_SIZE_SP = 12; + private static final int TEXT_MAX_WIDTH = 180; + private static final int SMALL_MIN_HEIGHT = 48; + private static final int LARGE_MIN_HEIGHT = 72; + + private int mTitleOffset; + + private boolean mDistributeEvenly = true; + + private TabItemSpec[] mTabItems; + private ViewPager mViewPager; + private SparseArray mContentDescriptions = new SparseArray(); + private ViewPager.OnPageChangeListener mViewPagerPageChangeListener; + + private final TabStrip mTabStrip; + + public TabLayout(Context context) { + this(context, null); + } + + public TabLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public TabLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + // Disable the Scroll Bar + setHorizontalScrollBarEnabled(false); + // Make sure that the Tab Strips fills this View + setFillViewport(true); + + mTitleOffset = (int) (TITLE_OFFSET_DIPS * getResources().getDisplayMetrics().density); + + mTabStrip = new TabStrip(context); + addView(mTabStrip, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + } + + /** + * Set the custom {@link TabColorizer} to be used. + * + * If you only require simple customisation then you can use + * {@link #setSelectedIndicatorColors(int...)} to achieve similar effects. + */ + public void setCustomTabColorizer(TabColorizer tabColorizer) { + mTabStrip.setCustomTabColorizer(tabColorizer); + } + + public void setDistributeEvenly(boolean distributeEvenly) { + mDistributeEvenly = distributeEvenly; + } + + /** + * Sets the colors to be used for indicating the selected tab. These colors + * are treated as a circular array. Providing one color will mean that all + * tabs are indicated with the same color. + */ + public void setSelectedIndicatorColors(int... colors) { + mTabStrip.setSelectedIndicatorColors(colors); + this.mSelectedIndicatorColors = colors; + } + + private int[] mSelectedIndicatorColors; + public int[] getSelectedIndicatorColors() { + return this.mSelectedIndicatorColors; + } + + public void setTabTextColor(int color){ + mTabStrip.setTabTextColor(color); + } + + public int getTabTextColor(){ + return mTabStrip.getTabTextColor(); + } + + public void setSelectedTabTextColor(int color){ + mTabStrip.setSelectedTabTextColor(color); + } + + public int getSelectedTabTextColor(){ + return mTabStrip.getSelectedTabTextColor(); + } + + public void setTabTextFontSize(float fontSize){ + mTabStrip.setTabTextFontSize(fontSize); + } + + public float getTabTextFontSize(){ + return mTabStrip.getTabTextFontSize(); + } + + /** + * Set the {@link ViewPager.OnPageChangeListener}. When using + * {@link TabLayout} you are required to set any + * {@link ViewPager.OnPageChangeListener} through this method. This is so + * that the layout can update it's scroll position correctly. + * + * @see ViewPager#setOnPageChangeListener(ViewPager.OnPageChangeListener) + */ + public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) { + mViewPagerPageChangeListener = listener; + } + + /** + * Sets the associated view pager. Note that the assumption here is that the + * pager content (number of tabs and tab titles) does not change after this + * call has been made. + */ + public void setViewPager(ViewPager viewPager) { + this.setItems(null, viewPager); + } + + public void setItems(TabItemSpec[] items, ViewPager viewPager) { + mTabStrip.removeAllViews(); + + mViewPager = viewPager; + mTabItems = items; + if (viewPager != null) { + viewPager.addOnPageChangeListener(new InternalViewPagerListener()); + populateTabStrip(); + } + } + + /** + * Updates the UI of an item at specified index + */ + public void updateItemAt(int position, TabItemSpec tabItem) { + LinearLayout ll = (LinearLayout)mTabStrip.getChildAt(position); + ImageView imgView = (ImageView)ll.getChildAt(0); + TextView textView = (TextView)ll.getChildAt(1); + this.setupItem(ll, textView, imgView, tabItem); + } + + /** + * Gets the TextView for tab item at index + */ + public TextView getTextViewForItemAt(int index){ + LinearLayout ll = this.getViewForItemAt(index); + return (ll != null) ? (TextView)ll.getChildAt(1) : null; + } + + /** + * Gets the LinearLayout container for tab item at index + */ + public LinearLayout getViewForItemAt(int index){ + LinearLayout result = null; + + if(this.mTabStrip.getChildCount() > index){ + result = (LinearLayout)this.mTabStrip.getChildAt(index); + } + + return result; + } + + /** + * Gets the number of realized tabs. + */ + public int getItemCount(){ + return this.mTabStrip.getChildCount(); + } + + /** + * Create a default view to be used for tabs. + */ + protected View createDefaultTabView(Context context, TabItemSpec tabItem) { + float density = getResources().getDisplayMetrics().density; + int padding = (int) (TAB_VIEW_PADDING_DIPS * density); + + LinearLayout ll = new LinearLayout(context); + ll.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); + ll.setGravity(Gravity.CENTER); + ll.setOrientation(LinearLayout.VERTICAL); + TypedValue outValue = new TypedValue(); + getContext().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true); + ll.setBackgroundResource(outValue.resourceId); + + ImageView imgView = new ImageView(context); + imgView.setScaleType(ScaleType.FIT_CENTER); + LinearLayout.LayoutParams imgLP = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + imgLP.gravity = Gravity.CENTER; + imgView.setLayoutParams(imgLP); + + TextView textView = new TextView(context); + textView.setGravity(Gravity.CENTER); + textView.setMaxWidth((int) (TEXT_MAX_WIDTH * density)); + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, TAB_VIEW_TEXT_SIZE_SP); + textView.setTypeface(Typeface.DEFAULT_BOLD); + textView.setEllipsize(TextUtils.TruncateAt.END); + textView.setAllCaps(true); + textView.setMaxLines(2); + textView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + textView.setPadding(padding, 0, padding, 0); + + this.setupItem(ll, textView, imgView, tabItem); + + ll.addView(imgView); + ll.addView(textView); + return ll; + } + + private void setupItem(LinearLayout ll, TextView textView,ImageView imgView, TabItemSpec tabItem){ + float density = getResources().getDisplayMetrics().density; + + if (tabItem.iconId != 0) { + imgView.setImageResource(tabItem.iconId); + imgView.setVisibility(VISIBLE); + } else if (tabItem.iconDrawable != null) { + imgView.setImageDrawable(tabItem.iconDrawable); + imgView.setVisibility(VISIBLE); + } else { + imgView.setVisibility(GONE); + } + + if (tabItem.title != null && !tabItem.title.isEmpty()) { + textView.setText(tabItem.title); + textView.setVisibility(VISIBLE); + } else { + textView.setVisibility(GONE); + } + + if (imgView.getVisibility() == VISIBLE && textView.getVisibility() == VISIBLE) { + ll.setMinimumHeight((int) (LARGE_MIN_HEIGHT * density)); + } else { + ll.setMinimumHeight((int) (SMALL_MIN_HEIGHT * density)); + } + + if (mDistributeEvenly) { + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) ll.getLayoutParams(); + lp.width = 0; + lp.weight = 1; + } + } + + private void populateTabStrip() { + final PagerAdapter adapter = mViewPager.getAdapter(); + final OnClickListener tabClickListener = new TabClickListener(); + + for (int i = 0; i < adapter.getCount(); i++) { + View tabView = null; + + TabItemSpec tabItem; + if (this.mTabItems != null && this.mTabItems.length > i) { + tabItem = this.mTabItems[i]; + } else { + tabItem = new TabItemSpec(); + tabItem.title = adapter.getPageTitle(i).toString(); + } + + tabView = createDefaultTabView(getContext(), tabItem); + + tabView.setOnClickListener(tabClickListener); + String desc = mContentDescriptions.get(i, null); + if (desc != null) { + tabView.setContentDescription(desc); + } + + mTabStrip.addView(tabView); + if (i == mViewPager.getCurrentItem()) { + tabView.setSelected(true); + } + } + } + + public void setContentDescription(int i, String desc) { + mContentDescriptions.put(i, desc); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mViewPager != null) { + scrollToTab(mViewPager.getCurrentItem(), 0); + } + } + + private void scrollToTab(int tabIndex, int positionOffset) { + final int tabStripChildCount = mTabStrip.getChildCount(); + if (tabStripChildCount == 0 || tabIndex < 0 || tabIndex >= tabStripChildCount) { + return; + } + + View selectedChild = mTabStrip.getChildAt(tabIndex); + if (selectedChild != null) { + int targetScrollX = selectedChild.getLeft() + positionOffset; + + if (tabIndex > 0 || positionOffset > 0) { + // If we're not at the first child and are mid-scroll, make sure + // we obey the offset + targetScrollX -= mTitleOffset; + } + + scrollTo(targetScrollX, 0); + } + } + + private class InternalViewPagerListener implements ViewPager.OnPageChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabStripChildCount = mTabStrip.getChildCount(); + if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) { + return; + } + + mTabStrip.onViewPagerPageChanged(position, positionOffset); + + View selectedTitle = mTabStrip.getChildAt(position); + int extraOffset = (selectedTitle != null) ? (int) (positionOffset * selectedTitle.getWidth()) : 0; + scrollToTab(position, extraOffset); + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageScrollStateChanged(state); + } + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + mTabStrip.onViewPagerPageChanged(position, 0f); + scrollToTab(position, 0); + } + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + mTabStrip.getChildAt(i).setSelected(position == i); + } + if (mViewPagerPageChangeListener != null) { + mViewPagerPageChangeListener.onPageSelected(position); + } + } + + } + + private class TabClickListener implements OnClickListener { + @Override + public void onClick(View v) { + for (int i = 0; i < mTabStrip.getChildCount(); i++) { + if (v == mTabStrip.getChildAt(i)) { + mViewPager.setCurrentItem(i); + return; + } + } + } + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabStrip.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabStrip.java new file mode 100644 index 000000000..b608df582 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabStrip.java @@ -0,0 +1,234 @@ +/* + * Copyright 2014 Google Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.nativescript.widgets; + + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +class TabStrip extends LinearLayout { + + private static final int DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS = 0; + private static final byte DEFAULT_BOTTOM_BORDER_COLOR_ALPHA = 0x26; + private static final int SELECTED_INDICATOR_THICKNESS_DIPS = 3; + private static final int DEFAULT_SELECTED_INDICATOR_COLOR = 0xFF33B5E5; + + private final int mBottomBorderThickness; + private final Paint mBottomBorderPaint; + + private final int mSelectedIndicatorThickness; + private final Paint mSelectedIndicatorPaint; + + private final int mDefaultBottomBorderColor; + + private int mSelectedPosition; + private float mSelectionOffset; + + private TabLayout.TabColorizer mCustomTabColorizer; + private final SimpleTabColorizer mDefaultTabColorizer; + + private int mTabTextColor; + private int mSelectedTabTextColor; + private float mTabTextFontSize; + + TabStrip(Context context) { + this(context, null); + } + + TabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + + setWillNotDraw(false); + + final float density = getResources().getDisplayMetrics().density; + + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.colorForeground, outValue, true); + final int themeForegroundColor = outValue.data; + + mDefaultBottomBorderColor = setColorAlpha(themeForegroundColor, + DEFAULT_BOTTOM_BORDER_COLOR_ALPHA); + + mDefaultTabColorizer = new SimpleTabColorizer(); + mDefaultTabColorizer.setIndicatorColors(DEFAULT_SELECTED_INDICATOR_COLOR); + + mBottomBorderThickness = (int) (DEFAULT_BOTTOM_BORDER_THICKNESS_DIPS * density); + mBottomBorderPaint = new Paint(); + mBottomBorderPaint.setColor(mDefaultBottomBorderColor); + + mSelectedIndicatorThickness = (int) (SELECTED_INDICATOR_THICKNESS_DIPS * density); + mSelectedIndicatorPaint = new Paint(); + + TextView defaultTextView = new TextView(context); + mTabTextColor = defaultTextView.getTextColors().getDefaultColor(); + mTabTextFontSize = defaultTextView.getTextSize(); + + // Default selected color is the same as mTabTextColor + mSelectedTabTextColor = mTabTextColor; + + setMeasureWithLargestChildEnabled(true); + } + + void setCustomTabColorizer(TabLayout.TabColorizer customTabColorizer) { + mCustomTabColorizer = customTabColorizer; + invalidate(); + } + + void setSelectedIndicatorColors(int... colors) { + // Make sure that the custom colorizer is removed + mCustomTabColorizer = null; + mDefaultTabColorizer.setIndicatorColors(colors); + invalidate(); + } + + void setTabTextColor(int color){ + mTabTextColor = color; + updateTabsTextColor(); + } + + int getTabTextColor(){ + return mTabTextColor; + } + + void setSelectedTabTextColor(int color){ + mSelectedTabTextColor = color; + updateTabsTextColor(); + } + + int getSelectedTabTextColor(){ + return mSelectedTabTextColor; + } + + private void updateTabsTextColor(){ + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++){ + LinearLayout linearLayout = (LinearLayout)getChildAt(i); + TextView textView = (TextView)linearLayout.getChildAt(1); + if (i == mSelectedPosition){ + textView.setTextColor(mSelectedTabTextColor); + } + else { + textView.setTextColor(mTabTextColor); + } + } + } + + void setTabTextFontSize(float fontSize){ + mTabTextFontSize = fontSize; + updateTabsTextFontSize(); + } + + float getTabTextFontSize(){ + return mTabTextFontSize; + } + + private void updateTabsTextFontSize(){ + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++){ + LinearLayout linearLayout = (LinearLayout)getChildAt(i); + TextView textView = (TextView)linearLayout.getChildAt(1); + textView.setTextSize(mTabTextFontSize); + } + } + + void onViewPagerPageChanged(int position, float positionOffset) { + mSelectedPosition = position; + mSelectionOffset = positionOffset; + invalidate(); + updateTabsTextColor(); + } + + @Override + protected void onDraw(Canvas canvas) { + final int height = getHeight(); + final int childCount = getChildCount(); + final TabLayout.TabColorizer tabColorizer = mCustomTabColorizer != null + ? mCustomTabColorizer + : mDefaultTabColorizer; + + // Thick colored underline below the current selection + if (childCount > 0) { + View selectedTitle = getChildAt(mSelectedPosition); + int left = selectedTitle.getLeft(); + int right = selectedTitle.getRight(); + int color = tabColorizer.getIndicatorColor(mSelectedPosition); + + if (mSelectionOffset > 0f && mSelectedPosition < (getChildCount() - 1)) { + int nextColor = tabColorizer.getIndicatorColor(mSelectedPosition + 1); + if (color != nextColor) { + color = blendColors(nextColor, color, mSelectionOffset); + } + + // Draw the selection partway between the tabs + View nextTitle = getChildAt(mSelectedPosition + 1); + left = (int) (mSelectionOffset * nextTitle.getLeft() + + (1.0f - mSelectionOffset) * left); + right = (int) (mSelectionOffset * nextTitle.getRight() + + (1.0f - mSelectionOffset) * right); + } + + mSelectedIndicatorPaint.setColor(color); + + canvas.drawRect(left, height - mSelectedIndicatorThickness, right, + height, mSelectedIndicatorPaint); + } + + // Thin underline along the entire bottom edge + canvas.drawRect(0, height - mBottomBorderThickness, getWidth(), height, mBottomBorderPaint); + } + + /** + * Set the alpha value of the {@code color} to be the given {@code alpha} value. + */ + private static int setColorAlpha(int color, byte alpha) { + return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + } + + /** + * Blend {@code color1} and {@code color2} using the given ratio. + * + * @param ratio of which to blend. 1.0 will return {@code color1}, 0.5 will give an even blend, + * 0.0 will return {@code color2}. + */ + private static int blendColors(int color1, int color2, float ratio) { + final float inverseRation = 1f - ratio; + float r = (Color.red(color1) * ratio) + (Color.red(color2) * inverseRation); + float g = (Color.green(color1) * ratio) + (Color.green(color2) * inverseRation); + float b = (Color.blue(color1) * ratio) + (Color.blue(color2) * inverseRation); + return Color.rgb((int) r, (int) g, (int) b); + } + + private static class SimpleTabColorizer implements TabLayout.TabColorizer { + private int[] mIndicatorColors; + + @Override + public final int getIndicatorColor(int position) { + return mIndicatorColors[position % mIndicatorColors.length]; + } + + void setIndicatorColors(int... colors) { + mIndicatorColors = colors; + } + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabViewPager.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabViewPager.java new file mode 100644 index 000000000..75a3b447a --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/TabViewPager.java @@ -0,0 +1,60 @@ +/** + * + */ +package org.nativescript.widgets; + +import android.content.Context; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.View; +import android.view.MotionEvent; +import android.view.KeyEvent; + +// See this thread for more information https://stackoverflow.com/questions/9650265 +public class TabViewPager extends ViewPager { + private boolean swipePageEnabled = true; + + public TabViewPager(Context context) { + super(context); + } + + public TabViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public void setSwipePageEnabled(boolean enabled) { + this.swipePageEnabled = enabled; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (this.swipePageEnabled) { + return super.onInterceptTouchEvent(event); + } + + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + if (this.swipePageEnabled) { + return super.onTouchEvent(event); + } + + return false; + } + + @Override + public boolean executeKeyEvent(KeyEvent event) { + if (this.swipePageEnabled) { + return super.executeKeyEvent(event); + } + + return false; + } + + @Override + public void setCurrentItem(int item) { + super.setCurrentItem(item, this.swipePageEnabled); + } +} diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/VerticalScrollView.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/VerticalScrollView.java new file mode 100644 index 000000000..db109467c --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/VerticalScrollView.java @@ -0,0 +1,258 @@ +/** + * + */ +package org.nativescript.widgets; + +import org.nativescript.widgets.HorizontalScrollView.SavedState; +import android.content.Context; +import android.graphics.Rect; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ScrollView; + +/** + * @author hhristov + * + */ +public class VerticalScrollView extends ScrollView { + + private final Rect mTempRect = new Rect(); + + private int contentMeasuredWidth = 0; + private int contentMeasuredHeight = 0; + private int scrollableLength = 0; + private SavedState mSavedState; + private boolean isFirstLayout = true; + private boolean scrollEnabled = true; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + public VerticalScrollView(Context context) { + super(context); + } + + public int getScrollableLength() { + return this.scrollableLength; + } + + public boolean getScrollEnabled() { + return this.scrollEnabled; + } + + public void setScrollEnabled(boolean value) { + this.scrollEnabled = value; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + // Do nothing with intercepted touch events if we are not scrollable + if (!this.scrollEnabled) { + return false; + } + + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!this.scrollEnabled && ev.getAction() == MotionEvent.ACTION_DOWN) { + return false; + } + + return super.onTouchEvent(ev); + } + + @Override + public void requestLayout() { + this.mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected CommonLayoutParams generateDefaultLayoutParams() { + return new CommonLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + public CommonLayoutParams generateLayoutParams(AttributeSet attrs) { + return new CommonLayoutParams(); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof CommonLayoutParams; + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams from) { + if (from instanceof CommonLayoutParams) + return new CommonLayoutParams((CommonLayoutParams)from); + + if (from instanceof FrameLayout.LayoutParams) + return new CommonLayoutParams((FrameLayout.LayoutParams)from); + + if (from instanceof ViewGroup.MarginLayoutParams) + return new CommonLayoutParams((ViewGroup.MarginLayoutParams)from); + + return new CommonLayoutParams(from); + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!this.mIsLayoutDirty) { + this.scrollToChild(focused); + } + else { + // The child may not be laid out yet, we can't compute the scroll yet + this.mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + // Don't call measure because it will measure content twice. + // ScrollView is expected to have single child so we measure only the first child. + View child = this.getChildCount() > 0 ? this.getChildAt(0) : null; + if (child == null) { + this.scrollableLength = 0; + this.contentMeasuredWidth = 0; + this.contentMeasuredHeight = 0; + this.setPadding(0, 0, 0, 0); + } + else { + CommonLayoutParams.measureChild(child, widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + this.contentMeasuredWidth = CommonLayoutParams.getDesiredWidth(child); + this.contentMeasuredHeight = CommonLayoutParams.getDesiredHeight(child); + + // Android ScrollView does not account to child margins so we set them as paddings. Otherwise you can never scroll to bottom. + CommonLayoutParams lp = (CommonLayoutParams)child.getLayoutParams(); + this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); + } + + // Don't add in our paddings because they are already added as child margins. (we will include them twice if we add them). + // check the previous line - this.setPadding(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); +// this.contentMeasuredWidth += this.getPaddingLeft() + this.getPaddingRight(); +// this.contentMeasuredHeight += this.getPaddingTop() + this.getPaddingBottom(); + + // Check against our minimum height + this.contentMeasuredWidth = Math.max(this.contentMeasuredWidth, this.getSuggestedMinimumWidth()); + this.contentMeasuredHeight = Math.max(this.contentMeasuredHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(this.contentMeasuredWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(this.contentMeasuredHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + int childHeight = 0; + if (this.getChildCount() > 0) { + View child = this.getChildAt(0); + childHeight = child.getMeasuredHeight(); + + int width = right - left; + int height = bottom - top; + + this.scrollableLength = this.contentMeasuredHeight - height; + CommonLayoutParams.layoutChild(child, 0, 0, width, Math.max(this.contentMeasuredHeight, height)); + this.scrollableLength = Math.max(0, this.scrollableLength); + } + + this.mIsLayoutDirty = false; + // Give a child focus if it needs it + if (this.mChildToScrollTo != null && HorizontalScrollView.isViewDescendantOf(this.mChildToScrollTo, this)) { + this.scrollToChild(this.mChildToScrollTo); + } + + this.mChildToScrollTo = null; + + int scrollX = this.getScrollX(); + int scrollY = this.getScrollY(); + if (this.isFirstLayout) { + this.isFirstLayout = false; + + final int scrollRange = Math.max(0, childHeight - (bottom - top - this.getPaddingTop() - this.getPaddingBottom())); + if (this.mSavedState != null) { + scrollY = mSavedState.scrollPosition; + mSavedState = null; + } + + // Don't forget to clamp + if (scrollY > scrollRange) { + scrollY = scrollRange; + } else if (scrollY < 0) { + scrollY = 0; + } + } + + // Calling this with the present values causes it to re-claim them + this.scrollTo(scrollX, scrollY); + + CommonLayoutParams.restoreOriginalParams(this); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + this.isFirstLayout = true; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + this.mSavedState = ss; + this.requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = this.getScrollY(); + return ss; + } + + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + if (scrollDelta != 0) { + this.scrollBy(scrollDelta, 0); + } + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ViewHelper.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ViewHelper.java new file mode 100644 index 000000000..0214b02ca --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/ViewHelper.java @@ -0,0 +1,552 @@ +package org.nativescript.widgets; + +import android.annotation.TargetApi; +import android.graphics.Rect; +import android.view.Gravity; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * Created by hhristov on 8/23/16. + */ + +public class ViewHelper { + private ViewHelper() { + + } + + static final int version = android.os.Build.VERSION.SDK_INT; + + public static int getMinWidth(android.view.View view) { + return view.getMinimumWidth(); + } + + public static void setMinWidth(android.view.View view, int value) { + view.setMinimumWidth(value); + } + + public static int getMinHeight(android.view.View view) { + return view.getMinimumHeight(); + } + + public static void setMinHeight(android.view.View view, int value) { + view.setMinimumHeight(value); + } + + public static int getWidth(android.view.View view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params != null) { + return params.width; + } + + return ViewGroup.LayoutParams.MATCH_PARENT; + } + + public static void setWidth(android.view.View view, int value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params == null) { + params = new CommonLayoutParams(); + } + + params.width = value; + if (params instanceof CommonLayoutParams) { + ((CommonLayoutParams)params).widthPercent = -1; + } + + view.setLayoutParams(params); + } + + public static void setWidthPercent(android.view.View view, float value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params == null) { + params = new CommonLayoutParams(); + } + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + lp.widthPercent = value; + lp.width = (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT; + view.setLayoutParams(params); + } + } + + public static int getHeight(android.view.View view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params != null) { + return params.height; + } + + return ViewGroup.LayoutParams.MATCH_PARENT; + } + + public static void setHeight(android.view.View view, int value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params == null) { + params = new CommonLayoutParams(); + } + + params.height = value; + if (params instanceof CommonLayoutParams) { + ((CommonLayoutParams)params).heightPercent = -1; + } + + view.setLayoutParams(params); + } + + public static void setHeightPercent(android.view.View view, float value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params == null) { + params = new CommonLayoutParams(); + } + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + lp.heightPercent = value; + lp.height = (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT; + view.setLayoutParams(params); + } + } + + public static Rect getMargin(android.view.View view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params; + return new Rect(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin); + } + + return new Rect(); + } + + public static void setMargin(android.view.View view, int left, int top, int right, int bottom) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + // Initialize if empty. + if (params == null) { + params = new CommonLayoutParams(); + } + + // Set margins only if params are of the correct type. + if (params instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params; + lp.leftMargin = left; + lp.topMargin = top; + lp.rightMargin = right; + lp.bottomMargin = bottom; + view.setLayoutParams(params); + } + } + + public static int getMarginLeft(android.view.View view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params; + return lp.leftMargin; + } + + return 0; + } + + public static void setMarginLeft(android.view.View view, int value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + // Initialize if empty. + if (params == null) { + params = new CommonLayoutParams(); + } + + // Set margins only if params are of the correct type. + if (params instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params; + lp.leftMargin = value; + view.setLayoutParams(params); + } + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + lp.leftMarginPercent = -1; + view.setLayoutParams(params); + } + } + + public static void setMarginLeftPercent(android.view.View view, float value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params == null) { + params = new CommonLayoutParams(); + } + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + lp.leftMargin = 0; + lp.leftMarginPercent = value; + view.setLayoutParams(params); + } + } + + public static int getMarginTop(android.view.View view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params; + return lp.topMargin; + } + + return 0; + } + + public static void setMarginTop(android.view.View view, int value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + // Initialize if empty. + if (params == null) { + params = new CommonLayoutParams(); + } + + // Set margins only if params are of the correct type. + if (params instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params; + lp.topMargin = value; + view.setLayoutParams(params); + } + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + lp.topMarginPercent = -1; + view.setLayoutParams(params); + } + } + + public static void setMarginTopPercent(android.view.View view, float value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params == null) { + params = new CommonLayoutParams(); + } + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + lp.topMargin = 0; + lp.topMarginPercent = value; + view.setLayoutParams(params); + } + } + + public static int getMarginRight(android.view.View view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params; + return lp.rightMargin; + } + + return 0; + } + + public static void setMarginRight(android.view.View view, int value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + // Initialize if empty. + if (params == null) { + params = new CommonLayoutParams(); + } + + // Set margins only if params are of the correct type. + if (params instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params; + lp.rightMargin = value; + view.setLayoutParams(params); + } + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + lp.rightMarginPercent = -1; + view.setLayoutParams(params); + } + } + + public static void setMarginRightPercent(android.view.View view, float value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params == null) { + params = new CommonLayoutParams(); + } + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + lp.rightMargin = 0; + lp.rightMarginPercent = value; + view.setLayoutParams(params); + } + } + + public static int getMarginBottom(android.view.View view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params; + return lp.bottomMargin; + } + + return 0; + } + + public static void setMarginBottom(android.view.View view, int value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + // Initialize if empty. + if (params == null) { + params = new CommonLayoutParams(); + } + + // Set margins only if params are of the correct type. + if (params instanceof ViewGroup.MarginLayoutParams) { + ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) params; + lp.bottomMargin = value; + view.setLayoutParams(params); + } + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + lp.bottomMarginPercent = -1; + view.setLayoutParams(params); + } + } + + public static void setMarginBottomPercent(android.view.View view, float value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params == null) { + params = new CommonLayoutParams(); + } + + if (params instanceof CommonLayoutParams) { + CommonLayoutParams lp = (CommonLayoutParams) params; + lp.bottomMargin = 0; + lp.bottomMarginPercent = value; + view.setLayoutParams(params); + } + } + + public static String getHorizontalAlignment(android.view.View view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) params; + if (Gravity.isHorizontal(lp.gravity)) { + switch (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.LEFT: + return "left"; + case Gravity.CENTER: + return "center"; + case Gravity.RIGHT: + return "right"; + case Gravity.FILL_HORIZONTAL: + return "stretch"; + + } + } + } + + return "stretch"; + } + + public static void setHorizontalAlignment(android.view.View view, String value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + // Initialize if empty. + if (params == null) { + params = new CommonLayoutParams(); + } + + // Set margins only if params are of the correct type. + if (params instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) params; + switch (value) { + case "left": + lp.gravity = Gravity.LEFT | (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK); + break; + case "center": + case "middle": + lp.gravity = Gravity.CENTER_HORIZONTAL | (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK); + break; + case "right": + lp.gravity = Gravity.RIGHT | (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK); + break; + case "stretch": + lp.gravity = Gravity.FILL_HORIZONTAL | (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK); + break; + } + view.setLayoutParams(params); + } + } + + public static String getVerticalAlignment(android.view.View view) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) params; + if (Gravity.isHorizontal(lp.gravity)) { + switch (lp.gravity & Gravity.VERTICAL_GRAVITY_MASK) { + case Gravity.TOP: + return "top"; + case Gravity.CENTER: + return "center"; + case Gravity.BOTTOM: + return "bottom"; + case Gravity.FILL_VERTICAL: + return "stretch"; + + } + } + } + + return "stretch"; + } + + public static void setVerticalAlignment(android.view.View view, String value) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + // Initialize if empty. + if (params == null) { + params = new CommonLayoutParams(); + } + + // Set margins only if params are of the correct type. + if (params instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) params; + switch (value) { + case "top": + lp.gravity = Gravity.TOP | (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK); + break; + case "center": + case "middle": + lp.gravity = Gravity.CENTER_VERTICAL | (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK); + break; + case "bottom": + lp.gravity = Gravity.BOTTOM | (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK); + break; + case "stretch": + lp.gravity = Gravity.FILL_VERTICAL | (lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK); + break; + } + view.setLayoutParams(params); + } + } + + public static Rect getPadding(android.view.View view) { + return new Rect(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); + } + + public static void setPadding(android.view.View view, int left, int top, int right, int bottom) { + view.setPadding(left, top, right, bottom); + } + + public static int getPaddingLeft(android.view.View view) { + return view.getPaddingLeft(); + } + + public static void setPaddingLeft(android.view.View view, int value) { + view.setPadding(value, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); + } + + public static int getPaddingTop(android.view.View view) { + return view.getPaddingTop(); + } + + public static void setPaddingTop(android.view.View view, int value) { + view.setPadding(view.getPaddingLeft(), value, view.getPaddingRight(), view.getPaddingBottom()); + } + + public static int getPaddingRight(android.view.View view) { + return view.getPaddingRight(); + } + + public static void setPaddingRight(android.view.View view, int value) { + view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), value, view.getPaddingBottom()); + } + + public static int getPaddingBottom(android.view.View view) { + return view.getPaddingBottom(); + } + + public static void setPaddingBottom(android.view.View view, int value) { + view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), value); + } + + public static float getRotate(android.view.View view) { + return view.getRotation(); + } + + public static void setRotate(android.view.View view, float value) { + view.setRotation(value); + } + + public static float getRotateX(android.view.View view) { + return view.getRotationX(); + } + + public static void setRotateX(android.view.View view, float value) { + view.setRotationX(value); + } + + public static float getRotateY(android.view.View view) { + return view.getRotationY(); + } + + public static void setRotateY(android.view.View view, float value) { + view.setRotationY(value); + } + + public static void setPerspective(android.view.View view, float value) { + view.setCameraDistance(value); + } + + public static float getScaleX(android.view.View view) { + return view.getScaleX(); + } + + public static void setScaleX(android.view.View view, float value) { + view.setScaleX(value); + } + + public static float getScaleY(android.view.View view) { + return view.getScaleY(); + } + + public static void setScaleY(android.view.View view, float value) { + view.setScaleY(value); + } + + public static float getTranslateX(android.view.View view) { + return view.getTranslationX(); + } + + public static void setTranslateX(android.view.View view, float value) { + view.setTranslationX(value); + } + + public static float getTranslateY(android.view.View view) { + return view.getTranslationY(); + } + + public static void setTranslateY(android.view.View view, float value) { + view.setTranslationY(value); + } + + @TargetApi(21) + public static float getZIndex(android.view.View view) { + if (ViewHelper.version >= 21) { + return view.getZ(); + } + + return 0; + } + + @TargetApi(21) + public static void setZIndex(android.view.View view, float value) { + if (ViewHelper.version >= 21) { + view.setZ(value); + } + } + + @TargetApi(21) + public static float getLetterspacing(android.widget.TextView textView) { + if (ViewHelper.version >= 21) { + return textView.getLetterSpacing(); + } + + return 0; + } + + @TargetApi(21) + public static void setLetterspacing(android.widget.TextView textView, float value) { + if (ViewHelper.version >= 21) { + textView.setLetterSpacing(value); + } + } +} + diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/WrapLayout.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/WrapLayout.java new file mode 100644 index 000000000..ead440970 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/WrapLayout.java @@ -0,0 +1,255 @@ +/** + * + */ +package org.nativescript.widgets; + +import java.util.ArrayList; +import android.content.Context; +import android.view.View; + +/** + * @author hhristov + * + */ +public class WrapLayout extends LayoutBase { + + private int _itemWidth = -1; + private int _itemHeight = -1; + private Orientation _orientation = Orientation.horizontal; + private ArrayList _lengths = new ArrayList(); + + public WrapLayout(Context context) { + super(context); + } + + public Orientation getOrientation() { + return this._orientation; + } + public void setOrientation(Orientation value) { + this._orientation = value; + this.requestLayout(); + } + + public int getItemWidth() { + return this._itemWidth; + } + public void setItemWidth(int value) { + this._itemWidth = value; + this.requestLayout(); + } + + public int getItemHeight() { + return this._itemHeight; + } + public void setItemHeight(int value) { + this._itemHeight = value; + this.requestLayout(); + } + + private static int getViewMeasureSpec(int parentMode, int parentLength, int itemLength) { + if (itemLength > 0) { + return MeasureSpec.makeMeasureSpec(itemLength, MeasureSpec.EXACTLY); + } + else if (parentMode == MeasureSpec.UNSPECIFIED) { + return MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + } + else { + return MeasureSpec.makeMeasureSpec(parentLength, MeasureSpec.AT_MOST); + } + } + + private int getDesiredWidth(View child) { + if (this._itemWidth > 0) { + return this._itemWidth; + } + + // Add margins because layoutChild will subtract them. + return CommonLayoutParams.getDesiredWidth(child); + } + + private int getDesiredHeight(View child) { + if (this._itemHeight > 0) { + return this._itemHeight; + } + + // Add margins because layoutChild will subtract them. + return CommonLayoutParams.getDesiredHeight(child); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + CommonLayoutParams.adjustChildrenLayoutParams(this, widthMeasureSpec, heightMeasureSpec); + + int measureWidth = 0; + int measureHeight = 0; + + boolean isVertical = this._orientation == Orientation.vertical; + int verticalPadding = this.getPaddingTop() + this.getPaddingBottom(); + int horizontalPadding = this.getPaddingLeft() + this.getPaddingRight(); + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int availableWidth = widthMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec) - horizontalPadding; + int availableHeight = heightMode == MeasureSpec.UNSPECIFIED ? Integer.MAX_VALUE : MeasureSpec.getSize(heightMeasureSpec) - verticalPadding; + + int childWidthMeasureSpec = getViewMeasureSpec(widthMode, availableWidth, this._itemWidth); + int childHeightMeasureSpec = getViewMeasureSpec(heightMode, availableHeight, this._itemHeight); + + int remainingWidth = availableWidth; + int remainingHeight = availableHeight; + + this._lengths.clear(); + int rowOrColumn = 0; + int maxLength = 0; + + for (int i = 0, count = this.getChildCount(); i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + CommonLayoutParams.measureChild(child, childWidthMeasureSpec, childHeightMeasureSpec); + final int childMeasuredWidth = this.getDesiredWidth(child); + final int childMeasuredHeight = this.getDesiredHeight(child); + final boolean isFirst = this._lengths.size() <= rowOrColumn; + + if (isVertical) { + if (childMeasuredHeight > remainingHeight) { + rowOrColumn++; + maxLength = Math.max(maxLength, measureHeight); + measureHeight = childMeasuredHeight; + remainingHeight = availableHeight - childMeasuredHeight; + this._lengths.add(isFirst ? rowOrColumn - 1 : rowOrColumn, childMeasuredWidth); + } + else { + remainingHeight -= childMeasuredHeight; + measureHeight += childMeasuredHeight; + } + } + else { + if (childMeasuredWidth > remainingWidth) { + rowOrColumn++; + maxLength = Math.max(maxLength, measureWidth); + measureWidth = childMeasuredWidth; + remainingWidth = availableWidth - childMeasuredWidth; + this._lengths.add(isFirst ? rowOrColumn - 1 : rowOrColumn, childMeasuredHeight); + } + else { + remainingWidth -= childMeasuredWidth; + measureWidth += childMeasuredWidth; + } + } + + if(isFirst) { + this._lengths.add(rowOrColumn, isVertical ? childMeasuredWidth : childMeasuredHeight); + } + else { + this._lengths.set(rowOrColumn, Math.max(this._lengths.get(rowOrColumn), isVertical ? childMeasuredWidth : childMeasuredHeight)); + } + } + + if (isVertical) { + measureHeight = Math.max(maxLength, measureHeight); + for (int i = 0, count = this._lengths.size(); i < count; i++) { + measureWidth += this._lengths.get(i); + } + } + else { + measureWidth = Math.max(maxLength, measureWidth); + for (int i = 0, count = this._lengths.size(); i < count; i++) { + measureHeight += this._lengths.get(i); + } + } + + // Add in our padding + measureWidth += horizontalPadding; + measureHeight += verticalPadding; + + // Check against our minimum sizes + measureWidth = Math.max(measureWidth, this.getSuggestedMinimumWidth()); + measureHeight = Math.max(measureHeight, this.getSuggestedMinimumHeight()); + + int widthSizeAndState = resolveSizeAndState(measureWidth, widthMeasureSpec, 0); + int heightSizeAndState = resolveSizeAndState(measureHeight, heightMeasureSpec, 0); + + this.setMeasuredDimension(widthSizeAndState, heightSizeAndState); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + boolean isVertical = this._orientation == Orientation.vertical; + int paddingLeft = this.getPaddingLeft(); + int paddingRight = this.getPaddingRight(); + int paddingTop = this.getPaddingTop(); + int paddingBottom = this.getPaddingBottom(); + + int childLeft = paddingLeft; + int childTop = paddingTop; + int childrenLength = isVertical ? bottom - top - paddingBottom : right - left - paddingRight; + + int rowOrColumn = 0; + for (int i = 0, count = this.getChildCount(); i < count; i++) { + View child = this.getChildAt(i); + if (child.getVisibility() == View.GONE) { + continue; + } + + int childWidth = this.getDesiredWidth(child); + int childHeight = this.getDesiredHeight(child); + + int length = this._lengths.get(rowOrColumn); + if (isVertical) { + childWidth = length; + final boolean isFirst = childTop == paddingTop; + if (childTop + childHeight > childrenLength) { + // Move to top. + childTop = paddingTop; + + if (!isFirst){ + // Move to right with current column width. + childLeft += length; + } + + // Move to next column. + rowOrColumn++; + + // Take respective column width. + childWidth = this._lengths.get(isFirst ? rowOrColumn - 1 : rowOrColumn); + } + } + else { + childHeight = length; + final boolean isFirst = childLeft == paddingLeft; + if (childLeft + childWidth > childrenLength) { + // Move to left. + childLeft = paddingLeft; + + if (!isFirst) { + // Move to bottom with current row height. + childTop += length; + } + + // Move to next row. + rowOrColumn++; + + // Take respective row height. + childHeight = this._lengths.get(isFirst ? rowOrColumn - 1 : rowOrColumn); + } + } + + CommonLayoutParams.layoutChild(child, childLeft, childTop, childLeft + childWidth, childTop + childHeight); + + if (isVertical) { + // Move next child Top position to bottom. + childTop += childHeight; + } + else { + // Move next child Left position to right. + childLeft += childWidth; + } + } + + CommonLayoutParams.restoreOriginalParams(this); + } +} \ No newline at end of file diff --git a/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/image/BitmapOwner.java b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/image/BitmapOwner.java new file mode 100644 index 000000000..2f6296796 --- /dev/null +++ b/tns-core-modules-widgets/android/widgets/src/main/java/org/nativescript/widgets/image/BitmapOwner.java @@ -0,0 +1,15 @@ +package org.nativescript.widgets.image; + +import android.graphics.Bitmap; +import android.graphics.drawable.Drawable; + +/** + * Created by hhristov on 4/18/17. + */ + +public interface BitmapOwner { + void setBitmap(Bitmap value); + void setDrawable(Drawable asyncDrawable); + Drawable getDrawable(); + +} diff --git a/tns-core-modules-widgets/build.android.sh b/tns-core-modules-widgets/build.android.sh new file mode 100755 index 000000000..6b4390df1 --- /dev/null +++ b/tns-core-modules-widgets/build.android.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +echo "Set exit on simple errors" +set -e + +echo "Use dumb gradle terminal" +export TERM=dumb + +echo "Clean dist" +rm -rf dist +mkdir dist +mkdir dist/package +mkdir dist/package/platforms + +echo "Build android" +mkdir dist/package/platforms/android +cd android +./gradlew --quiet assembleRelease +cd .. +cp android/widgets/build/outputs/aar/widgets-release.aar dist/package/platforms/android/widgets-release.aar + +echo "Copy NPM artefacts" +cp LICENSE dist/package/LICENSE +cp README.md dist/package/README.md +cp package.json dist/package/package.json +if [ "$1" ] +then + echo "Suffix package.json's version with tag: $1" + sed -i.bak 's/\(\"version\"\:[[:space:]]*\"[^\"]*\)\"/\1-'$1'"/g' ./dist/package/package.json +fi + +echo "NPM pack" +cd dist/package +PACKAGE="$(npm pack)" +cd ../.. +mv dist/package/$PACKAGE dist/$PACKAGE +echo "Output: dist/$PACKAGE" diff --git a/tns-core-modules-widgets/build.ios.sh b/tns-core-modules-widgets/build.ios.sh new file mode 100755 index 000000000..48ee95c95 --- /dev/null +++ b/tns-core-modules-widgets/build.ios.sh @@ -0,0 +1,38 @@ +#!/bin/sh + +echo "Set exit on simple errors" +set -e + +echo "Use dumb terminal" +export TERM=dumb + +echo "Clean dist" +rm -rf dist +mkdir dist +mkdir dist/package +mkdir dist/package/platforms + +echo "Build iOS" +mkdir dist/package/platforms/ios +cd ios +./build.sh +cd .. +cp -r ios/TNSWidgets/build/TNSWidgets.framework dist/package/platforms/ios/TNSWidgets.framework + +echo "Copy NPM artefacts" +cp LICENSE dist/package/LICENSE +cp README.md dist/package/README.md +cp package.json dist/package/package.json +if [ "$1" ] +then + echo "Suffix package.json's version with tag: $1" + sed -i.bak 's/\(\"version\"\:[[:space:]]*\"[^\"]*\)\"/\1-'$1'"/g' ./dist/package/package.json +fi + +echo "NPM pack" +cd dist/package +PACKAGE="$(npm pack)" +cd ../.. +mv dist/package/$PACKAGE dist/$PACKAGE +echo "Output: dist/$PACKAGE" + diff --git a/tns-core-modules-widgets/build.sh b/tns-core-modules-widgets/build.sh new file mode 100755 index 000000000..e1acb2588 --- /dev/null +++ b/tns-core-modules-widgets/build.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +echo "Set exit on simple errors" +set -e + +echo "Use dumb gradle terminal" +export TERM=dumb + +echo "Clean dist" +rm -rf dist +mkdir dist +mkdir dist/package +mkdir dist/package/platforms + +echo "Build android" +mkdir dist/package/platforms/android +cd android +./gradlew --quiet assembleRelease +cd .. +cp android/widgets/build/outputs/aar/widgets-release.aar dist/package/platforms/android/widgets-release.aar + +echo "Build iOS" +mkdir dist/package/platforms/ios +cd ios +./build.sh +cd .. +cp -r ios/TNSWidgets/build/TNSWidgets.framework dist/package/platforms/ios/TNSWidgets.framework + +echo "Copy NPM artefacts" +cp LICENSE dist/package/LICENSE +cp README.md dist/package/README.md +cp package.json dist/package/package.json +if [ "$1" ] +then + echo "Suffix package.json's version with tag: $1" + sed -i.bak 's/\(\"version\"\:[[:space:]]*\"[^\"]*\)\"/\1-'$1'"/g' ./dist/package/package.json +fi + +echo "NPM pack" +cd dist/package +PACKAGE="$(npm pack)" +cd ../.. +mv dist/package/$PACKAGE dist/$PACKAGE +echo "Output: dist/$PACKAGE" + diff --git a/tns-core-modules-widgets/ios/README.md b/tns-core-modules-widgets/ios/README.md new file mode 100644 index 000000000..06b5c6966 --- /dev/null +++ b/tns-core-modules-widgets/ios/README.md @@ -0,0 +1,8 @@ +### iOS + +The `TNSWidgets` directory contains a Xcode project. + +### How to open? +* In Xcode choose: File -> Open +* Navigate to `tns-core-modules-widgets/ios/TNSWidgetes/` folder +* On the left side of the screen choose the Project navigator and select `TNSWidgets` diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSProcess.h b/tns-core-modules-widgets/ios/TNSWidgets/TNSProcess.h new file mode 100644 index 000000000..baea95509 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSProcess.h @@ -0,0 +1,29 @@ +// +// TNSProcess.h +// TNSWidgets +// +// Created by Panayot Cankov on 15/05/2017. +// Copyright © 2017 Telerik A D. All rights reserved. +// + +#ifndef TNSProcess_h +#define TNSProcess_h + +#import + +/** + * Get the milliseconds since the process started. + */ +double __tns_uptime(); + +/** + * Provides access to NSLog. The runtime implementation of console.log is filtered in release. + * We rarely need to log in release but in cases such as when logging app startup times in release, + * this will be convenient shortcut to NSLog, NSLog is not exposed. + * + * Please note the {N} CLI may be filtering app output, prefixing the message with "CONSOLE LOG" + * will make the logs visible in "tns run ios --release" builds. + */ +void __nslog(NSString* message); + +#endif /* TNSProcess_h */ diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSProcess.m b/tns-core-modules-widgets/ios/TNSWidgets/TNSProcess.m new file mode 100644 index 000000000..dc237b562 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSProcess.m @@ -0,0 +1,30 @@ +// +// TNSProcess.c +// TNSWidgets +// +// Created by Panayot Cankov on 15/05/2017. +// Copyright © 2017 Telerik A D. All rights reserved. +// + +#include "TNSProcess.h" + +#include +#include +#include + +double __tns_uptime() { + pid_t pid = [[NSProcessInfo processInfo] processIdentifier]; + int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PID, pid }; + struct kinfo_proc proc; + size_t size = sizeof(proc); + sysctl(mib, 4, &proc, &size, NULL, 0); + + struct timeval current; + gettimeofday(¤t, NULL); + + return (double)(current.tv_sec - proc.kp_proc.p_starttime.tv_sec) * 1000.0 + (double)(current.tv_usec - proc.kp_proc.p_starttime.tv_usec) / 1000.0; +} + +void __nslog(NSString* message) { + NSLog(@"%@", message); +} diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.pbxproj b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.pbxproj new file mode 100644 index 000000000..d98948fbe --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.pbxproj @@ -0,0 +1,439 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 8B7321CF1D097ECD00884AC6 /* TNSLabel.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B7321CD1D097ECD00884AC6 /* TNSLabel.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8B7321D01D097ECD00884AC6 /* TNSLabel.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B7321CE1D097ECD00884AC6 /* TNSLabel.m */; }; + B8E76F52212C2DA2009CFCE2 /* NSObject+Swizzling.h in Headers */ = {isa = PBXBuildFile; fileRef = B8E76F50212C2DA2009CFCE2 /* NSObject+Swizzling.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B8E76F53212C2DA2009CFCE2 /* NSObject+Swizzling.m in Sources */ = {isa = PBXBuildFile; fileRef = B8E76F51212C2DA2009CFCE2 /* NSObject+Swizzling.m */; }; + B8E76F5A212C2F4E009CFCE2 /* NSObject+PropertyBag.h in Headers */ = {isa = PBXBuildFile; fileRef = B8E76F58212C2F4E009CFCE2 /* NSObject+PropertyBag.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B8E76F5B212C2F4E009CFCE2 /* NSObject+PropertyBag.m in Sources */ = {isa = PBXBuildFile; fileRef = B8E76F59212C2F4E009CFCE2 /* NSObject+PropertyBag.m */; }; + B8E76F5E212C3134009CFCE2 /* UIView+PassThroughParent.h in Headers */ = {isa = PBXBuildFile; fileRef = B8E76F5C212C3134009CFCE2 /* UIView+PassThroughParent.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B8E76F5F212C3134009CFCE2 /* UIView+PassThroughParent.m in Sources */ = {isa = PBXBuildFile; fileRef = B8E76F5D212C3134009CFCE2 /* UIView+PassThroughParent.m */; }; + F915D3551EC9EF5E00071914 /* TNSProcess.m in Sources */ = {isa = PBXBuildFile; fileRef = F915D3531EC9EF5E00071914 /* TNSProcess.m */; }; + F915D3561EC9EF5E00071914 /* TNSProcess.h in Headers */ = {isa = PBXBuildFile; fileRef = F915D3541EC9EF5E00071914 /* TNSProcess.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F98F5CB31CD0EFEA00978308 /* TNSWidgets.h in Headers */ = {isa = PBXBuildFile; fileRef = F98F5CB21CD0EFEA00978308 /* TNSWidgets.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F98F5CBA1CD0EFEA00978308 /* TNSWidgets.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F98F5CAF1CD0EFEA00978308 /* TNSWidgets.framework */; }; + F98F5CBF1CD0EFEA00978308 /* TNSWidgetsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F98F5CBE1CD0EFEA00978308 /* TNSWidgetsTests.m */; }; + F98F5CCB1CD0F09E00978308 /* UIImage+TNSBlocks.h in Headers */ = {isa = PBXBuildFile; fileRef = F98F5CC91CD0F09E00978308 /* UIImage+TNSBlocks.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F98F5CCC1CD0F09E00978308 /* UIImage+TNSBlocks.m in Sources */ = {isa = PBXBuildFile; fileRef = F98F5CCA1CD0F09E00978308 /* UIImage+TNSBlocks.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + F98F5CBB1CD0EFEA00978308 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = F98F5CA61CD0EFEA00978308 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F98F5CAE1CD0EFEA00978308; + remoteInfo = TNSWidgets; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 8B7321CD1D097ECD00884AC6 /* TNSLabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TNSLabel.h; sourceTree = ""; }; + 8B7321CE1D097ECD00884AC6 /* TNSLabel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TNSLabel.m; sourceTree = ""; }; + B8E76F50212C2DA2009CFCE2 /* NSObject+Swizzling.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSObject+Swizzling.h"; sourceTree = ""; }; + B8E76F51212C2DA2009CFCE2 /* NSObject+Swizzling.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSObject+Swizzling.m"; sourceTree = ""; }; + B8E76F58212C2F4E009CFCE2 /* NSObject+PropertyBag.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSObject+PropertyBag.h"; sourceTree = ""; }; + B8E76F59212C2F4E009CFCE2 /* NSObject+PropertyBag.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSObject+PropertyBag.m"; sourceTree = ""; }; + B8E76F5C212C3134009CFCE2 /* UIView+PassThroughParent.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIView+PassThroughParent.h"; sourceTree = ""; }; + B8E76F5D212C3134009CFCE2 /* UIView+PassThroughParent.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIView+PassThroughParent.m"; sourceTree = ""; }; + F915D3531EC9EF5E00071914 /* TNSProcess.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TNSProcess.m; path = ../TNSProcess.m; sourceTree = ""; }; + F915D3541EC9EF5E00071914 /* TNSProcess.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TNSProcess.h; path = ../TNSProcess.h; sourceTree = ""; }; + F98F5CAF1CD0EFEA00978308 /* TNSWidgets.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = TNSWidgets.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F98F5CB21CD0EFEA00978308 /* TNSWidgets.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TNSWidgets.h; sourceTree = ""; }; + F98F5CB41CD0EFEA00978308 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F98F5CB91CD0EFEA00978308 /* TNSWidgetsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TNSWidgetsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F98F5CBE1CD0EFEA00978308 /* TNSWidgetsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TNSWidgetsTests.m; sourceTree = ""; }; + F98F5CC01CD0EFEA00978308 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F98F5CC91CD0F09E00978308 /* UIImage+TNSBlocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIImage+TNSBlocks.h"; sourceTree = ""; }; + F98F5CCA1CD0F09E00978308 /* UIImage+TNSBlocks.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIImage+TNSBlocks.m"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F98F5CAB1CD0EFEA00978308 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F98F5CB61CD0EFEA00978308 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F98F5CBA1CD0EFEA00978308 /* TNSWidgets.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + F98F5CA51CD0EFEA00978308 = { + isa = PBXGroup; + children = ( + F98F5CB11CD0EFEA00978308 /* TNSWidgets */, + F98F5CBD1CD0EFEA00978308 /* TNSWidgetsTests */, + F98F5CB01CD0EFEA00978308 /* Products */, + ); + sourceTree = ""; + }; + F98F5CB01CD0EFEA00978308 /* Products */ = { + isa = PBXGroup; + children = ( + F98F5CAF1CD0EFEA00978308 /* TNSWidgets.framework */, + F98F5CB91CD0EFEA00978308 /* TNSWidgetsTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + F98F5CB11CD0EFEA00978308 /* TNSWidgets */ = { + isa = PBXGroup; + children = ( + F915D3531EC9EF5E00071914 /* TNSProcess.m */, + F915D3541EC9EF5E00071914 /* TNSProcess.h */, + F98F5CB21CD0EFEA00978308 /* TNSWidgets.h */, + 8B7321CD1D097ECD00884AC6 /* TNSLabel.h */, + 8B7321CE1D097ECD00884AC6 /* TNSLabel.m */, + F98F5CC91CD0F09E00978308 /* UIImage+TNSBlocks.h */, + F98F5CCA1CD0F09E00978308 /* UIImage+TNSBlocks.m */, + F98F5CB41CD0EFEA00978308 /* Info.plist */, + B8E76F50212C2DA2009CFCE2 /* NSObject+Swizzling.h */, + B8E76F51212C2DA2009CFCE2 /* NSObject+Swizzling.m */, + B8E76F58212C2F4E009CFCE2 /* NSObject+PropertyBag.h */, + B8E76F59212C2F4E009CFCE2 /* NSObject+PropertyBag.m */, + B8E76F5C212C3134009CFCE2 /* UIView+PassThroughParent.h */, + B8E76F5D212C3134009CFCE2 /* UIView+PassThroughParent.m */, + ); + path = TNSWidgets; + sourceTree = ""; + }; + F98F5CBD1CD0EFEA00978308 /* TNSWidgetsTests */ = { + isa = PBXGroup; + children = ( + F98F5CBE1CD0EFEA00978308 /* TNSWidgetsTests.m */, + F98F5CC01CD0EFEA00978308 /* Info.plist */, + ); + path = TNSWidgetsTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + F98F5CAC1CD0EFEA00978308 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + F915D3561EC9EF5E00071914 /* TNSProcess.h in Headers */, + F98F5CB31CD0EFEA00978308 /* TNSWidgets.h in Headers */, + B8E76F5E212C3134009CFCE2 /* UIView+PassThroughParent.h in Headers */, + F98F5CCB1CD0F09E00978308 /* UIImage+TNSBlocks.h in Headers */, + B8E76F52212C2DA2009CFCE2 /* NSObject+Swizzling.h in Headers */, + B8E76F5A212C2F4E009CFCE2 /* NSObject+PropertyBag.h in Headers */, + 8B7321CF1D097ECD00884AC6 /* TNSLabel.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + F98F5CAE1CD0EFEA00978308 /* TNSWidgets */ = { + isa = PBXNativeTarget; + buildConfigurationList = F98F5CC31CD0EFEA00978308 /* Build configuration list for PBXNativeTarget "TNSWidgets" */; + buildPhases = ( + F98F5CAA1CD0EFEA00978308 /* Sources */, + F98F5CAB1CD0EFEA00978308 /* Frameworks */, + F98F5CAC1CD0EFEA00978308 /* Headers */, + F98F5CAD1CD0EFEA00978308 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TNSWidgets; + productName = TNSWidgets; + productReference = F98F5CAF1CD0EFEA00978308 /* TNSWidgets.framework */; + productType = "com.apple.product-type.framework"; + }; + F98F5CB81CD0EFEA00978308 /* TNSWidgetsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F98F5CC61CD0EFEA00978308 /* Build configuration list for PBXNativeTarget "TNSWidgetsTests" */; + buildPhases = ( + F98F5CB51CD0EFEA00978308 /* Sources */, + F98F5CB61CD0EFEA00978308 /* Frameworks */, + F98F5CB71CD0EFEA00978308 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F98F5CBC1CD0EFEA00978308 /* PBXTargetDependency */, + ); + name = TNSWidgetsTests; + productName = TNSWidgetsTests; + productReference = F98F5CB91CD0EFEA00978308 /* TNSWidgetsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F98F5CA61CD0EFEA00978308 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0720; + ORGANIZATIONNAME = "Telerik A D"; + TargetAttributes = { + F98F5CAE1CD0EFEA00978308 = { + CreatedOnToolsVersion = 7.2; + }; + F98F5CB81CD0EFEA00978308 = { + CreatedOnToolsVersion = 7.2; + }; + }; + }; + buildConfigurationList = F98F5CA91CD0EFEA00978308 /* Build configuration list for PBXProject "TNSWidgets" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = F98F5CA51CD0EFEA00978308; + productRefGroup = F98F5CB01CD0EFEA00978308 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F98F5CAE1CD0EFEA00978308 /* TNSWidgets */, + F98F5CB81CD0EFEA00978308 /* TNSWidgetsTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F98F5CAD1CD0EFEA00978308 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F98F5CB71CD0EFEA00978308 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F98F5CAA1CD0EFEA00978308 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8B7321D01D097ECD00884AC6 /* TNSLabel.m in Sources */, + F915D3551EC9EF5E00071914 /* TNSProcess.m in Sources */, + F98F5CCC1CD0F09E00978308 /* UIImage+TNSBlocks.m in Sources */, + B8E76F53212C2DA2009CFCE2 /* NSObject+Swizzling.m in Sources */, + B8E76F5B212C2F4E009CFCE2 /* NSObject+PropertyBag.m in Sources */, + B8E76F5F212C3134009CFCE2 /* UIView+PassThroughParent.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F98F5CB51CD0EFEA00978308 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F98F5CBF1CD0EFEA00978308 /* TNSWidgetsTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + F98F5CBC1CD0EFEA00978308 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F98F5CAE1CD0EFEA00978308 /* TNSWidgets */; + targetProxy = F98F5CBB1CD0EFEA00978308 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + F98F5CC11CD0EFEA00978308 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.2; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + F98F5CC21CD0EFEA00978308 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.2; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + F98F5CC41CD0EFEA00978308 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = TNSWidgets/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.nativescript.TNSWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + F98F5CC51CD0EFEA00978308 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = TNSWidgets/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.nativescript.TNSWidgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; + F98F5CC71CD0EFEA00978308 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = TNSWidgetsTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.nativescript.TNSWidgetsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + F98F5CC81CD0EFEA00978308 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = TNSWidgetsTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.nativescript.TNSWidgetsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F98F5CA91CD0EFEA00978308 /* Build configuration list for PBXProject "TNSWidgets" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F98F5CC11CD0EFEA00978308 /* Debug */, + F98F5CC21CD0EFEA00978308 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F98F5CC31CD0EFEA00978308 /* Build configuration list for PBXNativeTarget "TNSWidgets" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F98F5CC41CD0EFEA00978308 /* Debug */, + F98F5CC51CD0EFEA00978308 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F98F5CC61CD0EFEA00978308 /* Build configuration list for PBXNativeTarget "TNSWidgetsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F98F5CC71CD0EFEA00978308 /* Debug */, + F98F5CC81CD0EFEA00978308 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F98F5CA61CD0EFEA00978308 /* Project object */; +} diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..25711d4cd --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..949b67898 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + BuildSystemType + Original + + diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/xcuserdata/cankov.xcuserdatad/UserInterfaceState.xcuserstate b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/xcuserdata/cankov.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 000000000..8f89dbaf8 Binary files /dev/null and b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/project.xcworkspace/xcuserdata/cankov.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/xcuserdata/cankov.xcuserdatad/xcschemes/TNSWidgets.xcscheme b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/xcuserdata/cankov.xcuserdatad/xcschemes/TNSWidgets.xcscheme new file mode 100644 index 000000000..a29b34c4d --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/xcuserdata/cankov.xcuserdatad/xcschemes/TNSWidgets.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/xcuserdata/cankov.xcuserdatad/xcschemes/xcschememanagement.plist b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/xcuserdata/cankov.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 000000000..b5df7e73c --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets.xcodeproj/xcuserdata/cankov.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,27 @@ + + + + + SchemeUserState + + TNSWidgets.xcscheme + + orderHint + 0 + + + SuppressBuildableAutocreation + + F98F5CAE1CD0EFEA00978308 + + primary + + + F98F5CB81CD0EFEA00978308 + + primary + + + + + diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/Info.plist b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/Info.plist new file mode 100644 index 000000000..d3de8eefb --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+PropertyBag.h b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+PropertyBag.h new file mode 100644 index 000000000..d759a03a0 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+PropertyBag.h @@ -0,0 +1,17 @@ +// +// NSObject+PropertyBag.h +// TNSWidgets +// +// Created by Manol Donev on 21.08.18. +// Copyright © 2018 Telerik A D. All rights reserved. +// + +#import + + +@interface NSObject (PropertyBag) + +- (id) propertyValueForKey:(NSString*) key; +- (void) setPropertyValue:(id) value forKey:(NSString*) key; + +@end diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+PropertyBag.m b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+PropertyBag.m new file mode 100644 index 000000000..d4a232ca2 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+PropertyBag.m @@ -0,0 +1,62 @@ +// +// NSObject+PropertyBag.m +// TNSWidgets +// +// Created by Manol Donev on 21.08.18. +// Copyright © 2018 Telerik A D. All rights reserved. +// + +#import "NSObject+PropertyBag.h" +#import "NSObject+Swizzling.h" + + +@implementation NSObject (PropertyBag) + ++ (void) load{ + [self loadPropertyBag]; +} + ++ (void) loadPropertyBag{ + @autoreleasepool { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + const SEL deallocSelector = NSSelectorFromString(@"dealloc"); //ARC forbids use of 'dealloc' in a @selector + [self swizzleInstanceMethodWithOriginalSelector:deallocSelector fromClass:self.class withSwizzlingSelector:@selector(propertyBag_dealloc)]; + }); + } +} + +__strong NSMutableDictionary *_propertyBagHolder; // Properties for every class will go in this property bag +- (id) propertyValueForKey:(NSString*) key { + return [[self propertyBag] valueForKey:key]; +} + +- (void) setPropertyValue:(id) value forKey:(NSString*) key { + [[self propertyBag] setValue:value forKey:key]; +} + +- (NSMutableDictionary*) propertyBag { + if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100]; + NSMutableDictionary *propBag = [_propertyBagHolder valueForKey:[[NSString alloc] initWithFormat:@"%p", self]]; + if (propBag == nil) { + propBag = [NSMutableDictionary dictionary]; + [self setPropertyBag:propBag]; + } + + return propBag; +} + +- (void) setPropertyBag:(NSDictionary*) propertyBag { + if (_propertyBagHolder == nil) { + _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100]; + } + + [_propertyBagHolder setValue:propertyBag forKey:[[NSString alloc] initWithFormat:@"%p", self]]; +} + +- (void)propertyBag_dealloc { + [self setPropertyBag:nil]; + [self propertyBag_dealloc]; // swizzled +} + +@end diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+Swizzling.h b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+Swizzling.h new file mode 100644 index 000000000..d3d71a88f --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+Swizzling.h @@ -0,0 +1,17 @@ +// +// NSObject+Swizzling.h +// TNSWidgets +// +// Created by Manol Donev on 21.08.18. +// Copyright © 2018 Telerik A D. All rights reserved. +// + +#import +#import + + +@interface NSObject (Swizzling) + ++ (void)swizzleInstanceMethodWithOriginalSelector:(SEL)originalSelector fromClass:(Class)classContainigOriginalSel withSwizzlingSelector:(SEL)swizzlingSelector; + +@end diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+Swizzling.m b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+Swizzling.m new file mode 100644 index 000000000..f9a65ac15 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/NSObject+Swizzling.m @@ -0,0 +1,68 @@ +// +// NSObject+Swizzling.m +// TNSWidgets +// +// Created by Manol Donev on 21.08.18. +// Copyright © 2018 Telerik A D. All rights reserved. +// + +#import "NSObject+Swizzling.h" + +@implementation NSObject (Swizzling) + +#pragma mark - Method Swizzling + ++ (void)swizzleInstanceMethodWithOriginalSelector:(SEL)originalSelector + fromClass:(Class)classContainigOriginalSel + withSwizzlingSelector:(SEL)swizzlingSelector { + Method originalMethod = class_getInstanceMethod(classContainigOriginalSel, originalSelector); + Method swizzlingMethod = class_getInstanceMethod(self.class, swizzlingSelector); + [self swizzleMethodWithOriginalSelector:originalSelector + originalMethod:originalMethod + fromClass:classContainigOriginalSel + withSwizzlingSelector:swizzlingSelector + swizzlingMethod:swizzlingMethod]; +} + +//MARK: Utilities + ++ (void)swizzleMethodWithOriginalSelector:(SEL)originalSelector + originalMethod:(Method)originalMethod + fromClass:(Class)classContainigOriginalSel + withSwizzlingSelector:(SEL)swizzlingSelector + swizzlingMethod:(Method)swizzlingMethod { + if (self == classContainigOriginalSel) { + BOOL didAddMethod = class_addMethod(classContainigOriginalSel, + originalSelector, + method_getImplementation(swizzlingMethod), + method_getTypeEncoding(swizzlingMethod)); + + if (didAddMethod) { + class_replaceMethod(self.class, + swizzlingSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + } else { + method_exchangeImplementations(originalMethod, swizzlingMethod); + } + + return; + } + + class_addMethod(classContainigOriginalSel, + swizzlingSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + + class_replaceMethod(classContainigOriginalSel, + originalSelector, + method_getImplementation(swizzlingMethod), + method_getTypeEncoding(swizzlingMethod)); + + class_replaceMethod(self, + swizzlingSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); +} + +@end diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/TNSLabel.h b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/TNSLabel.h new file mode 100644 index 000000000..1c170b9c9 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/TNSLabel.h @@ -0,0 +1,16 @@ +// +// TNSLabel.h +// TNSWidgets +// +// Created by Hristo Hristov on 6/9/16. +// Copyright © 2016 Telerik A D. All rights reserved. +// + +#import + +@interface TNSLabel : UILabel + +@property(nonatomic) UIEdgeInsets padding; +@property(nonatomic) UIEdgeInsets borderThickness; + +@end diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/TNSLabel.m b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/TNSLabel.m new file mode 100644 index 000000000..66191b842 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/TNSLabel.m @@ -0,0 +1,42 @@ +// +// TNSLabel.m +// TNSWidgets +// +// Created by Hristo Hristov on 6/9/16. +// Copyright © 2016 Telerik A D. All rights reserved. +// + +#import "TNSLabel.h" + +@implementation TNSLabel + + +- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines { + // UILabel.textRectForBounds:limitedToNumberOfLines: returns rect with CGSizeZero when empty + if (self.text.length == 0) { + return [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines]; + } + + // 1. Subtract the insets (border thickness & padding) + // 2. Calculate the original label bounds + // 3. Add the insets again + UIEdgeInsets insets = UIEdgeInsetsMake(self.borderThickness.top + self.padding.top, + self.borderThickness.left + self.padding.left, + self.borderThickness.bottom + self.padding.bottom, + self.borderThickness.right + self.padding.right); + + CGRect rect = [super textRectForBounds:UIEdgeInsetsInsetRect(bounds, insets) limitedToNumberOfLines:numberOfLines]; + + UIEdgeInsets inverseInsets = UIEdgeInsetsMake(-(self.borderThickness.top + self.padding.top), + -(self.borderThickness.left + self.padding.left), + -(self.borderThickness.bottom + self.padding.bottom), + -(self.borderThickness.right + self.padding.right)); + + return UIEdgeInsetsInsetRect(rect, inverseInsets); +} + +-(void)drawTextInRect:(CGRect)rect { + [super drawTextInRect:UIEdgeInsetsInsetRect(UIEdgeInsetsInsetRect(rect, self.borderThickness), self.padding)]; +} + +@end diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/TNSWidgets.h b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/TNSWidgets.h new file mode 100644 index 000000000..453d7cd6e --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/TNSWidgets.h @@ -0,0 +1,22 @@ +// +// TNSWidgets.h +// TNSWidgets +// +// Created by Panayot Cankov on 4/27/16. +// Copyright © 2016 Telerik A D. All rights reserved. +// + +#import + +//! Project version number for TNSWidgets. +FOUNDATION_EXPORT double TNSWidgetsVersionNumber; + +//! Project version string for TNSWidgets. +FOUNDATION_EXPORT const unsigned char TNSWidgetsVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import "UIImage+TNSBlocks.h" +#import "UIView+PassThroughParent.h" +#import "TNSLabel.h" +#import "TNSProcess.h" diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.h b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.h new file mode 100644 index 000000000..9e67228be --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.h @@ -0,0 +1,26 @@ +// +// UIImage+UIImage_Async.h +// TKImageAsync +// +// Created by Panayot Cankov on 4/18/16. +// Copyright © 2016 Telerik A D. All rights reserved. +// + +@interface UIImage (TNSBlocks) + +/** + * Similar to imageNamed: however it runs on a separate queue so the UI thread is not blocked. + * It also draws the UIImage in a small thumb to force decoding potentially avoiding UI hicckups when displayed. + */ ++ (void) tns_safeDecodeImageNamed: (NSString*) name completion: (void (^) (UIImage*))callback; + +/** + * Same as imageNamed, however calls to this method are sinchronized to be thread safe in iOS8 along with calls to tns_safeImageNamed and tns_safeDecodeImageNamed:completion: + * imageNamed is thread safe in iOS 9 and later so in later versions this methods simply fallbacks to imageNamed: + */ ++ (UIImage*) tns_safeImageNamed: (NSString*) name; + ++ (void) tns_decodeImageWithData: (NSData*) data completion: (void (^) (UIImage*))callback; ++ (void) tns_decodeImageWidthContentsOfFile: (NSString*) file completion: (void (^) (UIImage*))callback; + +@end diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.m b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.m new file mode 100644 index 000000000..155c9a9f1 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIImage+TNSBlocks.m @@ -0,0 +1,77 @@ +// +// UIImage+UIImage_Async.m +// TKImageAsync +// +// Created by Panayot Cankov on 4/18/16. +// Copyright © 2016 Telerik A D. All rights reserved. +// +#import +#import +#import "UIImage+TNSBlocks.h" + +@implementation UIImage (TNSBlocks) + +static dispatch_queue_t image_queue; +static NSLock* image_lock_handle; + ++ (void) initialize { + image_queue = dispatch_queue_create("org.nativescript.TNSWidgets.image", NULL); + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 9) { + // UIImage imageNamed: is said to be thread safe, in iOS9 and later, in offical Apple reference. + image_lock_handle = nil; + } else { + image_lock_handle = [NSLock new]; + } +} + +- (void) tns_forceDecode { + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = CGBitmapContextCreate(NULL, 1, 1, 8, 0, colorSpace, kCGImageAlphaPremultipliedFirst); + CGContextDrawImage(context, CGRectMake(0, 0, 1, 1), [self CGImage]); + CGContextRelease(context); + CGColorSpaceRelease(colorSpace); +} + ++ (void) tns_safeDecodeImageNamed: (NSString*) name completion: (void (^) (UIImage*))callback { + dispatch_async(image_queue, ^(void){ + [image_lock_handle lock]; + UIImage* image = [UIImage imageNamed: name]; + [image_lock_handle unlock]; + [image tns_forceDecode]; + + dispatch_async(dispatch_get_main_queue(), ^(void) { + callback(image); + }); + }); +} + ++ (UIImage*) tns_safeImageNamed: (NSString*) name { + [image_lock_handle lock]; + UIImage* image = [UIImage imageNamed: name]; + [image_lock_handle unlock]; + return image; +} + ++ (void) tns_decodeImageWithData: (NSData*) data completion: (void (^) (UIImage*))callback { + dispatch_async(image_queue, ^(void) { + UIImage* image = [UIImage imageWithData: data]; + [image tns_forceDecode]; + + dispatch_async(dispatch_get_main_queue(), ^(void) { + callback(image); + }); + }); +} + ++ (void) tns_decodeImageWidthContentsOfFile: (NSString*) file completion: (void (^) (UIImage*))callback { + dispatch_async(image_queue, ^(void) { + UIImage* image = [UIImage imageWithContentsOfFile: file]; + [image tns_forceDecode]; + + dispatch_async(dispatch_get_main_queue(), ^(void) { + callback(image); + }); + }); +} + +@end diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIView+PassThroughParent.h b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIView+PassThroughParent.h new file mode 100644 index 000000000..e5f68c568 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIView+PassThroughParent.h @@ -0,0 +1,17 @@ +// +// UIView+PassThroughParent.h +// TNSWidgets +// +// Created by Manol Donev on 21.08.18. +// Copyright © 2018 Telerik A D. All rights reserved. +// + +#import + + +@interface UIView (PassThroughParent) + +- (BOOL) passThroughParent; +- (void) setPassThroughParent:(BOOL) passThroughParent; + +@end diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIView+PassThroughParent.m b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIView+PassThroughParent.m new file mode 100644 index 000000000..1afefd84e --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgets/UIView+PassThroughParent.m @@ -0,0 +1,53 @@ +// +// UIView+PassThroughParent.m +// TNSWidgets +// +// Created by Manol Donev on 21.08.18. +// Copyright © 2018 Telerik A D. All rights reserved. +// + +#import "UIView+PassThroughParent.h" +#import "NSObject+Swizzling.h" +#import "NSObject+PropertyBag.h" + + +NSString * const TLKPassThroughParentKey = @"passThroughParent"; + +@implementation UIView (PassThroughParent) + ++ (void) load { + [self loadHitTest]; +} + ++ (void) loadHitTest { + @autoreleasepool { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self swizzleInstanceMethodWithOriginalSelector:@selector(hitTest:withEvent:) fromClass:self.class withSwizzlingSelector:@selector(passThrough_hitTest:withEvent:)]; + }); + } +} + +- (BOOL)passThroughParent { + NSNumber *passthrough = [self propertyValueForKey:TLKPassThroughParentKey]; + if (passthrough) { + return passthrough.boolValue; + }; + + return NO; +} + +- (void)setPassThroughParent:(BOOL)passThroughParent { + [self setPropertyValue:[NSNumber numberWithBool:passThroughParent] forKey:TLKPassThroughParentKey]; +} + +- (UIView *)passThrough_hitTest:(CGPoint)point withEvent:(UIEvent *)event { + UIView *hitTestView = [self passThrough_hitTest:point withEvent:event]; // swizzled + if (hitTestView == self && self.passThroughParent) { + hitTestView = nil; + } + + return hitTestView; +} + +@end diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgetsTests/Info.plist b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgetsTests/Info.plist new file mode 100644 index 000000000..ba72822e8 --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgetsTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgetsTests/TNSWidgetsTests.m b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgetsTests/TNSWidgetsTests.m new file mode 100644 index 000000000..2e70d8efc --- /dev/null +++ b/tns-core-modules-widgets/ios/TNSWidgets/TNSWidgetsTests/TNSWidgetsTests.m @@ -0,0 +1,39 @@ +// +// TNSWidgetsTests.m +// TNSWidgetsTests +// +// Created by Panayot Cankov on 4/27/16. +// Copyright © 2016 Telerik A D. All rights reserved. +// + +#import + +@interface TNSWidgetsTests : XCTestCase + +@end + +@implementation TNSWidgetsTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. + [super tearDown]; +} + +- (void)testExample { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. +} + +- (void)testPerformanceExample { + // This is an example of a performance test case. + [self measureBlock:^{ + // Put the code you want to measure the time of here. + }]; +} + +@end diff --git a/tns-core-modules-widgets/ios/build.sh b/tns-core-modules-widgets/ios/build.sh new file mode 100755 index 000000000..e287a8e8f --- /dev/null +++ b/tns-core-modules-widgets/ios/build.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +echo "Set exit on simple errors" +set -e + +echo "Build for iphonesimulator" +xcodebuild -project TNSWidgets/TNSWidgets.xcodeproj -sdk iphonesimulator -target TNSWidgets -configuration Release clean build CONFIGURATION_BUILD_DIR=build/Release-iphonesimulator -quiet + +echo "Build for iphoneos" +xcodebuild -project TNSWidgets/TNSWidgets.xcodeproj -sdk iphoneos -target TNSWidgets -configuration Release clean build CONFIGURATION_BUILD_DIR=build/Release-iphoneos CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -quiet + +echo "Build fat framework at TNSWidgets/build/TNSWidgets.framework" +rm -rf TNSWidgets/build/TNSWidgets.framework +mkdir TNSWidgets/build/TNSWidgets.framework + +cp -r TNSWidgets/build/Release-iphoneos/TNSWidgets.framework/Headers TNSWidgets/build/TNSWidgets.framework/Headers +cp -r TNSWidgets/build/Release-iphoneos/TNSWidgets.framework/Modules TNSWidgets/build/TNSWidgets.framework/Modules +cp -r TNSWidgets/build/Release-iphoneos/TNSWidgets.framework/Info.plist TNSWidgets/build/TNSWidgets.framework/Info.plist + +lipo -create TNSWidgets/build/Release-iphoneos/TNSWidgets.framework/TNSWidgets TNSWidgets/build/Release-iphonesimulator/TNSWidgets.framework/TNSWidgets -o TNSWidgets/build/TNSWidgets.framework/TNSWidgets +file TNSWidgets/build/TNSWidgets.framework/TNSWidgets diff --git a/tns-core-modules-widgets/package.json b/tns-core-modules-widgets/package.json new file mode 100644 index 000000000..84b4e57cc --- /dev/null +++ b/tns-core-modules-widgets/package.json @@ -0,0 +1,24 @@ +{ + "name": "tns-core-modules-widgets", + "version": "5.3.0", + "description": "Native widgets used in the NativeScript framework.", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NativeScript/android-widgets.git" + }, + "author": "NativeScript team", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/NativeScript/android-widgets/issues" + }, + "homepage": "https://github.com/NativeScript/android-widgets#readme", + "nativescript": { + "platforms": { + "ios": "4.0.0", + "android": "4.0.0" + } + } +}