So I've been having animation issues especially when two animations happen at once or right when an activity loads. I understand it's probably a resource problem and a lot of things are going on in the main thread causing the animations to stutter.
I've found a couple interesting suggestions:
1. Threads (ThreadPoolExecutor)
Here: How do I make my animation smoother Android
2. setDrawingCacheEnabled(true)
Here: How does Android's setDrawingCacheEnabled() work?
3. ViewGroup: animationCache = true
Here: http://www.curious-creature.org/2010/12/02/android-graphics-animations-and-tips-tricks/
However I haven't been able to find any sort of examples to implement these things. Any ideas?
I've reduced the amount of stutter on my animations by following these rules listed in order of importance when reducing stutter:
Don't initiate animations in onCreate, onStart or onResume.
Initiate animations on user events such as onClick and disable touch events until animation is complete.
Don't initiate more than 2 animations simultaneously.
If you are using animation you should follow the android docs; in fact in some cases, you might need to postpone your fragment transition for a short period of time. For example in my case I need to postpone my animation until my viewmodel return some data:
Use postponeEnterTransition() in the entering fragment onViewCreated() method:
public class A extends Fragment {
#Override
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
...
postponeEnterTransition();
}
}
Once the data are ready to start the transition, call startPostponedEnterTransition()
public class A extends Fragment {
#Override
public void onViewCreated(#NonNull View view, Bundle savedInstanceState) {
postponeEnterTransition();
final ViewGroup parentView = (ViewGroup) view.getParent();
// Wait for the data to load
viewModel.getData()
.observe(getViewLifecycleOwner(), new Observer<List<String>>() {
#Override
public void onChanged(List<String> list) {
// Set the data on the RecyclerView adapter
adapter.setData(list);
// Start the transition once all views have been
// measured and laid out
parentView.getViewTreeObserver()
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
parentView.getViewTreeObserver()
.removeOnPreDrawListener(this);
startPostponedEnterTransition();
return true;
});
}
});
}
}
Related
I am using a DialogFragment to display a 'modal' bottom sheet menu (more info here: https://material.io/develop/android/components/bottom-sheet-dialog-fragment/). Since it contains a kind of context menu for the items contained in a RecyclerView, it may be shown multiple times during runtime.
However, always DialogFragment.show() is called, Fragment.onCreateView() is also called, which leads to layout inflation, which can(?) be considered as a 'heavy' task to be computed in the UI thread, which I want to avoid for performance reasons. So to avoid layout inflation every time the DialogFragment is shown, I created a ViewGroup member object pointing to the View being returned Fragment.onCreateView() in order to be reused, like this:
public class BottomMenu extends BottomSheetDialogFragment {
private ViewGroup mLayout;
private TextView mLabel;
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
if (mLayout == null) {
mLayout = (ViewGroup) LayoutInflater.from(getContext()).inflate(R.layout.bottom_sheet, container, false);
mLabel = mLayout.findViewById(R.id.bottom_sheet_label);
}
return mLayout;
}
#Override
public void onDismiss(#NonNull DialogInterface dialog) {
super.onDismiss(dialog);
// The view cannot be reused if it's already attached to the previous parent view
((ViewGroup) mLayout.getParent()).removeView(mLayout);
}
public void setLabel(String label) {
mLabel.setText(label)
}
}
But once used for the first time, such view must be detached from the Fragment container view to be reused (see onDismissed() overriden method on posted snippet), which seems like a nasty workaround
So I post this question to check if anyone knows a better approach to reuse the layout for the same Fragment
More details here:
public class ActivityMain extends AppCompatActivity {
private BottomMenu mBottomMenu;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
[...]
mBottomMenu = new BottomMenu();
}
#Override
public boolean onLongClick(View v) {
mBottomSheet.setLabel(label);
// The following calls onCreateView() in Fragment, so try to return
// there the previously inflated layout, if any
mBottomSheet.show(getSupportFragmentManager(), "TAG?");
return true;
}
}
It is already a nice practice as long as you don't surrender to any possible bugs.. However there are one or two things I want to let you know about resuing dialogFragment.
public class BottomMenu extends BottomSheetDialogFragment {
private ViewGroup mLayout;
private TextView mLabel;
#Nullable
#Override
public View onCreateView(#NonNull LayoutInflater inflater, #Nullable ViewGroup container, #Nullable Bundle savedInstanceState) {
if (mLayout == null) {
mLayout = (ViewGroup) LayoutInflater.from(getContext()).inflate(R.layout.bottom_sheet, container, false);
mLabel = mLayout.findViewById(R.id.bottom_sheet_label);
} else if(mLayout.getParent()!=null) { // it's not a lot of code. just a few lines……
((ViewGroup)mLayout.getParent()).removeView(mLayout);
}
return mLayout;
}
}
One thing is about nested fragments. When the dialogFragment hold a viewpager and the viewpager have serveral sub-fragments, you must reset the viewpager's adapter on the reusing-call of onCreateView. The reason is that after closing the dialogFragment, the old fragmentManager returned by getChildFragmentManager() is no longer valid, and it should be updated.
... onCreateView(...)
if (mLayout == null) {
...
} else {
...
viewpager.setAdapter(new MyFragmentAdapter(getChildFragmentManager(), fragments));
}
If this step is omitted, you may observe strange behaviours when reusing the dialogFragment, such as recyclerviews in the sub-fragments stop updating in response to NotifyDatasetChanged, but if you scroll it, it will update.
Another thing is that I tend to use WeakRefernce to hold the dialogFragment to be reused. I even have an array of them.
In java applications, if you don't use similar mechanism, you can see rapid surge in memory usage when the user open and close the same dialog again and again. So at least it's not a bad practice to reuse dialogs when it's necessary.
Current situation:
We have different userinterfaces, which are build as fragments, for example the MenuFragment:
public class MenuFragment extends Fragment implements Hideable, View.OnClickListener {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
this.view = inflater.inflate(R.layout.menu, container, false);
}
#Override
public void hide() {
view.setVisibility(View.GONE);
}
#Override
public void show() {
view.setVisibility(View.VISIBLE);
}
}
Hideable:
public static interface Hideable {
void hide();
void show();
}
States are enums:
public enum InterfaceState {
STANDARD, TWO, THREE ;
private List<Hideable> visibleElements;
private void setVisibleElements(Hideable[] visibleElements) {
this.visibleElements = Arrays.asList(visibleElements);
}
}
Set them using:
InterfaceState.STANDARD.setVisibleElements(new Hideable[] { menuFragment });
where menuFragment is
menuFragment = (MenuFragment) activity.getFragmentManager().findFragmentById(R.id.menu_fragment);
Now i change the state calling changeToState:
public void changeToState(InterfaceState state) {
List<Hideable> hideList = new LinkedList<>();
for (Hideable e : this.currentState.visibleElements) {
if (!state.visibleElements.contains(e))
hideList.add(e);
e.hide();
}
List<Hideable> showList = new LinkedList<>();
for (Hideable e : state.visibleElements) {
if (!this.currentState.visibleElements.contains(e))
showList.add(e);
e.show();
}
The system seems to be buggy. Sometimes UI elements dont appear.
Questions:
Is this a good way to implement an UI? If not, what's a better way to do?
Does Android allocate space for a view, which has Visibility="GONE"? To use the upper mechanism, I need to define all UI elements (like MenuFragment menuFragment) directly on app start.
Any suggestions/improvements?
Thanks to all readers.
I always advocate the convention over configuration rule. Android has some nice conventions on how to design you user interface and you should s tick to them whenever possible.
No it does not. That is, View.GONE makes the view act as though it's not there (from the layouts point of view), where as View.INVISIBLE allocates the layout space needed for the view, but simply makes the view invisible. However, even with View.GONE, you'll still be able to get your view with findViewById().
I think that storing information in an enum like that is bad idea. You should find a different method that suits you (either put your list in an activity, in the Application class or something similar.)
EDIT: You can find some usefull information about the pattern on Wikipedia, and you can read more about the Android Design Guidelines here. There is also a nice document about the pattern here. You can also look at AOSP's code style guidelines (or, well, rules) as they provide some nice conventions on how to write code.
This is more of a design question and how one would go about designing applications. I have been having fun with fragments, but one thing that doesn't make sense to me something like this:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
page = getArguments().getInt("someInt", 2);
Button btnOne = (Button) getView().findViewById(R.id.one);
btnOne.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v) {
String currentText = getScreen().getText().toString();
currentText += "1";
getScreen().setText(currentText);
}
});
}
// Inflate the view for the fragment based on layout XML
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.standard_calculator, container, false);
return view;
}
/** Return the screen TextView object for manipulation
* #return screen
*/
public TextView getScreen() {
return screen;
}
Screen title are private variables in the class and this isn't the whole class, but just the part that I need to help my question. There are going to be at least 15 or so buttons that manipulate the screen and it doesn't feel like good practice to and put them all in the onCreate method, I was wondering whether it would be better design to have them in another class that let the methods in the fragment be more specific to the life-cycle of the fragment, although one can say that the buttons are used by the fragment and therefore should be part of the class. Perhaps an "initialising" method is needed.
I was hoping someone might be able to direct me to some design patterns or logical way of thinking about application design, it is quite different.
Many thanks.
Putting them in the XML is less versatile than doing it in code. If you don't want to have XXX anonymous methods, you can make your own Fragment/Class implement View.onClickListener and implement the onClick method:
#Override
public void onClick(final View v) {
if ( v.getId() == R.id.btn_logout ){
// Do One Thing
} else if ( v.getId() == R.id.btn_about) {
// Do Something Else
} else if ( v.getId() == R.id.btn_shutdown) {
// Or Maybe do this :)
}
}
Then in your onViewCreated just assign each button with "this"
final someBtn = (Button) view.findViewById(R.id.btn_logout);
someBtn.setOnClickListener(this);
That can be cleaner looking than a bunch of anonymous methods and you know you have your click listeners in one place.
You don't have to initialize them all in the onCreate() method. In fact, you don't have to initialize them in java code at all. You could simply define them in xml and define an "onClick" property in your xml. The method that you set in "onClick" will be called for that button. It's one way to make your Activities cleaner.
We're suffering from a very strange issue with ViewPager here. We embed lists on each ViewPager page, and trigger notifyDataSetChanged both on the list adapter and the view pager adapter when updating list data.
What we observe is that sometimes, the page does not update its view tree, i.e. remains blank, or sometimes even disappears when paging to it. When paging back and forth a few times, the content will suddenly reappear. It seems as if Android is missing a view update here. I also noticed that when debugging with hierarchy viewer, selecting a view will always make it reappear, apparently because hierarchy viewer forces the selected view to redraw itself.
I could not make this work programmatically though; invalidating the list view, or the entire view pager even, had no effect.
This is with the compatibility-v4_r7 library. I also tried to use the latest revision, since it claims to fix many issues related to view pager, but it made matters even worse (for instance, gestures were broken so that it wouldn't let me page through all pages anymore sometimes.)
Is anyone else running into these issues, too, or do you have an idea of what could be causing this?
If the ViewPager is set inside a Fragment with a FragmentPagerAdapter, use getChildFragmentManager() instead of getSupportFragmentManager() as the parameter to initialize your FragmentPagerAdapter.
mAdapter = new MyFragmentPagerAdapter(getChildFragmentManager());
Instead of
mAdapter = new MyFragmentPagerAdapter(getSupportFragmentManager());
We finally managed to find a solution. Apparently our implementation suffered of two issues:
our adapter did not remove the view in destroyItem().
we were caching views so that we'd have to inflate our layout just once, and, since we were not removing the view in destroyItem(), we were not adding it in instantiateItem() but just returning the cached view corresponding to the current position.
I haven't looked too deeply in the source code of the ViewPager - and it's not exactly explicit that you have to do that - but the docs says :
destroyItem()Remove a page for the given position. The adapter is responsible for removing the view from its container, although it only must ensure this is done by the time it returns from finishUpdate(ViewGroup).
and:
A very simple PagerAdapter may choose to use the page Views themselves as key objects, returning them from instantiateItem(ViewGroup, int) after creation and adding them to the parent ViewGroup. A matching destroyItem(ViewGroup, int, Object) implementation would remove the View from the parent ViewGroup and isViewFromObject(View, Object) could be implemented as return view == object;.
So my conclusion is that ViewPager relies on its underlying adapter to explicitly add/remove its children in instantiateItem()/destroyItem(). That is, if your adapter is a subclass of PagerAdapter, your subclass must implement this logic.
Side note: be aware of this if you use lists inside ViewPager.
I had the exact same problem but I actually destroyed the view in destroyItem (I thought). The problem however was that I destroyed it using viewPager.removeViewAt(index); insted of viewPager.removeView((View) object);
Wrong:
#Override
public void destroyItem(ViewGroup viewPager, int position, Object object) {
viewPager.removeViewAt(position);
}
Right:
#Override
public void destroyItem(ViewGroup viewPager, int position, Object object) {
viewPager.removeView((View) object);
}
ViewPager tries to do clever stuff around re-using items, but it requires you to return new item positions when things have changed. Try adding this to your PagerAdapter:
public int getItemPosition (Object object) { return POSITION_NONE; }
It basically tells ViewPager that everything has changed (and forces it to re-instantiate everything). That's the only thing I can think of off the top of my head.
Tried too many solutions but unexpectedly viewPager.post() worked
mAdapter = new NewsVPAdapter(getContext(), articles);
viewPager.post(new Runnable() {
#Override
public void run() {
viewPager.setAdapter(mAdapter);
}
});
The Android Support Library has a demo Activity that includes a ViewPager with a ListView on every page. You should probably have a look and see what it does.
In Eclipse (with Android Dev Tools r20):
Select New > Android Sample Project
Select your target API level (I suggest the newest available)
Select Support4Demos
Right-click the project and select Android Tools > Add Support Library
Run the app and select Fragment and then Pager
The code for this is in src/com.example.android.supportv4.app/FragmentPagerSupport.java. Good luck!
I ran into this and had very similar issues. I even asked it on stack overflow.
For me, in the parent of the parent of my view someone subclassed LinearLayout and overrode requestLayout() without calling super.requestLayout(). This prevented onMeasure and onLayout from being called on my ViewPager (although hierarchyviewer manually calls these). Without being measured they'll show up as blank in ViewPager.
So check your containing views. Make sure they subclass from View and don't blindly override requestLayout or anything similar.
Had the same issue, which is something to do with ListView (because my empty view shows up fine if the list is empty). I just called requestLayout() on the problematic ListView. Now it draws fine!
I ran into this same problem when using a ViewPager and FragmentStatePagerAdapter. I tried using a handler with a 3 second delay to call invalidate() and requestLayout() but it didn't work. What did work was resetting the viewPager's background color as follows:
MyFragment.java
private Handler mHandler;
private Runnable mBugUpdater;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = new ViewPager(getActivity());
//...Create your adapter and set it here...
mHandler = new Handler();
mBugUpdater = new Runnable(){
#Override
public void run() {
mVp.setBackgroundColor(mItem.getBackgroundColor());
mHandler = null;
mBugUpdater = null;
}
};
mHandler.postDelayed(mBugUpdater,50);
return rootView;
}
#Override
public void onPause() {
if(mHandler != null){
//Remove the callback if it hasn't triggered yet
mHandler.removeCallbacks(mBugUpdater);
mHandler = null;
mBugUpdater = null;
}
super.onPause();
}
I had a problem with the same symptoms, but a different cause that turned out to be a silly mistake on my part. Thought I'd add it here in case it helps anyone.
I had a ViewPager using FragmentStatePagerAdapter which used to have two fragments, but I later added a third. However, I forgot that the default off screen page limit is 1 -- so, when I'd switch to the new third fragment, the first one would get destroyed, then recreated after switching back. The problem was that my activity was in charge of notifying these fragments to initialize their UI state. This happened to work when the activity and fragment lifecycles were the same, but to fix it I had to change the fragments to initialize their own UI during their startup lifecycle. In the end I also wound up changing setOffscreenPageLimit to 2 so that all three fragments were kept alive at all times (safe in this case since they were not very memory intensive).
I had similar issue. I cache views because I need only 3 views in ViewPager. When I slide forward everything is okay but when I start to slide backward occurs error, it says that "my view already has a parent". The solution is to delete unneeded items manually.
#Override
public Object instantiateItem(ViewGroup container, int position) {
int localPos = position % SIZE;
TouchImageView view;
if (touchImageViews[localPos] != null) {
view = touchImageViews[localPos];
} else {
view = new TouchImageView(container.getContext());
view.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
touchImageViews[localPos] = view;
}
view.setImageDrawable(mDataModel.getPhoto(position));
Log.i(IRViewPagerAdpt.class.toString(), "Add view " + view.toString() + " at pos: " + position + " " + localPos);
if (view.getParent() == null) {
((ViewPager) container).addView(view);
}
return view;
}
#Override
public void destroyItem(ViewGroup container, int position, Object view) {
// ((ViewPager) container).removeView((View) view);
Log.i(IRViewPagerAdpt.class.toString(), "remove view " + view.toString() + " at pos: " + position);
}
..................
private static final int SIZE = 3;
private TouchImageView[] touchImageViews = new TouchImageView[SIZE];
For me the problem was coming back to the activity after the app process was killed. I am using a custom view pager adapter modified from the Android sources.The view pager is embedded directly in the activity.
Calling viewPager.setCurrentItem(position, true);
(with animation) after setting the data and notifyDataSetChanged() seems to work, but if the parameter is set to false it doesn't and the fragment is blank. This is an edge case which may be of help to someone.
For Kotlin users:
In your fragments;
Use childFragmentManager instead of viewPagerAdapter
I have 4 views that are controlled by 1 SherlockMapActivity. Currently I am switching between views with the tabs by removeAllViews() and then re-inflate the view again. This seams like a very inefficient way of going about it.
Is there any way to just "hide" a view that has been inflated already and re-position a new view to the front? I have tried every variation of setVisibility, etc, to no avail. Here is how I am going about it right now:
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
//load our views!
this.baseViewGroup = (ViewGroup)this.findViewById(android.R.id.content);
this.mapView = new MapView(ActivityMain.this, MAP_API_KEY);
this.mapView.setClickable(true);
this.createMenu();
}
#Override
public void onTabSelected(Tab tab, FragmentTransaction ft)
{
Log.v(CLASS_NAME, "tab selected: "+tab.getPosition());
if (0 == tab.getPosition())
{
this.baseViewGroup.removeAllViews();
this.getLayoutInflater().inflate(R.layout.map, this.baseViewGroup);
}
else if (1 == tab.getPosition())
{
this.baseViewGroup.removeAllViews();
this.getLayoutInflater().inflate(R.layout.list, this.baseViewGroup);
}
}
I can then do fancy things with ViewControllers (of sorts) to restart the previous state of the view when it is re-created but this just seams crazy. Is there a better way to do this?
Edit
I have tried saving the views (inflate once, remove but then just re-add) but I get this strange behavior. Basically, all inflated views are shown on top of each other, in a semi-transparent way. No amount of setVisibility() makes them totally go away.
The code I tried (added to onCreate() and onTabSelected() where appropriate):
//in onCreate()
this.mapLayout = this.getLayoutInflater().inflate(R.layout.map, this.baseViewGroup);
this.moreLayout = this.getLayoutInflater().inflate(R.layout.more, this.baseViewGroup);
//in onTabSelected()
ViewGroup content = (ViewGroup)this.mapLayout.getParent();
content.removeAllViews();
content.addView(this.mapLayout);
Donot inflate views again and again. instead, have 4 class level view variables like
private View firstView;
private View secondView;
private View thirdView;
private View fourthView;
now during every tab change/press. remove all child views from parent and add, appropriate view to the parent. like,
parentView.removeAllViews();
parentView.addView(secondView);
Edit:
Pass null for parentView.
instead of this,
this.moreLayout = this.getLayoutInflater().inflate(R.layout.more, this.baseViewGroup);
do this,
this.moreLayout = this.getLayoutInflater().inflate(R.layout.more, null);