I'm working on an interface that provides a set of multiple Button objects, each of which has attached the same OnClickListener. When said Buttons are clicked, they should launch an Activity, as specified in onClick.
Here is my code for reference:
public class Calcs extends SherlockFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// ...
CalcLoader buttonListener = new CalcLoader(getActivity());
LinearLayout buttons = (LinearLayout) v.findViewById(R.id.calculatorlist); // v is the inflated View
for (int i = 0; i < buttons.getChildCount(); i++) {
View b = buttons.getChildAt(i);
if (b instanceof Button) {
((Button) b).setOnClickListener(buttonListener);
}
}
// Test Code: Location 1
Intent i = new Intent(getActivity(), MyCalcActivity.class);
getActivity().startActivity(i);
// ...
}
private class CalcLoader implements OnClickListener {
private Activity mOwner;
public CalcLoader(Activity owner) {
mOwner = owner;
// Test Code: Location 2
Intent i = new Intent(mOwner, MyCalcActivity.class);
mOwner.startActivity(i);
}
public void onClick(View v) {
if (v instanceof Button) {
// Actual Code: Location 3
Intent i = new Intent(mOwner, MyCalcActivity.class);
mOwner.startActivity(i);
}
}
}
}
Despite this, however, I'm getting some odd behavior. In the above code, I've placed some startActivity tests, labelled locations 1 and 2. In both cases, the Activity launches correctly, and all is well!
However, at location 3, where the working code should execute, I get some strange behavior from the launched Activity:
At first, the Activity is launched just fine. It displays a single text field and it is focused, with the soft keyboard coming up. This is correct.
Now, when I click the back button, the keyboard closes. This is correct.
Click back again, and the field loses focus. This should NOT happen. Instead, the Activity SHOULD close and return to the previous one.
Click back again, and the entire app closes (instead of returning to the previous Activity). Obviously, this should NOT happen.
To reiterate, when the Activity is started from location 1 or 2, everything functions correctly; the back stack is correct and returns to the initial Activity properly.
What is going wrong here? Why, when I start my Activity from onClick, does it fail, while it works from any other location?
Update: Saving the Intent in the constructor and reusing it in the onClick method produces the same glitched result, as does starting the Activity from the UI thread.
Second update: Making the text field unfocusable had no effect on the glitch; the back button still closed the app. Additionally, running in the 2.3.3 emulator had the same result. Oddly, though, after the second back button press (the text field losing focus), if you wait ~3 seconds, the Activity closes and returns to the main one.
Third update: No key events (onKeyDown or onBackPressed) are fired for the back button that takes focus from the text field. Additionally, if you interact with the Activity after the text field loses focus, it shows the animation of loading a new Activity of the same type, but the glitch is present here as well.
This appears to be an OS-level issue, found in Android 2.2 (API 8), 2.3.1 (API 9), and 2.3.3 (API 10). Eclair (API 7), and APIs 11+ do not have this issue. At this point, I believe I'm looking for some kind of workaround...
Turns out my issue was not caused by something I detailed in my original post, so I apologize for that.
The issue was caused by a SurfaceView item on a tab unrelated to the tab I was testing on. After rebuilding the tabs and layouts from the ground up (and building each time), I discovered the lag-back-glitch was only caused when a SurfaceView was present in a non-focused tab.
I finally found that I was not the only one with this issue.
To solve:
Created an onPause method in the main Activity. In here, I destroy the SurfaceView using container.removeView(..);.
Created an onResume method in the main Activity. In here, I inflate the SurfaceView from a new XML file containing ONLY the SurfaceView item, and add it to the original container.
Lastly, implemented a android.view.SurfaceHolder.Callback in the SurfaceView to erase the contents of the surface before it is removed.
It stumps me that this happens only on APIs 8-10, but I'm glad it's solved now. Kudos to everyone that offered their assistance!
Related
I have 2 fragments in a ViewPager and I want the window to adjust differently to the soft keyboard on the 2nd fragment. Here's what I'm trying:
#Override
public void onPageSelected(int position) {
if(position == 1){ // desired for 2nd fragment
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
} else { // desired for 1st fragment
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED);
}
}
Observed behavior:
Enter 1st fragment and default softInputMode is working, as expected.
Swipe to 2nd fragment and breakpoint shows that the softInputMode should be set to ADJUST_NOTHING, but everything still behaves like the default.
Swipe back to 1st fragment and it behaves with ADJUST_NOTHING.
Swiping back and forth now reveals both fragments to behave like ADJUST_NOTHING, even though breakpoints show these calls are being made.
To top it off, I can switch fragments all I want and the input mode will behave as default until I pull up the soft keyboard. Then it starts its migration toward ADJUST_NOTHING. I'm quite baffled.
I don't have any relevant flags in the manifest, although in my Activity onCreate() I do set the input mode to SOFT_INPUT_STATE_ALWAYS_HIDDEN.
The solution I found works well enough, although I hope there's a smoother way out there somewhere. I did two things.
First, I stopped using SOFT_INPUT_ADJUST_UNSPECIFIEDas my "default" input state because the WindowManager seems to treat changes to and from this state a bit differently. 'SOFT_INPUT_ADJUST_RESIZE' gives the behavior I desire, so I changed my method to
#Override
public void onPageSelected(int position) {
getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
);
hide_keyboard(activity);
if(position == 1){ // desired for 2nd fragment
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING);
} else { // desired for 1st fragment
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
}
This fixed the "getting stuck in a certain input mode" issue. Still, I wasn't getting the input mode I wanted on my 2nd fragment, so I added a callto set the input mode right before launching the DialogFragments with text input fields from my 2nd fragment.
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING); // fragment behind doesn't get readjusted from keyboard.
editCatFragment.show(getActivity().getSupportFragmentManager(), "editCatFrag");
This actually doesn't guarantee the mode I want the first time the fragment is launched, but makes it work the 2nd time which, due to how my app is designed, actually works out okay.
Hopefully this helps someone and hopefully there's a better way of solving this problem. Thanks!
I'm a beginner in Android, so I apologize for the mistakes and I'd appreciate any constructive criticism.
I'm writing a basic application with a ListView of images, and when the user clicks on an item in the list, I want to display that image in a ViewPager, where the user can swipe back and forth to browse the whole list of images. Afterwards when the user presses the back button, I want to switch back to the ListView.
I manage the business logic in the MainActivity, which uses MainActivityFragment for the ListView and ImageHolderFragment for ViewPager.
The simplified code so far is as follows:
#Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListItems = new ArrayList<>();
mListItemAdapter = new ListItemAdapter(this, R.layout.list_item, R.id.list_item_name, mListItems);
mListView = (ListView) findViewById(R.id.list_view_content);
mListView.setAdapter(mListItemAdapter);
mDeletedListItems = new ArrayList<>();
mViewPager = (ViewPager) getLayoutInflater().inflate(R.layout.image_display, null, true);
mImageAdapter = new ImageAdapter(getSupportFragmentManager(), mListItems);
mViewPager.setAdapter(mImageAdapter);
mViewPager.setOffscreenPageLimit(3);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
mViewPager.setCurrentItem(position);
setContentView(mViewPager); // TODO: this is very wrong!
}
});
loadImages();
noContentText = (TextView) findViewById(R.id.no_content_text);
if (mListItems.isEmpty()) {
noContentText.setText(R.string.no_images);
} else {
mImageAdapter.notifyDataSetChanged();
}
}
Although this does work to some extent, meaning that it manages to display the ViewPager when an item in the list is clicked, there are two things about it ringing the alarm bells:
I've read that calling setContentView() for the second time in the same class is pretty much a sin. Nobody explained me why.
The back button doesn't work in this case. When it's pressed, the application is terminated instead of going back to the list view. I believe this is connected to the first point.
I would appreciate any help, explanations if my idea is completely wrong, and if my case is hopeless, I'd like to see a successful combination of ListView and ViewPager with transitions between each other.
Your activity already has R.layout.activity_main set as content view, which rightly displays the list view - that's what the responsibility of this activity is as you defined it. If we want to change what's shown on the screen, we should use a different instance of a building block (activity or fragment) to display the view pager images.
To say the least, imagine if you wanted to change the view to a third piece of functionality or UI, or a fourth... it would be a nightmare to maintain, extend and test as you're not separating functionality into manageable units. Fields that are needed in one view are mixed with those needed in another, your class file would grow larger and larger as each view brings its click listeners, callbacks, etc., you'd also have to override the back button so it does what you want - it's just not how the Android framework was designed to help you. And what if you wanted to re-use UI components in different contexts whilst tapping in to the framework's activity lifecycle callbacks? That's why fragments were introduced.
In your case, the list view could continue to run in your MainActivity and in your click listener, onItemClick you could start a new activity that will hold a viewPager:
Intent i = new Intent(MainActivity.this, MyLargePhotoActivityPager.class);
i.putExtra(KEY_POSITION, position);
// pass the data too
startActivityForResult(i, REQUEST_CODE);
Notice how you could pass the position to this activity as an int extra, in order for that second activity to nicely set the viewPager to the position that the user clicked on. I'll let you discover how to build the second activity and put the ViewPager there. You also get back button functionality assuming your launch modes are set accordingly, if needed. One thing to note is that when you do come back to the list View, you'd probably want to scroll to the position from the view pager, which is why you could supply that back as a result via a request code. The returned position can be supplied back to the list view.
Alternatively, you could use the same activity but have two fragments (see the link further above) and have an equivalent outcome. In fact, one of your fragments could store the list view, and the second fragment could be a fullscreen DialogFragment that stores a viewPager, like a photo gallery (some details here).
Hope this helps.
I've read that calling setContentView() for the second time in the
same class is pretty much a sin. Nobody explained me why.
Well, you kind of get an idea as to why.
When you use setContentView() to display another 'screen' you do no have a proper back stack.
You also keep references to Views (like mListView) that are not visible anymore and are therefore kind of 'useless' after you setContentView() for the second time.
Also keep in mind orientation changes or your app going to the background - you'll have to keep track of the state that your Activity was in which is way more complicated than it has to be if you have one Activity that does two different things.
You won't be arrested for doing things like you do right now, but it's just harder to debug and keep bug free.
I'd suggest using two different Activities for the two different things that you want to do, or use one Activity and two Fragments, swapping them back and forth.
If you insist on having it all in one Activity you need to override onBackPressed() (called when the user presses the back button) and restore the first state of your Activity (setContentView() again, pretty much starting all over).
Here is my scenario: when my app gets purged from memory by the OS and its reopened from the recent apps list, it goes straight to the Activity that was last open. When this happens I would like to initialize a couple of business objects.
My first approach to achieve this was to do the initialization in the InitializeLastChance method in the Setup class.
Inside the InitializeLastChance method I check if the current top activity is the MvxSplashScreenActivity. If it's not, I make the necessary initialization. The code is something like this:
protected override void InitializeLastChance()
{
try
{
var topActivity = Mvx.Resolve<IMvxAndroidCurrentTopActivity>().Activity;
if (topActivity.LocalClassName != _splashScreenName)
{
//Do special initialization
}
}
catch (Exception ex)
{
//Log error
}
base.InitializeLastChance();
}
This works as expected but the visual effect isn't the best: a blank screen with the name of the app appears for a couple of seconds while the initialization occurs. When the initialization is finished, the last open activity is loaded.
To avoid this blank screen, I wanted to try a different approach: show an Activity while the initialization is being done, similar to a splash screen, so that the user can have some feedback that something is happening. In this approach the ViewModel attached to the new Activity would do the initialization that I want.
To show this new activity, I tried creating a custom IMvxAppStart and register it in the App class, but that hasn't worked. My custom IMvxAppStart has the following code:
public class CustomAppStart
: MvxNavigatingObject
, IMvxAppStart
{
public void Start(object hint = null)
{
if (hint == null)
{
ShowViewModel<LoginViewModel>();
}
else
{
ShowViewModel<InitializationViewModel>();
}
}
}
I wanted to show the LoginViewModel when the app starts from scratch and in the cases where the app goes straight to the last loaded Activity, the InitializationViewModel would be shown. In the latter scenario the Start method isn't called. I've checked the MvvmCross source code and as far as I can understand, the Start method is called by the MvxSplashScreenActivity (correct me if I'm wrong). When an app is reopened and goes straight to the last opened Activity, the MvxSplashScreenActivity isn't used so the Start method in CustomAppStart is not called.
That being said, I have some questions regarding the CustomAppStart approach:
Is this approach the best way to show a splash screen or "initialization activity" in the cases where the app goes straight to the last opened activity?
How do I use the CustomAppStart in these situations, since it seems that the Start method is called only from the MvxSplashScreenActivity?
How do I pass a value to the hint parameter in the Start method?
Thanks
I have a simple activity that shows an animation with ObjectAnimator. The animation is created and started in onCreate method of the activity, it is a very simple animation:
cloudAnim = ObjectAnimator.ofFloat(cloud1ImageView, "x", sw);
cloudAnim.setDuration(35000);
cloudAnim.setRepeatCount(ValueAnimator.INFINITE);
cloudAnim.setRepeatMode(ValueAnimator.RESTART);
cloudAnim.setInterpolator(null);
cloudAnim.start();
it simply displays a cloud on the left of the screen and moves from the left to the right.
The problem is that in my nexus 5 (android 4.4 lastet version) the cloud is doing a frame jump when the activity starts.
This jump is only visible in my nexus 5, because i'm testing the app also in a huawei ascend y300 devide with android 4.1 and the jump is not visible, the movement is very smooth.
What is wrong with ObjectAnimator and Android 4.4?
Thanks
Starting animations in onCreate is not a good idea. When user will finally be able to see this animation (after activity being inflated and displayed on the screen with animation etc.) the animation is not in it's beginning but a bit after it, so user will miss the very beginning of the animation or perhaps will see some frame drops then as well. The final result really depends on the device, android version, standard window animations styles etc.
If you want to launch an animation right after creating an activity make use of onWindowFocusChanged method:
http://developer.android.com/reference/android/app/Activity.html#onWindowFocusChanged(boolean)
Called when the current Window of the activity gains or loses focus.
This is the best indicator of whether this activity is visible to the
user.
In addition you need to do some checks:
1. Window has focus (hasFocus==true) - it's visible to the user
2. Create boolean variable indicating that animation was already started, so it will be launched only once
private boolean cloudAnimStarted;
#Override
public void onWindowFocusChanged (boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus && !cloudAnimStarted) {
cloudAnimStarted = true;
cloudAnim.start();
}
}
So creating a cloudAnim object is fine in onCreate, but launching it should be done in onWindowFocusChanged method instead.
I am working on an android project, where i have got a list view to be populated. It happens in a group activity where intent is passed to a controller and from that controller the activity starts using specified intent.
In identifying the issue through debugging, i got that the debugger gets stuck in the line specified:
**View view = activityManager.startActivity(intentId, intent).getDecorView();**
and the list view never gets populated, however the when i check the array from which i have to populate items, that shows me that it contains the items. One interesting thing is that, when i close the application, and restart it and then see the list view, it shows up fine. This is strange for me. Why does it not show up first time and show up second time. the whole function has the following code:
public void startGroupActivity(String intentId, Intent intent) {
contentViewLayout.removeAllViews();
Log.e("Start Group Actvity", intentId);
View view = activityManager.startActivity(intentId, intent).getDecorView();
contentViewLayout.addView(view, contentViewLayoutParams);
}
That behavior is simply un-understandable for me. Any help is appreciated.