improved mouse scrolling

text selection should work on pre-lollipop devices
This commit is contained in:
Mariotaku Lee 2015-04-19 22:47:24 +08:00
parent d8eb58d3a2
commit 8d2712a0f8
7 changed files with 245 additions and 49 deletions

View File

@ -44,6 +44,7 @@ import android.support.v4.app.FragmentTransaction;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.support.v4.util.Pair;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.ActionMenuView;
import android.support.v7.widget.CardView;
import android.support.v7.widget.FixedLinearLayoutManager;
@ -90,7 +91,6 @@ import org.mariotaku.twidere.model.ParcelableLocation;
import org.mariotaku.twidere.model.ParcelableMedia;
import org.mariotaku.twidere.model.ParcelableStatus;
import org.mariotaku.twidere.model.SingleResponse;
import org.mariotaku.twidere.text.method.StatusContentMovementMethod;
import org.mariotaku.twidere.util.AsyncTaskUtils;
import org.mariotaku.twidere.util.AsyncTwitterWrapper;
import org.mariotaku.twidere.util.ClipboardUtils;
@ -583,6 +583,8 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
private final StatusLinkClickHandler linkClickHandler;
private final TwidereLinkify linkify;
private ParcelableStatus status;
public DetailStatusViewHolder(StatusAdapter adapter, View itemView) {
super(itemView);
this.linkClickHandler = new StatusLinkClickHandler(adapter.getContext(), null);
@ -623,6 +625,8 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
public void displayStatus(ParcelableStatus status) {
if (status == null) return;
if (status.equals(this.status)) return;
this.status = status;
final StatusFragment fragment = adapter.getFragment();
final Context context = adapter.getContext();
final Resources resources = context.getResources();
@ -780,6 +784,9 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
}
Utils.setMenuForStatus(context, menuBar.getMenu(), status, adapter.getStatusAccount());
quoteTextView.setTextIsSelectable(true);
textView.setTextIsSelectable(true);
}
@Override
@ -867,12 +874,6 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
mediaPreview.setStyle(adapter.getMediaPreviewStyle());
quoteTextView.setTextIsSelectable(true);
textView.setTextIsSelectable(true);
quoteTextView.setMovementMethod(StatusContentMovementMethod.getInstance());
textView.setMovementMethod(StatusContentMovementMethod.getInstance());
quoteTextView.setCustomSelectionActionModeCallback(new StatusActionModeCallback(quoteTextView, activity));
textView.setCustomSelectionActionModeCallback(new StatusActionModeCallback(textView, activity));
}
@ -900,6 +901,7 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
final URLSpan[] spans = string.getSpans(start, end, URLSpan.class);
final boolean avail = spans.length == 1 && URLUtil.isValidUrl(spans[0].getURL());
MenuUtils.setMenuItemAvailability(menu, android.R.id.copyUrl, avail);
MenuUtils.setMenuItemShowAsActionFlags(menu, android.R.id.copyUrl, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
return true;
}
@ -1017,8 +1019,8 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
private static class StatusAdapter extends Adapter<ViewHolder> implements IStatusesAdapter<List<ParcelableStatus>> {
private static final int VIEW_TYPE_DETAIL_STATUS = 0;
private static final int VIEW_TYPE_LIST_STATUS = 1;
private static final int VIEW_TYPE_LIST_STATUS = 0;
private static final int VIEW_TYPE_DETAIL_STATUS = 1;
private static final int VIEW_TYPE_CONVERSATION_LOAD_INDICATOR = 2;
private static final int VIEW_TYPE_REPLIES_LOAD_INDICATOR = 3;
private static final int VIEW_TYPE_SPACE = 4;
@ -1051,9 +1053,11 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
private List<ParcelableStatus> mConversation, mReplies;
private boolean mDetailMediaExpanded;
private StatusAdapterListener mStatusAdapterListener;
private DetailStatusViewHolder mCachedHolder;
private RecyclerView mRecyclerView;
public StatusAdapter(StatusFragment fragment, boolean compact) {
setHasStableIds(true);
final Context context = fragment.getActivity();
final Resources res = context.getResources();
final SharedPreferencesWrapper preferences = SharedPreferencesWrapper.getInstance(context,
@ -1270,7 +1274,6 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_DETAIL_STATUS: {
if (mCachedHolder != null) return mCachedHolder;
final View view;
if (mIsCompact) {
view = mInflater.inflate(R.layout.header_status_compact, parent, false);
@ -1361,19 +1364,15 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
}
@Override
public void onViewAttachedToWindow(ViewHolder holder) {
super.onViewAttachedToWindow(holder);
if (mCachedHolder == holder) {
mCachedHolder = null;
}
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
mRecyclerView = recyclerView;
}
@Override
public void onViewDetachedFromWindow(ViewHolder holder) {
super.onViewDetachedFromWindow(holder);
if (holder instanceof DetailStatusViewHolder) {
mCachedHolder = (DetailStatusViewHolder) holder;
}
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
mRecyclerView = null;
}
@Override
@ -1467,6 +1466,7 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
}
private void updateItemDecoration() {
if (mRecyclerView == null) return;
final DividerItemDecoration decoration = mFragment.getItemDecoration();
decoration.setDecorationStart(0);
if (isLoadMoreIndicatorVisible()) {
@ -1474,7 +1474,7 @@ public class StatusFragment extends BaseSupportFragment implements LoaderCallbac
} else {
decoration.setDecorationEndOffset(mReplies != null && mReplies.size() > 0 ? 1 : 2);
}
mFragment.mRecyclerView.invalidateItemDecorations();
mRecyclerView.invalidateItemDecorations();
}
}

View File

@ -19,6 +19,7 @@
package org.mariotaku.twidere.util;
import android.support.v4.view.MenuItemCompat;
import android.view.Menu;
import android.view.MenuItem;
@ -48,6 +49,14 @@ public class MenuUtils {
item.setIcon(icon);
}
public static void setMenuItemShowAsActionFlags(Menu menu, int id, int flags) {
if (menu == null) return;
final MenuItem item = menu.findItem(id);
if (item == null) return;
item.setShowAsActionFlags(flags);
MenuItemCompat.setShowAsAction(item, flags);
}
public static void setMenuItemTitle(final Menu menu, final int id, final int icon) {
if (menu == null) return;
final MenuItem item = menu.findItem(id);

View File

@ -0,0 +1,150 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2015 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.support.v4.view.MotionEventCompat;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
import android.widget.HorizontalScrollView;
import android.widget.ScrollView;
/**
* Warning! Evil code ahead!
* Guess mouse scroll direction by calculating scroll offset of system ScrollView
*/
public class MouseScrollDirectionDecider {
private final float factor;
private final View verticalView, horizontalView;
private int horizontalDirection = 0, verticalDirection = 0;
private float horizontalScroll, verticalScroll;
public MouseScrollDirectionDecider(Context context, float factor) {
this.factor = factor;
this.verticalView = new InternalScrollView(context, this);
this.horizontalView = new InternalHorizontalScrollView(context, this);
}
public float getHorizontalDirection() {
return horizontalDirection;
}
public boolean isHorizontalAvailable() {
return horizontalDirection != 0;
}
public boolean isVerticalAvailable() {
return verticalDirection != 0;
}
private void setHorizontalDirection(int direction) {
horizontalDirection = direction;
}
public float getVerticalDirection() {
return verticalDirection;
}
private void setVerticalDirection(int direction) {
verticalDirection = direction;
}
public boolean guessDirection(MotionEvent event) {
if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0) {
return false;
}
if (event.getAction() != MotionEventCompat.ACTION_SCROLL) return false;
verticalScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
horizontalScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
verticalView.onGenericMotionEvent(event);
horizontalView.onGenericMotionEvent(event);
return verticalScroll != 0 || horizontalScroll != 0;
}
@SuppressLint("ViewConstructor")
private static class InternalScrollView extends ScrollView {
private final int factor;
private final MouseScrollDirectionDecider decider;
public InternalScrollView(Context context, MouseScrollDirectionDecider decider) {
super(context);
this.decider = decider;
final View view = new View(context);
addView(view);
this.factor = Math.round(decider.factor);
view.setTop(-factor);
view.setBottom(factor);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.scrollTo(0, factor);
if (t != factor) {
float value = (t - oldt) * decider.verticalScroll;
if (value > 0) {
decider.setVerticalDirection(1);
} else if (value < 0) {
decider.setVerticalDirection(-1);
} else {
decider.setVerticalDirection(0);
}
}
}
}
@SuppressLint("ViewConstructor")
private class InternalHorizontalScrollView extends HorizontalScrollView {
private final int factor;
private final MouseScrollDirectionDecider decider;
public InternalHorizontalScrollView(Context context, MouseScrollDirectionDecider decider) {
super(context);
this.decider = decider;
final View view = new View(context);
addView(view);
this.factor = Math.round(decider.factor);
view.setLeft(-factor);
view.setRight(factor);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.scrollTo(factor, 0);
if (t != factor) {
float value = (t - oldt) * decider.horizontalScroll;
if (value > 0) {
decider.setHorizontalDirection(1);
} else if (value < 0) {
decider.setHorizontalDirection(-1);
} else {
decider.setHorizontalDirection(0);
}
}
}
}
}

View File

@ -27,24 +27,28 @@ import android.util.TypedValue;
import android.view.InputDevice;
import android.view.MotionEvent;
import org.mariotaku.twidere.util.MouseScrollDirectionDecider;
/**
* Created by mariotaku on 15/3/30.
*/
public class RecyclerViewBackport extends RecyclerView {
private final MouseScrollDirectionDecider mMouseScrollDirectionDecider;
// This value is used when handling generic motion events.
private float mScrollFactor = Float.MIN_VALUE;
public RecyclerViewBackport(Context context) {
super(context);
this(context, null);
}
public RecyclerViewBackport(Context context, AttributeSet attrs) {
super(context, attrs);
this(context, attrs, 0);
}
public RecyclerViewBackport(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mMouseScrollDirectionDecider = new MouseScrollDirectionDecider(context, getScrollFactorBackport());
}
@Override
@ -57,19 +61,28 @@ public class RecyclerViewBackport extends RecyclerView {
if (event.getAction() == MotionEventCompat.ACTION_SCROLL) {
final float vScroll, hScroll;
if (lm.canScrollVertically()) {
vScroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
vScroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
if (!mMouseScrollDirectionDecider.isVerticalAvailable()) {
mMouseScrollDirectionDecider.guessDirection(event);
}
} else {
vScroll = 0f;
}
if (lm.canScrollHorizontally()) {
hScroll = -event.getAxisValue(MotionEvent.AXIS_HSCROLL);
hScroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
if (!mMouseScrollDirectionDecider.isHorizontalAvailable()) {
mMouseScrollDirectionDecider.guessDirection(event);
}
} else {
hScroll = 0f;
}
if (vScroll != 0 || hScroll != 0) {
if ((vScroll != 0 || hScroll != 0)) {
final float scrollFactor = getScrollFactorBackport();
smoothScrollBy((int) (hScroll * scrollFactor), (int) (vScroll * scrollFactor));
float horizontalDirection = mMouseScrollDirectionDecider.getHorizontalDirection();
float verticalDirection = mMouseScrollDirectionDecider.getVerticalDirection();
final float hFactor = scrollFactor * (horizontalDirection != 0 ? horizontalDirection : -1);
final float vFactor = scrollFactor * (verticalDirection != 0 ? verticalDirection : -1);
smoothScrollBy((int) (hScroll * hFactor), (int) (vScroll * vFactor));
}
}
}

View File

@ -1,7 +1,10 @@
package org.mariotaku.twidere.view;
import android.content.Context;
import android.text.Editable;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.util.AttributeSet;
import org.mariotaku.twidere.view.themed.ThemedTextView;
@ -11,31 +14,24 @@ public class StatusTextView extends ThemedTextView {
private OnSelectionChangeListener mOnSelectionChangeListener;
public StatusTextView(final Context context) {
super(context);
this(context, null);
}
public StatusTextView(final Context context, final AttributeSet attrs) {
super(context, attrs);
this(context, attrs, 0);
}
public StatusTextView(final Context context, final AttributeSet attrs, final int defStyle) {
super(context, attrs, defStyle);
setEditableFactory(new SafeEditableFactory());
setSpannableFactory(new SafeSpannableFactory());
}
public void setOnSelectionChangeListener(final OnSelectionChangeListener l) {
mOnSelectionChangeListener = l;
}
@Override
public void setText(CharSequence text, BufferType type) {
if (text == null) {
super.setText(null, type);
return;
}
super.setText(new SafeSpannableStringWrapper(text), type);
}
@Override
protected void onSelectionChanged(final int selStart, final int selEnd) {
super.onSelectionChanged(selStart, selEnd);
@ -48,9 +44,9 @@ public class StatusTextView extends ThemedTextView {
void onSelectionChanged(int selStart, int selEnd);
}
private static class SafeSpannableStringWrapper extends SpannableString {
private static class SafeSpannableString extends SpannableString {
public SafeSpannableStringWrapper(CharSequence source) {
public SafeSpannableString(CharSequence source) {
super(source);
}
@ -64,4 +60,33 @@ public class StatusTextView extends ThemedTextView {
}
}
private static class SafeSpannableStringBuilder extends SpannableStringBuilder {
public SafeSpannableStringBuilder(CharSequence source) {
super(source);
}
@Override
public void setSpan(Object what, int start, int end, int flags) {
if (start < 0 || end < 0) {
// Silently ignore
return;
}
super.setSpan(what, start, end, flags);
}
}
private class SafeEditableFactory extends Editable.Factory {
@Override
public Editable newEditable(CharSequence source) {
return new SafeSpannableStringBuilder(source);
}
}
private class SafeSpannableFactory extends Spannable.Factory {
@Override
public Spannable newSpannable(CharSequence source) {
return new SafeSpannableString(source);
}
}
}

View File

@ -176,7 +176,6 @@
android:paddingRight="@dimen/element_spacing_normal"
android:textAppearance="?android:textAppearanceMedium"
android:textColor="?android:textColorPrimary"
android:textIsSelectable="true"
android:visibility="gone"
tools:text="@string/sample_status_text"
tools:visibility="visible"/>
@ -244,8 +243,9 @@
android:verticalSpacing="@dimen/element_spacing_xsmall"
android:visibility="gone">
<include layout="@layout/layout_card_media_preview"
tools:ignore="DuplicateIncludedIds"/>
<include
layout="@layout/layout_card_media_preview"
tools:ignore="DuplicateIncludedIds"/>
</org.mariotaku.twidere.view.CardMediaContainer>
@ -283,7 +283,6 @@
android:singleLine="false"
android:textAppearance="?android:textAppearanceMedium"
android:textColor="?android:textColorPrimary"
android:textIsSelectable="true"
tools:text="@string/sample_status_text"/>
<org.mariotaku.twidere.view.TwitterCardContainer
@ -294,7 +293,7 @@
android:layout_toRightOf="@+id/quote_indicator"
android:visibility="gone"/>
<org.mariotaku.twidere.view.themed.ThemedTextView
<org.mariotaku.twidere.view.ActionIconThemedTextView
android:id="@+id/location_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"

View File

@ -5,7 +5,7 @@
<item
android:id="@android:id/copyUrl"
android:icon="@drawable/ic_action_web"
app:showAsAction="always|withText"
android:title="@android:string/copyUrl"/>
android:title="@android:string/copyUrl"
app:showAsAction="always"/>
</menu>