diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 226aa378c09e8bd1fc9e89a9e60e669c392a1f3a..8299e64ebf21220f8de021b2d5c60d08d5725645 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -110,7 +110,7 @@ android { } dependencies { - implementation(kotlin("stdlib", "1.2.50")) + implementation(kotlin("stdlib", "1.2.51")) // App Compat withVersion("27.1.1") { @@ -166,7 +166,6 @@ dependencies { // UI implementation("me.zhanghai.android.materialprogressbar", "library", "1.4.2") - implementation("com.simplecityapps", "recyclerview-fastscroll", "1.0.18") withVersion("0.9.6.0") { implementation("com.afollestad.material-dialogs", "core", version) implementation("com.afollestad.material-dialogs", "commons", version) diff --git a/app/sampledata/libraries.json b/app/sampledata/libraries.json index 7fbe083e233112beb90777f9e89e29be1700f54c..22490ae5dcc76f6bd3e290cd1c7721f0d6241cf8 100644 --- a/app/sampledata/libraries.json +++ b/app/sampledata/libraries.json @@ -161,7 +161,7 @@ }, { "name": "Kotlin Standard Library", - "version": "1.2.50", + "version": "1.2.51", "license": { "short_name": "Apache-2.0", "full_name": "Apache License" diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt index ebcf54b35215f9c290ad6504e083280d0793d49d..f9d77a0ee1b1e634ad753bd5bb8df11ae48191d6 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/buffers/BufferListAdapter.kt @@ -29,7 +29,6 @@ import android.widget.ImageView import android.widget.TextView import butterknife.BindView import butterknife.ButterKnife -import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import de.kuschku.libquassel.protocol.BufferId import de.kuschku.libquassel.protocol.Buffer_Activity import de.kuschku.libquassel.protocol.NetworkId @@ -39,6 +38,7 @@ import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.settings.MessageSettings import de.kuschku.quasseldroid.util.helper.* import de.kuschku.quasseldroid.util.lists.ListAdapter +import de.kuschku.quasseldroid.util.ui.fastscroll.views.FastScrollRecyclerView import de.kuschku.quasseldroid.viewmodel.data.BufferListItem import de.kuschku.quasseldroid.viewmodel.data.BufferProps import de.kuschku.quasseldroid.viewmodel.data.BufferState diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt index ffb92f96aa7d9557e61bf2a262205769c20520b0..b009b83ae20e6fdd6c2f1ce053eb3913a5b30d04 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/chat/nicks/NickListAdapter.kt @@ -29,7 +29,6 @@ import android.widget.ImageView import android.widget.TextView import butterknife.BindView import butterknife.ButterKnife -import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import de.kuschku.libquassel.util.helpers.nullIf import de.kuschku.quasseldroid.R import de.kuschku.quasseldroid.settings.MessageSettings @@ -37,6 +36,7 @@ import de.kuschku.quasseldroid.util.helper.letIf import de.kuschku.quasseldroid.util.helper.loadAvatars import de.kuschku.quasseldroid.util.helper.visibleIf import de.kuschku.quasseldroid.util.ui.SpanFormatter +import de.kuschku.quasseldroid.util.ui.fastscroll.views.FastScrollRecyclerView import de.kuschku.quasseldroid.viewmodel.data.IrcUserItem class NickListAdapter( diff --git a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragment.kt b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragment.kt index e0cc24042eeeefb134eafcad2630177bbc1f1f24..4611a1feba47b8103a92c5b641ead3c584476f9b 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragment.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/ui/clientsettings/about/AboutFragment.kt @@ -227,7 +227,7 @@ class AboutFragment : DaggerFragment() { ), Library( name = "Kotlin Standard Library", - version = "1.2.50", + version = "1.2.51", license = apache2, url = "https://kotlinlang.org/" ), diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixApi.kt b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixApi.kt index 842a5e0bfb6a408582c070459743bb83e7de8746..67319783bd7e0d3c971e0447d44a33f869352380 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixApi.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/avatars/MatrixApi.kt @@ -29,7 +29,7 @@ interface MatrixApi { @GET("/_matrix/client/r0/profile/{name}/avatar_url") fun avatarUrl(@Path("name") name: String): Call<MatrixAvatarResponse> - @GET("/_matrix/media/r0/thumbnail/{server}/{id}/?width=32&height=32&method=crop") + @GET("/_matrix/media/r0/thumbnail/{server}/{id}/?width={width}&height={height}&method={method]") fun avatarThumbnail( @Path("server") server: String, @Path("id") id: String, diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/DrawerRecyclerView.kt b/app/src/main/java/de/kuschku/quasseldroid/util/ui/DrawerRecyclerView.kt index e4cb9d8cc30ff18fd88f7f5586a22069f7dbcefc..dd5417b104a0ffa5ab69f220750ef4587f43b617 100644 --- a/app/src/main/java/de/kuschku/quasseldroid/util/ui/DrawerRecyclerView.kt +++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/DrawerRecyclerView.kt @@ -25,8 +25,8 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.support.v4.view.ViewCompat import android.util.AttributeSet -import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView import de.kuschku.quasseldroid.R +import de.kuschku.quasseldroid.util.ui.fastscroll.views.FastScrollRecyclerView class DrawerRecyclerView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/interfaces/OnFastScrollStateChangeListener.java b/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/interfaces/OnFastScrollStateChangeListener.java new file mode 100644 index 0000000000000000000000000000000000000000..a44eb339387980a2d0d82775d656fb8ea97552ce --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/interfaces/OnFastScrollStateChangeListener.java @@ -0,0 +1,14 @@ +package de.kuschku.quasseldroid.util.ui.fastscroll.interfaces; + +public interface OnFastScrollStateChangeListener { + + /** + * Called when fast scrolling begins + */ + void onFastScrollStart(); + + /** + * Called when fast scrolling ends + */ + void onFastScrollStop(); +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/utils/Utils.java b/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/utils/Utils.java new file mode 100644 index 0000000000000000000000000000000000000000..d1c548b419cba3eb7ada2bc644f8d837dc5d8bf8 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/utils/Utils.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2016 Tim Malseed + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.kuschku.quasseldroid.util.ui.fastscroll.utils; + +import android.annotation.TargetApi; +import android.content.res.Resources; +import android.os.Build; +import android.util.TypedValue; +import android.view.View; + +public class Utils { + + /** + * Converts dp to px + * + * @param res Resources + * @param dp the value in dp + * @return int + */ + public static int toPixels(Resources res, float dp) { + return (int) (dp * res.getDisplayMetrics().density); + } + + /** + * Converts sp to px + * + * @param res Resources + * @param sp the value in sp + * @return int + */ + public static int toScreenPixels(Resources res, float sp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, res.getDisplayMetrics()); + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public static boolean isRtl(Resources res) { + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) && + (res.getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL); + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/views/FastScrollPopup.java b/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/views/FastScrollPopup.java new file mode 100644 index 0000000000000000000000000000000000000000..acc9fa585a545ea2620bdd2d060178b835e1d208 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/views/FastScrollPopup.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 2016 Tim Malseed + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.kuschku.quasseldroid.util.ui.fastscroll.views; + +import android.animation.ObjectAnimator; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.support.annotation.Keep; +import android.text.TextUtils; + +import de.kuschku.quasseldroid.util.ui.fastscroll.utils.Utils; + +public class FastScrollPopup { + + private FastScrollRecyclerView mRecyclerView; + + private Resources mRes; + + private int mBackgroundSize; + private int mCornerRadius; + + private Path mBackgroundPath = new Path(); + private RectF mBackgroundRect = new RectF(); + private Paint mBackgroundPaint; + private int mBackgroundColor = 0xff000000; + + private Rect mInvalidateRect = new Rect(); + private Rect mTmpRect = new Rect(); + + // The absolute bounds of the fast scroller bg + private Rect mBgBounds = new Rect(); + + private String mSectionName; + + private Paint mTextPaint; + private Rect mTextBounds = new Rect(); + + private float mAlpha = 1; + + private ObjectAnimator mAlphaAnimator; + private boolean mVisible; + + @FastScroller.FastScrollerPopupPosition + private int mPosition; + + FastScrollPopup(Resources resources, FastScrollRecyclerView recyclerView) { + + mRes = resources; + + mRecyclerView = recyclerView; + + mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setAlpha(0); + + setTextSize(Utils.toScreenPixels(mRes, 44)); + setBackgroundSize(Utils.toPixels(mRes, 88)); + } + + public void setBgColor(int color) { + mBackgroundColor = color; + mBackgroundPaint.setColor(color); + mRecyclerView.invalidate(mBgBounds); + } + + public void setTextColor(int color) { + mTextPaint.setColor(color); + mRecyclerView.invalidate(mBgBounds); + } + + public void setTextSize(int size) { + mTextPaint.setTextSize(size); + mRecyclerView.invalidate(mBgBounds); + } + + public void setBackgroundSize(int size) { + mBackgroundSize = size; + mCornerRadius = mBackgroundSize / 2; + mRecyclerView.invalidate(mBgBounds); + } + + public void setTypeface(Typeface typeface) { + mTextPaint.setTypeface(typeface); + mRecyclerView.invalidate(mBgBounds); + } + + /** + * Animates the visibility of the fast scroller popup. + */ + public void animateVisibility(boolean visible) { + if (mVisible != visible) { + mVisible = visible; + if (mAlphaAnimator != null) { + mAlphaAnimator.cancel(); + } + mAlphaAnimator = ObjectAnimator.ofFloat(this, "alpha", visible ? 1f : 0f); + mAlphaAnimator.setDuration(visible ? 200 : 150); + mAlphaAnimator.start(); + } + } + + @Keep + public float getAlpha() { + return mAlpha; + } + + // Setter/getter for the popup alpha for animations + @Keep + public void setAlpha(float alpha) { + mAlpha = alpha; + mRecyclerView.invalidate(mBgBounds); + } + + @FastScroller.FastScrollerPopupPosition + public int getPopupPosition() { + return mPosition; + } + + public void setPopupPosition(@FastScroller.FastScrollerPopupPosition int position) { + mPosition = position; + } + + private float[] createRadii() { + if (mPosition == FastScroller.FastScrollerPopupPosition.CENTER) { + return new float[]{mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius}; + } + + if (Utils.isRtl(mRes)) { + return new float[]{mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, 0, 0}; + } else { + return new float[]{mCornerRadius, mCornerRadius, mCornerRadius, mCornerRadius, 0, 0, mCornerRadius, mCornerRadius}; + } + } + + public void draw(Canvas canvas) { + if (isVisible()) { + // Draw the fast scroller popup + int restoreCount = canvas.save(); + canvas.translate(mBgBounds.left, mBgBounds.top); + mTmpRect.set(mBgBounds); + mTmpRect.offsetTo(0, 0); + + mBackgroundPath.reset(); + mBackgroundRect.set(mTmpRect); + + float[] radii = createRadii(); + + mBackgroundPath.addRoundRect(mBackgroundRect, radii, Path.Direction.CW); + + mBackgroundPaint.setAlpha((int) (Color.alpha(mBackgroundColor) * mAlpha)); + mTextPaint.setAlpha((int) (mAlpha * 255)); + canvas.drawPath(mBackgroundPath, mBackgroundPaint); + canvas.drawText(mSectionName, (mBgBounds.width() - mTextBounds.width()) / 2, + mBgBounds.height() - (mBgBounds.height() - mTextBounds.height()) / 2, + mTextPaint); + canvas.restoreToCount(restoreCount); + } + } + + public void setSectionName(String sectionName) { + if (!sectionName.equals(mSectionName)) { + mSectionName = sectionName; + mTextPaint.getTextBounds(sectionName, 0, sectionName.length(), mTextBounds); + // Update the width to use measureText since that is more accurate + mTextBounds.right = (int) (mTextBounds.left + mTextPaint.measureText(sectionName)); + } + } + + /** + * Updates the bounds for the fast scroller. + * + * @return the invalidation rect for this update. + */ + public Rect updateFastScrollerBounds(FastScrollRecyclerView recyclerView, int thumbOffsetY) { + mInvalidateRect.set(mBgBounds); + + mBgBounds.top += recyclerView.getPaddingTop(); + mBgBounds.left += recyclerView.getPaddingLeft(); + mBgBounds.right -= recyclerView.getPaddingRight(); + mBgBounds.bottom -= recyclerView.getPaddingBottom(); + + if (isVisible()) { + // Calculate the dimensions and position of the fast scroller popup + int edgePadding = recyclerView.getScrollBarWidth(); + int bgPadding = Math.round((mBackgroundSize - mTextBounds.height()) / 10) * 5; + int bgHeight = mBackgroundSize; + int bgWidth = Math.max(mBackgroundSize, mTextBounds.width() + (2 * bgPadding)); + if (mPosition == FastScroller.FastScrollerPopupPosition.CENTER) { + mBgBounds.left = (recyclerView.getWidth() - bgWidth) / 2; + mBgBounds.right = mBgBounds.left + bgWidth; + mBgBounds.top = (recyclerView.getHeight() - bgHeight) / 2; + } else { + if (Utils.isRtl(mRes)) { + mBgBounds.left = (2 * recyclerView.getScrollBarWidth()); + mBgBounds.right = mBgBounds.left + bgWidth; + } else { + mBgBounds.right = recyclerView.getWidth() - (2 * recyclerView.getScrollBarWidth()); + mBgBounds.left = mBgBounds.right - bgWidth; + } + mBgBounds.top = recyclerView.getPaddingTop() + thumbOffsetY - bgHeight + recyclerView.getScrollBarThumbHeight() / 2; + mBgBounds.top = Math.max(edgePadding + recyclerView.getPaddingTop(), Math.min(mBgBounds.top, recyclerView.getHeight() - edgePadding - bgHeight - recyclerView.getPaddingBottom())); + } + mBgBounds.bottom = mBgBounds.top + bgHeight; + } else { + mBgBounds.setEmpty(); + } + + // Combine the old and new fast scroller bounds to create the full invalidate rect + mInvalidateRect.union(mBgBounds); + return mInvalidateRect; + } + + public boolean isVisible() { + return (mAlpha > 0f) && (!TextUtils.isEmpty(mSectionName)); + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/views/FastScrollRecyclerView.java b/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/views/FastScrollRecyclerView.java new file mode 100644 index 0000000000000000000000000000000000000000..87ae6432071a54be3f47e52db265ccfd150228e8 --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/views/FastScrollRecyclerView.java @@ -0,0 +1,561 @@ +/* + * Copyright (c) 2016 Tim Malseed + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.kuschku.quasseldroid.util.ui.fastscroll.views; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Typeface; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseIntArray; +import android.view.MotionEvent; +import android.view.View; + +import de.kuschku.quasseldroid.R; +import de.kuschku.quasseldroid.util.ui.fastscroll.interfaces.OnFastScrollStateChangeListener; +import de.kuschku.quasseldroid.util.ui.fastscroll.utils.Utils; + +public class FastScrollRecyclerView extends RecyclerView implements RecyclerView.OnItemTouchListener { + + private static final String TAG = "FastScrollRecyclerView"; + + private FastScroller mScrollbar; + + private boolean mFastScrollEnabled = true; + private ScrollPositionState mScrollPosState = new ScrollPositionState(); + private int mDownX; + private int mDownY; + private int mLastY; + private SparseIntArray mScrollOffsets; + private ScrollOffsetInvalidator mScrollOffsetInvalidator; + private OnFastScrollStateChangeListener mStateChangeListener; + + public FastScrollRecyclerView(Context context) { + this(context, null); + } + + public FastScrollRecyclerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FastScrollRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + TypedArray typedArray = context.getTheme().obtainStyledAttributes( + attrs, R.styleable.FastScrollRecyclerView, 0, 0); + try { + mFastScrollEnabled = typedArray.getBoolean(R.styleable.FastScrollRecyclerView_fastScrollThumbEnabled, true); + } finally { + typedArray.recycle(); + } + + mScrollbar = new FastScroller(context, this, attrs); + mScrollOffsetInvalidator = new ScrollOffsetInvalidator(); + mScrollOffsets = new SparseIntArray(); + } + + public int getScrollBarWidth() { + return mScrollbar.getWidth(); + } + + public int getScrollBarThumbHeight() { + return mScrollbar.getThumbHeight(); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + addOnItemTouchListener(this); + } + + @Override + public void setAdapter(Adapter adapter) { + if (getAdapter() != null) { + getAdapter().unregisterAdapterDataObserver(mScrollOffsetInvalidator); + } + + if (adapter != null) { + adapter.registerAdapterDataObserver(mScrollOffsetInvalidator); + } + + super.setAdapter(adapter); + } + + /** + * We intercept the touch handling only to support fast scrolling when initiated from the + * scroll bar. Otherwise, we fall back to the default RecyclerView touch handling. + */ + @Override + public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) { + return handleTouchEvent(ev); + } + + @Override + public void onTouchEvent(RecyclerView rv, MotionEvent ev) { + handleTouchEvent(ev); + } + + /** + * Handles the touch event and determines whether to show the fast scroller (or updates it if + * it is already showing). + */ + private boolean handleTouchEvent(MotionEvent ev) { + int action = ev.getAction(); + int x = (int) ev.getX(); + int y = (int) ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + // Keep track of the down positions + mDownX = x; + mDownY = mLastY = y; + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY, mStateChangeListener); + break; + case MotionEvent.ACTION_MOVE: + mLastY = y; + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY, mStateChangeListener); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mScrollbar.handleTouchEvent(ev, mDownX, mDownY, mLastY, mStateChangeListener); + break; + } + return mScrollbar.isDragging(); + } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + + } + + /** + * Returns the available scroll height: + * AvailableScrollHeight = Total height of the all items - last page height + * + * @param yOffset the offset from the top of the recycler view to start tracking. + */ + protected int getAvailableScrollHeight(int adapterHeight, int yOffset) { + int visibleHeight = getHeight(); + int scrollHeight = getPaddingTop() + yOffset + adapterHeight + getPaddingBottom(); + int availableScrollHeight = scrollHeight - visibleHeight; + return availableScrollHeight; + } + + /** + * Returns the available scroll bar height: + * AvailableScrollBarHeight = Total height of the visible view - thumb height + */ + protected int getAvailableScrollBarHeight() { + int visibleHeight = getHeight(); + int availableScrollBarHeight = visibleHeight - mScrollbar.getThumbHeight(); + return availableScrollBarHeight; + } + + @Override + public void draw(Canvas c) { + super.draw(c); + if (mFastScrollEnabled) { + onUpdateScrollbar(); + mScrollbar.draw(c); + } + } + + /** + * Updates the scrollbar thumb offset to match the visible scroll of the recycler view. It does + * this by mapping the available scroll area of the recycler view to the available space for the + * scroll bar. + * + * @param scrollPosState the current scroll position + * @param rowCount the number of rows, used to calculate the total scroll height (assumes that + */ + protected void updateThumbPosition(ScrollPositionState scrollPosState, int rowCount) { + + int availableScrollHeight; + int availableScrollBarHeight; + int scrolledPastHeight; + + if (getAdapter() instanceof MeasurableAdapter) { + availableScrollHeight = getAvailableScrollHeight(calculateAdapterHeight(), 0); + scrolledPastHeight = calculateScrollDistanceToPosition(scrollPosState.rowIndex); + } else { + availableScrollHeight = getAvailableScrollHeight(rowCount * scrollPosState.rowHeight, 0); + scrolledPastHeight = scrollPosState.rowIndex * scrollPosState.rowHeight; + } + + availableScrollBarHeight = getAvailableScrollBarHeight(); + + // Only show the scrollbar if there is height to be scrolled + if (availableScrollHeight <= 0) { + mScrollbar.setThumbPosition(-1, -1); + return; + } + + // Calculate the current scroll position, the scrollY of the recycler view accounts for the + // view padding, while the scrollBarY is drawn right up to the background padding (ignoring + // padding) + int scrollY = getPaddingTop() + scrolledPastHeight - scrollPosState.rowTopOffset; + int scrollBarY = (int) (((float) scrollY / availableScrollHeight) * availableScrollBarHeight); + + // Calculate the position and size of the scroll bar + int scrollBarX; + if (Utils.isRtl(getResources())) { + scrollBarX = 0; + } else { + scrollBarX = getWidth() - mScrollbar.getWidth(); + } + mScrollbar.setThumbPosition(scrollBarX, scrollBarY); + } + + /** + * Maps the touch (from 0..1) to the adapter position that should be visible. + */ + public String scrollToPositionAtProgress(float touchFraction) { + int itemCount = getAdapter().getItemCount(); + if (itemCount == 0) { + return ""; + } + int spanCount = 1; + int rowCount = itemCount; + if (getLayoutManager() instanceof GridLayoutManager) { + spanCount = ((GridLayoutManager) getLayoutManager()).getSpanCount(); + rowCount = (int) Math.ceil((double) rowCount / spanCount); + } + + // Stop the scroller if it is scrolling + stopScroll(); + + getCurScrollState(mScrollPosState); + + float itemPos; + int availableScrollHeight; + + int scrollPosition; + int scrollOffset; + + if (getAdapter() instanceof MeasurableAdapter) { + itemPos = findItemPosition(touchFraction); + availableScrollHeight = calculateAdapterHeight(); + scrollPosition = (int) itemPos; + scrollOffset = calculateScrollDistanceToPosition(scrollPosition) - (int) (touchFraction * availableScrollHeight); + } else { + itemPos = findItemPosition(touchFraction); + availableScrollHeight = getAvailableScrollHeight(rowCount * mScrollPosState.rowHeight, 0); + + //The exact position of our desired item + int exactItemPos = (int) (availableScrollHeight * touchFraction); + + //The offset used here is kind of hard to explain. + //If the position we wish to scroll to is, say, position 10.5, we scroll to position 10, + //and then offset by 0.5 * rowHeight. This is how we achieve smooth scrolling. + scrollPosition = spanCount * exactItemPos / mScrollPosState.rowHeight; + scrollOffset = -(exactItemPos % mScrollPosState.rowHeight); + } + + LinearLayoutManager layoutManager = ((LinearLayoutManager) getLayoutManager()); + layoutManager.scrollToPositionWithOffset(scrollPosition, scrollOffset); + + if (!(getAdapter() instanceof SectionedAdapter)) { + return ""; + } + + int posInt = (int) ((touchFraction == 1) ? itemPos - 1 : itemPos); + + SectionedAdapter sectionedAdapter = (SectionedAdapter) getAdapter(); + return sectionedAdapter.getSectionName(posInt); + } + + @SuppressWarnings("unchecked") + private float findItemPosition(float touchFraction) { + + if (getAdapter() instanceof MeasurableAdapter) { + MeasurableAdapter measurer = (MeasurableAdapter) getAdapter(); + int viewTop = (int) (touchFraction * calculateAdapterHeight()); + + for (int i = 0; i < getAdapter().getItemCount(); i++) { + int top = calculateScrollDistanceToPosition(i); + int bottom = top + measurer.getViewTypeHeight(this, findViewHolderForAdapterPosition(i), getAdapter().getItemViewType(i)); + if (viewTop >= top && viewTop <= bottom) { + return i; + } + } + + // Should never happen + Log.w(TAG, "Failed to find a view at the provided scroll fraction (" + touchFraction + ")"); + return touchFraction * getAdapter().getItemCount(); + } else { + return getAdapter().getItemCount() * touchFraction; + } + } + + /** + * Updates the bounds for the scrollbar. + */ + public void onUpdateScrollbar() { + + if (getAdapter() == null) { + return; + } + + int rowCount = getAdapter().getItemCount(); + if (getLayoutManager() instanceof GridLayoutManager) { + int spanCount = ((GridLayoutManager) getLayoutManager()).getSpanCount(); + rowCount = (int) Math.ceil((double) rowCount / spanCount); + } + // Skip early if, there are no items. + if (rowCount == 0) { + mScrollbar.setThumbPosition(-1, -1); + return; + } + + // Skip early if, there no child laid out in the container. + getCurScrollState(mScrollPosState); + if (mScrollPosState.rowIndex < 0) { + mScrollbar.setThumbPosition(-1, -1); + return; + } + + updateThumbPosition(mScrollPosState, rowCount); + } + + /** + * Returns the current scroll state of the apps rows. + */ + private void getCurScrollState(ScrollPositionState stateOut) { + stateOut.rowIndex = -1; + stateOut.rowTopOffset = -1; + stateOut.rowHeight = -1; + + int itemCount = getAdapter().getItemCount(); + + // Return early if there are no items, or no children. + if (itemCount == 0 || getChildCount() == 0) { + return; + } + + View child = getChildAt(0); + + stateOut.rowIndex = getChildAdapterPosition(child); + if (getLayoutManager() instanceof GridLayoutManager) { + stateOut.rowIndex = stateOut.rowIndex / ((GridLayoutManager) getLayoutManager()).getSpanCount(); + } + stateOut.rowTopOffset = getLayoutManager().getDecoratedTop(child); + stateOut.rowHeight = child.getHeight() + getLayoutManager().getTopDecorationHeight(child) + + getLayoutManager().getBottomDecorationHeight(child); + } + + /** + * Calculates the total height of all views above a position in the recycler view. This method + * should only be called when the attached adapter implements {@link MeasurableAdapter}. + * + * @param adapterIndex The index in the adapter to find the total height above the + * corresponding view + * @return The total height of all views above {@code adapterIndex} in pixels + */ + @SuppressWarnings("unchecked") + private int calculateScrollDistanceToPosition(int adapterIndex) { + if (!(getAdapter() instanceof MeasurableAdapter)) { + throw new IllegalStateException("calculateScrollDistanceToPosition() should only be called where the RecyclerView.Adapter is an instance of MeasurableAdapter"); + } + + if (mScrollOffsets.indexOfKey(adapterIndex) >= 0) { + return mScrollOffsets.get(adapterIndex); + } + + int totalHeight = 0; + MeasurableAdapter measurer = (MeasurableAdapter) getAdapter(); + + // TODO Take grid layouts into account + + for (int i = 0; i < adapterIndex; i++) { + mScrollOffsets.put(i, totalHeight); + int viewType = getAdapter().getItemViewType(i); + totalHeight += measurer.getViewTypeHeight(this, findViewHolderForAdapterPosition(i), viewType); + } + + mScrollOffsets.put(adapterIndex, totalHeight); + return totalHeight; + } + + /** + * Calculates the total height of the recycler view. This method should only be called when the + * attached adapter implements {@link MeasurableAdapter}. + * + * @return The total height of all rows in the RecyclerView + */ + private int calculateAdapterHeight() { + if (!(getAdapter() instanceof MeasurableAdapter)) { + throw new IllegalStateException("calculateAdapterHeight() should only be called where the RecyclerView.Adapter is an instance of MeasurableAdapter"); + } + return calculateScrollDistanceToPosition(getAdapter().getItemCount()); + } + + public void showScrollbar() { + mScrollbar.show(); + } + + public void setThumbColor(@ColorInt int color) { + mScrollbar.setThumbColor(color); + } + + public void setTrackColor(@ColorInt int color) { + mScrollbar.setTrackColor(color); + } + + public void setPopupBgColor(@ColorInt int color) { + mScrollbar.setPopupBgColor(color); + } + + public void setPopupTextColor(@ColorInt int color) { + mScrollbar.setPopupTextColor(color); + } + + public void setPopupTextSize(int textSize) { + mScrollbar.setPopupTextSize(textSize); + } + + public void setPopUpTypeface(Typeface typeface) { + mScrollbar.setPopupTypeface(typeface); + } + + public void setAutoHideDelay(int hideDelay) { + mScrollbar.setAutoHideDelay(hideDelay); + } + + public void setAutoHideEnabled(boolean autoHideEnabled) { + mScrollbar.setAutoHideEnabled(autoHideEnabled); + } + + public void setOnFastScrollStateChangeListener(OnFastScrollStateChangeListener stateChangeListener) { + mStateChangeListener = stateChangeListener; + } + + @Deprecated + public void setStateChangeListener(OnFastScrollStateChangeListener stateChangeListener) { + setOnFastScrollStateChangeListener(stateChangeListener); + } + + public void setThumbInactiveColor(@ColorInt int color) { + mScrollbar.setThumbInactiveColor(color); + } + + public void allowThumbInactiveColor(boolean allowInactiveColor) { + mScrollbar.enableThumbInactiveColor(allowInactiveColor); + } + + @Deprecated + public void setThumbInactiveColor(boolean allowInactiveColor) { + allowThumbInactiveColor(allowInactiveColor); + } + + public void setFastScrollEnabled(boolean fastScrollEnabled) { + mFastScrollEnabled = fastScrollEnabled; + } + + @Deprecated + public void setThumbEnabled(boolean thumbEnabled) { + setFastScrollEnabled(thumbEnabled); + } + + /** + * Set the FastScroll Popup position. This is either {@link FastScroller.FastScrollerPopupPosition#ADJACENT}, + * meaning the popup moves adjacent to the FastScroll thumb, or {@link FastScroller.FastScrollerPopupPosition#CENTER}, + * meaning the popup is static and centered within the RecyclerView. + */ + public void setPopupPosition(@FastScroller.FastScrollerPopupPosition int popupPosition) { + mScrollbar.setPopupPosition(popupPosition); + } + + public interface SectionedAdapter { + @NonNull + String getSectionName(int position); + } + + /** + * FastScrollRecyclerView by default assumes that all items in a RecyclerView will have + * ItemViews with the same heights so that the total height of all views in the RecyclerView + * can be calculated. If your list uses different view heights, then make your adapter implement + * this interface. + */ + public interface MeasurableAdapter<VH extends ViewHolder> { + /** + * Gets the height of a specific view type, including item decorations + * + * @param recyclerView The recyclerView that this item view will be placed in + * @param viewHolder The viewHolder that corresponds to this item view + * @param viewType The view type to get the height of + * @return The height of a single view for the given view type in pixels + */ + int getViewTypeHeight(RecyclerView recyclerView, @Nullable VH viewHolder, int viewType); + } + + /** + * The current scroll state of the recycler view. We use this in onUpdateScrollbar() + * and scrollToPositionAtProgress() to determine the scroll position of the recycler view so + * that we can calculate what the scroll bar looks like, and where to jump to from the fast + * scroller. + */ + public static class ScrollPositionState { + // The index of the first visible row + int rowIndex; + // The offset of the first visible row + int rowTopOffset; + // The height of a given row (they are currently all the same height) + int rowHeight; + } + + private class ScrollOffsetInvalidator extends AdapterDataObserver { + private void invalidateAllScrollOffsets() { + mScrollOffsets.clear(); + } + + @Override + public void onChanged() { + invalidateAllScrollOffsets(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount) { + invalidateAllScrollOffsets(); + } + + @Override + public void onItemRangeChanged(int positionStart, int itemCount, Object payload) { + invalidateAllScrollOffsets(); + } + + @Override + public void onItemRangeInserted(int positionStart, int itemCount) { + invalidateAllScrollOffsets(); + } + + @Override + public void onItemRangeRemoved(int positionStart, int itemCount) { + invalidateAllScrollOffsets(); + } + + @Override + public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) { + invalidateAllScrollOffsets(); + } + } +} diff --git a/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/views/FastScroller.java b/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/views/FastScroller.java new file mode 100644 index 0000000000000000000000000000000000000000..1cc7cee4fde78e199ff8fbd88ab355457e07259d --- /dev/null +++ b/app/src/main/java/de/kuschku/quasseldroid/util/ui/fastscroll/views/FastScroller.java @@ -0,0 +1,407 @@ +/* + * Copyright (c) 2016 Tim Malseed + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.kuschku.quasseldroid.util.ui.fastscroll.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.support.annotation.ColorInt; +import android.support.annotation.IntDef; +import android.support.annotation.Keep; +import android.support.v4.view.animation.FastOutLinearInInterpolator; +import android.support.v4.view.animation.LinearOutSlowInInterpolator; +import android.support.v7.widget.RecyclerView; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import java.lang.annotation.Retention; + +import de.kuschku.quasseldroid.R; +import de.kuschku.quasseldroid.util.ui.fastscroll.interfaces.OnFastScrollStateChangeListener; +import de.kuschku.quasseldroid.util.ui.fastscroll.utils.Utils; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +public class FastScroller { + private static final int DEFAULT_AUTO_HIDE_DELAY = 1500; + private final Runnable mHideRunnable; + private FastScrollRecyclerView mRecyclerView; + private FastScrollPopup mPopup; + private int mThumbHeight; + private int mWidth; + private Paint mThumb; + private Paint mTrack; + private Rect mTmpRect = new Rect(); + private Rect mInvalidateRect = new Rect(); + private Rect mInvalidateTmpRect = new Rect(); + // The inset is the buffer around which a point will still register as a click on the scrollbar + private int mTouchInset; + // This is the offset from the top of the scrollbar when the user first starts touching. To + // prevent jumping, this offset is applied as the user scrolls. + private int mTouchOffset; + private Point mThumbPosition = new Point(-1, -1); + private Point mOffset = new Point(0, 0); + private boolean mIsDragging; + private Animator mAutoHideAnimator; + private boolean mAnimatingShow; + private int mAutoHideDelay = DEFAULT_AUTO_HIDE_DELAY; + private boolean mAutoHideEnabled = true; + private int mThumbActiveColor; + private int mThumbInactiveColor = 0x79000000; + private boolean mThumbInactiveState; + + public FastScroller(Context context, FastScrollRecyclerView recyclerView, AttributeSet attrs) { + + Resources resources = context.getResources(); + + mRecyclerView = recyclerView; + mPopup = new FastScrollPopup(resources, recyclerView); + + mThumbHeight = Utils.toPixels(resources, 48); + mWidth = Utils.toPixels(resources, 8); + + mTouchInset = Utils.toPixels(resources, -24); + + mThumb = new Paint(Paint.ANTI_ALIAS_FLAG); + mTrack = new Paint(Paint.ANTI_ALIAS_FLAG); + + TypedArray typedArray = context.getTheme().obtainStyledAttributes( + attrs, R.styleable.FastScrollRecyclerView, 0, 0); + try { + mAutoHideEnabled = typedArray.getBoolean(R.styleable.FastScrollRecyclerView_fastScrollAutoHide, true); + mAutoHideDelay = typedArray.getInteger(R.styleable.FastScrollRecyclerView_fastScrollAutoHideDelay, DEFAULT_AUTO_HIDE_DELAY); + mThumbInactiveState = typedArray.getBoolean(R.styleable.FastScrollRecyclerView_fastScrollEnableThumbInactiveColor, true); + mThumbActiveColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_fastScrollThumbColor, 0x79000000); + mThumbInactiveColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_fastScrollThumbInactiveColor, 0x79000000); + + int trackColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_fastScrollTrackColor, 0x28000000); + int popupBgColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_fastScrollPopupBgColor, 0xff000000); + int popupTextColor = typedArray.getColor(R.styleable.FastScrollRecyclerView_fastScrollPopupTextColor, 0xffffffff); + int popupTextSize = typedArray.getDimensionPixelSize(R.styleable.FastScrollRecyclerView_fastScrollPopupTextSize, Utils.toScreenPixels(resources, 44)); + int popupBackgroundSize = typedArray.getDimensionPixelSize(R.styleable.FastScrollRecyclerView_fastScrollPopupBackgroundSize, Utils.toPixels(resources, 88)); + @FastScrollerPopupPosition int popupPosition = typedArray.getInteger(R.styleable.FastScrollRecyclerView_fastScrollPopupPosition, FastScrollerPopupPosition.ADJACENT); + + mTrack.setColor(trackColor); + mThumb.setColor(mThumbInactiveState ? mThumbInactiveColor : mThumbActiveColor); + mPopup.setBgColor(popupBgColor); + mPopup.setTextColor(popupTextColor); + mPopup.setTextSize(popupTextSize); + mPopup.setBackgroundSize(popupBackgroundSize); + mPopup.setPopupPosition(popupPosition); + } finally { + typedArray.recycle(); + } + + mHideRunnable = new Runnable() { + @Override + public void run() { + if (!mIsDragging) { + if (mAutoHideAnimator != null) { + mAutoHideAnimator.cancel(); + } + mAutoHideAnimator = ObjectAnimator.ofInt(FastScroller.this, "offsetX", (Utils.isRtl(mRecyclerView.getResources()) ? -1 : 1) * mWidth); + mAutoHideAnimator.setInterpolator(new FastOutLinearInInterpolator()); + mAutoHideAnimator.setDuration(200); + mAutoHideAnimator.start(); + } + } + }; + + mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + if (!mRecyclerView.isInEditMode()) { + show(); + } + } + }); + + if (mAutoHideEnabled) { + postAutoHideDelayed(); + } + } + + public int getThumbHeight() { + return mThumbHeight; + } + + public int getWidth() { + return mWidth; + } + + public boolean isDragging() { + return mIsDragging; + } + + /** + * Handles the touch event and determines whether to show the fast scroller (or updates it if + * it is already showing). + */ + public void handleTouchEvent(MotionEvent ev, int downX, int downY, int lastY, + OnFastScrollStateChangeListener stateChangeListener) { + ViewConfiguration config = ViewConfiguration.get(mRecyclerView.getContext()); + + int action = ev.getAction(); + int y = (int) ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + if (isNearPoint(downX, downY)) { + mTouchOffset = downY - mThumbPosition.y; + } + break; + case MotionEvent.ACTION_MOVE: + // Check if we should start scrolling + if (!mIsDragging && isNearPoint(downX, downY) && + Math.abs(y - downY) > config.getScaledTouchSlop()) { + mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true); + mIsDragging = true; + mTouchOffset += (lastY - downY); + mPopup.animateVisibility(true); + if (stateChangeListener != null) { + stateChangeListener.onFastScrollStart(); + } + if (mThumbInactiveState) { + mThumb.setColor(mThumbActiveColor); + } + } + if (mIsDragging) { + // Update the fastscroller section name at this touch position + int top = 0; + int bottom = mRecyclerView.getHeight() - mThumbHeight; + float boundedY = (float) Math.max(top, Math.min(bottom, y - mTouchOffset)); + String sectionName = mRecyclerView.scrollToPositionAtProgress((boundedY - top) / (bottom - top)); + mPopup.setSectionName(sectionName); + mPopup.animateVisibility(!sectionName.isEmpty()); + mRecyclerView.invalidate(mPopup.updateFastScrollerBounds(mRecyclerView, mThumbPosition.y)); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mTouchOffset = 0; + if (mIsDragging) { + mIsDragging = false; + mPopup.animateVisibility(false); + if (stateChangeListener != null) { + stateChangeListener.onFastScrollStop(); + } + } + if (mThumbInactiveState) { + mThumb.setColor(mThumbInactiveColor); + } + break; + } + } + + public void draw(Canvas canvas) { + + if (mThumbPosition.x < 0 || mThumbPosition.y < 0) { + return; + } + + //Background + canvas.drawRect( + mThumbPosition.x + mOffset.x, + mOffset.y, + mWidth + mThumbPosition.x + mOffset.x, + mRecyclerView.getHeight() + mOffset.y, + mTrack + ); + + //Handle + canvas.drawRect( + mThumbPosition.x + mOffset.x, + mThumbPosition.y + mOffset.y, + mWidth + mThumbPosition.x + mOffset.x, + mThumbHeight + mThumbPosition.y + mOffset.y, + mThumb + ); + + //Popup + mPopup.draw(canvas); + } + + /** + * Returns whether the specified points are near the scroll bar bounds. + */ + private boolean isNearPoint(int x, int y) { + mTmpRect.set(mThumbPosition.x, mThumbPosition.y, mThumbPosition.x + mWidth, + mThumbPosition.y + mThumbHeight); + mTmpRect.inset(mTouchInset, mTouchInset); + return mTmpRect.contains(x, y); + } + + public void setThumbPosition(int x, int y) { + if (mThumbPosition.x == x && mThumbPosition.y == y) { + return; + } + // do not create new objects here, this is called quite often + mInvalidateRect.set(mRecyclerView.getPaddingLeft() + mThumbPosition.x + mOffset.x, mRecyclerView.getPaddingTop() + mOffset.y, mThumbPosition.x + mOffset.x + mWidth - mRecyclerView.getPaddingRight(), mRecyclerView.getPaddingTop() + mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom() + mOffset.y); + mThumbPosition.set( + x * (mRecyclerView.getWidth() - mRecyclerView.getPaddingLeft() - mRecyclerView.getPaddingLeft()) / mRecyclerView.getWidth() + mRecyclerView.getPaddingLeft(), + y * (mRecyclerView.getHeight() - mRecyclerView.getPaddingTop() - mRecyclerView.getPaddingBottom()) / mRecyclerView.getHeight() + mRecyclerView.getPaddingTop() + ); + mInvalidateTmpRect.set(mRecyclerView.getPaddingLeft() + mThumbPosition.x + mOffset.x, mRecyclerView.getPaddingTop() + mOffset.y, mThumbPosition.x + mOffset.x + mWidth - mRecyclerView.getPaddingRight(), mRecyclerView.getPaddingTop() + mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom() + mOffset.y); + mInvalidateRect.union(mInvalidateTmpRect); + mRecyclerView.invalidate(mInvalidateRect); + } + + public void setOffset(int x, int y) { + if (mOffset.x == x && mOffset.y == y) { + return; + } + // do not create new objects here, this is called quite often + mInvalidateRect.set(mRecyclerView.getPaddingLeft() + mThumbPosition.x + mOffset.x, mRecyclerView.getPaddingTop() + mOffset.y, mThumbPosition.x + mOffset.x + mWidth - mRecyclerView.getPaddingRight(), mRecyclerView.getPaddingTop() + mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom() + mOffset.y); + mOffset.set(x, y); + mInvalidateTmpRect.set(mRecyclerView.getPaddingLeft() + mThumbPosition.x + mOffset.x, mRecyclerView.getPaddingTop() + mOffset.y, mThumbPosition.x + mOffset.x + mWidth - mRecyclerView.getPaddingRight(), mRecyclerView.getPaddingTop() + mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom() + mOffset.y); + mInvalidateRect.union(mInvalidateTmpRect); + mRecyclerView.invalidate(mInvalidateRect); + } + + @Keep + public int getOffsetX() { + return mOffset.x; + } + + // Setter/getter for the popup alpha for animations + @Keep + public void setOffsetX(int x) { + setOffset(x, mOffset.y); + } + + public void show() { + if (!mAnimatingShow) { + if (mAutoHideAnimator != null) { + mAutoHideAnimator.cancel(); + } + mAutoHideAnimator = ObjectAnimator.ofInt(this, "offsetX", 0); + mAutoHideAnimator.setInterpolator(new LinearOutSlowInInterpolator()); + mAutoHideAnimator.setDuration(150); + mAutoHideAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + super.onAnimationCancel(animation); + mAnimatingShow = false; + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + mAnimatingShow = false; + } + }); + mAnimatingShow = true; + mAutoHideAnimator.start(); + } + if (mAutoHideEnabled) { + postAutoHideDelayed(); + } else { + cancelAutoHide(); + } + } + + protected void postAutoHideDelayed() { + if (mRecyclerView != null) { + cancelAutoHide(); + mRecyclerView.postDelayed(mHideRunnable, mAutoHideDelay); + } + } + + protected void cancelAutoHide() { + if (mRecyclerView != null) { + mRecyclerView.removeCallbacks(mHideRunnable); + } + } + + public void setThumbColor(@ColorInt int color) { + mThumb.setColor(color); + mRecyclerView.invalidate(mInvalidateRect); + } + + public void setTrackColor(@ColorInt int color) { + mTrack.setColor(color); + mRecyclerView.invalidate(mInvalidateRect); + } + + public void setPopupBgColor(@ColorInt int color) { + mPopup.setBgColor(color); + } + + public void setPopupTextColor(@ColorInt int color) { + mPopup.setTextColor(color); + } + + public void setPopupTypeface(Typeface typeface) { + mPopup.setTypeface(typeface); + } + + public void setPopupTextSize(int size) { + mPopup.setTextSize(size); + } + + public void setAutoHideDelay(int hideDelay) { + mAutoHideDelay = hideDelay; + if (mAutoHideEnabled) { + postAutoHideDelayed(); + } + } + + public void setAutoHideEnabled(boolean autoHideEnabled) { + mAutoHideEnabled = autoHideEnabled; + if (autoHideEnabled) { + postAutoHideDelayed(); + } else { + cancelAutoHide(); + } + } + + public void setPopupPosition(@FastScrollerPopupPosition int popupPosition) { + mPopup.setPopupPosition(popupPosition); + } + + public void setThumbInactiveColor(@ColorInt int color) { + mThumbInactiveColor = color; + enableThumbInactiveColor(true); + } + + public void enableThumbInactiveColor(boolean enableInactiveColor) { + mThumbInactiveState = enableInactiveColor; + mThumb.setColor(mThumbInactiveState ? mThumbInactiveColor : mThumbActiveColor); + } + + @Deprecated + public void setThumbInactiveColor(boolean thumbInactiveColor) { + enableThumbInactiveColor(thumbInactiveColor); + } + + @Retention(SOURCE) + @IntDef({FastScrollerPopupPosition.ADJACENT, FastScrollerPopupPosition.CENTER}) + public @interface FastScrollerPopupPosition { + int ADJACENT = 0; + int CENTER = 1; + } +} diff --git a/app/src/main/res/layout/fragment_chat_list.xml b/app/src/main/res/layout/fragment_chat_list.xml index e67970b4af5429ab20b4002f370425bdefc33625..7a9e4f6bb3cd77aa78722cb48e76d08169bbde6b 100644 --- a/app/src/main/res/layout/fragment_chat_list.xml +++ b/app/src/main/res/layout/fragment_chat_list.xml @@ -49,7 +49,7 @@ </android.support.design.widget.AppBarLayout> - <com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView + <de.kuschku.quasseldroid.util.ui.fastscroll.views.FastScrollRecyclerView android:id="@+id/chatList" style="@style/Widget.FastScroller" android:layout_width="match_parent" diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 6efa6e004d543c800e3c6737b23867844c219221..94405b9f6bdbab903a71db47ebfac3c705f4e5b0 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -88,4 +88,52 @@ <!-- InsetLayouts --> <attr name="insetBackground" format="color|reference" /> + + <!-- DrawerRecyclerView --> + <declare-styleable name="DrawerRecyclerView"> + <attr name="insetBackground" /> + </declare-styleable> + + <!-- ShadowView --> + <declare-styleable name="ShadowView"> + <attr name="android:gravity" /> + </declare-styleable> + + <!-- RingtonePreference --> + <declare-styleable name="RingtonePreference"> + <!-- Which ringtone type(s) to show in the picker. --> + <attr name="ringtoneType"> + <!-- Ringtones. --> + <flag name="ringtone" value="1" /> + <!-- Notification sounds. --> + <flag name="notification" value="2" /> + <!-- Alarm sounds. --> + <flag name="alarm" value="4" /> + <!-- All available ringtone sounds. --> + <flag name="all" value="7" /> + </attr> + <!-- Whether to show an item for a default sound. --> + <attr name="showDefault" format="boolean" /> + <!-- Whether to show an item for 'Silent'. --> + <attr name="showSilent" format="boolean" /> + </declare-styleable> + + <!-- FastScroll RecyclerView --> + <declare-styleable name="FastScrollRecyclerView"> + <attr name="fastScrollThumbColor" format="reference|color" /> + <attr name="fastScrollThumbInactiveColor" format="reference|color" /> + <attr name="fastScrollTrackColor" format="reference|color" /> + <attr name="fastScrollPopupBgColor" format="reference|color" /> + <attr name="fastScrollPopupTextColor" format="reference|color" /> + <attr name="fastScrollPopupTextSize" format="reference|dimension" /> + <attr name="fastScrollPopupBackgroundSize" format="reference|dimension" /> + <attr name="fastScrollPopupPosition" format="enum"> + <enum name="adjacent" value="0" /> + <enum name="center" value="1" /> + </attr> + <attr name="fastScrollAutoHide" format="reference|boolean" /> + <attr name="fastScrollAutoHideDelay" format="reference|integer" /> + <attr name="fastScrollEnableThumbInactiveColor" format="reference|boolean" /> + <attr name="fastScrollThumbEnabled" format="reference|boolean" /> + </declare-styleable> </resources> diff --git a/app/src/main/res/values/styles_widgets.xml b/app/src/main/res/values/styles_widgets.xml index ccb58f75eb82fe71d1ee013d9d258b3915179be7..4e88a61ea92954086417f51052a45701a9286644 100644 --- a/app/src/main/res/values/styles_widgets.xml +++ b/app/src/main/res/values/styles_widgets.xml @@ -335,33 +335,4 @@ <style name="Widget.NavigationDrawerLayout" parent=""> <item name="insetBackground">#4000</item> </style> - - <!-- DrawerRecyclerView --> - <declare-styleable name="DrawerRecyclerView"> - <attr name="insetBackground" /> - </declare-styleable> - - <!-- ShadowView --> - <declare-styleable name="ShadowView"> - <attr name="android:gravity" /> - </declare-styleable> - - <!-- RingtonePreference --> - <declare-styleable name="RingtonePreference"> - <!-- Which ringtone type(s) to show in the picker. --> - <attr name="ringtoneType"> - <!-- Ringtones. --> - <flag name="ringtone" value="1" /> - <!-- Notification sounds. --> - <flag name="notification" value="2" /> - <!-- Alarm sounds. --> - <flag name="alarm" value="4" /> - <!-- All available ringtone sounds. --> - <flag name="all" value="7" /> - </attr> - <!-- Whether to show an item for a default sound. --> - <attr name="showDefault" format="boolean" /> - <!-- Whether to show an item for 'Silent'. --> - <attr name="showSilent" format="boolean" /> - </declare-styleable> </resources> diff --git a/build.gradle.kts b/build.gradle.kts index 03fdb3062583cbf8e16f500a142f8103b3c86eb8..d5c316a7c308ada9733edc04c6dd2180194a7344 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -24,7 +24,7 @@ buildscript { } dependencies { classpath("com.android.tools.build:gradle:3.1.3") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.50") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.51") } } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 8fd9f554ff6eaa21cc31388f8c12b4ecf9bcf905..1fb6ecc290ef0765c891cc73b9f29dd2dc8fa8b3 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -29,7 +29,7 @@ plugins { } dependencies { - implementation(kotlin("stdlib", "1.2.50")) + implementation(kotlin("stdlib", "1.2.51")) withVersion("27.1.1") { implementation("com.android.support", "support-annotations", version) diff --git a/lifecycle-ktx/build.gradle.kts b/lifecycle-ktx/build.gradle.kts index d4ab27137d11262265321fedd1f64f29aec4f42b..5686bb4e6b25cc7b894918c2a0af6b459d02e752 100644 --- a/lifecycle-ktx/build.gradle.kts +++ b/lifecycle-ktx/build.gradle.kts @@ -27,7 +27,7 @@ plugins { } dependencies { - implementation(kotlin("stdlib", "1.2.50")) + implementation(kotlin("stdlib", "1.2.51")) withVersion("27.1.1") { implementation("com.android.support", "support-annotations", version) diff --git a/malheur/build.gradle.kts b/malheur/build.gradle.kts index 3c7d79c5cf1c9d64dcc30b5be43feca17dc96558..83cce734e4789505552f0fc3f1ab70f6fb74510e 100644 --- a/malheur/build.gradle.kts +++ b/malheur/build.gradle.kts @@ -45,7 +45,7 @@ android { } dependencies { - implementation(kotlin("stdlib", "1.2.50")) + implementation(kotlin("stdlib", "1.2.51")) implementation("com.google.code.gson", "gson", "2.8.2") } diff --git a/persistence/build.gradle.kts b/persistence/build.gradle.kts index 5b6d07e2f0a06bbd4ef69eb94906d07c33deadef..9838a3e8bf25d9f84d8b04a67bde20fb4f6123df 100644 --- a/persistence/build.gradle.kts +++ b/persistence/build.gradle.kts @@ -52,7 +52,7 @@ android { } dependencies { - implementation(kotlin("stdlib", "1.2.50")) + implementation(kotlin("stdlib", "1.2.51")) // App Compat withVersion("27.1.1") { diff --git a/viewmodel/build.gradle.kts b/viewmodel/build.gradle.kts index de116f3272fd139faf23fa3cad67f1aac43ceb3f..4f7be5fd726e2045363776908762c4f3130e0978 100644 --- a/viewmodel/build.gradle.kts +++ b/viewmodel/build.gradle.kts @@ -45,7 +45,7 @@ android { } dependencies { - implementation(kotlin("stdlib", "1.2.50")) + implementation(kotlin("stdlib", "1.2.51")) // App Compat withVersion("27.1.1") {