Google原生的blockly下载地址:https://github.com/google/blockly-android
1.自己创建Brockly工程后,发现运行起来会闪退。最终问题出在所有blockly界面相关的Activity需要在mainfest中定义android:windowSoftInputMode=”stateHidden|adjustPan”
2.所谓的xml转c语言其实是转换成javascript,然后根据generate函数转化的界面显示。
3.自定义block最简单的方法是在block factory 该网址下https://blockly-demo.appspot.com/static/demos/blockfactory/index.html 然后把生成的json放入相关模块的xml文件下,然后将type声明在toolbox中。
4.需要了解自定义block的定义规则。
代码块连接接口定义:
previousStatement 上部接口
nextStatement 下部接口
type:
input_value 右侧接口
input_statement 内部接口
input_dummy 右侧无接口
field_dropdown下拉列表:与 options[[],[]]选择列表连用
field_colour 会显示颜色 伴随 output colour
代码块属性定义:
colour 设置颜色 最大值为360
tooltip 声明代码块的含义
name 可自定义 在toolbox中可写入具体限制和类型
message 显示内容 %1 %2 ……为arg0[]中的内容 以[]包裹
inputsInline 为 true 表示在代码块内部
output 输出值 (colour boolean text)
check 检查类型
5.由于项目需求很多都不符合原生携带的功能,原来的blockly已经被我改的面目全非,具体面向需求的问题如下:
①需求:模块上的填写框需要换成文本框或图片都能显示,图片模块需要设置为res里的drawable文件。field_image就根据src值来判断显示,判断逻辑写在com.google.blockly.android.ui.fieldview.BasicFieldImageView中,相应的BasicFieldInputView继承EditText改为继承Linearlayout,注意改为继承TextView会有bug,点击对应区域会出现很长一段乱码字符。
②整个BlockView的点击事件要写在Dragger处理拖动事件中,就相当于判断ACTION_DOWN和ACTION_UP的时间差,小于指定值触发自定义点击事件。
③设置BlockView颜色,如果按照原生的给出hsv值中的h值去设置颜色会出现颜色较给出颜色偏暗,原因是sv都是默认的值,解决方案:
首先将toolbox中的颜色配置为RGB格式:
然后在依赖包blocklylib-core中修改com.google.blockly.utils.ColorUtils中的parseColor()方法:
/**
* Parses a string as an opaque color, either as a decimal hue (example: {@code 330}) using a
* standard set of saturation and value) or as six digit hex code (example: {@code #BB66FF}).
* If the string cannot be parsed, a {@link ParseException} is thrown.
*
* @param str The input to parse.
* @param tempHsvArray An optional previously allocated array for HSV calculations.
* @return The parsed color, in {@code int} form.
* @throws ParseException
*/
public static int parseColor(@NonNull String str, @Nullable float[] tempHsvArray)
throws ParseException {
Integer result = null;
char firstChar = str.charAt(0);
if (firstChar == '#' && str.length() == 7) {
try {
//result = Integer.parseInt(str.substring(1, 7), 16);
float[] hsv = new float[3];
//如果不来回转换一次,会出现BlockView中其他内容显示不正常。
Color.RGBToHSV(Integer.parseInt(str.substring(1, 3), 16),
Integer.parseInt(str.substring(3, 5), 16),
Integer.parseInt(str.substring(5, 7), 16),hsv);
result = Color.HSVToColor(hsv);
} catch (NumberFormatException e) {
throw new ParseException("Invalid hex color: " + str, 0);
}
return result;
} else if (Character.isDigit(firstChar) && str.length() <= 3) {
try {
int hue = Integer.parseInt(str);
result = getBlockColorForHue(hue, tempHsvArray);
} catch (NumberFormatException e) {
throw new ParseException("Invalid color hue: " + str, 0);
}
}
// Maybe other color formats? 3 digit hex, CSS color functions, etc.
return result;
}
6.新需求:要在slidingmenu里的fragment的实现google blockly的功能,踩了几天的坑,简单总结一下:
①首先写一个基类Fragment将blockly—core库中的AbstractBlocklyActivity中的功能整合进去(这个过程需要把context和activity部分内容重新定义,比较繁琐,但难度不大,这里只贴代码)
package com.tysd.eosthird.base;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.view.GravityCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBar;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import com.google.blockly.android.BlocklyActivityHelper;
import com.google.blockly.android.codegen.CodeGenerationRequest;
import com.google.blockly.android.codegen.LanguageDefinition;
import com.google.blockly.android.control.BlocklyController;
import com.google.blockly.android.ui.BlockViewFactory;
import com.google.blockly.android.ui.MutatorFragment;
import com.google.blockly.model.BlockExtension;
import com.google.blockly.model.BlocklySerializerException;
import com.google.blockly.model.CustomCategory;
import com.google.blockly.model.DefaultBlocks;
import com.google.blockly.model.Mutator;
import com.google.blockly.utils.BlockLoadingException;
import com.tysd.eosthird.R;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
/**
* Created by hxl on 2018/5/24 0024.
*/
public abstract class BaseBlocklyFragment extends BaseFragment {
/**
* Per the design guidelines, you should show the drawer on launch until the user manually
* expands it. This shared preference tracks this.
*/
private static final String PREF_USER_LEARNED_DRAWER = "navigation_drawer_learned";
private static final String TAG = "AbstractBlocklyActivity";
protected BlocklyActivityHelper mBlocklyActivityHelper;
protected ActionBar mActionBar;
protected DrawerLayout mDrawerLayout;
// These two may be null if {@link #onCreateAppNavigationDrawer} returns null.
protected View mNavigationDrawer;
protected ActionBarDrawerToggle mDrawerToggle;
private boolean mUserLearnedDrawer;
//ADD
public void onLoadWorkspaceFromString(String str) {
try{
mBlocklyActivityHelper.loadWorkspaceFromString(str);
}catch (Exception e){
}
}
/**
* 获取字符串
*/
protected String getCodeString() {
return mBlocklyActivityHelper.toCodeString();
}
/**
* Opens or closes the navigation drawer.
* @param open Opens the navigation drawer if true and closes it if false.
*/
public void setNavDrawerOpened(boolean open) {
boolean alreadyOpen = mDrawerLayout.isDrawerOpen(mNavigationDrawer);
if (open != alreadyOpen) {
if (open) {
mDrawerLayout.openDrawer(mNavigationDrawer);
} else {
mDrawerLayout.closeDrawer(mNavigationDrawer);
}
restoreActionBar();
}
}
/**
* Called when the user clicks the save action. Default implementation delegates handling to
* {@link BlocklyActivityHelper#saveWorkspaceToAppDir(String)} using
* {@link #getWorkspaceSavePath()}.
*/
public void onSaveWorkspace() {
mBlocklyActivityHelper.saveWorkspaceToAppDirSafely(getWorkspaceSavePath());
}
/**
* Called when the user clicks the load action. Default implementation delegates handling to
* {@link BlocklyActivityHelper#loadWorkspaceFromAppDir(String)}.
*/
public void onLoadWorkspace() {
mBlocklyActivityHelper.loadWorkspaceFromAppDirSafely(getWorkspaceSavePath());
}
/**
* Called when the user clicks the clear action. Default implementation resets the
* workspace, removing all blocks from the workspace, and then calls
* {@link #onInitBlankWorkspace()}.
*/
public void onClearWorkspace() {
getController().resetWorkspace();
onInitBlankWorkspace();
}
/**
* Saves a snapshot of the workspace to {@code outState}.
*
* @param outState The {@link Bundle} to save to.
*/
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
getController().onSaveSnapshot(outState);
}
/**
* @return The {@link BlocklyController} controlling the workspace in this activity.
*/
public final BlocklyController getController() {
return mBlocklyActivityHelper.getController();
}
/**
* Create a {@link BlocklyActivityHelper} to use for this Activity.
*/
protected BlocklyActivityHelper onCreateActivityHelper() {
return new BlocklyActivityHelper(getBaseActivity(),this);
}
/** Propagate lifecycle event to BlocklyActivityHelper. */
@Override
public void onStart() {
super.onStart();
mBlocklyActivityHelper.onStart();
}
/** Propagate lifecycle event to BlocklyActivityHelper. */
@Override
public void onPause() {
super.onPause();
mBlocklyActivityHelper.onPause();
onAutosave();
}
/** Propagate lifecycle event to BlocklyActivityHelper. */
@Override
public void onResume() {
super.onResume();
mBlocklyActivityHelper.onResume();
if (mNavigationDrawer != null) {
// Read in the flag indicating whether or not the user has demonstrated awareness of the
// drawer. See PREF_USER_LEARNED_DRAWER for details.
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getBaseActivity());
mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false);
if (!mUserLearnedDrawer) {
mDrawerLayout.openDrawer(mNavigationDrawer);
}
}
}
/** Propagate lifecycle event to BlocklyActivityHelper. */
@Override
public void onStop() {
super.onStop();
mBlocklyActivityHelper.onStop();
}
public LayoutInflater mInflater = null;
public View mLayout = null;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (null == mLayout) {
mLayout = getContentView(inflater);
boolean loadedPriorInstance = checkAllowRestoreBlocklyState(savedInstanceState)
&& (getController().onRestoreSnapshot(savedInstanceState) || onAutoload());
if (!loadedPriorInstance) {
onLoadInitialWorkspace();
}
}
if (null != mLayout.getParent()) {
((ViewGroup) mLayout.getParent()).removeView(mLayout);
}
return mLayout;
}
@Override
protected View getContentView(LayoutInflater inflater) {
mLayout = inflater.inflate(com.google.blockly.android.R.layout.drawers_and_action_bar, null);
mInflater =inflater;
onCreateActivityRootView();
mBlocklyActivityHelper = onCreateActivityHelper();
if (mBlocklyActivityHelper == null) {
throw new IllegalStateException("BlocklyActivityHelper is null. "
+ "onCreateActivityHelper must return a instance.");
}
onConfigureTrashIcon();
resetBlockFactory(); // Initial load of block definitions, extensions, and mutators.
configureCategoryFactories(); // After BlockFactory; before Toolbox
reloadToolbox();
addAction();
return mLayout;
}
protected void onConfigureTrashIcon() {
View trashIcon = mLayout.findViewById(R.id.blockly_trash_icon);
if (mBlocklyActivityHelper.getController() != null && trashIcon != null) {
mBlocklyActivityHelper.getController().setTrashIcon(trashIcon);
}
}
public abstract void addAction();
/**
*
* Returns true if the app should proceed to restore the blockly state from the
* {@code savedInstanceState} Bundle or the {@link #onAutoload() auto save} file. By default, it
* always returns true, but Activity developers can override this method to add conditional
* logic.
* <p/>
* This does not prevent the state from saving to a Bundle during {@link #onSaveInstanceState}
* or saving to a file in {@link #onAutosave()}.
*
* @param savedInstanceState The Bundle to restore state from.
* @return True if Blockly state should be restored. Otherwise, null.
*/
protected boolean checkAllowRestoreBlocklyState(Bundle savedInstanceState) {
return true;
}
/**
* Hook for subclasses to load an initial workspace. Default implementation just calls
* {@link #onInitBlankWorkspace()}.
*/
protected void onLoadInitialWorkspace() {
onInitBlankWorkspace();
getController().closeFlyouts();
}
/**
* Called when an autosave of the workspace is triggered, typically by {@link #onPause()}.
* By default this saves the workspace to a file in the app's directory.
*/
protected void onAutosave() {
try {
mBlocklyActivityHelper.saveWorkspaceToAppDir(getWorkspaceAutosavePath());
} catch (FileNotFoundException | BlocklySerializerException e) {
Log.e(TAG, "Failed to autosaving workspace.", e);
}
}
/**
* Called when the activity tries to restore the autosaved workspace, typically by
* {@link #onCreate(Bundle)} if there was no workspace data in the bundle.
*
* @return true if a previously saved workspace was loaded, false otherwise.
*/
protected boolean onAutoload() {
String filePath = getWorkspaceAutosavePath();
try {
mBlocklyActivityHelper.loadWorkspaceFromAppDir(filePath);
return true;
} catch (FileNotFoundException e) {
// No workspace was saved previously.
} catch (BlockLoadingException | IOException e) {
Log.e(TAG, "Failed to load workspace", e);
mBlocklyActivityHelper.getController().resetWorkspace();
File file = getBaseActivity().getFileStreamPath(filePath);
if (!file.delete()) {
Log.e(TAG, "Failed to delete corrupted autoload workspace: " + filePath);
}
}
return false;
}
/**
* Hook for subclasses to initialize a new blank workspace. Initialization may include
* configuring default variables or other setup.
*/
protected void onInitBlankWorkspace() {}
/**
* @return The name to show in the {@link ActionBar}. Defaults to the activity name.
*/
@NonNull
protected CharSequence getWorkspaceTitle() {
return getBaseActivity().getTitle();
}
/**
* @return The asset path for the xml toolbox config.
*/
@NonNull
abstract protected String getToolboxContentsXmlPath();
/**
* @return The asset path for the json block definitions.
*/
@NonNull
abstract protected List<String> getBlockDefinitionsJsonPaths();
/**
* @return The asset path for the core language file used to generate code.
*/
@NonNull
protected LanguageDefinition getBlockGeneratorLanguage() {
return DefaultBlocks.LANGUAGE_DEFINITION;
}
/**
* This method provides a hook to register {@link BlockExtension}s that support the block
* definitions in this activity. By default, it adds all extensions in
* {@link DefaultBlocks#getExtensions() DefaultBlocks} to the block factory, via the
* {@link #onCreateActivityHelper() BlocklyActivityHelper}
* {@link BlocklyActivityHelper#configureExtensions() implementation}.
* <p/>
* Extensions with the same key will replace existing extensions, so it is safe
* to call super and then update specific extensions.
* <p/>
* Called from {@link #resetBlockFactory()}.
*/
protected void configureBlockExtensions() {
mBlocklyActivityHelper.configureExtensions();
}
/**
* This method provides a hook to register {@link Mutator.Factory}s and
* {@link MutatorFragment.Factory}s that support the block definitions in this activity. By
* default, it adds the mutators in {@link DefaultBlocks#getMutators() DefaultBlocks} to the
* BlockFactory, via the {@link #onCreateActivityHelper() BlocklyActivityHelper}
* {@link BlocklyActivityHelper#configureMutators() implementation}.
* <p/>
* Mutators with the same key will replace existing mutators, so it is safe
* to call super and then update specific mutators.
* <p/>
* Called from {@link #resetBlockFactory()}.
*/
protected void configureMutators() {
mBlocklyActivityHelper.configureMutators();
}
/**
* This method provides a hook to register custom {@link CustomCategory}s that support
* the toolboxes in this activity. By default, it registers the categories in
* {@link DefaultBlocks}, via the {@link #onCreateActivityHelper() BlocklyActivityHelper}
* {@link BlocklyActivityHelper#configureMutators() implementation}.
* <p/>
* Category factories with the same {@code custom} key will replace existing
* {@link CustomCategory}s, so it is safe to call super and then update specific categories.
* <p/>
* Called once at activity creation.
*/
protected void configureCategoryFactories() {
mBlocklyActivityHelper.configureCategoryFactories();
}
/**
* Returns the asset file paths to the generators (JS files) to use for the most
* recently requested "Run" action. Called from {@link #onRunCode()}. This is expected to be a
* list of JavaScript files that contain the block generators.
*
* @return The list of file paths to the block generators.
*/
@NonNull
abstract protected List<String> getGeneratorsJsPaths();
/**
* Returns a generation callback to use for the most recently requested "Run" action.
* Called from {@link #onRunCode()}.
*
* @return The generation callback.
*/
@NonNull
abstract protected CodeGenerationRequest.CodeGeneratorCallback getCodeGenerationCallback();
/**
* @return The path to the saved workspace file on the local device. By default,
* "workspace.xml".
*/
@NonNull
protected String getWorkspaceSavePath() {
return "workspace.xml";
}
/**
* @return The path to the automatically saved workspace file on the local device. By default,
* "autosave_workspace.xml".
*/
@NonNull
protected String getWorkspaceAutosavePath() {
return "autosave_workspace.xml";
}
/**
* Creates or loads the root content view (by default, {@link com.google.blockly.android.R.layout#drawers_and_action_bar})
* for the Activity. It is also responsible for assigning {@link #mActionBar} and
* {@link #mDrawerLayout}, and adding the view returned by {@link #onCreateContentView}.
*/
protected void onCreateActivityRootView() {
mDrawerLayout = (DrawerLayout) mLayout.findViewById(com.google.blockly.android.R.id.drawer_layout);
mActionBar = getBaseActivity().getSupportActionBar();
//mActionBar.setDisplayShowTitleEnabled(true);
// Create and attach content view into content container. If content is a fragment, content
// will be null here and the container will be populated during the FragmentTransaction.
View content = onCreateContentView(com.google.blockly.android.R.id.content_container);
if (content != null) {
FrameLayout contentContainer = (FrameLayout) mLayout.findViewById(com.google.blockly.android.R.id.content_container);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
if (content.getParent() != contentContainer) {
contentContainer.addView(content, lp);
} else {
content.setLayoutParams(lp);
}
}
mNavigationDrawer = onCreateAppNavigationDrawer();
if (mNavigationDrawer != null) {
setupAppNaviagtionDrawer();
}
}
/**
* Constructs (or inflates) the primary content view of the Activity.
*
* @param containerId The container id to target if using a {@link Fragment}
* @return The {@link View} constructed. If using a {@link Fragment}, return null.
*/
protected View onCreateContentView(int containerId) {
return mInflater.inflate(com.google.blockly.android.R.layout.blockly_unified_workspace, null);
}
/**
* @return The {@link View} to be used for the navigation menu. Otherwise null.
*/
protected View onCreateAppNavigationDrawer() {
return null;
}
/**
* Configures the activity to support a navigation menu and drawer provided by
* {@link #onCreateAppNavigationDrawer}.
*/
protected void setupAppNaviagtionDrawer() {
DrawerLayout.LayoutParams lp = new DrawerLayout.LayoutParams(
getResources().getDimensionPixelSize(com.google.blockly.android.R.dimen.navigation_drawer_width),
ViewGroup.LayoutParams.MATCH_PARENT,
Gravity.START);
// Add navigation drawer above the content view, as the first drawer.
mDrawerLayout.addView(mNavigationDrawer, 1, lp);
// set a custom shadow that overlays the main content when the drawer opens
mDrawerLayout.setDrawerShadow(com.google.blockly.android.R.drawable.drawer_shadow,
GravityCompat.START);
mActionBar.setDisplayHomeAsUpEnabled(true);
mActionBar.setHomeButtonEnabled(true);
// ActionBarDrawerToggle ties together the the proper interactions
// between the navigation drawer and the action bar app icon.
mDrawerToggle = new ActionBarDrawerToggle(getActivity(), mDrawerLayout,
com.google.blockly.android.R.string.navigation_drawer_open, /* "open drawer" description for accessibility */
com.google.blockly.android.R.string.navigation_drawer_close /* "close drawer" description for accessibility */
) {
@Override
public void onDrawerClosed(View drawerView) {
super.onDrawerClosed(drawerView);
getBaseActivity().supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu()
}
@Override
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
if (!mUserLearnedDrawer) {
// The user manually opened the drawer; store this flag to prevent auto-showing
// the navigation drawer automatically in the future.
mUserLearnedDrawer = true;
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(
getActivity());
sp.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true).apply();
}
getBaseActivity().supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu()
}
};
// Defer code dependent on restoration of previous instance state.
mDrawerLayout.post(new Runnable() {
@Override
public void run() {
mDrawerToggle.syncState();
}
});
mDrawerLayout.addDrawerListener(mDrawerToggle);
}
/**
* Runs the code generator. Called when user selects "Run" action.
* <p/>
* Gets the latest block definitions and generator code by calling
* {@link #getBlockDefinitionsJsonPaths()} and {@link #getGeneratorsJsPaths()} just before
* invoking generation.
*
* @see #getCodeGenerationCallback()
*/
protected void onRunCode() {
mBlocklyActivityHelper.requestCodeGeneration(
getBlockGeneratorLanguage(),
getBlockDefinitionsJsonPaths(),
getGeneratorsJsPaths(),
getCodeGenerationCallback());
}
/**
* Restores the {@link ActionBar} contents when the navigation window closes, per <a
* href="http://developer.android.com/design/material/index.html">Material design
* guidelines</a>.
*/
protected void restoreActionBar() {
ActionBar actionBar = getBaseActivity().getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setTitle(getWorkspaceTitle());
}
}
/**
* Reloads the toolbox contents using the path provided by {@link #getToolboxContentsXmlPath()}.
*/
protected void reloadToolbox() {
mBlocklyActivityHelper.reloadToolbox(getToolboxContentsXmlPath());
}
/**
* Reloads the block definitions, including extensions and mutators. Calls
* {@link #getBlockDefinitionsJsonPaths()} and {@link #configureBlockExtensions()}.
*
* @throws IOException If there is a fundamental problem with the input.
* @throws BlockLoadingException If the definition is malformed.
*/
protected void resetBlockFactory() {
mBlocklyActivityHelper.resetBlockFactory(
getBlockDefinitionsJsonPaths());
configureBlockExtensions();
configureMutators();
configureCategoryFactories();
// Reload the toolbox?
}
/**
* @return True if the navigation menu was closed and the back event should be consumed.
* Otherwise false.
*/
protected boolean onBackToCloseNavMenu() {
if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
mDrawerLayout.closeDrawer(GravityCompat.START);
return true;
}
return false;
}
}
②简单改写后Fragment中实现blockly操作会有两个问题:第一,拖动带有BasicFieldInputView的控件会有放手后先返回初始位置又到放手的位置,这个做安卓相关的EditText拖动都知道,重写EditText的onDragEvent方法 return false即可。第二,拖至垃圾桶无法删除,这是因为BlocklyActivityHelper的onConfigureTrashIcon()方法中:
protected void onConfigureTrashIcon() {
View trashIcon = mActivity.findViewById(R.id.blockly_trash_icon);
if (mController != null && trashIcon != null) {
mController.setTrashIcon(trashIcon);
}
}
trashIcon 为空,无法设置拖动监听事件,上面的fragment基类已解决此问题,将该方法改写到fragment基类中:
protected void onConfigureTrashIcon() {
View trashIcon = mLayout.findViewById(R.id.blockly_trash_icon);
if (mBlocklyActivityHelper.getController() != null && trashIcon != null) {
mBlocklyActivityHelper.getController().setTrashIcon(trashIcon);
}
}