├── .github └── workflows │ └── build-gradle.yml ├── .gitignore ├── .idea ├── compiler.xml ├── discord.xml ├── gradle.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── openblocks │ │ └── blocks │ │ └── view │ │ └── example │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── openblocks │ │ │ └── blocks │ │ │ └── view │ │ │ └── example │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_file.xml │ │ ├── ic_launcher_background.xml │ │ └── ic_select_event.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── menu │ │ └── main_menu.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── openblocks │ └── blocks │ └── view │ └── example │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── openblocks │ │ └── blocks │ │ └── view │ │ └── ExampleInstrumentedTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── openblocks │ │ └── blocks │ │ └── view │ │ ├── Block.java │ │ ├── BlockField.java │ │ ├── BlocksView.java │ │ ├── BlocksViewEvent.java │ │ ├── DrawHelper.java │ │ ├── NestedBlock.java │ │ └── SketchwareBlocksParser.java │ └── res │ ├── drawable │ └── sketchware_block.9.png │ └── values │ └── attrs.xml ├── screenshots └── 1.png └── settings.gradle /.github/workflows/build-gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Java CI with Gradle 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | 16 | app/debug/output-metadata.json 17 | app/debug/app-debug.apk 18 | app/release/app-release.apk 19 | app/release/output-metadata.json 20 | 21 | app/google-services.json 22 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/discord.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at contact@openblocks.tk. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Iyxan 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blocks View 2 | A custom widget library for android used to display blocks of codes similar to Sketchware and Scratch. 3 | 4 | This project is very experimental, and is not suitable for production usage. 5 | 6 | ## Features 7 | - [x] Blocks 8 | - [x] Nested Blocks 9 | - [x] Parameter Blocks 10 | - [x] String, Integer, Boolean, and Other Fields 11 | - [x] Custom blocks 12 | - [x] Drag blocks 13 | - [ ] Drop blocks (TODO) 14 | 15 | ## Sample 16 | 17 | 18 | ## Using 19 | We currently haven't published this library into any platforms yet because it's still in development, if you want to try it, just clone this project into your project directory, then create a new module, select gradle project, and select the folder that you cloned this repository into (make sure to pick `:lib`). 20 | 21 | ### Basic usage: 22 | ``` 23 | 31 | ``` 32 | With this, you will be greeted with the demo blocks (as shown in sample). 33 | 34 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | } 4 | 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion "30.0.3" 8 | 9 | defaultConfig { 10 | applicationId "com.openblocks.blocks.view.example" 11 | minSdkVersion 21 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | buildFeatures { 30 | viewBinding true 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation project(':lib') 36 | implementation 'androidx.appcompat:appcompat:1.2.0' 37 | implementation 'com.google.android.material:material:1.3.0' 38 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 39 | testImplementation 'junit:junit:4.13.2' 40 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 41 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 42 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/openblocks/blocks/view/example/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view.example; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.iyxan23.blocks.view", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/openblocks/blocks/view/example/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view.example; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | import android.util.Log; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.widget.Toast; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | import androidx.appcompat.app.AlertDialog; 14 | import androidx.appcompat.app.AppCompatActivity; 15 | 16 | import com.openblocks.blocks.view.SketchwareBlocksParser; 17 | import com.openblocks.blocks.view.BlocksView; 18 | import com.openblocks.blocks.view.BlocksViewEvent; 19 | 20 | import java.io.ByteArrayOutputStream; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.util.ArrayList; 24 | 25 | import javax.crypto.Cipher; 26 | import javax.crypto.spec.IvParameterSpec; 27 | import javax.crypto.spec.SecretKeySpec; 28 | 29 | public class MainActivity extends AppCompatActivity { 30 | 31 | final int PICK_EVENT_REC_CODE = 10; 32 | ArrayList events = new ArrayList<>(); 33 | 34 | @Override 35 | protected void onCreate(Bundle savedInstanceState) { 36 | super.onCreate(savedInstanceState); 37 | setContentView(R.layout.activity_main); 38 | } 39 | 40 | @Override 41 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 42 | super.onActivityResult(requestCode, resultCode, data); 43 | 44 | if (requestCode == PICK_EVENT_REC_CODE) { 45 | if (data != null) { 46 | Uri uri = data.getData(); 47 | try { 48 | InputStream stream = getContentResolver().openInputStream(uri); 49 | 50 | int count; 51 | byte[] buffer = new byte[1024]; 52 | ByteArrayOutputStream byteStream = 53 | new ByteArrayOutputStream(stream.available()); 54 | 55 | while (true) { 56 | count = stream.read(buffer); 57 | if (count <= 0) 58 | break; 59 | byteStream.write(buffer, 0, count); 60 | } 61 | 62 | stream.close(); 63 | 64 | events = new SketchwareBlocksParser(decrypt(byteStream.toByteArray())).parse(); 65 | 66 | byteStream.close(); 67 | } catch (IOException e) { 68 | e.printStackTrace(); 69 | } 70 | 71 | pickEvent(); 72 | } 73 | } 74 | } 75 | 76 | private void pickFile() { 77 | Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT); 78 | i .addCategory(Intent.CATEGORY_OPENABLE) 79 | .setType("*/*"); 80 | 81 | startActivityForResult(i, PICK_EVENT_REC_CODE); 82 | 83 | Toast.makeText(this, "Navigate to a logic file", Toast.LENGTH_LONG).show(); 84 | } 85 | 86 | private void pickEvent() { 87 | AlertDialog.Builder alertDialog = new AlertDialog.Builder(this); 88 | alertDialog.setTitle("Choose an event to display"); 89 | 90 | String[] items = new String[events.size()]; 91 | 92 | for (int i = 0; i < events.size(); i++) { 93 | items[i] = events.get(i).activity_name + ": " + events.get(i).name; 94 | } 95 | 96 | alertDialog.setItems(items, (dialog, which) -> { 97 | BlocksView blocksView = findViewById(R.id.blocks_view); 98 | blocksView.setEvent(events.get(which)); 99 | Log.d("MainActivity", "pickEvent: event: " + events); 100 | blocksView.invalidate(); 101 | }); 102 | 103 | AlertDialog alert = alertDialog.create(); 104 | alert.show(); 105 | } 106 | 107 | public static String decrypt(byte[] data) { 108 | try { 109 | Cipher instance = Cipher.getInstance("AES/CBC/PKCS5Padding"); 110 | byte[] bytes = "sketchwaresecure".getBytes(); 111 | instance.init(2, new SecretKeySpec(bytes, "AES"), new IvParameterSpec(bytes)); 112 | 113 | return new String(instance.doFinal(data)); 114 | } catch (Exception e) { 115 | Log.e("Decryptor", "Error while decrypting"); 116 | e.printStackTrace(); 117 | } 118 | return ""; 119 | } 120 | 121 | @Override 122 | public boolean onCreateOptionsMenu(Menu menu) { 123 | getMenuInflater().inflate(R.menu.main_menu, menu); 124 | return true; 125 | } 126 | 127 | @Override 128 | public boolean onOptionsItemSelected(@NonNull MenuItem item) { 129 | int id = item.getItemId(); 130 | 131 | if (id == R.id.open_file) { 132 | pickFile(); 133 | 134 | return true; 135 | } else if (id == R.id.open_event) { 136 | pickEvent(); 137 | 138 | return true; 139 | } 140 | 141 | return super.onOptionsItemSelected(item); 142 | } 143 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_file.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_select_event.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | sketchware-blocks-view 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/test/java/com/openblocks/blocks/view/example/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view.example; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath "com.android.tools.build:gradle:4.1.2" 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | task clean(type: Delete) { 23 | delete rootProject.buildDir 24 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Feb 03 15:37:18 WIB 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /lib/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | } 4 | 5 | android { 6 | compileSdkVersion 30 7 | buildToolsVersion "30.0.3" 8 | 9 | defaultConfig { 10 | minSdkVersion 21 11 | targetSdkVersion 30 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | } 30 | 31 | dependencies { 32 | 33 | implementation 'androidx.appcompat:appcompat:1.2.0' 34 | implementation 'com.google.android.material:material:1.3.0' 35 | testImplementation 'junit:junit:4.+' 36 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 37 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 38 | } -------------------------------------------------------------------------------- /lib/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/lib/consumer-rules.pro -------------------------------------------------------------------------------- /lib/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /lib/src/androidTest/java/com/openblocks/blocks/view/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | assertEquals("com.iyxan23.blocks.view.test", appContext.getPackageName()); 25 | } 26 | } -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /lib/src/main/java/com/openblocks/blocks/view/Block.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.graphics.Rect; 7 | import android.util.Log; 8 | import android.util.Pair; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Arrays; 14 | import java.util.Objects; 15 | import java.util.regex.Matcher; 16 | import java.util.regex.Pattern; 17 | 18 | /** 19 | * This class is a model used to represent a block 20 | */ 21 | public class Block { 22 | 23 | private static final String TAG = "Block"; 24 | 25 | // TODO: 3/8/21 REMOVE next_block AND id 26 | 27 | // Variables =================================================================================== 28 | /** 29 | * This variable format is formatted in sketchware's way, example: 30 | * 31 | * Toast %s 32 | * 33 | * the % indicates that this is a parameter, s means that it's a string parameter type 34 | */ 35 | private String format; 36 | private ArrayList parsed_format; 37 | public String id; 38 | public ArrayList parameters; 39 | 40 | // Indicates if this block can't have a next_block, e.g. Finish Activity block 41 | public boolean is_bottom; 42 | 43 | int next_block; 44 | 45 | public int color; 46 | 47 | int text_padding = 10; 48 | 49 | // Indicates if this block returns a value or not 50 | boolean is_return_block; 51 | 52 | // Indicates this block's return type (if is_return_block is true) 53 | BlockField.Type return_type; 54 | 55 | // Will be used in the overloaded draw function 56 | public int default_height = 60; // The same as in BlocksView 57 | // Variables =================================================================================== 58 | 59 | 60 | 61 | // Constructors ================================================================================ 62 | /** 63 | * Constructs a simple block with just a text and a color 64 | * 65 | * @param text The text of the block 66 | * @param color The color of the block 67 | */ 68 | public Block(String text, int color) { 69 | this(text, "1", 2, color); 70 | } 71 | 72 | public Block(String text, String id, int next_block, int color) { 73 | this(text, id, next_block, new ArrayList<>(), color, false, null); 74 | } 75 | 76 | public Block(String format, ArrayList parameters, int color) { 77 | this(format, "1", 2, parameters, color); 78 | } 79 | 80 | public Block(String format, String id, int next_block, ArrayList parameters, int color) { 81 | this(format, id, next_block, parameters, color, false, null); 82 | } 83 | 84 | public Block(String format, String id, int next_block, ArrayList parameters, int color, boolean is_parameter, BlockField.Type parameter_type) { 85 | this.id = id; 86 | this.next_block = next_block; 87 | this.parameters = parameters; 88 | this.color = color; 89 | this.is_return_block = is_parameter; 90 | this.return_type = parameter_type; 91 | this.setFormat(format); 92 | 93 | // next_block is -1 if there is nothing after it 94 | this.is_bottom = next_block == -1; 95 | } 96 | // Constructors ================================================================================ 97 | 98 | 99 | 100 | // Factory constructors ======================================================================== 101 | 102 | /** 103 | * Create a simple block 104 | * @param text The text of the block 105 | * @param color The color of the block 106 | * @return The block according to the parameters given 107 | */ 108 | public static Block newSimpleBlock(String text, int color) { 109 | return new Block(text, color); 110 | } 111 | 112 | /** 113 | * Create a simple block with parameters in it 114 | * @param text The text of the block 115 | * @param color The color of the block 116 | * @param parameters The parameters of the block 117 | * @return The block according to the parameters given 118 | */ 119 | public static Block newBlockWithParameters(String text, int color, BlockField... parameters) { 120 | return new Block(text, new ArrayList<>(Arrays.asList(parameters)), color); 121 | } 122 | 123 | /** 124 | * Create a parameter block 125 | * @param text The text of the block 126 | * @param color The color of the block 127 | * @param return_type The return type of the block 128 | * @return The block according to the parameters given 129 | */ 130 | public static Block newReturnBlock(String text, int color, BlockField.Type return_type) { 131 | return new Block(text, "1", 2, new ArrayList<>(), color, true, return_type); 132 | } 133 | 134 | /** 135 | * Create a parameter block that has parameters in it 136 | * @param text The text of the block 137 | * @param color The color of the block 138 | * @param return_type The return type of the block 139 | * @param parameters The parameters for this block 140 | * @return The block according to the parameters given 141 | */ 142 | public static Block newReturnBlockWithParams(String text, int color, BlockField.Type return_type, BlockField... parameters) { 143 | return new Block(text, "1", 2, new ArrayList<>(Arrays.asList(parameters)), color, true, return_type); 144 | } 145 | // Factory constructors ======================================================================== 146 | 147 | 148 | 149 | // Getters and Setters ========================================================================= 150 | public String getFormat() { 151 | return format; 152 | } 153 | 154 | public void setFormat(String format) { 155 | this.format = format; 156 | parsed_format = parseFormat(); 157 | } 158 | // Getters and Setters ========================================================================= 159 | 160 | 161 | 162 | // Essential functions ========================================================================= 163 | /** 164 | * This function parses the format 165 | * 166 | * @return Returns ArrayList of [start_pos, end_pos, name, BlocksViewField] 167 | */ 168 | public ArrayList parseFormat() { 169 | ArrayList tmp = new ArrayList<>(); 170 | 171 | Pattern pattern = Pattern.compile("%[a-z]\\.(\\w+)|%[a-z]"); 172 | Matcher matcher = pattern.matcher(getFormat()); 173 | 174 | int index = 0; 175 | while (matcher.find()) { 176 | if (parameters.size() <= index) 177 | //throw new IllegalStateException("Parameters have less elements than the format"); 178 | continue; 179 | 180 | tmp.add(new Object[] { 181 | matcher.start(), 182 | matcher.end(), 183 | matcher.groupCount() == 1 ? matcher.group(0).substring(1) : "", // Skip the first char because we want to skip the . 184 | parameters.get(index) 185 | }); 186 | index++; 187 | } 188 | 189 | return tmp; 190 | } 191 | 192 | /** 193 | * This function returns the approximate width of the block 194 | * 195 | * @param text_paint The text paint that is going to be used 196 | * @return The block's width 197 | */ 198 | public int getWidth(Paint text_paint) { 199 | if (parameters.size() == 0) 200 | return text_padding + (int) text_paint.measureText(getFormat()) + text_padding; 201 | 202 | // Remove these 203 | ArrayList params = parseFormat(); 204 | StringBuilder final_string = new StringBuilder(); 205 | 206 | // The parameters widths (will be added with the measure text) 207 | int params_widths = 0; 208 | 209 | int last_num = 0; 210 | for (Object[] param: params) { 211 | final_string.append(getFormat().substring(last_num, (int) param[0])); 212 | last_num = (int) param[1]; 213 | 214 | BlockField field = (BlockField) param[3]; 215 | 216 | params_widths += 217 | field.getWidth(text_paint) + 218 | text_padding; // The padding between the text and the field 219 | } 220 | 221 | // Add the last string at the end 222 | final_string.append(getFormat().substring(last_num)); 223 | 224 | return text_padding + (int) text_paint.measureText(final_string.toString()) + text_padding + params_widths; 225 | } 226 | 227 | public int getHeight(Paint text_paint) { 228 | // Return the default height if this is a parameter and it has no other parameters 229 | if (is_return_block && parameters.size() == 0) 230 | return default_height; 231 | 232 | // If there aren't any parameters, and this isn't a parameter block, this means that this 233 | // block is just a freestanding block, nothing special in it, get text height and add 2 text_padding. 234 | if (parameters.size() == 0) 235 | return Math.max(default_height, (int) text_paint.getTextSize() + text_padding * 2); 236 | 237 | // Let's calculate the height 238 | // Quite easy, just loop per every parameters and get the maximum height 239 | int max_height = 0; 240 | for (BlockField parameter : parameters) { 241 | if (parameter.is_block) { 242 | // This is a block! 243 | // We can just call the getHeight of that block recursively 244 | max_height = Math.max(parameter.block.getHeight(text_paint), max_height); 245 | } else { 246 | max_height = Math.max(parameter.getHeight(text_paint), max_height); 247 | } 248 | } 249 | 250 | return Math.max(default_height, max_height + text_padding * 2); // 2 paddings because there will be padding on the top and the bottom 251 | } 252 | 253 | /** 254 | * This function is used to get / grab a field when the user clicked at a specific location 255 | * @param x The x position 256 | * @param y The y position 257 | * @return The field the user clicked 258 | */ 259 | public BlockField onClick(int x, int y, Paint text_paint) { 260 | int x_total = 0; 261 | int x_before; 262 | 263 | // We're using a similar method of drawParameters 264 | // Loop per each parameters 265 | int last_substring_index = 0; 266 | for (Object[] param: parsed_format) { 267 | // Ik this looks stupid, but if i just pass in x_total, the x_before 's reference will attach to x_total's reference 268 | // if x_total changed, x_before will change (which is the thing i don't want) 269 | x_before = Integer.parseInt(String.valueOf(x_total)); 270 | 271 | Log.d(TAG, "onClick: [Init] before: " + x_before + " total: " + x_total); 272 | 273 | // Get the text between a field (should be 0 for the first time) and another field 274 | String text = getFormat().substring(last_substring_index, (int) param[0]); 275 | 276 | x_total += text_paint.measureText(text) + 5; 277 | 278 | Log.d(TAG, "onClick: [Text Check] before: " + x_before + " total: " + x_total); 279 | 280 | // Check if the X is somewhere in this text 281 | if (x > x_before && x < x_total) { 282 | Log.d(TAG, "onClick: Somewhere in text, nope"); 283 | 284 | // User is clicking on this block on the text, nothing 285 | // To optimize this, return null 286 | return null; 287 | } 288 | 289 | // Update the x_before to the text 290 | x_before = Integer.parseInt(String.valueOf(x_total)); 291 | 292 | last_substring_index = (int) param[1]; 293 | 294 | BlockField field = (BlockField) param[3]; 295 | 296 | x_total += field.getWidth(text_paint) + 5; 297 | 298 | Log.d(TAG, "onClick: [Field Check] before: " + x_before + " total: " + x_total); 299 | 300 | // Check if X is somewhere in this field 301 | if (x > x_before && x < x_total) { 302 | Log.d(TAG, "onClick: Dragging a field"); 303 | 304 | // Yup, this the user is dragging a field, check if this is a block 305 | if (field.is_block) { 306 | Log.d(TAG, "onClick: Is a block"); 307 | 308 | // Ohk this is a block, check if the parameter block has a parameter too 309 | if (field.block.parameters.size() == 0) { 310 | Log.d(TAG, "onClick: No parameters, not this one"); 311 | 312 | return null; 313 | } else { 314 | Log.d(TAG, "onClick: Has parameter, recursive call"); 315 | 316 | // This block has a parameter, recursively call onHover! 317 | // oh yeah don't forget to offset the x 318 | 319 | return field.block.onClick(x - x_before, y, text_paint); 320 | } 321 | } else { 322 | Log.d(TAG, "onClick: Value parameter, this is it!"); 323 | 324 | return field; 325 | } 326 | } 327 | } 328 | 329 | // Wat, nothing? 330 | Log.d(TAG, "onClick: Weird, nothing"); 331 | 332 | // This shouldn't happen but meh 333 | return null; 334 | } 335 | 336 | /** 337 | * This function is called when the user hovers a return block on top of this block 338 | * @param x The x location of the hover, should be relative to OUR block's 0, 0 point (top left) 339 | * @param y The y location of the hover, should be relative to OUR block's 0, 0 point (top left) 340 | * @param block The block that is hovered 341 | * @param drop Should we drop this block to this parameter? 342 | * @param text_paint The {@link Paint} used to draw the block text 343 | * @return The bounds of the field that this block is hovering relative to the canvas, null if it's not hovering a field 344 | */ 345 | public Rect onHover(int x, int y, Block block, boolean drop, Paint text_paint) { 346 | int x_total = 0; 347 | int x_before; 348 | 349 | // This index indicates the position of the parameter (in variable parameters) we're in the loop 350 | int index = 0; 351 | 352 | // We're using a similar method of drawParameters 353 | // Loop per each parameters 354 | int last_substring_index = 0; 355 | for (Object[] param: parsed_format) { 356 | // Ik this looks stupid, but if i just pass in x_total, the x_before 's reference will attach to x_total's reference 357 | // if x_total changed, x_before will change (which is the thing i don't want) 358 | x_before = Integer.parseInt(String.valueOf(x_total)); 359 | 360 | Log.d(TAG, "onHover: [Init] before: " + x_before + " total: " + x_total); 361 | 362 | // Get the text between a field (should be 0 for the first time) and another field 363 | String text = getFormat().substring(last_substring_index, (int) param[0]); 364 | 365 | x_total += text_paint.measureText(text) + 5; 366 | 367 | Log.d(TAG, "onHover: [Text Check] before: " + x_before + " total: " + x_total); 368 | 369 | // Check if the X is somewhere in this text 370 | if (x > x_before && x < x_total) { 371 | Log.d(TAG, "onHover: Somewhere in text, nope"); 372 | 373 | // User is hovering this block on the text, nothing 374 | // To optimize this, return null 375 | return null; 376 | } 377 | 378 | // Update the x_before to the text 379 | x_before = Integer.parseInt(String.valueOf(x_total)); 380 | 381 | last_substring_index = (int) param[1]; 382 | 383 | BlockField field = (BlockField) param[3]; 384 | 385 | x_total += field.getWidth(text_paint) + 5; 386 | 387 | if (!field.type.equals(block.return_type)) { 388 | // This the field and the block doesn't have the same type, skip this 389 | index++; // To not break the index 390 | 391 | continue; 392 | } 393 | 394 | Log.d(TAG, "onHover: [Field Check] before: " + x_before + " total: " + x_total); 395 | 396 | // Check if X is somewhere in this field 397 | if (x > x_before && x < x_total) { 398 | Log.d(TAG, "onHover: Dragging a field"); 399 | 400 | // Yup, this the user is dragging a field, check if this is a block 401 | if (field.is_block) { 402 | Log.d(TAG, "onHover: Is a block"); 403 | 404 | // Ohk this is a block, check if the parameter block has a parameter too 405 | if (field.block.parameters.size() == 0) { 406 | Log.d(TAG, "onHover: No parameters, this is it!"); 407 | 408 | return field.block.getBounds(x, y, text_paint); 409 | 410 | } else { 411 | Log.d(TAG, "onHover: Has parameter, recursive call"); 412 | 413 | // This block has a parameter, recursively call onHover! 414 | // oh yeah don't forget to offset the x 415 | 416 | return field.block.onHover(x - x_before, y, block, drop, text_paint); 417 | } 418 | } else { 419 | Log.d(TAG, "onHover: Value parameter, this is it!"); 420 | 421 | return field.block.getBounds(x, y, text_paint); 422 | } 423 | } 424 | 425 | index++; 426 | } 427 | 428 | // Wat, nothing? 429 | Log.d(TAG, "onHover: Weird, nothing"); 430 | 431 | // This shouldn't happen but meh 432 | return null; 433 | } 434 | 435 | /** 436 | * This function is called when a pickup is happened at the location somewhere in this block 437 | * @param x The x location of the pickup, should be relative to OUR block's 0, 0 point (top left) 438 | * @param y The y location of the pickup, should be relative to OUR block's 0, 0 point (top left) 439 | * @param text_paint The {@link Paint} used to draw the block text 440 | * @return Pair of "Should we remove this block from the block list" and the block that is picked up 441 | */ 442 | public Pair onPickup(int x, int y, Paint text_paint) { 443 | // int x = left + text_padding; // The initial x's text position 444 | 445 | Log.d(TAG, "onPickup: x: " + x + " y: " + y); 446 | 447 | // int text_top = top + ((getHeight(text_paint) + shadow_height + block_outset_height + text_padding) / 2); 448 | 449 | int x_total = 0; 450 | int x_before; 451 | 452 | // This index indicates the position of the parameter (in variable parameters) we're in the loop 453 | int index = 0; 454 | 455 | // We're using a similar method of drawParameters 456 | // Loop per each parameters 457 | int last_substring_index = 0; 458 | for (Object[] param: parsed_format) { 459 | // Ik this looks stupid, but if i just pass in x_total, the x_before 's reference will attach to x_total's reference 460 | // if x_total changed, x_before will change (which is the thing i don't want) 461 | x_before = Integer.parseInt(String.valueOf(x_total)); 462 | 463 | Log.d(TAG, "onPickup: [Init] before: " + x_before + " total: " + x_total); 464 | 465 | // Get the text between a field (should be 0 for the first time) and another field 466 | String text = getFormat().substring(last_substring_index, (int) param[0]); 467 | 468 | x_total += text_paint.measureText(text) + 5; 469 | 470 | Log.d(TAG, "onPickup: [Text Check] before: " + x_before + " total: " + x_total); 471 | 472 | // Check if the X is somewhere in this text 473 | if (x > x_before && x < x_total) { 474 | Log.d(TAG, "onPickup: Somewhere in text, self"); 475 | 476 | // Oop, then just pick ourself, i guess 477 | return new Pair<>(true, this); 478 | } 479 | 480 | // Update the x_before to the text 481 | x_before = Integer.parseInt(String.valueOf(x_total)); 482 | 483 | last_substring_index = (int) param[1]; 484 | 485 | BlockField field = (BlockField) param[3]; 486 | 487 | x_total += field.getWidth(text_paint) + 5; 488 | 489 | Log.d(TAG, "onPickup: [Field Check] before: " + x_before + " total: " + x_total); 490 | 491 | // Check if X is somewhere in this field 492 | if (x > x_before && x < x_total) { 493 | Log.d(TAG, "onPickup: Dragging a field"); 494 | 495 | // Yup, this the user is dragging a field, check if this is a block 496 | if (field.is_block) { 497 | Log.d(TAG, "onPickup: Is a block"); 498 | 499 | // Ohk this is a block, check if the parameter block has a parameter too 500 | if (field.block.parameters.size() == 0) { 501 | Log.d(TAG, "onPickup: No parameters, pick up ourself"); 502 | 503 | // Nop it doesn't have any, means we can pick this parameter! 504 | // Remove the field from parameters 505 | parameters.set(index, new BlockField("", field.block.return_type, field.other_type)); 506 | 507 | parsed_format.get(index)[3] = new BlockField("", field.block.return_type, field.other_type); 508 | 509 | // Then set the block 510 | return new Pair<>(false, field.block); 511 | } else { 512 | Log.d(TAG, "onPickup: Has parameter, recursive call"); 513 | 514 | // This block has a parameter, recursively call onPickup! 515 | // oh yeah don't forget to offset the x 516 | Pair pickup_block = field.block.onPickup(x - x_before, y, text_paint); 517 | 518 | // Should we remove this block? 519 | if (pickup_block.first) { 520 | // Yep, we should, for now, it will just be a text, nothing fancy 521 | parameters.set(index, new BlockField("", field.block.return_type, field.other_type)); 522 | 523 | parsed_format.get(index)[3] = new BlockField("", field.block.return_type, field.other_type); 524 | } 525 | 526 | return new Pair<>(false, pickup_block.second); 527 | } 528 | } else { 529 | Log.d(TAG, "onPickup: Value parameter, self"); 530 | 531 | // Oop, this is just a value parameter, just pick ourself, i guess 532 | return new Pair<>(true, this); 533 | } 534 | } 535 | 536 | index++; 537 | } 538 | 539 | // Wat, nothing? 540 | Log.d(TAG, "onPickup: Weird, nothing"); 541 | 542 | // This shouldn't happen but meh, let's just pick ourself 543 | return new Pair<>(true, this); 544 | } 545 | 546 | /** 547 | * This function returns the bounds of this block in RectF 548 | * @param x The x position of this block 549 | * @param y The y position of this block 550 | * @param text_paint The paint used to draw the block text 551 | * @return This block's bounds 552 | */ 553 | public Rect getBounds(int x, int y, Paint text_paint) { 554 | return new Rect( 555 | x, 556 | y, 557 | x + getWidth(text_paint), 558 | y + getHeight(text_paint) 559 | ); 560 | } 561 | // Essential functions ========================================================================= 562 | 563 | 564 | 565 | // Draw functions ============================================================================== 566 | /** 567 | * This function is an overloaded function of draw, but without height 568 | * the height is get by using the global variable (can be set manually) 569 | * 570 | * This function will be used to draw the lowest child inside every parameters 571 | * 572 | * @param canvas The canvas where it will be drawn into 573 | * @param rect_paint The paint for the rectangle 574 | * @param text_paint The paint for the text 575 | * @param top The y position of the block 576 | * @param left The x position of the block 577 | * @param shadow_height The shadow height of the block 578 | * @param block_outset_left_margin The outset block's left margin (RTL) 579 | * @param block_outset_width The outset block's width 580 | * @param block_outset_height The outset block's height 581 | * @param is_overlapping Do you want to overlap the block above's shadow? 582 | * @param previous_block_color The previous block's color, used to draw the outset of the block above 583 | */ 584 | public void draw(Context context, Canvas canvas, Paint rect_paint, Paint text_paint, int top, int left, int shadow_height, int block_outset_left_margin, int block_outset_width, int block_outset_height, boolean is_overlapping, int previous_block_color, boolean is_round, int round_radius) { 585 | draw(context, canvas, rect_paint, text_paint, top, left, getHeight(text_paint), shadow_height, block_outset_left_margin, block_outset_left_margin, block_outset_width, block_outset_height, is_overlapping, previous_block_color, is_round, round_radius); 586 | } 587 | 588 | // TODO: Maybe at least reduce the parameters 589 | /** 590 | * This function draws the block into the canvas specified at a given level to the bottom (blocks_down) 591 | * 592 | * @param context The context 593 | * @param canvas The canvas where it will be drawn into 594 | * @param rect_paint The paint for the rectangle 595 | * @param text_paint The paint for the text 596 | * @param top The y position of the block 597 | * @param left The x position of the block 598 | * @param height The height of the block 599 | * @param shadow_height The shadow height of the block 600 | * @param block_outset_left_margin The outset block's left margin (RTL) 601 | * @param top_block_outset_left_margin The block's top outset's left margin 602 | * @param block_outset_width The outset block's width 603 | * @param block_outset_height The outset block's height 604 | * @param is_overlapping Do you want to overlap the block above's shadow? 605 | * @param previous_block_color The previous block's color, used to draw the outset of the block above 606 | */ 607 | public void draw(Context context, 608 | Canvas canvas, 609 | Paint rect_paint, 610 | Paint text_paint, 611 | int top, 612 | int left, 613 | int height, 614 | int shadow_height, 615 | int block_outset_left_margin, 616 | int top_block_outset_left_margin, 617 | int block_outset_width, 618 | int block_outset_height, 619 | boolean is_overlapping, 620 | int previous_block_color, 621 | boolean is_round, 622 | int round_radius 623 | ) { 624 | // int block_width = (int) text_paint.measureText(format) + 20; 625 | int block_width = getWidth(text_paint); 626 | 627 | int left_parameter = left; 628 | 629 | // FIXME: 3/8/21 Height shouldn't be added with the shadow height 630 | // Draw the block body 631 | if (is_return_block) { 632 | switch (return_type) { 633 | case STRING: 634 | DrawHelper.drawRect(canvas, left, top, block_width, height + shadow_height, color); 635 | break; 636 | 637 | case INTEGER: 638 | DrawHelper.drawIntegerField(canvas, left, top, block_width + height / 5, height + shadow_height, color); 639 | 640 | left_parameter += 5; 641 | break; 642 | 643 | case BOOLEAN: 644 | DrawHelper.drawBooleanField(canvas, left, top, block_width + height / 5, height + shadow_height, color); 645 | 646 | left_parameter += height / 5; 647 | break; 648 | 649 | case OTHER: 650 | DrawHelper.drawRectSimpleOutsideShadow(canvas, left, top, block_width, height + shadow_height, shadow_height, color); 651 | break; 652 | } 653 | } else { 654 | if (is_round) { 655 | DrawHelper.drawRoundRectSimpleOutsideShadow(canvas, left, top, block_width, height + shadow_height, shadow_height, round_radius, color); 656 | } else { 657 | DrawHelper.drawRectSimpleOutsideShadow(canvas, left, top, block_width, height + shadow_height, shadow_height, color); 658 | } 659 | } 660 | 661 | // If this is a return block, don't draw the outset 662 | // return blocks doesn't have an outset cheems 663 | if (!is_return_block) { 664 | // Should the blocks overlap each other? 665 | if (!is_overlapping) { 666 | // Ohk no, draw the outset with shadow and the top block's outset 667 | 668 | // Draw the outset 669 | DrawHelper.drawRectSimpleOutsideShadow(canvas, left + block_outset_left_margin, top, block_outset_width, height + block_outset_height + block_outset_height, shadow_height, color); 670 | 671 | // Draw the top block's outset 672 | DrawHelper.drawRect(canvas, left + top_block_outset_left_margin, top, block_outset_width, block_outset_height, DrawHelper.manipulateColor(previous_block_color, 0.8f)); 673 | } else { 674 | // Yes, just draw the top block's outset 675 | 676 | // Draw the top block's outset 677 | DrawHelper.drawRect(canvas, left + top_block_outset_left_margin, top, block_outset_width, block_outset_height, previous_block_color); 678 | } 679 | } 680 | 681 | // Draw the block's text and parameters 682 | drawParameters(context, canvas, left_parameter, top, top + ((getHeight(text_paint) + shadow_height + block_outset_height + text_padding) / 2), height, shadow_height, text_paint); 683 | } 684 | 685 | public final void drawParameters(Context context, Canvas canvas, int left, int top, int block_text_location, int height, int shadow_height, Paint text_paint) { 686 | // Draw the parameters 687 | int x = left + text_padding; // The initial x's text position 688 | 689 | // int text_top = top + ((getHeight(text_paint) + shadow_height + block_outset_height + text_padding) / 2); 690 | 691 | int last_num = 0; 692 | for (Object[] param: parsed_format) { 693 | String text = getFormat().substring(last_num, (int) param[0]); 694 | canvas.drawText(text, x, block_text_location, text_paint); 695 | 696 | x += text_paint.measureText(text) + 5; 697 | 698 | last_num = (int) param[1]; 699 | 700 | BlockField field = (BlockField) param[3]; 701 | 702 | if (shadow_height == 0) 703 | shadow_height = text_padding; 704 | 705 | field.draw(context, canvas, x, top + text_padding, text_paint, height - text_padding - shadow_height, DrawHelper.manipulateColor(color, .8f)); 706 | 707 | x += field.getWidth(text_paint) + 5; 708 | } 709 | 710 | String text = getFormat().substring(last_num); 711 | canvas.drawText(text, x, block_text_location, text_paint); 712 | } 713 | // Draw functions ============================================================================== 714 | 715 | 716 | 717 | @NonNull 718 | @Override 719 | public String toString() { 720 | return "Block{" + 721 | "format='" + getFormat() + '\'' + 722 | ", id='" + id + '\'' + 723 | ", parameters=" + parameters + 724 | ", is_bottom=" + is_bottom + 725 | ", next_block=" + next_block + 726 | ", color=" + color + 727 | ", text_padding=" + text_padding + 728 | ", is_parameter=" + is_return_block + 729 | ", default_height=" + default_height + 730 | '}'; 731 | } 732 | 733 | @Override 734 | public boolean equals(Object o) { 735 | if (this == o) return true; 736 | if (o == null || getClass() != o.getClass()) return false; 737 | 738 | Block that = (Block) o; 739 | return is_bottom == that.is_bottom && 740 | next_block == that.next_block && 741 | color == that.color && 742 | text_padding == that.text_padding && 743 | is_return_block == that.is_return_block && 744 | default_height == that.default_height && 745 | format.equals(that.format) && 746 | parsed_format.equals(that.parsed_format) && 747 | id.equals(that.id) && 748 | parameters.equals(that.parameters) && 749 | return_type == that.return_type; 750 | } 751 | 752 | @Override 753 | public int hashCode() { 754 | return Objects.hash(format, parsed_format, id, parameters, is_bottom, next_block, color, text_padding, is_return_block, return_type, default_height); 755 | } 756 | } 757 | -------------------------------------------------------------------------------- /lib/src/main/java/com/openblocks/blocks/view/BlockField.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | 10 | import java.util.Objects; 11 | 12 | public class BlockField { 13 | 14 | /** 15 | * This type enum is used to identify what this field's type is 16 | */ 17 | public enum Type { 18 | STRING, // Will look like block / rectangle, something like this [ ] 19 | INTEGER, // Will look like an ellipse / rounded rectangle, something like this ( ) 20 | BOOLEAN, // Will look something like this < > 21 | OTHER // Some kind of subset of other objects, ex: ArrayList, where you can put the same subset, but not with other subset, will look the same as string 22 | } 23 | 24 | public boolean is_block; // This boolean indicates if this is a block or not 25 | public String value = ""; // This value is going to be used if is_block is false 26 | 27 | public Block block; 28 | public Type type; 29 | 30 | // Indicates the return type if type is OTHER 31 | public String other_type; 32 | 33 | Paint text_paint = new Paint(); 34 | Paint rect_paint = new Paint(); 35 | Paint other_paint = new Paint(); // This paint is used to draw text if type is OTHER 36 | 37 | // This is the padding around the field 38 | int text_padding = 10; 39 | 40 | /** 41 | * This will initialize this class as a Block (return value block) 42 | * @param block The block 43 | */ 44 | public BlockField(Block block, Type return_type) { 45 | this.block = block; 46 | this.block.is_return_block = true; 47 | this.block.return_type = return_type; 48 | is_block = true; 49 | init(); 50 | } 51 | 52 | /** 53 | * This constructor initializes this class as a fixed value 54 | * @param value The value 55 | */ 56 | public BlockField(String value) { 57 | this.value = value; 58 | this.type = Type.STRING; 59 | is_block = false; 60 | init(); 61 | } 62 | 63 | /** 64 | * This constructor initializes this class as a fixed value 65 | * @param value The value 66 | * @param type The type of this field 67 | * @param other_type The custom type if {@param type} is OTHER, set to null if otherwise 68 | */ 69 | public BlockField(String value, Type type, @Nullable String other_type) { 70 | this.value = value; 71 | this.type = type; 72 | this.other_type = other_type; 73 | 74 | is_block = false; 75 | init(); 76 | } 77 | 78 | 79 | /** 80 | * This function initializes paints, just like initialize() on BlocksView 81 | */ 82 | private void init() { 83 | text_paint.setColor(0xFF000000); 84 | text_paint.setAntiAlias(true); 85 | text_paint.setStyle(Paint.Style.FILL); 86 | text_paint.setTextSize(20f); 87 | 88 | rect_paint.setStyle(Paint.Style.FILL); 89 | rect_paint.setColor(0xFFFFFFFF); 90 | rect_paint.setAntiAlias(true); 91 | 92 | other_paint.setStyle(Paint.Style.FILL); 93 | other_paint.setColor(0xFFFFFFFF); 94 | other_paint.setTextSize(18f); 95 | other_paint.setAntiAlias(true); 96 | } 97 | 98 | /** 99 | * This function returns the width of this field. 100 | * @param block_text_paint The paint used for the block, our field paint is different, this is just used for "block fields", if you 100% thinks that this isn't a block field, set this to null 101 | * @return The width of this field 102 | */ 103 | public int getWidth(Paint block_text_paint) { 104 | int height = getHeight(block_text_paint); 105 | 106 | int initial_width = is_block ? block.getWidth(block_text_paint) : (int) text_paint.measureText(value); 107 | 108 | if (type == Type.OTHER) { 109 | return (int) other_paint.measureText(other_type + ": " + value) + text_padding * 2; 110 | } 111 | 112 | // If it's boolean, we don't need the padding, just measure half of the parent's height 113 | int estimate_width = initial_width + (type == Type.BOOLEAN ? getHeight(block_text_paint) / 2 : text_padding) * 2; 114 | 115 | // Minimal width for Integer fields are 64 116 | if (type == Type.INTEGER && estimate_width <= 64) { 117 | return 64; 118 | } 119 | 120 | if (type == Type.INTEGER) { 121 | estimate_width += height / 2; 122 | } 123 | 124 | if (type == Type.BOOLEAN && is_block) { 125 | estimate_width += height / 2; 126 | } 127 | 128 | return estimate_width; 129 | } 130 | 131 | /** 132 | * This function returns the height of this field. 133 | * @param block_text_paint The paint used for the block, our field paint is different, this is just used for "block fields", if you 100% thinks that this isn't a block field, set this to null 134 | * @return The height of this field 135 | */ 136 | public int getHeight(Paint block_text_paint) { 137 | if (!is_block) { 138 | Paint.FontMetrics fm; 139 | 140 | if (type == Type.OTHER) { 141 | fm = other_paint.getFontMetrics(); 142 | } else { 143 | fm = text_paint.getFontMetrics(); 144 | } 145 | 146 | float height = fm.descent - fm.ascent; 147 | 148 | return (int) height + text_padding * 2; 149 | } else { 150 | return block.getHeight(block_text_paint); 151 | } 152 | } 153 | 154 | 155 | /** 156 | * This function draws the field at the specified location (We won't add any paddings to it, jokes on you, the left parameter is added with padding) 157 | * @param context The context 158 | * @param canvas The canvas 159 | * @param left The left position 160 | * @param top The top position 161 | * @param block_text_paint The paint used for blocks, if you're sure that this isn't a block field, set this to null 162 | * @param parent_block_height The parent's block height, NOT THE BLOCK ON TOP OF THE FIELD 163 | */ 164 | public void draw(Context context, Canvas canvas, int left, int top, Paint block_text_paint, int parent_block_height, int parent_block_dark_color) { 165 | // NOTE: top has been applied with the padding, you don't need to add padding yourself 166 | 167 | if (!is_block) { 168 | int bottom = top + parent_block_height; 169 | int half_of_parent_height = parent_block_height / 2; 170 | int middle = top + half_of_parent_height; 171 | 172 | int height = getHeight(block_text_paint); 173 | int width = getWidth(block_text_paint); 174 | 175 | switch (type) { 176 | case STRING: 177 | // Draw the white background 178 | canvas.drawRect(left, top, left + width, bottom, rect_paint); 179 | 180 | // Draw the text / value 181 | canvas.drawText(value, left + text_padding, top - ((top - bottom) / 2) + text_padding, text_paint); 182 | break; 183 | 184 | case INTEGER: 185 | DrawHelper.drawIntegerField(canvas, left, top, getWidth(block_text_paint), getHeight(block_text_paint), rect_paint); 186 | 187 | // Draw the text / value 188 | canvas.drawText(value, left + text_padding, middle + text_padding, text_paint); 189 | 190 | break; 191 | 192 | case BOOLEAN: 193 | DrawHelper.drawBooleanField(canvas, left, top, getWidth(block_text_paint), getHeight(block_text_paint), rect_paint); 194 | canvas.drawText(value, left + half_of_parent_height, middle + text_padding, text_paint); 195 | 196 | break; 197 | 198 | case OTHER: 199 | DrawHelper.drawRect(canvas, left, top + (parent_block_height / 2 - height / 2) + text_padding / 2, width, height, parent_block_dark_color); 200 | 201 | canvas.drawText(other_type + ": " + value, left + text_padding, middle + text_padding, other_paint); 202 | break; 203 | } 204 | } else { 205 | // Well, draw the block as the parameter, I guess 206 | block.draw(context, canvas, rect_paint, block_text_paint, top, left, 0, 0, 0, text_padding, false, 0x00000000, false, 0); 207 | // ^ 208 | /* we're setting the outset_height to add a padding to the text, this shouldn't be a thing TODO */ 209 | } 210 | } 211 | 212 | @NonNull 213 | @Override 214 | public String toString() { 215 | return "BlocksViewField{" + 216 | "is_block=" + is_block + 217 | ", value='" + value + '\'' + 218 | ", block=" + block + 219 | ", text_paint=" + text_paint + 220 | ", rect_paint=" + rect_paint + 221 | ", padding=" + text_padding + 222 | '}'; 223 | } 224 | 225 | @Override 226 | public boolean equals(Object o) { 227 | if (this == o) return true; 228 | if (o == null || getClass() != o.getClass()) return false; 229 | 230 | BlockField that = (BlockField) o; 231 | return is_block == that.is_block && 232 | text_padding == that.text_padding && 233 | value.equals(that.value) && 234 | block.equals(that.block) && 235 | type == that.type && 236 | text_paint.equals(that.text_paint) && 237 | rect_paint.equals(that.rect_paint); 238 | } 239 | 240 | @Override 241 | public int hashCode() { 242 | return Objects.hash(is_block, value, block, type, text_paint, rect_paint, text_padding); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /lib/src/main/java/com/openblocks/blocks/view/BlocksView.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.Rect; 10 | import android.graphics.RectF; 11 | import android.graphics.Typeface; 12 | import android.os.Vibrator; 13 | import android.util.AttributeSet; 14 | import android.util.Log; 15 | import android.util.Pair; 16 | import android.view.GestureDetector; 17 | import android.view.MotionEvent; 18 | import android.view.View; 19 | import android.widget.Toast; 20 | 21 | import androidx.annotation.NonNull; 22 | import androidx.annotation.Nullable; 23 | 24 | import java.util.ArrayList; 25 | 26 | /** 27 | * This class is the View that should be used inside your layout, Used to display blocks in an event 28 | */ 29 | public class BlocksView extends View { 30 | 31 | /** 32 | * TAG is used to call logging functions, (e.g. Log.d(TAG, "Hello World!");) 33 | */ 34 | private static final String TAG = "BlocksView"; 35 | 36 | /** 37 | * rect_paint is a {@link Paint} that will be used to draw blocks 38 | */ 39 | Paint rect_paint; 40 | 41 | /** 42 | * text_paint is a {@link Paint} that will be used to draw texts in the blocks 43 | */ 44 | Paint text_paint; 45 | 46 | /** 47 | * shadow_paint is a {@link Paint} that will be used to draw shadow when we picked up a block 48 | */ 49 | Paint shadow_paint; 50 | 51 | 52 | // Customizations variables v=================================================================== 53 | 54 | int left_position = 50; 55 | int top_position = 50; // TODO: DELETE THIS 56 | 57 | int shadow_height = 10; 58 | int block_outset_height = 10; 59 | 60 | int block_height = 60; 61 | int event_top = 50; 62 | 63 | int nested_bottom_margin = 50; 64 | 65 | int event_height = 50; 66 | 67 | int block_outset_left_margin = 50; 68 | int block_outset_width = 75; 69 | 70 | float block_text_size = 30f; 71 | int block_text_color = 0xFFFFFFFF; 72 | 73 | boolean is_overlapping = false; 74 | 75 | boolean is_round = true; 76 | int round_radius = 10; 77 | 78 | // Customizations variables ^ ================================================================== 79 | 80 | 81 | // Other variables ============================================================================= 82 | 83 | /** This variable is used to store blocks of collections */ 84 | BlocksViewEvent event; 85 | 86 | /** This variable is used to detect long presses */ 87 | GestureDetector gestureDetector; 88 | 89 | /** This variable is used to vibrate when the user picked up a block */ 90 | Vibrator vibrator; 91 | 92 | /** Well, Context */ 93 | Context context; 94 | 95 | /** Indicates if we're holding a block */ 96 | boolean isHolding = false; 97 | 98 | /** When the block is picked up, we need to know the offset between the xy point of the (picked up) block (top left) and the cursor */ 99 | int picked_up_x_offset = 0; 100 | int picked_up_y_offset = 0; 101 | 102 | /** When we move the blocks, the {@link #unconnected_blocks} must also move */ 103 | int unconnected_top_offset = 0; 104 | int unconnected_left_offset = 0; 105 | 106 | /** The index of the block we picked inside {@link #unconnected_blocks} */ 107 | int picked_up_block_index = -1; 108 | 109 | /** This array list is used to store unconnected blocks with its real (not modified by movement) coordinates 110 | * then those real coordinates will be added with {@link #unconnected_left_offset} and {@link #unconnected_top_offset} 111 | * 112 | * A Very Important Note: the Vector2D is in it's raw form (doesn't store offset-ed numbers), because most of the blocks are calculated on their raw form, and will be added with event_top and left_position at draw 113 | * */ 114 | ArrayList> unconnected_blocks = new ArrayList<>(); 115 | 116 | /** This array list is used to indicate where the block should land on when dropped */ 117 | // Important note: top_positions are also modified when the view is moved / free move 118 | ArrayList top_positions = new ArrayList<>(); 119 | 120 | /** 121 | * This array list contains "optimized" blocks (blocks that are visible in the canvas) 122 | * Use this list if you're in need of looping each blocks to respond to user interaction 123 | */ 124 | ArrayList optimized_blocks = new ArrayList<>(); 125 | 126 | /** 127 | * This variable is used to determine how many blocks from the top that got 128 | * optimized / removed when drawing (blocks that are visible in the canvas) 129 | */ 130 | int top_optimize_cut = 0; 131 | 132 | /** 133 | * This variable is used to determine how many blocks from the bottom that got 134 | * optimized / removed when drawing (blocks that are visible in the canvas) 135 | */ 136 | int bottom_optimize_cut = 0; 137 | 138 | // Other variables ============================================================================= 139 | 140 | 141 | 142 | // Constructors ================================================================================ 143 | public BlocksView(Context context) { 144 | super(context); 145 | initialize(context, null); 146 | } 147 | 148 | public BlocksView(Context context, @Nullable AttributeSet attrs) { 149 | super(context, attrs); 150 | initialize(context, attrs); 151 | } 152 | 153 | public BlocksView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 154 | super(context, attrs, defStyleAttr); 155 | initialize(context, attrs); 156 | } 157 | 158 | public BlocksView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { 159 | super(context, attrs, defStyleAttr, defStyleRes); 160 | initialize(context, attrs); 161 | } 162 | // Constructors ================================================================================ 163 | 164 | 165 | 166 | // Useful functions ============================================================================ 167 | /** 168 | * Set the event (collection of blocks) that is to be displayed 169 | * @param event The event / collection of blocks 170 | */ 171 | public void setEvent(BlocksViewEvent event) { 172 | this.event = (BlocksViewEvent) event.clone(); 173 | unconnected_blocks.clear(); 174 | picked_up_block_index = -1; 175 | 176 | initialize(this.context, null); 177 | } 178 | 179 | /** 180 | * This method adds a floating block into the editor 181 | * @param block The block that is to be added 182 | * @param x The x position 183 | * @param y The y position 184 | */ 185 | public void addBlock(Block block, int x, int y) { 186 | unconnected_blocks.add(new Pair<>( 187 | new Vector2D(x, y), 188 | block 189 | )); 190 | } 191 | 192 | /** 193 | * This function checks if the user is dragging a block 194 | * @return Is the user dragging a block? 195 | */ 196 | public boolean isDraggingBlock() { 197 | return picked_up_block_index != -1; 198 | } 199 | // Useful functions ============================================================================ 200 | 201 | 202 | 203 | // Initializers ================================================================================ 204 | /** 205 | * Initializes variables 206 | * 207 | * @param context The context 208 | * @param attrs The attribute set 209 | */ 210 | private void initialize(Context context, AttributeSet attrs) { 211 | // Initialize DrawHelper 212 | DrawHelper.initialize(); 213 | 214 | // Set the context 215 | this.context = context; 216 | 217 | fieldClick = field -> { 218 | Toast.makeText(context, field.value, Toast.LENGTH_SHORT).show(); 219 | Log.d(TAG, "onFieldClick: " + field.toString()); 220 | }; 221 | 222 | // Get the vibrator system service 223 | try { 224 | vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); 225 | } catch (AssertionError ignored) { 226 | // Vibrator service isn't supported, it might be because we are in an emulation or smth, so skip instead 227 | } 228 | 229 | // Check if attribute is not null 230 | if (attrs != null) { 231 | // Initialize our attributes 232 | initializeAttributes(attrs); 233 | } 234 | 235 | // Is the event set? 236 | if (event == null) { 237 | // If not, show demo blocks instead 238 | demoBlocks(); 239 | } 240 | 241 | // Initialize Paints 242 | 243 | // text_paint is used to draw our block's texts 244 | text_paint = new Paint(); 245 | text_paint.setTypeface(Typeface.DEFAULT); 246 | text_paint.setStyle(Paint.Style.FILL); 247 | text_paint.setFakeBoldText(true); 248 | text_paint.setAntiAlias(true); 249 | text_paint.setColor(block_text_color); 250 | text_paint.setTextSize(block_text_size); 251 | 252 | // line_paint is used to draw the line that indicates where will the block dropped to 253 | line_paint = new Paint(); 254 | line_paint.setColor(Color.parseColor("#000000")); 255 | line_paint.setAntiAlias(true); 256 | line_paint.setStyle(Paint.Style.FILL_AND_STROKE); 257 | 258 | // rect_paint is used to draw the block 259 | rect_paint = new Paint(); 260 | rect_paint.setAntiAlias(true); 261 | rect_paint.setStyle(Paint.Style.FILL); 262 | 263 | // shadow_paint is used to draw the shadow when we picked up a block 264 | shadow_paint = new Paint(); 265 | shadow_paint.setShadowLayer(16, 0, 0, Color.BLACK); 266 | 267 | // black_rect is used to draw a "block drop to field" indicator 268 | black_rect = new Paint(); 269 | black_rect.setColor(0x88000000); 270 | black_rect.setStyle(Paint.Style.FILL); 271 | 272 | // Initialize our gesture detector 273 | initGestureDetector(); 274 | } 275 | 276 | /** 277 | * This method sets an attribute to our variables 278 | * 279 | * @param attrs The attribute set 280 | */ 281 | private void initializeAttributes(@NonNull AttributeSet attrs) { 282 | TypedArray attributes = context.obtainStyledAttributes(attrs, R.styleable.BlocksView); 283 | 284 | left_position = attributes.getDimensionPixelSize(R.styleable.BlocksView_left_position, left_position); 285 | top_position = attributes.getDimensionPixelSize(R.styleable.BlocksView_top_position, top_position); 286 | 287 | shadow_height = attributes.getDimensionPixelSize(R.styleable.BlocksView_shadow_height, shadow_height); 288 | 289 | block_outset_height = attributes.getDimensionPixelSize(R.styleable.BlocksView_block_outset_height, block_outset_height); 290 | block_outset_width = attributes.getDimensionPixelSize(R.styleable.BlocksView_block_outset_width, block_outset_width); 291 | block_outset_left_margin = attributes.getDimensionPixelSize(R.styleable.BlocksView_block_outset_left_margin, block_outset_left_margin); 292 | block_text_size = attributes.getDimensionPixelSize(R.styleable.BlocksView_block_text_size, (int) block_text_size); 293 | block_height = attributes.getDimensionPixelSize(R.styleable.BlocksView_block_height, block_height); 294 | 295 | block_text_color = attributes.getColor(R.styleable.BlocksView_block_text_color, block_text_color); 296 | 297 | event_top = attributes.getDimensionPixelSize(R.styleable.BlocksView_event_top, event_top); 298 | event_height = attributes.getDimensionPixelSize(R.styleable.BlocksView_event_height, event_height); 299 | 300 | is_overlapping = attributes.getBoolean(R.styleable.BlocksView_is_overlapping, is_overlapping); 301 | 302 | nested_bottom_margin = attributes.getDimensionPixelSize(R.styleable.BlocksView_nested_bottom_margin, nested_bottom_margin); 303 | 304 | attributes.recycle(); 305 | } 306 | 307 | /** 308 | * This method adds blocks used to demonstration into our block collection ({@link #event}) 309 | */ 310 | private void demoBlocks() { 311 | event = new BlocksViewEvent("MainActivity", "onCreate"); 312 | 313 | event.blocks.add(new Block("Hello World", "1", 2, new ArrayList<>(), 0xFFE10C0C)); 314 | event.blocks.add(new Block("This is BlocksView", "2", 3, new ArrayList<>(), 0xFFD1159C)); 315 | event.blocks.add(new Block("This block resizes", "3", 4, new ArrayList<>(), 0xFF14D231)); 316 | event.blocks.add(new Block("According to the text's width", "4", 5, new ArrayList<>(), 0xFF2115D1)); 317 | 318 | ArrayList fields = new ArrayList<>(); 319 | fields.add(new BlockField("parameters")); 320 | fields.add(new BlockField("yeah")); 321 | 322 | event.blocks.add(new Block("This block has %s cool right? %s.kek", "5", 6, fields, 0xFFE10C0C)); 323 | 324 | ArrayList types_fields = new ArrayList<>(); 325 | types_fields.add(new BlockField("1945", BlockField.Type.INTEGER, null)); 326 | 327 | event.blocks.add(new Block("Oh yeah, integers %i", types_fields, 0xFFE65319)); 328 | 329 | ArrayList booleans = new ArrayList<>(); 330 | booleans.add(new BlockField("false", BlockField.Type.BOOLEAN, null)); 331 | 332 | event.blocks.add(new Block("And booleans %b", booleans, 0xFF2115D1)); 333 | 334 | ArrayList value_of_recursive = new ArrayList<>(); 335 | 336 | ArrayList get_id_recursive = new ArrayList<>(); 337 | get_id_recursive.add(new BlockField("Hello World")); 338 | 339 | ArrayList recursive_fields_root = new ArrayList<>(); 340 | recursive_fields_root.add(new BlockField(new Block("get ID %s", "10", -1, get_id_recursive, 0xFF0000FF), BlockField.Type.INTEGER)); 341 | 342 | value_of_recursive.add(new BlockField(new Block("Is empty %s", "10", -1, recursive_fields_root, 0xFF15D807), BlockField.Type.BOOLEAN)); 343 | 344 | event.blocks.add(new Block("Also, recursive fields! %m.view", "6", 7, value_of_recursive, 0xFFE65319)); 345 | 346 | ArrayList bloks = new ArrayList<>(); 347 | bloks.add(new Block("Yeah, nested blocks!", "1", 2, new ArrayList<>(), 0xFFE10C0C)); 348 | bloks.add(new Block("Very cool, right?", "2", 3, new ArrayList<>(), 0xFFE65319)); 349 | 350 | ArrayList imageview_set_image = new ArrayList<>(); 351 | 352 | imageview_set_image.add(new BlockField("imageView1", BlockField.Type.OTHER, "ImageView")); 353 | imageview_set_image.add(new BlockField("image_file", BlockField.Type.OTHER, "File")); 354 | 355 | bloks.add(new Block("%m.img Set image to PNG %o.file", "3", -1, imageview_set_image, 0xFFE65319)); 356 | 357 | ArrayList a = new ArrayList<>(); 358 | a.add(new BlockField("oh god")); 359 | 360 | event.blocks.add(new NestedBlock("Did i say nested? %a", "7", 8, a, 0xFF21167B, bloks)); //0xFFE10C0C 361 | 362 | event.blocks.add(new Block("Originally Made by Iyxan23 (github.com/Iyxan23)", "8", 9, new ArrayList<>(), 0xFF2115D1)); 363 | event.blocks.add(new Block("Repository transferred to OpenBlocksTeam (github.com/OpenBlocksTeam)", "9", 10, new ArrayList<>(), 0xFFE10C0C)); 364 | 365 | event.blocks.add(new Block("Finish Activity", "10", -1, new ArrayList<>(), 0xFF1173E4)); 366 | } 367 | // Initializers ================================================================================ 368 | 369 | 370 | 371 | // Touch detectors ============================================================================= 372 | /** 373 | * This function initializes our {@link GestureDetector} used for detecting long clicks 374 | */ 375 | private void initGestureDetector() { 376 | gestureDetector = new GestureDetector(new GestureDetector.SimpleOnGestureListener() { 377 | 378 | // Used to detect clicking on fields 379 | @Override 380 | public boolean onSingleTapConfirmed(MotionEvent e) { 381 | checkFieldClick((int) e.getX(), (int) e.getY()); 382 | 383 | return super.onSingleTapConfirmed(e); 384 | } 385 | 386 | // Used to detect dragging blocks 387 | @Override 388 | public void onLongPress(MotionEvent e) { 389 | Log.d(TAG, "onLongPress: long press!"); 390 | 391 | // Get the x y position of the long press 392 | int x = (int) e.getX(); 393 | int y = (int) e.getY(); 394 | 395 | // Pick up the block 396 | picked_up_block_index = pickup_block(x, y); 397 | 398 | // Check if there isn't any blocks below us 399 | if (picked_up_block_index == -1) 400 | // Meh, nothing, just return 401 | return; 402 | 403 | // If no, get the block, and pick it up! 404 | Pair block = unconnected_blocks.get(picked_up_block_index); 405 | picked_up_x_offset = block.first.x - x; 406 | picked_up_y_offset = block.first.y - y; 407 | 408 | // Oh yeah vibrate a little, just to give some sense 409 | vibrator.vibrate(100); 410 | 411 | // And set that we're holding something 412 | isHolding = true; 413 | } 414 | }); 415 | } 416 | 417 | // Variables used to calculate the offset between when we move the canvas / blocks 418 | int move_x_delta = 0; 419 | int move_y_delta = 0; 420 | 421 | /** The position of an element inside {@link #top_positions} where the dragged block wants to be dropped to */ 422 | int drop_location = -1; 423 | 424 | @SuppressLint("ClickableViewAccessibility") 425 | @Override 426 | public boolean onTouchEvent(MotionEvent mot_event) { 427 | // Check if this is a long press 428 | gestureDetector.onTouchEvent(mot_event); 429 | 430 | switch (mot_event.getAction()) { 431 | case MotionEvent.ACTION_DOWN: 432 | Log.d(TAG, "onTouchEvent: DOWN"); 433 | 434 | // If we didn't picked up anything, and we're just touching the canvas 435 | if (picked_up_block_index == -1) { 436 | // The user is moving the canvas! 437 | // Set the delta / differences 438 | move_x_delta = (int) mot_event.getX() - left_position; 439 | move_y_delta = (int) mot_event.getY() - event_top; 440 | } 441 | 442 | // We handled the ACTION_DOWN, return true! 443 | return true; 444 | 445 | case MotionEvent.ACTION_MOVE: 446 | 447 | // Get the x and y position of our cursor / touch position 448 | int x = (int) mot_event.getX(); 449 | int y = (int) mot_event.getY(); 450 | 451 | // Are we holding a block? 452 | if (isHolding) { 453 | // Move the block to the designated location 454 | 455 | // Check if we picked up a block 456 | if (picked_up_block_index == -1) { 457 | // We didn't picked anything, just quit 458 | break; 459 | } 460 | 461 | // K, let's move the block 462 | unconnected_blocks.get(picked_up_block_index).first.x = x + picked_up_x_offset; 463 | unconnected_blocks.get(picked_up_block_index).first.y = y + picked_up_y_offset; 464 | 465 | // Predict the drop location of where the block should be dropped to 466 | drop_location = predictDropLocation(); 467 | } else { 468 | // so the user is casually moving the view 469 | 470 | // Set the offset for unconnected blocks 471 | 472 | unconnected_top_offset = y - move_y_delta + event_top; 473 | unconnected_left_offset = x - move_x_delta + left_position; 474 | 475 | // and also for every blocks 476 | event_top = y - move_y_delta; 477 | left_position = x - move_x_delta; 478 | 479 | } 480 | 481 | // Redraw 482 | invalidate(); 483 | 484 | return true; 485 | 486 | case MotionEvent.ACTION_UP: 487 | Log.d(TAG, "onTouchEvent: UP"); 488 | // Check if we have a relevant drop location below us 489 | if (drop_location != -1) { 490 | // Ohh ok, let's add the block into the block collection, at the specified index 491 | event.blocks.add(top_positions.indexOf(drop_location), unconnected_blocks.get(picked_up_block_index).second); 492 | 493 | // Then remove it from the unconnected blocks 494 | unconnected_blocks.remove(picked_up_block_index); 495 | } 496 | 497 | // Reset values 498 | isHolding = false; 499 | picked_up_block_index = -1; 500 | 501 | draw_line_at_pos = -1; 502 | drop_location = -1; 503 | 504 | // predicted_drop_field = null; 505 | 506 | move_y_delta = 0; 507 | move_x_delta = 0; 508 | 509 | return true; 510 | } 511 | 512 | return false; 513 | } 514 | // Touch detectors ============================================================================= 515 | 516 | 517 | 518 | // Pickup, drop blocks utilities =============================================================== 519 | 520 | // The bounds where of how big we should detect if the user wants to drop a block 521 | int detection_distance_vertical = 20; 522 | int detection_distance_right = 400; 523 | 524 | // debug 525 | int draw_line_at_pos = -1; 526 | 527 | // The paint used to draw the line that indicates where the block should be placed / dropped 528 | Paint line_paint; 529 | 530 | // This rect is used to draw a basic black rectangle in the canvas if the picked up block is hovering over a field 531 | Rect predicted_drop_field = null; 532 | Paint black_rect; // This paint is used to draw the rect above 533 | 534 | /** 535 | * This function predicts the location of where the picked up block will be dropped 536 | * @return The index element of where the block will be dropped in {@link #top_positions}, returns -1 if the block is dropped on nothing 537 | */ 538 | private int predictDropLocation() { 539 | Pair picked_up_block = unconnected_blocks.get(picked_up_block_index); 540 | // The picked up block's position 541 | Vector2D picked_up_block_position = picked_up_block.first; 542 | Block picked_up_block_block = picked_up_block.second; 543 | 544 | if (!picked_up_block_block.is_return_block) { 545 | int index = 0; 546 | 547 | for (Integer point : top_positions) { 548 | // Check if the picked up block position is inside the bounds of 549 | // We must add these offsets because the top_positions are modified / offset-ed version of it 550 | if ( 551 | picked_up_block_position.y + event_top > point - detection_distance_vertical && 552 | picked_up_block_position.y + event_top < point + detection_distance_vertical && 553 | picked_up_block_position.x + left_position > left_position && 554 | picked_up_block_position.x + left_position < event.blocks.get(index).getWidth(text_paint) 555 | ) { 556 | draw_line_at_pos = point; 557 | // Log.d(TAG, "predictDropLocation: top: " + point); 558 | return point; 559 | } 560 | 561 | index++; 562 | } 563 | } else { 564 | 565 | int index = 0; 566 | for (Integer block_top : top_positions) { 567 | 568 | int previous_block_top = index - 1 != -1 ? top_positions.get(index - 1) : 0; 569 | 570 | // Check if this block is in the bounds of the drag position 571 | if (!(picked_up_block_position.y + unconnected_top_offset > block_top && picked_up_block_position.y + unconnected_top_offset < previous_block_top)) 572 | continue; 573 | 574 | Log.d(TAG, "predictDropLocation: m"); 575 | 576 | // do onHover 577 | 578 | Rect field_bounds = event.blocks.get(index).onHover(picked_up_block_position.x, picked_up_block_position.y, event.blocks.get(index), false, text_paint); 579 | 580 | if (field_bounds != null) { 581 | predicted_drop_field = field_bounds; 582 | } 583 | 584 | index++; 585 | } 586 | } 587 | 588 | draw_line_at_pos = -1; 589 | return -1; 590 | } 591 | 592 | /** 593 | * This function is used to pickup a block at the specified location 594 | * 595 | * @param x The x coordinate 596 | * @param y The y coordinate 597 | * @return The index of this block in unconnected_blocks 598 | */ 599 | private int pickup_block(int x, int y) { 600 | // Check if a block already exists in the unconnected_blocks 601 | int index = 0; 602 | for (Pair block : unconnected_blocks) { 603 | Vector2D block_position = block.first.clone(); 604 | 605 | Block mBlock = block.second; 606 | 607 | RectF block_bounds = new RectF( 608 | block_position.x + left_position, 609 | block_position.y + event_top, 610 | 611 | block_position.x + left_position 612 | + mBlock.getWidth(text_paint), 613 | 614 | block_position.y + event_top 615 | + mBlock.getHeight(text_paint) 616 | ); 617 | 618 | if (block_bounds.contains(x, y)) { 619 | // Ohh, we're here bois, let's just return the location 620 | return index; 621 | } 622 | 623 | index++; 624 | } 625 | 626 | Log.d(TAG, "pickup_block: Not picked, picking blocks"); 627 | 628 | // Looks like it hasn't been dragged yet, let's just check each blocks 629 | // This code is almost the same as in the onDraw() method 630 | int previous_top_position = event_height; // Start with event_offset 631 | int previous_block_height = event_top; // Because if not, the first block would get overlapped by the event 632 | 633 | for (int i = 0; i < event.blocks.size(); i++) { 634 | Block current_block = event.blocks.get(i); 635 | 636 | int top_position; 637 | 638 | top_position = previous_top_position + previous_block_height + shadow_height; 639 | 640 | if (is_overlapping) { 641 | // Overlap the previous block's shadow 642 | top_position -= shadow_height; 643 | } 644 | 645 | previous_top_position = top_position; 646 | 647 | previous_block_height = current_block.getHeight(text_paint); 648 | 649 | // Apply the bottom margin if this is a nested block 650 | if (current_block instanceof NestedBlock) { 651 | ((NestedBlock) current_block).bottom_margin = nested_bottom_margin; 652 | } 653 | 654 | Rect bounds = new Rect( 655 | left_position, 656 | top_position, // TODO: IMPLEMENT DRAGGING BLOCKS INSIDE A NESTED BLOCK, AND ALSO PARAMETER BLOCKS 657 | left_position + current_block.getWidth(text_paint), 658 | top_position + current_block.getHeight(text_paint) 659 | ); 660 | 661 | if (bounds.contains(x, y)) { 662 | Log.d(TAG, "pickup_block: x: " + x + " y: " + y); 663 | Log.d(TAG, "pickup_block: block bounds: " + bounds); 664 | Log.d(TAG, "pickup_block: Inside the block " + top_position); 665 | 666 | // Ohk, call onPickup of the block 667 | Pair pickup = event.blocks.get(i).onPickup(x - left_position, y - top_position, text_paint); 668 | 669 | // The first pair is to determine if we should remove the block or not? 670 | if (pickup.first) { 671 | // Yup, remove it 672 | 673 | try { 674 | event.blocks.remove(pickup.second); 675 | } catch (Exception ignored) { } 676 | } 677 | 678 | // Add it to the unconnected blocks (picking it up) 679 | unconnected_blocks.add(0, new Pair<>(new Vector2D(x - left_position, y - event_top), pickup.second)); 680 | 681 | // And returning it's position 682 | return 0; 683 | 684 | /* 685 | switch (pickup.first) { 686 | case PICKUP_SELF: 687 | Log.d(TAG, "pickup_block: self"); 688 | 689 | // Ok, first, we're gonna pop out this block from the event.blocks 690 | // then we're gonna add this block to the unconnected blocks 691 | event.blocks.remove(i); 692 | 693 | unconnected_blocks.add(0, new Pair<>(new Vector2D(x - left_position, y - event_top), current_block)); 694 | 695 | // Return the position of the unconnected block (it should be at the first item) 696 | return 0; 697 | 698 | case PICKUP_OTHER_BLOCK: 699 | Log.d(TAG, "pickup_block: parameter"); 700 | 701 | // Ah, this guy is picking up a parameter, anyway, because the parameter is 702 | // already removed on onPickup, we're just gonna add this to the unconnected blocks 703 | 704 | // Add it to the unconnected blocks 705 | unconnected_blocks.add(0, new Pair<>(new Vector2D(x - left_position, y - event_top), pickup.second)); 706 | 707 | // Return the position of this parameter of unconnected_blocks 708 | return 0; 709 | case PICKUP_NONE: 710 | Log.d(TAG, "pickup_block: Pickup none"); 711 | // Nothing 712 | break; 713 | } 714 | */ 715 | } 716 | } 717 | 718 | // Ok, this guy is just clicking nothing 719 | Log.d(TAG, "pickup_block: meh, nothing"); 720 | return -1; 721 | } 722 | 723 | FieldClick fieldClick = null; 724 | 725 | /** 726 | * This function sets the field click 727 | * @param fieldClick The field click listener 728 | */ 729 | public void setFieldClick(FieldClick fieldClick) { 730 | this.fieldClick = fieldClick; 731 | } 732 | 733 | /** 734 | * This function is used to check if a click clicks a field 735 | */ 736 | private void checkFieldClick(int x, int y) { 737 | Log.d(TAG, "checkFieldClick: y: " + y); 738 | if (fieldClick == null) 739 | return; 740 | 741 | int top = event_top + event_height + event_height; // I have no idea why this works, but this works 742 | 743 | for (Block block : event.blocks) { 744 | Log.d(TAG, "checkFieldClick: at block " + block.getFormat()); 745 | Log.d(TAG, "checkFieldClick: top: " + top); 746 | int current_block_height = block.getHeight(text_paint); 747 | Log.d(TAG, "checkFieldClick: bottom: " + (top + current_block_height)); 748 | 749 | // Only check if the top is lower than the y - previous block height 750 | if (top < y && top + current_block_height > y) { 751 | Log.d(TAG, "checkFieldClick: ye this is a click"); 752 | BlockField field = block.onClick(x - left_position, y - top, text_paint); 753 | 754 | if (field != null) { 755 | Log.d(TAG, "checkFieldClick: valid click"); 756 | fieldClick.onFieldClick(field); 757 | return; 758 | } 759 | } 760 | 761 | top += current_block_height; 762 | } 763 | } 764 | // Pickup, drop blocks utilities =============================================================== 765 | 766 | 767 | 768 | // Measurer ==================================================================================== 769 | @Override 770 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 771 | Log.v("Chart onMeasure w", MeasureSpec.toString(widthMeasureSpec)); 772 | Log.v("Chart onMeasure h", MeasureSpec.toString(heightMeasureSpec)); 773 | 774 | int largest_width = 0; 775 | int blocks_height_sum = 0; 776 | for (Block block : event.blocks) { 777 | largest_width = Math.max(block.getWidth(text_paint), largest_width); 778 | 779 | blocks_height_sum += block.getHeight(text_paint) + shadow_height; 780 | } 781 | 782 | int desiredWidth = left_position + largest_width + getPaddingLeft() + getPaddingRight() + left_position /* Just to get some padding on the right */; 783 | 784 | int desiredHeight = event_top + event_height + blocks_height_sum + getPaddingTop() + getPaddingBottom() + top_position; 785 | 786 | setMeasuredDimension(measureDimension(desiredWidth, widthMeasureSpec), 787 | measureDimension(desiredHeight, heightMeasureSpec)); 788 | } 789 | 790 | private int measureDimension(int desiredSize, int measureSpec) { 791 | int result; 792 | int specMode = MeasureSpec.getMode(measureSpec); 793 | int specSize = MeasureSpec.getSize(measureSpec); 794 | 795 | if (specMode == MeasureSpec.EXACTLY) { 796 | result = specSize; 797 | } else { 798 | result = desiredSize; 799 | if (specMode == MeasureSpec.AT_MOST) { 800 | result = Math.min(result, specSize); 801 | } 802 | } 803 | 804 | if (result < desiredSize){ 805 | Log.w("BlocksView", "The view is too small, the content might get cut"); 806 | } 807 | 808 | return result; 809 | } 810 | // Measurer ==================================================================================== 811 | 812 | 813 | 814 | @Override 815 | protected void onDraw(Canvas canvas) { 816 | super.onDraw(canvas); 817 | 818 | top_positions.clear(); 819 | 820 | // Empty our canvas 821 | canvas.drawColor(0xFFFFFFFF); 822 | 823 | // Draw the blocks from top to bottom 824 | int previous_block_color = event.color; 825 | int previous_top_position = event_height; // Start with event_offset 826 | int previous_block_height = event_top; // Because if not, the first block would get overlapped by the event 827 | 828 | // Are we overlapping? 829 | if (is_overlapping) { 830 | // If yes, then increase the top position, as the event can overlap our first block 831 | previous_top_position += block_outset_height; 832 | } 833 | 834 | // Loop per each block 835 | for (int i = 0; i < event.blocks.size(); i++) { 836 | 837 | // get the current block 838 | Block current_block = event.blocks.get(i); 839 | 840 | // Set the height to the defined height 841 | current_block.default_height = block_height; 842 | 843 | // Get the top position of that block 844 | int top_position; 845 | top_position = previous_top_position + previous_block_height + shadow_height; 846 | 847 | if (is_overlapping) { 848 | // Overlap the previous block's shadow 849 | top_position -= shadow_height; 850 | } 851 | 852 | int current_block_height = current_block.getHeight(text_paint); 853 | 854 | // Set the previous stuff to this stuff, will be used later 855 | previous_top_position = top_position; 856 | previous_block_height = current_block_height; 857 | 858 | // Apply the bottom margin if this is a nested block 859 | if (current_block instanceof NestedBlock) { 860 | ((NestedBlock) current_block).bottom_margin = nested_bottom_margin; 861 | } 862 | 863 | // To optimize the drawing, check if this block is actually visible to the user 864 | if (top_position + current_block_height < 0) { 865 | // no, this block isn't visible, skip this 866 | top_optimize_cut++; 867 | continue; 868 | } else if (top_position > getHeight()) { 869 | // this block is too far down, skip this 870 | bottom_optimize_cut++; 871 | continue; 872 | } else { 873 | // This block is visible / being drawn, add this to the optimized blocks 874 | optimized_blocks.add(current_block); 875 | } 876 | 877 | // Oh yeah add the top_position to our top_positions array list 878 | top_positions.add(top_position); 879 | 880 | // Finally, draw our block 881 | current_block 882 | .draw( 883 | context, 884 | canvas, 885 | rect_paint, 886 | text_paint, 887 | top_position, 888 | left_position, 889 | shadow_height, 890 | block_outset_left_margin, 891 | block_outset_width, 892 | block_outset_height, 893 | is_overlapping, 894 | previous_block_color, 895 | is_round, 896 | round_radius 897 | ); 898 | 899 | previous_block_color = current_block.color; 900 | } 901 | 902 | // After drawing all the blocks, let's draw the event / the yellow thing on the top 903 | event.draw( 904 | canvas, 905 | event_height, 906 | 10, 907 | left_position, 908 | event_top, 909 | 15, 910 | block_outset_left_margin, 911 | block_outset_width, 912 | shadow_height, 913 | rect_paint, 914 | text_paint, 915 | is_round, 916 | round_radius 917 | ); 918 | 919 | 920 | // Null check 921 | if (unconnected_blocks == null) 922 | return; 923 | 924 | // Draw the unconnected blocks 925 | int index = 0; 926 | for (Pair block : unconnected_blocks) { 927 | Vector2D position = block.first.clone(); 928 | position.x += left_position; 929 | position.y += event_top; 930 | 931 | // Important for certain APIs 932 | setLayerType(LAYER_TYPE_SOFTWARE, shadow_paint); 933 | 934 | if (index == picked_up_block_index) { 935 | canvas.drawRect( 936 | position.x, 937 | position.y, 938 | position.x + block.second.getWidth(text_paint), 939 | position.y + block.second.getHeight(text_paint), 940 | shadow_paint 941 | ); 942 | } 943 | 944 | // Draw the block 945 | block.second.draw( 946 | context, 947 | canvas, 948 | rect_paint, 949 | text_paint, 950 | position.y, 951 | position.x, 952 | shadow_height, 953 | block_outset_left_margin, 954 | block_outset_width, 955 | block_outset_height, 956 | is_overlapping, 957 | 0x00000000, 958 | is_round, 959 | round_radius 960 | ); 961 | 962 | index++; 963 | } 964 | 965 | // Draw the line where it indicates if we're dropping a block into the collection 966 | if (draw_line_at_pos != -1) { 967 | canvas.drawRect(left_position, draw_line_at_pos - 5, left_position + detection_distance_right, draw_line_at_pos + 5, line_paint); 968 | } 969 | 970 | // Draw the rect where it indicates if we're dropping a block into a field 971 | if (predicted_drop_field != null) { 972 | canvas.drawRect(predicted_drop_field, black_rect); 973 | } 974 | } 975 | 976 | 977 | 978 | // Utility classes ============================================================================= 979 | 980 | /** 981 | * This class is used to store 2 dimensional coordinates 982 | */ 983 | public static class Vector2D { 984 | public int x; 985 | public int y; 986 | 987 | public Vector2D(int x, int y) { this.x = x; this.y = y; } 988 | 989 | @NonNull 990 | @Override 991 | protected Vector2D clone() { 992 | return new Vector2D(this.x, this.y); 993 | } 994 | } 995 | // Utility classes ============================================================================= 996 | 997 | 998 | 999 | // Interfaces ================================================================================== 1000 | public interface FieldClick { 1001 | void onFieldClick(BlockField field); 1002 | } 1003 | // Interfaces ================================================================================== 1004 | } 1005 | -------------------------------------------------------------------------------- /lib/src/main/java/com/openblocks/blocks/view/BlocksViewEvent.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import java.util.ArrayList; 9 | 10 | /** 11 | * This class is used to represent a collection of blocks and an event / orange-yellow thingy at the top 12 | */ 13 | public class BlocksViewEvent { 14 | 15 | public String activity_name; 16 | public String name; 17 | 18 | public int color = 0xFFF39B0E; 19 | 20 | public int text_padding = 10; 21 | 22 | public ArrayList blocks = new ArrayList<>(); 23 | 24 | public BlocksViewEvent(String activity_name, String name) { 25 | this.activity_name = activity_name; 26 | this.name = name; 27 | } 28 | 29 | /** 30 | * Draws the event to a canvas with these specific parameters 31 | * 32 | * @param canvas The canvas to be drawn on 33 | * @param height The event's height (The body, this doesn't include the bump at the top) 34 | * @param outset_height The outset (or the tiny block coming out from the bottom)'s height 35 | * @param left_position The left position or Y 36 | * @param top_position The top position or X 37 | * @param top_bump_height The top bump's height 38 | * @param shadow_height The shadow's height 39 | * @param rect_paint The paint that is used to draw the rect 40 | * @param text_paint The paint that is used to draw the text 41 | */ 42 | public void draw(Canvas canvas, int height, int outset_height, int left_position, int top_position, int top_bump_height, int outset_left_margin, int outset_width, int shadow_height, Paint rect_paint, Paint text_paint, boolean is_round, int round_radius) { 43 | 44 | // int height = 50; 45 | // int outset_height = 10; 46 | 47 | String text = activity_name + ": " + name; 48 | int text_width = (int) text_paint.measureText(text) + text_padding * 2; 49 | 50 | if (!is_round) { 51 | // Draw the block's body 52 | DrawHelper.drawRectSimpleOutsideShadow(canvas, left_position, top_position, text_width + text_padding, height + text_padding, shadow_height, color); 53 | 54 | // top bump, don't draw shadow 55 | DrawHelper.drawRect(canvas, left_position, top_position - top_bump_height, 250, height, color); 56 | 57 | // outset 58 | DrawHelper.drawRectSimpleOutsideShadow(canvas, left_position + outset_left_margin, top_position, outset_width, height + outset_height + outset_height, shadow_height, color); 59 | } else { 60 | // Draw the block's body 61 | DrawHelper.drawRoundRectSimpleOutsideShadow(canvas, left_position, top_position, text_width + text_padding, height + text_padding, shadow_height, round_radius, color); 62 | 63 | // top bump, don't draw shadow 64 | DrawHelper.drawRoundRect(canvas, left_position, top_position - top_bump_height, 250, height, round_radius, color); 65 | 66 | // outset 67 | DrawHelper.drawRectSimpleOutsideShadow(canvas, left_position + outset_left_margin, top_position, outset_width, height + outset_height + outset_height, shadow_height, color); 68 | } 69 | 70 | 71 | // Draw the text 72 | canvas.drawText(text, left_position + text_padding, top_bump_height + top_position + (height >> 1), text_paint); 73 | } 74 | 75 | @NonNull 76 | @Override 77 | public String toString() { 78 | return "BlocksView Event:\n\tActivityName:" + activity_name + "\n\tEventName: " + name + "\nBlocks:\n" + blocks.toString(); 79 | } 80 | 81 | @NonNull 82 | @Override 83 | protected Object clone() { 84 | BlocksViewEvent clone_event = new BlocksViewEvent(this.activity_name, this.name); 85 | clone_event.blocks = (ArrayList) blocks.clone(); 86 | return clone_event; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/src/main/java/com/openblocks/blocks/view/DrawHelper.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Color; 5 | import android.graphics.Paint; 6 | import android.graphics.Path; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | /** 11 | * A simple helper class used to draw stuff on the canvas 12 | */ 13 | public class DrawHelper { 14 | 15 | // There are basically no way except this 16 | static Paint paint; 17 | 18 | public static void initialize() { 19 | paint = new Paint(Paint.ANTI_ALIAS_FLAG); 20 | } 21 | 22 | public static void drawBooleanField(Canvas canvas, int x, int y, int width, int height, int color) { 23 | paint.setColor(color); 24 | 25 | drawBooleanField(canvas, x, y, width, height, paint); 26 | } 27 | 28 | public static void drawBooleanField(Canvas canvas, int x, int y, int width, int height, Paint p) { 29 | int half_height = height / 2; 30 | int middle = y + half_height; 31 | 32 | Path path = new Path(); 33 | path.setFillType(Path.FillType.EVEN_ODD); 34 | 35 | //////////////////////////////////////////////////////////////////////////////// 36 | 37 | // Draw the left triangle 38 | 39 | // Start from the bottom part of the triangle 40 | path.moveTo(x + half_height, y + height); 41 | 42 | // Then go to the sharp point (left) of the triangle 43 | path.lineTo(x, middle); 44 | 45 | // Then go to the y part of the left triangle 46 | path.lineTo(x + half_height, y); 47 | 48 | //////////////////////////////////////////////////////////////////////////////// 49 | 50 | // Draw the right triangle 51 | 52 | // Move to the right triangle's top part 53 | path.lineTo(x + width - half_height, y); 54 | 55 | // Then go to the sharp point (right) of the triangle 56 | path.lineTo(x + width, middle); 57 | 58 | // Then go to the bottom part of the right triangle 59 | path.lineTo(x + width - half_height, y + height); 60 | 61 | //////////////////////////////////////////////////////////////////////////////// 62 | 63 | // Finally, go back to the previous bottom part of the x triangle to connect the lines 64 | path.moveTo(x + half_height, y + height); 65 | path.close(); 66 | 67 | canvas.drawPath(path, p); 68 | } 69 | 70 | 71 | public static void drawIntegerField(Canvas canvas, int x, int y, int width, int height, int color) { 72 | paint.setColor(color); 73 | 74 | drawIntegerField(canvas, x, y, width, height, paint); 75 | } 76 | 77 | public static void drawIntegerField(@NonNull Canvas canvas, int x, int y, int width, int height, Paint p) { 78 | int half_height = height / 2; 79 | int middle = half_height + y; 80 | 81 | // Draw the oval-ly background 82 | // Draw the x circle 83 | canvas.drawCircle(half_height + x, middle, half_height, p); 84 | 85 | // Draw the right circle 86 | canvas.drawCircle(x + width - half_height, middle, half_height, p); 87 | 88 | // Draw a rectangle between the half part of the x circle to the half part of the right circle 89 | canvas.drawRect(x + half_height, y, x + width - half_height, y + height, p); 90 | } 91 | 92 | 93 | // The shadow will be drawn inside the block 94 | public static void drawRectSimpleInsideShadow(@NonNull Canvas canvas, int x, int y, int width, int height, int shadow_height, int color) { 95 | drawRectSimpleOutsideShadow(canvas, x, y, width, height - shadow_height, shadow_height, color); 96 | } 97 | 98 | // The shadow will be inside the block, not outside 99 | public static void drawRoundRectSimpleOutsideShadow(@NonNull Canvas canvas, int x, int y, int width, int height, int shadow_height, int radius, int color) { 100 | // Draw shadow 101 | drawRoundRect(canvas, x, y, width, height, radius, manipulateColor(color, 0.8f)); 102 | 103 | // Draw the actual block 104 | drawRoundRect(canvas, x, y, width, height - shadow_height, radius, color); 105 | } 106 | 107 | // The shadow will be inside the block, not outside 108 | public static void drawRectSimpleOutsideShadow(@NonNull Canvas canvas, int x, int y, int width, int height, int shadow_height, int color) { 109 | // Draw shadow 110 | drawRect(canvas, x, y, width, height, manipulateColor(color, 0.8f)); 111 | 112 | // Draw the actual block 113 | drawRect(canvas, x, y, width, height - shadow_height, color); 114 | } 115 | 116 | public static void drawRectAbsolute(@NonNull Canvas canvas, int x, int y, int x1, int y1, int color) { 117 | paint.setColor(color); 118 | 119 | canvas.drawRect(x, y, x1, y1, paint); 120 | } 121 | 122 | public static void drawRect(@NonNull Canvas canvas, int x, int y, int width, int height, int color) { 123 | paint.setColor(color); 124 | 125 | canvas.drawRect(x, y, x + width, y + height, paint); 126 | } 127 | 128 | public static void drawRoundRect(@NonNull Canvas canvas, int x, int y, int width, int height, int radius, int color) { 129 | paint.setColor(color); 130 | 131 | canvas.drawRoundRect(x, y, x + width, y + height, radius, radius, paint); 132 | } 133 | 134 | /** 135 | * Multiples color by the factor (should be below 1.0f) 136 | * 137 | * @param color The color 138 | * @param factor The factor (should be below 1.0f) 139 | * @return The multiplied color 140 | */ 141 | public static int manipulateColor(int color, float factor) { 142 | int a = Color.alpha(color); 143 | 144 | int r = Math.round(Color.red(color) * factor); 145 | int g = Math.round(Color.green(color) * factor); 146 | int b = Math.round(Color.blue(color) * factor); 147 | 148 | return Color.argb(a, 149 | Math.min(r,255), 150 | Math.min(g,255), 151 | Math.min(b,255)); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/src/main/java/com/openblocks/blocks/view/NestedBlock.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.util.Pair; 7 | 8 | import java.util.ArrayList; 9 | 10 | /** 11 | * This class is used to represent a nested block, where it can contain a collection of blocks inside itself 12 | */ 13 | public class NestedBlock extends Block { 14 | 15 | public ArrayList blocks; 16 | 17 | public int block_bottom_height = 50; 18 | public int indent_width = 40; 19 | 20 | public int bottom_margin = 20; 21 | 22 | public NestedBlock(String format, String id, int next_block, ArrayList parameters, int color, ArrayList blocks_inside) { 23 | super(format, id, next_block, parameters, color); 24 | blocks = blocks_inside; 25 | } 26 | 27 | @Override 28 | public int getWidth(Paint text_paint) { 29 | return super.getWidth(text_paint); 30 | } 31 | 32 | /* Difference between getHeight and getBlockHeight is that: 33 | * 34 | * getHeight: Get the entire blocks' height + the block itself height 35 | * getBlockHeight: Get just the block's height 36 | */ 37 | 38 | @Override 39 | public int getHeight(Paint text_paint) { 40 | return getBlockHeight(text_paint) + calculateBlockHeights(text_paint) + bottom_margin + block_bottom_height; 41 | } 42 | 43 | public int getBlockHeight(Paint text_paint) { 44 | return super.getHeight(text_paint); 45 | } 46 | 47 | private int calculateBlockHeights(Paint block_text_paint) { 48 | int sum = 0; 49 | 50 | for (Block block : blocks) { 51 | sum += block.getHeight(block_text_paint); 52 | } 53 | 54 | return sum; 55 | } 56 | 57 | @Override 58 | public Pair onPickup(int x, int y, Paint text_paint) { 59 | int y_start = getBlockHeight(text_paint); 60 | 61 | // TODO: 3/17/21 optimize this 62 | 63 | int index = 0; 64 | for (Block block : blocks) { 65 | 66 | // Check if this pickup has a block under it 67 | if (block.getBounds(0, y_start, text_paint).contains(x - indent_width, y)) { 68 | // Ohk, remove and return the block 69 | blocks.remove(index); 70 | return new Pair<>(true, block); 71 | } 72 | 73 | y_start += block.getHeight(text_paint); 74 | index++; 75 | } 76 | 77 | // Ight, nothing, return our self instead 78 | // TODO: Check each blocks so if the user picked up the white part inside our bounds we don't pickup 79 | return new Pair<>(true, this); 80 | } 81 | 82 | @Override 83 | public void draw(Context context, Canvas canvas, Paint rect_paint, Paint text_paint, int top, int left, int height, int shadow_height, int block_outset_left_margin, int top_block_outset_left_margin, int block_outset_width, int block_outset_height, boolean is_overlapping, int previous_block_color, boolean is_round, int round_radius) { 84 | Paint original_block_paint = new Paint(); 85 | original_block_paint.setColor(rect_paint.getColor()); 86 | original_block_paint.setTextSize(rect_paint.getTextSize()); 87 | 88 | super.draw(context, canvas, rect_paint, text_paint, top, left, getBlockHeight(text_paint), shadow_height, block_outset_left_margin + indent_width, block_outset_left_margin, block_outset_width, block_outset_height, is_overlapping, previous_block_color, true, round_radius); 89 | 90 | int block_outset_left = left + block_outset_left_margin; 91 | 92 | // Draw the childs! (similar to BlocksView) 93 | int childes_previous_block_color = color; 94 | int block_height = getBlockHeight(text_paint); 95 | int previous_top_position = top + shadow_height; 96 | int previous_block_height = block_height - shadow_height; // Because if not, the first block would get overlapped by the event 97 | 98 | for (int i = 0; i < blocks.size(); i++) { 99 | 100 | Block current_block = blocks.get(i); 101 | current_block.default_height = block_height; 102 | 103 | int top_position; 104 | 105 | top_position = previous_top_position + previous_block_height + shadow_height; 106 | 107 | if (is_overlapping) { 108 | // Overlap the previous block's shadow 109 | top_position -= shadow_height; 110 | } 111 | 112 | previous_top_position = top_position; 113 | 114 | previous_block_height = current_block.getHeight(text_paint); 115 | 116 | current_block 117 | .draw( 118 | context, 119 | canvas, 120 | rect_paint, 121 | text_paint, 122 | top_position, 123 | left + indent_width, 124 | shadow_height, 125 | block_outset_left_margin, 126 | block_outset_width, 127 | block_outset_height, 128 | is_overlapping, 129 | childes_previous_block_color, 130 | is_round, 131 | round_radius 132 | ); 133 | 134 | childes_previous_block_color = current_block.color; 135 | } 136 | 137 | int bottom_block_top_position = top + getHeight(text_paint) + shadow_height - block_bottom_height; 138 | 139 | if (is_round) { 140 | DrawHelper.drawRoundRectSimpleOutsideShadow(canvas, left, bottom_block_top_position, getWidth(text_paint), block_bottom_height, shadow_height, round_radius, color); 141 | 142 | // Ok, draw the "indent" 143 | DrawHelper.drawRoundRectSimpleOutsideShadow(canvas, left, top, indent_width, getHeight(text_paint) + shadow_height, shadow_height, round_radius, color); 144 | } else { 145 | DrawHelper.drawRectSimpleOutsideShadow(canvas, left, bottom_block_top_position, getWidth(text_paint), block_bottom_height, shadow_height, color); 146 | 147 | // Ok, draw the "indent" 148 | DrawHelper.drawRectSimpleOutsideShadow(canvas, left, top, indent_width, getHeight(text_paint) + shadow_height, shadow_height, color); 149 | } 150 | 151 | if (!is_overlapping) { 152 | // Ohk no, draw the outset with shadow and the top block's outset 153 | 154 | // Draw the outset 155 | DrawHelper.drawRectSimpleOutsideShadow(canvas, left + block_outset_left_margin, top + getHeight(text_paint), block_outset_width, block_outset_height + shadow_height, shadow_height, color); 156 | 157 | // Draw the top block's outset 158 | DrawHelper.drawRect(canvas, left + top_block_outset_left_margin, top, block_outset_width, block_outset_height, DrawHelper.manipulateColor(previous_block_color, 0.8f)); 159 | } else { 160 | // Yes, just draw the top block's outset 161 | 162 | // Draw the top block's outset 163 | DrawHelper.drawRect(canvas, left + top_block_outset_left_margin, top, block_outset_width, block_outset_height, previous_block_color); 164 | } 165 | 166 | drawParameters(context,canvas, left, top, top + ((getBlockHeight(text_paint) + shadow_height + block_outset_height + text_padding) / 2), getBlockHeight(text_paint), shadow_height, text_paint); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lib/src/main/java/com/openblocks/blocks/view/SketchwareBlocksParser.java: -------------------------------------------------------------------------------- 1 | package com.openblocks.blocks.view; 2 | 3 | import org.json.JSONArray; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | 7 | import java.util.ArrayList; 8 | import java.util.HashMap; 9 | import java.util.Objects; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | /** 14 | * This class is a parser that can be used to parse Sketchware's decrypted logic file 15 | */ 16 | public class SketchwareBlocksParser { 17 | 18 | String logic_data; 19 | 20 | private ArrayList block_id_blacklist = new ArrayList<>(); 21 | // "id": JSONObject 22 | private final HashMap tmp_blocks = new HashMap<>(); 23 | 24 | public SketchwareBlocksParser() { } 25 | 26 | /** 27 | * This constructor will parse the logic_data 28 | * 29 | * @param logic_data The decrypted raw format of data/logic 30 | */ 31 | public SketchwareBlocksParser(String logic_data) { 32 | this.logic_data = logic_data; 33 | } 34 | 35 | /** 36 | * Parses the logic_data string into an array of SketchwareEvents 37 | * @return Parsed data 38 | */ 39 | public ArrayList parse() { 40 | if (logic_data == null) 41 | throw new IllegalStateException("logic_data hasn't been set!"); 42 | 43 | ArrayList events = new ArrayList<>(); 44 | 45 | // We need this so we can evaluate the blocks before we hit the end of the file 46 | logic_data += "\n"; 47 | 48 | String[] lines = logic_data.split("\n"); 49 | 50 | // This boolean is going to skip an entire event until a blank line 51 | boolean skip_event = false; 52 | 53 | String event_name = ""; 54 | String activity_name = ""; 55 | 56 | for (String line: lines) { 57 | 58 | // Check if we're outside of an event 59 | if (line.trim().equals("")) { 60 | skip_event = false; 61 | 62 | // Okay, Let's do the sorting here 63 | BlocksViewEvent event = new BlocksViewEvent(activity_name, event_name); 64 | 65 | // Loop for every blocks 66 | for (String id: tmp_blocks.keySet()) { 67 | 68 | if (block_id_blacklist.contains(Integer.parseInt(id))) 69 | continue; 70 | 71 | JSONObject block = tmp_blocks.get(id); 72 | 73 | try { 74 | event.blocks.add(new Block( 75 | /* Format: */ block.getString("spec"), 76 | /* Block ID: */ id, 77 | /* Next Block ID: */ Integer.parseInt(block.getString("nextBlock")), 78 | /* Parameter: */ parseParameter(block, id), 79 | /* Block color: */ block.getInt("color") 80 | )); 81 | } catch (JSONException e) { 82 | // Weird, probably a project corruption 83 | } 84 | } 85 | 86 | events.add(event); 87 | 88 | block_id_blacklist.clear(); 89 | tmp_blocks.clear(); 90 | 91 | // Reset the event name as we are out of any block collection 92 | event_name = ""; 93 | activity_name = ""; 94 | continue; 95 | 96 | } else { 97 | if (skip_event) { 98 | continue; 99 | } 100 | } 101 | 102 | // If we're aren't in an event name 103 | if (event_name.equals("")) { 104 | String header_regex = "@(\\w+).java_(.+)"; 105 | 106 | Pattern r = Pattern.compile(header_regex); 107 | Matcher m = r.matcher(line); 108 | 109 | if (m.find()) { 110 | if (Objects.equals(m.group(2), "var") || Objects.equals(m.group(2), "func")) { 111 | skip_event = true; 112 | continue; 113 | } 114 | 115 | activity_name = m.group(1); 116 | event_name = m.group(2); 117 | } 118 | 119 | } else { 120 | // Fetch every blocks in this event into tmp_blocks 121 | 122 | try { 123 | JSONObject json = new JSONObject(line); 124 | 125 | tmp_blocks.put(json.getString("id"), json); 126 | } catch (JSONException e) { 127 | e.printStackTrace(); 128 | // Something is wrong 129 | } 130 | } 131 | } 132 | 133 | return events; 134 | } 135 | 136 | private ArrayList parseParameter(JSONObject block, String id) throws JSONException { 137 | ArrayList params = new ArrayList<>(); 138 | 139 | JSONArray params_ = block.getJSONArray("parameters"); 140 | 141 | for (int index = 0; index < params_.length(); index++) { 142 | // Check if this parameter references into another block 143 | String reference_block_regex = "@(\\d+)"; 144 | Pattern r = Pattern.compile(reference_block_regex); 145 | Matcher m = r.matcher(params_.getString(index)); 146 | 147 | if (m.find()) { 148 | // Ah it references into m.group(1) block id 149 | String block_reference = m.group(1); 150 | 151 | // blacklist the id so we don't accidentally parse a return value block 152 | block_id_blacklist.add(Integer.parseInt(block_reference)); 153 | 154 | JSONObject parameter_block = tmp_blocks.get(block_reference); 155 | 156 | params.add( 157 | new BlockField( 158 | new Block( 159 | /* Format: */ block.getString("spec"), 160 | /* Block ID: */ id, 161 | /* Next Block ID: */ Integer.parseInt(block.getString("nextBlock")), 162 | /* Parameter: */ parseParameter(parameter_block, block_reference), 163 | /* Block color: */ block.getInt("color") 164 | ), 165 | // TODO: 3/18/21 This needs a database of blocks, we can't determine a block's return type with the block information 166 | // what if we check the parameter wildcard to determine it? 167 | BlockField.Type.STRING 168 | ) 169 | ); 170 | } else { 171 | params.add( 172 | new BlockField( 173 | (String) params_.get(index) 174 | ) 175 | ); 176 | } 177 | } 178 | 179 | return params; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /lib/src/main/res/drawable/sketchware_block.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/lib/src/main/res/drawable/sketchware_block.9.png -------------------------------------------------------------------------------- /lib/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenBlocksTeam/blocks-view/c408375a97571f546f4e08f304bafabd07b3c6f1/screenshots/1.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "sketchware-blocks-view" 2 | include ':app' 3 | include ':lib' 4 | --------------------------------------------------------------------------------