I followed this tutorial to create an Image cropping page https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/bitmaps/cropping. However i now bound two views of the PhotoCropperCanvasView to an carousel view an wanted to Bind the CroppedBitMap so that i can access this property directl in the viewmodel. I just cannot figure ouit how i would achieve that. I f i just make this a Bindable Property the property does not change when i make a new rectangle. So i think i kind of have to exclude the Code of the property but i am very confused.
The whole ocde:
public class PhotoCropperCanvasView : SKCanvasView, INotifyPropertyChanged
{
const int CORNER = 50; // pixel length of cropper corner
const int RADIUS = 100; // pixel radius of touch hit-test
//SKBitmap bitmap;
//CroppingRectangle croppingRect;
SKMatrix inverseBitmapMatrix;
public SKBitmap testmap;
//public SKBitmap bitmap { get; set; }
public Image testImage { get; set; }
public static readonly BindableProperty mapProperty =
BindableProperty.Create(nameof(map), typeof(SKBitmap), typeof(PhotoCropperCanvasView), null);
public SKBitmap map
{
get
{
return (SKBitmap)GetValue(mapProperty);
}
set
{
SetValue(mapProperty, value);
}
}
public CroppingRectangle croppingRect { get; set; }
//public SKMatrix inverseBitmapMatrix { get; set; }
public static readonly BindableProperty bitmapProperty =
BindableProperty.Create(nameof(bitmap), typeof(SKBitmap), typeof(Image),null,propertyChanged: OnbitmapChanged);
static void OnbitmapChanged(BindableObject bindable, object oldValue, object newValue)
{
Console.WriteLine("test");
}
public SKBitmap bitmap
{
get
{
return (SKBitmap)GetValue(bitmapProperty);
}
set
{
SetValue(bitmapProperty, value);
}
}
public SKBitmap CroppedBitmap
{
get
{
SKRect cropRect = new SKRect(croppingRect.Rect.Left,croppingRect.Rect.Top,croppingRect.Rect.Right,croppingRect.Rect.Bottom);
SKBitmap croppedBitmap = new SKBitmap((int)cropRect.Width,
(int)cropRect.Height);
SKRect dest = new SKRect(0, 0, cropRect.Width, cropRect.Height);
SKRect source = new SKRect(cropRect.Left, cropRect.Top,
cropRect.Right, cropRect.Bottom);
using (SKCanvas canvas = new SKCanvas(croppedBitmap))
{
canvas.DrawBitmap(bitmap, source, dest);
}
return croppedBitmap;
}
}
// Touch tracking
TouchEffect touchEffect = new TouchEffect();
struct TouchPoint
{
public int CornerIndex { set; get; }
public SKPoint Offset { set; get; }
}
Dictionary<long, TouchPoint> touchPoints = new Dictionary<long, TouchPoint>();
// Drawing objects
SKPaint cornerStroke = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.White,
StrokeWidth = 10
};
SKPaint edgeStroke = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.White,
StrokeWidth = 2
};
// this constructor for profile image
public PhotoCropperCanvasView(SKBitmap bitmap, float? aspectRatio = null)
{
this.bitmap = bitmap;
SKRect bitmapRect = new SKRect(0, 0, bitmap.Width, bitmap.Height);
croppingRect = new CroppingRectangle(bitmapRect, aspectRatio);
touchEffect.TouchAction += OnTouchEffectTouchAction;
}
// this constructor for post images
public PhotoCropperCanvasView()
{
}
protected override void OnPropertyChanged([CallerMemberName] string propertyName = nameof(bitmap))
{
base.OnPropertyChanged(propertyName);
if (bitmap != null)
{
SKRect bitmapRect = new SKRect(0, 0, bitmap.Width, bitmap.Width);
croppingRect = new CroppingRectangle(bitmapRect, 1);
touchEffect.TouchAction += OnTouchEffectTouchAction;
}
}
protected override void OnParentSet()
{
base.OnParentSet();
// Attach TouchEffect to parent view
Parent.Effects.Add(touchEffect);
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs args)
{
base.OnPaintSurface(args);
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Gray);
// Calculate rectangle for displaying bitmap
float scale = Math.Min((float)info.Width / bitmap.Width, (float)info.Height / bitmap.Height);
float x = (info.Width - scale * bitmap.Width) / 2;
float y = (info.Height - scale * bitmap.Height) / 2;
SKRect bitmapRect = new SKRect(x, y, x + scale * bitmap.Width, y + scale * bitmap.Height);
canvas.DrawBitmap(bitmap, bitmapRect);
// Calculate a matrix transform for displaying the cropping rectangle
SKMatrix bitmapScaleMatrix = SKMatrix.MakeIdentity();
bitmapScaleMatrix.SetScaleTranslate(scale, scale, x, y);
// Display rectangle
SKRect scaledCropRect = bitmapScaleMatrix.MapRect(croppingRect.Rect);
canvas.DrawRect(scaledCropRect, edgeStroke);
// Display heavier corners
using (SKPath path = new SKPath())
{
path.MoveTo(scaledCropRect.Left, scaledCropRect.Top + CORNER);
path.LineTo(scaledCropRect.Left, scaledCropRect.Top);
path.LineTo(scaledCropRect.Left + CORNER, scaledCropRect.Top);
path.MoveTo(scaledCropRect.Right - CORNER, scaledCropRect.Top);
path.LineTo(scaledCropRect.Right, scaledCropRect.Top);
path.LineTo(scaledCropRect.Right, scaledCropRect.Top + CORNER);
path.MoveTo(scaledCropRect.Right, scaledCropRect.Bottom - CORNER);
path.LineTo(scaledCropRect.Right, scaledCropRect.Bottom);
path.LineTo(scaledCropRect.Right - CORNER, scaledCropRect.Bottom);
path.MoveTo(scaledCropRect.Left + CORNER, scaledCropRect.Bottom);
path.LineTo(scaledCropRect.Left, scaledCropRect.Bottom);
path.LineTo(scaledCropRect.Left, scaledCropRect.Bottom - CORNER);
canvas.DrawPath(path, cornerStroke);
}
// Invert the transform for touch tracking
bitmapScaleMatrix.TryInvert(out inverseBitmapMatrix);
}
void OnTouchEffectTouchAction(object sender, TouchActionEventArgs args)
{
int i = 0;
SKPoint pixelLocation = ConvertToPixel(args.Location);
SKPoint bitmapLocation = inverseBitmapMatrix.MapPoint(pixelLocation);
switch (args.Type)
{
case TouchActionType.Pressed:
// Convert radius to bitmap/cropping scale
float radius = inverseBitmapMatrix.ScaleX * RADIUS;
// Find corner that the finger is touching
int cornerIndex = croppingRect.HitTest(bitmapLocation, radius);
if (cornerIndex != -1 && !touchPoints.ContainsKey(args.Id))
{
TouchPoint touchPoint = new TouchPoint
{
CornerIndex = cornerIndex,
Offset = bitmapLocation - croppingRect.Corners[cornerIndex]
};
touchPoints.Add(args.Id, touchPoint);
}
break;
case TouchActionType.Moved:
if (touchPoints.ContainsKey(args.Id))
{
TouchPoint touchPoint = touchPoints[args.Id];
croppingRect.MoveCorner(touchPoint.CornerIndex,
bitmapLocation - touchPoint.Offset);
InvalidateSurface();
}
break;
case TouchActionType.Released:
case TouchActionType.Cancelled:
if (touchPoints.ContainsKey(args.Id))
{
touchPoints.Remove(args.Id);
//map = CroppedBitmap;
}
break;
}
}
SKPoint ConvertToPixel(Xamarin.Forms.Point pt)
{
return new SKPoint((float)(CanvasSize.Width * pt.X / Width),
(float)(CanvasSize.Height * pt.Y / Height));
}
}
}
Since the case TouchActionType.Cancelled gets only triggerd once everytime the rectangel was moved, i thought i would set thew bindable Proeprty map to the Cropped bitmap property so that i can get the Cropped Image from the view obver a Binding to the viewmodel. This part works, however, when i activate the line map = CroppedBitmap the cropping rectangle can only be moved by opposite corners. So if i start moving it with the bottom right corner i con only use the top left or bottom right. If i leave the line map = CroppedBitman(249) deactivated i can move the rectangle on all corners at every times. I do not understand this behaviour.
the view:
<CarouselView Grid.Row="0"
IsSwipeEnabled="False"
x:Name="carousel"
Margin="0,-40,0,0"
CurrentItem="{Binding CurrentCutImage, Mode=TwoWay}"
CurrentItemChanged="CarouselView_CurrentItemChanged"
HorizontalScrollBarVisibility="Always"
IsScrollAnimated="True"
ItemsSource="{Binding ImageObjects}"
VerticalScrollBarVisibility="Always"
>
<CarouselView.ItemTemplate>
<DataTemplate x:DataType="viewmodel:CutImages">
<Grid>
<bitmaps:PhotoCropperCanvasView bitmap="{Binding ImageSource }" map="{Binding MapSource, Mode=TwoWay}" >
</bitmaps:PhotoCropperCanvasView>
</Grid>
</DataTemplate>
</CarouselView.ItemTemplate>
</CarouselView>
and the VM:
public partial class CutImagesViewModel : ObservableObject
{
// == observable properties ==
[ObservableProperty]
public Collection<CutImages> imageObjects = new Collection<CutImages>();
[ObservableProperty]
CutImages currentCutImage;
[ObservableProperty]
public SKBitmap maps;
public CutImagesViewModel(Collection<SKBitmapImageSource> images)
{
foreach(var image in images)
{
ImageObjects.Add(new CutImages(image));
}
this.CurrentCutImage = this.ImageObjects.FirstOrDefault();
}
}
public partial class CutImages : ObservableObject
{
[ObservableProperty]
public SKBitmap imageSource;
[ObservableProperty]
public SKBitmap mapSource;
partial void OnMapSourceChanged(SKBitmap value)
{
if(!images.Contains(value))
{
images.Add(value);
}
}
[ObservableProperty]
Collection<SKBitmap> images = new Collection<SKBitmap>();
public CutImages(ImageSource imageSource)
{
SKBitmapImageSource sourceImage = (SKBitmapImageSource)imageSource;
SKBitmap image = sourceImage;
ImageSource = image;
}
}
Related
I have a ListView in Xamarin.Forms of this way :
this.listView = new ListView();
this.listView.HasUnevenRows = true;
var dataTemplate = new DataTemplate(() =>
{
return new ViewCell { View = new CustomButtonTemplate()};
});
this.listView.ItemTemplate = dataTemplate;
CustomButtonTemplate.xaml
<local:CustomButton
Margin="6"
Padding="0"
HeightRequest="-1"
WidthRequest="-1"
Style="{StaticResource Title_LabelStyle}"
Text="{Binding DisplayText}" />
I also got one button renderer but dont work (without HeightRequest,WidthRequest,Padding dont work either):
[assembly: ExportRenderer(typeof(CustomButton), typeof(CustomButtonMultilineRenderer))]
namespace SGUK.ClassAction.IOS.Renderers
{
public class CustomButtonMultilineRenderer : ButtonRenderer
{
public CustomButtonMultilineRenderer()
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.Button> e)
{
base.OnElementChanged(e);
if (this.Control != null)
{
this.Control.TitleLabel.LineBreakMode = UILineBreakMode.WordWrap;
this.Control.TitleEdgeInsets = new UIEdgeInsets(0, 10, 0, 10);
this.Control.TitleLabel.TextAlignment = UITextAlignment.Center;
this.Control.HorizontalAlignment = UIControlContentHorizontalAlignment.Center;
}
}
}
}
(with MaterialButtonRenderer dont work either)
The auto height with HasUnevenRows=true works fine on iOS if not using a custom renderer. If using a custom renderer, then it is up to the renderer to set the height of the cell, you have to calculate your own row height in the GetHeightForRow method in the custom renderer.
[assembly: ExportRenderer(typeof(ListView), typeof(MyLVRenderer))]
namespace App79.iOS
{
public class MyLVRenderer : ListViewRenderer
{
//UITableViewSource originalSource;
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
{
base.OnElementChanged(e);
UITableViewSource originalSource = (UIKit.UITableViewSource)Control.Source;
Control.Source = new MyLVSource(originalSource, e.NewElement);
}
}
public class MyLVSource : UITableViewSource
{
UITableViewSource originalSource;
ListView myListView;
public MyLVSource(UITableViewSource origSource, ListView myListV)
{
originalSource = origSource;
myListView = myListV;
}
public override nint RowsInSection(UITableView tableview, nint section)
{
return originalSource.RowsInSection(tableview, section);
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
return originalSource.GetCell(tableView, indexPath);
}
public override nfloat GetHeightForFooter(UITableView tableView, nint section)
{
return originalSource.GetHeightForFooter(tableView, section);
}
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
nfloat origHeight = originalSource.GetHeightForRow(tableView, indexPath);
// calculate your own row height here
ObservableCollection<Employee> employees = myListView.ItemsSource as ObservableCollection<Employee>;
string displayName = employees[indexPath.Row].DisplayName;
nfloat height = MeasureTextSize(displayName,UIScreen.MainScreen.Bounds.Size.Width-50,UIFont.SystemFontSize,null);
return height;
}
public nfloat MeasureTextSize(string text, double width, double fontSize, string fontName = null)
{
var nsText = new NSString(text);
var boundSize = new SizeF((float)width, float.MaxValue);
var options = NSStringDrawingOptions.UsesFontLeading | NSStringDrawingOptions.UsesLineFragmentOrigin;
if (fontName == null)
{
fontName = "HelveticaNeue";
}
var attributes = new UIStringAttributes
{
Font = UIFont.FromName(fontName, (float)fontSize)
};
var sizeF = nsText.GetBoundingRect(boundSize, options, attributes, null).Size;
//return new Xamarin.Forms.Size((double)sizeF.Width, (double)sizeF.Height);
return sizeF.Height + 5;
}
}
}
Here is the result:
I uploaded a sample here and you can check.
I'm trying to create a cardpane with custom HBox CardCells.
Issue #1
How do I set the background of this CardPane? I want it to be transparent, but it won't change from this grey color. I have tried adding styling to the node directly as well as add a custom stylesheet. I have also tried the setBackground method:
Issue #2
Taken from this SO post, I was able to add an animation for cell generation in which it fades in upwards. However, in random card inserts, different cells lose the node that I have embedded in that cell. I don't know if this is because of the recycling concept of these cards (based on Gluon docs) or what:
Issue #3
I created functionality such that the user can delete the cards by swiping left. However, the same issue from Issue #2 arises, but to an even greater extent in which the entire cell is missing but still taking space. If I have only one cell and swipe left, it works all the time. However when I have more than one cell (for example I have 3 cells and I delete the 2nd cell), things get broken, event handlers for cells get removed, swiping left on one cell starts the animation on a cell below it, etc. Is there a way I can perform this functionality or is my best bet to just get rid of the CardPane and use a combination of VBox and HBox elements?
private void addToCardPane(CustomCard newCard) {
ObservableList<Node> items = cardpane.getItems();
boolean override = false;
for (int i = 0; i < cardpane.getItems().size(); i++) {
CustomCard box = (CustomCard) items.get(i);
if (box.checkEquality(newCard)) {
box.increaseNumber(newCard);
override = true;
break;
}
}
if (override == false) {
cardpane.getItems().add(newCard);
cardpane.layout();
VirtualFlow vf = (VirtualFlow) cardpane.lookup(".virtual-flow");
Node cell = vf.getCell(cardpane.getItems().size() - 1);
cell.setTranslateX(0);
cell.setOpacity(1.0);
if (!cardpane.lookup(".scroll-bar").isVisible()) {
FadeInUpTransition f = new FadeInUpTransition(cell);
f.setRate(2);
f.play();
} else {
PauseTransition p = new PauseTransition(Duration.millis(20));
p.setOnFinished(e -> {
vf.getCell(cardpane.getItems().size() - 1).setOpacity(0);
vf.show(cardpane.getItems().size() - 1);
FadeTransition f = new FadeTransition();
f.setDuration(Duration.seconds(1));
f.setFromValue(0);
f.setToValue(1);
f.setNode(vf.getCell(cardpane.getItems().size() - 1));
f.setOnFinished(t -> {
});
f.play();
});
p.play();
}
}
initializeDeletionLogic();
}
private void initializeDeletionLogic() {
VirtualFlow vf = (VirtualFlow) cardpane.lookup(".virtual-flow");
for (int i = 0; i < cardpane.getItems().size(); i++) {
CustomCard card = (CustomCard ) cardpane.getItems().get(i);
Node cell2 = vf.getCell(i);
addRemovalLogicForCell(card, cell2);
}
}
private static double initX = 0;
private void addRemovalLogicForCell(OpioidCard card, Node cell) {
card.setOnMousePressed(e -> {
initX = e.getX();
});
card.setOnMouseDragged(e -> {
double current = e.getX();
if (current < initX) {
if ((current - initX) < 0 && (current - initX) > -50) {
cell.setTranslateX(current - initX);
}
}
});
card.setOnMouseReleased(e -> {
double current = e.getX();
double delta = current - initX;
System.out.println(delta);
if (delta > -50) {
int originalMillis = 500;
double ratio = (50 - delta) / 50;
int newMillis = (int) (500 * ratio);
TranslateTransition translate = new TranslateTransition(Duration.millis(newMillis));
translate.setToX(0);
translate.setNode(cell);
translate.play();
} else {
FadeTransition ft = new FadeTransition(Duration.millis(300), cell);
ft.setFromValue(1.0);
ft.setToValue(0);
TranslateTransition translateTransition
= new TranslateTransition(Duration.millis(300), cell);
translateTransition.setFromX(cell.getTranslateX());
translateTransition.setToX(-400);
ParallelTransition parallel = new ParallelTransition();
parallel.getChildren().addAll(ft, translateTransition);
parallel.setOnFinished(evt -> {
removeCard(card);
ObservableList<CustomCard > cells = FXCollections.observableArrayList();
for(int i = 0; i < this.cardpane.getItems().size(); i++){
cells.add((CustomCard )this.cardpane.getItems().get(i));
}
this.cardpane.getItems().clear();
for(int i = 0; i < cells.size(); i++){
this.cardpane.getItems().add(cells.get(i));
}
initializeDeletionLogic();
initX = 0;
});
parallel.play();
}
});
}
private void removeCard(OpioidCard card) {
for (int i = 0; i < cardpane.getItems().size(); i++) {
if (cardpane.getItems().get(i) == card) {
cardpane.getItems().remove(i);
updateNumber(this.totalNumber);
break;
}
}
for (int i = 0; i < dataList.size(); i++) {
if (dataList.get(i).getName().equalsIgnoreCase(card.getName())) {
dataList.remove(i);
}
}
this.cardpane.layout();
initializeDeletionLogic();
}
WORKING DEMO OF ISSUE:
package com.mobiletestapp;
import com.gluonhq.charm.glisten.animation.FadeInUpTransition;
import com.gluonhq.charm.glisten.control.AppBar;
import com.gluonhq.charm.glisten.control.CardCell;
import com.gluonhq.charm.glisten.control.CardPane;
import com.gluonhq.charm.glisten.mvc.View;
import com.gluonhq.charm.glisten.visual.MaterialDesignIcon;
import com.sun.javafx.scene.control.skin.VirtualFlow;
import javafx.animation.FadeTransition;
import javafx.animation.ParallelTransition;
import javafx.animation.PauseTransition;
import javafx.animation.TranslateTransition;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
public class BasicView extends View {
class CustomCard extends StackPane{
public CustomCard(String text){
this.getChildren().add(new Label(text));
}
}
private static double initX = 0;
private static void addRemovalLogicForCell(CustomCard card, Node cell) {
card.setOnMousePressed(e -> {
initX = e.getX();
});
card.setOnMouseDragged(e -> {
double current = e.getX();
if (current < initX) {
if ((current - initX) < 0 && (current - initX) > -50) {
cell.setTranslateX(current - initX);
}
}
});
card.setOnMouseReleased(e -> {
double current = e.getX();
double delta = current - initX;
System.out.println(delta);
if (delta > -50) {
int originalMillis = 500;
double ratio = (50 - delta) / 50;
int newMillis = (int) (500 * ratio);
TranslateTransition translate = new TranslateTransition(Duration.millis(newMillis));
translate.setToX(0);
translate.setNode(cell);
translate.play();
} else {
FadeTransition ft = new FadeTransition(Duration.millis(300), cell);
ft.setFromValue(1.0);
ft.setToValue(0);
TranslateTransition translateTransition
= new TranslateTransition(Duration.millis(300), cell);
translateTransition.setFromX(cell.getTranslateX());
translateTransition.setToX(-400);
ParallelTransition parallel = new ParallelTransition();
parallel.getChildren().addAll(ft, translateTransition);
parallel.setOnFinished(evt -> {
for(int i = 0; i < cardPane.getItems().size(); i++){
if(cardPane.getItems().get(i) == card){
cardPane.getItems().remove(i);
}
}
initX = 0;
});
parallel.play();
}
});
}
private static CardPane cardPane = null;
public BasicView(String name) {
super(name);
cardPane = new CardPane();
cardPane.setCellFactory(p -> new CardCell<CustomCard>() {
#Override
public void updateItem(CustomCard item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setText(null);
setGraphic(item);
} else {
setText(null);
setGraphic(null);
}
}
});
setCenter(cardPane);
}
private static void addCard(CustomCard newCard){
cardPane.getItems().add(newCard);
cardPane.layout();
VirtualFlow vf = (VirtualFlow) cardPane.lookup(".virtual-flow");
Node cell = vf.getCell(cardPane.getItems().size() - 1);
cell.setTranslateX(0);
cell.setOpacity(1.0);
if (!cardPane.lookup(".scroll-bar").isVisible()) {
FadeInUpTransition f = new FadeInUpTransition(cell);
f.setRate(2);
f.play();
} else {
PauseTransition p = new PauseTransition(Duration.millis(20));
p.setOnFinished(e -> {
vf.getCell(cardPane.getItems().size() - 1).setOpacity(0);
vf.show(cardPane.getItems().size() - 1);
FadeTransition f = new FadeTransition();
f.setDuration(Duration.seconds(1));
f.setFromValue(0);
f.setToValue(1);
f.setNode(vf.getCell(cardPane.getItems().size() - 1));
f.setOnFinished(t -> {
});
f.play();
});
p.play();
}
addRemovalLogicForCell(newCard, cell);
}
#Override
protected void updateAppBar(AppBar appBar) {
appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("Menu")));
appBar.setTitleText("Basic View");
appBar.getActionItems().add(MaterialDesignIcon.ADD.button(e -> addCard(new CustomCard("Hello"))));
}
}
This leads to the following output when adding and swiping left for deletion:
If you check with ScenicView, you will notice that the CardPane holds a CharmListView control, which in terms uses an inner ListView that takes the size of its parent.
So this should work:
.card-pane > .charm-list-view > .list-view {
-fx-background-color: transparent;
}
As I mentioned, the control is based on a ListView, so the way to provide cells is using the cell factory. As you can read in the control's JavaDoc:
The CardPane is prepared for a big number of items by reusing its cards.
A developer may personalize cell creation by specifying a cell factory through cellFactoryProperty(). The default cell factory is prepared to accept objects from classes that extend Node or other classes that don't extend from Node, in the latter case the card text will be given by the Object.toString() implementation of the object.
If you are not using it yet, consider using something like this:
cardPane.setCellFactory(p -> new CardCell<T>() {
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setText(null);
setGraphic(createContent(item));
} else {
setText(null);
setGraphic(null);
}
}
});
This should manage for you the cards layout, avoiding blank cells or wrong reuse of them.
As for the animation, there shouldn't be a problem in using it.
For swipe animations, the Comments2.0 sample provides a similar use case: A ListView where each cell uses a SlidingListTile. Have a look at its implementation.
You should be able to reuse it with the CardPane.
Try it out, and if you still have issues, post a working sample here (or provide a link), so we can reproduce them.
EDIT
Based on the posted code, a comment related to how the factory cell should be set:
All the JavaFX controls using cells (like ListView or TableView), and also the Gluon CardPane, follow the MVC pattern:
Model. The control is bound to a model, using an observable list of items of that model. In the case of the sample, a String, or any regular POJO, or, as the preferred choice, a JavaFX bean (with observable properties).
So in this case, you should have:
CardPane<String> cardPane = new CardPane<>();
View. The control has a method to set how the cell renders the model, the cellFactory. This factory can define just text, or any graphic node, like your CustomCard.
In this case, you should have:
cardPane.setCellFactory(p -> new CardCell<String>() {
private final CustomCard card;
{
card = new CustomCard();
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
card.setText(item);
setGraphic(card);
setText(null);
} else {
setGraphic(null);
setText(null);
}
}
});
where:
class CustomCard extends StackPane {
private final Label label;
public CustomCard(){
label = new Label();
getChildren().add(label);
}
public void setText(String text) {
label.setText(text);
}
}
Internally, the control uses a VirtualFlow that manages to reuse cells, and only modify the content (the model) when scrolling.
As you can see in the cell factory, now you'll iterate over the model (String), while the CustomCard remains the same, and only the content its updated.
Using this approach doesn't present any of the issues you have described, at least when adding cells.
EDIT 2
I've come up with a solution that works fine for me and should solve all the issues mentioned. Besides what was mentioned before, it is also required restoring the transformations applied to the CustomCard in the updateItem callbacks.
public class BasicView extends View {
private final CardPane<String> cardPane;
public BasicView(String name) {
super(name);
cardPane = new CardPane<>();
cardPane.setCellFactory(p -> new CardCell<String>() {
private final CustomCard card;
private final HBox box;
{
card = new CustomCard();
card.setMaxWidth(Double.MAX_VALUE);
card.prefWidthProperty().bind(widthProperty());
box = new HBox(card);
box.setAlignment(Pos.CENTER);
box.setStyle("-fx-background-color: grey");
addRemovalLogicForCell(card);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
card.setText(item);
card.setTranslateX(0);
card.setOpacity(1.0);
setGraphic(box);
setText(null);
} else {
setGraphic(null);
setText(null);
}
}
});
setCenter(cardPane);
}
class CustomCard extends StackPane {
private final Label label;
public CustomCard(){
label = new Label();
label.setStyle("-fx-font-size: 20;");
getChildren().add(label);
setStyle("-fx-padding: 20; -fx-background-color: white");
setPrefHeight(100);
}
public void setText(String text) {
label.setText(text);
}
public String getText() {
return label.getText();
}
}
private double initX = 0;
private void addRemovalLogicForCell(CustomCard card) {
card.setOnMousePressed(e -> {
initX = e.getX();
});
card.setOnMouseDragged(e -> {
double current = e.getX();
if ((current - initX) < 0 && (current - initX) > -50) {
card.setTranslateX(current - initX);
}
});
card.setOnMouseReleased(e -> {
double current = e.getX();
double delta = current - initX;
if (delta < 50) {
if (delta > -50) {
int originalMillis = 500;
double ratio = (50 - delta) / 50;
int newMillis = (int) (500 * ratio);
TranslateTransition translate = new TranslateTransition(Duration.millis(newMillis));
translate.setToX(0);
translate.setNode(card);
translate.play();
} else {
FadeTransition ft = new FadeTransition(Duration.millis(300), card);
ft.setFromValue(1.0);
ft.setToValue(0);
TranslateTransition translateTransition
= new TranslateTransition(Duration.millis(300), card);
translateTransition.setFromX(card.getTranslateX());
translateTransition.setToX(-400);
ParallelTransition parallel = new ParallelTransition();
parallel.getChildren().addAll(ft, translateTransition);
parallel.setOnFinished(evt -> {
cardPane.getItems().remove(card.getText());
initX = 0;
});
parallel.play();
}
}
});
}
private void addCard(String newCard){
cardPane.getItems().add(newCard);
cardPane.layout();
VirtualFlow vf = (VirtualFlow) cardPane.lookup(".virtual-flow");
IndexedCell cell = vf.getCell(cardPane.getItems().size() - 1);
cell.setTranslateX(0);
cell.setOpacity(0);
if (! cardPane.lookup(".scroll-bar").isVisible()) {
FadeInUpTransition f = new FadeInUpTransition(cell, true);
f.setRate(2);
f.play();
} else {
PauseTransition p = new PauseTransition(Duration.millis(20));
p.setOnFinished(e -> {
vf.show(cardPane.getItems().size() - 1);
FadeInTransition f = new FadeInTransition(cell);
f.setRate(2);
f.play();
});
p.play();
}
}
#Override
protected void updateAppBar(AppBar appBar) {
appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> System.out.println("Menu")));
appBar.setTitleText("Basic View");
appBar.getActionItems().add(MaterialDesignIcon.ADD.button(e -> addCard("Hello #" + new Random().nextInt(100))));
}
}
I need to make directed graph from undirected. I can draw line-Edge, but I don't know how to make arrow:
public class Edge extends Group {
protected Cell source;
protected Cell target;
Line line;
public Edge(Cell source, Cell target) {
this.source = source;
this.target = target;
source.addCellChild(target);
target.addCellParent(source);
line = new Line();
line.startXProperty().bind(source.layoutXProperty().add(source.getBoundsInParent().getWidth() / 2.0));
line.startYProperty().bind(source.layoutYProperty().add(source.getBoundsInParent().getHeight() / 2.0));
line.endXProperty().bind(target.layoutXProperty().add( target.getBoundsInParent().getWidth() / 2.0));
line.endYProperty().bind(target.layoutYProperty().add( target.getBoundsInParent().getHeight() / 2.0));
getChildren().addAll(line);
}
You need to add 2 more lines to make an arrow head (or a Polygon with the same points for a filled arrow head).
Note that the direction of the arrow can be determined based on the difference between start and end of the line ends of the "main" connection. One end of each of the lines that make up the arrow head need to be at the same coordinates as the end of the main line. The other end can be calculated by combining a part in direction of the main line and a part ortogonal to the main line:
public class Arrow extends Group {
private final Line line;
public Arrow() {
this(new Line(), new Line(), new Line());
}
private static final double arrowLength = 20;
private static final double arrowWidth = 7;
private Arrow(Line line, Line arrow1, Line arrow2) {
super(line, arrow1, arrow2);
this.line = line;
InvalidationListener updater = o -> {
double ex = getEndX();
double ey = getEndY();
double sx = getStartX();
double sy = getStartY();
arrow1.setEndX(ex);
arrow1.setEndY(ey);
arrow2.setEndX(ex);
arrow2.setEndY(ey);
if (ex == sx && ey == sy) {
// arrow parts of length 0
arrow1.setStartX(ex);
arrow1.setStartY(ey);
arrow2.setStartX(ex);
arrow2.setStartY(ey);
} else {
double factor = arrowLength / Math.hypot(sx-ex, sy-ey);
double factorO = arrowWidth / Math.hypot(sx-ex, sy-ey);
// part in direction of main line
double dx = (sx - ex) * factor;
double dy = (sy - ey) * factor;
// part ortogonal to main line
double ox = (sx - ex) * factorO;
double oy = (sy - ey) * factorO;
arrow1.setStartX(ex + dx - oy);
arrow1.setStartY(ey + dy + ox);
arrow2.setStartX(ex + dx + oy);
arrow2.setStartY(ey + dy - ox);
}
};
// add updater to properties
startXProperty().addListener(updater);
startYProperty().addListener(updater);
endXProperty().addListener(updater);
endYProperty().addListener(updater);
updater.invalidated(null);
}
// start/end properties
public final void setStartX(double value) {
line.setStartX(value);
}
public final double getStartX() {
return line.getStartX();
}
public final DoubleProperty startXProperty() {
return line.startXProperty();
}
public final void setStartY(double value) {
line.setStartY(value);
}
public final double getStartY() {
return line.getStartY();
}
public final DoubleProperty startYProperty() {
return line.startYProperty();
}
public final void setEndX(double value) {
line.setEndX(value);
}
public final double getEndX() {
return line.getEndX();
}
public final DoubleProperty endXProperty() {
return line.endXProperty();
}
public final void setEndY(double value) {
line.setEndY(value);
}
public final double getEndY() {
return line.getEndY();
}
public final DoubleProperty endYProperty() {
return line.endYProperty();
}
}
Use
#Override
public void start(Stage primaryStage) {
Pane root = new Pane();
Arrow arrow = new Arrow();
root.getChildren().add(arrow);
root.setOnMouseClicked(evt -> {
switch (evt.getButton()) {
case PRIMARY:
// set pos of end with arrow head
arrow.setEndX(evt.getX());
arrow.setEndY(evt.getY());
break;
case SECONDARY:
// set pos of end without arrow head
arrow.setStartX(evt.getX());
arrow.setStartY(evt.getY());
break;
}
});
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
I need to make directed graph from undirected. I can draw line-Edge, but I don't know how to make arrow:
public class Edge extends Group {
protected Cell source;
protected Cell target;
Line line;
public Edge(Cell source, Cell target) {
this.source = source;
this.target = target;
source.addCellChild(target);
target.addCellParent(source);
line = new Line();
line.startXProperty().bind(source.layoutXProperty().add(source.getBoundsInParent().getWidth() / 2.0));
line.startYProperty().bind(source.layoutYProperty().add(source.getBoundsInParent().getHeight() / 2.0));
line.endXProperty().bind(target.layoutXProperty().add( target.getBoundsInParent().getWidth() / 2.0));
line.endYProperty().bind(target.layoutYProperty().add( target.getBoundsInParent().getHeight() / 2.0));
getChildren().addAll(line);
}
You need to add 2 more lines to make an arrow head (or a Polygon with the same points for a filled arrow head).
Note that the direction of the arrow can be determined based on the difference between start and end of the line ends of the "main" connection. One end of each of the lines that make up the arrow head need to be at the same coordinates as the end of the main line. The other end can be calculated by combining a part in direction of the main line and a part ortogonal to the main line:
public class Arrow extends Group {
private final Line line;
public Arrow() {
this(new Line(), new Line(), new Line());
}
private static final double arrowLength = 20;
private static final double arrowWidth = 7;
private Arrow(Line line, Line arrow1, Line arrow2) {
super(line, arrow1, arrow2);
this.line = line;
InvalidationListener updater = o -> {
double ex = getEndX();
double ey = getEndY();
double sx = getStartX();
double sy = getStartY();
arrow1.setEndX(ex);
arrow1.setEndY(ey);
arrow2.setEndX(ex);
arrow2.setEndY(ey);
if (ex == sx && ey == sy) {
// arrow parts of length 0
arrow1.setStartX(ex);
arrow1.setStartY(ey);
arrow2.setStartX(ex);
arrow2.setStartY(ey);
} else {
double factor = arrowLength / Math.hypot(sx-ex, sy-ey);
double factorO = arrowWidth / Math.hypot(sx-ex, sy-ey);
// part in direction of main line
double dx = (sx - ex) * factor;
double dy = (sy - ey) * factor;
// part ortogonal to main line
double ox = (sx - ex) * factorO;
double oy = (sy - ey) * factorO;
arrow1.setStartX(ex + dx - oy);
arrow1.setStartY(ey + dy + ox);
arrow2.setStartX(ex + dx + oy);
arrow2.setStartY(ey + dy - ox);
}
};
// add updater to properties
startXProperty().addListener(updater);
startYProperty().addListener(updater);
endXProperty().addListener(updater);
endYProperty().addListener(updater);
updater.invalidated(null);
}
// start/end properties
public final void setStartX(double value) {
line.setStartX(value);
}
public final double getStartX() {
return line.getStartX();
}
public final DoubleProperty startXProperty() {
return line.startXProperty();
}
public final void setStartY(double value) {
line.setStartY(value);
}
public final double getStartY() {
return line.getStartY();
}
public final DoubleProperty startYProperty() {
return line.startYProperty();
}
public final void setEndX(double value) {
line.setEndX(value);
}
public final double getEndX() {
return line.getEndX();
}
public final DoubleProperty endXProperty() {
return line.endXProperty();
}
public final void setEndY(double value) {
line.setEndY(value);
}
public final double getEndY() {
return line.getEndY();
}
public final DoubleProperty endYProperty() {
return line.endYProperty();
}
}
Use
#Override
public void start(Stage primaryStage) {
Pane root = new Pane();
Arrow arrow = new Arrow();
root.getChildren().add(arrow);
root.setOnMouseClicked(evt -> {
switch (evt.getButton()) {
case PRIMARY:
// set pos of end with arrow head
arrow.setEndX(evt.getX());
arrow.setEndY(evt.getY());
break;
case SECONDARY:
// set pos of end without arrow head
arrow.setStartX(evt.getX());
arrow.setStartY(evt.getY());
break;
}
});
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
I am newbie to android please help me, is their any possibility to add swipe action in vertical scroll view of activity screen.I am trying hard, but not getting...
I just converted vertical scroll view to Listview, Its works like a charm... Thanks to omid nazifi and wwyt, for more u can see this link Gesture in listview android
public class MainActivity extends ListActivity {
private OnTouchListener gestureListener;
private GestureDetector gestureDetector;
private int REL_SWIPE_MIN_DISTANCE;
private int REL_SWIPE_MAX_OFF_PATH;
private int REL_SWIPE_THRESHOLD_VELOCITY;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// As paiego pointed out, it's better to use density-aware measurements.
DisplayMetrics dm = getResources().getDisplayMetrics();
REL_SWIPE_MIN_DISTANCE = (int)(1.0f * dm.densityDpi / 160.0f + 0.5);
REL_SWIPE_MAX_OFF_PATH = (int)(250.0f * dm.densityDpi / 160.0f + 0.5);
REL_SWIPE_THRESHOLD_VELOCITY = (int)(200.0f * dm.densityDpi / 160.0f + 0.5);
ListView lv = getListView();
lv.setAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,
m_Starbucks));
final GestureDetector gestureDetector = new GestureDetector(new MyGestureDetector());
View.OnTouchListener gestureListener = new View.OnTouchListener() {
public boolean onTouch(View v, MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}};
lv.setOnTouchListener(gestureListener);
// Long-click still works in the usual way.
lv.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
String str = MessageFormat.format("Item long clicked = {0,number}", position);
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
return true;
}
});
/*lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
String str = MessageFormat.format("Item #extra clicked = {0,number}", position);
Toast.makeText(MainActivity.this, str, Toast.LENGTH_SHORT).show();
}
});*/
}
// Do not use LitView.setOnItemClickListener(). Instead, I override
// SimpleOnGestureListener.onSingleTapUp() method, and it will call to this method when
// it detects a tap-up event.
private void myOnItemClick(int position, View v) {
String str = MessageFormat.format("Item clicked = {0,number}", position);
Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
}
private void onLTRFling() {
Toast.makeText(this, "Left-to-right fling", Toast.LENGTH_SHORT).show();
}
private void onRTLFling() {
Toast.makeText(this, "Right-to-left fling", Toast.LENGTH_SHORT).show();
}
class MyGestureDetector extends SimpleOnGestureListener{
// Detect a single-click and call my own handler.
#Override
public boolean onSingleTapUp(MotionEvent e) {
View lv = (View)getListView();
int pos = ((AbsListView) lv).pointToPosition((int)e.getX(), (int)e.getY());
myOnItemClick(pos,lv);
return false;
}
#Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (Math.abs(e1.getY() - e2.getY()) > REL_SWIPE_MAX_OFF_PATH)
return false;
if(e1.getX() - e2.getX() > REL_SWIPE_MIN_DISTANCE &&
Math.abs(velocityX) > REL_SWIPE_THRESHOLD_VELOCITY) {
onRTLFling();
} else if (e2.getX() - e1.getX() > REL_SWIPE_MIN_DISTANCE &&
Math.abs(velocityX) > REL_SWIPE_THRESHOLD_VELOCITY) {
onLTRFling();
}
return false;
}
}
private static final String[] m_Starbucks = {
"Latte", "Cappuccino", "Caramel Macchiato", "Americano", "Mocha", "White Mocha",
"Mocha Valencia", "Cinnamon Spice Mocha", "Toffee Nut Latte", "Espresso",
"Espresso Macchiato", "Espresso Con Panna"
};
}