contact selection reeemix
1) RecyclerView-based, with better long scroller and more material-inspired look. 2) Add badge for Signal users to contact selection list. // FREEBIEpull/1/head
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 875 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 596 B |
After Width: | Height: | Size: 900 B |
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:topLeftRadius="44dp"
|
||||
android:topRightRadius="44dp"
|
||||
android:bottomLeftRadius="44dp"
|
||||
android:bottomRightRadius="0dp"/>
|
||||
<solid android:color="#FF0288D1" />
|
||||
<size android:height="88dp" android:width="88dp" />
|
||||
</shape>
|
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<bitmap android:src="@drawable/ic_badge_24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:gravity="bottom|right"
|
||||
android:tileMode="disabled" />
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:topLeftRadius="44dp"
|
||||
android:topRightRadius="44dp"
|
||||
android:bottomLeftRadius="0dp"
|
||||
android:bottomRightRadius="44dp"/>
|
||||
<solid android:color="#FF0288D1" />
|
||||
<size android:height="88dp" android:width="88dp" />
|
||||
</shape>
|
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true">
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="#FF0288D1"/>
|
||||
<size android:height="32dp" android:width="4dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<corners android:radius="2dp"/>
|
||||
<solid android:color="#FF737373"/>
|
||||
<size android:height="32dp" android:width="4dp"/>
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
@ -1,72 +1,79 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.thoughtcrime.securesms.contacts.ContactSelectionListItem
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:paddingRight="50dp">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:background="@drawable/conversation_list_item_background"
|
||||
android:paddingLeft="48dp"
|
||||
android:paddingRight="20dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/contact_photo_image"
|
||||
android:layout_width="@dimen/contact_selection_photo_size"
|
||||
android:layout_height="@dimen/contact_selection_photo_size"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:foreground="@drawable/contact_photo_background"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:cropToPadding="true"
|
||||
app:showBadge="true"
|
||||
tools:src="@color/md_material_blue_600"
|
||||
android:layout_marginRight="10dp"
|
||||
android:contentDescription="@string/SingleContactSelectionActivity_contact_photo" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/number_container"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dip"
|
||||
android:layout_marginLeft="14dip"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_toRightOf="@id/contact_photo_image">
|
||||
|
||||
<TextView android:id="@+id/number"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:fontFamily="sans-serif-light" />
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView android:id="@+id/label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="10dip"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:fontFamily="sans-serif-light"/>
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textSize="16sp"
|
||||
tools:text="Frieeeeeeedrich Nieeeeeeeeeetzsche" />
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout android:id="@+id/number_container"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@id/number_container"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginBottom="1dip"
|
||||
android:layout_marginLeft="14dip"
|
||||
android:checkMark="?android:attr/listChoiceIndicatorMultiple"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:layout_toRightOf="@id/contact_photo_image"
|
||||
android:gravity="center_vertical|left"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium" />
|
||||
<TextView android:id="@+id/number"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="marquee"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textSize="14sp"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="+1 (555) 555-5555" />
|
||||
|
||||
<TextView android:id="@+id/label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="10dip"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:fontFamily="sans-serif-light"
|
||||
tools:text="Mobile" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<CheckBox android:id="@+id/check_box"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginTop="13dp"
|
||||
android:focusable="false"
|
||||
android:clickable="false" />
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:focusable="false"
|
||||
android:clickable="false" />
|
||||
|
||||
</org.thoughtcrime.securesms.contacts.ContactSelectionListItem>
|
||||
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="?android:attr/listPreferredItemHeight"
|
||||
android:gravity="center"
|
||||
android:textColor="#666"
|
||||
android:textSize="22sp"
|
||||
tools:text="H" />
|
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView android:id="@+id/fastscroller_bubble"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/recycler_view_fast_scroller_bubble"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="48sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:text="A" />
|
||||
|
||||
<ImageView android:id="@+id/fastscroller_handle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/recycler_view_fast_scroller_handle" />
|
||||
|
||||
</merge>
|
@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Modified version of
|
||||
* https://github.com/AndroidDeveloperLB/LollipopContactsRecyclerViewFastScroller
|
||||
*
|
||||
* Their license:
|
||||
*
|
||||
* 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 org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.os.Build.VERSION;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public class RecyclerViewFastScroller extends LinearLayout {
|
||||
private static final int BUBBLE_ANIMATION_DURATION = 100;
|
||||
private static final int TRACK_SNAP_RANGE = 5;
|
||||
|
||||
private TextView bubble;
|
||||
private View handle;
|
||||
private RecyclerView recyclerView;
|
||||
private int height;
|
||||
private ObjectAnimator currentAnimator;
|
||||
|
||||
private final RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
|
||||
if (bubble == null || handle.isSelected())
|
||||
return;
|
||||
final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
|
||||
final int verticalScrollRange = recyclerView.computeVerticalScrollRange();
|
||||
float proportion = (float)verticalScrollOffset / ((float)verticalScrollRange - height);
|
||||
setBubbleAndHandlePosition(height * proportion);
|
||||
}
|
||||
};
|
||||
|
||||
public interface FastScrollAdapter {
|
||||
CharSequence getBubbleText(int pos);
|
||||
}
|
||||
|
||||
public RecyclerViewFastScroller(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public RecyclerViewFastScroller(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setOrientation(HORIZONTAL);
|
||||
setClipChildren(false);
|
||||
inflate(context, R.layout.recycler_view_fast_scroller, this);
|
||||
bubble = ViewUtil.findById(this, R.id.fastscroller_bubble);
|
||||
handle = ViewUtil.findById(this, R.id.fastscroller_handle);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
height = h;
|
||||
}
|
||||
|
||||
@Override
|
||||
@TargetApi(11)
|
||||
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
||||
final int action = event.getAction();
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
if (event.getX() < ViewUtil.getX(handle) - handle.getPaddingLeft() ||
|
||||
event.getY() < ViewUtil.getY(handle) - handle.getPaddingTop() ||
|
||||
event.getY() > ViewUtil.getY(handle) + handle.getHeight() + handle.getPaddingBottom())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (currentAnimator != null) {
|
||||
currentAnimator.cancel();
|
||||
}
|
||||
if (bubble.getVisibility() != VISIBLE) {
|
||||
showBubble();
|
||||
}
|
||||
handle.setSelected(true);
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
final float y = event.getY();
|
||||
setBubbleAndHandlePosition(y);
|
||||
setRecyclerViewPosition(y);
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
handle.setSelected(false);
|
||||
hideBubble();
|
||||
return true;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public void setRecyclerView(final RecyclerView recyclerView) {
|
||||
this.recyclerView = recyclerView;
|
||||
recyclerView.addOnScrollListener(onScrollListener);
|
||||
recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
recyclerView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
if (bubble == null || handle.isSelected())
|
||||
return true;
|
||||
final int verticalScrollOffset = recyclerView.computeVerticalScrollOffset();
|
||||
final int verticalScrollRange = recyclerView.computeVerticalScrollRange();
|
||||
float proportion = (float)verticalScrollOffset / ((float)verticalScrollRange - height);
|
||||
setBubbleAndHandlePosition(height * proportion);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
if (recyclerView != null)
|
||||
recyclerView.removeOnScrollListener(onScrollListener);
|
||||
}
|
||||
|
||||
private void setRecyclerViewPosition(float y) {
|
||||
if (recyclerView != null) {
|
||||
final int itemCount = recyclerView.getAdapter().getItemCount();
|
||||
float proportion;
|
||||
if (ViewUtil.getY(handle) == 0)
|
||||
proportion = 0f;
|
||||
else if (ViewUtil.getY(handle) + handle.getHeight() >= height - TRACK_SNAP_RANGE)
|
||||
proportion = 1f;
|
||||
else
|
||||
proportion = y / (float) height;
|
||||
final int targetPos = Util.clamp((int)(proportion * (float)itemCount), 0, itemCount - 1);
|
||||
((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(targetPos, 0);
|
||||
final CharSequence bubbleText = ((FastScrollAdapter) recyclerView.getAdapter()).getBubbleText(targetPos);
|
||||
if (bubble != null)
|
||||
bubble.setText(bubbleText);
|
||||
}
|
||||
}
|
||||
|
||||
private void setBubbleAndHandlePosition(float y) {
|
||||
final int handleHeight = handle.getHeight();
|
||||
final int bubbleHeight = bubble.getHeight();
|
||||
ViewUtil.setY(handle, Util.clamp((int)(y - handleHeight / 2), 0, height - handleHeight));
|
||||
ViewUtil.setY(bubble, Util.clamp((int)(y - bubbleHeight), 0, height - bubbleHeight - handleHeight / 2));
|
||||
}
|
||||
|
||||
@TargetApi(11)
|
||||
private void showBubble() {
|
||||
bubble.setVisibility(VISIBLE);
|
||||
if (VERSION.SDK_INT >= 11) {
|
||||
if (currentAnimator != null) currentAnimator.cancel();
|
||||
currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 0f, 1f).setDuration(BUBBLE_ANIMATION_DURATION);
|
||||
currentAnimator.start();
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(11)
|
||||
private void hideBubble() {
|
||||
if (VERSION.SDK_INT >= 11) {
|
||||
if (currentAnimator != null) currentAnimator.cancel();
|
||||
currentAnimator = ObjectAnimator.ofFloat(bubble, "alpha", 1f, 0f).setDuration(BUBBLE_ANIMATION_DURATION);
|
||||
currentAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
super.onAnimationEnd(animation);
|
||||
bubble.setVisibility(INVISIBLE);
|
||||
currentAnimator = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
super.onAnimationCancel(animation);
|
||||
bubble.setVisibility(INVISIBLE);
|
||||
currentAnimator = null;
|
||||
}
|
||||
});
|
||||
currentAnimator.start();
|
||||
} else {
|
||||
bubble.setVisibility(INVISIBLE);
|
||||
}
|
||||
}
|
||||
}
|