How to implement WhatsApp like material design SearchView? - android

WhatsApp has such Toolbar:
When 'Search' menu item clicked, from the top SearchView comes down which takes whole space of toolbar:
When I tried to implement SearchView, it looks like this:
I found some libraries to implement this:
Android Material SearchView by Eugene Horan
and MaterialSearchView by krishnakapil. But they are not like in WhatsApp.
This question may seem weird, I could not find the way how to do this. So my question is how to implement WhatsApp like material design SearchView which comes from the top?

I have developed a well received library by the comunity.
Does exactly what are you looking for.
Give it a try and tell if if was usufull for you.
Here it is the Github repo for MaterialSearchView.

You can create this with android.support.v7 library
First of all create menu item in menu.xml like:
<item android:id="#+id/action_search"
android:title="Search"
android:icon="#drawable/abc_ic_search_api_mtrl_alpha"
app:showAsAction="ifRoom|collapseActionView"
app:actionViewClass="android.support.v7.widget.SearchView" />
Extend AppCompatActivity and retrieve the SearchView in onCreateOptionsMenu like:
import android.support.v7.widget.SearchView;
...
public class YourActivity extends AppCompatActivity {
...
#Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_home, menu);
// Retrieve the SearchView and plug it into SearchManager
final SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.action_search));
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
return true;
}
...
}
Thats it. Hope this helps you. Accept, if you find it useful.

Sorry, I misread the question.
This answer should solve your issue.
Answer taken from :- Creating a SearchView that looks like the material design guidelines
After a week of puzzling over this. I think I've figured it out.
I'm now using just an EditText inside of the Toolbar. This was suggested to me by oj88 on reddit.
I now have this:
First inside onCreate() of my activity I added the EditText with an image view on the right hand side to the Toolbar like this:
// Setup search container view
searchContainer = new LinearLayout(this);
Toolbar.LayoutParams containerParams = new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
containerParams.gravity = Gravity.CENTER_VERTICAL;
searchContainer.setLayoutParams(containerParams);
// Setup search view
toolbarSearchView = new EditText(this);
// Set width / height / gravity
int[] textSizeAttr = new int[]{android.R.attr.actionBarSize};
int indexOfAttrTextSize = 0;
TypedArray a = obtainStyledAttributes(new TypedValue().data, textSizeAttr);
int actionBarHeight = a.getDimensionPixelSize(indexOfAttrTextSize, -1);
a.recycle();
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, actionBarHeight);
params.gravity = Gravity.CENTER_VERTICAL;
params.weight = 1;
toolbarSearchView.setLayoutParams(params);
// Setup display
toolbarSearchView.setBackgroundColor(Color.TRANSPARENT);
toolbarSearchView.setPadding(2, 0, 0, 0);
toolbarSearchView.setTextColor(Color.WHITE);
toolbarSearchView.setGravity(Gravity.CENTER_VERTICAL);
toolbarSearchView.setSingleLine(true);
toolbarSearchView.setImeActionLabel("Search", EditorInfo.IME_ACTION_UNSPECIFIED);
toolbarSearchView.setHint("Search");
toolbarSearchView.setHintTextColor(Color.parseColor("#b3ffffff"));
try {
// Set cursor colour to white
// https://stackoverflow.com/a/26544231/1692770
// https://github.com/android/platform_frameworks_base/blob/kitkat-release/core/java/android/widget/TextView.java#L562-564
Field f = TextView.class.getDeclaredField("mCursorDrawableRes");
f.setAccessible(true);
f.set(toolbarSearchView, R.drawable.edittext_whitecursor);
} catch (Exception ignored) {
}
// Search text changed listener
toolbarSearchView.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.container);
if (mainFragment != null && mainFragment instanceof MainListFragment) {
((MainListFragment) mainFragment).search(s.toString());
}
}
#Override
public void afterTextChanged(Editable s) {
// https://stackoverflow.com/a/6438918/1692770
if (s.toString().length() <= 0) {
toolbarSearchView.setHintTextColor(Color.parseColor("#b3ffffff"));
}
}
});
((LinearLayout) searchContainer).addView(toolbarSearchView);
// Setup the clear button
searchClearButton = new ImageView(this);
Resources r = getResources();
int px = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, r.getDisplayMetrics());
LinearLayout.LayoutParams clearParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
clearParams.gravity = Gravity.CENTER;
searchClearButton.setLayoutParams(clearParams);
searchClearButton.setImageResource(R.drawable.ic_close_white_24dp); // TODO: Get this image from here: https://github.com/google/material-design-icons
searchClearButton.setPadding(px, 0, px, 0);
searchClearButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
toolbarSearchView.setText("");
}
});
((LinearLayout) searchContainer).addView(searchClearButton);
// Add search view to toolbar and hide it
searchContainer.setVisibility(View.GONE);
toolbar.addView(searchContainer);
This worked, but then I came across an issue where onOptionsItemSelected() wasn't being called when I tapped on the home button. So I wasn't able to cancel the search by pressing the home button. I tried a few different ways of registering the click listener on the home button but they didn't work. Eventually I found out that the ActionBarDrawerToggle I had was interfering with things, so I removed it. This listener then started working:
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// toolbarHomeButtonAnimating is a boolean that is initialized as false. It's used to stop the user pressing the home button while it is animating and breaking things.
if (!toolbarHomeButtonAnimating) {
// Here you'll want to check if you have a search query set, if you don't then hide the search box.
// My main fragment handles this stuff, so I call its methods.
FragmentManager fragmentManager = getFragmentManager();
final Fragment fragment = fragmentManager.findFragmentById(R.id.container);
if (fragment != null && fragment instanceof MainListFragment) {
if (((MainListFragment) fragment).hasSearchQuery() || searchContainer.getVisibility() == View.VISIBLE) {
displaySearchView(false);
return;
}
}
}
if (mDrawerLayout.isDrawerOpen(findViewById(R.id.navigation_drawer)))
mDrawerLayout.closeDrawer(findViewById(R.id.navigation_drawer));
else
mDrawerLayout.openDrawer(findViewById(R.id.navigation_drawer));
}
});
So I can now cancel the search with the home button, but I can't press the back button to cancel it yet. So I added this to onBackPressed():
FragmentManager fragmentManager = getFragmentManager();
final Fragment mainFragment = fragmentManager.findFragmentById(R.id.container);
if (mainFragment != null && mainFragment instanceof MainListFragment) {
if (((MainListFragment) mainFragment).hasSearchQuery() || searchContainer.getVisibility() == View.VISIBLE) {
displaySearchView(false);
return;
}
}
I created this method to toggle visibility of the EditText and menu item:
public void displaySearchView(boolean visible) {
if (visible) {
// Stops user from being able to open drawer while searching
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
// Hide search button, display EditText
menu.findItem(R.id.action_search).setVisible(false);
searchContainer.setVisibility(View.VISIBLE);
// Animate the home icon to the back arrow
toggleActionBarIcon(ActionDrawableState.ARROW, mDrawerToggle, true);
// Shift focus to the search EditText
toolbarSearchView.requestFocus();
// Pop up the soft keyboard
new Handler().postDelayed(new Runnable() {
public void run() {
toolbarSearchView.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN, 0, 0, 0));
toolbarSearchView.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(), SystemClock.uptimeMillis(), MotionEvent.ACTION_UP, 0, 0, 0));
}
}, 200);
} else {
// Allows user to open drawer again
mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
// Hide the EditText and put the search button back on the Toolbar.
// This sometimes fails when it isn't postDelayed(), don't know why.
toolbarSearchView.postDelayed(new Runnable() {
#Override
public void run() {
toolbarSearchView.setText("");
searchContainer.setVisibility(View.GONE);
menu.findItem(R.id.action_search).setVisible(true);
}
}, 200);
// Turn the home button back into a drawer icon
toggleActionBarIcon(ActionDrawableState.BURGER, mDrawerToggle, true);
// Hide the keyboard because the search box has been hidden
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(toolbarSearchView.getWindowToken(), 0);
}
}
I needed a way to toggle the home button on the toolbar between the drawer icon and the back button. I eventually found the method below in this SO answer. Though I modified it slightly to made more sense to me:
private enum ActionDrawableState {
BURGER, ARROW
}
/**
* Modified version of this, https://stackoverflow.com/a/26836272/1692770<br>
* I flipped the start offset around for the animations because it seemed like it was the wrong way around to me.<br>
* I also added a listener to the animation so I can find out when the home button has finished rotating.
*/
private void toggleActionBarIcon(final ActionDrawableState state, final ActionBarDrawerToggle toggle, boolean animate) {
if (animate) {
float start = state == ActionDrawableState.BURGER ? 1.0f : 0f;
float end = Math.abs(start - 1);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
ValueAnimator offsetAnimator = ValueAnimator.ofFloat(start, end);
offsetAnimator.setDuration(300);
offsetAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
offsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
float offset = (Float) animation.getAnimatedValue();
toggle.onDrawerSlide(null, offset);
}
});
offsetAnimator.addListener(new Animator.AnimatorListener() {
#Override
public void onAnimationStart(Animator animation) {
}
#Override
public void onAnimationEnd(Animator animation) {
toolbarHomeButtonAnimating = false;
}
#Override
public void onAnimationCancel(Animator animation) {
}
#Override
public void onAnimationRepeat(Animator animation) {
}
});
toolbarHomeButtonAnimating = true;
offsetAnimator.start();
}
} else {
if (state == ActionDrawableState.BURGER) {
toggle.onDrawerClosed(null);
} else {
toggle.onDrawerOpened(null);
}
}
}
This works, I've managed to work out a few bugs that I found along the way. I don't think it's 100% but it works well enough for me.
EDIT: If you want to add the search view in XML instead of Java do this:
toolbar.xml:
<android.support.v7.widget.Toolbar
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/toolbar"
contentInsetLeft="72dp"
contentInsetStart="72dp"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:minHeight="?attr/actionBarSize"
app:contentInsetLeft="72dp"
app:contentInsetStart="72dp"
app:popupTheme="#style/ActionBarPopupThemeOverlay"
app:theme="#style/ActionBarThemeOverlay">
<LinearLayout
android:id="#+id/search_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="horizontal">
<EditText
android:id="#+id/search_view"
android:layout_width="0dp"
android:layout_height="?attr/actionBarSize"
android:layout_weight="1"
android:background="#android:color/transparent"
android:gravity="center_vertical"
android:hint="Search"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
android:paddingLeft="2dp"
android:singleLine="true"
android:textColor="#ffffff"
android:textColorHint="#b3ffffff" />
<ImageView
android:id="#+id/search_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:src="#drawable/ic_close_white_24dp" />
</LinearLayout>
</android.support.v7.widget.Toolbar>
onCreate() of your Activity:
searchContainer = findViewById(R.id.search_container);
toolbarSearchView = (EditText) findViewById(R.id.search_view);
searchClearButton = (ImageView) findViewById(R.id.search_clear);
// Setup search container view
try {
// Set cursor colour to white
// https://stackoverflow.com/a/26544231/1692770
// https://github.com/android/platform_frameworks_base/blob/kitkat-release/core/java/android/widget/TextView.java#L562-564
Field f = TextView.class.getDeclaredField("mCursorDrawableRes");
f.setAccessible(true);
f.set(toolbarSearchView, R.drawable.edittext_whitecursor);
} catch (Exception ignored) {
}
// Search text changed listener
toolbarSearchView.addTextChangedListener(new TextWatcher() {
#Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
#Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
Fragment mainFragment = getFragmentManager().findFragmentById(R.id.container);
if (mainFragment != null && mainFragment instanceof MainListFragment) {
((MainListFragment) mainFragment).search(s.toString());
}
}
#Override
public void afterTextChanged(Editable s) {
}
});
// Clear search text when clear button is tapped
searchClearButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
toolbarSearchView.setText("");
}
});
// Hide the search view
searchContainer.setVisibility(View.GONE);

I've just post my open source which imitates exactly what whatsapp toolbar does (Include circular animation).
Code
Full example

Related

Android hamburger menu button shake animation

I need to create an animation to periodically make the hamburger menu button shake. If this were just a view I could figure it out but it isn't. Here's how I'm setting up the icon with a custom image:
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
toolbar.post(new Runnable() {
#Override
public void run() {
Drawable d = ResourcesCompat.getDrawable(getResources(), R.drawable.ham_menu, null);
toolbar.setNavigationIcon(d);
}
});
I can't just animate the toolbar (which is a View where I could set an animation), I need to animate just the menu icon. Searching for an answer is not working because all the results are about the animation between the icon and the arrow when the drawer opens and closes. Is this even possible?
I worked around it by switching the drawable back and forth, but if anyone has a better solution that would be great.
private void animateMenu(final Toolbar toolbar){
// Flash the menu several times quickly, then wait a few seconds. Repeat.
animateMenu(toolbar, 6, R.drawable.ham_menu_2, new GenericCallback() {
#Override
public void success(Object result) {
handler.postDelayed(new Runnable() {
#Override
public void run() {
animateMenu(toolbar);
}
}, 5000);
}
#Override
public void failure(Throwable error) {
}
});
}
private void animateMenu(final Toolbar toolbar, final int count, final int drawable, final GenericCallback callback){
toolbar.postDelayed(new Runnable() {
#Override
public void run() {
Drawable d2 = ResourcesCompat.getDrawable(getResources(), drawable, null);
toolbar.setNavigationIcon(d2);
int i = count - 1;
if(i == 0){
callback.success(null);
}
else {
animateMenu(toolbar, i, drawable == R.drawable.ham_menu ? R.drawable.ham_menu_2 : R.drawable.ham_menu, callback);
}
}
}, 200);
}

How to provide custom animation during sorting (notifyDataSetChanged) on RecyclerView

Currently, by using the default animator android.support.v7.widget.DefaultItemAnimator, here's the outcome I'm having during sorting
DefaultItemAnimator animation video : https://youtu.be/EccI7RUcdbg
public void sortAndNotifyDataSetChanged() {
int i0 = 0;
int i1 = models.size() - 1;
while (i0 < i1) {
DemoModel o0 = models.get(i0);
DemoModel o1 = models.get(i1);
models.set(i0, o1);
models.set(i1, o0);
i0++;
i1--;
//break;
}
// adapter is created via adapter = new RecyclerViewDemoAdapter(models, mRecyclerView, this);
adapter.notifyDataSetChanged();
}
However, instead of the default animation during sorting (notifyDataSetChanged), I prefer to provide custom animation as follow. Old item will slide out via right side, and new item will slide up.
Expected animation video : https://youtu.be/9aQTyM7K4B0
How I achieve such animation without RecylerView
Few years ago, I achieve this effect by using LinearLayout + View, as that time, we don't have RecyclerView yet.
This is how the animation is being setup
PropertyValuesHolder alpha = PropertyValuesHolder.ofFloat("alpha", 1.0f, 0f);
PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", 0f, (float) width);
ObjectAnimator animOut = ObjectAnimator.ofPropertyValuesHolder(this, alpha, translationX);
animOut.setDuration(duration);
animOut.setInterpolator(accelerateInterpolator);
animOut.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator anim) {
final View view = (View) ((ObjectAnimator) anim).getTarget();
Message message = (Message)view.getTag(R.id.TAG_MESSAGE_ID);
if (message == null) {
return;
}
view.setAlpha(0f);
view.setTranslationX(0);
NewsListFragment.this.refreshUI(view, message);
final Animation animation = AnimationUtils.loadAnimation(NewsListFragment.this.getActivity(),
R.anim.slide_up);
animation.setAnimationListener(new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
}
#Override
public void onAnimationEnd(Animation animation) {
view.setVisibility(View.VISIBLE);
view.setTag(R.id.TAG_MESSAGE_ID, null);
}
#Override
public void onAnimationRepeat(Animation animation) {
}
});
view.startAnimation(animation);
}
});
layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animOut);
this.nowLinearLayout.setLayoutTransition(layoutTransition);
and, this is how the animation is being triggered.
// messageView is view being added earlier in nowLinearLayout
for (int i = 0, ei = messageViews.size(); i < ei; i++) {
View messageView = messageViews.get(i);
messageView.setTag(R.id.TAG_MESSAGE_ID, messages.get(i));
messageView.setVisibility(View.INVISIBLE);
}
I was wondering, how I can achieve the same effect in RecylerView?
Here is one more direction you can look at, if you don't want your scroll to reset on each sort (GITHUB demo project):
Use some kind of RecyclerView.ItemAnimator, but instead of rewriting animateAdd() and animateRemove() functions, you can implement animateChange() and animateChangeImpl(). After sort you can call adapter.notifyItemRangeChanged(0, mItems.size()); to triger animation.
So code to trigger animation will look pretty simple:
for (int i = 0, j = mItems.size() - 1; i < j; i++, j--)
Collections.swap(mItems, i, j);
adapter.notifyItemRangeChanged(0, mItems.size());
For animation code you can use android.support.v7.widget.DefaultItemAnimator, but this class has private animateChangeImpl() so you will have to copy-pasted code and changed this method or use reflection. Or you can create your own ItemAnimator class like #Andreas Wenger did in his example of SlidingAnimator. The point here is to implement animateChangeImpl Simmilar to your code there are 2 animations:
1) Slide old view to the right
private void animateChangeImpl(final ChangeInfo changeInfo) {
final RecyclerView.ViewHolder oldHolder = changeInfo.oldHolder;
final View view = oldHolder == null ? null : oldHolder.itemView;
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder;
final View newView = newHolder != null ? newHolder.itemView : null;
if (view == null) return;
mChangeAnimations.add(oldHolder);
final ViewPropertyAnimatorCompat animOut = ViewCompat.animate(view)
.setDuration(getChangeDuration())
.setInterpolator(interpolator)
.translationX(view.getRootView().getWidth())
.alpha(0);
animOut.setListener(new VpaListenerAdapter() {
#Override
public void onAnimationStart(View view) {
dispatchChangeStarting(oldHolder, true);
}
#Override
public void onAnimationEnd(View view) {
animOut.setListener(null);
ViewCompat.setAlpha(view, 1);
ViewCompat.setTranslationX(view, 0);
dispatchChangeFinished(oldHolder, true);
mChangeAnimations.remove(oldHolder);
dispatchFinishedWhenDone();
// starting 2-nd (Slide Up) animation
if (newView != null)
animateChangeInImpl(newHolder, newView);
}
}).start();
}
2) Slide up new view
private void animateChangeInImpl(final RecyclerView.ViewHolder newHolder,
final View newView) {
// setting starting pre-animation params for view
ViewCompat.setTranslationY(newView, newView.getHeight());
ViewCompat.setAlpha(newView, 0);
mChangeAnimations.add(newHolder);
final ViewPropertyAnimatorCompat animIn = ViewCompat.animate(newView)
.setDuration(getChangeDuration())
.translationY(0)
.alpha(1);
animIn.setListener(new VpaListenerAdapter() {
#Override
public void onAnimationStart(View view) {
dispatchChangeStarting(newHolder, false);
}
#Override
public void onAnimationEnd(View view) {
animIn.setListener(null);
ViewCompat.setAlpha(newView, 1);
ViewCompat.setTranslationY(newView, 0);
dispatchChangeFinished(newHolder, false);
mChangeAnimations.remove(newHolder);
dispatchFinishedWhenDone();
}
}).start();
}
Here is demo image with working scroll and kinda similar animation
https://i.gyazo.com/04f4b767ea61569c00d3b4a4a86795ce.gif
https://i.gyazo.com/57a52b8477a361c383d44664392db0be.gif
Edit:
To speed up RecyclerView preformance, instead of adapter.notifyItemRangeChanged(0, mItems.size()); you probably would want to use something like:
LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
int firstVisible = layoutManager.findFirstVisibleItemPosition();
int lastVisible = layoutManager.findLastVisibleItemPosition();
int itemsChanged = lastVisible - firstVisible + 1;
// + 1 because we start count items from 0
adapter.notifyItemRangeChanged(firstVisible, itemsChanged);
First of all:
This solution assumes that items that are still visible, after the dataset changed, also slide out to the right and later slide in from the bottom again (This is at least what I understood you are asking for)
Because of this requirement I couldn't find an easy and nice solution for this problem (At least during the first iteration). The only way I found was to trick the adapter - and fight the framework to do something that it was not intended for. This is why the first part (How it normally works) describes how to achieve nice animations with the RecyclerView the default way. The second part describes the solution how to enforce the slide out/slide in animation for all items after the dataset changed.
Later on I found a better solution that doesn't require to trick the adapter with random ids (jump to the bottom for the updated version).
How it normally works
To enable animations you need to tell the RecyclerView how the dataset changed (So that it knows what kind of animations should be run). This can be done in two ways:
1) Simple Version:
We need to set adapter.setHasStableIds(true); and providing the ids of your items via public long getItemId(int position) in your Adapter to the RecyclerView. The RecyclerView utilizes these ids to figure out which items were removed/added/moved during the call to adapter.notifyDataSetChanged();
2) Advanced Version: Instead of calling adapter.notifyDataSetChanged(); you can also explicitly state how the dataset changed. The Adapter provides several methods, like adapter.notifyItemChanged(int position),adapter.notifyItemInserted(int position),... to describe the changes in the dataset
The animations that are triggered to reflect the changes in the dataset are managed by the ItemAnimator. The RecyclerView is already equipped with a nice default DefaultItemAnimator. Furthermore it is possible to define custom animation behavior with a custom ItemAnimator.
Strategy to implement the slide out (right), slide in (bottom)
The slide to the right is the animation that should be played if items are removed from the dataset. The slide from bottom animation should be played for items that were added to the dataset. As mentioned at the beginning I assume that it is desired that all elements slide out to the right and slide in from the bottom. Even if they are visible before and after the dataset change. Normally RecyclerView would play to change/move animation for such items that stay visible. However, because we want to utilize the remove/add animation for all items we need to trick the adapter into thinking that there are only new elements after the change and all previously available items were removed. This can be achieved by providing a random id for each item in the adapter:
#Override
public long getItemId(int position) {
return Math.round(Math.random() * Long.MAX_VALUE);
}
Now we need to provide a custom ItemAnimator that manages the animations for the added/removed items. The structure of the presented SlidingAnimator is very similar to theandroid.support.v7.widget.DefaultItemAnimator that is provided with the RecyclerView. Also Notice this is a prove of concept and should be adjusted before used in any app:
public class SlidingAnimator extends SimpleItemAnimator {
List<RecyclerView.ViewHolder> pendingAdditions = new ArrayList<>();
List<RecyclerView.ViewHolder> pendingRemovals = new ArrayList<>();
#Override
public void runPendingAnimations() {
final List<RecyclerView.ViewHolder> additionsTmp = pendingAdditions;
List<RecyclerView.ViewHolder> removalsTmp = pendingRemovals;
pendingAdditions = new ArrayList<>();
pendingRemovals = new ArrayList<>();
for (RecyclerView.ViewHolder removal : removalsTmp) {
// run the pending remove animation
animateRemoveImpl(removal);
}
removalsTmp.clear();
if (!additionsTmp.isEmpty()) {
Runnable adder = new Runnable() {
public void run() {
for (RecyclerView.ViewHolder addition : additionsTmp) {
// run the pending add animation
animateAddImpl(addition);
}
additionsTmp.clear();
}
};
// play the add animation after the remove animation finished
ViewCompat.postOnAnimationDelayed(additionsTmp.get(0).itemView, adder, getRemoveDuration());
}
}
#Override
public boolean animateAdd(RecyclerView.ViewHolder holder) {
pendingAdditions.add(holder);
// translate the new items vertically so that they later slide in from the bottom
holder.itemView.setTranslationY(300);
// also make them invisible
holder.itemView.setAlpha(0);
// this requests the execution of runPendingAnimations()
return true;
}
#Override
public boolean animateRemove(final RecyclerView.ViewHolder holder) {
pendingRemovals.add(holder);
// this requests the execution of runPendingAnimations()
return true;
}
private void animateAddImpl(final RecyclerView.ViewHolder holder) {
View view = holder.itemView;
final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
anim
// undo the translation we applied in animateAdd
.translationY(0)
// undo the alpha we applied in animateAdd
.alpha(1)
.setDuration(getAddDuration())
.setInterpolator(new DecelerateInterpolator())
.setListener(new ViewPropertyAnimatorListener() {
#Override
public void onAnimationStart(View view) {
dispatchAddStarting(holder);
}
#Override
public void onAnimationEnd(View view) {
anim.setListener(null);
dispatchAddFinished(holder);
// cleanup
view.setTranslationY(0);
view.setAlpha(1);
}
#Override
public void onAnimationCancel(View view) {
}
}).start();
}
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) {
View view = holder.itemView;
final ViewPropertyAnimatorCompat anim = ViewCompat.animate(view);
anim
// translate horizontally to provide slide out to right
.translationX(view.getWidth())
// fade out
.alpha(0)
.setDuration(getRemoveDuration())
.setInterpolator(new AccelerateInterpolator())
.setListener(new ViewPropertyAnimatorListener() {
#Override
public void onAnimationStart(View view) {
dispatchRemoveStarting(holder);
}
#Override
public void onAnimationEnd(View view) {
anim.setListener(null);
dispatchRemoveFinished(holder);
// cleanup
view.setTranslationX(0);
view.setAlpha(1);
}
#Override
public void onAnimationCancel(View view) {
}
}).start();
}
#Override
public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) {
// don't handle animateMove because there should only be add/remove animations
dispatchMoveFinished(holder);
return false;
}
#Override
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromLeft, int fromTop, int toLeft, int toTop) {
// don't handle animateChange because there should only be add/remove animations
if (newHolder != null) {
dispatchChangeFinished(newHolder, false);
}
dispatchChangeFinished(oldHolder, true);
return false;
}
#Override
public void endAnimation(RecyclerView.ViewHolder item) { }
#Override
public void endAnimations() { }
#Override
public boolean isRunning() { return false; }
}
This is the final result:
Update: While Reading the post again I figured out a better solution
This updated solution doesn't require to trick the adapter with random ids into thinking all items were removed and only new items were added. If we apply the 2) Advanced Version - how to notify the adapter about dataset changes, we can just tell the adapter that all previous items were removed and all the new items were added:
int oldSize = oldItems.size();
oldItems.clear();
// Notify the adapter all previous items were removed
notifyItemRangeRemoved(0, oldSize);
oldItems.addAll(items);
// Notify the adapter all the new items were added
notifyItemRangeInserted(0, items.size());
// don't call notifyDataSetChanged
//notifyDataSetChanged();
The previously presented SlidingAnimator is still necessary to animate the changes.

Delayed panning after map in gallery was moved away by a dialog

I have a map in a gallery, together with other pages.
When I am sliding the gallery pages with touch gestures everything works fine.
But when I move the map away using a button on a dialog, and then slide back to the map, the map does no longer smoothely follow the panning gesture. It rather follows with a noticeable delay.
As soon as I hide the map once again with a dialog and close the dialog, e.g. with the back button, the behaviour returns to normal.
To reproduce it, I have stripped down the code as much as possible.
The gallery contains a DummyView, then a MapView, and finally again a DummyView as pages. DummyView and GalleryView are added as local classes to the Activity.
public class MainActivity extends FragmentActivity {
private static final int REQUEST_CODE_RECOVER_PLAY_SERVICES = 0;
private Gallery gallery;
private MapView mapView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map_in_gallery);
final View[] views = new View[3];
views[0] = new DummyView(this, "Page 1");
views[1] = mapView = new MapView(this);
views[2] = new DummyView(this, "Page 3");
gallery = (Gallery) findViewById(R.id.gallery);
BaseAdapter adapter = new ArrayAdapter<View>(this, 0, views) {
#Override
public View getView(int position, View convertView, ViewGroup parent) {
return views[position];
}
};
gallery.setAdapter(adapter);
// Let every page cover the whole screen
gallery.setUnselectedAlpha(1);
gallery.setSpacing(30);
}
#Override
protected void onResume() {
super.onResume();
checkPlayServices();
};
#Override
/** Define the dialog, which appears, when the Page-Button is pressed and
* which allows to page in the gallery instead of swiping.
*/
protected Dialog onCreateDialog(int id) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Page");
builder.setPositiveButton("->", new OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
page(+1);
}
});
builder.setNegativeButton("<-", new OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
page(-1);
}
});
return builder.create();
}
private void page(int increment) {
int count = gallery.getAdapter().getCount();
int nextPos = gallery.getSelectedItemPosition() + increment;
if (nextPos >= count) {
nextPos = 0;
} else if (nextPos < 0) {
nextPos = count - 1;
}
gallery.setSelection(nextPos);
}
private boolean checkPlayServices() {
int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (status != ConnectionResult.SUCCESS) {
if (GooglePlayServicesUtil.isUserRecoverableError(status)) {
showErrorDialog(status);
} else {
Toast.makeText(this, "This device is not supported.",
Toast.LENGTH_LONG).show();
finish();
}
return false;
}
return true;
}
void showErrorDialog(int code) {
GooglePlayServicesUtil.getErrorDialog(code, this,
REQUEST_CODE_RECOVER_PLAY_SERVICES).show();
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_CODE_RECOVER_PLAY_SERVICES:
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, "Google Play Services must be installed.",
Toast.LENGTH_SHORT).show();
finish();
}
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
public void onButtonClick1(View v) {
showDialog(0); // Only one dialog defined
}
public void onButtonClick2(View v) {
mapView.togglePanOrSlide();
}
}
class DummyView extends View {
private String text;
private Paint paint;
/** Simple view which draws a text in the center of the screen. */
public DummyView(Context context, String text) {
super(context);
this.text = text;
paint = new Paint();
paint.setColor(Color.WHITE);
paint.setTextSize(20);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(text, getWidth() / 2, getHeight() / 2, paint);
}
}
class MapView extends RelativeLayout {
/** Switches the map gestures off, to allow sliding the map in the gallery. */
private boolean panAllowed;
public MapView(Context context) {
super(context);
inflate(context, R.layout.map_fragment, this);
}
public void togglePanOrSlide() {
panAllowed = !panAllowed;
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!panAllowed) {
// Catch the event from the map, so the gallery will slide the page
// instead.
return true;
}
return super.onInterceptTouchEvent(event);
}
}
Here are the two layouts inflated in the coding:
activity_map_in_gallery.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="#+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onButtonClick1"
android:text="Open Paging Dialog" />
<Button
android:id="#+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="onButtonClick2"
android:text="Pan Map <-> Slide Page" />
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="0dip"
android:layout_weight="1" >
<Gallery
android:id="#+id/gallery"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_gravity="center"
android:background="#android:drawable/alert_dark_frame" />
</RelativeLayout>
</LinearLayout>
and map_fragment.xml:
<?xml version="1.0" encoding="utf-8"?>
<!-- Merged into a RelativeLayout-Extension -->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<fragment
android:id="#+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.SupportMapFragment" />
</merge>
The layout adds two buttons on top of the gallery. The first opens a dialog, which allows to page back and forth in the gallery.
The second button switches the maps behaviour. It can be either panned, or its full page can be slid to the left or right.
When the map is paged away using the first button, and then slid back using a slide gesture the problem appears.
The second button allows to switch off the maps panning, so the map reacts on usual gallery sliding. When the map is paged back and forth using sliding gestures, the panning works afterwards without problem (after the second button was pressed again.)
Any idea, why this happens, and how to avoid it?
The fragment transaction is performed before the dialog is closed, this causes the weird behavior.
Instead of directly performing the fragment transaction, post it on the Handler. Once the dialog is closed, then the fragment transaction will be performed.
Try following code. I've applied same logic as done in one of my SO answer.
private void page(int increment) {
int count = gallery.getAdapter().getCount();
int nextPos = gallery.getSelectedItemPosition() + increment;
if (nextPos >= count) {
nextPos = 0;
} else if (nextPos < 0) {
nextPos = count - 1;
}
final int finalNextPos = nextPos;
Handler handler = new Handler();
handler.post(new Runnable() {
#Override
public void run() {
gallery.setSelection(finalNextPos);
}
});
}

How to handle HTML text links in current activity

In my UI I have a list of names which I have displayed using Html.fromHtml() due to the way the names should be highlighted (see pic, the red names).
Under the names I have a ScrollView (see pic, the grey bit). I would like to be able to scroll to a certain part of the scrollview when a name is pressed.
So I sort of have several pieces to solve here:
Make each name clickable individually
Let my current activity handle the click
Not underline the name
OR
4. Solve the text wrapping layout using individual textviews
I know the best thing to do would be to create individual textviews for each name BUT if I do that I lose the text wrapping as seen in the picture.
Thanks for your time.
Edit: I found this Link but it uses an intent so it's not quite the same, I don't want another activity to handle the click, just the current one.
This is how I've added onClick actions to particular words in a string of text.
main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#ffffff">
<TextView
android:id="#+id/mytextview1"
android:textColor="#000000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp" />
</LinearLayout>
Main activity:
public class HtmlTextLinkTestActivity extends Activity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
TextView tv = (TextView) findViewById(R.id.mytextview1);
tv.setText("Whale and Lion");
clickify(tv, "Whale", new ClickSpan.OnClickListener() {
#Override
public void onClick() {
Toast.makeText(HtmlTextLinkTestActivity.this, "Whale was clicked!", Toast.LENGTH_SHORT).show();
}
});
clickify(tv, "Lion", new ClickSpan.OnClickListener() {
#Override
public void onClick() {
Toast.makeText(HtmlTextLinkTestActivity.this, "Lion was clicked!", Toast.LENGTH_SHORT).show();
}
});
}
public static void clickify(TextView view, final String clickableText, final ClickSpan.OnClickListener listener) {
CharSequence text = view.getText();
String string = text.toString();
ClickSpan span = new ClickSpan(listener);
int start = string.indexOf(clickableText);
int end = start + clickableText.length();
if (start == -1) return;
if (text instanceof Spannable) {
((Spannable)text).setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
SpannableString s = SpannableString.valueOf(text);
s.setSpan(span, start, end, Spanned.SPAN_MARK_MARK);
view.setText(s);
}
MovementMethod m = view.getMovementMethod();
if ((m == null) || !(m instanceof LinkMovementMethod)) {
view.setMovementMethod(LinkMovementMethod.getInstance());
}
}
}
class ClickSpan extends ClickableSpan {
private OnClickListener mListener;
public ClickSpan(OnClickListener listener) {
mListener = listener;
}
#Override
public void onClick(View widget) {
if (mListener != null) mListener.onClick();
}
public interface OnClickListener {
void onClick();
}
#Override
public void updateDrawState(TextPaint ds) {
ds.setColor(0xff0000ff); // remove this if you don't want to want to override the textView's color if you specified it in main.xml
}
}

Button in custom Android Toast?

Is it possible to have a button in a Toast?
In theory, yes because you can build a custom Toast from a layout in XML, but I tried to put a button in it and couldn't get it to register the click.
Did anyone manage to do something like that?
A toast can not be clicked. It is not possible to capture a click inside a toast message.
You will need to build a dialog for that. Look at Creating Dialogs for more info.
The API on the Toast class state that a toast will never receive the focus and because a toast is not a view there is no onClick message. I would assume that therefore childs of a Toast can not be clicked as well.
A toast cant contain a button. Except that the gmail app and the gallery app in jelly beans have a semi toast that contains a button, here is how Google did it
https://gist.github.com/benvd/4090998
I guess this answers your question.
Snippet shows implementation of custom Toast that:
Have similar interface as original Toast class
Can be used as Dialog (have clickable buttons like Gmail app)
Have possibility to set length in millis
Have possibility to set show and cancel animation
Lives only with initialized Activity
Current Limitations:
No screen orientation change are supported
Usage:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
View toastView = new View(getBaseContext());
//init your toast view
ActivityToast toast = new ActivityToast(this, toastView);
//set toast Gravity ( Gravity.BOTTOM | Gravity.FILL_HORIZONTAL by default)
toast.setGravity(Gravity.CENTER);
toast.setLength(10000); //set toast show duration to 10 seconds (2 seconds by default)
Animation showAnim; // init animation
Animation.AnimationListener showAnimListener; //init anim listener
toast.setShowAnimation(showAnim);
toast.setShowAnimationListener(showAnimListener);
Animation cancelAnim; // init animation
Animation.AnimationListener cancelAnimListener; //init anim listener
toast.setCancelAnimation(showAnim);
toast.setCancelAnimationListener(showAnimListener);
toast.show(); //show toast view
toast.isShowing(); // check if toast is showing now
toast.cancel(); //cancel toast view
toast.getView(); //get toast view to update it or to do something ..
}
Sources
import android.app.Activity;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.FrameLayout;
public class ActivityToast {
public static final long LENGTH_SHORT = 2000;
public static final long LENGTH_LONG = 3000;
public static final int DEFAULT_ANIMATION_DURATION = 400;
private final Activity mActivity;
private FrameLayout.LayoutParams mLayoutParams;
private Handler mHandler = new Handler();
private ViewGroup mParent;
private FrameLayout mToastHolder;
private View mToastView;
private Animation mShowAnimation;
private Animation mCancelAnimation;
private long mLength = LENGTH_SHORT;
private Animation.AnimationListener mShowAnimationListener;
private Animation.AnimationListener mCancelAnimationListener;
private boolean mIsAnimationRunning;
private boolean mIsShown;
/**
* #param activity Toast will be shown at top of the widow of this Activity
*/
public ActivityToast(#NonNull Activity activity, View toastView) {
mActivity = activity;
mParent = (ViewGroup) activity.getWindow().getDecorView();
mToastHolder = new FrameLayout(activity.getBaseContext());
mLayoutParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
Gravity.BOTTOM | Gravity.FILL_HORIZONTAL
);
mToastHolder.setLayoutParams(mLayoutParams);
mShowAnimation = new AlphaAnimation(0.0f, 1.0f);
mShowAnimation.setDuration(DEFAULT_ANIMATION_DURATION);
mShowAnimation.setAnimationListener(mHiddenShowListener);
mCancelAnimation = new AlphaAnimation(1.0f, 0.0f);
mCancelAnimation.setDuration(DEFAULT_ANIMATION_DURATION);
mCancelAnimation.setAnimationListener(mHiddenCancelListener);
mToastView = toastView;
mToastHolder.addView(mToastView);
mToastHolder.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
cancel();
}
return false;
}
});
}
public void show() {
if (!isShowing()) {
mParent.addView(mToastHolder);
mIsShown = true;
if (mShowAnimation != null) {
mToastHolder.startAnimation(mShowAnimation);
} else {
mHandler.postDelayed(mCancelTask, mLength);
}
}
}
public void cancel() {
if (isShowing() && !mIsAnimationRunning) {
if (mCancelAnimation != null) {
mToastHolder.startAnimation(mCancelAnimation);
} else {
mParent.removeView(mToastHolder);
mHandler.removeCallbacks(mCancelTask);
mIsShown = false;
}
}
}
public boolean isShowing() {
return mIsShown;
}
/**
* Pay attention that Action bars is the part of Activity window
*
* #param gravity Position of view in Activity window
*/
public void setGravity(int gravity) {
mLayoutParams.gravity = gravity;
if (isShowing()) {
mToastHolder.requestLayout();
}
}
public void setShowAnimation(Animation showAnimation) {
mShowAnimation = showAnimation;
}
public void setCancelAnimation(Animation cancelAnimation) {
mCancelAnimation = cancelAnimation;
}
/**
* #param cancelAnimationListener cancel toast animation. Note: you should use this instead of
* Animation.setOnAnimationListener();
*/
public void setCancelAnimationListener(Animation.AnimationListener cancelAnimationListener) {
mCancelAnimationListener = cancelAnimationListener;
}
/**
* #param showAnimationListener show toast animation. Note: you should use this instead of
* Animation.setOnAnimationListener();
*/
public void setShowAnimationListener(Animation.AnimationListener showAnimationListener) {
mShowAnimationListener = showAnimationListener;
}
public void setLength(long length) {
mLength = length;
}
public View getView() {
return mToastView;
}
private Runnable mCancelTask = new Runnable() {
#Override
public void run() {
cancel();
}
};
private Animation.AnimationListener mHiddenShowListener = new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
if (mShowAnimationListener != null) {
mShowAnimationListener.onAnimationStart(animation);
}
mIsAnimationRunning = true;
}
#Override
public void onAnimationEnd(Animation animation) {
mHandler.postDelayed(mCancelTask, mLength);
if (mShowAnimationListener != null) {
mShowAnimationListener.onAnimationEnd(animation);
}
mIsAnimationRunning = false;
}
#Override
public void onAnimationRepeat(Animation animation) {
if (mShowAnimationListener != null) {
mShowAnimationListener.onAnimationRepeat(animation);
}
}
};
private Animation.AnimationListener mHiddenCancelListener = new Animation.AnimationListener() {
#Override
public void onAnimationStart(Animation animation) {
if (mCancelAnimationListener != null) {
mCancelAnimationListener.onAnimationStart(animation);
}
mIsAnimationRunning = true;
}
#Override
public void onAnimationEnd(Animation animation) {
mParent.removeView(mToastHolder);
mHandler.removeCallbacks(mCancelTask);
if (mCancelAnimationListener != null) {
mCancelAnimationListener.onAnimationEnd(animation);
}
mIsAnimationRunning = false;
mIsShown = false;
}
#Override
public void onAnimationRepeat(Animation animation) {
if (mCancelAnimationListener != null) {
mCancelAnimationListener.onAnimationRepeat(animation);
}
}
};
}
My original post on github
Post that shows implementation of custom layout in this post
A custom view passed to a toast can contain anything; however, toasts cannot receive any touch events so no components that use touch events will work in a stock toast (buttons, radiobuttons, etc.). The only choice you have is to create a custom view with a button in it and add it to your layout. There are many examples of how to do this and a few libraries you can check out to see how other people are doing it.
UndoBar
MessageBar
Nurik's UndoBar
Of course you are also welcome to use the SuperToasts library I put together however it might be a little overkill for one usage. The way that I do it is outlined in the SuperActivityToast class.
You should use a Snackbar. It is in the latest android support library(at time of answer) and is compatible with older api levels. It is much easier to implement than a Dialog or custom View and has the ability to have a button unlike a Toast.
Download Android Support Library from Extras in the SDK Manager(revision 22.2.1 or later).
In the build.gradle add this to the class dependencies: com.android.support:design:22.2.0.
Implement:
Snackbar.make(this.findViewById(android.R.id.content), "Toast Message", Snackbar.LENGTH_LONG)
.setAction("Click here to activate action", onClickListener)
.setActionTextColor(Color.RED)
.show;
And that is it. No github projects and implementation is very similiar to Toast. I used it in one of my projects and it works great.
You can try SuperToast in this case. It can create toast with button. It has custom duration feature, colourful background, colourful fonts, custom fonts, animated effect. Hope u will enjoy it
Use an alertbox, if you want to add a button :-). Here are some examples
Dialog boxes in Android
Creating a system overlay window (always on top)
This is suggesting that it can be done, I also need buttons in a toast so I still have to make my own implementation. If I find more I will add it to my post

Categories

Resources