improved user fragment gesture

This commit is contained in:
Mariotaku Lee 2017-10-28 13:56:21 +08:00
parent 0ec9096a9d
commit ffec110336
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
28 changed files with 378 additions and 900 deletions

View File

@ -25,9 +25,6 @@ import org.mariotaku.restfu.annotation.param.Param;
import org.mariotaku.restfu.http.BodyType;
import org.mariotaku.restfu.http.mime.Body;
/**
* Created by mariotaku on 2017/4/17.
*/
public interface MediaResources {

View File

@ -1,45 +0,0 @@
/*
* 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 android.support.v7.widget;
import android.content.Context;
import android.view.View;
/**
* Created by mariotaku on 15/3/24.
*/
public class FixedLinearLayoutManager extends LinearLayoutManager {
public FixedLinearLayoutManager(Context context) {
super(context);
}
public FixedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
@Override
View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible, boolean acceptPartiallyVisible) {
// XXX Fixed NPE by add a simple check to child view count
if (getChildCount() < 0) return null;
return super.findOneVisibleChild(fromIndex, toIndex, completelyVisible, acceptPartiallyVisible);
}
}

View File

@ -1,81 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2014 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.os.SystemClock;
import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;
import android.view.View;
import org.mariotaku.twidere.view.HeaderDrawerLayout.DrawerCallback;
/**
* Created by mariotaku on 14/12/2.
*/
public class SimpleDrawerCallback implements DrawerCallback {
private final RecyclerView mRecyclerView;
public SimpleDrawerCallback(RecyclerView recyclerView) {
mRecyclerView = recyclerView;
}
@Override
public void fling(float velocity) {
mRecyclerView.fling(0, (int) velocity);
}
@Override
public void scrollBy(float dy) {
mRecyclerView.scrollBy(0, (int) dy);
}
@Override
public boolean canScroll(float dy) {
return mRecyclerView.canScrollVertically((int) dy);
}
@Override
public boolean isScrollContent(float x, float y) {
final View v = mRecyclerView;
final int[] location = new int[2];
v.getLocationInWindow(location);
return x >= location[0] && x <= location[0] + v.getWidth()
&& y >= location[1] && y <= location[1] + v.getHeight();
}
@Override
public void cancelTouch() {
mRecyclerView.dispatchTouchEvent(MotionEvent.obtain(SystemClock.uptimeMillis(),
SystemClock.uptimeMillis(), MotionEvent.ACTION_CANCEL, 0, 0, 0));
}
@Override
public boolean shouldLayoutHeaderBottom() {
return true;
}
@Override
public void topChanged(int offset) {
}
}

View File

@ -23,6 +23,7 @@ import android.annotation.TargetApi;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
@ -172,7 +173,7 @@ public final class ViewSupport {
view.setClipToOutline(clipToOutline);
}
public static void setOutlineProvider(View view, ViewOutlineProviderCompat outlineProvider) {
public static void setOutlineProvider(View view, @Nullable ViewOutlineProviderCompat outlineProvider) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return;
view.setOutlineProvider(new ViewOutlineProviderCompat.ViewOutlineProviderL(outlineProvider));
}

View File

@ -23,6 +23,7 @@ import android.annotation.TargetApi;
import android.graphics.Outline;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.Nullable;
import android.view.View;
import android.view.ViewOutlineProvider;
@ -83,15 +84,18 @@ public abstract class ViewOutlineProviderCompat {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public static class ViewOutlineProviderL extends ViewOutlineProvider {
@Nullable
private final ViewOutlineProviderCompat providerCompat;
public ViewOutlineProviderL(ViewOutlineProviderCompat providerCompat) {
public ViewOutlineProviderL(@Nullable ViewOutlineProviderCompat providerCompat) {
this.providerCompat = providerCompat;
}
@Override
public void getOutline(View view, Outline outline) {
providerCompat.getOutline(view, new OutlineCompat.OutlineL(outline));
if (providerCompat != null) {
providerCompat.getOutline(view, new OutlineCompat.OutlineL(outline));
}
}
}
}

View File

@ -1,572 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2014 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.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.OverScroller;
import org.mariotaku.twidere.R;
import kotlin.ranges.RangesKt;
/**
* Custom ViewGroup for user profile page like Google+ but with tab swipe
*
* @author mariotaku
*/
public class HeaderDrawerLayout extends ViewGroup {
private final ViewDragHelper mDragHelper;
private final OverScroller mScroller;
private final GestureDetector mGestureDetector;
private final InternalContainer mContainer;
private final DragCallback mDragCallback;
private DrawerCallback mDrawerCallback;
private boolean mUsingDragHelper;
private boolean mScrollingHeaderByGesture, mScrollingContentCallback;
private boolean mTouchDown, mTouchingScrollableContent;
private int mHeaderOffset;
private int mTop;
public HeaderDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.HeaderDrawerLayout);
final int headerLayoutId = a.getResourceId(R.styleable.HeaderDrawerLayout_hdl_headerLayout, 0);
final int contentLayoutId = a.getResourceId(R.styleable.HeaderDrawerLayout_hdl_contentLayout, 0);
addView(mContainer = new InternalContainer(this, context, headerLayoutId, contentLayoutId),
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
a.recycle();
mDragHelper = ViewDragHelper.create(this, mDragCallback = new DragCallback(this));
mGestureDetector = new GestureDetector(context, new GestureListener(this));
mScroller = new OverScroller(context);
}
public HeaderDrawerLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HeaderDrawerLayout(Context context) {
this(context, null);
}
@Override
public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
mScroller.abortAnimation();
mTouchDown = true;
mTouchingScrollableContent = isScrollContentCallback(ev.getX(), ev.getY());
mUsingDragHelper = false;
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
mTouchDown = false;
mTouchingScrollableContent = false;
mUsingDragHelper = false;
}
}
mGestureDetector.onTouchEvent(ev);
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (mDragHelper.shouldInterceptTouchEvent(ev) || mScrollingHeaderByGesture) {
mUsingDragHelper = true;
return true;
}
return false;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
mDragHelper.processTouchEvent(event);
return true;
}
@Override
public boolean canScrollVertically(final int direction) {
if (direction > 0) {
return getHeaderTop() > getHeaderTopMaximum();
} else if (direction < 0) {
return getHeaderTop() < getHeaderTopMaximum();
}
return false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final View child = getChildAt(0);
final int childWidthMeasureSpec = makeChildMeasureSpec(widthMeasureSpec, getPaddingLeft() + getPaddingRight());
final int childHeightMeasureSpec = makeChildMeasureSpec(heightMeasureSpec, getPaddingTop() + getPaddingBottom());
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0, j = getChildCount(); i < j; i++) {
final View child = getChildAt(i);
final int left = getPaddingLeft(), right = left + child.getMeasuredWidth();
final int top, bottom;
if (i == 0) {
if (shouldLayoutHeaderBottomCallback() && child.getHeight() != 0) {
// How far did we moved?
int delta = mHeaderOffset + getPaddingTop() - child.getTop();
bottom = child.getBottom() + delta;
top = bottom - child.getHeight();
} else {
top = mHeaderOffset + getPaddingTop();
bottom = top + child.getMeasuredHeight();
}
} else {
top = getChildAt(i - 1).getBottom();
bottom = top + child.getMeasuredHeight();
}
child.layout(left, top, right, bottom);
notifyOffsetChanged();
}
}
@Override
public void computeScroll() {
boolean invalidate = mDragHelper.continueSettling(true);
if (!mTouchDown && mScroller.computeScrollOffset()) {
if (!invalidate) {
offsetHeaderBy(mScroller.getCurrY() - getHeaderTop());
}
invalidate = true;
}
updateViewOffset();
if (invalidate) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
if (getChildCount() != 1) {
throw new IllegalArgumentException("Add subview by XML is not allowed.");
}
}
public void flingHeader(float velocity) {
if (mTouchDown) {
mScroller.abortAnimation();
return;
}
mScroller.fling(0, getHeaderTop(), 0, (int) velocity, 0, 0,
mContainer.getHeaderTopMinimum(), mContainer.getHeaderTopMaximum());
ViewCompat.postInvalidateOnAnimation(this);
}
public View getContent() {
return mContainer.getContent();
}
public View getHeader() {
return mContainer.getHeader();
}
public int getHeaderTop() {
return mContainer.getTop();
}
public int getHeaderTopMaximum() {
return mContainer.getHeaderTopMaximum();
}
public int getHeaderTopMinimum() {
return mContainer.getHeaderTopMinimum();
}
public void setDrawerCallback(DrawerCallback callback) {
mDrawerCallback = callback;
}
private boolean canScrollCallback(float dy) {
return mDrawerCallback.canScroll(dy);
}
private void cancelTouchCallback() {
mDrawerCallback.cancelTouch();
}
private void flingCallback(float velocity) {
mDrawerCallback.fling(velocity);
}
private float getDragTouchSlop() {
return mDragHelper.getTouchSlop();
}
private int getScrollRange() {
return mContainer.getScrollRange();
}
private boolean isScrollContentCallback(float x, float y) {
return mDrawerCallback.isScrollContent(x, y);
}
private boolean isScrollingContentCallback() {
return mScrollingContentCallback;
}
private void setScrollingContentCallback(boolean scrolling) {
mScrollingContentCallback = scrolling;
}
private boolean isScrollingHeaderByHelper() {
return mDragCallback.isScrollingHeaderByHelper();
}
private boolean isTouchingScrollableContent() {
return mTouchingScrollableContent;
}
private boolean isUsingDragHelper() {
return mUsingDragHelper;
}
private boolean isValidScroll(float direction, float other) {
return Math.abs(direction) > getDragTouchSlop() && Math.abs(direction) > Math.abs(other);
}
private static int makeChildMeasureSpec(int spec, int padding) {
final int size = MeasureSpec.getSize(spec), mode = MeasureSpec.getMode(spec);
return MeasureSpec.makeMeasureSpec(size - padding, mode);
}
private void notifyOffsetChanged() {
final int top = getHeaderTop();
if (mTop == top) return;
mHeaderOffset = top - getPaddingTop();
mDrawerCallback.topChanged(top);
mTop = top;
}
private void offsetHeaderBy(int dy) {
final int prevTop = mContainer.getTop();
final int clampedDy = RangesKt.coerceIn(prevTop + dy, getHeaderTopMinimum(), getHeaderTopMaximum()) - prevTop;
mContainer.offsetTopAndBottom(clampedDy);
}
private void scrollByCallback(float dy) {
final int top = getHeaderTop();
setScrollingContentCallback(top > getHeaderTopMinimum() && top < getHeaderTopMaximum());
mDrawerCallback.scrollBy(dy);
}
private void setScrollingHeaderByGesture(boolean scrolling) {
mScrollingHeaderByGesture = scrolling;
}
private boolean shouldLayoutHeaderBottomCallback() {
if (mDragCallback == null || isInEditMode()) return false;
return mDrawerCallback.shouldLayoutHeaderBottom();
}
private void updateViewOffset() {
}
public interface DrawerCallback {
boolean canScroll(float dy);
void cancelTouch();
void fling(float velocity);
boolean isScrollContent(float x, float y);
void scrollBy(float dy);
boolean shouldLayoutHeaderBottom();
void topChanged(int offset);
}
private static class DragCallback extends ViewDragHelper.Callback {
private final HeaderDrawerLayout mDrawer;
private long mTime;
private float mDx, mDy, mVelocity;
private boolean mScrollingHeaderByHelper;
public DragCallback(HeaderDrawerLayout drawer) {
mDrawer = drawer;
mTime = -1;
mDx = Float.NaN;
mDy = Float.NaN;
mVelocity = Float.NaN;
}
@Override
public void onViewDragStateChanged(int state) {
switch (state) {
case ViewDragHelper.STATE_SETTLING:
case ViewDragHelper.STATE_DRAGGING: {
mScrollingHeaderByHelper = false;
break;
}
case ViewDragHelper.STATE_IDLE: {
if (mTime > 0 && !Float.isNaN(mVelocity)) {
final float velocity = mVelocity;
if (velocity < 0 && mDrawer.getHeaderTop() <= mDrawer.getHeaderTopMinimum()) {
mDrawer.flingCallback(-velocity);
}
}
mTime = -1;
mDx = Float.NaN;
mDy = Float.NaN;
mVelocity = Float.NaN;
mScrollingHeaderByHelper = false;
break;
}
}
super.onViewDragStateChanged(state);
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
final long time = SystemClock.uptimeMillis();
final float timeDelta = time - mTime;
mVelocity = mDy / timeDelta * 1000;
mTime = time;
mDy = dy;
}
@Override
public void onViewReleased(View releasedChild, float xVel, float yVel) {
mDrawer.mDragHelper.flingCapturedView(mDrawer.getPaddingLeft(),
mDrawer.getHeaderTopMinimum(), mDrawer.getPaddingLeft(),
mDrawer.getHeaderTopMaximum());
ViewCompat.postInvalidateOnAnimation(mDrawer);
}
@Override
public int getViewVerticalDragRange(View child) {
return mDrawer.getScrollRange();
}
@Override
public boolean tryCaptureView(View view, int pointerId) {
return view == mDrawer.mContainer;
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
mDx = dx;
return mDrawer.getPaddingLeft();
}
@Override
public int clampViewPositionVertical(final View child, final int top, final int dy) {
final int current = mDrawer.getHeaderTop();
if (!Float.isNaN(mDx) && mDrawer.isValidScroll(mDx, dy)) {
mScrollingHeaderByHelper = false;
return current;
}
if (dy > 0 && mDrawer.canScrollCallback(-dy) && mDrawer.isTouchingScrollableContent()) {
if (!mDrawer.isUsingDragHelper()) {
// Scrolling up while list still has space to scroll, so make header still
mScrollingHeaderByHelper = false;
return current;
} else {
mDrawer.scrollByCallback(-dy);
mScrollingHeaderByHelper = false;
return current;
}
}
final int min = mDrawer.getHeaderTopMinimum(), max = mDrawer.getHeaderTopMaximum();
if (top < min && mDrawer.isTouchingScrollableContent() && mDrawer.isUsingDragHelper()) {
mDrawer.scrollByCallback(-dy);
}
mScrollingHeaderByHelper = true;
return RangesKt.coerceIn(top, min, max);
}
private boolean isScrollingHeaderByHelper() {
return mScrollingHeaderByHelper;
}
}
private static class GestureListener extends SimpleOnGestureListener {
private final HeaderDrawerLayout mDrawer;
public GestureListener(HeaderDrawerLayout drawer) {
mDrawer = drawer;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (!mDrawer.isUsingDragHelper() && mDrawer.isValidScroll(distanceY, distanceX)) {
final int offset = mDrawer.getHeaderTop(), min = mDrawer.getHeaderTopMinimum();
if (!mDrawer.canScrollCallback(-1)) {
if (distanceY < 0) {
if (!mDrawer.isScrollingHeaderByHelper()) {
mDrawer.offsetHeaderBy(Math.round(-distanceY));
}
mDrawer.setScrollingHeaderByGesture(true);
} else if (distanceY > 0 && offset > min) {
// Scrolling up when scrolling to list top, so we cancel touch event and scrolling header up
mDrawer.cancelTouchCallback();
if (!mDrawer.isScrollingHeaderByHelper()) {
mDrawer.offsetHeaderBy(Math.round(-distanceY));
}
} else if (offset <= min) {
mDrawer.scrollByCallback(-distanceX);
}
}
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
final int top = mDrawer.getHeaderTop(), min = mDrawer.getHeaderTopMinimum();
final boolean showingFullContent = top <= min, flingUp = velocityY < 0;
final boolean verticalFling = Math.abs(velocityY) > Math.abs(velocityX);
if (!verticalFling) return true;
if (showingFullContent) {
if (flingUp) {
// Fling list up when showing full content
if (mDrawer.isScrollingContentCallback()) {
mDrawer.flingCallback(-velocityY);
}
} else {
// Fling down when list reached top and not dragging user ViewDragHelper,
// so we fling header down here
if (!mDrawer.canScrollCallback(1) && !mDrawer.isUsingDragHelper()) {
mDrawer.flingHeader(velocityY);
}
}
} else {
// Header still visible
}
return true;
}
@Override
public boolean onDown(MotionEvent e) {
mDrawer.setScrollingHeaderByGesture(false);
mDrawer.setScrollingContentCallback(false);
return true;
}
}
@SuppressLint("ViewConstructor")
private static class InternalContainer extends ViewGroup {
private final HeaderDrawerLayout mParent;
private final View mHeaderView, mContentView;
public InternalContainer(HeaderDrawerLayout parent, Context context, int headerLayoutId, int contentLayoutId) {
super(context);
mParent = parent;
final LayoutInflater inflater = LayoutInflater.from(context);
addView(mHeaderView = inflater.inflate(headerLayoutId, this, false));
addView(mContentView = inflater.inflate(contentLayoutId, this, false));
}
public View getContent() {
return mContentView;
}
public View getHeader() {
return mHeaderView;
}
public int getHeaderTopMaximum() {
return mParent.getPaddingTop();
}
public int getHeaderTopMinimum() {
return mParent.getPaddingTop() - mHeaderView.getHeight();
}
public int getScrollRange() {
return getHeaderTopMaximum() - getHeaderTopMinimum();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
int heightSum = 0;
for (int i = 0, j = getChildCount(); i < j; i++) {
final View child = getChildAt(i);
final LayoutParams lp = child.getLayoutParams();
final int childHeightSpec;
if (lp.height == LayoutParams.MATCH_PARENT) {
childHeightSpec = heightMeasureSpec;
} else if (lp.height == LayoutParams.WRAP_CONTENT) {
childHeightSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.UNSPECIFIED);
} else {
childHeightSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY);
}
child.measure(widthMeasureSpec, childHeightSpec);
heightSum += child.getMeasuredHeight();
}
final int hSpec = MeasureSpec.makeMeasureSpec(heightSum, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, hSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0, j = getChildCount(); i < j; i++) {
final View child = getChildAt(i);
final int left = getPaddingLeft(), right = left + child.getMeasuredWidth();
final int top = i == 0 ? getPaddingTop() : getChildAt(i - 1).getBottom();
final int bottom = top + child.getMeasuredHeight();
child.layout(left, top, right, bottom);
}
}
@Override
public void offsetTopAndBottom(int offset) {
super.offsetTopAndBottom(offset);
mParent.notifyOffsetChanged();
}
}
}

View File

@ -22,9 +22,13 @@ package android.support.design.widget
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.OverScroller
internal open class AccessorHeaderBehavior<V : View>(context: Context, attrs: AttributeSet? = null) : HeaderBehavior<V>(context, attrs) {
internal val scroller: OverScroller?
get() = mScroller
internal override fun getScrollRangeForDragFling(view: V): Int {
return super.getScrollRangeForDragFling(view)
}
@ -45,11 +49,21 @@ internal open class AccessorHeaderBehavior<V : View>(context: Context, attrs: At
return super.getTopBottomOffsetForScrollingSibling()
}
internal override fun onFlingFinished(parent: CoordinatorLayout?, layout: V) {
internal override fun onFlingFinished(parent: CoordinatorLayout, layout: V) {
super.onFlingFinished(parent, layout)
}
internal override fun canDragView(view: V): Boolean {
return super.canDragView(view)
}
internal fun scrollAccessor(coordinatorLayout: CoordinatorLayout, header: V,
dy: Int, minOffset: Int, maxOffset: Int): Int {
return scroll(coordinatorLayout, header, dy, minOffset, maxOffset)
}
internal fun flingAccessor(coordinatorLayout: CoordinatorLayout, layout: V, minOffset: Int,
maxOffset: Int, velocityY: Float): Boolean {
return fling(coordinatorLayout, layout, minOffset, maxOffset, velocityY)
}
}

View File

@ -0,0 +1,28 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 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 android.support.design.widget
import android.animation.TimeInterpolator
object AnimationUtilsAccessor {
val DECELERATE_INTERPOLATOR: TimeInterpolator = AnimationUtils.DECELERATE_INTERPOLATOR
}

View File

@ -0,0 +1,36 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 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 android.support.v7.widget
import android.content.Context
import android.view.View
open class FixedLinearLayoutManager : LinearLayoutManager {
constructor(context: Context) : super(context)
constructor(context: Context, orientation: Int, reverseLayout: Boolean) : super(context, orientation, reverseLayout)
internal override fun findOneVisibleChild(fromIndex: Int, toIndex: Int, completelyVisible: Boolean, acceptPartiallyVisible: Boolean): View? {
// XXX Fixed NPE by add a simple check to child view count
return if (childCount < 0) null else super.findOneVisibleChild(fromIndex, toIndex, completelyVisible, acceptPartiallyVisible)
}
}

View File

@ -19,4 +19,4 @@ fun LongArray.toStringArray(): Array<String> {
return Array(this.size) { idx -> this[idx].toString() }
}
fun <T> T.weak(): WeakReference<T> = WeakReference(this)
fun <T> T.toWeak(): WeakReference<T> = WeakReference(this)

View File

@ -0,0 +1,38 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 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.ktextension
import java.lang.ref.WeakReference
import kotlin.reflect.KProperty
class WeakDelegate<T> {
private var weakRef: WeakReference<T>? = null
operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
return weakRef?.get()
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
weakRef = if (value != null) WeakReference(value) else null
}
}
fun <T> weak(): WeakDelegate<T> = WeakDelegate()

View File

@ -12,7 +12,7 @@ import nl.komponents.kovenant.combine.and
import nl.komponents.kovenant.task
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.mariotaku.ktextension.weak
import org.mariotaku.ktextension.toWeak
import org.mariotaku.twidere.R
import org.mariotaku.twidere.activity.iface.IBaseActivity
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_INTENT
@ -55,7 +55,7 @@ class InvalidAccountAlertActivity : FragmentActivity(), IBaseActivity<InvalidAcc
fun removeInvalidAccounts() {
val am = AccountManager.get(this)
val weakThis = weak()
val weakThis = toWeak()
val invalidAccounts = AccountUtils.getAccounts(am).filter { !am.isAccountValid(it) }
(showProgressDialog("remove_invalid_accounts") and task {
invalidAccounts.forEach { account ->

View File

@ -536,7 +536,7 @@ class MediaViewerActivity : BaseActivity(), IMediaViewerActivity, MediaSwipeClos
private fun saveMediaToContentUri(data: Uri) {
val fileInfo = getCurrentCacheFileInfo(viewPager.currentItem) ?: return
val weakThis = weak()
val weakThis = toWeak()
(showProgressDialog("save_media_to_progress") and task {
val a = weakThis.get() ?: throw InterruptedException()
fileInfo.inputStream().use { st ->

View File

@ -35,7 +35,6 @@ import nl.komponents.kovenant.android.startKovenant
import nl.komponents.kovenant.android.stopKovenant
import nl.komponents.kovenant.task
import okhttp3.Dns
import org.mariotaku.commons.logansquare.LoganSquareMapperFinder
import org.mariotaku.kpreferences.KPreferences
import org.mariotaku.kpreferences.get
import org.mariotaku.kpreferences.set
@ -56,7 +55,6 @@ import org.mariotaku.twidere.model.DefaultFeatures
import org.mariotaku.twidere.receiver.ConnectivityStateReceiver
import org.mariotaku.twidere.service.StreamingService
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.concurrent.ConstantFuture
import org.mariotaku.twidere.util.content.TwidereSQLiteOpenHelper
import org.mariotaku.twidere.util.dagger.GeneralComponent
import org.mariotaku.twidere.util.emoji.EmojiOneShortCodeMap
@ -71,8 +69,6 @@ import org.mariotaku.twidere.util.refresh.AutoRefreshController
import org.mariotaku.twidere.util.sync.DataSyncProvider
import org.mariotaku.twidere.util.sync.SyncController
import java.util.*
import java.util.concurrent.Callable
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -309,16 +305,10 @@ class TwidereApplication : Application(), OnSharedPreferenceChangeListener {
Class.forName(AsyncTask::class.java.name)
} catch (ignore: ClassNotFoundException) {
}
LoganSquareMapperFinder.setDefaultExecutor(object : LoganSquareMapperFinder.FutureExecutor {
override fun <T> submit(callable: Callable<T>): Future<T> {
return ConstantFuture(callable.call())
}
})
}
companion object {
private val KEY_KEYBOARD_SHORTCUT_INITIALIZED = "keyboard_shortcut_initialized"
var instance: TwidereApplication? = null
private set

View File

@ -25,7 +25,7 @@ import android.database.ContentObserver
import android.net.Uri
import android.os.Handler
import android.os.Looper
import org.mariotaku.ktextension.weak
import org.mariotaku.ktextension.toWeak
import org.mariotaku.twidere.extension.queryAll
import org.mariotaku.twidere.extension.queryCount
@ -40,7 +40,7 @@ class CursorObjectTiledDataSource<T>(
) : TiledDataSource<T>() {
init {
val weakThis = weak()
val weakThis = toWeak()
val observer = object : ContentObserver(MainHandler) {
override fun onChange(selfChange: Boolean) {
weakThis.get()?.invalidate()

View File

@ -36,9 +36,11 @@ import org.mariotaku.twidere.adapter.LoadMoreSupportAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter.IndicatorPosition
import org.mariotaku.twidere.fragment.iface.RefreshScrollTopInterface
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.ContentScrollHandler
import org.mariotaku.twidere.util.RecyclerViewScrollHandler
import org.mariotaku.twidere.util.ThemeUtils
import org.mariotaku.twidere.util.TwidereColorUtils
import org.mariotaku.twidere.view.ExtendedSwipeRefreshLayout
import org.mariotaku.twidere.view.HeaderDrawerLayout
import org.mariotaku.twidere.view.iface.IExtendedView
/**
@ -46,7 +48,7 @@ import org.mariotaku.twidere.view.iface.IExtendedView
*/
abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<RecyclerView.ViewHolder>,
L : RecyclerView.LayoutManager> : BaseFragment(), SwipeRefreshLayout.OnRefreshListener,
HeaderDrawerLayout.DrawerCallback, RefreshScrollTopInterface, IControlBarActivity.ControlBarOffsetListener,
RefreshScrollTopInterface, IControlBarActivity.ControlBarOffsetListener,
ContentScrollHandler.ContentListSupport<A>, ControlBarShowHideHelper.ControlBarAnimationListener {
lateinit var layoutManager: L
@ -57,7 +59,6 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
private set
// Callbacks and listeners
private lateinit var drawerCallback: SimpleDrawerCallback
lateinit var scrollListener: RecyclerViewScrollHandler<A>
// Data fields
private val systemWindowsInsets = Rect()
@ -84,22 +85,6 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
swipeLayout.isRefreshing = layoutRefreshing
}
override fun canScroll(dy: Float): Boolean {
return drawerCallback.canScroll(dy)
}
override fun cancelTouch() {
drawerCallback.cancelTouch()
}
override fun fling(velocity: Float) {
drawerCallback.fling(velocity)
}
override fun isScrollContent(x: Float, y: Float): Boolean {
return drawerCallback.isScrollContent(x, y)
}
override fun onControlBarOffsetChanged(activity: IControlBarActivity, offset: Float) {
updateRefreshProgressOffset()
}
@ -115,10 +100,6 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
updateRefreshProgressOffset()
}
override fun scrollBy(dy: Float) {
drawerCallback.scrollBy(dy)
}
override fun scrollToStart(): Boolean {
scrollToPositionWithOffset(0, 0)
recyclerView.stopScroll()
@ -148,14 +129,6 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
}
}
override fun shouldLayoutHeaderBottom(): Boolean {
return drawerCallback.shouldLayoutHeaderBottom()
}
override fun topChanged(offset: Int) {
drawerCallback.topChanged(offset)
}
var refreshEnabled: Boolean
get() = swipeLayout.isEnabled
set(value) {
@ -180,7 +153,6 @@ abstract class AbsContentRecyclerViewFragment<A : LoadMoreSupportAdapter<Recycle
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
drawerCallback = SimpleDrawerCallback(recyclerView)
val backgroundColor = ThemeUtils.getColorBackground(context)
val colorRes = TwidereColorUtils.getContrastYIQ(backgroundColor,

View File

@ -20,7 +20,6 @@
package org.mariotaku.twidere.fragment
import android.accounts.AccountManager
import android.animation.ArgbEvaluator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
@ -30,7 +29,6 @@ import android.content.Intent
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.Rect
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.nfc.NdefMessage
import android.nfc.NdefRecord
@ -79,7 +77,6 @@ import nl.komponents.kovenant.ui.alwaysUi
import nl.komponents.kovenant.ui.failUi
import nl.komponents.kovenant.ui.successUi
import org.mariotaku.chameleon.Chameleon
import org.mariotaku.chameleon.ChameleonUtils
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.*
import org.mariotaku.library.objectcursor.ObjectCursor
@ -136,7 +133,9 @@ import org.mariotaku.twidere.util.menu.TwidereMenuInfo
import org.mariotaku.twidere.util.shortcut.ShortcutCreator
import org.mariotaku.twidere.util.support.ActivitySupport
import org.mariotaku.twidere.util.support.ActivitySupport.TaskDescriptionCompat
import org.mariotaku.twidere.util.support.ViewSupport
import org.mariotaku.twidere.util.support.WindowSupport
import org.mariotaku.twidere.util.support.view.ViewOutlineProviderCompat
import org.mariotaku.twidere.view.TabPagerIndicator
import org.mariotaku.twidere.view.iface.IExtendedView.OnSizeChangedListener
import java.lang.ref.WeakReference
@ -152,7 +151,6 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
get() = coordinatorLayout.toolbar
private lateinit var profileBirthdayBanner: View
private lateinit var actionBarBackground: ActionBarDrawable
private lateinit var pagerAdapter: SupportTabsAdapter
// Data fields
@ -169,7 +167,6 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
private var actionBarShadowColor: Int = 0
private var uiColor: Int = 0
private var primaryColor: Int = 0
private var primaryColorDark: Int = 0
private var nameFirst: Boolean = false
private var previousTabItemIsDark: Int = 0
private var previousActionBarItemIsDark: Int = 0
@ -635,10 +632,8 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
profileHeaderBackground.setBackgroundColor(cardBackgroundColor)
toolbarTabs.setBackgroundColor(cardBackgroundColor)
actionBarBackground = ActionBarDrawable(ResourcesCompat.getDrawable(activity.resources,
R.drawable.shadow_user_banner_action_bar, null)!!)
setupBaseActionBar()
setupViewStyle()
setupViewSettings()
setupUserPages()
getUserInfo(accountKey, userKey, screenName, false)
@ -1269,20 +1264,18 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
uiColor = if (color != 0) color else theme.colorPrimary
previousActionBarItemIsDark = 0
previousTabItemIsDark = 0
setupBaseActionBar()
setupViewStyle()
val activity = activity as BaseActivity
if (theme.isToolbarColored) {
primaryColor = color
primaryColor = if (theme.isToolbarColored) {
color
} else {
primaryColor = theme.colorToolbar
theme.colorToolbar
}
primaryColorDark = ChameleonUtils.darkenColor(primaryColor)
actionBarBackground.color = primaryColor
val taskColor: Int
if (theme.isToolbarColored) {
taskColor = ColorUtils.setAlphaComponent(color, 0xFF)
(toolbar.background as? ActionBarDrawable)?.color = primaryColor
val taskColor = if (theme.isToolbarColored) {
ColorUtils.setAlphaComponent(color, 0xFF)
} else {
taskColor = ColorUtils.setAlphaComponent(theme.colorToolbar, 0xFF)
ColorUtils.setAlphaComponent(theme.colorToolbar, 0xFF)
}
val user = this.user
if (user != null) {
@ -1298,17 +1291,28 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
profileBanner.setBackgroundColor(color)
}
private fun setupBaseActionBar() {
private fun setupViewStyle() {
val activity = activity as? LinkHandlerActivity ?: return
val actionBar = activity.supportActionBar ?: return
if (!ThemeUtils.isWindowFloating(activity) && ThemeUtils.isTransparentBackground(activity.currentThemeBackgroundOption)) {
profileBanner.alpha = activity.currentThemeBackgroundAlpha / 255f
}
actionBar.setBackgroundDrawable(actionBarBackground)
actionBar.setBackgroundDrawable(ActionBarDrawable(ResourcesCompat.getDrawable(activity.resources,
R.drawable.shadow_user_banner_action_bar, null)!!))
val actionBarElevation = ThemeUtils.getSupportActionBarElevation(activity)
ViewCompat.setElevation(toolbar, actionBarElevation)
// ViewCompat.setElevation(profileHeader, actionBarElevation)
ViewCompat.setElevation(statusBarBackground, actionBarElevation * 2)
ViewSupport.setOutlineProvider(toolbar, ViewOutlineProviderCompat.BACKGROUND)
// ViewSupport.setOutlineProvider(profileHeader, ViewOutlineProviderCompat.BOUNDS)
ViewSupport.setOutlineProvider(statusBarBackground, null)
}
private fun setupViewStyle() {
private fun setupViewSettings() {
profileImage.style = preferences[profileImageStyleKey]
val lightFont = preferences[lightFontKey]
@ -1360,63 +1364,6 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
}
}
private fun updateScrollOffset(offset: Int) {
val spaceHeight = profileBannerSpace.height
val factor = (if (spaceHeight == 0) 0f else offset / spaceHeight.toFloat()).coerceIn(0f, 1f)
val activity = activity as BaseActivity
val statusBarColor = sArgbEvaluator.evaluate(factor, 0xA0000000.toInt(),
ChameleonUtils.darkenColor(primaryColorDark)) as Int
val window = activity.window
WindowSupport.setLightStatusBar(window, ThemeUtils.isLightColor(statusBarColor))
val stackedTabColor = primaryColor
val profileContentHeight = profileHeaderBackground.height.toFloat()
val tabOutlineAlphaFactor: Float
if (offset - spaceHeight > 0) {
tabOutlineAlphaFactor = 1f - ((offset - spaceHeight) / profileContentHeight).coerceIn(0f, 1f)
} else {
tabOutlineAlphaFactor = 1f
}
actionBarBackground.apply {
this.factor = factor
this.outlineAlphaFactor = tabOutlineAlphaFactor
}
val currentTabColor = sArgbEvaluator.evaluate(tabOutlineAlphaFactor,
stackedTabColor, cardBackgroundColor) as Int
val tabBackground = toolbarTabs.background
(tabBackground as ColorDrawable).color = currentTabColor
val tabItemIsDark = ThemeUtils.isLightColor(currentTabColor)
if (previousTabItemIsDark == 0 || (if (tabItemIsDark) 1 else -1) != previousTabItemIsDark) {
val tabContrastColor = ThemeUtils.getColorDependent(currentTabColor)
toolbarTabs.setIconColor(tabContrastColor)
toolbarTabs.setLabelColor(tabContrastColor)
val theme = Chameleon.getOverrideTheme(activity, activity)
if (theme.isToolbarColored) {
toolbarTabs.setStripColor(tabContrastColor)
} else {
toolbarTabs.setStripColor(ThemeUtils.getOptimalAccentColor(uiColor, tabContrastColor))
}
toolbarTabs.updateAppearance()
}
previousTabItemIsDark = if (tabItemIsDark) 1 else -1
val currentActionBarColor = sArgbEvaluator.evaluate(factor, actionBarShadowColor,
stackedTabColor) as Int
val actionItemIsDark = ThemeUtils.isLightColor(currentActionBarColor)
if (previousActionBarItemIsDark == 0 || (if (actionItemIsDark) 1 else -1) != previousActionBarItemIsDark) {
ThemeUtils.applyToolbarItemColor(activity, toolbar, currentActionBarColor)
}
previousActionBarItemIsDark = if (actionItemIsDark) 1 else -1
}
override var controlBarOffset: Float
get() = 0f
set(value) = Unit //Ignore
@ -1649,7 +1596,6 @@ class UserFragment : BaseFragment(), OnClickListener, OnLinkClickListener,
companion object {
private val sArgbEvaluator = ArgbEvaluator()
private val LOADER_ID_USER = 1
private val LOADER_ID_FRIENDSHIP = 2

View File

@ -215,7 +215,7 @@ class FilteredUsersFragment : BaseFiltersFragment() {
}
private fun exportToMutedUsers(accountKey: UserKey, items: Array<UserKey>) {
val weakThis = this.weak()
val weakThis = this.toWeak()
showProgressDialog("export_to_muted").then {
val fragment = weakThis.get() ?: throw InterruptedException()
val am = AccountManager.get(fragment.context)

View File

@ -3,9 +3,6 @@ package org.mariotaku.twidere.fragment.iface
import android.support.v4.app.FragmentActivity
import android.support.v7.widget.Toolbar
/**
* Created by mariotaku on 16/3/16.
*/
interface IToolBarSupportFragment {
val toolbar: Toolbar

View File

@ -9,7 +9,7 @@ import android.view.MenuItem
import com.squareup.otto.Subscribe
import nl.komponents.kovenant.combine.and
import nl.komponents.kovenant.ui.alwaysUi
import org.mariotaku.ktextension.weak
import org.mariotaku.ktextension.toWeak
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.SYNC_PREFERENCES_NAME
import org.mariotaku.twidere.constant.dataSyncProviderInfoKey
@ -80,7 +80,7 @@ class SyncSettingsFragment : BasePreferenceFragment() {
private fun cleanupAndDisconnect() {
val providerInfo = kPreferences[dataSyncProviderInfoKey] ?: return
val weakThis = weak()
val weakThis = toWeak()
val task = showProgressDialog("cleanup_sync_cache").
and(syncController.cleanupSyncCache(providerInfo))
task.alwaysUi {

View File

@ -28,7 +28,7 @@ import android.support.v4.view.MenuItemCompat
import android.support.v7.widget.ActionMenuView
import android.view.MenuItem
import android.view.View
import org.mariotaku.ktextension.weak
import org.mariotaku.ktextension.toWeak
import org.mariotaku.twidere.extension.view.findItemView
import org.mariotaku.twidere.graphic.like.LikeAnimationDrawable
import org.mariotaku.twidere.graphic.like.LikeAnimationDrawable.Style
@ -75,7 +75,7 @@ class FavoriteItemProvider(context: Context) : ActionProvider(context) {
}
private class ViewCallback(view: View) : Drawable.Callback {
private val viewRef = view.weak()
private val viewRef = view.toWeak()
override fun invalidateDrawable(who: Drawable) {
val view = viewRef.get() ?: return

View File

@ -24,14 +24,17 @@ import android.content.Context
import android.support.design.widget.AccessorHeaderBehavior
import android.support.design.widget.CoordinatorLayout
import android.support.graphics.drawable.ArgbEvaluator
import android.support.v4.view.ViewCompat
import android.support.v7.widget.RecyclerView
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import kotlinx.android.synthetic.main.fragment_user.view.*
import kotlinx.android.synthetic.main.header_user.view.*
import org.mariotaku.chameleon.Chameleon
import org.mariotaku.chameleon.ChameleonUtils
import org.mariotaku.kpreferences.get
import org.mariotaku.twidere.R
import org.mariotaku.ktextension.weak
import org.mariotaku.twidere.constant.themeBackgroundAlphaKey
import org.mariotaku.twidere.constant.themeBackgroundOptionKey
import org.mariotaku.twidere.extension.view.measureChildIgnoringInsets
@ -39,7 +42,8 @@ import org.mariotaku.twidere.graphic.drawable.userprofile.ActionBarDrawable
import org.mariotaku.twidere.util.ThemeUtils
import org.mariotaku.twidere.util.dagger.DependencyHolder
internal class HeaderBehavior(context: Context, attrs: AttributeSet? = null) : AccessorHeaderBehavior<View>(context, attrs) {
internal class HeaderBehavior(context: Context, attrs: AttributeSet? = null) :
AccessorHeaderBehavior<ViewGroup>(context, attrs) {
var offsetDelta: Int = 0
private set
@ -47,48 +51,119 @@ internal class HeaderBehavior(context: Context, attrs: AttributeSet? = null) : A
private val cardBackgroundColor: Int
private var tabItemIsDark: Int = 0
private var lastNestedScrollingChild by weak<View>()
private var nestedScrollTarget by weak<View>()
init {
val preferences = DependencyHolder.get(context).preferences
cardBackgroundColor = ThemeUtils.getCardBackgroundColor(context,
preferences[themeBackgroundOptionKey], preferences[themeBackgroundAlphaKey])
}
override fun onMeasureChild(parent: CoordinatorLayout, child: View,
override fun onStartNestedScroll(parent: CoordinatorLayout, child: ViewGroup,
directTargetChild: View, target: View, nestedScrollAxes: Int, type: Int): Boolean {
// Return true if we're nested scrolling vertically, and we have scrollable children
// and the scrolling view is big enough to scroll
val started = (nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
&& child.hasScrollableChildren
&& parent.height - directTargetChild.height <= child.height)
// A new nested scroll has started so clear out the previous ref
lastNestedScrollingChild = null
return started
}
override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, child: ViewGroup,
target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
if (dy != 0) {
val min: Int
val max: Int
if (dy < 0) {
// We're scrolling down
min = -child.totalScrollRange
max = min + child.downNestedPreScrollRange
} else {
// We're scrolling up
min = -child.upNestedPreScrollRange
max = 0
}
if (min != max) {
consumed[1] = scrollAccessor(coordinatorLayout, child, dy, min, max)
}
}
}
override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: ViewGroup,
target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,
type: Int) {
nestedScrollTarget = target
if (dyUnconsumed < 0) {
// If the scrolling view is scrolling down but not consuming, it's probably be at
// the top of it's content
if (scrollAccessor(coordinatorLayout, child, dyUnconsumed, -child.downNestedScrollRange,
0) == 0) {
if (target is RecyclerView) {
target.stopScroll()
} else {
ViewCompat.stopNestedScroll(target)
}
}
}
}
override fun onStopNestedScroll(coordinatorLayout: CoordinatorLayout, child: ViewGroup, target: View,
type: Int) {
// Keep a reference to the previous nested scrolling child
lastNestedScrollingChild = target
}
override fun onMeasureChild(parent: CoordinatorLayout, child: ViewGroup,
parentWidthMeasureSpec: Int, widthUsed: Int, parentHeightMeasureSpec: Int,
heightUsed: Int): Boolean {
return parent.measureChildIgnoringInsets(child, parentWidthMeasureSpec, widthUsed,
parentHeightMeasureSpec, heightUsed)
}
override fun onLayoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int): Boolean {
val result = super.onLayoutChild(parent, child, layoutDirection)
override fun onLayoutChild(parent: CoordinatorLayout, child: ViewGroup, layoutDirection: Int): Boolean {
val handled = super.onLayoutChild(parent, child, layoutDirection)
updateTabColor(parent, child, topAndBottomOffset)
return result
return handled
}
override fun layoutChild(parent: CoordinatorLayout, child: View, layoutDirection: Int) {
override fun layoutChild(parent: CoordinatorLayout, child: ViewGroup, layoutDirection: Int) {
child.layout(0, 0, child.measuredWidth, child.measuredHeight)
}
override fun getScrollRangeForDragFling(view: View): Int {
val parent = view.parent as CoordinatorLayout
val toolbar = parent.findViewById<View>(R.id.toolbar)
val toolbarTabs = parent.findViewById<View>(R.id.toolbarTabs)
return view.height - toolbar.bottom - toolbarTabs.height
override fun canDragView(view: ViewGroup): Boolean {
// Else we'll use the default behaviour of seeing if it can scroll down
val scrollingView = lastNestedScrollingChild
return if (scrollingView != null) {
// If we have a reference to a scrolling view, check it
scrollingView.isShown && !scrollingView.canScrollVertically(-1)
} else {
// Otherwise we assume that the scrolling view hasn't been scrolled and can drag.
true
}
}
override fun getMaxDragOffset(view: View): Int {
val parent = view.parent as CoordinatorLayout
val toolbar = parent.findViewById<View>(R.id.toolbar)
val toolbarTabs = parent.findViewById<View>(R.id.toolbarTabs)
return -(view.height - toolbar.bottom - toolbarTabs.height)
override fun onFlingFinished(parent: CoordinatorLayout, layout: ViewGroup) {
// At the end of a manual fling, check to see if we need to snap to the edge-child
}
override fun setHeaderTopBottomOffset(parent: CoordinatorLayout, header: View, newOffset: Int, minOffset: Int, maxOffset: Int): Int {
override fun getMaxDragOffset(view: ViewGroup): Int {
return -view.downNestedScrollRange
}
override fun getScrollRangeForDragFling(view: ViewGroup): Int {
return view.totalScrollRange
}
override fun setHeaderTopBottomOffset(parent: CoordinatorLayout, header: ViewGroup, newOffset: Int, minOffset: Int, maxOffset: Int): Int {
val curOffset = topBottomOffsetForScrollingSibling
var consumed = 0
if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
if (minOffset != 0 && curOffset in minOffset..maxOffset) {
// If we have some scrolling range, and we're currently within the min and max
// offsets, calculate a new offset
val clampedOffset = newOffset.coerceIn(minOffset, maxOffset)
@ -110,6 +185,10 @@ internal class HeaderBehavior(context: Context, attrs: AttributeSet? = null) : A
return consumed
}
override fun getTopBottomOffsetForScrollingSibling(): Int {
return topAndBottomOffset + offsetDelta
}
@SuppressLint("RestrictedApi")
private fun updateTabColor(parent: CoordinatorLayout, header: View, offset: Int) {
val actionBarBackground = parent.toolbar.background as? ActionBarDrawable ?: return
@ -146,6 +225,5 @@ internal class HeaderBehavior(context: Context, attrs: AttributeSet? = null) : A
this.tabItemIsDark = tabItemIsDark
}
override fun canDragView(view: View) = true
}

View File

@ -0,0 +1,49 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 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.view.behavior.userprofile
import android.support.design.widget.CoordinatorLayout
import android.view.View
import kotlinx.android.synthetic.main.fragment_user.view.*
import kotlinx.android.synthetic.main.header_user.view.*
internal val View.hasScrollableChildren: Boolean
get() = totalScrollRange != 0
internal val View.totalScrollRange: Int
get() {
val parent = parent as CoordinatorLayout
val toolbar = parent.toolbar
val toolbarTabs = parent.toolbarTabs
return toolbarTabs.top - toolbar.bottom
}
internal val View.downNestedScrollRange: Int
get() {
val parent = parent as CoordinatorLayout
val toolbar = parent.toolbar
val toolbarTabs = parent.toolbarTabs
return toolbarTabs.top - toolbar.bottom
}
internal val View.downNestedPreScrollRange: Int
get() = 0
internal val View.upNestedPreScrollRange: Int
get() = totalScrollRange

View File

@ -22,20 +22,16 @@ package org.mariotaku.twidere.view.behavior.userprofile
import android.content.Context
import android.graphics.Rect
import android.support.design.widget.AccessorHeaderScrollingViewBehavior
import android.support.design.widget.AppBarLayout
import android.support.design.widget.CoordinatorLayout
import android.support.v4.view.ViewCompat
import android.util.AttributeSet
import android.view.View
import kotlinx.android.synthetic.main.header_user.view.*
import org.mariotaku.twidere.R
import org.mariotaku.twidere.extension.view.measureChildIgnoringInsets
internal class PagerBehavior(context: Context, attrs: AttributeSet? = null) : AccessorHeaderScrollingViewBehavior(context, attrs) {
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
return dependency.id == R.id.profileHeader
}
override fun onMeasureChild(parent: CoordinatorLayout, child: View,
parentWidthMeasureSpec: Int, widthUsed: Int, parentHeightMeasureSpec: Int,
heightUsed: Int): Boolean {
@ -43,6 +39,10 @@ internal class PagerBehavior(context: Context, attrs: AttributeSet? = null) : Ac
parentHeightMeasureSpec, heightUsed)
}
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
return dependency.id == R.id.profileHeader
}
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
offsetChildAsNeeded(parent, child, dependency)
@ -51,7 +51,7 @@ internal class PagerBehavior(context: Context, attrs: AttributeSet? = null) : Ac
override fun onRequestChildRectangleOnScreen(parent: CoordinatorLayout, child: View,
rectangle: Rect, immediate: Boolean): Boolean {
val header = findFirstDependency(parent.getDependencies(child)) ?: return false
val header = parent.getDependencies(child).first()
// Offset the rect by the child's left/top
rectangle.offset(child.left, child.top)
@ -72,33 +72,30 @@ internal class PagerBehavior(context: Context, attrs: AttributeSet? = null) : Ac
// Offset the child, pinning it to the bottom the header-dependency, maintaining
// any vertical gap and overlap
ViewCompat.offsetTopAndBottom(child, (dependency.bottom - child.top
ViewCompat.offsetTopAndBottom(child, (dependency.contentBottom - child.top
+ behavior.offsetDelta + verticalLayoutGapAccessor)
- getOverlapPixelsForOffsetAccessor(dependency))
}
override fun getOverlapRatioForOffset(header: View): Float {
// if (header is AppBarLayout) {
// val abl = header as AppBarLayout?
// val totalScrollRange = abl!!.totalScrollRange
// val preScrollDown = abl.getDownNestedPreScrollRange()
// val offset = getAppBarLayoutOffset(abl)
//
// if (preScrollDown != 0 && totalScrollRange + offset <= preScrollDown) {
// // If we're in a pre-scroll down. Don't use the offset at all.
// return 0f
// } else {
// val availScrollRange = totalScrollRange - preScrollDown
// if (availScrollRange != 0) {
// // Else we'll use a interpolated ratio of the overlap, depending on offset
// return 1f + offset / availScrollRange.toFloat()
// }
// }
// }
val totalScrollRange = header.totalScrollRange
val preScrollDown = header.downNestedPreScrollRange
val offset = getAppBarLayoutOffset(header)
if (preScrollDown != 0 && totalScrollRange + offset <= preScrollDown) {
// If we're in a pre-scroll down. Don't use the offset at all.
return 0f
} else {
val availScrollRange = totalScrollRange - preScrollDown
if (availScrollRange != 0) {
// Else we'll use a interpolated ratio of the overlap, depending on offset
return 1f + offset / availScrollRange.toFloat()
}
}
return 0f
}
private fun getAppBarLayoutOffset(abl: AppBarLayout): Int {
private fun getAppBarLayoutOffset(abl: View): Int {
val lp = abl.layoutParams as CoordinatorLayout.LayoutParams
val behavior = lp.behavior as? HeaderBehavior ?: return 0
return behavior.topBottomOffsetForScrollingSibling
@ -109,7 +106,10 @@ internal class PagerBehavior(context: Context, attrs: AttributeSet? = null) : Ac
}
override fun getScrollRange(v: View): Int {
return (v as? AppBarLayout)?.totalScrollRange ?: super.getScrollRange(v)
if (v.id == R.id.profileHeader) return v.totalScrollRange
return super.getScrollRange(v)
}
private val View.contentBottom
get() = top + toolbarTabs.bottom
}

View File

@ -0,0 +1,30 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 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.view.userprofile
import android.content.Context
import android.support.design.widget.CoordinatorLayout
import android.util.AttributeSet
import org.mariotaku.chameleon.ChameleonView
class UserProfileCoordinatorLayout(context: Context, attrs: AttributeSet?) :
CoordinatorLayout(context, attrs), ChameleonView.StatusBarThemeable {
override fun isStatusBarColorHandled() = true
}

View File

@ -18,7 +18,7 @@
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<android.support.design.widget.CoordinatorLayout
<org.mariotaku.twidere.view.userprofile.UserProfileCoordinatorLayout
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"
@ -26,7 +26,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:statusBarBackground="@android:color/transparent"
app:statusBarBackground="@null"
tools:theme="@style/Theme.Twidere.NoActionBar">
<org.mariotaku.twidere.view.ExtendedViewPager
@ -119,6 +119,7 @@
<View
android:layout_width="match_parent"
android:id="@+id/statusBarBackground"
android:layout_height="wrap_content"
app:layout_behavior="org.mariotaku.twidere.view.behavior.userprofile.StatusBarBehavior"
tools:layout_height="25dp"/>
@ -128,7 +129,6 @@
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_alignParentTop="true"
android:elevation="@dimen/toolbar_elevation"
app:layout_behavior="org.mariotaku.twidere.view.behavior.userprofile.ToolbarBehavior"
app:popupTheme="?actionBarPopupTheme"
tools:background="@drawable/shadow_bottom"
@ -144,4 +144,4 @@
android:layout_height="wrap_content"
android:background="?android:windowContentOverlay"/>
</android.support.design.widget.CoordinatorLayout>
</org.mariotaku.twidere.view.userprofile.UserProfileCoordinatorLayout>

View File

@ -23,11 +23,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/headerUserProfile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
android:layout_height="wrap_content">
<org.mariotaku.twidere.view.ProfileBannerSpace
android:id="@+id/profileBannerSpace"
@ -305,6 +302,7 @@
android:id="@+id/toolbarTabs"
android:layout_width="match_parent"
android:layout_height="@dimen/element_size_normal"
android:outlineProvider="bounds"
android:textColor="?android:textColorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -315,7 +313,9 @@
app:tabShowDivider="false"
tools:background="?cardBackgroundColor"
tools:ignore="UnusedAttribute"
tools:listitem="@layout/layout_tab_item"/>
tools:layoutManager="android.support.v7.widget.LinearLayoutManager"
tools:listitem="@layout/layout_tab_item"
tools:orientation="horizontal"/>
<org.mariotaku.twidere.view.ProfileImageView
android:id="@+id/profileImage"

View File

@ -113,10 +113,6 @@
<attr name="sivDrawShadow" format="boolean"/>
<attr name="sivShape"/>
</declare-styleable>
<declare-styleable name="HeaderDrawerLayout">
<attr name="hdl_headerLayout" format="reference"/>
<attr name="hdl_contentLayout" format="reference"/>
</declare-styleable>
<declare-styleable name="TintedStatusLayout">
<attr name="setPadding" format="boolean"/>
</declare-styleable>