I noticed that after using the .HeightRequest property on a View in Xamarin.Forms that a method I have assigned to the SizeChanged event is no longer called.
The content within the View that had called .HeightRequest is a Label that contains text which can be zoomed in. When zooming in, the text now exceeds the "bounds" of the View's height, and the height is no longer adjusting as it once was.
public ExpandableEntryView(...)
{
InitializeComponent();
// my beautiful code
SizeChanged += ExpandableEntryView_SizeChanged;
UpdateUI();
}
private void ExpandableEntryView_SizeChanged(object sender, EventArgs e)
{
// this is never called once .HeightRequest is assigned a value
}
private void SomeFunction() {
sectionsStackLayout.HeightRequest = 0; // this causes SizeChanged to no longer work
}
I essentially need this View to minimize its height to 0, and then return to its original height when I tell it to. I am also using IsVisible, but the Height property is required for an animation I am trying to do.
Thank you in advance.
Nevermind, I was able to figure it out. The reason why the SizeChanged event is no longer called is because the view is no longer responsible for keeping track of its size after assigning a value to HeightRequest. So, if you would like for your View to return to being able to determine its own size once again, (i.e. if a child element in that View happens to change size/scale) you need to assign the HeightRequest to -1. This will then reenable the SizeChanged event.
Hope this helps somebody!
Related
I have implemented a custom clickable label class in Xamarin.Forms along with a custom renderer, that adds a RippleDrawable as the controls Foreground. I am creating the RippleDrawable with the following code:
public static Drawable CreateRippleDrawable(Context context)
{
var typedValue = new TypedValue();
context.Theme.ResolveAttribute(Resource.Attribute.SelectableItemBackground, typedValue, true);
var rippleDrawable = context.Resources.GetDrawable(typedValue.ResourceId, context.Theme);
return rippleDrawable;
}
In my custom renderer I assign the drawable
this.Control.Foreground = DrawableHelper.CreateRippleDrawable(this.Context);
and update the ripple when the user touches the control
private void LinkLabelRenderer_Touch(object sender, TouchEventArgs e)
{
if (e.Event.Action == MotionEventActions.Down)
{
this.Pressed = true;
}
if (e.Event.Action == MotionEventActions.Cancel)
{
this.Pressed = false;
}
if (e.Event.Action == MotionEventActions.Up)
{
this.Ripple.SetHotspot(e.Event.GetX(), e.Event.GetY());
this.Pressed = false;
// raise the event of the Xamarin.Forms control
}
}
Now, whenever I click the control, the ripple will be shown, which is the expected behavior, but if I touch (tap or long-press) the parents of the control (e.g. the StackLayout, Grid or whatever layout contains the label, including their parent Layout, Page or View) the ripple animation will be triggered. Anyway, the event handler LinkLabelRenderer_Touch in not called in this case, only when the actual control is touched.
I can work around this behavior by adding an empty GestureRecognizer to the respective parent(s), but I really dislike this solution, because this is but a hack. And to make things worse it is a hack I'll always have to remember whenever I use the control.
How can I prevent the RippleDrawable being shown when the parent is touched?
Turned out I got things fundamentally wrong. Subscribing the Touch event is not the way to go. I had to make the control clickable and subscribe the Click event
this.Control.Clickable = true;
this.Click += LinkLabelRenderer_OnClick;
There is no need to handle all that RippleTouch stuff the way I did (via the Touch event) but could let android handle things for me.
I'm writing a program using Qt 4.8 that displays a table (QTableWidget) filled with filenames and file's params. First an user adds files to the list and then clicks process. The code itself updates the contents of the table with simple progress description. I want the table by default to be scrolled automatically to show the last processed file and that code is ready.
If I want to scroll it by hand the widget is being scrolled automatically as soon as something changes moving the viewport to the last element. I want to be able to override the automated scroll if I detect that it was the user who wanted to change view.
This behavior can be seen in many terminal emulator programs. When there's a new line added the view is scrolled but when user forces the terminal to see some previous lines the terminal does not try to scroll down.
How could I do that?
Solution:
I created an object which filters event processed by my QTableWidget and QScrollBar embedded inside. If I spot the event that should turn off automatic scrolling I just set a flag and stop scrolling view if that flag is set.
Everything is implemented inside tableController class. Here are parts of three crucial methods.
bool tableController::eventFilter(QObject* object, QEvent* event)
{
switch (event->type())
{
case QEvent::KeyPress:
case QEvent::KeyRelease:
case QEvent::Wheel:
case QEvent::MouseButtonDblClick:
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
_autoScrollEnabled = false;
default:
break;
}
return QObject::eventFilter(object, event);
}
void tableController::changeFile(int idx)
{
[...]
if (_autoScrollEnabled)
{
QTableWidgetItem* s = _table.item(_engine.getLastProcessed(), 1);
_table.scrollToItem(s);
}
[...]
}
void tableController::tableController()
{
[...]
_autoScrollEnabled = true;
_table.installEventFilter(this);
_table.verticalTableScrollbar()->installEventFilter(this);
[...]
}
Thanks for all the help. I hope somebody will find it useful :)
Subclass QTableWidget and overload its wheelEvent. You can use the parameters of the supplied QWheelEvent object in order to determine if the user scrolled up or down.
Then use a simple boolean flag which is set (or reset) in your wheelEvent override. The method which is responsible for calling scrollToBottom() should then consider this boolean flag.
You will have to find a way to figure out when to set or reset that flag, e.g. always set it when the user scrolls up and reset it when the user scrolls down and the currently displayed area is at the bottom.
connect(_table->view()->verticalScrollBar(), &QAbstractSlider::actionTriggered, this, [this](int) {
_autoScrollEnabled = false;
});
I have a simple list and a background refresh protocol.
When the list is scrolled down, the refresh scrolls it back to the top. I want to stop this.
I have tried catching the COLLECTION_CHANGE event and
validateNow(); // try to get the component to reset to the new data
list.ensureIndexIsVisible(previousIndex); // actually, I search for the previous data id in the IList, but that's not important
This fails because the list resets itself after the change (in DataGroup.commitProperties).
I hate to use a Timer, ENTER_FRAME, or callLater(), but I cannot seem to figure out a way.
The only other alternatives I can see is sub-classing the List so it can catch the dataProviderChanged event the DataGroup in the skin is throwing.
Any ideas?
Actually MUCH better solution to this is to extend DataGroup. You need to override this.
All the solutions here create a flicker as the scrollbar gets resetted to 0 and the it's set back to the previous value. That looks wrong. This solution works without any flicker and the best of all, you just change DataGroup to FixedDataGroup in your code and it works, no other changes in code are needed ;).
Enjoy guys.
public class FixedDataGroup extends spark.components.DataGroup
{
private var _dataProviderChanged:Boolean;
private var _lastScrollPosition:Number = 0;
public function FixedDataGroup()
{
super();
}
override public function set dataProvider(value:IList):void
{
if ( this.dataProvider != null && value != this.dataProvider )
{
dataProvider.removeEventListener(CollectionEvent.COLLECTION_CHANGE, onDataProviderChanged);
}
super.dataProvider = value;
if ( value != null )
{
value.addEventListener(CollectionEvent.COLLECTION_CHANGE, onDataProviderChanged);
}
}
override protected function commitProperties():void
{
var lastScrollPosition:Number = _lastScrollPosition;
super.commitProperties();
if ( _dataProviderChanged )
{
verticalScrollPosition = lastScrollPosition;
}
}
private function onDataProviderChanged(e:CollectionEvent):void
{
_dataProviderChanged = true;
invalidateProperties();
}
override public function set verticalScrollPosition(value:Number):void
{
super.verticalScrollPosition = value;
_lastScrollPosition = value;
}
}
I ll try to explain my approach...If you are still unsure let me know and I ll give you the source code as well.
1) Create a variable to store the current scroll position of the viewport.
2) Add Event listener for Event.CHANGE and MouseEvent.MOUSE_WHEEL on the scroller and update the variable created in step 1 with the current scroll position;
3) Add a event listener on your viewport for FlexEvent.UpdateComplete and set the scroll position to the variable stored.
In a nutshell, what we are doing is to have the scroll position stored in variable every time user interacts with it and when our viewport is updated (due to dataprovider change) we just set the scroll position we have stored previously in the variable.
I have faced this problem before and solved it by using a data proxy pattern with a matcher. Write a matcher for your collection that supports your list by updating only changed objects and by updating only attributes for existing objects. The goal is to avoid creation of new objects when your data source refreshes.
When you have new data for the list (after a refresh), loop through your list of new data objects, copying attributes from these objects into the objects in the collection supporting your list. Typically you will match the objects based on id. Any objects in the new list that did not exist in the old one get added. Your scroll position will normally not change and any selections are usually kept.
Here is an example.
for each(newObject:Object in newArrayValues){
var found:Boolean = false;
for each(oldObject:Object in oldArrayValues){
if(oldObject.id == newObject.id){
found = true;
oldObject.myAttribute = newObject.myAttribute;
oldObject.myAttribute2 = newObject.myAttribute2;
}
}
if(!found){
oldArrayValues.addItem(newObject);
}
}
My solution for this problem was targeting a specific situation, but it has the advantage of being very simple so perhaps you can draw something that fits your needs from it. Since I don't know exactly what issue you're trying to solve I'll give you a description of mine:
I had a List that was progressively loading data from the server. When the user scrolled down and the next batch of items would be added to the dataprovider, the scrollposition would jump back to the start.
The solution for this was as simple as stopping the propagation of the COLLECTION_CHANGE event so that the List wouldn't catch it.
myDataProvider.addEventListener(
CollectionEvent.COLLECTION_CHANGE, preventRefresh
);
private function preventRefresh(event:CollectionEvent):void {
event.stopImmediatePropagation();
}
You have to know that this effectively prevents a redraw of the List component, hence any added items would not be shown. This was not an issue for me since the items would be added at the end of the List (outside the viewport) and when the user would scroll, the List would automatically be redrawn and the new items would be displayed. Perhaps in your situation you can force the redraw if need be.
When all items had been loaded I could then remove the event listener and return to the normal behavior of the List component.
I have a Datagrid filled with a table. Now the vertical scrollbar shows up because the table doesn't fit. That's fine so far. Now in the last column I have defined a Button in the xaml file. All these buttons have the same callback, but I can distinguish from the selectedIndex of the table what this callback should do. Because clicking the button automatically also selects the line in the DataGrid where this button lives. That's fine so far. Now in my app, for some rows I want to disable the Button, because it has no meaning for that specific row. So what I did is take a subscription on event Load of each Button and let the callback set the MaxWidth = 0, if the button has no meaning. This works fine too, but only initially. As soon as I start dragging the scrollbar, at random places in the Button column buttons show up, or wrong buttons get MaxWidth = 0. I have the strong feeling that cells that scrolled out at the top are being reused at the bottom, but I don't get an event, or at least I don't know which event I should subscribe on. I don't know how to identify the scrollbar. Has anyone a suggestion to tackle this problem?
I finally found a solution to this problem myself, and I will post it for the record.
The event you should subscribe on is LoadingRow (generated by the DataGrid).
In the callback
void TableLoadingRow(object sender, DataGridRowEventArgs e)
you can identify an element in a cell by using VisualTreeHelper for instance as follows:
private void ButtonSetMaxWidth(DependencyObject reference, int maxWidth)
{
if (reference != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(reference); i++)
{
var child = VisualTreeHelper.GetChild(reference, i);
if (child.GetType() == typeof(Button))
{
Button b = (Button)child;
if (b.Name == "TheNameOfTheButtonInTheXAML")
{
b.MaxWidth = maxWidth;
return;
}
}
ButtonSetMaxWidth(child, maxWidth);
}
}
return;
}
I have a DataGrid that I have bound to a property:
<cd:DataGrid
Name="myDataGrid"
ItemsSource="{Binding Mode=OneWay,Path=Thingies}"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
...
When the Thingies property changes, once all rows in the DataGrid have been populated with the new contents of Thingies, I want the DataGrid to scroll to the bottom row.
In WinForms, I would have done this by subscribing to the DataBindingComplete event. MSDN Forums contains several suggestions on how to do this with Silverlight 4.0 but they range from completely evil to just plain fugly:
start a 100ms timer on load, and scroll when it elapses
count rows as they're added, and scroll to the bottom when the number of added rows equals the number of entities in the data source
Is there an idiomatic, elegant way of doing what I want in Silverlight 4.0?
I stumbled upon this while searching for a resolution to the same problem. I was finding that when I attempted to scroll the selected item into view after filter and sort changes that I frequently received a run time error (index out of bounds). I knew instinctively that this was because the grid was not populated at that particular moment.
Aaron's suggestion worked for me. When the grid is defined, I add an event listener:
_TheGrid.LayoutUpdated += (sender, args) => TheGrid.ScrollIntoView(TheGrid.SelectedItem, TheGrid.CurrentColumn);
This solved my problem, and seems to silently exit when the parameters are null, too.
Why not derive from DataGrid and simply create your own ItemsSourceChanged event?
public class DataGridExtended : DataGrid
{
public delegate void ItemsSourceChangedHandler(object sender, EventArgs e);
public event ItemsSourceChangedHandler ItemSourceChanged;
public new System.Collections.IEnumerable ItemsSource
{
get { return base.ItemsSource; }
set
{
base.ItemsSource = value;
EventArgs e = new EventArgs();
OnItemsSourceChanged(e);
}
}
protected virtual void OnItemsSourceChanged(EventArgs e)
{
if (ItemSourceChanged != null)
ItemSourceChanged(this, e);
}
}
Use the ScrollIntoView method for achieving this.
myDataGrid.ItemSource = Thingies;
myDataGrid.UpdateLayout();
myDataGrid.ScrollIntoView(MyObservableCollection[MyObservableCollection.Count - 1], myDataGrid.Columns[1]);
You don't need to have any special event for this.
I think the nice way to do it, in xaml, is to have the binding NotifyOnTargetUpdated=true, and then you can hook the TargetUpdated to any event of your choice.
<ThisControl BindedProperty="{Binding xxx, NotifyOnTargetUpdated=true}"
TargetUpdated="BindingEndedHandler">