Compare commits
40 Commits
v1.1.5+for
...
upstream-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28f6d00530 | ||
|
|
6e718d6765 | ||
|
|
b26d491eda | ||
|
|
abdbab9d7b | ||
|
|
af1c7194e6 | ||
|
|
a3b6decb72 | ||
|
|
ab3a98fd60 | ||
|
|
6e6fdbccd5 | ||
|
|
1764e5f3d1 | ||
|
|
836c493951 | ||
|
|
d667b8fa98 | ||
|
|
6a1032cd61 | ||
|
|
557d535e5a | ||
|
|
60517b00f3 | ||
|
|
faf5e8e82b | ||
|
|
7264982761 | ||
|
|
fedf74258f | ||
|
|
def4960be6 | ||
|
|
525cc69c70 | ||
|
|
7ed1b164b5 | ||
|
|
aafb08d7b3 | ||
|
|
a5fa44213d | ||
|
|
68e9d9d91c | ||
|
|
d9df150cf8 | ||
|
|
6f2e5a63d7 | ||
|
|
2433d457b6 | ||
|
|
9ad0bc2454 | ||
|
|
b79c769001 | ||
|
|
0dcdda75be | ||
|
|
e9fe4a82df | ||
|
|
b7dd376066 | ||
|
|
46081bed3e | ||
|
|
7053f4acb6 | ||
|
|
64e85f6992 | ||
|
|
aee27d36d5 | ||
|
|
165255235b | ||
|
|
144fdd562f | ||
|
|
5d6201d415 | ||
|
|
efe41f407e | ||
|
|
e5e36f4b10 |
11
README.md
11
README.md
@@ -3,10 +3,17 @@ Mastodon for Android
|
||||
|
||||
[](https://crowdin.com/project/mastodon-for-android)
|
||||
|
||||
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android"><img src="img/google-play-badge.png" height="50"></a>
|
||||
|
||||
This is the repository for the official Android app for Mastodon.
|
||||
|
||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||
alt="Get it on F-Droid"
|
||||
height="80">](https://f-droid.org/packages/org.joinmastodon.android/)
|
||||
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
|
||||
alt="Get it on Google Play"
|
||||
height="80">](https://play.google.com/store/apps/details?id=org.joinmastodon.android)
|
||||
|
||||
Or get the APK from the [The Releases Section](https://github.com/mastodon/mastodon-android/releases/latest).
|
||||
|
||||
## Contributing
|
||||
|
||||
Our goal is delivering a polished, professionally designed and user-friendly app. We proceed according to wireframes provided by a professional UX designer that works with Mastodon gGmbH. This means that any outside contributions that change the app visually must first be coordinated with the UX designer. *This can take time.* Furthermore, we work off of an internal roadmap and aim for feature-parity and consistency with our iOS app. The iOS app is designated as the "primary" between the two, therefore, if you want to request features, please do so in the [Mastodon for iOS](https://github.com/mastodon/mastodon-ios) repository, as you are requesting a feature to be both in iOS and Android (exceptions being system integrations specific to Android). On the other hand, any contributions that improve existing functionality, performance, or accessibility should not have any roadblocks to being merged.
|
||||
|
||||
@@ -4,21 +4,31 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
def getGitHash = { ->
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'rev-parse', '--short', 'upstream/master'
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
}
|
||||
|
||||
compileSdk 33
|
||||
defaultConfig {
|
||||
applicationId "org.joinmastodon.android"
|
||||
archivesBaseName = "upstream-${getGitHash()}"
|
||||
applicationId "org.joinmastodon.android.git"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 46
|
||||
versionName "1.1.5"
|
||||
versionCode 47
|
||||
versionName "1.1.5+${getGitHash()}"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "nl-rNL", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
// minifyEnabled true
|
||||
// shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
debug{
|
||||
|
||||
@@ -162,6 +162,8 @@ public class PushSubscriptionManager{
|
||||
@Override
|
||||
public void onSuccess(PushSubscription result){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
result.serverKey=result.serverKey.replace('/','_');
|
||||
result.serverKey=result.serverKey.replace('+','-');
|
||||
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
|
||||
|
||||
AccountSession session=AccountSessionManager.getInstance().tryGetAccount(accountID);
|
||||
|
||||
@@ -9,7 +9,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
|
||||
Request r=new Request();
|
||||
r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
|
||||
r.data.alerts=alerts;
|
||||
r.data.policy=policy;
|
||||
r.policy=policy;
|
||||
r.subscription.keys.p256dh=encryptionKey;
|
||||
r.subscription.keys.auth=authKey;
|
||||
setRequestBody(r);
|
||||
@@ -18,6 +18,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
|
||||
private static class Request{
|
||||
public Subscription subscription=new Subscription();
|
||||
public Data data=new Data();
|
||||
public PushSubscription.Policy policy;
|
||||
|
||||
private static class Keys{
|
||||
public String p256dh;
|
||||
@@ -31,7 +32,6 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
|
||||
|
||||
private static class Data{
|
||||
public PushSubscription.Alerts alerts;
|
||||
public PushSubscription.Policy policy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,36 @@ package org.joinmastodon.android.api.requests.notifications;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
public class UpdatePushSettings extends MastodonAPIRequest<PushSubscription>{
|
||||
private final PushSubscription.Policy policy;
|
||||
|
||||
public UpdatePushSettings(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
|
||||
super(HttpMethod.PUT, "/push/subscription", PushSubscription.class);
|
||||
setRequestBody(new Request(alerts, policy));
|
||||
this.policy=policy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateAndPostprocessResponse(PushSubscription respObj, Response httpResponse) throws IOException{
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
respObj.policy=policy;
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public Data data=new Data();
|
||||
public PushSubscription.Policy policy;
|
||||
|
||||
public Request(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
|
||||
this.data.alerts=alerts;
|
||||
this.data.policy=policy;
|
||||
this.policy=policy;
|
||||
}
|
||||
|
||||
private static class Data{
|
||||
public PushSubscription.Alerts alerts;
|
||||
public PushSubscription.Policy policy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ import org.joinmastodon.android.model.Status;
|
||||
import java.util.List;
|
||||
|
||||
public class GetTrendingStatuses extends MastodonAPIRequest<List<Status>>{
|
||||
public GetTrendingStatuses(int limit){
|
||||
public GetTrendingStatuses(int offset, int limit){
|
||||
super(HttpMethod.GET, "/trends/statuses", new TypeToken<>(){});
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
if(offset>0)
|
||||
addQueryParameter("offset", ""+offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import static android.os.ext.SdkExtensions.getExtensionVersion;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
@@ -20,6 +22,7 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
@@ -211,7 +214,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
else
|
||||
charLimit=500;
|
||||
|
||||
loadDefaultStatusVisibility(savedInstanceState);
|
||||
if (editingStatus == null) loadDefaultStatusVisibility(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -410,6 +413,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
|
||||
mainEditText.setSelectionListener(this);
|
||||
mainEditText.addTextChangedListener(new TextWatcher(){
|
||||
private int lastChangeStart, lastChangeCount;
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after){
|
||||
|
||||
@@ -419,6 +424,16 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count){
|
||||
if(s.length()==0)
|
||||
return;
|
||||
lastChangeStart=start;
|
||||
lastChangeCount=count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){
|
||||
if(s.length()==0)
|
||||
return;
|
||||
int start=lastChangeStart;
|
||||
int count=lastChangeCount;
|
||||
// offset one char back to catch an already typed '@' or '#' or ':'
|
||||
int realStart=start;
|
||||
start=Math.max(0, start-1);
|
||||
@@ -464,10 +479,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
editable.removeSpan(span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){
|
||||
updateCharCounter();
|
||||
}
|
||||
});
|
||||
@@ -514,7 +526,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
DraftMediaAttachment da=new DraftMediaAttachment();
|
||||
da.serverAttachment=att;
|
||||
da.description=att.description;
|
||||
da.uri=Uri.parse(att.previewUrl);
|
||||
da.uri=att.previewUrl!=null ? Uri.parse(att.previewUrl) : null;
|
||||
da.state=AttachmentUploadState.DONE;
|
||||
attachmentsView.addView(createMediaAttachmentView(da));
|
||||
attachments.add(da);
|
||||
@@ -782,14 +794,50 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if Android platform photopicker is available on the device\
|
||||
* @return whether the device supports photopicker intents.
|
||||
*/
|
||||
private boolean isPhotoPickerAvailable() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return true;
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
return getExtensionVersion(Build.VERSION_CODES.R) >= 2;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the correct intent for the device version to select media.
|
||||
*
|
||||
* <p>For Device version > T or R_SDK_v2, use the android platform photopicker via
|
||||
* {@link MediaStore#ACTION_PICK_IMAGES}
|
||||
*
|
||||
* <p>For earlier versions use the built in docs ui via {@link Intent#ACTION_GET_CONTENT}
|
||||
*/
|
||||
private void openFilePicker(){
|
||||
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
if(instance.configuration!=null && instance.configuration.mediaAttachments!=null && instance.configuration.mediaAttachments.supportedMimeTypes!=null && !instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()){
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, instance.configuration.mediaAttachments.supportedMimeTypes.toArray(new String[0]));
|
||||
}else{
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
|
||||
Intent intent;
|
||||
boolean usePhotoPicker = isPhotoPickerAvailable();
|
||||
if (usePhotoPicker) {
|
||||
intent = new Intent(MediaStore.ACTION_PICK_IMAGES);
|
||||
intent.putExtra(MediaStore.EXTRA_PICK_IMAGES_MAX, MediaStore.getPickImagesMaxLimit());
|
||||
} else {
|
||||
intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("*/*");
|
||||
}
|
||||
if (!usePhotoPicker && instance.configuration != null &&
|
||||
instance.configuration.mediaAttachments != null &&
|
||||
instance.configuration.mediaAttachments.supportedMimeTypes != null &&
|
||||
!instance.configuration.mediaAttachments.supportedMimeTypes.isEmpty()) {
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES,
|
||||
instance.configuration.mediaAttachments.supportedMimeTypes.toArray(
|
||||
new String[0]));
|
||||
} else {
|
||||
if (!usePhotoPicker) {
|
||||
// If photo picker is being used these are the default mimetypes.
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
|
||||
}
|
||||
}
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
startActivityForResult(intent, MEDIA_RESULT);
|
||||
@@ -871,7 +919,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
|
||||
ImageView img=thumb.findViewById(R.id.thumb);
|
||||
if(draft.serverAttachment!=null){
|
||||
ViewImageLoader.load(img, draft.serverAttachment.blurhashPlaceholder, new UrlImageLoaderRequest(draft.serverAttachment.previewUrl, V.dp(250), V.dp(250)));
|
||||
if(draft.serverAttachment.previewUrl!=null)
|
||||
ViewImageLoader.load(img, draft.serverAttachment.blurhashPlaceholder, new UrlImageLoaderRequest(draft.serverAttachment.previewUrl, V.dp(250), V.dp(250)));
|
||||
}else{
|
||||
if(draft.mimeType.startsWith("image/")){
|
||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(draft.uri, V.dp(250), V.dp(250)));
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
@@ -61,6 +62,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
private AnimatorSet currentNewPostsAnim;
|
||||
|
||||
private String maxID;
|
||||
private String lastSavedMarkerID;
|
||||
|
||||
public HomeTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
@@ -142,6 +144,29 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(!data.isEmpty()){
|
||||
String topPostID=displayItems.get(list.getChildAdapterPosition(list.getChildAt(0))-getMainAdapterOffset()).parentID;
|
||||
if(!topPostID.equals(lastSavedMarkerID)){
|
||||
lastSavedMarkerID=topPostID;
|
||||
new SaveMarkers(topPostID, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(SaveMarkers.Response result){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
lastSavedMarkerID=null;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onStatusCreated(StatusCreatedEvent ev){
|
||||
prependItems(Collections.singletonList(ev.status), true);
|
||||
}
|
||||
|
||||
@@ -281,7 +281,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
username+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
|
||||
}
|
||||
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+username));
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU || UiUtils.isMIUI()){ // Android 13+ SystemUI shows its own thing when you put things into the clipboard
|
||||
Toast.makeText(getActivity(), R.string.text_copied, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -17,11 +17,11 @@ public class DiscoverPostsFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetTrendingStatuses(count)
|
||||
currentRequest=new GetTrendingStatuses(offset, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, false);
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
}
|
||||
|
||||
private void onButtonClick(){
|
||||
if(!password.getText().equals(passwordConfirm.getText())){
|
||||
if(!password.getText().toString().equals(passwordConfirm.getText().toString())){
|
||||
passwordConfirm.setError(getString(R.string.signup_passwords_dont_match));
|
||||
passwordConfirmWrap.setErrorState();
|
||||
return;
|
||||
|
||||
@@ -23,6 +23,7 @@ public class PushSubscription extends BaseModel implements Cloneable{
|
||||
", endpoint='"+endpoint+'\''+
|
||||
", alerts="+alerts+
|
||||
", serverKey='"+serverKey+'\''+
|
||||
", policy="+policy+
|
||||
'}';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.Activity;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
@@ -10,6 +21,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
|
||||
public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{
|
||||
public PhotoStatusDisplayItem(String parentID, Status status, Attachment photo, BaseStatusListFragment parentFragment, int index, int totalPhotos, PhotoLayoutHelper.TiledLayoutResult tiledLayout, PhotoLayoutHelper.TiledLayoutResult.Tile thisTile){
|
||||
@@ -23,9 +35,109 @@ public class PhotoStatusDisplayItem extends ImageStatusDisplayItem{
|
||||
}
|
||||
|
||||
public static class Holder extends ImageStatusDisplayItem.Holder<PhotoStatusDisplayItem>{
|
||||
private final FrameLayout altTextWrapper;
|
||||
private final TextView altTextButton;
|
||||
private final View altTextScroller;
|
||||
private final ImageButton altTextClose;
|
||||
private final TextView altText;
|
||||
|
||||
private boolean altTextShown;
|
||||
private AnimatorSet currentAnim;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_photo, parent);
|
||||
altTextWrapper=findViewById(R.id.alt_text_wrapper);
|
||||
altTextButton=findViewById(R.id.alt_button);
|
||||
altTextScroller=findViewById(R.id.alt_text_scroller);
|
||||
altTextClose=findViewById(R.id.alt_text_close);
|
||||
altText=findViewById(R.id.alt_text);
|
||||
|
||||
altTextButton.setOnClickListener(this::onShowHideClick);
|
||||
altTextClose.setOnClickListener(this::onShowHideClick);
|
||||
// altTextScroller.setNestedScrollingEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ImageStatusDisplayItem item){
|
||||
super.onBind(item);
|
||||
altTextShown=false;
|
||||
if(currentAnim!=null)
|
||||
currentAnim.cancel();
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
altTextButton.setVisibility(View.VISIBLE);
|
||||
if(TextUtils.isEmpty(item.attachment.description)){
|
||||
altTextWrapper.setVisibility(View.GONE);
|
||||
}else{
|
||||
altTextWrapper.setVisibility(View.VISIBLE);
|
||||
altText.setText(item.attachment.description);
|
||||
}
|
||||
}
|
||||
|
||||
private void onShowHideClick(View v){
|
||||
boolean show=v.getId()==R.id.alt_button;
|
||||
|
||||
if(altTextShown==show)
|
||||
return;
|
||||
if(currentAnim!=null)
|
||||
currentAnim.cancel();
|
||||
|
||||
altTextShown=show;
|
||||
if(show){
|
||||
altTextScroller.setVisibility(View.VISIBLE);
|
||||
altTextClose.setVisibility(View.VISIBLE);
|
||||
}else{
|
||||
altTextButton.setVisibility(View.VISIBLE);
|
||||
// Hide these views temporarily so FrameLayout measures correctly
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// This is the current size...
|
||||
int prevLeft=altTextWrapper.getLeft();
|
||||
int prevRight=altTextWrapper.getRight();
|
||||
int prevTop=altTextWrapper.getTop();
|
||||
altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
|
||||
// ...and this is after the layout pass, right now the FrameLayout has its final size, but we animate that change
|
||||
if(!show){
|
||||
// Show these views again so they're visible for the duration of the animation.
|
||||
// No one would notice they were missing during measure/layout.
|
||||
altTextScroller.setVisibility(View.VISIBLE);
|
||||
altTextClose.setVisibility(View.VISIBLE);
|
||||
}
|
||||
AnimatorSet set=new AnimatorSet();
|
||||
set.playTogether(
|
||||
ObjectAnimator.ofInt(altTextWrapper, "left", prevLeft, altTextWrapper.getLeft()),
|
||||
ObjectAnimator.ofInt(altTextWrapper, "right", prevRight, altTextWrapper.getRight()),
|
||||
ObjectAnimator.ofInt(altTextWrapper, "top", prevTop, altTextWrapper.getTop()),
|
||||
ObjectAnimator.ofFloat(altTextButton, View.ALPHA, show ? 1f : 0f, show ? 0f : 1f),
|
||||
ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f),
|
||||
ObjectAnimator.ofFloat(altTextClose, View.ALPHA, show ? 0f : 1f, show ? 1f : 0f)
|
||||
);
|
||||
set.setDuration(300);
|
||||
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
set.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
if(show){
|
||||
altTextButton.setVisibility(View.GONE);
|
||||
}else{
|
||||
altTextScroller.setVisibility(View.GONE);
|
||||
altTextClose.setVisibility(View.GONE);
|
||||
}
|
||||
currentAnim=null;
|
||||
}
|
||||
});
|
||||
set.start();
|
||||
currentAnim=set;
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import android.os.Looper;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
@@ -587,4 +588,17 @@ public class UiUtils{
|
||||
}
|
||||
launchWebBrowser(context, url);
|
||||
}
|
||||
|
||||
private static String getSystemProperty(String key){
|
||||
try{
|
||||
Class<?> props=Class.forName("android.os.SystemProperties");
|
||||
Method get=props.getMethod("get", String.class);
|
||||
return (String)get.invoke(null, key);
|
||||
}catch(Exception ignore){}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isMIUI(){
|
||||
return !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.code"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
public class NestableScrollView extends ScrollView{
|
||||
private float downY, touchslop;
|
||||
private boolean didDisallow;
|
||||
|
||||
public NestableScrollView(Context context){
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public NestableScrollView(Context context, AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public NestableScrollView(Context context, AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
public NestableScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes){
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init(){
|
||||
touchslop=ViewConfiguration.get(getContext()).getScaledTouchSlop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev){
|
||||
if(ev.getAction()==MotionEvent.ACTION_DOWN){
|
||||
if(canScrollVertically(-1) || canScrollVertically(1)){
|
||||
getParent().requestDisallowInterceptTouchEvent(true);
|
||||
didDisallow=true;
|
||||
}else{
|
||||
didDisallow=false;
|
||||
}
|
||||
downY=ev.getY();
|
||||
}else if(didDisallow && ev.getAction()==MotionEvent.ACTION_MOVE){
|
||||
if(Math.abs(downY-ev.getY())>=touchslop){
|
||||
if(!canScrollVertically((int)(downY-ev.getY()))){
|
||||
didDisallow=false;
|
||||
getParent().requestDisallowInterceptTouchEvent(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onTouchEvent(ev);
|
||||
}
|
||||
}
|
||||
5
mastodon/src/main/res/drawable/bg_image_alt_overlay.xml
Normal file
5
mastodon/src/main/res/drawable/bg_image_alt_overlay.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#D9000000"/>
|
||||
<corners android:radius="4dp"/>
|
||||
</shape>
|
||||
5
mastodon/src/main/res/drawable/ic_baseline_close_24.xml
Normal file
5
mastodon/src/main/res/drawable/ic_baseline_close_24.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
@@ -10,4 +11,62 @@
|
||||
android:layout_gravity="center"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<!-- This is hidden from screenreaders because that same alt text is set as content description on the ImageView -->
|
||||
<FrameLayout
|
||||
android:id="@+id/alt_text_wrapper"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|bottom"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:importantForAccessibility="noHideDescendants"
|
||||
android:background="@drawable/bg_image_alt_overlay">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/alt_button"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="22dp"
|
||||
android:textAppearance="@style/m3_label_large"
|
||||
android:textColor="#FFF"
|
||||
android:gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:text="ALT"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/alt_text_close"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="end|top"
|
||||
android:src="@drawable/ic_baseline_close_24"
|
||||
android:tint="#FFF"
|
||||
android:background="?android:selectableItemBackgroundBorderless"/>
|
||||
|
||||
<org.joinmastodon.android.ui.views.NestableScrollView
|
||||
android:id="@+id/alt_text_scroller"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="40dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/alt_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:textAppearance="@style/m3_body_medium"
|
||||
android:textColor="#FFF"
|
||||
tools:text="Alt text goes here"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</org.joinmastodon.android.ui.views.NestableScrollView>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</org.joinmastodon.android.ui.views.ImageAttachmentFrameLayout>
|
||||
Reference in New Issue
Block a user