I'm using AppGyver Steroids and Supersonic to build an app and I'm having some issues navigating between views programmatically.
Based on the docs, you navigate between views like this:
var view_obj = new supersonic.ui.View("main#index");
supersonic.ui.layers.push(view_obj);
However, when I inspect things via the Chrome DevTools, it appears that a second duplicate view is created i.e. If I navigate away from the index page and then navigate back, I now have two index pages, instead of what [I think] should be one. It also doesn't close the previous view I was on.
How can I prevent this from happening and simply move to the existing view, instead of duplicating views? How do I close a view after I have navigated away from it?
Thanks.
The problem you're encountering is that you're creating a new supersonic.ui.View("main#index") every time you navigate. On top of this, I think you want to return to the same view when you navigate back to a view for the second time, i.e. you want the view to remain in memory even if it has been removed from the navigation stack with pop() (rather than pushing a new instance of that view). For this, you need to preload or "start()" the view, as described in the docs here.
I implemented my own helper function to make this easier; here is my code:
start = function(dest, isModal) {
var viewId=dest,
view=new supersonic.ui.View({
location: dest,
id: viewId
});
view.isStarted().then(function(started) {
if (started) {
if (isModal) {supersonic.ui.modal.show(view);}
else {supersonic.ui.layers.push(view);}
} else {
// Start Spinner
supersonic.ui.views.start(view).then(function() {
if (isModal) {supersonic.ui.modal.show(view);}
else {supersonic.ui.layers.push(view);}
// Stop Spinner
}, function(error) {
// Stop Spinner
A.error(error);
});
}
});
};
Use it like start('module#view');. As a bonus, you can pass true as the second argument and it gets pushed as a modal instead.
It checks if you've already started a view - if so, it just pushes that view back onto the stack. If not, it start()s (i.e. preloads) it, then pushes it. This ensures that the view stays in memory (with any user input that has been modified) even when you pop() it from the stack.
You have to imagine that the layer stack is actually a stack in the Computer Science sense. You can only add and remove views at the top of the stack. The consequence of this is that complex navigations such as A > B > C > D > B are difficult/hacky to do (in this case, you'd have to pop() D and C in succession to get back to B).
Views will close if you pop() them, as long as you didn't start() them. If you did, and you pop() them, they remain in memory. To kill that view, you have to call stop() on it, as described in the docs I linked above.
try
var view_obj = new supersonic.ui.View("main#index");
supersonic.ui.layers.replace(view_obj);
And take a look at supersonic.ui.layers.pop();
Thanks to LeedsEbooks for helping me get my head around this challenge. I was able to find a solution. Here is the code:
var start = function(route_str, isModal) {
var regex = /(.*?)#(.*)/g;
var match_obj = regex.exec(route_str);
var view_id_str = match_obj[2],
view_location_str = route_str,
view = new supersonic.ui.View({
location: view_location_str,
id: view_id_str
});
view.isStarted().then(function(started) {
if (started)
{
if (isModal)
{
supersonic.ui.modal.show(view);
}
else {
supersonic.ui.layers.push(view);
}
}
else
{
// Start Spinner
supersonic.ui.views.start(view).then(function() {
if (isModal)
{
supersonic.ui.modal.show(view);
}
else
{
supersonic.ui.layers.push(view);
}
// Stop Spinner
}, function(error) {
// Stop Spinner
A.error(error);
});
}
});
};
You must ensure that your route has the format module#view as defined in the documentation.
PLEASE NOTE
There seems to some problem with the supersonic ui method for starting views. If you run the following code:
supersonic.ui.views.start("myapp#first-view");
supersonic.ui.views.find("first-view").then( function(startedView) {
console.log(startedView);
});
You'll notice that your view id and location are identical. This seems to be wrong as the id should be first-view and location should be myapp#first-view.
So I decided to not use the AppGyver methods and create my own preload method instead, which I run from the controller attached to my home view (this ensures that all the views I want to preload are handled when the app loads). Here is the function to do this:
var preload = function(route_str)
{
var regex = /(.*?)#(.*)/g;
var match_obj = regex.exec(route_str);
var view = new supersonic.ui.View({
location: route_str,
id: match_obj[2]
});
view.start();
};
By doing this, I'm sure that the view will get loaded with the right location and id, and that when I use my start() function later, I won't have any problems.
You'll want to make sure that your structure.coffee file doesn't have any preload instructions so as not to create duplicate views that you'll have problems with later.
Finally, I have a view that is 2 levels in that is a form that posts data via AJAX operation. I wanted the view to go back to the previous view when the AJAX operation was complete. Using my earlier function resulted in the push() being rejected. It would be nice if AppGyver Supersonic could intelligently detect that pushing to a previous view should default to a layers.pop operation, but you don't always get what you want. Anyway, I managed to solve this using supersonic.ui.layers.pop(), which simply does what the Back button would have done.
Everything working as intended now.
Related
I have a problem I have not yet been able to understand, nor solve.
On my android project, I have a BaseActivity. There, among other functions, I decided to add a function to show or hide a loading view when necessary. It works as intended, but sometimes an error happens.
I will try to raise some important info about my project I think can be useful. My app is integrated with an external login app. I call it when the services I call need to refresh it's token. When the user logs in on the app, it calls a listener and give me back control on mine.
The problem is the next one:
I come to an activity that needs to call a service and I have the token all right, but then I lock the phone. After a long period of time, I unlock the phone and, from the same activity, I call again the service. My activity shows de loader as intended and, as my token is expired, I call the login app from it's SDK.
When I come back to my app, and I call the service I wanted successfully, the app tries to hide the loader. This is where the fail comes, as I can't change the visibility to GONE. I looked for it on the view hierarchy and find it, but with visibility = VISIBLE.
Here is the piece of code from the loader, hope someone can find where I'm making the mistake!
abstract class BaseActivity : DaggerAppCompatActivity(){
// These are the IDS of the Views I'm adding to the activity, so I can track them and change their visibility
var imgLoadingID = -1
var rvLoadingID = -1
fun showLoading() {
// If the views are added I show them
if (imgLoadingID > 0 && rvLoadingID > 0) {
val imageView = findViewById<ImageView>(imgLoadingID)
val relativeLayout = findViewById<RelativeLayout>(rvLoadingID)
relativeLayout.visibility = View.VISIBLE
imageView.visibility = VISIBLE
imageView.isClickable = false
imageView.isFocusable = false
} else {
// else I create them and show them
val imgLoading = ImageView(this)
imgLoading.id = View.generateViewId()
imgLoadingID = imgLoading.id
val maxpx = CustomUtils.ViewUtils.converIntToDps(65, this)
Glide.with(this).asGif().load(R.mipmap.loading).into(imgLoading)
val relativeLayout = RelativeLayout(this)
relativeLayout.id = View.generateViewId()
rvLoadingID = relativeLayout.id
var params = RelativeLayout.LayoutParams(maxpx, WRAP_CONTENT)
params.addRule(RelativeLayout.CENTER_IN_PARENT)
relativeLayout.addView(imgLoading, params)
relativeLayout.background = getDrawable(R.color.pure_white_97)
relativeLayout.isClickable = true
relativeLayout.isFocusable = true
findViewById<ViewGroup>(android.R.id.content).addView(relativeLayout, RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
imgLoading.visibility = VISIBLE
}
// I lock the back button so people don't cancel my requests
esBackPressedBlocked = true
}
// Here I find the views and change their visibility.
fun hideLoading(){
if(imgLoadingID > 0 && rvLoadingID > 0) {
val imageView = findViewById<ImageView>(imgLoadingID)
val relativeLayout = findViewById<RelativeLayout>(rvLoadingID)
relativeLayout.visibility = View.GONE
imageView.visibility = View.GONE
}
esBackPressedBlocked = false
}
}
I deleted some logs I added to the whole function, but when it fails, it enters on the hideLoading() function, even on the relativeLayout.visibility = View.GONE part.
The function used to be with the Views as an whole object instead of their ids, but I found it more reliable this way, and saving the views instead of their Id's had the same problem.
My main concern is how Android manages my application while the phone is locked for this period of time (the fails happened after 8-10 hours of inactivity). I think something there can be creating this issue. I thought also about the external Login app, since it's sdk is launching their app's intent and calling me from a listener when coming back, but, since my code is being executed, I think Android it's managing my views on an strange way. Or maybe I try to hide the loading view before I'm on the resumed activity... I don't really know.
BTW, I know there are easier solutions on showing a loader, but I wanted to create it the cleanest way. If you have any cleaner approach I'm open to any solution.
If there is anything unclear let me know in the comments, and I hope my English was clear enough to express myself, it's a tricky problem that I can't understand, so it's difficult for me to explain it.
Thanks!!
In my application I have created my own Loading indicator with the help of Page class in xamarin.forms, and I use 'PushModalAsync' api to show Loading Indicator wherever needed like below
var loadingindicator=new LoadingIndicator();
MyApp.CurrentAppInstance.MainPage.Navigation.PushModalAsync(loadingindicator,false);
Everything works as expected and It is looking fine and I also managed to make it look like AndHUD by tweeking the alpha for the controls in that page until I hit an issue,
The issue is that, every time I show and hide the loadingindicator page 'OnAppearing()' getting called on the current top page on view stack.
I can fix this by introducing one additional functionality on all pages where I am using my LoadingIndicator, But I feel there might be some other cleaner way to solve this issue.
Can you guys suggest me if there is any cleaner approach available to solve this issue?
(I target this solution mainly for android and I want to achieve it through common code)
If I have understand the problem, I think there are some way to add Loading indicator.
Use ActivityIndicator
Use aritchie/userdialogs's Loading
using (this.Dialogs.Loading("Test Loading"))
await Task.Delay(3000);
Create a PopUp With Rg.Plugins.Popup
// Use these methods in PopupNavigation globally or Navigation in your pages
// Open new PopupPage
Task PushAsync(PopupPage page, bool animate = true) // Navigation.PushPopupAsync
// Hide last PopupPage
Task PopAsync(bool animate = true) // Navigation.PopPopupAsync
// Hide all PopupPage with animations
Task PopAllAsync(bool animate = true) // Navigation.PopAllPopupAsync
// Remove one popup page in stack
Task RemovePageAsync(PopupPage page, bool animate = true) // Navigation.RemovePopupPageAsync
how to reload a page (with a new template) when the orientation of the device changes?
I want a different layout in landscape mode. This is in NativeScript, not Java.
the correct xml file is selected if i arrive at the page in landscape, but if i change orientation, nothing gets reloaded.
Note that this is not a question of refreshing the CSS, it is a different XML file needed.
NativeScript solution:
First you need to tie into the orientation event. You have a couple ways you can tie into that event.
The first and easiest method is to install the nativescript-orientation plugin, it ties into the event globally and then it will just automatically run your exported function on each of the current page called orientation, each time the orientation changes.
To Install:
tns plugin install nativescript-orientation
Open your app.js file and add at the top of the file;
require('nativescript-orientation');
Then by creating:
exports.orientation = function(args) {
if (args.landscaped) { /* do landscape stuff */ }
else { /* do port */
};
on any page you want to be notified that the orientation changed, it will be called on those pages that have that function and you can handle the event how you need to.
However, if you prefer to not use a plugin, you can directly tie into the orientation event yourself by doing:
var application = require('application');
exports.onNavigateTo = function() {
application.on(application.orientationChangedEvent,myOrientationFunction);
}
exports.onNavigateFrom = function() {
application.off(application.orientationChangedEvent, myOrientationFunction);
function myOrientationFunction(args) {
// We have an orientation event
}
However you must ask to be notified of the event when your page first open and you must remove your self from the notification when your page closes. This is a lot of extra code per page that the plugin above just handles for you. Please note when you are doing this yourself you also need to to add the NavigateTo/NavigatedFrom to the <Page> tag in your Declarative UI XML file, otherwise those functions won't be called.
Ok, now that you have the event which ever way you prefer; lets look at how we can make your idea work.
Now, you are asking to switch layouts each time the page changes; this is typically the worst thing to do; but I will answer it first and then give you the alternative method that I use to do complex layouts that work in both Portrait and Landscape modes pretty much automatically.
MyPage-Landscape.xml
<Page><StackLayout><Label text="Landscape"/></StackLayout></Page>
MyPage-Portrait.xml
<Page><StackLayout><Label text="Portrait"/></StackLayout></Page>
MyPage-Landscape.js
var commonPage = require("./MyPage.js");
var frame = require('ui/frame');
exports.orientation = function(args) {
if (args.landscape === false) {
frame.topmost().navigate('MyPage-Portrait');
}
};
// Tie all the common page code into this pages exports
for (var key in commonPage) {
if (commonPage.hasOwnProperty(key)) {
exports[key] = commonPage[key];
}
}
MyPage-Portrait.js
var commonPage = require("./MyPage.js");
var frame = require('ui/frame');
exports.orientation = function(args) {
if (args.landscape === true) {
frame.topmost().navigate('MyPage-Landscape');
}
};
// Tie all the common page code into this pages exports
for (var key in commonPage) {
if (commonPage.hasOwnProperty(key)) {
exports[key] = commonPage[key];
}
}
MyPage.js
exports.TapHandler = function() { /* Someone Tapped */ }
exports.someOtherLogic = function() { /* More logic */ }
exports.etc = function() { /* etc */ }
You put all your common page logic in the MyPage file; but you specifically navigate to the specific landscape or portrait page; and each of them are responsible to navigate to the other version if the page orientation changes.
Notes about the above solution:
You need to navigate to the proper version of the page from any other page; ie. if you are in Landscape mode; when you navigate to another page; you need to make sure you navigate to the Landscaped version of the page.
the NS-Orientation plugin does give you a handle helper function to find out the current orientation to make this easier.
Remember to make the MyPage.js have all the common code; you want to try and eliminate any custom code on a specific page version.
Their is a frame reload command you can use; however it totally clears the history; meaning you can't navigate backwards. i.e. Page1 -> Page2, frame.reloadPage() means that the back button will NOT go back to Page1. If this is acceptable; you can make the above system a lot simpler; rather than create separate xml & js files you just need a myPage.landscape.xml and myPage.portrait.xml and you need to on every orientation change just call the frame.reloadPage();
Now to me the above is some serious overkill for what is probably a simple change that you need done between pages. So I'm going to describe how I do it in my apps; which has some pretty complex screens but they look very nice and completely change functionality on a orientation change.
This is part of the reason the NativeScript-orientation plugin was written. On a page orientation change will automatically add / remove a "landscape" class name to the <Page> element in your XML. What this allows you to do in your CSS is:
.myLabel {
font-size: 12;
background-color: blue;
height: 20;
}
.landscape .myLabel {
font-size: 16;
background-color: green;
height: 40;
}
If you haven't figured out where I am going with this; this allows you to have custom CSS for the page while in landscape mode vs it being in portrait mode. In addition when you use the exports.orientation function in union with it also you can then run custom code depending on the orientation.
So in my case; On a phone my scroll list is a single scroll list of items going up down and is sized perfectly to the screen, and looks very sharp. When you switch to landscape mode; it hides the actionbar, adds a fab button, resizes the entire grid item to fit with the same proportions and switches scrolling modes to right-left. The majority of the entire look change is done in pure css; and the rest is done in the exports.orientation function which handles things like switching scroll direction.
Disclaimer: I am the author of the NativeScript-orientation plugin
In your activity:
#Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
setContentView(R.layout.my_layout);
}
Make sure you have layout XML with same name for both orientations
res/layout/my_layout.xml
res/layout-land/my_layout.xml
With correct resource file names Android system automatically reloads your Activities/Fragments with proper resources.
You can catch and handle events manually by setting android:configChanges (but it is a rare case).
Check some documentation:
Handling Runtime Changes
Providing Resources
I will try to explain this as clearly as possible. I have an android app using web view to basically load a webpage as my app. I have everything working great, however the back button seems to be an issue. I have set this page up all on one html page, it will load in a div when certain buttons are clicked to give the feel of a new page without actually having one. I basically want the back button (on the android tablet or smartphone) to load the previously loaded div, but I have no idea where to start with this. Here is what the content switching jquery looks like -
function contentSwitcher(settings){
var settings = {
contentClass : '.contentToLoad',
navigationId : '#sideMenu',
servFront : '#clickHomeHome'
};
//Hide all of the content except the first one on the nav
$(settings.contentClass).not(':first').hide();
$(settings.navigationId).find('li:first').addClass('active');
//onClick set the active state,
//hide the content panels and show the correct one
$(settings.navigationId).find('a').click(function(e){
var contentToShow = $(this).attr('href');
contentToShow = $(contentToShow);
//dissable normal link behaviour
e.preventDefault();
//set the proper active class for active state css
$(settings.navigationId).find('li').removeClass('active');
$(this).parent('li').addClass('active');
//hide the old content and show the new
$(settings.contentClass).hide();
contentToShow.show("slow");
});
}
contentSwitcher();
});
note: I've cropped out a bunch of it just to show how it works on a basic level.
Does anyone have any suggestions as to where to begin. I'd just like the back button function to be able to maybe check a started previous div name stored somewhere and load that.
thanks!
You can try using the History API. There are numerous tutorials on the web e.g. this one is quite good:
http://diveintohtml5.info/history.html
Basically this is how it works. When the user clicks the link for the div to show you push the state to the history stack.
history.pushState({<object with any information about state>}, pageTitle, newUrl);
This will push the state to the history stack meaning that when the user presses the back button on any modern browser like webkit it will take that state into consideration. When back action is taken it will then pop the state from the history stack. This action you have to listen to and handle in any way you see fit:
window.addEventListener("popstate", function(event) {
// event object contains the information from the pushed state
// do whatever needed to load the previous page here
});
The History API requires you to structure your code in a certain way for it to work well. For this I would recommend to use some existing framework that handle the back events for you e.g. Backbone.js. Hope this helps.
EDIT: tl;dr: WebView appears as white box, even though I appear to be setting it up correctly, and indeed it does work the first two times, but fails subsequently)
EDIT: Video showing the problem in action...
I have the following bit of code which inflates a view (Which contains a WebView) from the xml which defines it:
private void createCard(ViewGroup cvFrame, Card card) {
//... setup vairables...
cvFrame.clearDisappearingChildren();
cvFrame.clearAnimation();
try {
View cv = LayoutInflater.from(getBaseContext()).inflate(R.layout.card_back_view,
cvFrame, true);
cv.setBackgroundDrawable(Drawable.createFromStream(mngr.open(deckName + "_Card_back.png"), deckName));
TextView suit = (TextView)cv.findViewWithTag("card_back_suit");
//...setup text view for suit, this code works fine every time...
WebView title = (WebView)cv.findViewWithTag("card_back_title");
//This WebView doesn't appear to be the one which actually appears on screen (I can change settings till I'm blue in the face, with no effect)
if (title != null) {
title.setBackgroundColor(0x00000000);
title.loadData(titleText, "text/html", "UTF-8");
} else {
Log.e("CardView", "Error can't find title WebView");
}
} catch (IOException e) {
Log.e("CardView", "Error making cards: ", e);
}
}
When this method is called as part of the onCreate method in my Activity, the WebView contains the correct code, and is suitably transparent.
I have a gesture listener which replaces the contents of the ViewGroup with different content (It animates the top card off to the left, replaces the contents of the top card with card 2, puts the top card back, then replaces card 2 with card 3)
//Gesture listener event
ViewGroup cvFrame = (ViewGroup)findViewById(R.id.firstCard);
cardLoc++
cvFrame.startAnimation(slideLeft);
(onAnimationEnd code)
public void onAnimationEnd(Animation animation) {
if (animation == slideLeft) {
ViewGroup cvFrameOldFront = (ViewGroup)findViewById(R.id.firstCard);
ViewGroup cvFrameNewFront = (ViewGroup)findViewById(R.id.secondCard);
createCard(cvFrameOldFront, cards.get((cardLoc)%cards.size()));
createCard(cvFrameNewFront, cards.get((cardLoc+1)%cards.size()));
TranslateAnimation slideBack = new TranslateAnimation(0,0,0,0);
slideBack.setDuration(1);
slideBack.setFillAfter(true);
cvFrameOldFront.startAnimation(slideBack);
}
}
When the animation has happened and I replace the contents of the cards, the TextView suit is replaced fine and the code definitely passes through the code to replace the WebView contents, but for some reason I end up with a white rectangle the size and shape of the WebView, no content, no transparency.
If I change the WebView to a TextView, it's contents is replaced fine, so it's an issue that occurs only with the WebView control :S
Can anyone tell me why / suggest a fix?
It turns out the WebView doesn't get cleared down when using the LayoutInflater to replace the contents of a ViewGroup. The other controls all seem to get removed (or at least the findViewWithTag() returns the right reference for every other control). I've just added in the line cvFrame.removeAllViews() immediately before the LayoutInflater does it's stuff and that fixed the issue.
If anyone has any better explanation for this I'll throw the points their way otherwise they will just go into the ether...
By calling findViewById, you are getting a reference on the previously loaded webview do you ?
so the loadData call that fails is the second one you make on a single webview instance.
you may want to check this :
Android WebView - 1st LoadData() works fine, subsequent calls do not update display
It appears that loadData() won't load data twice... you may want to try WebView.loadDataWithBaseUri()
Hope that helps.
I had a similar problem loading several WebViews content.
It was because of a misusing of the pauseTimers function
The situation was : the first webView weren't needed anymore, conscientiously I wanted to pause it before to release it. Calling onPause() and pauseTimers()
pauseTimers being common to any web views, it broke every use of webviews occuring after that, there were displaying only white rectangles.
Maybe its not your problem here, but it's worth checking your not calling WebView.pauseTimers() somewhere.
To confirm your answer, the source code for LayoutInflater.inflate(int resource, ViewGroup root, boolean attachToRoot) does in fact internally calls root.addView() which attaches the newly inflated view at the end of the root's children instead of replacing them.
So the mystery now is why did your call to findViewWithTag() is returning the expected objects for your other widgets (which would be the top, most recently created instances), but for your WebView it was returning something else.
Is it possible that there is another object in your layout XML which shares the same "card_back_title" tag?
I was also wondering why you didn't use the more common findViewById() instead, but I am not sure whether it would make a difference.