In my Nativescript Angular application, I have a TextView within a ScrollView, defined as such:
<ScrollView orientation="vertical" height="70%" width="100%" style="margin-bottom: 1%; background-color:white" (loaded)="onScrollerLoaded($event)">
<TextView
id="terminal" [text]="terminalText" editable="false"
style="color: whitesmoke; background-color: black; font-size: 8%; font-family: monospace" height="100%"
(tap)="onTap($event)" (loaded)="onTerminalLoaded($event)">
</TextView>
</ScrollView>
The purpose of this element is to act as a terminal, and is rapidly printing incoming messages from a bluetooth device.
Currently, the ScrollView is scrolling back to the top whenever I add some text to the terminalText variable, to which the TextView is bound. I would like to be able to keep the ScrollView at the bottom of the TextView.
A few notes:
I am adding text to the terminalText variable within my associated component class through this method:
public appendToTerminal(appendStr){
this.terminalText += appendStr;
}
I have tried implementing the following code that would execute once the ScrollView loads:
private scrollIntervalId;
onScrollerLoaded(data: EventData){
if(!this.scrollIntervalId){
this.scrollIntervalId = setInterval(()=>{
this.ngZone.run(() =>
(data.object as any).scrollToVerticalOffset((data.object as any).scrollableHeight, false)
)
}, 10);
}
}
(This attempt is based on an explanation given here
I have only tried this on an Android device, as I do not have access to an Apple device.
you are setting TextView to fixed height 100% which is will be same as ScrollView that is why scrollableHeight will always be 0. you should use minHeight="100%".
then you can scroll programmatically to the end when you are appending text to terminal text this.terminalText += appendStr.
like this
public appendToTerminal(appendStr){
this.terminalText += appendStr;
setTimeout(()=>{
scrollView.scrollToVerticalOffset(scrollView.scrollableHeight, false);
},150);
}
this will append the text then get the scrollableHeight and then will scroll to it.
here is working playground demo:https://play.nativescript.org/?template=play-ng&id=Rs0xnP&v=16
The function below can only be used in the Angular ngDoCheck() or ngAfterContentChecked() lifecycle:
// See https://angular.io/guide/lifecycle-hooks
function goDownScrollView(scrollView: object, animate: boolean = true): boolean {
let neScrollView: ScrollView = <ScrollView>getNativeElement(scrollView),
scrollableHeight: number = neScrollView.scrollableHeight;
console.log("neScrollView:", neScrollView);
console.log("neScrollView scrollableHeight:", scrollableHeight);
if (scrollableHeight > 0) {
neScrollView.scrollToVerticalOffset(scrollableHeight, animate);
return true;
} else {
return false;
}
}
An helper to always get the native element:
function getNativeElement(object: object): object {
return (object.hasOwnProperty("nativeElement")) ? object['nativeElement'] : object;
}
The scrollable height may be zero at the first pass of the lifecycle (for example, if you add elements to your ScrollView with an HTTP request). That's why you have to test the current content with new before scroll:
// See https://angular.io/api/core/ViewChild
#ViewChild("content") private _pageContent: ElementRef<ScrollView>;
public currentContent: object;
private _previousContent: object;
...
ngAfterContentChecked(): void {
if (this.currentContent != this._previousContent) {
let isScrollDown: boolean = goDownScrollView(this._pageContent);
if (isScrollDown) {
this._previousContent = this.currentContent;
}
}
}
Related
I have a custom WkWebView where I set the height of the view to the size of the html content.
This works great when I initialize it, but the problem comes when I change the source of the WkWebView to a shorter html.
I've already encountered this problem in the android version of my app and I fixed that by setting the HeightRequest to 0 before EvaluateJavaScriptAsync. In that way the view will always get bigger.
I tried the same thing with iOS but it keeps the highest content I had .
So for example :
If I set the source of the WkWebView to a html file that is 200px height and change it to a html file that is 1000px, it works fine and I can see all the content. BUT, if I try to go back to my 200px html file, I get a 800px blank space underneath and keep the 1000px height.
The goal is to always have the height of the WKWebView to adapt to the height of its content.
Here is the current version of the custom render
namespace MyNamespace.iOS.CustomControl
{
public class AutoHeightWebViewRenderer : WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
try
{
base.OnElementChanged(e);
if (NativeView != null)
{
var webView = (WKWebView)NativeView;
NavigationDelegate = new ExtendedWKWebViewDelegate(this);
}
}
catch (Exception ex)
{
//Log the Exception
}
}
}
class ExtendedWKWebViewDelegate : WKNavigationDelegate
{
AutoHeightWebViewRenderer webViewRenderer;
public ExtendedWKWebViewDelegate(AutoHeightWebViewRenderer _webViewRenderer = null)
{
webViewRenderer = _webViewRenderer ?? new AutoHeightWebViewRenderer();
}
public override async void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
try
{
var _webView = webViewRenderer.Element as AutoHeightWebView;
if (_webView != null)
{
_webView.HeightRequest = 0d;
await Task.Delay(300);
var result = await _webView.EvaluateJavaScriptAsync("(function(){return document.body.scrollHeight;})()");
_webView.HeightRequest = Convert.ToDouble(result);
}
}
catch (Exception ex)
{
//Log the Exception
}
}
}
}
EDIT 1 :
To be clearer, I'm able to change the height of the webview, but I dont know the height because it always returned the height of the largest html displayed so far. Nomatter if I use _webView.EvaluateJavaScriptAsync("(function(){return document.body.scrollHeight;})()") or webView.ScrollView.ContentSize.Height.
EDIT 2 :
Here a little sample to help understand the problem. I got two buttons and my custom webview (initialize with a 40 HeightRequest empty html).
The first button set the Source of the webview to a 70px long HTML. The second one set the Source to a 280px long HTML.
In this example, I click on the first button than the second one and finaly back on the first button again. You see the webview getting bigger on the first 2 clicks. But then then webview should get shrunk when I choose back the first html (passing from 280px to 70px) but it keeps the 280px long.
First button (70px long)
Second button (280px long)
Back to the first button (should be 70px long instead of 280px).
The problem occured on both simulator and iOS device.
You can change the Frame of webView to change the height.
public override async void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
try
{
var _webView = webViewRenderer.Element as WebView;
if (_webView != null)
{
webView.Frame = new CGRect(webView.Frame.Location, new CGSize(webView.Frame.Size.Width, 0));
var a = webView.ScrollView.ContentSize.Height;
var b = await _webView.EvaluateJavaScriptAsync("(function(){return document.body.scrollHeight;})()");
Console.WriteLine(a);
Console.WriteLine(b);
CGRect tempRect = webView.Frame;
// for test
webView.Frame = new CGRect(tempRect.Location.X, tempRect.Location.Y, tempRect.Size.Width, float.Parse(b));
}
}
catch (Exception ex)
{
//Log the Exception
}
}
I had the same problem and the CustomNavigationDelegate would also only show the same large size, which was the Device Display size.
I found that I had set this on the init part of the XAML and code behind, which somehow overrides the later content-based calculation.
See my fix here: https://stackoverflow.com/a/62409859/3443564
I'm trying to make my WebPart responsive to the column width in my section layout.
I get the width of the bounding rectangle by calling
const width: number = this.domElement.getBoundingClientRect().width;
My render-function looks like this:
public render(): void {
const width: number = this.domElement.getBoundingClientRect().width;
this.domElement.innerHTML = `<div>${width}</div>`;
}
When I insert my WebPart into the SharePoint workbench, the number 736 is shown.
However, if I change the layout of the section from one column to something else, the number doesn't change.
What do I need to do to trigger the render function as soon as the layout (and therefor the width) changes?
To do that you can create an event listener and in your function set the state to re-render your webpart:
private handleWindowSizeChange() {
this.setState({
size: window.innerWidth
});
}
public componentDidMount() {
window.addEventListener('resize', this.handleWindowSizeChange.bind(this));
}
public componentWillUnmount() {
window.removeEventListener('resize', this.handleWindowSizeChange.bind(this));
}
I have used the ResizeObserver api to listen for the layout change in the layout.
The only problem is that it is not supported by IE, Edge, and Safari.
public componentDidMount() {
if(window.ResizeObserver) {
this.resizeObserver = new ResizeObserver(this.handleResize);
this.resizeObserver.observe(this.domElement)
}
}
public componentWillUnmount() {
if(window.ResizeObserver) {
this.resizeObserver.disconnect()
}
}
I have a UICollectionView, from this UICollectionView when a cell is selected a UITabBarController is pushed.
Navigating back and forth multiple times (around 14 times), makes the UI in ViewController to become black. Here is a screenshot to show how the UI becomes after navigating back and forth multiple times https://imgur.com/a/H3ywdnX
Any suggestion why this can happen.
This is what I call for navigation to the view
The setup of View I navigate to:
public partial class NavigationView : UITabBarController
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
SetupTabBar();
}
private void SetupTabBar()
{
//ios7 layout
if (RespondsToSelector(new Selector("edgesForExtendedLayout")))
{
EdgesForExtendedLayout = UIRectEdge.None;
}
UITabBar.Appearance.TintColor = _iosConstants.ApplicationColors.GreenTitleColor;
UIControlHelpers.SetTabBarButtonTextAttributes(_iosConstants.IphoneProjectView.BarTextColor, "Helvetica Neue", 11.0f, UIControlState.Normal);
UIControlHelpers.SetTabBarButtonTextAttributes(UIColor.White, "Helvetica Neue", 11.0f, UIControlState.Selected);
NavigationController.NavigationBar.BarTintColor = _iosConstants.General.TabBarGreenColor;
NavigationController.NavigationBar.TintColor = _iosConstants.General.TabBarGreenColor;
this.TabBar.Translucent = false;
this.TabBar.ShadowImage = null;
NavigationController.SetNavigationBarHidden(true, false);
SelectedIndex = 0;
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
TabBar.InvalidateIntrinsicContentSize();
}
}
NavigationController.PushViewController(_ctrl, true);
I have found the issue to my problem. The problem in the first place was that the UITabViewController was not the root controller, and as I found it's not correct to push a new instance of an UITabViewController.
What I am trying to do is:
I have a scrollview inside my view and it's height is like 2/9 of the parent height. Then user can translate this scrollview to make it bigger. However scrollview's size does not change obviously. So even though it is bigger scrollview's size remains the same, killing the point. I could make it bigger initially. However it won't scroll since the size is big enough not to scroll.
I don't know if was able to explain it right. Hope i did.
Regards.
Edit
-------------
Some code to explain my point further
This is the scrollview
public class TranslatableScrollView : ScrollView
{
public Action TranslateUp { get; set; }
public Action TranslateDown { get; set; }
bool SwipedUp;
public TranslatableScrollView()
{
SwipedUp = false;
Scrolled += async delegate {
if (!SwipedUp && ScrollY > 0) {
TranslateUp.Invoke ();
SwipedUp = true;
} else if (SwipedUp && ScrollY <= 0) {
TranslateDown.Invoke ();
SwipedUp = false;
}
};
}
}
And this is the code in the page
sv_footer = new TranslatableScrollView {
Content = new StackLayout {
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
Children = {
l_details,
l_place
},
}
};
sv_footer.TranslateUp += new Action (async delegate {
Parent.ForceLayout();
await cv_scrollContainer.TranslateTo(0, -transX, aSpeed, easing);
});
sv_footer.TranslateDown += new Action (async delegate {
await cv_scrollContainer.TranslateTo(0, 0, aSpeed, easing);
});
cv_scrollContainer = new ContentView {
Content = sv_footer,
VerticalOptions = LayoutOptions.Fill
};
I put the scrollview inside a contentview otherwise Its scroll indexes becomes 0 when they are translated. Ref: Xamarin Forms: ScrollView returns to begging on TranslateTo
The problem was, translateTo only changes the position of the element. I could use scaleTo after the translation. However it changes the both dimensions. Someone from Xamarin Forums suggested me to use LayoutTo which I did not know existed. With layoutTo you can change both the size and location. Giving an instance of Rectangle type.
I would like to create a layout with a fullscreen background image and some UI elements on top of it. The twist is this:
I would like the background image to swipeable like a carousel, but I would like the UI elements to stay in place. That is if I swipe the screen, the background image should slide to the side and a new image should replace it. I know about CarouselPage, but it seems to me that it won't do the trick, since a Page can have only one child which it replaces on swipe, meaning that the UI elements would be descendants of the CarouselPage and therefore would also be animated.
I am guessing I need some sort of custom renderer here, but how should I go about designing it? Should it be one fullscreen Image control replaced be another fullscreen Image control with the UI elements on top of it? And how can I do this? Or is there an all together better approach?
I am developing for iOS and Android using Xamarin.Forms.
Thanks in advance.
I don't like repeating myself much, and I think that multiple layers of actionable items can lead to confusion, but the problems appeals to me and I can see a niche for this kind of UI, so here's my take on your question.
Let's assume this is the (Xamarin.Forms.)Page you want to render with a custom carousel background:
public class FunkyPage : ContentPage
{
public IList<string> ImagePaths { get; set; }
public FunkyPage ()
{
Content = new StackLayout {
VerticalOptions = LayoutOptions.Center,
HorizontalOptions = LayoutOptions.Center,
Spacing = 12,
Children = {
new Label { Text = "Foo" },
new Label { Text = "Bar" },
new Label { Text = "Baz" },
new Label { Text = "Qux" },
}
};
ImagePaths = new List<string> { "red.png", "green.png", "blue.png", "orange.png" };
}
}
The renderer for iOS could look like this:
[assembly: ExportRenderer (typeof (FunkyPage), typeof (FunkyPageRenderer))]
public class FunkyPageRenderer : PageRenderer
{
UIScrollView bgCarousel = new UIScrollView (RectangleF.Empty) {
PagingEnabled = true,
ScrollEnabled=true
};
List<UIImageView> uiimages = new List<UIImageView> ();
protected override void OnElementChanged (VisualElementChangedEventArgs e)
{
foreach (var sub in uiimages)
sub.RemoveFromSuperview ();
uiimages.Clear ();
if (e.NewElement != null) {
var page = e.NewElement as FunkyPage;
foreach (var image in page.ImagePaths) {
var uiimage = new UIImageView (new UIImage (image));
bgCarousel.Add (uiimage);
uiimages.Add (uiimage);
}
}
base.OnElementChanged (e);
}
public override void ViewDidLoad ()
{
Add (bgCarousel);
base.ViewDidLoad ();
}
public override void ViewWillLayoutSubviews ()
{
base.ViewWillLayoutSubviews ();
bgCarousel.Frame = View.Frame;
var origin = 0f;
foreach (var image in uiimages) {
image.Frame = new RectangleF (origin, 0, View.Frame.Width, View.Frame.Height);
origin += View.Frame.Width;
}
bgCarousel.ContentSize = new SizeF (origin, View.Frame.Height);
}
}
This was tested and works. Adding a UIPageControl (the dots) is easy on top of this. Autoscrolling of the background is trivial too.
The process is similar on Android, the overrides are a bit different.