improved mouse scrolling
text selection should work on pre-lollipop devices
This commit is contained in:
parent
d8eb58d3a2
commit
8d2712a0f8
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue