diff --git a/appinventor/components-ios/src/ListView.swift b/appinventor/components-ios/src/ListView.swift index 7e235bebb8..e86fd6d15e 100644 --- a/appinventor/components-ios/src/ListView.swift +++ b/appinventor/components-ios/src/ListView.swift @@ -147,6 +147,15 @@ open class ListView: ViewComponent, AbstractMethodsForViewComponent, _view.reloadData() } } + + // This property is not supported in iOS + @objc open var BounceEdgeEffect: Bool { + get { + return false; + } + set(addEffect) { + } + } //ListData @objc open var ListData: String { @@ -346,6 +355,14 @@ open class ListView: ViewComponent, AbstractMethodsForViewComponent, // MARK: Methods + @objc open func AddItem(_ mainText: String, _ detailText: String, _ imageName: String) { + _listData.append(["Text1": mainText, "Text2": detailText, "Image": imageName]) + } + + @objc open func AddItemAtIndex(_ addIndex: Int32, _ mainText: String, _ detailText: String, _ imageName: String) { + _listData.insert(["Text1": mainText, "Text2": detailText, "Image": imageName], at: Int(addIndex - 1)) + } + @objc open func CreateElement(_ mainText: String, _ detailText: String, _ imageName: String) -> YailDictionary { return [ "Text1": mainText, diff --git a/appinventor/components/src/com/google/appinventor/components/common/LayoutType.java b/appinventor/components/src/com/google/appinventor/components/common/LayoutType.java new file mode 100644 index 0000000000..9c2424629f --- /dev/null +++ b/appinventor/components/src/com/google/appinventor/components/common/LayoutType.java @@ -0,0 +1,43 @@ +// -*- mode: java; c-basic-offset: 2; -*- +// Copyright 2023 MIT, All rights reserved +// Released under the Apache License, Version 2.0 +// http://www.apache.org/licenses/LICENSE-2.0 + +package com.google.appinventor.components.common; + +import java.util.HashMap; +import java.util.Map; + +/** + * Defines a parameter of ListViewLayout used by the ListView component. + */ +public enum LayoutType implements OptionList { + @Default + MainText(0), + MainText_DetailText_Vertical(1), + MainText_DetailText_Horizontal(2), + Image_MainText(3), + Image_MainText_DetailText_Vertical(4); + + private final int layout; + + LayoutType(int value) { + this.layout = value; + } + + public Integer toUnderlyingValue() { + return layout; + } + + private static final Map lookup = new HashMap<>(); + + static { + for (LayoutType value : LayoutType.values()) { + lookup.put(value.toUnderlyingValue(), value); + } + } + + public static LayoutType fromUnderlyingValue(Integer value) { + return lookup.get(value); + } +} diff --git a/appinventor/components/src/com/google/appinventor/components/common/ListOrientation.java b/appinventor/components/src/com/google/appinventor/components/common/ListOrientation.java new file mode 100644 index 0000000000..d7ef79cf78 --- /dev/null +++ b/appinventor/components/src/com/google/appinventor/components/common/ListOrientation.java @@ -0,0 +1,44 @@ +// -*- mode: java; c-basic-offset: 2; -*- +// Copyright 2024 MIT, All rights reserved +// Released under the Apache License, Version 2.0 +// http://www.apache.org/licenses/LICENSE-2.0 + +package com.google.appinventor.components.common; + +import java.util.HashMap; +import java.util.Map; + +/** + * Defines a parameter of ListOrientation used by the ListView component. + */ +public enum ListOrientation implements OptionList { + @Default + Vertical(0), + Horizontal(1); + + private final int orientation; + + ListOrientation(int value) { + this.orientation = value; + } + + public Integer toUnderlyingValue() { + return orientation; + } + + private static final Map lookup = new HashMap<>(); + + static { + for(ListOrientation value : ListOrientation.values()) { + lookup.put(value.toUnderlyingValue(), value); + } + } + + public static ListOrientation fromUnderlyingValue(Integer value) { + return lookup.get(value); + } + + public static ListOrientation fromUnderlyingValue(String value) { + return fromUnderlyingValue(Integer.parseInt(value)); + } +} diff --git a/appinventor/components/src/com/google/appinventor/components/runtime/ListAdapterWithRecyclerView.java b/appinventor/components/src/com/google/appinventor/components/runtime/ListAdapterWithRecyclerView.java index 08fd2f83a9..b5e425613e 100644 --- a/appinventor/components/src/com/google/appinventor/components/runtime/ListAdapterWithRecyclerView.java +++ b/appinventor/components/src/com/google/appinventor/components/runtime/ListAdapterWithRecyclerView.java @@ -1,5 +1,5 @@ // -*- mode: java; c-basic-offset: 2; -*- -// Copyright 2021 MIT, All rights reserved +// Copyright 2024 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 @@ -12,6 +12,7 @@ import android.view.View; import android.view.ViewGroup; +import android.view.Gravity; import android.widget.Filter; import android.widget.Filterable; @@ -38,48 +39,63 @@ import java.util.List; public class ListAdapterWithRecyclerView - extends RecyclerView.Adapter implements Filterable { + extends RecyclerView.Adapter implements Filterable { private static final String LOG_TAG = "ListAdapterRecyclerView"; private ClickListener clickListener; - public Boolean[] selection; - public Boolean[] isVisible; private int textMainColor; private float textMainSize; private int textDetailColor; private float textDetailSize; private String textMainFont; private String textDetailFont; - private int layoutType; private int backgroundColor; private int selectionColor; private int imageHeight; private int imageWidth; - private CardView[] itemViews; - private boolean multiSelect; - private List items; - private List filterItems; + private float radius; + private List items = new ArrayList<>(); + private List oryginalItems = new ArrayList<>(); + private List oryginalPositions = new ArrayList<>(); protected final ComponentContainer container; + private int idFirst = -1; + private int idSecond = -1; + private int idImages = -1; + private int idCard = -1; + private List selectedItems = new ArrayList<>(); + protected final Filter filter = new Filter() { @Override protected FilterResults performFiltering(CharSequence charSequence) { String filterQuery = charSequence.toString().toLowerCase(); FilterResults results = new FilterResults(); - List filteredList = new ArrayList<>(); - + List filteredList = new ArrayList<>(); + oryginalPositions = new ArrayList<>(); if (filterQuery == null || filterQuery.length() == 0) { - filteredList = new ArrayList<>(items); + filteredList = new ArrayList<>(oryginalItems); + items = new ArrayList<>(oryginalItems); } else { - for (YailDictionary itemDict : items) { - Object o = itemDict.get(Component.LISTVIEW_KEY_DESCRIPTION); - String filterString = itemDict.get(Component.LISTVIEW_KEY_MAIN_TEXT).toString(); - if (o != null) { - filterString += " " + o.toString().toLowerCase(); + for (int index = 0; index < oryginalItems.size(); index++) { + Object item = oryginalItems.get(index); + String filterString; + if (item instanceof YailDictionary) { + if (((YailDictionary) item).containsKey(Component.LISTVIEW_KEY_MAIN_TEXT)) { + Object o = ((YailDictionary) item).get(Component.LISTVIEW_KEY_DESCRIPTION); + filterString = ((YailDictionary) item).get(Component.LISTVIEW_KEY_MAIN_TEXT).toString(); + if (o != null) { + filterString += " " + o.toString(); + } + } else { + filterString = item.toString(); + } + } else { + filterString = item.toString(); } if (filterString.toLowerCase().contains(filterQuery)) { - filteredList.add(itemDict); + filteredList.add(item); + oryginalPositions.add(index); } } } @@ -90,154 +106,63 @@ protected FilterResults performFiltering(CharSequence charSequence) { @Override protected void publishResults(CharSequence charSequence, FilterResults filterResults) { - filterItems = (List) filterResults.values; - // Usually GUI objects take up no screen space when set to invisible, but setting a CardView object to invisible - // was displaying an empty object. Therefore, set the height to 0 as well. - // Setting visibility on individual entries will keep the selected index(ices) the same regardless of filter. - for (int i = 0; i < items.size(); ++i) { - if (filterItems.size() > 0 && filterItems.contains(items.get(i))) { - isVisible[i] = true; - if (itemViews[i] != null) { - itemViews[i].setVisibility(View.VISIBLE); - itemViews[i].getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; - } - } else { - isVisible[i] = false; - if (itemViews[i] != null) { - itemViews[i].setVisibility(View.GONE); - itemViews[i].getLayoutParams().height = 0; - } - } - } + items = new ArrayList<>((List) filterResults.values); + clearSelections(); + // We store the original data in the originalItems variable + // We store the original item indexes in the originalPositions variable + // We have eliminated hiding/showing CardView to improve performance } }; - public boolean isSelected = false; - - private int idFirst = -1; - private int idSecond = -1; - private int idImages = -1; - private int idCard = 1; - - public ListAdapterWithRecyclerView(ComponentContainer container, List items, int textMainColor, int textDetailColor, float textMainSize, float textDetailSize, String textMainFont, String textDetailFont, int layoutType, int backgroundColor, int selectionColor, int imageWidth, int imageHeight, boolean multiSelect) { - - this.items = items; + public ListAdapterWithRecyclerView(ComponentContainer container, List data, int layoutType, int textMainColor, int textDetailColor, float textMainSize, float textDetailSize, String textMainFont, String textDetailFont, int backgroundColor, int selectionColor, int imageWidth, int imageHeight, int radius) { this.container = container; - this.textMainSize = textMainSize; + this.layoutType = layoutType; this.textMainColor = textMainColor; this.textDetailColor = textDetailColor; + this.textMainSize = textMainSize; this.textDetailSize = textDetailSize; this.textMainFont = textMainFont; this.textDetailFont = textDetailFont; - this.layoutType = layoutType; this.backgroundColor = backgroundColor; this.selectionColor = selectionColor; - this.imageHeight = imageHeight; this.imageWidth = imageWidth; - this.itemViews = new CardView[items.size()]; - this.multiSelect = multiSelect; - - this.selection = new Boolean[items.size()]; - Arrays.fill(selection, Boolean.FALSE); - this.isVisible = new Boolean[items.size()]; - Arrays.fill(isVisible, Boolean.TRUE); - } - - public ListAdapterWithRecyclerView(ComponentContainer container, List stringItems, int textMainColor, float textMainSize, String textMainFont, int backgroundColor, int selectionColor) { - // Legacy Support - this.container = container; - this.textMainSize = textMainSize; - this.textMainColor = textMainColor; - this.textMainFont = textMainFont; - this.textDetailColor = textMainColor; - this.textDetailSize = 0; - this.textDetailFont = Component.TYPEFACE_DEFAULT; - this.layoutType = Component.LISTVIEW_LAYOUT_SINGLE_TEXT; - this.backgroundColor = backgroundColor; - this.selectionColor = selectionColor; - this.imageHeight = 0; - this.imageWidth = 0; - this.multiSelect = false; - this.itemViews = new CardView[stringItems.size()]; - this.selection = new Boolean[stringItems.size()]; - Arrays.fill(selection, Boolean.FALSE); - this.isVisible = new Boolean[stringItems.size()]; - Arrays.fill(isVisible, Boolean.TRUE); - - // Build the list of strings into a list of dictionaries - this.items = new ArrayList<>(); - // YailList is 1-indexed - for(String itemString : stringItems) { - YailDictionary itemDict = new YailDictionary(); - itemDict.put(Component.LISTVIEW_KEY_MAIN_TEXT, itemString); - this.items.add(itemDict); - } - } - - public void clearSelections() { - Arrays.fill(selection, Boolean.FALSE); - for (int i = 0; i < itemViews.length; i++) { - itemViews[i].setBackgroundColor(backgroundColor); - } + this.imageHeight = imageHeight; + this.radius = (float) radius; + updateData(data); } - public void toggleSelection(int pos) { - // With single select, clicked item becomes the only selected item - // Using 0-indexed array. - Arrays.fill(selection, Boolean.FALSE); - for (int i = 0; i < itemViews.length; i++) { - // Views are created when they are displayed, so this list may not be fully populated. - if (itemViews[i] != null) { - itemViews[i].setBackgroundColor(backgroundColor); - } - } - if (pos >= 0) { - selection[pos] = true; - if (itemViews[pos] != null) { - itemViews[pos].setBackgroundColor(selectionColor); - } + public void updateData(List newItems) { + this.oryginalItems = newItems; + if (oryginalPositions.isEmpty()) { + this.items = new ArrayList<>(newItems); } - } - - public void changeSelections(int pos) { - // With multi select, clicking an item toggles its selection status on and off - selection[pos] = !selection[pos]; - if (selection[pos]) { - itemViews[pos].setBackgroundColor(selectionColor); - } else { - itemViews[pos].setBackgroundColor(backgroundColor); - } - } - - public boolean hasVisibleItems() { - return Arrays.asList(isVisible).contains(true); + clearSelections(); } @Override public RvViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) { CardView cardView = new CardView(container.$context()); - cardView.setUseCompatPadding(true); - cardView.setContentPadding(10, 10, 10, 10); - cardView.setPreventCornerOverlap(true); - cardView.setCardElevation(2.1f); - cardView.setRadius(0); + cardView.setContentPadding(15, 15, 15, 15); + cardView.setPreventCornerOverlap(false); cardView.setMaxCardElevation(3f); - cardView.setBackgroundColor(backgroundColor); - cardView.setClickable(isSelected); + cardView.setCardBackgroundColor(backgroundColor); + cardView.setRadius(radius); + cardView.setCardElevation(2.1f); + ViewCompat.setElevation(cardView, 20); + + cardView.setClickable(true); idCard = ViewCompat.generateViewId(); cardView.setId(idCard); CardView.LayoutParams params1 = new CardView.LayoutParams(CardView.LayoutParams.FILL_PARENT, CardView.LayoutParams.WRAP_CONTENT); params1.setMargins(0, 0, 0, 0); - ViewCompat.setElevation(cardView, 20); - // All layouts have a textview containing MainText TextView textViewFirst = new TextView(container.$context()); idFirst = ViewCompat.generateViewId(); textViewFirst.setId(idFirst); LinearLayout.LayoutParams layoutParams1 = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); - layoutParams1.topMargin = 10; + // layoutParams1.topMargin = 10; textViewFirst.setLayoutParams(layoutParams1); textViewFirst.setTextSize(textMainSize); textViewFirst.setTextColor(textMainColor); @@ -254,6 +179,7 @@ public RvViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) { imageView.setId(idImages); LinearLayout.LayoutParams layoutParamsImage = new LinearLayout.LayoutParams(imageWidth, imageHeight); imageView.setLayoutParams(layoutParamsImage); + linearLayout1.setGravity(Gravity.CENTER_VERTICAL); linearLayout1.addView(imageView); } @@ -270,7 +196,6 @@ public RvViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) { TextViewUtil.setFontTypeface(container.$form(), textViewSecond, textDetailFont, false, false); textViewSecond.setTextColor(textDetailColor); if (layoutType == Component.LISTVIEW_LAYOUT_TWO_TEXT || layoutType == Component.LISTVIEW_LAYOUT_IMAGE_TWO_TEXT) { - layoutParams2.topMargin = 10; textViewSecond.setLayoutParams(layoutParams2); LinearLayout linearLayout2 = new LinearLayout(container.$context()); @@ -284,7 +209,7 @@ public RvViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) { } else if (layoutType == Component.LISTVIEW_LAYOUT_TWO_TEXT_LINEAR) { // Unlike the other two text layouts, linear does not wrap - layoutParams2.setMargins(50, 10, 0, 0); + layoutParams2.setMargins(50, 0, 0, 0); textViewSecond.setLayoutParams(layoutParams2); textViewSecond.setMaxLines(1); textViewSecond.setEllipsize(null); @@ -301,20 +226,26 @@ public RvViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) { @Override public void onBindViewHolder(final RvViewHolder holder, int position) { - - holder.cardView.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - holder.onClick(v); + Object o = items.get(position); + YailDictionary dictItem = new YailDictionary(); + if (o instanceof YailDictionary) { + if (((YailDictionary) o).containsKey(Component.LISTVIEW_KEY_MAIN_TEXT)) { + dictItem = (YailDictionary) o; + } else { + dictItem.put(Component.LISTVIEW_KEY_MAIN_TEXT, o.toString()); } - }); - - YailDictionary dictItem = items.get(position); + } else { + dictItem.put(Component.LISTVIEW_KEY_MAIN_TEXT, o.toString()); + } String first = dictItem.get(Component.LISTVIEW_KEY_MAIN_TEXT).toString(); String second = ""; if (dictItem.containsKey(Component.LISTVIEW_KEY_DESCRIPTION)) { second = dictItem.get(Component.LISTVIEW_KEY_DESCRIPTION).toString(); } + String imageName = ""; + if (dictItem.containsKey(Component.LISTVIEW_KEY_IMAGE)) { + imageName = dictItem.get(Component.LISTVIEW_KEY_IMAGE).toString(); + } if (layoutType == Component.LISTVIEW_LAYOUT_SINGLE_TEXT) { holder.textViewFirst.setText(first); } else if (layoutType == Component.LISTVIEW_LAYOUT_TWO_TEXT) { @@ -324,7 +255,6 @@ public void onClick(View v) { holder.textViewFirst.setText(first); holder.textViewSecond.setText(second); } else if (layoutType == Component.LISTVIEW_LAYOUT_IMAGE_SINGLE_TEXT) { - String imageName = dictItem.get(Component.LISTVIEW_KEY_IMAGE).toString(); Drawable drawable = new BitmapDrawable(); try { drawable = MediaUtil.getBitmapDrawable(container.$form(), imageName); @@ -334,7 +264,6 @@ public void onClick(View v) { holder.textViewFirst.setText(first); ViewUtil.setImage(holder.imageVieww, drawable); } else if (layoutType == Component.LISTVIEW_LAYOUT_IMAGE_TWO_TEXT) { - String imageName = dictItem.get(Component.LISTVIEW_KEY_IMAGE).toString(); Drawable drawable = new BitmapDrawable(); try { drawable = MediaUtil.getBitmapDrawable(container.$form(), imageName); @@ -347,26 +276,49 @@ public void onClick(View v) { } else { Log.e(LOG_TAG, "onBindViewHolder Layout not recognized: " + layoutType); } - if (selection[position]) { - holder.cardView.setBackgroundColor(selectionColor); - } else { - holder.cardView.setBackgroundColor(backgroundColor); - } - if (!isVisible[position]) - { - holder.cardView.setVisibility(View.GONE); - holder.cardView.getLayoutParams().height = 0; + if (selectedItems.contains(position)) { + holder.cardView.setCardBackgroundColor(selectionColor); } else { - holder.cardView.setVisibility(View.VISIBLE); - holder.cardView.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + holder.cardView.setCardBackgroundColor(backgroundColor); } - itemViews[position] = holder.cardView; } - @Override public int getItemCount() { - return (itemViews.length); + return (items.size()); + } + + public void toggleSelection(int position) { + if(!oryginalPositions.isEmpty()) { + position = oryginalPositions.indexOf(position); + } + if (selectedItems.contains(position)) { + return; + } + if (!selectedItems.isEmpty()) { + int oldPosition = selectedItems.get(0); + selectedItems.clear(); + notifyItemChanged(oldPosition); + } + selectedItems.add(position); + notifyItemChanged(position); +} + + public void changeSelections(int position) { + if(!oryginalPositions.isEmpty()) { + position = oryginalPositions.indexOf(position); + } + if (selectedItems.contains(position)) { + selectedItems.remove(Integer.valueOf(position)); + } else { + selectedItems.add(position); + } + notifyItemChanged(position); + } + + public void clearSelections() { + selectedItems.clear(); + notifyDataSetChanged(); } class RvViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { @@ -378,7 +330,7 @@ class RvViewHolder extends RecyclerView.ViewHolder implements View.OnClickListen public RvViewHolder(View view) { super(view); - + view.setOnClickListener(this); cardView = view.findViewById(idCard); @@ -387,22 +339,19 @@ public RvViewHolder(View view) { if (idSecond != -1) { textViewSecond = view.findViewById(idSecond); } - if (idImages != -1) { imageVieww = view.findViewById(idImages); } } - @Override public void onClick(View v) { int position = getAdapterPosition(); - if (multiSelect) { - changeSelections(position); - } else { - toggleSelection(position); + + if (!oryginalPositions.isEmpty()) { + position = oryginalPositions.get(position); } clickListener.onItemClick(position, v); - } + } } public void setOnItemClickListener(ClickListener clickListener) { @@ -413,20 +362,6 @@ public interface ClickListener { void onItemClick(int position, View v); } - public String getSelectedItems() { - StringBuilder sb = new StringBuilder(); - String sep = ""; - for (int i = 0; i < selection.length; ++i) { - if (selection[i]) { - YailDictionary dictItem = items.get(i); - sb.append(sep); - sb.append(dictItem.get(Component.LISTVIEW_KEY_MAIN_TEXT).toString()); - sep = ","; - } - } - return sb.toString(); - } - @Override public Filter getFilter() { return filter; diff --git a/appinventor/components/src/com/google/appinventor/components/runtime/ListBounceEdgeEffectFactory.java b/appinventor/components/src/com/google/appinventor/components/runtime/ListBounceEdgeEffectFactory.java new file mode 100644 index 0000000000..539cdf7162 --- /dev/null +++ b/appinventor/components/src/com/google/appinventor/components/runtime/ListBounceEdgeEffectFactory.java @@ -0,0 +1,118 @@ +// -*- mode: java; c-basic-offset: 2; -*- +// Copyright 2024 MIT, All rights reserved +// Released under the Apache License, Version 2.0 +// http://www.apache.org/licenses/LICENSE-2.0 + +package com.google.appinventor.components.runtime; + +import android.graphics.Canvas; +import android.widget.EdgeEffect; +import androidx.dynamicanimation.animation.SpringAnimation; +import androidx.dynamicanimation.animation.SpringForce; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.LinearLayoutManager; + +public class ListBounceEdgeEffectFactory extends RecyclerView.EdgeEffectFactory { + + private static final float OVERSCROLL_TRANSLATION_MAGNITUDE = 0.2f; + private static final float FLING_TRANSLATION_MAGNITUDE = 0.5f; + + @Override + public EdgeEffect createEdgeEffect(RecyclerView recyclerView, int direction) { + LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); + boolean isHorizontal = (layoutManager != null && layoutManager.getOrientation() == LinearLayoutManager.HORIZONTAL); + return new BounceEdgeEffect(recyclerView, direction, isHorizontal); + } + + private static class BounceEdgeEffect extends EdgeEffect { + private SpringAnimation translationAnim; + private final RecyclerView recyclerView; + private final int direction; + private final boolean isHorizontal; + + public BounceEdgeEffect(RecyclerView recyclerView, int direction, boolean isHorizontal) { + super(recyclerView.getContext()); + this.recyclerView = recyclerView; + this.direction = direction; + this.isHorizontal = isHorizontal; + } + + @Override + public void onPull(float deltaDistance) { + super.onPull(deltaDistance); + handlePull(deltaDistance); + } + + @Override + public void onPull(float deltaDistance, float displacement) { + super.onPull(deltaDistance, displacement); + handlePull(deltaDistance); + } + + private void handlePull(float deltaDistance) { + int sign = (direction == DIRECTION_BOTTOM || (isHorizontal && direction == DIRECTION_RIGHT)) ? -1 : 1; + float translationDelta = sign * recyclerView.getWidth() * deltaDistance * OVERSCROLL_TRANSLATION_MAGNITUDE; + translateRecyclerView(translationDelta); + if (translationAnim != null) { + translationAnim.cancel(); + } + } + + @Override + public void onRelease() { + super.onRelease(); + if (getTranslation() != 0f) { + translationAnim = createAnim(); + if (translationAnim != null) { + translationAnim.start(); + } + } + } + + @Override + public void onAbsorb(int velocity) { + super.onAbsorb(velocity); + int sign = (direction == DIRECTION_BOTTOM || (isHorizontal && direction == DIRECTION_RIGHT)) ? -1 : 1; + float translationVelocity = sign * velocity * FLING_TRANSLATION_MAGNITUDE; + if (translationAnim != null) { + translationAnim.cancel(); + } + translationAnim = createAnim(); + if (translationAnim != null) { + translationAnim.setStartVelocity(translationVelocity); + translationAnim.start(); + } + } + + @Override + public boolean draw(Canvas canvas) { + return false; + } + + @Override + public boolean isFinished() { + return (translationAnim == null || !translationAnim.isRunning()); + } + + private SpringAnimation createAnim() { + return new SpringAnimation(recyclerView, (isHorizontal ? SpringAnimation.TRANSLATION_X : SpringAnimation.TRANSLATION_Y)) + .setSpring(new SpringForce() + .setFinalPosition(0f) + .setDampingRatio(SpringForce.DAMPING_RATIO_MEDIUM_BOUNCY) + .setStiffness(SpringForce.STIFFNESS_LOW) + ); + } + + private float getTranslation() { + return isHorizontal ? recyclerView.getTranslationX() : recyclerView.getTranslationY(); + } + + private void translateRecyclerView(float translationDelta) { + if (isHorizontal) { + recyclerView.setTranslationX(getTranslation() + translationDelta); + } else { + recyclerView.setTranslationY(getTranslation() + translationDelta); + } + } + } +} diff --git a/appinventor/components/src/com/google/appinventor/components/runtime/ListView.java b/appinventor/components/src/com/google/appinventor/components/runtime/ListView.java index 7e18c99d69..300e1a9417 100644 --- a/appinventor/components/src/com/google/appinventor/components/runtime/ListView.java +++ b/appinventor/components/src/com/google/appinventor/components/runtime/ListView.java @@ -1,6 +1,6 @@ // -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved -// Copyright 2011-2019 MIT, All rights reserved +// Copyright 2011-2024 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 @@ -10,7 +10,6 @@ import android.text.TextWatcher; import android.util.Log; import android.view.View; -import android.widget.AdapterView; import android.widget.EditText; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.LinearLayoutManager; @@ -26,21 +25,28 @@ import com.google.appinventor.components.annotations.UsesLibraries; import com.google.appinventor.components.annotations.UsesPermissions; import com.google.appinventor.components.annotations.SimpleFunction; +import com.google.appinventor.components.annotations.Options; import com.google.appinventor.components.common.ComponentCategory; import com.google.appinventor.components.common.ComponentConstants; import com.google.appinventor.components.common.PropertyTypeConstants; import com.google.appinventor.components.common.YaVersion; +import com.google.appinventor.components.common.LayoutType; +import com.google.appinventor.components.common.ListOrientation; import com.google.appinventor.components.runtime.util.ElementsUtil; import com.google.appinventor.components.runtime.util.ErrorMessages; import com.google.appinventor.components.runtime.util.YailList; +import com.google.appinventor.components.runtime.util.YailDictionary; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import com.google.appinventor.components.runtime.util.YailDictionary; import java.util.ArrayList; import java.util.List; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; + /** * This is a visible component that displays a list of text and image elements in your {@link Form} to * display. Simple lists of strings may be set using the {@link #ElementsFromString(String)} property. @@ -60,17 +66,17 @@ @DesignerComponent(version = YaVersion.LISTVIEW_COMPONENT_VERSION, description = "

This is a visible component that displays a list of text and image elements.

" + - "

Simple lists of strings may be set using the ElementsFromString property." + - " More complex lists of elements containing multiple strings and/or images can be created using " + - "the ListData and ListViewLayout properties.

", + "

Simple lists of strings may be set using the ElementsFromString property." + + " More complex lists of elements containing multiple strings and/or images can be created" + + " using the ListData and ListViewLayout properties.

", category = ComponentCategory.USERINTERFACE, nonVisible = false, iconName = "images/listView.png") @SimpleObject -@UsesLibraries(libraries ="recyclerview.jar, cardview.jar, cardview.aar") +@UsesLibraries(libraries ="recyclerview.jar, cardview.jar, cardview.aar, dynamicanimation.jar") @UsesPermissions(permissionNames = "android.permission.INTERNET," + - "android.permission.READ_EXTERNAL_STORAGE") -public final class ListView extends AndroidViewComponent implements AdapterView.OnItemClickListener { + "android.permission.READ_EXTERNAL_STORAGE") +public final class ListView extends AndroidViewComponent { private static final String LOG_TAG = "ListView"; @@ -80,11 +86,11 @@ public final class ListView extends AndroidViewComponent implements AdapterView. private RecyclerView recyclerView; private ListAdapterWithRecyclerView listAdapterWithRecyclerView; - private List stringItems; - private List dictItems; + private LinearLayoutManager layoutManager; + private List items; private int selectionIndex; private String selection; - private String selectionDetailText; + private String selectionDetailText = "Uninitialized"; private boolean showFilter = false; private static final boolean DEFAULT_ENABLED = false; private int orientation; @@ -92,6 +98,8 @@ public final class ListView extends AndroidViewComponent implements AdapterView. private int backgroundColor; private static final int DEFAULT_BACKGROUND_COLOR = Component.COLOR_BLACK; + private int elementColor; + private int textColor; private int detailTextColor; @@ -113,6 +121,22 @@ public final class ListView extends AndroidViewComponent implements AdapterView. private int layout; private String propertyValue; // JSON string representing data entered through the Designer + private boolean multiSelect; + private boolean divider; + private Paint dividerPaint; + private int dividerColor; + private int dividerSize; + private static final int DEFAULT_DIVIDER_SIZE = 0; + private boolean first = true; //flag for first element margins + private int margins; + private static final int DEFAULT_RADIUS = 0; + private int radius; + private RecyclerView.EdgeEffectFactory edgeEffectFactory; + private ListBounceEdgeEffectFactory bounceEdgeEffectFactory; + private boolean bounceEffect; + private final LinearLayout listLayout; + private static final int DEFAULT_MARGINS_SIZE = 0; + /** * Creates a new ListView component. * @@ -122,8 +146,7 @@ public ListView(ComponentContainer container) { super(container); this.container = container; - stringItems = new ArrayList<>(); - dictItems = new ArrayList<>(); + items = new ArrayList<>(); linearLayout = new LinearLayout(container.$context()); linearLayout.setOrientation(LinearLayout.VERTICAL); @@ -134,35 +157,40 @@ public ListView(ComponentContainer container) { LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); recyclerView.setLayoutParams(params); + layoutManager = new LinearLayoutManager(container.$context(), LinearLayoutManager.VERTICAL, false); + recyclerView.setLayoutManager(layoutManager); + + listLayout = new LinearLayout(container.$context()); + LinearLayout.LayoutParams paramsList = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT); + listLayout.setLayoutParams(paramsList); + listLayout.setOrientation(LinearLayout.VERTICAL); + + dividerColor = Component.COLOR_WHITE; + dividerSize = DEFAULT_DIVIDER_SIZE; + margins = DEFAULT_DIVIDER_SIZE; + + edgeEffectFactory = recyclerView.getEdgeEffectFactory(); + bounceEdgeEffectFactory = new ListBounceEdgeEffectFactory(); + txtSearchBox = new EditText(container.$context()); txtSearchBox.setSingleLine(true); txtSearchBox.setWidth(Component.LENGTH_FILL_PARENT); txtSearchBox.setPadding(10, 10, 10, 10); - txtSearchBox.setHint("Search list..."); if (!AppInventorCompatActivity.isClassicMode()) { txtSearchBox.setBackgroundColor(COLOR_WHITE); } - if (container.$form().isDarkTheme()) { + if (container.$form().isDarkTheme()) { txtSearchBox.setTextColor(COLOR_BLACK); txtSearchBox.setHintTextColor(COLOR_BLACK); } - //set up the listener + //set up the listener txtSearchBox.addTextChangedListener(new TextWatcher() { - @Override public void onTextChanged(CharSequence cs, int arg1, int arg2, int arg3) { // When user changed the Text - if (cs.length() > 0) { - if (!listAdapterWithRecyclerView.hasVisibleItems()) { - setAdapterData(); - } - listAdapterWithRecyclerView.getFilter().filter(cs); - recyclerView.setAdapter(listAdapterWithRecyclerView); - } else { - setAdapterData(); - } + listAdapterWithRecyclerView.getFilter().filter(cs); } @Override @@ -186,11 +214,14 @@ public void afterTextChanged(Editable arg0) { // note that the TextColor and ElementsFromString setters // need to have the textColor set first, since they reset the // adapter - + ElementColor(Component.COLOR_BLACK); BackgroundColor(Component.COLOR_BLACK); SelectionColor(Component.COLOR_LTGRAY); TextColor(Component.COLOR_WHITE); TextColorDetail(Component.COLOR_WHITE); + DividerColor(Component.COLOR_WHITE); + DividerThickness(DEFAULT_DIVIDER_SIZE); + ElementMarginsWidth(DEFAULT_MARGINS_SIZE); FontSize(22.0f); // This was the original size of ListView text. FontSizeDetail(Component.FONT_DEFAULT_SIZE); FontTypeface(Component.TYPEFACE_DEFAULT); @@ -198,17 +229,22 @@ public void afterTextChanged(Editable arg0) { // initially assuming that the image is of square shape ImageWidth(DEFAULT_IMAGE_WIDTH); ImageHeight(DEFAULT_IMAGE_WIDTH); + ElementCornerRadius(DEFAULT_RADIUS); + MultiSelect(false); + BounceEdgeEffect(false); ElementsFromString(""); ListData(""); + listLayout.addView(recyclerView); linearLayout.addView(txtSearchBox); - linearLayout.addView(recyclerView); + linearLayout.addView(listLayout); linearLayout.requestLayout(); container.$add(this); Width(Component.LENGTH_FILL_PARENT); ListViewLayout(ComponentConstants.LISTVIEW_LAYOUT_SINGLE_TEXT); // initialize selectionIndex which also sets selection SelectionIndex(0); + setDivider(); } @Override @@ -223,7 +259,7 @@ public View getView() { */ @Override @SimpleProperty(description = "Determines the height of the list on the view.", - category = PropertyCategory.APPEARANCE) + category = PropertyCategory.APPEARANCE) public void Height(int height) { if (height == LENGTH_PREFERRED) { height = LENGTH_FILL_PARENT; @@ -238,7 +274,7 @@ public void Height(int height) { */ @Override @SimpleProperty(description = "Determines the width of the list on the view.", - category = PropertyCategory.APPEARANCE) + category = PropertyCategory.APPEARANCE) public void Width(int width) { if (width == LENGTH_PREFERRED) { width = LENGTH_FILL_PARENT; @@ -246,6 +282,19 @@ public void Width(int width) { super.Width(width); } + /** + * Returns true or false depending on the visibility of the Filter bar element + * + * @return true or false (visibility) + * @suppressdoc + */ + @SimpleProperty(description = "List filter bar, allows to search the list for relevant items. " + + "True will display the bar, Falseness will hide it.", + category = PropertyCategory.BEHAVIOR) + public boolean ShowFilterBar() { + return showFilter; + } + /** * Sets visibility of the filter bar. `true`{:.logic.block} will show the bar, * `false`{:.logic.block} will hide it. @@ -253,9 +302,8 @@ public void Width(int width) { * @param showFilter set the visibility according to this input */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, - defaultValue = DEFAULT_ENABLED ? "True" : "False") - @SimpleProperty(description = "Sets visibility of ShowFilterBar. True will show the bar, " + - "False will hide it.") + defaultValue = DEFAULT_ENABLED ? "True" : "False") + @SimpleProperty public void ShowFilterBar(boolean showFilter) { this.showFilter = showFilter; if (showFilter) { @@ -266,66 +314,29 @@ public void ShowFilterBar(boolean showFilter) { } /** - * Returns true or false depending on the visibility of the Filter bar element + * Elements property getter method * - * @return true or false (visibility) + * @return a YailList representing the list of strings to be picked from * @suppressdoc */ - @SimpleProperty(category = PropertyCategory.BEHAVIOR, - description = "Returns current state of ShowFilterBar for visibility.") - public boolean ShowFilterBar() { - return showFilter; + @SimpleProperty(description = "List of elements to show in the ListView. Depending " + + "on the ListView, this may be a list of strings or a list of 3-element sub-lists " + + "containing Text, Description, and Image file name.", + category = PropertyCategory.BEHAVIOR) + public List Elements() { + return items; } /** * Specifies the list of choices to display. * - * @param itemsList a YailList containing the strings to be added to the ListView - */ - @SimpleProperty(description = "List of elements to show in the ListView. Depending on the ListView, this may be a list of strings or a list of 3-element sub-lists containing Text, Description, and Image file name.", - category = PropertyCategory.BEHAVIOR) - public void Elements(YailList itemsList) { - dictItems.clear(); - stringItems = new ArrayList<>(); - if (itemsList.size() > 0) { - Object firstitem = itemsList.getObject(0); - // Check to see if this is a list of strings (backward compatibility) or a list of Dictionaries - if (firstitem instanceof YailDictionary) { - // To preserve backward compatibility with the old single-string ListView, we check to be sure we - // have dictionary elements. If first element is a dictionary, treat all as such. - for (int i = 0; i < itemsList.size(); i++) { - Object o = itemsList.getObject(i); - if (o instanceof YailDictionary) { - YailDictionary yailItem = (YailDictionary) o; - dictItems.add(i, yailItem); - } else { - // Support strings mixed in with the Dictionary elements because somebody will end up doing this. - YailDictionary yailItem = new YailDictionary(); - yailItem.put(Component.LISTVIEW_KEY_MAIN_TEXT, YailList.YailListElementToString(o)); - dictItems.add(i, yailItem); - } - } - } else { - // Support legacy single-string ListViews - stringItems = ElementsUtil.elementsStrings(itemsList, "ListView"); - } - } - setAdapterData(); - } - - /** - * Elements property getter method - * - * @return a YailList representing the list of strings to be picked from - * @suppressdoc + * @param itemsList a List containing the strings to be added to the ListView */ - @SimpleProperty(category = PropertyCategory.BEHAVIOR) - public YailList Elements() { - if (dictItems.size() > 0) { - return YailList.makeList(dictItems); - } else { - return ElementsUtil.makeYailListFromList(stringItems); - } + @SimpleProperty + public void Elements(List itemsList) { + items = new ArrayList<>(itemsList); + updateAdapterData(); + listAdapterWithRecyclerView.notifyDataSetChanged(); } /** @@ -335,45 +346,14 @@ public YailList Elements() { * @param itemstring a string containing a comma-separated list of the strings to be picked from */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_TEXTAREA, defaultValue = "") - @SimpleProperty(description = "The TextView elements specified as a string with the " + - "stringItems separated by commas " + - "such as: Cheese,Fruit,Bacon,Radish. Each word before the comma will be an element in the " + - "list.", category = PropertyCategory.BEHAVIOR) + @SimpleProperty(description = "The TextView elements specified as a string with the " + + "stringItems separated by commas such as: Cheese,Fruit,Bacon,Radish. Each word " + + "before the comma will be an element in the list.", + category = PropertyCategory.BEHAVIOR) public void ElementsFromString(String itemstring) { - stringItems = ElementsUtil.elementsListFromString(itemstring); - setAdapterData(); - } - - /** - * Sets the stringItems of the ListView through an adapter - */ - public void setAdapterData() { - LinearLayoutManager layoutManager; - if (!dictItems.isEmpty()) { - // if the data is available in AddData property - listAdapterWithRecyclerView = new ListAdapterWithRecyclerView(container, dictItems, textColor, detailTextColor, fontSizeMain, fontSizeDetail, fontTypeface, fontTypeDetail, layout, backgroundColor, selectionColor, imageWidth, imageHeight, false); - - if (orientation == ComponentConstants.LAYOUT_ORIENTATION_HORIZONTAL) { - layoutManager = new LinearLayoutManager(container.$context(), LinearLayoutManager.HORIZONTAL, false); - } else { // if (orientation == ComponentConstants.LAYOUT_ORIENTATION_VERTICAL) { - layoutManager = new LinearLayoutManager(container.$context(), LinearLayoutManager.VERTICAL, false); - } - } else { - // Legacy Support: if the data is not available in AddData property but is available in ElementsFromString property - listAdapterWithRecyclerView = new ListAdapterWithRecyclerView(container, stringItems, textColor, fontSizeMain, fontTypeface, backgroundColor, selectionColor); - - layoutManager = new LinearLayoutManager(container.$context(), LinearLayoutManager.VERTICAL, false); - } - listAdapterWithRecyclerView.setOnItemClickListener(new ListAdapterWithRecyclerView.ClickListener() { - @Override - public void onItemClick(int position, View v) { - listAdapterWithRecyclerView.toggleSelection(position); - SelectionIndex(position + 1); - AfterPicking(); - } - }); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setAdapter(listAdapterWithRecyclerView); + items = new ArrayList(ElementsUtil.elementsListFromString(itemstring)); + updateAdapterData(); + listAdapterWithRecyclerView.notifyDataSetChanged(); } /** @@ -382,78 +362,53 @@ public void onItemClick(int position, View v) { * number of items in the `ListView`, `SelectionIndex` will be set to `0`, and * {@link #Selection(String)} will be set to the empty text. */ - @SimpleProperty( - description = "The index of the currently selected item, starting at " + - "1. If no item is selected, the value will be 0. If an attempt is " + - "made to set this to a number less than 1 or greater than the number " + - "of stringItems in the ListView, SelectionIndex will be set to 0, and " + - "Selection will be set to the empty text.", - category = PropertyCategory.BEHAVIOR) + @SimpleProperty(description = "The index of the currently selected item, starting at 1. " + + "If no item is selected, the value will be 0. If an attempt is made to set this " + + "to a number less than 1 or greater than the number of stringItems in the ListView, " + + "SelectionIndex will be set to 0, and Selection will be set to the empty text.", + category = PropertyCategory.BEHAVIOR) public int SelectionIndex() { return selectionIndex; } - /** * Sets the index to the passed argument for selection * * @param index the index to be selected * @suppressdoc */ - @SimpleProperty(description = "Specifies the one-indexed position of the selected item in the " + - "ListView. This could be used to retrieve" + - "the text at the chosen position. If an attempt is made to set this to a " + - "number less than 1 or greater than the number of stringItems in the ListView, SelectionIndex " + - "will be set to 0, and Selection will be set to the empty text." - , - category = PropertyCategory.BEHAVIOR) + @SimpleProperty public void SelectionIndex(int index) { - if (!dictItems.isEmpty()) { - selectionIndex = ElementsUtil.selectionIndex(index, YailList.makeList(dictItems)); - if (selectionIndex > 0) { - selection = dictItems.get(selectionIndex - 1).get(Component.LISTVIEW_KEY_MAIN_TEXT).toString(); - selectionDetailText = ElementsUtil.toStringEmptyIfNull(dictItems.get(selectionIndex - 1).get(Component.LISTVIEW_KEY_DESCRIPTION).toString()); + selectionIndex = index; + if (index > 0 && index <= items.size()) { + Object o = items.get(index - 1); + if (o instanceof YailDictionary) { + if (((YailDictionary) o).containsKey(Component.LISTVIEW_KEY_MAIN_TEXT)) { + selection = ((YailDictionary) o).get(Component.LISTVIEW_KEY_MAIN_TEXT).toString(); + selectionDetailText = ElementsUtil.toStringEmptyIfNull(((YailDictionary) o) + .get(Component.LISTVIEW_KEY_DESCRIPTION)); + } else { + selection = o.toString(); + } } else { - selection = ""; - selectionDetailText = ""; + selection = o.toString(); + } + if (multiSelect) { + listAdapterWithRecyclerView.changeSelections(selectionIndex - 1); + } else { + listAdapterWithRecyclerView.toggleSelection(selectionIndex - 1); } } else { - selectionIndex = ElementsUtil.selectionIndexInStringList(index, stringItems); - // Now, we need to change Selection to correspond to SelectionIndex. - selection = ElementsUtil.setSelectionFromIndexInStringList(index, stringItems); - selectionDetailText = ""; - } - if (listAdapterWithRecyclerView != null) { - listAdapterWithRecyclerView.toggleSelection(selectionIndex - 1); - } - } - - /** - * Removes Item from list at a given index - */ - @SimpleFunction( - description = "Removes Item from list at a given index") - public void RemoveItemAtIndex(int index) { - if (index < 1 || index > Math.max(dictItems.size(), stringItems.size())) { - container.$form().dispatchErrorOccurredEvent(this, "RemoveItemAtIndex", - ErrorMessages.ERROR_LISTVIEW_INDEX_OUT_OF_BOUNDS, index); - return; - } - if (dictItems.size() >= index) { - dictItems.remove(index - 1); + selection = ""; + listAdapterWithRecyclerView.clearSelections(); } - if (stringItems.size() >= index) { - stringItems.remove(index - 1); - } - setAdapterData(); } /** * Returns the text in the `ListView` at the position of {@link #SelectionIndex(int)}. */ - @SimpleProperty(description = "Returns the text last selected in the ListView.", - category = PropertyCategory - .BEHAVIOR) + @SimpleProperty(description = "The text value of the most recently selected item in the ListView.", + category = PropertyCategory.BEHAVIOR) public String Selection() { return selection; } @@ -464,62 +419,63 @@ public String Selection() { * @suppressdoc */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, - defaultValue = "") + defaultValue = "") @SimpleProperty public void Selection(String value) { selection = value; // Now, we need to change SelectionIndex to correspond to Selection. - if (!dictItems.isEmpty()) { - for (int i = 0; i < dictItems.size(); ++i) { - YailDictionary item = dictItems.get(i); - if (item.get(Component.LISTVIEW_KEY_MAIN_TEXT).toString() == value) { - selectionIndex = i + 1; - selectionDetailText = ElementsUtil.toStringEmptyIfNull(item.get("Text2")); - break; + if (!items.isEmpty()) { + for (int i = 0; i < items.size(); ++i) { + Object item = items.get(i); + if (item instanceof YailDictionary) { + if (((YailDictionary) item).containsKey(Component.LISTVIEW_KEY_MAIN_TEXT)) { + if (((YailDictionary) item).get(Component.LISTVIEW_KEY_MAIN_TEXT).toString() == value) { + selectionIndex = i + 1; + selectionDetailText = ElementsUtil.toStringEmptyIfNull(((YailDictionary) item) + .get(Component.LISTVIEW_KEY_DESCRIPTION)); + break; + } + // Not found + selectionIndex = 0; + selectionDetailText = "Not Found"; + } else { + if (item.toString().equals(value)) { + selectionIndex = i + 1; + break; + } + selectionIndex = 0; + } + } else { + if (item.toString().equals(value)) { + selectionIndex = i + 1; + break; + } + selectionIndex = 0; } - // Not found - selectionIndex = 0; } - } else { - selectionIndex = ElementsUtil.setSelectedIndexFromValueInStringList(value, stringItems); + SelectionIndex(selectionIndex); } - SelectionIndex(selectionIndex); } /** * Returns the Secondary or Detail text in the ListView at the position set by SelectionIndex */ @SimpleProperty(description = "Returns the secondary text of the selected row in the ListView.", - category = PropertyCategory.BEHAVIOR) + category = PropertyCategory.BEHAVIOR) public String SelectionDetailText() { return selectionDetailText; } - /** - * Simple event to raise when the component is clicked. Implementation of - * AdapterView.OnItemClickListener - */ - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - YailDictionary item = (YailDictionary) parent.getAdapter().getItem(position); - this.selection = ElementsUtil.toStringEmptyIfNull(item.get(Component.LISTVIEW_KEY_MAIN_TEXT).toString()); - this.selectionDetailText = ElementsUtil.toStringEmptyIfNull(item.get("Text2")); - this.selectionIndex = position + 1; - AfterPicking(); - } - /** * Simple event to be raised after the an element has been chosen in the list. * The selected element is available in the {@link #Selection(String)} property. */ - @SimpleEvent(description = "Simple event to be raised after the an element has been chosen in the" + - " list. The selected element is available in the Selection property.") + @SimpleEvent(description = "Simple event to be raised after the an element has been chosen in the" + + " list. The selected element is available in the Selection property.") public void AfterPicking() { EventDispatcher.dispatchEvent(this, "AfterPicking"); } - - /** * Returns the listview's background color as an alpha-red-green-blue * integer, i.e., {@code 0xAARRGGBB}. An alpha of {@code 00} @@ -528,9 +484,8 @@ public void AfterPicking() { * @return background color in the format 0xAARRGGBB, which includes * alpha, red, green, and blue components */ - @SimpleProperty( - description = "The color of the listview background.", - category = PropertyCategory.APPEARANCE) + @SimpleProperty(description = "The color of the listview background.", + category = PropertyCategory.APPEARANCE) @IsColor public int BackgroundColor() { return backgroundColor; @@ -546,13 +501,44 @@ public int BackgroundColor() { * indicates fully transparent and {@code FF} means opaque. */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_COLOR, - defaultValue = Component.DEFAULT_VALUE_COLOR_BLACK) + defaultValue = Component.DEFAULT_VALUE_COLOR_BLACK) @SimpleProperty public void BackgroundColor(int argb) { backgroundColor = argb; recyclerView.setBackgroundColor(backgroundColor); linearLayout.setBackgroundColor(backgroundColor); -// setBackgroundColor(backgroundColor); + } + + /** + * Returns the listview's element color as an alpha-red-green-blue + * integer, i.e., {@code 0xAARRGGBB}. An alpha of {@code 00} + * indicates fully transparent and {@code FF} means opaque. + * + * @return background color in the format 0xAARRGGBB, which includes + * alpha, red, green, and blue components + */ + @SimpleProperty(description = "The color of the listview background.", + category = PropertyCategory.APPEARANCE) + @IsColor + public int ElementColor() { + return elementColor; + } + + /** + * The color of the `ListView` element. + * + * @param argb element color in the format 0xAARRGGBB, which + * includes alpha, red, green, and blue components + * @internaldoc Specifies the ListView's element color as an alpha-red-green-blue + * integer, i.e., {@code 0xAARRGGBB}. An alpha of {@code 00} + * indicates fully transparent and {@code FF} means opaque. + */ + @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_COLOR, + defaultValue = Component.DEFAULT_VALUE_COLOR_BLACK) + @SimpleProperty + public void ElementColor(int argb) { + elementColor = argb; + setAdapterData(); } /** @@ -564,7 +550,8 @@ public void BackgroundColor(int argb) { * @return selection color in the format 0xAARRGGBB, which includes * alpha, red, green, and blue components */ - @SimpleProperty(description = "The color of the item when it is selected.") + @SimpleProperty(description = "The color of the item when it is selected.", + category = PropertyCategory.APPEARANCE) @IsColor public int SelectionColor() { return selectionColor; @@ -581,8 +568,8 @@ public int SelectionColor() { * Is not supported on Icecream Sandwich or earlier */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_COLOR, - defaultValue = Component.DEFAULT_VALUE_COLOR_LTGRAY) - @SimpleProperty(category = PropertyCategory.APPEARANCE) + defaultValue = Component.DEFAULT_VALUE_COLOR_LTGRAY) + @SimpleProperty public void SelectionColor(int argb) { selectionColor = argb; setAdapterData(); @@ -596,9 +583,8 @@ public void SelectionColor(int argb) { * @return background color in the format 0xAARRGGBB, which includes * alpha, red, green, and blue components */ - @SimpleProperty( - description = "The text color of the listview stringItems.", - category = PropertyCategory.APPEARANCE) + @SimpleProperty(description = "The text color of the listview stringItems.", + category = PropertyCategory.APPEARANCE) @IsColor public int TextColor() { return textColor; @@ -614,7 +600,7 @@ public int TextColor() { * indicates fully transparent and {@code FF} means opaque. */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_COLOR, - defaultValue = Component.DEFAULT_VALUE_COLOR_WHITE) + defaultValue = Component.DEFAULT_VALUE_COLOR_WHITE) @SimpleProperty public void TextColor(int argb) { textColor = argb; @@ -626,9 +612,8 @@ public void TextColor(int argb) { * * @return color of the secondary text */ - @SimpleProperty( - description = "The text color of DetailText of listview stringItems. ", - category = PropertyCategory.APPEARANCE) + @SimpleProperty(description = "The text color of DetailText of listview stringItems. ", + category = PropertyCategory.APPEARANCE) public int TextColorDetail() { return detailTextColor; } @@ -640,7 +625,7 @@ public int TextColorDetail() { * includes alpha, red, green, and blue components */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_COLOR, - defaultValue = Component.DEFAULT_VALUE_COLOR_WHITE) + defaultValue = Component.DEFAULT_VALUE_COLOR_WHITE) @SimpleProperty public void TextColorDetail(int argb) { detailTextColor = argb; @@ -656,8 +641,7 @@ public void TextColorDetail(int argb) { * * @return text size as an integer */ - @SimpleProperty( - description = "The text size of the listview items.", + @SimpleProperty(description = "The text size of the listview items.", category = PropertyCategory.APPEARANCE) public int TextSize() { return Math.round(fontSizeMain); @@ -672,8 +656,8 @@ public int TextSize() { defaultValue = DEFAULT_TEXT_SIZE + "") @SimpleProperty public void TextSize(int textSize) { - if (textSize >1000) { - textSize = 999; + if (textSize > 1000) { + textSize = 999; } FontSize(Float.valueOf(textSize)); } @@ -683,10 +667,9 @@ public void TextSize(int textSize) { * * @return text size as an float */ - @SimpleProperty( - description = "The text size of the listview stringItems.", - category = PropertyCategory.APPEARANCE, - userVisible = false) + @SimpleProperty(description = "The text size of the listview stringItems.", + category = PropertyCategory.APPEARANCE, + userVisible = false) public float FontSize() { return fontSizeMain; } @@ -708,14 +691,14 @@ public void FontSize(float fontSize) { fontSizeMain = fontSize; setAdapterData(); } + /** * Returns the listview's text font Size * * @return text size as an float */ - @SimpleProperty( - description = "The text size of the listview stringItems.", - category = PropertyCategory.APPEARANCE) + @SimpleProperty(description = "The text size of the listview stringItems.", + category = PropertyCategory.APPEARANCE) public float FontSizeDetail() { return fontSizeDetail; } @@ -727,7 +710,7 @@ public float FontSizeDetail() { */ @SuppressWarnings("JavadocReference") @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_FLOAT, - defaultValue = Component.FONT_DEFAULT_SIZE + "") + defaultValue = Component.FONT_DEFAULT_SIZE + "") @SimpleProperty public void FontSizeDetail(float fontSize) { if (fontSize > 1000 || fontSize < 1) @@ -741,14 +724,13 @@ public void FontSizeDetail(float fontSize) { * Returns the label's text's font face as default, serif, sans * serif, or monospace. * - * @return one of {@link Component#TYPEFACE_DEFAULT}, - * {@link Component#TYPEFACE_SERIF}, - * {@link Component#TYPEFACE_SANSSERIF} or - * {@link Component#TYPEFACE_MONOSPACE} + * @return one of {@link Component#TYPEFACE_DEFAULT}, + * {@link Component#TYPEFACE_SERIF}, + * {@link Component#TYPEFACE_SANSSERIF} or + * {@link Component#TYPEFACE_MONOSPACE} */ - @SimpleProperty( - category = PropertyCategory.APPEARANCE, - userVisible = false) + @SimpleProperty(category = PropertyCategory.APPEARANCE, + userVisible = false) public String FontTypeface() { return fontTypeface; } @@ -757,15 +739,14 @@ public String FontTypeface() { * Specifies the label's text's font face as default, serif, sans * serif, or monospace. * - * @param typeface one of {@link Component#TYPEFACE_DEFAULT}, - * {@link Component#TYPEFACE_SERIF}, - * {@link Component#TYPEFACE_SANSSERIF} or - * {@link Component#TYPEFACE_MONOSPACE} + * @param typeface one of {@link Component#TYPEFACE_DEFAULT}, + * {@link Component#TYPEFACE_SERIF}, + * {@link Component#TYPEFACE_SANSSERIF} or + * {@link Component#TYPEFACE_MONOSPACE} */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_TYPEFACE, - defaultValue = Component.TYPEFACE_DEFAULT + "") - @SimpleProperty( - userVisible = false) + defaultValue = Component.TYPEFACE_DEFAULT + "") + @SimpleProperty(userVisible = false) public void FontTypeface(String typeface) { fontTypeface = typeface; setAdapterData(); @@ -775,14 +756,13 @@ public void FontTypeface(String typeface) { * Returns the label's text's font face as default, serif, sans * serif, or monospace. * - * @return one of {@link Component#TYPEFACE_DEFAULT}, - * {@link Component#TYPEFACE_SERIF}, - * {@link Component#TYPEFACE_SANSSERIF} or - * {@link Component#TYPEFACE_MONOSPACE} + * @return one of {@link Component#TYPEFACE_DEFAULT}, + * {@link Component#TYPEFACE_SERIF}, + * {@link Component#TYPEFACE_SANSSERIF} or + * {@link Component#TYPEFACE_MONOSPACE} */ - @SimpleProperty( - category = PropertyCategory.APPEARANCE, - userVisible = false) + @SimpleProperty(category = PropertyCategory.APPEARANCE, + userVisible = false) public String FontTypefaceDetail() { return fontTypeDetail; } @@ -791,27 +771,26 @@ public String FontTypefaceDetail() { * Specifies the label's text's font face as default, serif, sans * serif, or monospace. * - * @param typeface one of {@link Component#TYPEFACE_DEFAULT}, - * {@link Component#TYPEFACE_SERIF}, - * {@link Component#TYPEFACE_SANSSERIF} or - * {@link Component#TYPEFACE_MONOSPACE} + * @param typeface one of {@link Component#TYPEFACE_DEFAULT}, + * {@link Component#TYPEFACE_SERIF}, + * {@link Component#TYPEFACE_SANSSERIF} or + * {@link Component#TYPEFACE_MONOSPACE} */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_TYPEFACE, - defaultValue = Component.TYPEFACE_DEFAULT + "") - @SimpleProperty( - userVisible = false) + defaultValue = Component.TYPEFACE_DEFAULT + "") + @SimpleProperty(userVisible = false) public void FontTypefaceDetail(String typeface) { fontTypeDetail = typeface; setAdapterData(); } + /** * Returns the image width of ListView layouts containing images * * @return width of image */ - @SimpleProperty( - description = "The image width of the listview image.", - category = PropertyCategory.APPEARANCE) + @SimpleProperty(description = "The image width of the listview image.", + category = PropertyCategory.APPEARANCE) public int ImageWidth() { return imageWidth; } @@ -822,7 +801,7 @@ public int ImageWidth() { * @param width sets the width of image in the ListView row */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER, - defaultValue = DEFAULT_IMAGE_WIDTH + "") + defaultValue = DEFAULT_IMAGE_WIDTH + "") @SimpleProperty public void ImageWidth(int width) { imageWidth = width; @@ -834,9 +813,8 @@ public void ImageWidth(int width) { * * @return height of image */ - @SimpleProperty( - description = "The image height of the listview image stringItems.", - category = PropertyCategory.APPEARANCE) + @SimpleProperty(description = "The image height of the listview image stringItems.", + category = PropertyCategory.APPEARANCE) public int ImageHeight() { return imageHeight; } @@ -847,7 +825,7 @@ public int ImageHeight() { * @param height sets the height of image in the ListView row */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER, - defaultValue = DEFAULT_IMAGE_WIDTH + "") + defaultValue = DEFAULT_IMAGE_WIDTH + "") @SimpleProperty public void ImageHeight(int height) { imageHeight = height; @@ -855,36 +833,66 @@ public void ImageHeight(int height) { } /** - * Returns type of layout selected to display. Designer only property. + * Returns type of layout selected to display. * * @return layout as integer value */ - @SimpleProperty(category = PropertyCategory.APPEARANCE, userVisible = false) + @SimpleProperty(description = "Selecting the text and image layout in the ListView element.", + category = PropertyCategory.APPEARANCE) public int ListViewLayout() { return layout; } /** - * Specifies type of layout for ListView row. Designer only property. + * Specifies type of layout for ListView row. * * @param value integer value to determine type of ListView layout */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_LISTVIEW_LAYOUT, - defaultValue = ComponentConstants.LISTVIEW_LAYOUT_SINGLE_TEXT + "") - @SimpleProperty(userVisible = false) - public void ListViewLayout(int value) { + defaultValue = ComponentConstants.LISTVIEW_LAYOUT_SINGLE_TEXT + "") + @SimpleProperty + public void ListViewLayout(@Options(LayoutType.class) int value) { layout = value; setAdapterData(); } /** - * Returns the style of the button. + * Returns true or false depending on the enabled state of multiselect. + * + * @return true or false (is multiselect) + * @suppressdoc + */ + @SimpleProperty(description = "A function that allows you to select multiple elements. " + + "True - function enabled, false - disabled.", + category = PropertyCategory.BEHAVIOR) + public boolean MultiSelect() { + return multiSelect; + } + + /** + * Sets the multiselect function. `true`{:.logic.block} will enable the function, + * `false`{:.logic.block} will disable. + * + * @param multiSelect sets the function according to this input + */ + @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, + defaultValue = "False") + @SimpleProperty + public void MultiSelect(boolean multi) { + if (selectionIndex > 0) { + listAdapterWithRecyclerView.clearSelections(); + } + this.multiSelect = multi; + } + + /** + * Returns the type of layout's orientation. * * @return one of {@link ComponentConstants#LAYOUT_ORIENTATION_VERTICAL}, * {@link ComponentConstants#LAYOUT_ORIENTATION_HORIZONTAL}, */ - @SimpleProperty( - category = PropertyCategory.APPEARANCE) + @SimpleProperty(description = "Specifies the layout's orientation (vertical, horizontal).", + category = PropertyCategory.APPEARANCE) public int Orientation() { return orientation; } @@ -895,15 +903,19 @@ public int Orientation() { * allows the user to swipe left or right to brows the elements. * * @param orientation one of {@link ComponentConstants#LAYOUT_ORIENTATION_VERTICAL}, - * {@link ComponentConstants#LAYOUT_ORIENTATION_HORIZONTAL}, - * @throws IllegalArgumentException if orientation is not a legal value. + * {@link ComponentConstants#LAYOUT_ORIENTATION_HORIZONTAL}, */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_RECYCLERVIEW_ORIENTATION, - defaultValue = ComponentConstants.LAYOUT_ORIENTATION_VERTICAL + "") - @SimpleProperty(description = "Specifies the layout's orientation (vertical, horizontal). ") - public void Orientation(int orientation) { + defaultValue = ComponentConstants.LAYOUT_ORIENTATION_VERTICAL + "") + @SimpleProperty + public void Orientation(@Options(ListOrientation.class) int orientation) { this.orientation = orientation; - setAdapterData(); + if (orientation == ComponentConstants.LAYOUT_ORIENTATION_HORIZONTAL) { + layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + } else { // if (orientation == ComponentConstants.LAYOUT_ORIENTATION_VERTICAL) { + layoutManager.setOrientation(LinearLayoutManager.VERTICAL); + } + recyclerView.requestLayout(); } /** @@ -911,7 +923,8 @@ public void Orientation(int orientation) { * * @return string form of the array of JsonObject */ - @SimpleProperty(category = PropertyCategory.BEHAVIOR, userVisible = false) + @SimpleProperty(category = PropertyCategory.BEHAVIOR, + userVisible = false) public String ListData() { return propertyValue; } @@ -926,10 +939,10 @@ public String ListData() { * @param propertyValue string representation of row data (JsonArray of JsonObjects) */ @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_LISTVIEW_ADD_DATA) - @SimpleProperty(userVisible = false, category = PropertyCategory.BEHAVIOR) + @SimpleProperty(userVisible = false) public void ListData(String propertyValue) { this.propertyValue = propertyValue; - dictItems.clear(); + items.clear(); if (propertyValue != null && propertyValue != "") { try { JSONArray arr = new JSONArray(propertyValue); @@ -937,35 +950,36 @@ public void ListData(String propertyValue) { for (int i = 0; i < arr.length(); ++i) { JSONObject jsonItem = arr.getJSONObject(i); YailDictionary yailItem = new YailDictionary(); - if (jsonItem.has("Text1")) { - yailItem.put(Component.LISTVIEW_KEY_MAIN_TEXT, jsonItem.getString("Text1")); - yailItem.put(Component.LISTVIEW_KEY_DESCRIPTION, jsonItem.has("Text2") ? jsonItem.getString("Text2") : ""); - yailItem.put(Component.LISTVIEW_KEY_IMAGE, jsonItem.has("Image") ? jsonItem.getString("Image") : ""); - dictItems.add(yailItem); + if (jsonItem.has(Component.LISTVIEW_KEY_MAIN_TEXT)) { + yailItem.put(Component.LISTVIEW_KEY_MAIN_TEXT, jsonItem.getString(Component.LISTVIEW_KEY_MAIN_TEXT)); + yailItem.put(Component.LISTVIEW_KEY_DESCRIPTION, jsonItem.has(Component.LISTVIEW_KEY_DESCRIPTION) ? jsonItem.getString(Component.LISTVIEW_KEY_DESCRIPTION) : ""); + yailItem.put(Component.LISTVIEW_KEY_IMAGE, jsonItem.has(Component.LISTVIEW_KEY_IMAGE) ? jsonItem + .getString(Component.LISTVIEW_KEY_IMAGE) : ""); + items.add(yailItem); } } } catch (JSONException e) { Log.e(LOG_TAG, "Malformed JSON in ListView.ListData", e); container.$form().dispatchErrorOccurredEvent(this, "ListView.ListData", ErrorMessages.ERROR_DEFAULT, e.getMessage()); } + updateAdapterData(); + listAdapterWithRecyclerView.notifyDataSetChanged(); } - setAdapterData(); } /** * Creates a * - * @param mainText Primary text of the entry. Should be unique if possible. - * @param detailText Additional descriptive text. - * @param imageName File name of an image that has been uploaded to media. - * + * @param mainText Primary text of the entry. Should be unique if possible. + * @param detailText Additional descriptive text. + * @param imageName File name of an image that has been uploaded to media. */ @SimpleFunction(description = "Create a ListView entry. MainText is required. DetailText and ImageName are optional.") public YailDictionary CreateElement(final String mainText, final String detailText, final String imageName) { YailDictionary dictItem = new YailDictionary(); dictItem.put(Component.LISTVIEW_KEY_MAIN_TEXT, mainText); - dictItem.put("Text2", detailText); - dictItem.put("Image", imageName); + dictItem.put(Component.LISTVIEW_KEY_DESCRIPTION, detailText); + dictItem.put(Component.LISTVIEW_KEY_IMAGE, imageName); return dictItem; } @@ -976,14 +990,364 @@ public String GetMainText(final YailDictionary listElement) { @SimpleFunction(description = "Get the Detail Text of a ListView element.") public String GetDetailText(final YailDictionary listElement) { - return listElement.get("Text2").toString(); + return listElement.get(Component.LISTVIEW_KEY_DESCRIPTION).toString(); } @SimpleFunction(description = "Get the filename of the image of a ListView element that has been uploaded to Media.") public String GetImageName(final YailDictionary listElement) { - return listElement.get("Image").toString(); + return listElement.get(Component.LISTVIEW_KEY_IMAGE).toString(); + } + + /** + * Returns the listview's divider color as an alpha-red-green-blue + * integer, i.e., {@code 0xAARRGGBB}. An alpha of {@code 00} + * indicates fully transparent and {@code FF} means opaque. + * + * @return divider color in the format 0xAARRGGBB, which includes + * alpha, red, green, and blue components + */ + @SimpleProperty(description = "The color of the list view divider.", + category = PropertyCategory.APPEARANCE) + @IsColor + public int DividerColor() { + return dividerColor; + } + + /** + * The color of the `ListView` divider. + * + * @param argb divider color in the format 0xAARRGGBB, which + * includes alpha, red, green, and blue components + * @internaldoc Specifies the ListView's divider color as an alpha-red-green-blue + * integer, i.e., {@code 0xAARRGGBB}. An alpha of {@code 00} + * indicates fully transparent and {@code FF} means opaque. + */ + @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_COLOR, + defaultValue = Component.DEFAULT_VALUE_COLOR_WHITE) + @SimpleProperty + public void DividerColor(int argb) { + dividerColor = argb; + dividerPaint = new Paint(); + dividerPaint.setColor(argb); + setDivider(); + } + + /** + * Returns the divider thickness of list view + * + * @return thickness of divider + */ + @SimpleProperty(description = "The thickness of the element divider in the list view. " + + "If the thickness is 0, the divider is not visible.", + category = PropertyCategory.APPEARANCE) + public int DividerThickness() { + return dividerSize; + } + + /** + * Specifies the divider thickness of list view + * + * @param size sets the thickness of divider in the list view + */ + @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER, + defaultValue = DEFAULT_DIVIDER_SIZE + "") + @SimpleProperty + public void DividerThickness(int size) { + this.dividerSize = size; + setDivider(); + } + + /** + * Returns the margins width of list view element + * + * @return width of margins + */ + @SimpleProperty(description = "The margins width of the list view element.", + category = PropertyCategory.APPEARANCE) + public int ElementMarginsWidth() { + return margins; + } + + /** + * Specifies the width of the margins of a list view element + * + * @param width sets the width of the margins in the list view element + */ + @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER, + defaultValue = DEFAULT_MARGINS_SIZE + "") + @SimpleProperty + public void ElementMarginsWidth(int width) { + this.margins = width; + setDivider(); + } + + /** + * Returns the corner radius of the list view element. + * + * @return corner radius + */ + @SimpleProperty(description = "The radius of the rounded corners of a list view item.", + category = PropertyCategory.APPEARANCE) + public int ElementCornerRadius() { + return radius; + } + + /** + * Specifies the corner radius of the list view element. + * + * @param radius sets the radius of the corners in the list view element + */ + @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_NON_NEGATIVE_INTEGER, + defaultValue = DEFAULT_RADIUS + "") + @SimpleProperty + public void ElementCornerRadius(int radius) { + this.radius = radius; + setAdapterData(); + } + + /** + * Returns true or false depending on the enabled state of bounce effect. + * + * @return true or false (is bounce effect) + * @suppressdoc + */ + @SimpleProperty(description = "The effect of bounce from the edge after scrolling the list to the end. " + + " True will enable the bounce effect, false will disable it.", + category = PropertyCategory.BEHAVIOR) + public boolean BounceEdgeEffect() { + return bounceEffect; + } + + /** + * Sets the bounce effect function. `true`{:.logic.block} will enable the function, + * `false`{:.logic.block} will disable. + * + * @param bounce sets the function according to this input + */ + @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_BOOLEAN, + defaultValue = "False") + @SimpleProperty + public void BounceEdgeEffect(boolean bounce) { + if (bounce) { + recyclerView.setEdgeEffectFactory(bounceEdgeEffectFactory); + } else { + recyclerView.setEdgeEffectFactory(edgeEffectFactory); + } + this.bounceEffect = bounce; + } + + /** + * Removes Item from list at a given index + */ + @SimpleFunction(description = "Removes Item from list at a given index.") + public void RemoveItemAtIndex(int index) { + if (index < 1 || index > items.size()) { + container.$form().dispatchErrorOccurredEvent(this, "RemoveItemAtIndex", + ErrorMessages.ERROR_LISTVIEW_INDEX_OUT_OF_BOUNDS, index); + return; + } + items.remove(index - 1); + updateAdapterData(); + listAdapterWithRecyclerView.notifyItemRemoved(index - 1); + } + + /** + * Add new Item to list + */ + @SimpleFunction(description = "Add new Item to list at the end.") + public void AddItem(String mainText, String detailText, String imageName) { + if (!items.isEmpty()) { + Object o = items.get(0); + if (o instanceof YailDictionary) { + if (((YailDictionary) o).containsKey(Component.LISTVIEW_KEY_MAIN_TEXT)) { + items.add(CreateElement(mainText, detailText, imageName)); + } else { + items.add(mainText); + } + } else { + items.add(mainText); + } + } else { + if (layout == Component.LISTVIEW_LAYOUT_SINGLE_TEXT) { + items.add(mainText); + } else { + items.add(CreateElement(mainText, detailText, imageName)); + } + } + updateAdapterData(); + listAdapterWithRecyclerView.notifyItemChanged(listAdapterWithRecyclerView.getItemCount() - 1); + } + + /** + * Add new Items to list + */ + @SimpleFunction(description = "Add new Items to list at the end.") + public void AddItems(List itemsList) { + if (!itemsList.isEmpty()) { + int positionStart = items.size(); + int itemCount = itemsList.size(); + items.addAll(itemsList); + updateAdapterData(); + listAdapterWithRecyclerView.notifyItemRangeChanged(positionStart, itemCount); + } + } + + /** + * Add new Item to list at a given index + */ + @SimpleFunction(description = "Add new Item to list at a given index.") + public void AddItemAtIndex(int index, String mainText, String detailText, String imageName) { + if (index < 1 || index > items.size() + 1) { + container.$form().dispatchErrorOccurredEvent(this, "AddItemAtIndex", + ErrorMessages.ERROR_LISTVIEW_INDEX_OUT_OF_BOUNDS, index); + return; + } + if (!items.isEmpty()) { + Object o = items.get(0); + if (o instanceof YailDictionary) { + if (((YailDictionary) o).containsKey(Component.LISTVIEW_KEY_MAIN_TEXT)) { + items.add(index - 1, CreateElement(mainText, detailText, imageName)); + } else { + items.add(index - 1, mainText); + } + } else { + items.add(index - 1, mainText); + } + } else { + if (layout == Component.LISTVIEW_LAYOUT_SINGLE_TEXT) { + items.add(index - 1, mainText); + } else { + items.add(index - 1, CreateElement(mainText, detailText, imageName)); + } + } + updateAdapterData(); + listAdapterWithRecyclerView.notifyItemInserted(index - 1); + } + + /** + * Add new Items to list at specific index + */ + @SimpleFunction(description = "Add new Items to list at specific index.") + public void AddItemsAtIndex(int index, YailList itemsList) { + if (index < 1 || index > items.size() + 1) { + container.$form().dispatchErrorOccurredEvent(this, "AddItemsAtIndex", + ErrorMessages.ERROR_LISTVIEW_INDEX_OUT_OF_BOUNDS, index); + return; + } + if (!itemsList.isEmpty()) { + int positionStart = index - 1; + int itemCount = itemsList.size(); + items.addAll(positionStart, itemsList); + updateAdapterData(); + listAdapterWithRecyclerView.notifyItemRangeChanged(positionStart, itemCount); + } + } + + /** + * Create a new adapter and apply visual changes, load data if it exists. + */ + public void setAdapterData() { + listAdapterWithRecyclerView = new ListAdapterWithRecyclerView(container, items, layout, textColor, detailTextColor, fontSizeMain, fontSizeDetail, fontTypeface, fontTypeDetail, elementColor, selectionColor, imageWidth, imageHeight, radius); + listAdapterWithRecyclerView.setOnItemClickListener(new ListAdapterWithRecyclerView.ClickListener() { + @Override + public void onItemClick(int position, View v) { + SelectionIndex(position + 1); + AfterPicking(); + } + }); + recyclerView.setAdapter(listAdapterWithRecyclerView); + } + + /** + * Deselect the item and update the data in adapter. + */ + public void updateAdapterData() { + SelectionIndex(0); + listAdapterWithRecyclerView.updateData(items); + } + + /** + * Sets new dividers or margins in RecyclerView + */ + private void setDivider() { + DividerItemDecoration dividerDecoration = new DividerItemDecoration(); + for (int i = 0; i < recyclerView.getItemDecorationCount(); i++) { + RecyclerView.ItemDecoration decoration = recyclerView.getItemDecorationAt(i); + if (decoration instanceof DividerItemDecoration) { + recyclerView.removeItemDecorationAt(i); + break; + } + } + recyclerView.addItemDecoration(dividerDecoration); + } + + /** + * A class that creates dividers between elements or margins, depending on the options selected. + */ + private class DividerItemDecoration extends RecyclerView.ItemDecoration { + public DividerItemDecoration() { + } + + @Override + public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) { + //If margins are set, dividers will not be created. + if (margins == 0) { + int childCount = parent.getChildCount(); + if (orientation == ComponentConstants.LAYOUT_ORIENTATION_HORIZONTAL) { + for (int i = 0; i < childCount - 1; i++) { + View child = parent.getChildAt(i); + int position = parent.getChildAdapterPosition(child); + if (position != RecyclerView.NO_POSITION) { + int left = child.getRight(); + int right = left + dividerSize; + int top = child.getTop(); + int bottom = child.getBottom(); + canvas.drawRect(left, top, right, bottom, dividerPaint); + } + } + } else { + int width = parent.getWidth(); + for (int i = 0; i < childCount - 1; i++) { + View child = parent.getChildAt(i); + int position = parent.getChildAdapterPosition(child); + if (position != RecyclerView.NO_POSITION) { + int top = child.getBottom(); + int bottom = top + dividerSize; + canvas.drawRect(0, top, width, bottom, dividerPaint); + } + } + } + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + int position = parent.getChildAdapterPosition(view); + int spanCount = 1; //No GridLayout support, so spanCount set to 1. + if (margins == 0) { + if (position != RecyclerView.NO_POSITION && position < parent.getAdapter().getItemCount() - 1) { + if (orientation == ComponentConstants.LAYOUT_ORIENTATION_HORIZONTAL) { + outRect.set(0, 0, dividerSize, 0); + } else { + outRect.set(0, 0, 0, dividerSize); + } + } else { + outRect.setEmpty(); + } + } else { + int column = position % spanCount; + outRect.left = margins - column * margins / spanCount; + outRect.right = (column + 1) * margins / spanCount; + if (position < spanCount || first) { + first = false; + outRect.top = margins; + } + outRect.bottom = margins; + } + } } + @Deprecated @SimpleFunction(description = "Reload the ListView to reflect any changes in the data.") public void Refresh() { setAdapterData(); diff --git a/appinventor/components/tests/com/google/appinventor/components/runtime/ListViewTest.java b/appinventor/components/tests/com/google/appinventor/components/runtime/ListViewTest.java index e2300bba05..623774b316 100644 --- a/appinventor/components/tests/com/google/appinventor/components/runtime/ListViewTest.java +++ b/appinventor/components/tests/com/google/appinventor/components/runtime/ListViewTest.java @@ -83,7 +83,8 @@ public void testFilter() throws InterruptedException { Thread.sleep(100); // Filtering runs on a separate thread for performance reasons runAllEvents(); - RecyclerView rv = (RecyclerView) ((LinearLayout) listView1.getView()).getChildAt(1); + LinearLayout listlayout = (LinearLayout) ((LinearLayout) listView1.getView()).getChildAt(1); + RecyclerView rv = (RecyclerView) listlayout.getChildAt(0); int count = 0; for (int i = 0; i < rv.getLayoutManager().getChildCount(); i++) { if (rv.getLayoutManager().getChildAt(i).getVisibility() == View.VISIBLE) { @@ -116,7 +117,8 @@ public void testSelectionRemovalWithDictBasedElements() { } private View getViewForPosition(ListView listView, int position) { - RecyclerView rv = (RecyclerView) ((LinearLayout) listView.getView()).getChildAt(1); + LinearLayout listLayout = (LinearLayout) ((LinearLayout) listView.getView()).getChildAt(1); + RecyclerView rv = (RecyclerView) listLayout.getChildAt(0); RecyclerView.ViewHolder vh = rv.findViewHolderForAdapterPosition(position); assertNotNull(vh); return vh.itemView; diff --git a/appinventor/docs/html/reference/components/userinterface.html b/appinventor/docs/html/reference/components/userinterface.html index 211f41afe5..c67e1b4e08 100644 --- a/appinventor/docs/html/reference/components/userinterface.html +++ b/appinventor/docs/html/reference/components/userinterface.html @@ -732,6 +732,19 @@

Properties

BackgroundColor
The color of the ListView background.
+
BounceEdgeEffect
+
Sets the bounce effect function. true will enable the function, + false will disable.
+
DividerColor
+
The color of the ListView divider.
+
DividerThickness
+
Specifies the divider thickness of list view
+
ElementColor
+
The color of the ListView element.
+
ElementCornerRadius
+
Specifies the corner radius of the list view element.
+
ElementMarginsWidth
+
Specifies the width of the margins of a list view element
Elements
Specifies the list of choices to display.
ElementsFromString
@@ -760,8 +773,11 @@

Properties

layout is Image,MainText this property will allow any number of elements to be defined, each containing a filename for Image and a string for MainText. Designer only property. -
ListViewLayout
-
Specifies type of layout for ListView row. Designer only property.
+
ListViewLayout
+
Specifies type of layout for ListView row.
+
MultiSelect
+
Sets the multiselect function. true will enable the function, + false will disable.
Orientation
Specifies the layout’s orientation. This may be: Vertical, which displays elements in rows one after the other; or Horizontal, which displays one element at a time and @@ -807,6 +823,14 @@

Events

Methods

+
AddItem(mainText,detailText,imageName)
+
Add new Item to list
+
AddItemAtIndex(index,mainText,detailText,imageName)
+
Add new Item to list at a given index
+
AddItems(itemsList)
+
Add new Items to list
+
AddItemsAtIndex(index,itemsList)
+
Add new Items to list at specific index
CreateElement(mainText,detailText,imageName)
Creates a
GetDetailText(listElement)
@@ -815,8 +839,6 @@

Methods

Get the filename of the image of a ListView element that has been uploaded to Media.
GetMainText(listElement)
Get the Main Text of a ListView element.
-
Refresh()
-
Reload the ListView to reflect any changes in the data.
RemoveItemAtIndex(index)
Removes Item from list at a given index
diff --git a/appinventor/docs/markdown/reference/components/userinterface.md b/appinventor/docs/markdown/reference/components/userinterface.md index 11e1ee733d..3d601182e7 100644 --- a/appinventor/docs/markdown/reference/components/userinterface.md +++ b/appinventor/docs/markdown/reference/components/userinterface.md @@ -773,6 +773,25 @@ This is a visible component that displays a list of text and image elements in y {:id="ListView.BackgroundColor" .color} *BackgroundColor* : The color of the `ListView` background. +{:id="ListView.BounceEdgeEffect" .boolean} *BounceEdgeEffect* +: Sets the bounce effect function. `true`{:.logic.block} will enable the function, + `false`{:.logic.block} will disable. + +{:id="ListView.DividerColor" .color} *DividerColor* +: The color of the `ListView` divider. + +{:id="ListView.DividerThickness" .number} *DividerThickness* +: Specifies the divider thickness of list view + +{:id="ListView.ElementColor" .color} *ElementColor* +: The color of the `ListView` element. + +{:id="ListView.ElementCornerRadius" .number} *ElementCornerRadius* +: Specifies the corner radius of the list view element. + +{:id="ListView.ElementMarginsWidth" .number} *ElementMarginsWidth* +: Specifies the width of the margins of a list view element + {:id="ListView.Elements" .list .bo} *Elements* : Specifies the list of choices to display. @@ -811,8 +830,12 @@ This is a visible component that displays a list of text and image elements in y defined, each containing a filename for Image and a string for MainText. Designer only property. -{:id="ListView.ListViewLayout" .number .do} *ListViewLayout* -: Specifies type of layout for ListView row. Designer only property. +{:id="ListView.ListViewLayout" .number} *ListViewLayout* +: Specifies type of layout for ListView row. + +{:id="ListView.MultiSelect" .boolean} *MultiSelect* +: Sets the multiselect function. `true`{:.logic.block} will enable the function, + `false`{:.logic.block} will disable. {:id="ListView.Orientation" .number} *Orientation* : Specifies the layout's orientation. This may be: `Vertical`, which displays elements @@ -870,6 +893,18 @@ This is a visible component that displays a list of text and image elements in y {:.methods} +{:id="ListView.AddItem" class="method"} AddItem(*mainText*{:.text},*detailText*{:.text},*imageName*{:.text}) +: Add new Item to list + +{:id="ListView.AddItemAtIndex" class="method"} AddItemAtIndex(*index*{:.number},*mainText*{:.text},*detailText*{:.text},*imageName*{:.text}) +: Add new Item to list at a given index + +{:id="ListView.AddItems" class="method"} AddItems(*itemsList*{:.list}) +: Add new Items to list + +{:id="ListView.AddItemsAtIndex" class="method"} AddItemsAtIndex(*index*{:.number},*itemsList*{:.list}) +: Add new Items to list at specific index + {:id="ListView.CreateElement" class="method returns dictionary"} CreateElement(*mainText*{:.text},*detailText*{:.text},*imageName*{:.text}) : Creates a @@ -882,9 +917,6 @@ This is a visible component that displays a list of text and image elements in y {:id="ListView.GetMainText" class="method returns text"} GetMainText(*listElement*{:.dictionary}) : Get the Main Text of a ListView element. -{:id="ListView.Refresh" class="method"} Refresh() -: Reload the ListView to reflect any changes in the data. - {:id="ListView.RemoveItemAtIndex" class="method"} RemoveItemAtIndex(*index*{:.number}) : Removes Item from list at a given index diff --git a/appinventor/lib/android/support/dynamicanimation-1.0.0.jar b/appinventor/lib/android/support/dynamicanimation-1.0.0.jar new file mode 100644 index 0000000000..88f5191e8a Binary files /dev/null and b/appinventor/lib/android/support/dynamicanimation-1.0.0.jar differ