Added swipe actions for messages.

- there is a minor layout problem on pre-Lollipop devices
This commit is contained in:
Christian Basler 2016-09-21 23:46:57 +02:00
parent d416db1307
commit 1c226a6a5b
21 changed files with 790 additions and 185 deletions

View File

@ -28,7 +28,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label;
/**
* @author Christian Basler
*/
public abstract class AbstractItemListFragment<T> extends ListFragment {
public abstract class AbstractItemListFragment<T> extends ListFragment implements ListHolder {
/**
* The serialization (saved instance state) Bundle key representing the
* activated item position. Only used on tablets.
@ -55,8 +55,6 @@ public abstract class AbstractItemListFragment<T> extends ListFragment {
private int activatedPosition = ListView.INVALID_POSITION;
private boolean activateOnItemClick;
abstract void updateList(Label label);
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

View File

@ -166,7 +166,7 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr
}
@Override
void updateList(Label label) {
public void updateList(Label label) {
updateList();
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit;
import ch.dissem.bitmessage.entity.valueobject.Label;
/**
* @author Christian Basler
*/
public interface ListHolder {
void updateList(Label label);
void setActivateOnItemClick(boolean activateOnItemClick);
}

View File

@ -216,7 +216,7 @@ public class MainActivity extends AppCompatActivity
}
}
private void changeList(AbstractItemListFragment<?> listFragment) {
private <F extends Fragment & ListHolder> void changeList(F listFragment) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.item_list, listFragment)

View File

@ -204,7 +204,7 @@ public class MessageDetailFragment extends Fragment {
}
}
private boolean isInTrash(Plaintext item) {
public static boolean isInTrash(Plaintext item) {
for (Label label : item.getLabels()) {
if (label.getType() == Label.Type.TRASH) {
return true;

View File

@ -17,19 +17,29 @@
package ch.dissem.apps.abit;
import android.content.Intent;
import android.graphics.Typeface;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator;
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager;
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager;
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils;
import java.util.List;
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter;
import ch.dissem.apps.abit.listener.ActionBarListener;
import ch.dissem.apps.abit.listener.ListSelectionListener;
import ch.dissem.apps.abit.service.Singleton;
@ -37,6 +47,8 @@ import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.Label;
import ch.dissem.bitmessage.ports.MessageRepository;
import static ch.dissem.apps.abit.MessageDetailFragment.isInTrash;
/**
* A list fragment representing a list of Messages. This fragment
* also supports tablet devices by allowing list items to be given an
@ -46,10 +58,19 @@ import ch.dissem.bitmessage.ports.MessageRepository;
* Activities containing this fragment MUST implement the {@link ListSelectionListener}
* interface.
*/
public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
public class MessageListFragment extends Fragment implements ListHolder {
private RecyclerView recyclerView;
private RecyclerView.LayoutManager layoutManager;
private SwipeableMessageAdapter adapter;
private RecyclerView.Adapter wrappedAdapter;
private RecyclerViewSwipeManager recyclerViewSwipeManager;
private RecyclerViewTouchActionGuardManager recyclerViewTouchActionGuardManager;
private Label currentLabel;
private MenuItem emptyTrashMenuItem;
private MessageRepository messageRepo;
private List<Plaintext> messages;
/**
* Mandatory empty constructor for the fragment manager to instantiate the
@ -68,8 +89,10 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
@Override
public void onResume() {
super.onResume();
MainActivity activity = (MainActivity) getActivity();
messageRepo = Singleton.getMessageRepository(activity);
doUpdateList(((MainActivity) getActivity()).getSelectedLabel());
doUpdateList(activity.getSelectedLabel());
}
@Override
@ -82,34 +105,7 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
}
private void doUpdateList(Label label) {
setListAdapter(new ArrayAdapter<Plaintext>(
getActivity(),
android.R.layout.simple_list_item_activated_1,
android.R.id.text1,
Singleton.getMessageRepository(getContext()).findMessages(label)) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = LayoutInflater.from(getContext());
convertView = inflater.inflate(R.layout.message_row, null, false);
}
Plaintext item = getItem(position);
((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item.getFrom()));
TextView sender = (TextView) convertView.findViewById(R.id.sender);
sender.setText(item.getFrom().toString());
TextView subject = (TextView) convertView.findViewById(R.id.subject);
subject.setText(item.getSubject());
((TextView) convertView.findViewById(R.id.text)).setText(item.getText());
if (item.isUnread()) {
sender.setTypeface(Typeface.DEFAULT_BOLD);
subject.setTypeface(Typeface.DEFAULT_BOLD);
} else {
sender.setTypeface(Typeface.DEFAULT);
subject.setTypeface(Typeface.DEFAULT);
}
return convertView;
}
});
messages = Singleton.getMessageRepository(getContext()).findMessages(label);
if (getActivity() instanceof ActionBarListener) {
if (label != null) {
((ActionBarListener) getActivity()).updateTitle(label.toString());
@ -120,26 +116,127 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
if (emptyTrashMenuItem != null) {
emptyTrashMenuItem.setVisible(label != null && label.getType() == Label.Type.TRASH);
}
adapter.setData(label, messages);
adapter.notifyDataSetChanged();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_message_list, container, false);
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
// Show the dummy content as text in a TextView.
FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id.fab_compose_message);
FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id
.fab_compose_message);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class);
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity(getActivity()));
Intent intent = new Intent(getActivity().getApplicationContext(),
ComposeMessageActivity.class);
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity
(getActivity()));
startActivity(intent);
}
});
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
// animation is running)
recyclerViewTouchActionGuardManager = new RecyclerViewTouchActionGuardManager();
recyclerViewTouchActionGuardManager.setInterceptVerticalScrollingWhileAnimationRunning
(true);
recyclerViewTouchActionGuardManager.setEnabled(true);
// swipe manager
recyclerViewSwipeManager = new RecyclerViewSwipeManager();
//adapter
adapter = new SwipeableMessageAdapter();
adapter.setEventListener(new SwipeableMessageAdapter.EventListener() {
@Override
public void onItemDeleted(Plaintext item) {
if (isInTrash(item)) {
messageRepo.remove(item);
} else {
item.getLabels().clear();
item.addLabels(messageRepo.getLabels(Label.Type.TRASH));
messageRepo.save(item);
}
}
@Override
public void onItemArchived(Plaintext item) {
item.getLabels().clear();
messageRepo.save(item);
}
@Override
public void onItemViewClicked(View v, boolean pinned) {
int position = recyclerView.getChildAdapterPosition(v);
if (position != RecyclerView.NO_POSITION) {
Plaintext item = adapter.getItem(position);
((MainActivity) getActivity()).onItemSelected(item);
}
}
});
// wrap for swiping
wrappedAdapter = recyclerViewSwipeManager.createWrappedAdapter(adapter);
final GeneralItemAnimator animator = new SwipeDismissItemAnimator();
// Change animations are enabled by default since support-v7-recyclerview v22.
// Disable the change animation in order to make turning back animation of swiped item
// works properly.
animator.setSupportsChangeAnimations(false);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(wrappedAdapter); // requires *wrapped* adapter
recyclerView.setItemAnimator(animator);
recyclerView.addItemDecoration(new SimpleListDividerDecorator(
ContextCompat.getDrawable(getContext(), R.drawable.list_divider_h), true));
// NOTE:
// The initialization order is very important! This order determines the priority of
// touch event handling.
//
// priority: TouchActionGuard > Swipe > DragAndDrop
recyclerViewTouchActionGuardManager.attachRecyclerView(recyclerView);
recyclerViewSwipeManager.attachRecyclerView(recyclerView);
return rootView;
}
@Override
public void onDestroyView() {
if (recyclerViewSwipeManager != null) {
recyclerViewSwipeManager.release();
recyclerViewSwipeManager = null;
}
if (recyclerViewTouchActionGuardManager != null) {
recyclerViewTouchActionGuardManager.release();
recyclerViewTouchActionGuardManager = null;
}
if (recyclerView != null) {
recyclerView.setItemAnimator(null);
recyclerView.setAdapter(null);
recyclerView = null;
}
if (wrappedAdapter != null) {
WrapperAdapterUtils.releaseAll(wrappedAdapter);
wrappedAdapter = null;
}
adapter = null;
layoutManager = null;
super.onDestroyView();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.message_list, menu);
@ -164,4 +261,8 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
}
}
@Override
public void setActivateOnItemClick(boolean activateOnItemClick) {
// TODO
}
}

View File

@ -0,0 +1,282 @@
package ch.dissem.apps.abit.adapter;
import android.graphics.Typeface;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action
.SwipeResultActionMoveToSwipedDirection;
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem;
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder;
import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils;
import java.util.Collections;
import java.util.List;
import ch.dissem.apps.abit.Identicon;
import ch.dissem.apps.abit.R;
import ch.dissem.bitmessage.entity.Plaintext;
import ch.dissem.bitmessage.entity.valueobject.Label;
/**
* @author Christian Basler
*/
public class SwipeableMessageAdapter
extends RecyclerView.Adapter<SwipeableMessageAdapter.MyViewHolder>
implements SwipeableItemAdapter<SwipeableMessageAdapter.MyViewHolder>, SwipeableItemConstants {
private List<Plaintext> data = Collections.emptyList();
private EventListener eventListener;
private View.OnClickListener itemViewOnClickListener;
private View.OnClickListener swipeableViewContainerOnClickListener;
private Label label;
public interface EventListener {
void onItemDeleted(Plaintext item);
void onItemArchived(Plaintext item);
void onItemViewClicked(View v, boolean pinned);
}
public static class MyViewHolder extends AbstractSwipeableItemViewHolder {
public FrameLayout container;
public final ImageView avatar;
public final TextView sender;
public final TextView subject;
public final TextView extract;
public MyViewHolder(View v) {
super(v);
container = (FrameLayout) v.findViewById(R.id.container);
avatar = (ImageView) v.findViewById(R.id.avatar);
sender = (TextView) v.findViewById(R.id.sender);
subject = (TextView) v.findViewById(R.id.subject);
extract = (TextView) v.findViewById(R.id.text);
}
@Override
public View getSwipeableContainerView() {
return container;
}
}
public SwipeableMessageAdapter() {
itemViewOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
onItemViewClick(v);
}
};
swipeableViewContainerOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
onSwipeableViewContainerClick(v);
}
};
// SwipeableItemAdapter requires stable ID, and also
// have to implement the getItemId() method appropriately.
setHasStableIds(true);
}
public void setData(Label label, List<Plaintext> data) {
this.label = label;
this.data = data;
}
private void onItemViewClick(View v) {
if (eventListener != null) {
eventListener.onItemViewClicked(v, true); // pinned
}
}
private void onSwipeableViewContainerClick(View v) {
if (eventListener != null) {
eventListener.onItemViewClicked(
RecyclerViewAdapterUtils.getParentViewHolderItemView(v), false); // not pinned
}
}
public Plaintext getItem(int position) {
return data.get(position);
}
@Override
public long getItemId(int position) {
return (long) data.get(position).getId();
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final View v = inflater.inflate(R.layout.message_row, parent, false);
return new MyViewHolder(v);
}
@Override
public void onBindViewHolder(MyViewHolder holder, int position) {
final Plaintext item = data.get(position);
// set listeners
// (if the item is *pinned*, click event comes to the itemView)
holder.itemView.setOnClickListener(itemViewOnClickListener);
// (if the item is *not pinned*, click event comes to the container)
holder.container.setOnClickListener(swipeableViewContainerOnClickListener);
// set data
holder.avatar.setImageDrawable(new Identicon(item.getFrom()));
holder.sender.setText(item.getFrom().toString());
holder.subject.setText(item.getSubject());
holder.extract.setText(item.getText());
if (item.isUnread()) {
holder.sender.setTypeface(Typeface.DEFAULT_BOLD);
holder.subject.setTypeface(Typeface.DEFAULT_BOLD);
} else {
holder.sender.setTypeface(Typeface.DEFAULT);
holder.subject.setTypeface(Typeface.DEFAULT);
}
}
@Override
public int getItemCount() {
return data.size();
}
@Override
public int onGetSwipeReactionType(MyViewHolder holder, int position, int x, int y) {
if (label == null) {
return REACTION_CAN_NOT_SWIPE_BOTH_H_WITH_RUBBER_BAND_EFFECT;
}
if (label.getType() == Label.Type.TRASH) {
return REACTION_CAN_SWIPE_LEFT | REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT;
}
return REACTION_CAN_SWIPE_BOTH_H;
}
@Override
public void onSetSwipeBackground(MyViewHolder holder, int position, int type) {
int bgRes = 0;
if (label == null) {
bgRes = R.drawable.bg_swipe_item_neutral;
} else {
switch (type) {
case DRAWABLE_SWIPE_NEUTRAL_BACKGROUND:
bgRes = R.drawable.bg_swipe_item_neutral;
break;
case DRAWABLE_SWIPE_LEFT_BACKGROUND:
bgRes = R.drawable.bg_swipe_item_left;
break;
case DRAWABLE_SWIPE_RIGHT_BACKGROUND:
if (label.getType() == Label.Type.TRASH) {
bgRes = R.drawable.bg_swipe_item_neutral;
} else {
bgRes = R.drawable.bg_swipe_item_right;
}
break;
}
}
holder.itemView.setBackgroundResource(bgRes);
}
@Override
public SwipeResultAction onSwipeItem(MyViewHolder holder, final int position, int result) {
switch (result) {
// swipe right
case RESULT_SWIPED_RIGHT:
return new SwipeRightResultAction(this, position);
case RESULT_SWIPED_LEFT:
return new SwipeLeftResultAction(this, position);
// other --- do nothing
case RESULT_CANCELED:
default:
return null;
}
}
public void setEventListener(EventListener eventListener) {
this.eventListener = eventListener;
}
private static class SwipeLeftResultAction extends SwipeResultActionMoveToSwipedDirection {
private SwipeableMessageAdapter adapter;
private final int position;
private final Plaintext item;
SwipeLeftResultAction(SwipeableMessageAdapter adapter, int position) {
this.adapter = adapter;
this.position = position;
this.item = adapter.data.get(position);
}
@Override
protected void onPerformAction() {
super.onPerformAction();
adapter.data.remove(position);
adapter.notifyItemRemoved(position);
}
@Override
protected void onSlideAnimationEnd() {
super.onSlideAnimationEnd();
if (adapter.eventListener != null) {
adapter.eventListener.onItemDeleted(item);
}
}
@Override
protected void onCleanUp() {
super.onCleanUp();
// clear the references
adapter = null;
}
}
private static class SwipeRightResultAction extends SwipeResultActionRemoveItem {
private SwipeableMessageAdapter adapter;
private final int position;
private final Plaintext item;
SwipeRightResultAction(SwipeableMessageAdapter adapter, int position) {
this.adapter = adapter;
this.position = position;
this.item = adapter.data.get(position);
}
@Override
protected void onPerformAction() {
super.onPerformAction();
adapter.data.remove(position);
adapter.data.remove(position);
adapter.notifyItemRemoved(position);
}
@Override
protected void onSlideAnimationEnd() {
super.onSlideAnimationEnd();
if (adapter.eventListener != null) {
adapter.eventListener.onItemArchived(item);
}
}
@Override
protected void onCleanUp() {
super.onCleanUp();
// clear the references
adapter = null;
}
}
}

View File

@ -28,6 +28,7 @@ import android.os.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import ch.dissem.apps.abit.service.Singleton;
@ -35,6 +36,7 @@ import ch.dissem.apps.abit.util.Preferences;
import ch.dissem.bitmessage.BitmessageContext;
import ch.dissem.bitmessage.entity.BitmessageAddress;
import ch.dissem.bitmessage.entity.CustomMessage;
import ch.dissem.bitmessage.exception.DecryptionFailedException;
import ch.dissem.bitmessage.extensions.CryptoCustomMessage;
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest;
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
@ -68,6 +70,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
@Override
public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) {
try {
if (account.equals(Authenticator.ACCOUNT_SYNC)) {
if (Preferences.isConnectionAllowed(getContext())) {
syncData();
@ -75,11 +78,16 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
} else if (account.equals(Authenticator.ACCOUNT_POW)) {
syncPOW();
} else {
throw new RuntimeException("Unknown " + account);
syncResult.stats.numAuthExceptions++;
}
} catch (IOException e) {
syncResult.stats.numIoExceptions++;
} catch (DecryptionFailedException e) {
syncResult.stats.numAuthExceptions++;
}
}
private void syncData() {
private void syncData() throws IOException {
// If the Bitmessage context acts as a full node, synchronization isn't necessary
if (bmc.isRunning()) {
LOG.info("Synchronization skipped, Abit is acting as a full node");
@ -87,7 +95,6 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
}
LOG.info("Synchronizing Bitmessage");
try {
LOG.info("Synchronization started");
bmc.synchronize(
Preferences.getTrustedNode(getContext()),
@ -95,16 +102,12 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
Preferences.getTimeoutInSeconds(getContext()),
true);
LOG.info("Synchronization finished");
} catch (RuntimeException e) {
LOG.error(e.getMessage(), e);
}
}
private void syncPOW() {
private void syncPOW() throws IOException, DecryptionFailedException {
// If the Bitmessage context acts as a full node, synchronization isn't necessary
LOG.info("Looking for completed POW");
try {
BitmessageAddress identity = Singleton.getIdentity(getContext());
byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey();
byte[] signingKey = cryptography().createPublicKey(identity.getPublicDecryptionKey());
@ -139,9 +142,6 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
stopPowSync(getContext());
}
LOG.info("Synchronization finished");
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
public static void startSync(Context ctx) {

View File

@ -23,6 +23,7 @@ import android.preference.PreferenceManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
@ -49,7 +50,7 @@ public class Preferences {
* Warning, this method might do a network call and therefore can't be called from
* the UI thread.
*/
public static InetAddress getTrustedNode(Context ctx) {
public static InetAddress getTrustedNode(Context ctx) throws IOException {
String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE);
if (trustedNode == null) return null;
trustedNode = trustedNode.trim();
@ -59,15 +60,7 @@ public class Preferences {
int index = trustedNode.lastIndexOf(':');
trustedNode = trustedNode.substring(0, index);
}
try {
return InetAddress.getByName(trustedNode);
} catch (UnknownHostException e) {
new ErrorNotification(ctx)
.setError(R.string.error_invalid_sync_host)
.show();
LOG.error(e.getMessage(), e);
return null;
}
}
public static int getTrustedNodePort(Context ctx) {

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_item_normal_state"/>
</item>
</selector>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_item_swiping_active_state"/>
</item>
</layer-list>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_item_swiping_state"/>
</item>
</layer-list>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_swipe_item_trash"/>
</item>
<item
android:drawable="@drawable/ic_item_swipe_trash"
android:gravity="right|center_vertical"
android:width="24dp"
android:height="24dp"
android:right="16dp"/>
</layer-list>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<color
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/bg_swipe_item_neutral"/>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/bg_swipe_item_archive"/>
</item>
<item
android:drawable="@drawable/ic_item_swipe_archive"
android:gravity="left|center_vertical"
android:left="16dp"/>
</layer-list>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,3L4.99,3c-1.11,0 -1.98,0.89 -1.98,2L3,19c0,1.1 0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.11 -0.9,-2 -2,-2zM19,15h-4c0,1.66 -1.35,3 -3,3s-3,-1.34 -3,-3L4.99,15L4.99,5L19,5v10z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<shape
android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="1px"/>
<solid android:color="@color/divider"/>
</shape>

View File

@ -4,26 +4,26 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@id/android:list"
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:clipToPadding="false"
android:paddingBottom="88dp"
android:clipToPadding="false"
android:scrollbarStyle="outsideOverlay"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"/>
android:scrollbars="vertical"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_compose_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_action_compose_message"
app:elevation="8dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_margin="16dp"/>
android:layout_margin="16dp"
android:src="@drawable/ic_action_compose_message"
app:elevation="8dp"/>
</RelativeLayout>

View File

@ -15,64 +15,85 @@
~ limitations under the License.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_swipe_item_neutral">
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:background="@drawable/bg_item_normal_state"
tools:ignore="UselessParent">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/avatar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:src="@color/colorAccent"
android:layout_alignParentTop="true"
android:layout_margin="16dp"
android:src="@color/colorAccent"
tools:ignore="ContentDescription"/>
<TextView
android:id="@+id/sender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Sender"
android:lines="1"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_alignTop="@+id/avatar"
android:layout_toEndOf="@+id/avatar"
android:layout_marginTop="-5dp"
android:paddingTop="0dp"
android:layout_toEndOf="@+id/avatar"
android:ellipsize="end"
android:lines="1"
android:paddingBottom="0dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="0dp"
android:paddingTop="0dp"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textStyle="bold"
tools:text="Sender"
/>
<TextView
android:id="@+id/subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Subject"
android:lines="1"
android:layout_below="@+id/sender"
android:layout_toEndOf="@+id/avatar"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall"
android:lines="1"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:layout_below="@+id/sender"
android:layout_toEndOf="@+id/avatar"/>
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Subject"/>
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Text"
android:lines="1"
android:layout_below="@+id/subject"
android:layout_toEndOf="@+id/avatar"
android:ellipsize="end"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="center_vertical"
android:lines="1"
android:paddingBottom="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingBottom="8dp"
android:layout_below="@+id/subject"
android:layout_toEndOf="@+id/avatar"/>
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Text"/>
</RelativeLayout>
</RelativeLayout>
</FrameLayout>
</FrameLayout>

View File

@ -45,4 +45,12 @@
<color name="material_drawer_dark_selected_text">@color/material_drawer_primary</color>
<color name="material_drawer_dark_header_selection_text">#FFF</color>
<!-- swipeable list -->
<color name="bg_item_normal_state">#ffffffff</color>
<color name="bg_item_swiping_state">@color/colorPrimaryLight</color>
<color name="bg_item_swiping_active_state">@color/colorPrimary</color>
<color name="bg_swipe_item_neutral">@android:color/transparent</color>
<color name="bg_swipe_item_trash">#fff45f30</color>
<color name="bg_swipe_item_archive">#fff9930d</color>
</resources>