Added basic drag and drop in UI Designer

This commit is contained in:
Akash Yadav
2021-12-14 13:09:02 +05:30
parent 7ae38c8369
commit d628251c51
20 changed files with 453 additions and 123 deletions
+4
View File
@@ -3,9 +3,13 @@
<component name="DesignSurface">
<option name="filePathToZoomLevelMap">
<map>
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable-mdpi/bg_widgets_list.xml" value="0.3105485232067511" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable/bg_design_container_background.xml" value="0.3105485232067511" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable/bg_ripple.xml" value="0.3105485232067511" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable/bg_ripple_pressed.xml" value="0.3105485232067511" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable/bg_widget_drag_placeholder.xml" value="0.33" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable/bg_widget_group_selected.xml" value="0.33" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable/bg_widgets_list.xml" value="0.3105485232067511" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable/ic_replace.xml" value="0.3105485232067511" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable/ic_widget_edittext.xml" value="0.3105485232067511" />
<entry key="..\:/Projects/AndroidStudioProjects/android-ide/app/src/main/res/drawable/ic_widget_seekbar.xml" value="0.3105485232067511" />
@@ -17,7 +17,9 @@
**************************************************************************************/
package com.itsaky.androidide;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
@@ -37,38 +39,43 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.core.content.ContextCompat;
import androidx.core.view.GravityCompat;
import androidx.transition.Fade;
import androidx.transition.Slide;
import androidx.transition.TransitionManager;
import androidx.transition.TransitionSet;
import com.blankj.utilcode.util.ThrowableUtils;
import com.itsaky.androidide.adapters.WidgetGroupItemAdapter;
import com.itsaky.androidide.adapters.WidgetItemAdapter;
import com.itsaky.androidide.app.StudioActivity;
import com.itsaky.androidide.databinding.ActivityDesignerBinding;
import com.itsaky.androidide.layoutinflater.WidgetDragListener;
import com.itsaky.androidide.models.UIWidget;
import com.itsaky.androidide.models.UIWidgetGroup;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.layoutinflater.ILayoutInflater;
import com.itsaky.layoutinflater.IView;
import com.itsaky.toaster.Toaster;
import org.jetbrains.annotations.Contract;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class DesignerActivity extends StudioActivity {
public class DesignerActivity extends StudioActivity implements WidgetItemAdapter.OnDragStartListener {
private ActivityDesignerBinding mBinding;
private UIWidgetGroup selectedGroup;
public static final String KEY_LAYOUT_PATH = "designer_layoutPath";
public static final String DRAGGING_WIDGET_TAG = "DRAGGING_WIDGET";
public static final String DRAGGING_WIDGET_MIME = "application/ide_widget";
private static final Logger LOG = Logger.instance("DesignerActivity");
private final List<UIWidgetGroup> widgetGroups = new ArrayList<>();
private final WidgetItemAdapter.OnWidgetDragListener dragListener = (widget, v, event) -> {
getApp().toast("Dragging: " + widget.getName(), Toaster.Type.INFO);
return true;
};
@Override
protected View bindLayout() {
mBinding = ActivityDesignerBinding.inflate(getLayoutInflater());
@@ -79,21 +86,26 @@ public class DesignerActivity extends StudioActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setSupportActionBar(mBinding.toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
final var toggle = new ActionBarDrawerToggle(this, mBinding.getRoot(), mBinding.toolbar, R.string.app_name, R.string.app_name);
mBinding.getRoot().addDrawerListener(toggle);
toggle.syncState();
final var extras = getIntent().getExtras();
final var path = extras.getString(KEY_LAYOUT_PATH, null);
final var name = path.substring(path.lastIndexOf(File.separator) + 1);
getSupportActionBar().setTitle(name);
setSupportActionBar(mBinding.toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
final Bundle extras = getIntent().getExtras();
final String path = extras.getString(KEY_LAYOUT_PATH, null);
try {
final ILayoutInflater inflater = getApp().getLayoutInflater();
inflater.resetContextProvider(newContextProvider());
final IView view = inflater.inflatePath(path, mBinding.layoutContainer);
mBinding.layoutContainer.addView(view.asView());
mBinding.layoutContainer.setOnDragListener(new WidgetDragListener(this));
} catch (Throwable th) {
mBinding.layoutContainer.removeAllViews();
mBinding.layoutContainer.addView(createErrorText(th));
@@ -120,11 +132,12 @@ public class DesignerActivity extends StudioActivity {
// IMPORTANT: Do not select any other groups here
common.setSelected(true);
this.selectedGroup = common;
widgetGroups.add(common);
final var layouts = new UIWidgetGroup("Layouts");
layouts.addChild(new UIWidget("LinearLayout", R.drawable.ic_widget_linear, LinearLayout.class));
layouts.addChild(new UIWidget("RelativeLayout", R.drawable.ic_widget_relative, RelativeLayout.class));
final var layouts = new UIWidgetGroup(getString(R.string.widget_group_layouts));
layouts.addChild(new UIWidget(getString(R.string.layout_linear), R.drawable.ic_widget_linear, LinearLayout.class));
layouts.addChild(new UIWidget(getString(R.string.layout_relative), R.drawable.ic_widget_relative, RelativeLayout.class));
widgetGroups.add(layouts);
@@ -134,9 +147,22 @@ public class DesignerActivity extends StudioActivity {
showWidgetGroup(common);
}
@SuppressLint("NotifyDataSetChanged")
private void showWidgetGroup (@NonNull UIWidgetGroup group) {
final var children = group.getChildren();
mBinding.widgetItems.setAdapter(new WidgetItemAdapter(children, dragListener));
this.mBinding.widgetItems.setAdapter(new WidgetItemAdapter(children, this));
this.selectedGroup.setSelected(false);
this.selectedGroup = group;
this.selectedGroup.setSelected(true);
TransitionManager.beginDelayedTransition(mBinding.navigation,
new TransitionSet()
.addTransition(new Slide(Gravity.END))
.addTransition(new Fade())
);
//noinspection ConstantConditions
this.mBinding.groupItems.getAdapter().notifyDataSetChanged();
}
@NonNull
@@ -154,4 +180,9 @@ public class DesignerActivity extends StudioActivity {
error.setBackgroundColor(ContextCompat.getColor(this, android.R.color.white));
return error;
}
@Override
public void onDragStarted(View view) {
mBinding.getRoot().closeDrawer(GravityCompat.START);
}
}
@@ -34,6 +34,9 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.view.menu.MenuBuilder;
@@ -118,6 +121,8 @@ import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.DidChangeConfigurationParams;
import org.eclipse.lsp4j.InitializeResult;
import org.eclipse.lsp4j.Position;
import org.jetbrains.annotations.Contract;
import com.itsaky.androidide.lsp.IDELanguageClientImpl;
public class EditorActivity extends StudioActivity implements FileTreeFragment.FileActionListener,
@@ -392,7 +397,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
if (getBuildService() == null) return false;
int id = item.getItemId();
if (id == R.id.menuEditor_runDebug || id == R.id.menuEditor_quickRun) {
getBuildService().assembleDebug(true);
getmBuildServiceHandler().assembleDebug(true);
} else if (id == R.id.menuEditor_runRelease) {
getBuildService().assembleRelease();
} else if (id == R.id.menuEditor_runClean) {
@@ -661,7 +666,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
@Override
public void onDiagnosticClick(File file, Diagnostic diagnostic) {
public void onDiagnosticClick(File file, @NonNull Diagnostic diagnostic) {
openFileAndSelect(file, diagnostic.getRange());
hideDiagnostics();
hideViewOptions();
@@ -686,7 +691,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
* @param initialX Initial X coordinate of view
* @param initialY Initial Y coordinate of view
*/
public void positionViewWithinScreen(final View view, final float initialX, final float initialY) {
public void positionViewWithinScreen(@NonNull final View view, final float initialX, final float initialY) {
view.setX(initialX);
view.setY(initialY);
@@ -727,7 +732,8 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
@Override
public void onTabSelected(TabLayout.Tab tab) {
public void
onTabSelected(@NonNull TabLayout.Tab tab) {
EditorFragment current = mPagerAdapter.getFrag(tab.getPosition());
if(current != null && current.getFile() != null) {
this.mCurrentFragment = current;
@@ -739,7 +745,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {
public void onTabUnselected(@NonNull TabLayout.Tab tab) {
EditorFragment frag = mPagerAdapter.getFrag(tab.getPosition());
if(frag == null) return;
boolean isGradle = frag.isModified() && frag.getFile().getName().endsWith(EditorFragment.EXT_GRADLE);
@@ -755,7 +761,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
@Override
public boolean onNavigationItemSelected(MenuItem p1) {
public boolean onNavigationItemSelected(@NonNull MenuItem p1) {
final int id = p1.getItemId();
if(id == R.id.editornav_discuss) {
getApp().openTelegramGroup();
@@ -980,7 +986,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
return result.gradleSaved;
}
public void install(File apk) {
public void install(@NonNull File apk) {
if(apk.exists()) {
Intent i = IntentUtils.getInstallAppIntent(apk);
if(i != null) {
@@ -1096,6 +1102,8 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
.create();
}
@NonNull
@Contract(" -> new")
private ILayoutInflater.ContextProvider getContextProvider() {
return new ILayoutInflater.ContextProvider() {
@Override
@@ -1105,6 +1113,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
};
}
@NonNull
private Set<File> getResourceDirectories() {
final Set<File> dirs = new HashSet<>();
if (mProject != null && mProject.getModulePaths() != null && !mProject.getModulePaths().isEmpty()) {
@@ -1211,10 +1220,12 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
handleSearchResultVisibility(true);
}
@NonNull
private MaterialContainerTransform createContainerTransformFor(View start, View end) {
return createContainerTransformFor(start, end, mBinding.realContainer);
}
@NonNull
private MaterialContainerTransform createContainerTransformFor(View start, View end, View drawingView) {
return TransformUtils.createContainerTransformFor(start, end, drawingView);
}
@@ -1231,10 +1242,10 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
mFileOptionsHandler.start();
mBuildServiceHandler.start();
getBuildService().assembleDebug(false);
getmBuildServiceHandler().assembleDebug(false);
}
private void removeFromParent(View v) {
private void removeFromParent(@NonNull View v) {
if(v.getParent() != null && v.getParent() instanceof ViewGroup) {
((ViewGroup) v.getParent()).removeView(v);
}
@@ -1253,7 +1264,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
}
}
private void refreshSymbolInput(EditorFragment frag) {
private void refreshSymbolInput(@NonNull EditorFragment frag) {
symbolInput.bindEditor(frag.getEditor());
symbolInput.setSymbols(Symbols.forFile(frag.getFile()));
}
@@ -1263,7 +1274,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
getSyncBanner()
.setNegative(android.R.string.cancel, null)
.setPositive(android.R.string.ok, v -> {
getBuildService().assembleDebug(false);
getmBuildServiceHandler().assembleDebug(false);
})
.show();
}
@@ -1305,6 +1316,7 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
return mFindInProjectDialog == null ? createFindInProjectDialog() : mFindInProjectDialog;
}
@Nullable
private AlertDialog createFindInProjectDialog() {
if(mProject == null
|| mProject.getModulePaths() == null
@@ -1385,6 +1397,10 @@ public class EditorActivity extends StudioActivity implements FileTreeFragment.F
private IDEService getBuildService() {
return mBuildServiceHandler.getService();
}
public BuildServiceHandler getmBuildServiceHandler () {
return mBuildServiceHandler;
}
private void showNeedHelpDialog() {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this, R.style.AppTheme_MaterialAlertDialog);
@@ -70,6 +70,7 @@ public class SimpleIconTextAdapter extends RecyclerView.Adapter<SimpleIconTextAd
} else {
binding.icon.setImageResource(icon);
}
binding.text.setText(item.getText());
if (this.bindListener != null) {
@@ -17,6 +17,13 @@
package com.itsaky.androidide.adapters;
import android.graphics.Color;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.itsaky.androidide.R;
import com.itsaky.androidide.models.IconTextListItem;
import com.itsaky.androidide.models.UIWidgetGroup;
@@ -26,30 +33,32 @@ public class WidgetGroupItemAdapter extends SimpleIconTextAdapter implements Sim
private final OnGroupClickListener clickListener;
public WidgetGroupItemAdapter(List<UIWidgetGroup> groups, OnGroupClickListener clickListener) {
public WidgetGroupItemAdapter(@NonNull List<UIWidgetGroup> groups, @Nullable OnGroupClickListener clickListener) {
super(groups);
this.clickListener = clickListener;
super.setOnBindListener(this);
}
@Override
public boolean onBind(IconTextListItem item, VH holder, int position) {
final var binding = holder.binding;
final var group = (UIWidgetGroup) getItemAt(position);
if (this.clickListener != null) {
binding.getRoot().setOnClickListener(v -> this.clickListener.onGroupClick(group));
}
// Ignored
return false;
}
@Override
public void postBind(IconTextListItem item, VH holder, int position) {
public void postBind(IconTextListItem item, @NonNull VH holder, int position) {
final var binding = holder.binding;
final var group = (UIWidgetGroup) item;
if (group.isSelected()) {
binding.getRoot().setBackground(ContextCompat.getDrawable(binding.getRoot().getContext(), R.drawable.bg_widget_group_selected));
} else {
binding.getRoot().setBackgroundColor(Color.TRANSPARENT);
}
if (this.clickListener != null) {
binding.getRoot().setOnClickListener(v -> this.clickListener.onGroupClick(group));
}
}
@@ -17,11 +17,13 @@
package com.itsaky.androidide.adapters;
import android.view.DragEvent;
import android.content.ClipData;
import android.view.View;
import androidx.annotation.NonNull;
import com.itsaky.androidide.DesignerActivity;
import com.itsaky.androidide.layoutinflater.WidgetDragShadowBuilder;
import com.itsaky.androidide.models.IconTextListItem;
import com.itsaky.androidide.models.UIWidget;
@@ -29,30 +31,43 @@ import java.util.List;
public class WidgetItemAdapter extends SimpleIconTextAdapter implements SimpleIconTextAdapter.OnBindListener {
private final OnWidgetDragListener dragListener;
private final OnDragStartListener dragStartListener;
public WidgetItemAdapter(List<UIWidget> widgets, OnWidgetDragListener dragListener) {
public WidgetItemAdapter(List<UIWidget> widgets, OnDragStartListener dragStartListener) {
super(widgets);
this.dragListener = dragListener;
this.dragStartListener = dragStartListener;
setOnBindListener(this);
}
@Override
public boolean onBind(IconTextListItem item, @NonNull VH holder, int position) {
final var binding = holder.binding;
final var widget = (UIWidget) getItemAt(position);
if (this.dragListener != null) {
binding.getRoot().setOnDragListener((v, event) -> {
return this.dragListener.onDrag(widget, v, event);
});
}
return false; // Returning true will not set the text and icon
}
public static interface OnWidgetDragListener {
boolean onDrag(UIWidget widget, View v, DragEvent event);
@Override
public void postBind(IconTextListItem item, VH holder, int position) {
final var binding = holder.binding;
final var root = binding.getRoot();
final var widget = (UIWidget) getItemAt(position);
root.setOnLongClickListener(v -> {
final var shadow = new WidgetDragShadowBuilder(binding.icon);
final var dataItem = new ClipData.Item(DesignerActivity.DRAGGING_WIDGET_TAG);
final var data = new ClipData(DesignerActivity.DRAGGING_WIDGET_TAG,
new String[]{DesignerActivity.DRAGGING_WIDGET_MIME},
dataItem);
binding.icon.startDragAndDrop(data, shadow, widget, 0);
if (this.dragStartListener != null) {
this.dragStartListener.onDragStarted(v);
}
return true;
});
}
public interface OnDragStartListener {
void onDragStarted (View view);
}
}
@@ -20,6 +20,7 @@ package com.itsaky.androidide.app;
import com.google.firebase.messaging.FirebaseMessaging;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.language.xml.completion.XMLCompletionService;
import com.itsaky.androidide.layoutinflater.IDELayoutInflater;
import com.itsaky.androidide.services.MessagingService;
import com.itsaky.layoutinflater.ILayoutInflater;
import com.itsaky.layoutinflater.LayoutInflaterConfiguration;
@@ -73,7 +74,7 @@ public class StudioApp extends BaseApplication {
}
public void createInflater (LayoutInflaterConfiguration config) {
this.mLayoutInflater = ILayoutInflater.newInstance(config);
this.mLayoutInflater = new IDELayoutInflater(config);
}
public ILayoutInflater getLayoutInflater () {
@@ -40,7 +40,8 @@ import java.util.stream.Collectors;
public class BuildServiceHandler extends IDEHandler implements BuildListener {
private IDEService service;
private boolean shouldInstallApk = false;
public BuildServiceHandler(Provider provider) {
super(provider);
}
@@ -51,15 +52,18 @@ public class BuildServiceHandler extends IDEHandler implements BuildListener {
@Override
public void start() {
if(this.activity() == null)
if(this.activity() == null) {
throwNPE();
}
AndroidProject project = provider.provideAndroidProject();
if(project == null)
if(project == null) {
throwNPE();
}
if(project.getProjectPath() == null)
if(project.getProjectPath() == null) {
throwNPE();
}
this.service = new IDEService(new File(project.getProjectPath()));
this.service.setListener(this);
@@ -116,11 +120,12 @@ public class BuildServiceHandler extends IDEHandler implements BuildListener {
public void onBuildSuccessful(GradleTask task, String msg) {
appendOutput(task, msg);
if(task == null || activity() == null) return;
if(task.canOutput())
if(task.canOutput()) {
activity().setStatus(msg);
}
// If this task generates APK, ask user if he/she wants to install them
if (task instanceof ApkGeneratingTask) {
if (shouldInstallApk && task instanceof ApkGeneratingTask) {
final IDEProject project = activity().provideIDEProject();
final Optional<IDEModule> app = project.getModuleByPath(":app"); // TODO Handle multiple application modules
if(app.isPresent()) {
@@ -175,6 +180,15 @@ public class BuildServiceHandler extends IDEHandler implements BuildListener {
}
activity().getBinding().buildProgressIndicator.setVisibility(View.VISIBLE);
}
public void setShouldInstallApk (boolean install) {
this.shouldInstallApk = install;
}
public void assembleDebug (boolean shouldInstallApk) {
setShouldInstallApk(shouldInstallApk);
getService().assembleDebug(shouldInstallApk);
}
private boolean isClasspathValid(String path) {
if(path == null || path.trim().length() <= 0 || path.trim().equals("/")) return false;
@@ -0,0 +1,53 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.layoutinflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import com.itsaky.layoutinflater.LayoutInflaterConfiguration;
import com.itsaky.layoutinflater.XMLLayoutInflater;
import java.lang.reflect.InvocationTargetException;
/**
* A layout inflater for inflating views from XML.
* This implementation handles IDE-specific stuff. Like, setting drag listeners to view groups.
*
* @author Akash Yadav
*/
public class IDELayoutInflater extends XMLLayoutInflater {
public IDELayoutInflater(LayoutInflaterConfiguration config) {
super(config);
}
@NonNull
@Override
protected View createAndroidViewForName(String name) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
final var view = super.createAndroidViewForName(name);
if (view instanceof ViewGroup) {
final var group = (ViewGroup) view;
group.setOnDragListener(new WidgetDragListener(group.getContext()));
}
return view;
}
}
@@ -0,0 +1,82 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.layoutinflater;
import android.content.Context;
import android.graphics.drawable.GradientDrawable;
import android.view.DragEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.SizeUtils;
import com.itsaky.androidide.DesignerActivity;
import com.itsaky.androidide.R;
import com.itsaky.toaster.Toaster;
public class WidgetDragListener implements View.OnDragListener {
private final View placeholder;
private final int PLACEHOLDER_HEIGHT = 20; // in dp
private final int PLACEHOLDER_WIDTH = 40; // in dp
public WidgetDragListener(@NonNull Context context) {
this.placeholder = new View(context);
this.placeholder.setBackgroundResource(R.drawable.bg_widget_drag_placeholder);
this.placeholder.setLayoutParams(new ViewGroup.LayoutParams(SizeUtils.dp2px(PLACEHOLDER_WIDTH), SizeUtils.dp2px(PLACEHOLDER_HEIGHT)));
}
@Override
public boolean onDrag(View v, @NonNull DragEvent event) {
final var group = (ViewGroup) v; // Assuming that this drag listener is always set to ViewGroup(s)
final var action = event.getAction();
final var clipDesc = event.getClipDescription();
switch (action) {
case DragEvent.ACTION_DRAG_STARTED:
if (clipDesc.hasMimeType(DesignerActivity.DRAGGING_WIDGET_MIME)) {
return true;
}
break;
case DragEvent.ACTION_DRAG_ENTERED:
removePlaceholderFromParent();
group.addView(this.placeholder);
break;
case DragEvent.ACTION_DRAG_EXITED:
removePlaceholderFromParent();
break;
case DragEvent.ACTION_DROP:
removePlaceholderFromParent();
Toast.makeText(group.getContext(), "Widget dropped", Toast.LENGTH_SHORT).show();
break;
}
return false;
}
private void removePlaceholderFromParent () {
if (this.placeholder.getParent() != null) {
((ViewGroup) this.placeholder.getParent()).removeView((this.placeholder));
}
}
}
@@ -0,0 +1,42 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.layoutinflater;
import android.graphics.Point;
import android.view.View;
public class WidgetDragShadowBuilder extends View.DragShadowBuilder {
private final double TOUCH_Y_OFFSET = 0.3;
public WidgetDragShadowBuilder (View view) {
super(view);
}
@Override
public void onProvideShadowMetrics(Point outShadowSize, Point outShadowTouchPoint) {
if (getView() != null) {
final var width = getView().getWidth();
final var height = getView().getHeight();
outShadowSize.set(width, height);
outShadowTouchPoint.set(width / 2, height + (int) (height * TOUCH_Y_OFFSET));
} else {
super.onProvideShadowMetrics(outShadowSize, outShadowTouchPoint);
}
}
}
@@ -50,6 +50,7 @@ import org.eclipse.lsp4j.Diagnostic;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
// TODO Request Language server for syntax highlighting instead of waiting for it to send highlight ranges
public class JavaLexerImpl extends io.github.rosemoe.editor.langs.AbstractCodeAnalyzer {
private final EditorLanguage language;
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke android:width="1dp"
android:color="@android:color/darker_gray"
android:dashGap="3dp"
android:dashWidth="6dp"/>
</shape>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="@color/primaryLightColor"
/>
<corners
android:radius="5dp"
/>
</shape>
@@ -1,32 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="@android:color/black"
/>
<corners
android:bottomLeftRadius="10dp"
android:topLeftRadius="10dp"
android:topRightRadius="0dp"
android:bottomRightRadius="0dp"/>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="@color/bg_widget_selected"
/>
<corners
android:bottomLeftRadius="10dp"
android:topLeftRadius="10dp"
android:topRightRadius="0dp"
android:bottomRightRadius="0dp"/>
</shape>
+20 -21
View File
@@ -5,8 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
tools:context=".DesignerActivity"
tools:openDrawer="start">
tools:context=".DesignerActivity">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/main_container"
@@ -35,13 +34,18 @@
android:layout_height="match_parent"
android:fillViewport="true"
android:contentDescription="@string/label_ui_designer_content"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="32dp"
android:paddingTop="32dp"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<LinearLayout
android:id="@+id/layout_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" />
android:orientation="vertical"
android:background="@drawable/bg_design_container_background"/>
</androidx.core.widget.NestedScrollView>
@@ -58,10 +62,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingTop="16dp"
android:paddingEnd="8dp"
android:paddingBottom="16dp"
android:weightSum="2.0">
<androidx.recyclerview.widget.RecyclerView
@@ -69,28 +69,27 @@
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.7"
android:paddingStart="8dp"
android:paddingTop="16dp"
android:paddingEnd="0dp"
android:paddingBottom="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/layout_simple_icon_text"
tools:itemCount="1"/>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginStart="1dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="4dp"
android:layout_marginBottom="8dp"
android:background="@color/primaryLightColor" />
tools:itemCount="1"
tools:listitem="@layout/layout_simple_icon_text" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/widget_items"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1.3"
android:background="@color/bg_widget_selected"
android:paddingStart="0dp"
android:paddingTop="16dp"
android:paddingEnd="8dp"
android:paddingBottom="16dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/layout_simple_icon_text"
tools:itemCount="3" />
tools:itemCount="3"
tools:listitem="@layout/layout_simple_icon_text" />
</LinearLayout>
@@ -19,13 +19,13 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_ripple"
android:clickable="true"
android:descendantFocusability="blocksDescendants"
android:focusable="true"
android:orientation="horizontal"
android:paddingTop="2dp"
android:paddingBottom="2dp"
android:paddingStart="4dp"
android:minHeight="35dp">
<ImageView
+1
View File
@@ -29,6 +29,7 @@
<color name="diagnostic_warning">@color/log_warning</color>
<color name="content_background">#212121</color>
<color name="bg_green">#4CAF50</color>
<color name="bg_widget_selected">#212121</color>
<color name="transform_scrim">#70000000</color>
<color name="textActionNameColor">@android:color/tertiary_text_light</color>
+3
View File
@@ -303,4 +303,7 @@
<string name="widget_radio_button">RadioButton</string>
<string name="widget_seekbar">SeekBar</string>
<string name="widget_spinner">Spinner</string>
<string name="widget_group_layouts">Layouts</string>
<string name="layout_linear">LinearLayout</string>
<string name="layout_relative">RelativeLayout</string>
</resources>
@@ -28,10 +28,6 @@ import com.itsaky.androidide.app.BaseApplication;
import com.itsaky.androidide.ui.util.Preconditions;
import com.itsaky.androidide.utils.Logger;
import com.itsaky.attrinfo.AttrInfo;
import com.itsaky.layoutinflater.IAttribute;
import com.itsaky.layoutinflater.IAttributeAdapter;
import com.itsaky.layoutinflater.IView;
import com.itsaky.layoutinflater.IViewGroup;
import com.itsaky.layoutinflater.adapters.android.view.ViewAttrAdapter;
import com.itsaky.layoutinflater.adapters.android.view.ViewGroupAttrAdapter;
import com.itsaky.layoutinflater.impl.BaseView;
@@ -54,7 +50,7 @@ import org.jsoup.parser.Parser;
import static com.itsaky.androidide.ui.util.Preconditions.*;
class XMLLayoutInflater extends BaseLayoutInflater {
public class XMLLayoutInflater extends BaseLayoutInflater {
private final Set<File> resDirs;
private final AttrInfo attrInfo;
@@ -67,10 +63,10 @@ class XMLLayoutInflater extends BaseLayoutInflater {
public static final String ATTR_ADAPTER_SUFFIX = "AttrAdapter";
private XMLLayoutInflater () {
throw new UnsupportedOperationException("Cannot instantiate inflater");
throw new UnsupportedOperationException();
}
XMLLayoutInflater(LayoutInflaterConfiguration config) {
public XMLLayoutInflater(LayoutInflaterConfiguration config) {
this.resDirs = config.resDirs;
this.attrInfo = config.attrInfo;
this.widgetInfo = config.widgetInfo;
@@ -263,9 +259,7 @@ class XMLLayoutInflater extends BaseLayoutInflater {
// TODO Try to load classes directly from .class files
try {
final Class<? extends View> loaded = Class.forName(name).asSubclass(View.class);
final Constructor<? extends View> constructor = loaded.getConstructor(Context.class /*, AttributeSet.class, int.class*/);
final View created = constructor.newInstance(contextProvider.getContext()/*, null, style*/);
final View created = createAndroidViewForName(name);
final BaseView view =
created instanceof ViewGroup
? new UiViewGroup (name, (ViewGroup) created)
@@ -276,6 +270,14 @@ class XMLLayoutInflater extends BaseLayoutInflater {
}
}
@NonNull
protected View createAndroidViewForName(String name) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
final Class<? extends View> loaded = Class.forName(name).asSubclass(View.class);
final Constructor<? extends View> constructor = loaded.getConstructor(Context.class /*, AttributeSet.class, int.class*/);
final View created = constructor.newInstance(contextProvider.getContext()/*, null, style*/);
return created;
}
protected IView applyLayoutParams(IView view, ViewGroup parent) {
final ViewGroup.LayoutParams params = generateLayoutParams(parent);
view.asView().setLayoutParams(params);