mirror of https://codeberg.org/tom79/Fedilab
commit
867d2ed492
@ -0,0 +1,156 @@
|
||||
package app.fedilab.android.activities;
|
||||
/* Copyright 2022 Thomas Schneider
|
||||
*
|
||||
* This file is a part of Fedilab
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Fedilab 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 Fedilab; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.PagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import app.fedilab.android.R;
|
||||
import app.fedilab.android.client.entities.app.Timeline;
|
||||
import app.fedilab.android.databinding.ActivityTrendsBinding;
|
||||
import app.fedilab.android.helper.Helper;
|
||||
import app.fedilab.android.helper.ThemeHelper;
|
||||
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTag;
|
||||
import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline;
|
||||
|
||||
|
||||
public class TrendsActivity extends BaseActivity {
|
||||
|
||||
|
||||
private ActivityTrendsBinding binding;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.applyThemeBar(this);
|
||||
binding = ActivityTrendsBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary)));
|
||||
}
|
||||
|
||||
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.tags)));
|
||||
binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.toots)));
|
||||
binding.searchTabLayout.setTabTextColors(ThemeHelper.getAttColor(TrendsActivity.this, R.attr.mTextColor), ContextCompat.getColor(TrendsActivity.this, R.color.cyanea_accent_dark_reference));
|
||||
binding.searchTabLayout.setTabIconTint(ThemeHelper.getColorStateList(TrendsActivity.this));
|
||||
binding.searchTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
binding.trendsViewpager.setCurrentItem(tab.getPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
Fragment fragment;
|
||||
if (binding.trendsViewpager.getAdapter() != null) {
|
||||
fragment = (Fragment) binding.trendsViewpager.getAdapter().instantiateItem(binding.trendsViewpager, tab.getPosition());
|
||||
if (fragment instanceof FragmentMastodonTimeline) {
|
||||
FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
|
||||
fragmentMastodonTimeline.scrollToTop();
|
||||
} else if (fragment instanceof FragmentMastodonTag) {
|
||||
FragmentMastodonTag fragmentMastodonTag = ((FragmentMastodonTag) fragment);
|
||||
fragmentMastodonTag.scrollToTop();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
|
||||
binding.trendsViewpager.setAdapter(mPagerAdapter);
|
||||
binding.trendsViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
TabLayout.Tab tab = binding.searchTabLayout.getTabAt(position);
|
||||
if (tab != null)
|
||||
tab.select();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NotNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pager adapter for the 4 fragments
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
private static class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
|
||||
ScreenSlidePagerAdapter(FragmentManager fm) {
|
||||
super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
Bundle bundle = new Bundle();
|
||||
if (position == 0) {
|
||||
FragmentMastodonTag fragmentMastodonTag = new FragmentMastodonTag();
|
||||
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TREND_TAG);
|
||||
fragmentMastodonTag.setArguments(bundle);
|
||||
return fragmentMastodonTag;
|
||||
}
|
||||
FragmentMastodonTimeline fragmentMastodonTimeline = new FragmentMastodonTimeline();
|
||||
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TREND_MESSAGE);
|
||||
fragmentMastodonTimeline.setArguments(bundle);
|
||||
return fragmentMastodonTimeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,249 @@
|
||||
package app.fedilab.android.helper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.SparseArray;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Random;
|
||||
|
||||
import app.fedilab.android.R;
|
||||
|
||||
//Original work at https://stackoverflow.com/a/17830245
|
||||
public class CirclesDrawingView extends View {
|
||||
|
||||
|
||||
// Radius limit in pixels
|
||||
private final static int RADIUS_LIMIT = 100;
|
||||
private static final int CIRCLES_LIMIT = 1;
|
||||
private final Random mRadiusGenerator = new Random();
|
||||
/**
|
||||
* All available circles
|
||||
*/
|
||||
private final HashSet<CircleArea> mCircles = new HashSet<>(CIRCLES_LIMIT);
|
||||
private final SparseArray<CircleArea> mCirclePointer = new SparseArray<>(CIRCLES_LIMIT);
|
||||
/**
|
||||
* Paint to draw circles
|
||||
*/
|
||||
private Paint mCirclePaint;
|
||||
private CircleArea touchedCircle;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*
|
||||
* @param ct {@link android.content.Context}
|
||||
*/
|
||||
public CirclesDrawingView(final Context ct) {
|
||||
super(ct);
|
||||
init(ct);
|
||||
}
|
||||
|
||||
public CirclesDrawingView(final Context ct, final AttributeSet attrs) {
|
||||
super(ct, attrs);
|
||||
init(ct);
|
||||
}
|
||||
|
||||
public CirclesDrawingView(final Context ct, final AttributeSet attrs, final int defStyle) {
|
||||
super(ct, attrs, defStyle);
|
||||
init(ct);
|
||||
}
|
||||
|
||||
public CircleArea getTouchedCircle() {
|
||||
return this.touchedCircle;
|
||||
}
|
||||
|
||||
private void init(final Context ct) {
|
||||
// Generate bitmap used for background
|
||||
mCirclePaint = new Paint();
|
||||
|
||||
mCirclePaint.setColor(ResourcesCompat.getColor(getContext().getResources(), R.color.cyanea_accent, getContext().getTheme()));
|
||||
mCirclePaint.setStrokeWidth(10);
|
||||
mCirclePaint.setStyle(Paint.Style.STROKE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(final Canvas canv) {
|
||||
// background bitmap to cover all area
|
||||
for (CircleArea circle : mCircles) {
|
||||
canv.drawCircle(circle.centerX, circle.centerY, circle.radius, mCirclePaint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
boolean handled = false;
|
||||
|
||||
|
||||
int xTouch;
|
||||
int yTouch;
|
||||
int pointerId;
|
||||
int actionIndex = event.getActionIndex();
|
||||
|
||||
// get touch event coordinates and make transparent circle from it
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// it's the first pointer, so clear all existing pointers data
|
||||
clearCirclePointer();
|
||||
|
||||
xTouch = (int) event.getX(0);
|
||||
yTouch = (int) event.getY(0);
|
||||
|
||||
// check if we've touched inside some circle
|
||||
touchedCircle = obtainTouchedCircle(xTouch, yTouch);
|
||||
touchedCircle.centerX = xTouch;
|
||||
touchedCircle.centerY = yTouch;
|
||||
mCirclePointer.put(event.getPointerId(0), touchedCircle);
|
||||
invalidate();
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
// It secondary pointers, so obtain their ids and check circles
|
||||
pointerId = event.getPointerId(actionIndex);
|
||||
|
||||
xTouch = (int) event.getX(actionIndex);
|
||||
yTouch = (int) event.getY(actionIndex);
|
||||
|
||||
// check if we've touched inside some circle
|
||||
touchedCircle = obtainTouchedCircle(xTouch, yTouch);
|
||||
|
||||
mCirclePointer.put(pointerId, touchedCircle);
|
||||
touchedCircle.centerX = xTouch;
|
||||
touchedCircle.centerY = yTouch;
|
||||
invalidate();
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
final int pointerCount = event.getPointerCount();
|
||||
|
||||
for (actionIndex = 0; actionIndex < pointerCount; actionIndex++) {
|
||||
// Some pointer has moved, search it by pointer id
|
||||
pointerId = event.getPointerId(actionIndex);
|
||||
|
||||
xTouch = (int) event.getX(actionIndex);
|
||||
yTouch = (int) event.getY(actionIndex);
|
||||
|
||||
touchedCircle = mCirclePointer.get(pointerId);
|
||||
|
||||
if (null != touchedCircle) {
|
||||
touchedCircle.centerX = xTouch;
|
||||
touchedCircle.centerY = yTouch;
|
||||
}
|
||||
}
|
||||
invalidate();
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
clearCirclePointer();
|
||||
invalidate();
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
// not general pointer was up
|
||||
pointerId = event.getPointerId(actionIndex);
|
||||
|
||||
mCirclePointer.remove(pointerId);
|
||||
invalidate();
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
// do nothing
|
||||
break;
|
||||
}
|
||||
|
||||
return super.onTouchEvent(event) || handled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all CircleArea - pointer id relations
|
||||
*/
|
||||
private void clearCirclePointer() {
|
||||
|
||||
mCirclePointer.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search and creates new (if needed) circle based on touch area
|
||||
*
|
||||
* @param xTouch int x of touch
|
||||
* @param yTouch int y of touch
|
||||
* @return obtained {@link CircleArea}
|
||||
*/
|
||||
private CircleArea obtainTouchedCircle(final int xTouch, final int yTouch) {
|
||||
CircleArea touchedCircle = getTouchedCircle(xTouch, yTouch);
|
||||
|
||||
if (null == touchedCircle) {
|
||||
touchedCircle = new CircleArea(xTouch, yTouch, mRadiusGenerator.nextInt(RADIUS_LIMIT) + RADIUS_LIMIT);
|
||||
|
||||
if (mCircles.size() == CIRCLES_LIMIT) {
|
||||
// remove first circle
|
||||
mCircles.clear();
|
||||
}
|
||||
|
||||
mCircles.add(touchedCircle);
|
||||
}
|
||||
|
||||
return touchedCircle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines touched circle
|
||||
*
|
||||
* @param xTouch int x touch coordinate
|
||||
* @param yTouch int y touch coordinate
|
||||
* @return {@link CircleArea} touched circle or null if no circle has been touched
|
||||
*/
|
||||
private CircleArea getTouchedCircle(final int xTouch, final int yTouch) {
|
||||
CircleArea touched = null;
|
||||
|
||||
for (CircleArea circle : mCircles) {
|
||||
if ((circle.centerX - xTouch) * (circle.centerX - xTouch) + (circle.centerY - yTouch) * (circle.centerY - yTouch) <= circle.radius * circle.radius) {
|
||||
touched = circle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return touched;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
new Rect(0, 0, getMeasuredWidth(), getMeasuredHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores data about single circle
|
||||
*/
|
||||
public static class CircleArea {
|
||||
public int centerX;
|
||||
public int centerY;
|
||||
int radius;
|
||||
|
||||
CircleArea(int centerX, int centerY, int radius) {
|
||||
this.radius = radius;
|
||||
this.centerX = centerX;
|
||||
this.centerY = centerY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Circle[" + centerX + ", " + centerY + ", " + radius + "]";
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,209 @@
|
||||
package app.fedilab.android.helper;
|
||||
/* Copyright 2021 Thomas Schneider
|
||||
*
|
||||
* This file is a part of Fedilab
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Fedilab 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 Fedilab; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
import static com.bumptech.glide.load.resource.bitmap.TransformationUtils.PAINT_FLAGS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
|
||||
import com.bumptech.glide.load.resource.bitmap.TransformationUtils;
|
||||
import com.bumptech.glide.util.Synthetic;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import jp.wasabeef.glide.transformations.BitmapTransformation;
|
||||
|
||||
public class GlideFocus extends BitmapTransformation {
|
||||
|
||||
|
||||
private static final int VERSION = 1;
|
||||
private static final String ID = "app.fedilab.android.GlideFocus." + VERSION;
|
||||
private static final Set<String> MODELS_REQUIRING_BITMAP_LOCK =
|
||||
new HashSet<>(
|
||||
Arrays.asList(
|
||||
// Moto X gen 2
|
||||
"XT1085",
|
||||
"XT1092",
|
||||
"XT1093",
|
||||
"XT1094",
|
||||
"XT1095",
|
||||
"XT1096",
|
||||
"XT1097",
|
||||
"XT1098",
|
||||
// Moto G gen 1
|
||||
"XT1031",
|
||||
"XT1028",
|
||||
"XT937C",
|
||||
"XT1032",
|
||||
"XT1008",
|
||||
"XT1033",
|
||||
"XT1035",
|
||||
"XT1034",
|
||||
"XT939G",
|
||||
"XT1039",
|
||||
"XT1040",
|
||||
"XT1042",
|
||||
"XT1045",
|
||||
// Moto G gen 2
|
||||
"XT1063",
|
||||
"XT1064",
|
||||
"XT1068",
|
||||
"XT1069",
|
||||
"XT1072",
|
||||
"XT1077",
|
||||
"XT1078",
|
||||
"XT1079"));
|
||||
private static final Lock BITMAP_DRAWABLE_LOCK =
|
||||
MODELS_REQUIRING_BITMAP_LOCK.contains(Build.MODEL) ? new ReentrantLock() : new NoLock();
|
||||
private static final Paint DEFAULT_PAINT = new Paint(PAINT_FLAGS);
|
||||
private final float focalX;
|
||||
private final float focalY;
|
||||
|
||||
public GlideFocus(float focalX, float focalY) {
|
||||
this.focalX = focalX;
|
||||
this.focalY = focalY;
|
||||
}
|
||||
|
||||
private static void applyMatrix(
|
||||
@NonNull Bitmap inBitmap, @NonNull Bitmap targetBitmap, Matrix matrix) {
|
||||
BITMAP_DRAWABLE_LOCK.lock();
|
||||
try {
|
||||
Canvas canvas = new Canvas(targetBitmap);
|
||||
canvas.drawBitmap(inBitmap, matrix, DEFAULT_PAINT);
|
||||
clear(canvas);
|
||||
} finally {
|
||||
BITMAP_DRAWABLE_LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Bitmap.Config getNonNullConfig(@NonNull Bitmap bitmap) {
|
||||
return bitmap.getConfig() != null ? bitmap.getConfig() : Bitmap.Config.ARGB_8888;
|
||||
}
|
||||
|
||||
// Avoids warnings in M+.
|
||||
private static void clear(Canvas canvas) {
|
||||
canvas.setBitmap(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap transform(@NonNull Context context, @NonNull BitmapPool pool,
|
||||
@NonNull Bitmap inBitmap, int width, int height) {
|
||||
|
||||
if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) {
|
||||
return inBitmap;
|
||||
}
|
||||
// From ImageView/Bitmap.createScaledBitmap.
|
||||
final float scale;
|
||||
final float dx;
|
||||
final float dy;
|
||||
Matrix m = new Matrix();
|
||||
if (inBitmap.getWidth() * height > width * inBitmap.getHeight()) {
|
||||
scale = (float) height / (float) inBitmap.getHeight();
|
||||
dx = (width - inBitmap.getWidth() * scale) * 0.5f * (1 + focalX);
|
||||
dy = 0;
|
||||
} else {
|
||||
scale = (float) width / (float) inBitmap.getWidth();
|
||||
dx = 0;
|
||||
dy = (height - inBitmap.getHeight() * scale) * 0.5f * (1 + focalY);
|
||||
}
|
||||
|
||||
m.setScale(scale, scale);
|
||||
m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
|
||||
|
||||
Bitmap result = pool.get(width, height, getNonNullConfig(inBitmap));
|
||||
// We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given.
|
||||
TransformationUtils.setAlpha(inBitmap, result);
|
||||
|
||||
applyMatrix(inBitmap, result, m);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) {
|
||||
messageDigest.update((ID + focalX + focalY).getBytes(CHARSET));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof GlideFocus &&
|
||||
((GlideFocus) o).focalX == focalX &&
|
||||
((GlideFocus) o).focalY == focalY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (int) (ID.hashCode() + focalX * 100000 + focalY * 1000);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CropTransformation(width=" + focalX + ", height=" + focalY + ")";
|
||||
}
|
||||
|
||||
private static final class NoLock implements Lock {
|
||||
|
||||
@Synthetic
|
||||
NoLock() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lock() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lockInterruptibly() throws InterruptedException {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryLock() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryLock(long time, @NonNull TimeUnit unit) throws InterruptedException {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlock() {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Condition newCondition() {
|
||||
throw new UnsupportedOperationException("Should not be called");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
package app.fedilab.android.helper
|
||||
|
||||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.widget.FrameLayout
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.sign
|
||||
|
||||
/**
|
||||
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
|
||||
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
|
||||
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
|
||||
*
|
||||
* This solution has limitations when using multiple levels of nested scrollable elements
|
||||
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
|
||||
*/
|
||||
class NestedScrollableHost : FrameLayout {
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
private var touchSlop = 0
|
||||
private var initialX = 0f
|
||||
private var initialY = 0f
|
||||
private val parentViewPager: ViewPager2?
|
||||
get() {
|
||||
var v: View? = parent as? View
|
||||
while (v != null && v !is ViewPager2) {
|
||||
v = v.parent as? View
|
||||
}
|
||||
return v as? ViewPager2
|
||||
}
|
||||
|
||||
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
|
||||
|
||||
init {
|
||||
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
}
|
||||
|
||||
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
|
||||
val direction = -delta.sign.toInt()
|
||||
return when (orientation) {
|
||||
0 -> child?.canScrollHorizontally(direction) ?: false
|
||||
1 -> child?.canScrollVertically(direction) ?: false
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
|
||||
handleInterceptTouchEvent(e)
|
||||
return super.onInterceptTouchEvent(e)
|
||||
}
|
||||
|
||||
private fun handleInterceptTouchEvent(e: MotionEvent) {
|
||||
val orientation = parentViewPager?.orientation ?: return
|
||||
|
||||
// Early return if child can't scroll in same direction as parent
|
||||
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.action == MotionEvent.ACTION_DOWN) {
|
||||
initialX = e.x
|
||||
initialY = e.y
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
} else if (e.action == MotionEvent.ACTION_MOVE) {
|
||||
val dx = e.x - initialX
|
||||
val dy = e.y - initialY
|
||||
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
|
||||
|
||||
// assuming ViewPager2 touch-slop is 2x touch-slop of child
|
||||
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
|
||||
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
|
||||
|
||||
if (scaledDx > touchSlop || scaledDy > touchSlop) {
|
||||
if (isVpHorizontal == (scaledDy > scaledDx)) {
|
||||
// Gesture is perpendicular, allow all parents to intercept
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
} else {
|
||||
// Gesture is parallel, query child if movement in that direction is possible
|
||||
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
|
||||
// Child can scroll, disallow all parents to intercept
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
} else {
|
||||
// Child cannot scroll, allow all parents to intercept
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package app.fedilab.android.ui.drawer;
|
||||
/* Copyright 2022 Thomas Schneider
|
||||
*
|
||||
* This file is a part of Fedilab
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Fedilab 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 Fedilab; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Spannable;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.fedilab.android.R;
|
||||
import app.fedilab.android.client.entities.api.Field;
|
||||
import app.fedilab.android.databinding.DrawerFieldBinding;
|
||||
|
||||
|
||||
public class FieldAdapter extends RecyclerView.Adapter<FieldAdapter.FieldViewHolder> {
|
||||
|
||||
private final List<Field> fields;
|
||||
private Context context;
|
||||
|
||||
public FieldAdapter(List<Field> fields) {
|
||||
this.fields = fields;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return fields.size();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FieldViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
context = parent.getContext();
|
||||
DrawerFieldBinding itemBinding = DrawerFieldBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new FieldViewHolder(itemBinding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull FieldViewHolder holder, int position) {
|
||||
Field field = fields.get(position);
|
||||
if (field.verified_at != null) {
|
||||
holder.binding.value.setCompoundDrawablesWithIntrinsicBounds(null, null, ContextCompat.getDrawable(context, R.drawable.ic_baseline_verified_24), null);
|
||||
field.value_span.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, R.color.verified_text)), 0, field.value_span.toString().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
holder.binding.value.setText(field.value_span != null ? field.value_span : field.value, TextView.BufferType.SPANNABLE);
|
||||
holder.binding.value.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
holder.binding.label.setText(field.name);
|
||||
}
|
||||
|
||||
|
||||
public static class FieldViewHolder extends RecyclerView.ViewHolder {
|
||||
DrawerFieldBinding binding;
|
||||
|
||||
FieldViewHolder(DrawerFieldBinding itemView) {
|
||||
super(itemView.getRoot());
|
||||
binding = itemView;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M5,15L3,15v4c0,1.1 0.9,2 2,2h4v-2L5,19v-4zM5,5h4L9,3L5,3c-1.1,0 -2,0.9 -2,2v4h2L5,5zM19,3h-4v2h4v4h2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19h-4v2h4c1.1,0 2,-0.9 2,-2v-4h-2v4zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
|
||||
</vector>
|
@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16,6l2.29,2.29 -4.88,4.88 -4,-4L2,16.59 3.41,18l6,-6 4,4 6.3,-6.29L22,12V6z" />
|
||||
</vector>
|
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,8l-1.41,-1.41L13,12.17V0h-2v12.17l-5.58,-5.59L4,8l8,8 8,-8z" />
|
||||
</vector>
|
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,16l1.41,1.41L11,11.83V24h2V11.83l5.58,5.59L20,16l-8,-8 -8,8z" />
|
||||
</vector>
|
@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
Copyright 2022 Thomas Schneider
|
||||
|
||||
This file is a part of Fedilab
|
||||
|
||||
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.
|
||||
|
||||
Fedilab 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 Fedilab; if not,
|
||||
see <http://www.gnu.org/licenses>.
|
||||
-->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/search_tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?backgroundColorLight"
|
||||
app:tabGravity="fill"
|
||||
app:tabIndicatorColor="@color/cyanea_accent_dark_reference"
|
||||
app:tabMode="fixed" />
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/trends_viewpager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
</LinearLayout>
|
@ -1,9 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fetch_more"
|
||||
style="@style/MyOutlinedButton"
|
||||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/fetch_more_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="6dp"
|
||||
android:text="@string/fetch_more_messages" />
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="6dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fetch_more_min"
|
||||
style="@style/Widget.App.Button.IconOnly.Outline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:contentDescription="@string/fetch_more_messages"
|
||||
app:icon="@drawable/ic_fetch_more_arrow_upward"
|
||||
app:iconPadding="0dp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="6dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/fetch_more_messages"
|
||||
android:textAlignment="center"
|
||||
android:textColor="?colorAccent"
|
||||
android:textSize="18sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fetch_more_max"
|
||||
style="@style/Widget.App.Button.IconOnly.Outline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="6dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:contentDescription="@string/fetch_more_messages"
|
||||
app:icon="@drawable/ic_fetch_more_arrow_downward"
|
||||
app:iconPadding="0dp" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/field1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:minHeight="20dp"
|
||||
android:padding="5dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp" />
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/valueBG"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="2"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:textIsSelectable="true" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
@ -0,0 +1,13 @@
|
||||
Added:
|
||||
- Allow to set a focus point on previews (media editor)
|
||||
- Respect the focus point with previews
|
||||
- Pagination with the fetch more button support reading up or down
|
||||
- Add trends
|
||||
|
||||
Fixed:
|
||||
- Only last push notification is displayed (not grouped)
|
||||
- Bad behavior with the right/left scroll
|
||||
- Fix long profiles not fully displayed
|
||||
- Issues with some polls
|
||||
- Some crashes
|
||||
- Some bad behaviors
|
Loading…
Reference in new issue