I'm having an issue with FAB being anchored to a listview (See Video Example), it seems to flicker on draw by changing the anchor position. The problem appears within the emulator, and also on any device I have tested on post API level 19.
I have the main activity:
<LinearLayout
android:id="@+id/main_layout"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- our toolbar -->
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
android:elevation="4dp"/>
<!-- our tablayout to display tabs -->
<android.support.design.widget.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
android:elevation="4dp"/>
<!-- View pager to swipe views -->
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="fill_parent" />
</LinearLayout>
Which then holds the bottom nav
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#fff">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/svBody"
android:background="#fff">
</FrameLayout>
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottomNavigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:itemBackground="@color/colorPrimary"
android:background="@color/colorPrimary"
app:itemIconTint="@color/cardview_light_background"
app:itemTextColor="@color/cardview_light_background"
app:menu="@menu/bottom_navigation_main">
</android.support.design.widget.BottomNavigationView>
</LinearLayout>
Which holds the fragment:
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/lvWords"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#fff">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal"
android:background="#fff"
android:elevation="4dp">
<TextView
android:text="Lipshapes"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:id="@+id/lipshapes_titlestrip"
android:background="#33b5e5"
android:textColor="#fff"
android:textSize="32sp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:elevation="6dp"
android:paddingLeft="@dimen/margin_medium"
android:paddingRight="@dimen/margin_medium"/>
<TextView
android:id="@+id/words_titlestrip"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="bottom|right"
android:layout_weight="1"
android:background="#059FD8"
android:elevation="10dp"
android:paddingBottom="4dp"
android:paddingLeft="@dimen/margin_medium"
android:paddingRight="@dimen/margin_medium"
android:paddingTop="4dp"
android:text="Words"
android:textColor="#fff"
android:textSize="32sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="0dp"
android:orientation="vertical"
android:layout_weight="1"
android:elevation="0dp"
android:background="#fff">
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="@drawable/shadow" />
</LinearLayout>
</LinearLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="16dp"
android:src="@drawable/plus_white_48"
app:layout_anchor="@android:id/list"
app:layout_anchorGravity="bottom|right|end"
app:fabSize="normal" />
And here is that fragments code:
public static class WordListSectionFragment extends ListFragment implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener, View.OnClickListener, LoaderManager.LoaderCallbacks<Cursor> {
public SimpleCursorAdapter wordAdapter;
final static int LIST_VIEW = 0;
private FloatingActionButton fab;
String lipshape = null;
int lipshape_id = 0;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_section_wordlist, container, false);
fab = (FloatingActionButton) rootView.findViewById(R.id.fab);
Bundle args = getArguments();
lipshape = args.getString("lipshape_selected_desc");
lipshape_id = Integer.valueOf(args.getString("lipshape_selected_id"));
((TextView) rootView.findViewById(R.id.words_titlestrip)).setText(
lipshape
);
TextView lipshapesTV = (TextView) rootView.findViewById(R.id.lipshapes_titlestrip);
lipshapesTV.setOnClickListener(this);
return rootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// if you only want one column do it like this
// String[] projection = new String[]{BaseColumns._ID, VideoFilesContract.videoFiles.FILENAME};
wordAdapter =
new SimpleCursorAdapter(
getContext(),
R.layout.item_word,
null,
new String[] {WordContract.wordItems.DESCRIPTION, "number_of_videos"},
new int[] {R.id.tvWord, R.id.tvVideoCount},
LIST_VIEW);
// Setup cursor adapter using cursor from last step
setListAdapter(wordAdapter);
//getListView().setOnItemClickListener(this);
getLoaderManager().initLoader(LIST_VIEW, null, this);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
getListView().setOnItemClickListener(this);
getListView().setOnItemLongClickListener(this);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Click action
//Toast.makeText(getActivity(), "Button Clicked!", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(getActivity(), AddWord.class);
intent.putExtra("lipshape_selected_id", lipshape_id);
intent.putExtra("lipshape_selected_desc", lipshape);
startActivityForResult(intent, 1);
}
});
}
public void restartLoader()
{
wordAdapter.swapCursor(null);
getLoaderManager().restartLoader(LIST_VIEW, null, this);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch(requestCode) {
case (1) : {
if (resultCode == Activity.RESULT_OK) {
Boolean returnValue = data.getBooleanExtra("added_word", false);
if (returnValue)
{
restartLoader();
Snackbar.make(getView().getRootView().findViewById(R.id.lvWords), "Word Added Successfully!", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
}
break;
}
}
}
@Override
public void onClick(View v)
{
if (v.getId() == R.id.lipshapes_titlestrip)
{
LipshapeSectionFragment fragment = new LipshapeSectionFragment();
FragmentManager fragmentManager = getFragmentManager();
fragmentManager.beginTransaction().replace(R.id.svBody, fragment).commit();
}
}
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position,long id)
{
Cursor _cursor = ((Cursor) getListView().getItemAtPosition(position));
Cursor cursor;
int count =0;
//do a count for videos if 0
int word_id = _cursor.getInt(_cursor.getColumnIndex("_id"));
String word = _cursor.getString(_cursor.getColumnIndex(WordContract.wordItems.DESCRIPTION));
ContentResolver resolver = getContext().getContentResolver();
if (word_id!=0) {
cursor =
resolver.query(VideoFilesContract.videoFiles.CONTENT_URI,
VideoFilesContract.videoFiles.PROJECTION_ALL,
"word_id=?",
new String[]{Integer.toString(word_id)},
null,
null);
count = cursor.getCount();
}
switch(count)
{
case 0:
Snackbar.make(view, "There are no videos recorded with this word.", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
break;
default:
Intent intent = new Intent(getActivity(), CollectionDemoActivity.class);
intent.putExtra("word_id", word_id);
intent.putExtra("word", word);
startActivity(intent);
}
//Toast.makeText(getActivity(), "Item: " + test, Toast.LENGTH_SHORT).show();
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle bundle) {
CursorLoader loader = null;
Bundle args = getArguments();
switch (id) {
case LIST_VIEW:
loader = new CursorLoader(
this.getActivity(),
WordContract.wordItems.CONTENT_TEST,
new String[]{WordContract.wordItems._ID, WordContract.wordItems.DESCRIPTION, "number_of_videos"},
null,
new String[]{args.getString("lipshape_selected_id")},
null);
return loader;
default:
return loader;
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
((SimpleCursorAdapter)this.getListAdapter()).
swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
((SimpleCursorAdapter)this.getListAdapter()).
swapCursor(null);
}
public void showDialog(final int word_id, String word_clicked) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("Delete Word: "+ word_clicked + "?");
builder.setMessage("Deleting this word will also delete all videos tagged by this word.");
String positiveText = "Delete";
builder.setPositiveButton(positiveText,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
deleteWord(word_id);
//reload
}
});
String negativeText = getString(android.R.string.cancel);
builder.setNegativeButton(negativeText,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// negative button logic
}
});
AlertDialog dialog = builder.create();
// display dialog
dialog.show();
}
public boolean deleteWord(int word_id)
{
ContentResolver resolver = getContext().getContentResolver();
Uri delUri = ContentUris.withAppendedId(WordContract.wordItems.CONTENT_URI, word_id);
//resolver.delete(VideoFilesContract.videoFiles.CONTENT_URI, "_id=?", selectionArgs);
long resultCount = resolver.delete(delUri, null, null);
if (resultCount == 0)
{
//couldn't delete word with that id
return false;
}
else
{
restartLoader();
Snackbar.make(getListView(), "Word deleted successfully.", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
return true;
}
}
@Override
public boolean onItemLongClick(AdapterView<?> adapterView, View view, int i, long l)
{
//Snackbar.make(view, "Item has been long clicked!", Snackbar.LENGTH_LONG)
// .setAction("Action", null).show();
Cursor _cursor = ((Cursor) getListView().getItemAtPosition(i));
Cursor cursor;
int count =0;
//do a count for videos if 0
int word_id = _cursor.getInt(_cursor.getColumnIndex("_id"));
String word = _cursor.getString(_cursor.getColumnIndex(WordContract.wordItems.DESCRIPTION));
showDialog(word_id, word);
return true;
}
}
I couldn't find a cleaner fix than setting the FAB's visibility to gone
initially, but setting the anchor to the listview:
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right|end"
android:layout_margin="16dp"
android:src="@drawable/plus_white_48"
android:visibility="gone"
app:layout_anchor="@android:id/list"
app:layout_anchorGravity="bottom|right|end"
app:fabSize="normal" />
And then if the list is not empty after the cursor has finished loading simply make it visible and it anchors as above and in cases where the lis is empty, we set the layout params to anchor on the empty list:
if (wordAdapter.getCursor().getCount()<1)
{
//the linear layout for the empty list view state
ll.setVisibility(View.VISIBLE);
//set the text of this to be visible too, default is invisible as otherwise it's visible when the list is loading
tvFeedback.setVisibility(View.VISIBLE);
CoordinatorLayout.LayoutParams p (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
p.setAnchorId(R.id.empty_list_view);
fab.setLayoutParams(p);
fab.setVisibility(View.VISIBLE);
}
This results in no flickering when loading, also results in the anchor always being at the correct place even when items from the list are deleted.
You see the FAB at the left top corner because at that point the ListView
are not laid out on the ListView
. Once the items are populated the ListView
matches its parent and FAB is positioned properly.
Attach the anchor of the FAB to a parent that already matches entire space and holds your ListView
.
Something like this
<LinearLayout
**android:id="@+id/list_parent"**
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#fff">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:orientation="horizontal"
android:background="#fff"
android:elevation="4dp">
<TextView
android:text="Lipshapes"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:id="@+id/lipshapes_titlestrip"
android:background="#33b5e5"
android:textColor="#fff"
android:textSize="32sp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:elevation="6dp"
android:paddingLeft="@dimen/margin_medium"
android:paddingRight="@dimen/margin_medium"/>
<TextView
android:id="@+id/words_titlestrip"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="bottom|right"
android:layout_weight="1"
android:background="#059FD8"
android:elevation="10dp"
android:paddingBottom="4dp"
android:paddingLeft="@dimen/margin_medium"
android:paddingRight="@dimen/margin_medium"
android:paddingTop="4dp"
android:text="Words"
android:textColor="#fff"
android:textSize="32sp" />
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="0dp"
android:orientation="vertical"
android:layout_weight="1"
android:elevation="0dp"
android:background="#fff">
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="@drawable/shadow" />
</LinearLayout>
</LinearLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="16dp"
android:src="@drawable/plus_white_48"
app:layout_anchor="@+id/list_parent"
app:layout_anchorGravity="bottom|right|end"
app:fabSize="normal" />
Friendly tip, switch to RecyclerView
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With