Basic status rendering
This commit is contained in:
parent
42d5f52ff5
commit
dfbc1fd2e2
|
@ -32,7 +32,8 @@ dependencies {
|
|||
implementation 'me.grishka.litex:recyclerview:1.2.1'
|
||||
implementation 'me.grishka.litex:swiperefreshlayout:1.1.0'
|
||||
implementation 'me.grishka.litex:browser:1.4.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.0'
|
||||
implementation 'me.grishka.appkit:appkit:1.1'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
}
|
|
@ -9,7 +9,8 @@
|
|||
android:allowBackup="true"
|
||||
android:label="@string/app_name"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Mastodon">
|
||||
android:theme="@style/Theme.Mastodon"
|
||||
android:largeHeap="true">
|
||||
|
||||
<activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize">
|
||||
<intent-filter>
|
||||
|
|
|
@ -4,6 +4,9 @@ import android.annotation.SuppressLint;
|
|||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.NetworkUtils;
|
||||
|
||||
public class MastodonApp extends Application{
|
||||
|
||||
@SuppressLint("StaticFieldLeak") // it's not a leak
|
||||
|
@ -12,6 +15,11 @@ public class MastodonApp extends Application{
|
|||
@Override
|
||||
public void onCreate(){
|
||||
super.onCreate();
|
||||
ImageCache.Parameters params=new ImageCache.Parameters();
|
||||
params.diskCacheSize=100*1024*1024;
|
||||
params.maxMemoryCacheSize=Integer.MAX_VALUE;
|
||||
ImageCache.setParams(params);
|
||||
NetworkUtils.setUserAgent("MastodonAndroid/"+BuildConfig.VERSION_NAME);
|
||||
context=getApplicationContext();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,6 +60,8 @@ public class MastodonAPIController{
|
|||
public <T> void submitRequest(final MastodonAPIRequest<T> req){
|
||||
thread.postRunnable(()->{
|
||||
try{
|
||||
if(req.canceled)
|
||||
return;
|
||||
Request.Builder builder=new Request.Builder()
|
||||
.url(req.getURL().toString())
|
||||
.method(req.getMethod(), req.getRequestBody())
|
||||
|
|
|
@ -32,6 +32,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||
TypeToken<T> respTypeToken;
|
||||
Call okhttpCall;
|
||||
Token token;
|
||||
boolean canceled;
|
||||
|
||||
public MastodonAPIRequest(HttpMethod method, String path, Class<T> respClass){
|
||||
this.path=path;
|
||||
|
@ -47,6 +48,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
|
|||
|
||||
@Override
|
||||
public synchronized void cancel(){
|
||||
canceled=true;
|
||||
if(okhttpCall!=null){
|
||||
okhttpCall.cancel();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package org.joinmastodon.android.api.requests.timelines;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetHomeTimeline extends MastodonAPIRequest<List<Status>>{
|
||||
public GetHomeTimeline(String maxID, String minID, int limit){
|
||||
super(HttpMethod.GET, "/timelines/home", new TypeToken<>(){});
|
||||
if(maxID!=null)
|
||||
addQueryParameter("max_id", maxID);
|
||||
if(minID!=null)
|
||||
addQueryParameter("min_id", minID);
|
||||
if(limit>0)
|
||||
addQueryParameter("limit", ""+limit);
|
||||
}
|
||||
}
|
|
@ -159,6 +159,7 @@ public class AccountSessionManager{
|
|||
.build();
|
||||
|
||||
new CustomTabsIntent.Builder()
|
||||
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
|
||||
.build()
|
||||
.launchUrl(context, uri);
|
||||
}
|
||||
|
|
|
@ -4,15 +4,43 @@ import android.os.Bundle;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class HomeFragment extends AppKitFragment{
|
||||
private FragmentRootLinearLayout content;
|
||||
private HomeTimelineFragment homeTimelineFragment;
|
||||
|
||||
private String accountID;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
}
|
||||
|
||||
public class HomeFragment extends ToolbarFragment{
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
|
||||
return new View(getActivity());
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
|
||||
content=new FragmentRootLinearLayout(getActivity());
|
||||
content.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
FrameLayout fragmentContainer=new FrameLayout(getActivity());
|
||||
fragmentContainer.setId(R.id.fragment_wrap);
|
||||
content.addView(fragmentContainer, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f));
|
||||
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
homeTimelineFragment=new HomeTimelineFragment();
|
||||
homeTimelineFragment.setArguments(args);
|
||||
getChildFragmentManager().beginTransaction().add(R.id.fragment_wrap, homeTimelineFragment).commit();
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class HomeTimelineFragment extends StatusListFragment{
|
||||
private String accountID;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.app_name);
|
||||
accountID=getArguments().getString("account");
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
new GetHomeTimeline(null, null, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
onDataLoaded(result, false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.fragments.BaseRecyclerFragment;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class StatusListFragment extends BaseRecyclerFragment<Status>{
|
||||
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
|
||||
|
||||
public StatusListFragment(){
|
||||
super(20);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
return new DisplayItemsAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppendItems(List<Status> items){
|
||||
super.onAppendItems(items);
|
||||
for(Status s:items){
|
||||
displayItems.addAll(StatusDisplayItem.buildItems(this, s));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClearItems(){
|
||||
super.onClearItems();
|
||||
displayItems.clear();
|
||||
}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
||||
public DisplayItemsAdapter(){
|
||||
super(imgLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public BindableViewHolder<StatusDisplayItem> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return (BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType], getActivity(), parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(BindableViewHolder<StatusDisplayItem> holder, int position){
|
||||
holder.bind(displayItems.get(position));
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return displayItems.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return displayItems.get(position).getType().ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return displayItems.get(position).getImageCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
return displayItems.get(position).getImageRequest(image);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -127,7 +127,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
|||
group.activeUsers+=instance.lastWeekUsers;
|
||||
}
|
||||
return group;
|
||||
}).sorted(Comparator.comparingInt(g->g.activeUsers)).forEachOrdered(ig->sortedList.addAll(ig.instances));
|
||||
}).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances));
|
||||
onDataLoaded(sortedList, false);
|
||||
updateFilteredList();
|
||||
}
|
||||
|
@ -299,18 +299,13 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInstanc
|
|||
}
|
||||
Instance cachedInstance=instancesCache.get(domain);
|
||||
if(cachedInstance!=null){
|
||||
boolean found=false;
|
||||
for(CatalogInstance ci:filteredData){
|
||||
if(ci.domain.equals(currentSearchQuery)){
|
||||
found=true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!found){
|
||||
CatalogInstance ci=cachedInstance.toCatalogInstance();
|
||||
filteredData.add(0, ci);
|
||||
adapter.notifyItemInserted(0);
|
||||
if(ci.domain.equals(currentSearchQuery))
|
||||
return;
|
||||
}
|
||||
CatalogInstance ci=cachedInstance.toCatalogInstance();
|
||||
filteredData.add(0, ci);
|
||||
adapter.notifyItemInserted(0);
|
||||
return;
|
||||
}
|
||||
if(loadingInstanceDomain!=null){
|
||||
|
|
|
@ -22,4 +22,13 @@ public class AccountField extends BaseModel{
|
|||
* Timestamp of when the server verified a URL value for a rel="me” link.
|
||||
*/
|
||||
public Instant verifiedAt;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "AccountField{"+
|
||||
"name='"+name+'\''+
|
||||
", value='"+value+'\''+
|
||||
", verifiedAt="+verifiedAt+
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.joinmastodon.android.ui.utils.BlurHashDecoder;
|
||||
import org.joinmastodon.android.ui.utils.BlurHashDrawable;
|
||||
|
||||
public class Attachment extends BaseModel{
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public Type type;
|
||||
@RequiredField
|
||||
public String url;
|
||||
@RequiredField
|
||||
public String previewUrl;
|
||||
public String remoteUrl;
|
||||
public String description;
|
||||
public String blurhash;
|
||||
public Metadata meta;
|
||||
|
||||
public transient Drawable blurhashPlaceholder;
|
||||
|
||||
public int getWidth(){
|
||||
if(meta==null)
|
||||
return 0;
|
||||
if(meta.width>0)
|
||||
return meta.width;
|
||||
if(meta.original!=null && meta.original.width>0)
|
||||
return meta.original.width;
|
||||
if(meta.small!=null && meta.small.width>0)
|
||||
return meta.small.width;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int getHeight(){
|
||||
if(meta==null)
|
||||
return 0;
|
||||
if(meta.height>0)
|
||||
return meta.height;
|
||||
if(meta.original!=null && meta.original.height>0)
|
||||
return meta.original.height;
|
||||
if(meta.small!=null && meta.small.height>0)
|
||||
return meta.small.height;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
if(blurhash!=null){
|
||||
Bitmap placeholder=BlurHashDecoder.decode(blurhash, 16, 16);
|
||||
if(placeholder!=null)
|
||||
blurhashPlaceholder=new BlurHashDrawable(placeholder, getWidth(), getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Attachment{"+
|
||||
"id='"+id+'\''+
|
||||
", type="+type+
|
||||
", url='"+url+'\''+
|
||||
", previewUrl='"+previewUrl+'\''+
|
||||
", remoteUrl='"+remoteUrl+'\''+
|
||||
", description='"+description+'\''+
|
||||
", blurhash='"+blurhash+'\''+
|
||||
", meta="+meta+
|
||||
'}';
|
||||
}
|
||||
|
||||
public enum Type{
|
||||
@SerializedName("image")
|
||||
IMAGE,
|
||||
@SerializedName("gifv")
|
||||
GIFV,
|
||||
@SerializedName("video")
|
||||
VIDEO,
|
||||
@SerializedName("audio")
|
||||
AUDIO,
|
||||
@SerializedName("unknown")
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public static class Metadata{
|
||||
public double duration;
|
||||
public int width;
|
||||
public int height;
|
||||
public double aspect;
|
||||
public PointF focus;
|
||||
public SizeMetadata original;
|
||||
public SizeMetadata small;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Metadata{"+
|
||||
"duration="+duration+
|
||||
", width="+width+
|
||||
", height="+height+
|
||||
", aspect="+aspect+
|
||||
", focus="+focus+
|
||||
", original="+original+
|
||||
", small="+small+
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class SizeMetadata{
|
||||
public int width;
|
||||
public int height;
|
||||
public double aspect;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "SizeMetadata{"+
|
||||
"width="+width+
|
||||
", height="+height+
|
||||
", aspect="+aspect+
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.joinmastodon.android.ui.utils.BlurHashDecoder;
|
||||
import org.joinmastodon.android.ui.utils.BlurHashDrawable;
|
||||
|
||||
public class Card extends BaseModel{
|
||||
@RequiredField
|
||||
public String url;
|
||||
@RequiredField
|
||||
public String description;
|
||||
@RequiredField
|
||||
public Type type;
|
||||
public String authorName;
|
||||
public String authorUrl;
|
||||
public String providerName;
|
||||
public String providerUrl;
|
||||
// public String html;
|
||||
public int width;
|
||||
public int height;
|
||||
public String image;
|
||||
public String embedUrl;
|
||||
public String blurhash;
|
||||
|
||||
public transient Drawable blurhashPlaceholder;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
if(blurhash!=null){
|
||||
Bitmap placeholder=BlurHashDecoder.decode(blurhash, 16, 16);
|
||||
if(placeholder!=null)
|
||||
blurhashPlaceholder=new BlurHashDrawable(placeholder, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Card{"+
|
||||
"url='"+url+'\''+
|
||||
", description='"+description+'\''+
|
||||
", type="+type+
|
||||
", authorName='"+authorName+'\''+
|
||||
", authorUrl='"+authorUrl+'\''+
|
||||
", providerName='"+providerName+'\''+
|
||||
", providerUrl='"+providerUrl+'\''+
|
||||
", width="+width+
|
||||
", height="+height+
|
||||
", image='"+image+'\''+
|
||||
", embedUrl='"+embedUrl+'\''+
|
||||
", blurhash='"+blurhash+'\''+
|
||||
'}';
|
||||
}
|
||||
|
||||
public enum Type{
|
||||
@SerializedName("link")
|
||||
LINK,
|
||||
@SerializedName("photo")
|
||||
PHOTO,
|
||||
@SerializedName("video")
|
||||
VIDEO,
|
||||
@SerializedName("rich")
|
||||
RICH
|
||||
}
|
||||
}
|
|
@ -30,4 +30,15 @@ public class Emoji extends BaseModel{
|
|||
* Used for sorting custom emoji in the picker.
|
||||
*/
|
||||
public String category;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Emoji{"+
|
||||
"shortcode='"+shortcode+'\''+
|
||||
", url='"+url+'\''+
|
||||
", staticUrl='"+staticUrl+'\''+
|
||||
", visibleInPicker="+visibleInPicker+
|
||||
", category='"+category+'\''+
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Hashtag extends BaseModel{
|
||||
@RequiredField
|
||||
public String name;
|
||||
@RequiredField
|
||||
public String url;
|
||||
public List<History> history;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Hashtag{"+
|
||||
"name='"+name+'\''+
|
||||
", url='"+url+'\''+
|
||||
", history="+history+
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public class History extends BaseModel{
|
||||
public long day; // unixtime
|
||||
public int uses;
|
||||
public int accounts;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "History{"+
|
||||
"day="+day+
|
||||
", uses="+uses+
|
||||
", accounts="+accounts+
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public class Mention extends BaseModel{
|
||||
public String id;
|
||||
public String username;
|
||||
public String acct;
|
||||
public String url;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Mention{"+
|
||||
"id='"+id+'\''+
|
||||
", username='"+username+'\''+
|
||||
", acct='"+acct+'\''+
|
||||
", url='"+url+'\''+
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public class Poll extends BaseModel{
|
||||
public String id;
|
||||
public Instant expiresAt;
|
||||
public boolean expired;
|
||||
public boolean multiple;
|
||||
public int votersCount;
|
||||
public boolean voted;
|
||||
public int[] ownVotes;
|
||||
public List<Option> options;
|
||||
public List<Emoji> emojis;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
for(Emoji e:emojis)
|
||||
e.postprocess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Poll{"+
|
||||
"id='"+id+'\''+
|
||||
", expiresAt="+expiresAt+
|
||||
", expired="+expired+
|
||||
", multiple="+multiple+
|
||||
", votersCount="+votersCount+
|
||||
", voted="+voted+
|
||||
", ownVotes="+Arrays.toString(ownVotes)+
|
||||
", options="+options+
|
||||
", emojis="+emojis+
|
||||
'}';
|
||||
}
|
||||
|
||||
public static class Option{
|
||||
public String title;
|
||||
public Integer votesCount;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Option{"+
|
||||
"title='"+title+'\''+
|
||||
", votesCount="+votesCount+
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -42,4 +42,16 @@ public class Source extends BaseModel{
|
|||
for(AccountField f:fields)
|
||||
f.postprocess();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Source{"+
|
||||
"note='"+note+'\''+
|
||||
", fields="+fields+
|
||||
", privacy="+privacy+
|
||||
", sensitive="+sensitive+
|
||||
", language='"+language+'\''+
|
||||
", followRequestCount="+followRequestCount+
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public class Status extends BaseModel{
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public String uri;
|
||||
@RequiredField
|
||||
public Instant createdAt;
|
||||
@RequiredField
|
||||
public Account account;
|
||||
@RequiredField
|
||||
public String content;
|
||||
@RequiredField
|
||||
public StatusPrivacy visibility;
|
||||
public boolean sensitive;
|
||||
@RequiredField
|
||||
public String spoilerText;
|
||||
@RequiredField
|
||||
public List<Attachment> mediaAttachments;
|
||||
public Application application;
|
||||
@RequiredField
|
||||
public List<Mention> mentions;
|
||||
@RequiredField
|
||||
public List<Hashtag> tags;
|
||||
@RequiredField
|
||||
public List<Emoji> emojis;
|
||||
public int reblogsCount;
|
||||
public int favouritesCount;
|
||||
public int repliesCount;
|
||||
|
||||
public String url;
|
||||
public String inReplyToId;
|
||||
public String inReplyToAccountId;
|
||||
public Status reblog;
|
||||
public Poll poll;
|
||||
public Card card;
|
||||
public String language;
|
||||
public String text;
|
||||
|
||||
public boolean favourited;
|
||||
public boolean reblogged;
|
||||
public boolean muted;
|
||||
public boolean bookmarked;
|
||||
public boolean pinned;
|
||||
|
||||
public transient CharSequence processedContent;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
if(application!=null)
|
||||
application.postprocess();
|
||||
for(Mention m:mentions)
|
||||
m.postprocess();
|
||||
for(Hashtag t:tags)
|
||||
t.postprocess();
|
||||
for(Emoji e:emojis)
|
||||
e.postprocess();
|
||||
for(Attachment a:mediaAttachments)
|
||||
a.postprocess();
|
||||
account.postprocess();
|
||||
if(poll!=null)
|
||||
poll.postprocess();
|
||||
if(card!=null)
|
||||
card.postprocess();
|
||||
if(reblog!=null)
|
||||
reblog.postprocess();
|
||||
|
||||
if(!TextUtils.isEmpty(content)){
|
||||
processedContent=HtmlParser.parse(content, emojis);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Status{"+
|
||||
"id='"+id+'\''+
|
||||
", uri='"+uri+'\''+
|
||||
", createdAt="+createdAt+
|
||||
", account="+account+
|
||||
", content='"+content+'\''+
|
||||
", visibility="+visibility+
|
||||
", sensitive="+sensitive+
|
||||
", spoilerText='"+spoilerText+'\''+
|
||||
", mediaAttachments="+mediaAttachments+
|
||||
", application="+application+
|
||||
", mentions="+mentions+
|
||||
", tags="+tags+
|
||||
", emojis="+emojis+
|
||||
", reblogsCount="+reblogsCount+
|
||||
", favouritesCount="+favouritesCount+
|
||||
", repliesCount="+repliesCount+
|
||||
", url='"+url+'\''+
|
||||
", inReplyToId='"+inReplyToId+'\''+
|
||||
", inReplyToAccountId='"+inReplyToAccountId+'\''+
|
||||
", reblog="+reblog+
|
||||
", poll="+poll+
|
||||
", card="+card+
|
||||
", language='"+language+'\''+
|
||||
", text='"+text+'\''+
|
||||
", favourited="+favourited+
|
||||
", reblogged="+reblogged+
|
||||
", muted="+muted+
|
||||
", bookmarked="+bookmarked+
|
||||
", pinned="+pinned+
|
||||
", processedContent="+processedContent+
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
|
||||
public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
private Account user;
|
||||
private Instant createdAt;
|
||||
private ImageLoaderRequest avaRequest;
|
||||
|
||||
public HeaderStatusDisplayItem(Status status, Account user, Instant createdAt){
|
||||
super(status);
|
||||
this.user=user;
|
||||
this.createdAt=createdAt;
|
||||
avaRequest=new UrlImageLoaderRequest(user.avatar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.HEADER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return avaRequest;
|
||||
}
|
||||
|
||||
public static class Holder extends BindableViewHolder<HeaderStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final TextView name, subtitle;
|
||||
private final ImageView avatar;
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_header, parent);
|
||||
name=findViewById(R.id.name);
|
||||
subtitle=findViewById(R.id.subtitle);
|
||||
avatar=findViewById(R.id.avatar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(HeaderStatusDisplayItem item){
|
||||
name.setText(item.user.displayName);
|
||||
subtitle.setText('@'+item.user.acct);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
avatar.setImageDrawable(drawable);
|
||||
if(drawable instanceof Animatable)
|
||||
((Animatable) drawable).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
avatar.setImageBitmap(null);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
|
||||
public class PhotoStatusDisplayItem extends StatusDisplayItem{
|
||||
private Attachment attachment;
|
||||
private ImageLoaderRequest request;
|
||||
public PhotoStatusDisplayItem(Status status, Attachment photo){
|
||||
super(status);
|
||||
this.attachment=photo;
|
||||
request=new UrlImageLoaderRequest(photo.url, 1000, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.PHOTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return request;
|
||||
}
|
||||
|
||||
public static class Holder extends BindableViewHolder<PhotoStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final ImageView photo;
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_photo, parent);
|
||||
photo=findViewById(R.id.photo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(PhotoStatusDisplayItem item){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
photo.setImageDrawable(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
photo.setImageDrawable(item.attachment.blurhashPlaceholder);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
|
||||
public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
public ReblogOrReplyLineStatusDisplayItem(Status status){
|
||||
super(status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.REBLOG_OR_REPLY_LINE;
|
||||
}
|
||||
|
||||
public static class Holder extends BindableViewHolder<ReblogOrReplyLineStatusDisplayItem>{
|
||||
private final TextView text;
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_reblog_or_reply_line, parent);
|
||||
text=findViewById(R.id.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ReblogOrReplyLineStatusDisplayItem item){
|
||||
if(item.status.reblog!=null){
|
||||
text.setText(itemView.getContext().getString(R.string.user_boosted, item.status.account.displayName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.text.TextUtils;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
|
||||
public abstract class StatusDisplayItem{
|
||||
public final Status status;
|
||||
|
||||
public StatusDisplayItem(Status status){
|
||||
this.status=status;
|
||||
}
|
||||
|
||||
public abstract Type getType();
|
||||
|
||||
public int getImageCount(){
|
||||
return 0;
|
||||
}
|
||||
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return null;
|
||||
}
|
||||
|
||||
public static BindableViewHolder<? extends StatusDisplayItem> createViewHolder(Type type, Activity activity, ViewGroup parent){
|
||||
return switch(type){
|
||||
case HEADER -> new HeaderStatusDisplayItem.Holder(activity, parent);
|
||||
case REBLOG_OR_REPLY_LINE -> new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent);
|
||||
case TEXT -> new TextStatusDisplayItem.Holder(activity, parent);
|
||||
case PHOTO -> new PhotoStatusDisplayItem.Holder(activity, parent);
|
||||
default -> throw new UnsupportedOperationException();
|
||||
};
|
||||
}
|
||||
|
||||
public static List<StatusDisplayItem> buildItems(Fragment fragment, Status status){
|
||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||
Status statusForContent=status.reblog==null ? status : status.reblog;
|
||||
if(status.reblog!=null){
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(status));
|
||||
}
|
||||
items.add(new HeaderStatusDisplayItem(status, statusForContent.account, statusForContent.createdAt));
|
||||
if(!TextUtils.isEmpty(statusForContent.content))
|
||||
items.add(new TextStatusDisplayItem(status, statusForContent.processedContent, fragment));
|
||||
for(Attachment attachment:statusForContent.mediaAttachments){
|
||||
if(attachment.type==Attachment.Type.IMAGE){
|
||||
items.add(new PhotoStatusDisplayItem(status, attachment));
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public enum Type{
|
||||
HEADER,
|
||||
REBLOG_OR_REPLY_LINE,
|
||||
TEXT,
|
||||
PHOTO,
|
||||
VIDEO,
|
||||
GIFV,
|
||||
AUDIO,
|
||||
POLL_HEADER,
|
||||
POLL_OPTION,
|
||||
POLL_FOOTER,
|
||||
CARD,
|
||||
FOOTER,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Spanned;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
import org.joinmastodon.android.ui.text.LinkSpan;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.MovieDrawable;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class TextStatusDisplayItem extends StatusDisplayItem implements LinkSpan.OnLinkClickListener{
|
||||
private CharSequence text;
|
||||
private ImageLoaderRequest[] emojiRequests;
|
||||
private Fragment parentFragment;
|
||||
public TextStatusDisplayItem(Status status, CharSequence text, Fragment parentFragment){
|
||||
super(status);
|
||||
this.text=text;
|
||||
this.parentFragment=parentFragment;
|
||||
if(text instanceof Spanned){
|
||||
CustomEmojiSpan[] emojiSpans=((Spanned) text).getSpans(0, text.length(), CustomEmojiSpan.class);
|
||||
emojiRequests=new ImageLoaderRequest[emojiSpans.length];
|
||||
int emojiSize=V.dp(20);
|
||||
for(int i=0; i<emojiSpans.length; i++){
|
||||
emojiRequests[i]=new UrlImageLoaderRequest(emojiSpans[i].emoji.url, emojiSize, emojiSize);
|
||||
}
|
||||
LinkSpan[] linkSpans=((Spanned) text).getSpans(0, text.length(), LinkSpan.class);
|
||||
for(LinkSpan span:linkSpans){
|
||||
span.setListener(this);
|
||||
}
|
||||
}else{
|
||||
emojiRequests=new ImageLoaderRequest[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.TEXT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCount(){
|
||||
return emojiRequests.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int index){
|
||||
return emojiRequests[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLinkClick(LinkSpan span){
|
||||
switch(span.getType()){
|
||||
case URL -> UiUtils.launchWebBrowser(parentFragment.getActivity(), span.getLink());
|
||||
case HASHTAG, MENTION -> Toast.makeText(parentFragment.getActivity(), "Not implemented yet", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Holder extends BindableViewHolder<TextStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||
private final LinkedTextView text;
|
||||
private CustomEmojiSpan[] emojiSpans;
|
||||
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_text, parent);
|
||||
text=findViewById(R.id.text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(TextStatusDisplayItem item){
|
||||
text.setText(item.text);
|
||||
if(emojiSpans!=null){
|
||||
for(CustomEmojiSpan span:emojiSpans){
|
||||
span.setDrawable(null);
|
||||
}
|
||||
}
|
||||
if(item.text instanceof Spanned)
|
||||
emojiSpans=((Spanned) item.text).getSpans(0, item.text.length(), CustomEmojiSpan.class);
|
||||
else
|
||||
emojiSpans=new CustomEmojiSpan[0];
|
||||
text.setInvalidateOnEveryFrame(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
emojiSpans[index].setDrawable(image);
|
||||
text.invalidate();
|
||||
if(image instanceof Animatable){
|
||||
((Animatable) image).start();
|
||||
if(image instanceof MovieDrawable)
|
||||
text.setInvalidateOnEveryFrame(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
emojiSpans[index].setDrawable(null);
|
||||
text.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.CornerPathEffect;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SoundEffectConstants;
|
||||
import android.widget.TextView;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ClickableLinksDelegate {
|
||||
|
||||
private Paint hlPaint;
|
||||
private Path hlPath;
|
||||
private LinkSpan selectedSpan;
|
||||
private TextView view;
|
||||
|
||||
public ClickableLinksDelegate(TextView view) {
|
||||
this.view=view;
|
||||
hlPaint=new Paint();
|
||||
hlPaint.setAntiAlias(true);
|
||||
hlPaint.setPathEffect(new CornerPathEffect(V.dp(3)));
|
||||
// view.setHighlightColor(view.getResources().getColor(android.R.color.holo_blue_light));
|
||||
}
|
||||
|
||||
public boolean onTouch(MotionEvent event) {
|
||||
if(event.getAction()==MotionEvent.ACTION_DOWN){
|
||||
int line=-1;
|
||||
Rect rect=new Rect();
|
||||
Layout l=view.getLayout();
|
||||
for(int i=0;i<l.getLineCount();i++){
|
||||
view.getLineBounds(i, rect);
|
||||
if(rect.contains((int)event.getX(), (int)event.getY())){
|
||||
line=i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(line==-1){
|
||||
return false;
|
||||
}
|
||||
CharSequence text=view.getText();
|
||||
if(text instanceof Spanned){
|
||||
Spanned s=(Spanned)text;
|
||||
LinkSpan[] spans=s.getSpans(0, s.length()-1, LinkSpan.class);
|
||||
if(spans.length>0){
|
||||
for(LinkSpan span:spans){
|
||||
int start=s.getSpanStart(span);
|
||||
int end=s.getSpanEnd(span);
|
||||
int lstart=l.getLineForOffset(start);
|
||||
int lend=l.getLineForOffset(end);
|
||||
if(line>=lstart && line<=lend){
|
||||
if(line==lstart && event.getX()-view.getPaddingLeft()<l.getPrimaryHorizontal(start)){
|
||||
continue;
|
||||
}
|
||||
if(line==lend && event.getX()-view.getPaddingLeft()>l.getPrimaryHorizontal(end)){
|
||||
continue;
|
||||
}
|
||||
hlPath=new Path();
|
||||
selectedSpan=span;
|
||||
hlPaint.setColor((span.getColor() & 0x00FFFFFF) | 0x33000000);
|
||||
//l.getSelectionPath(start, end, hlPath);
|
||||
for(int j=lstart;j<=lend;j++){
|
||||
Rect bounds=new Rect();
|
||||
l.getLineBounds(j, bounds);
|
||||
//bounds.left+=view.getPaddingLeft();
|
||||
if(j==lstart){
|
||||
bounds.left=Math.round(l.getPrimaryHorizontal(start));
|
||||
}
|
||||
if(j==lend){
|
||||
bounds.right=Math.round(l.getPrimaryHorizontal(end));
|
||||
}else{
|
||||
CharSequence lineChars=view.getText().subSequence(l.getLineStart(j), l.getLineEnd(j));
|
||||
bounds.right=Math.round(view.getPaint().measureText(lineChars.toString()))/*+view.getPaddingRight()*/;
|
||||
}
|
||||
bounds.inset(V.dp(-2), V.dp(-2));
|
||||
hlPath.addRect(new RectF(bounds), Path.Direction.CW);
|
||||
}
|
||||
hlPath.offset(view.getPaddingLeft(), 0);
|
||||
view.invalidate();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(event.getAction()==MotionEvent.ACTION_UP && selectedSpan!=null){
|
||||
view.playSoundEffect(SoundEffectConstants.CLICK);
|
||||
selectedSpan.onClick(view.getContext());
|
||||
hlPath=null;
|
||||
selectedSpan=null;
|
||||
view.invalidate();
|
||||
return false;
|
||||
}
|
||||
if(event.getAction()==MotionEvent.ACTION_CANCEL){
|
||||
hlPath=null;
|
||||
selectedSpan=null;
|
||||
view.invalidate();
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onDraw(Canvas canvas){
|
||||
if(hlPath!=null){
|
||||
canvas.save();
|
||||
canvas.translate(0, view.getPaddingTop());
|
||||
canvas.drawPath(hlPath, hlPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.style.ReplacementSpan;
|
||||
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class CustomEmojiSpan extends ReplacementSpan{
|
||||
public final Emoji emoji;
|
||||
private Drawable drawable;
|
||||
|
||||
public CustomEmojiSpan(Emoji emoji){
|
||||
this.emoji=emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
|
||||
return Math.round(paint.descent()-paint.ascent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint){
|
||||
int size=Math.round(paint.descent()-paint.ascent());
|
||||
if(drawable==null){
|
||||
canvas.drawRect(x, top, x+size, top+size, paint);
|
||||
}else{
|
||||
// AnimatedImageDrawable doesn't like when its bounds don't start at (0, 0)
|
||||
Rect bounds=drawable.getBounds();
|
||||
int dw=drawable.getIntrinsicWidth();
|
||||
int dh=drawable.getIntrinsicHeight();
|
||||
if(bounds.left!=0 || bounds.top!=0 || bounds.right!=dw || bounds.left!=dh){
|
||||
drawable |