added media previews to timeline and media viewers

This commit is contained in:
Vavassor 2017-01-10 01:14:27 -05:00
parent acbd5acb20
commit e551de7521
13 changed files with 328 additions and 37 deletions

View file

@ -30,6 +30,7 @@
<activity <activity
android:name=".ComposeActivity" android:name=".ComposeActivity"
android:windowSoftInputMode="stateVisible|adjustResize" /> android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".ViewVideoActivity" />
</application> </application>
</manifest> </manifest>

View file

@ -1,9 +1,13 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.provider.MediaStore;
import android.text.Html; import android.text.Html;
import android.text.Spanned; import android.text.Spanned;
import com.android.volley.toolbox.NetworkImageView;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@ -38,6 +42,9 @@ public class Status {
/** whether the authenticated user has favourited this status */ /** whether the authenticated user has favourited this status */
private boolean favourited; private boolean favourited;
private Visibility visibility; private Visibility visibility;
private MediaAttachment[] attachments = null;
public static final int MAX_MEDIA_ATTACHMENTS = 4;
public Status(String id, String accountId, String displayName, String username, Spanned content, public Status(String id, String accountId, String displayName, String username, Spanned content,
String avatar, Date createdAt, boolean reblogged, boolean favourited, String avatar, Date createdAt, boolean reblogged, boolean favourited,
@ -52,6 +59,7 @@ public class Status {
this.reblogged = reblogged; this.reblogged = reblogged;
this.favourited = favourited; this.favourited = favourited;
this.visibility = Visibility.valueOf(visibility.toUpperCase()); this.visibility = Visibility.valueOf(visibility.toUpperCase());
this.attachments = new MediaAttachment[0];
} }
public String getId() { public String getId() {
@ -98,6 +106,10 @@ public class Status {
return visibility; return visibility;
} }
public MediaAttachment[] getAttachments() {
return attachments;
}
public void setRebloggedByUsername(String name) { public void setRebloggedByUsername(String name) {
rebloggedByUsername = name; rebloggedByUsername = name;
} }
@ -110,6 +122,10 @@ public class Status {
this.favourited = favourited; this.favourited = favourited;
} }
public void setAttachments(MediaAttachment[] attachments) {
this.attachments = attachments;
}
@Override @Override
public int hashCode() { public int hashCode() {
return id.hashCode(); return id.hashCode();
@ -173,6 +189,21 @@ public class Status {
String username = account.getString("acct"); String username = account.getString("acct");
String avatar = account.getString("avatar"); String avatar = account.getString("avatar");
JSONArray mediaAttachments = object.getJSONArray("media_attachments");
MediaAttachment[] attachments = null;
if (mediaAttachments != null) {
int n = mediaAttachments.length();
attachments = new MediaAttachment[n];
for (int i = 0; i < n; i++) {
JSONObject attachment = mediaAttachments.getJSONObject(i);
String url = attachment.getString("url");
String previewUrl = attachment.getString("preview_url");
String type = attachment.getString("type");
attachments[i] = new MediaAttachment(url, previewUrl,
MediaAttachment.Type.valueOf(type.toUpperCase()));
}
}
Status reblog = null; Status reblog = null;
/* This case shouldn't be hit after the first recursion at all. But if this method is /* This case shouldn't be hit after the first recursion at all. But if this method is
* passed unusual data this check will prevent extra recursion */ * passed unusual data this check will prevent extra recursion */
@ -193,6 +224,9 @@ public class Status {
id, accountId, displayName, username, contentPlus, avatar, createdAt, id, accountId, displayName, username, contentPlus, avatar, createdAt,
reblogged, favourited, visibility); reblogged, favourited, visibility);
} }
if (attachments != null) {
status.setAttachments(attachments);
}
return status; return status;
} }
@ -204,4 +238,33 @@ public class Status {
} }
return statuses; return statuses;
} }
public static class MediaAttachment {
enum Type {
IMAGE,
VIDEO,
}
private String url;
private String previewUrl;
private Type type;
public MediaAttachment(String url, String previewUrl, Type type) {
this.url = url;
this.previewUrl = previewUrl;
this.type = type;
}
public String getUrl() {
return url;
}
public String getPreviewUrl() {
return previewUrl;
}
public Type getType() {
return type;
}
}
} }

View file

@ -6,4 +6,5 @@ public interface StatusActionListener {
void onReblog(final boolean reblog, final int position); void onReblog(final boolean reblog, final int position);
void onFavourite(final boolean favourite, final int position); void onFavourite(final boolean favourite, final int position);
void onMore(View view, final int position); void onMore(View view, final int position);
void onViewMedia(String url, Status.MediaAttachment.Type type);
} }

View file

@ -2,8 +2,10 @@ package com.keylesspalace.tusky;
import android.content.Context; import android.content.Context;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v7.widget.PagerSnapHelper;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.Spanned; import android.text.Spanned;
import android.text.style.ImageSpan;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -53,6 +55,7 @@ public class TimelineAdapter extends RecyclerView.Adapter {
} else { } else {
holder.setRebloggedByUsername(rebloggedByUsername); holder.setRebloggedByUsername(rebloggedByUsername);
} }
holder.setMediaPreviews(status.getAttachments(), listener);
holder.setupButtons(listener, position); holder.setupButtons(listener, position);
if (status.getVisibility() == Status.Visibility.PRIVATE) { if (status.getVisibility() == Status.Visibility.PRIVATE) {
holder.disableReblogging(); holder.disableReblogging();
@ -112,6 +115,11 @@ public class TimelineAdapter extends RecyclerView.Adapter {
private ImageButton moreButton; private ImageButton moreButton;
private boolean favourited; private boolean favourited;
private boolean reblogged; private boolean reblogged;
private NetworkImageView mediaPreview0;
private NetworkImageView mediaPreview1;
private NetworkImageView mediaPreview2;
private NetworkImageView mediaPreview3;
private String[] mediaAttachmentUrls;
public ViewHolder(View itemView) { public ViewHolder(View itemView) {
super(itemView); super(itemView);
@ -128,6 +136,10 @@ public class TimelineAdapter extends RecyclerView.Adapter {
moreButton = (ImageButton) itemView.findViewById(R.id.status_more); moreButton = (ImageButton) itemView.findViewById(R.id.status_more);
reblogged = false; reblogged = false;
favourited = false; favourited = false;
mediaPreview0 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_0);
mediaPreview1 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_1);
mediaPreview2 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_2);
mediaPreview3 = (NetworkImageView) itemView.findViewById(R.id.status_media_preview_3);
} }
public void setDisplayName(String name) { public void setDisplayName(String name) {
@ -234,6 +246,37 @@ public class TimelineAdapter extends RecyclerView.Adapter {
} }
} }
public void setMediaPreviews(final Status.MediaAttachment[] attachments,
final StatusActionListener listener) {
final NetworkImageView[] previews = {
mediaPreview0,
mediaPreview1,
mediaPreview2,
mediaPreview3
};
Context context = mediaPreview0.getContext();
ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader();
int n = Math.min(attachments.length, Status.MAX_MEDIA_ATTACHMENTS);
for (int i = 0; i < n; i++) {
String previewUrl = attachments[i].getPreviewUrl();
previews[i].setImageUrl(previewUrl, imageLoader);
previews[i].setVisibility(View.VISIBLE);
final String url = attachments[i].getUrl();
final Status.MediaAttachment.Type type = attachments[i].getType();
previews[i].setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onViewMedia(url, type);
}
});
}
// Hide any of the placeholder previews beyond the ones set.
for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) {
previews[i].setImageUrl(null, imageLoader);
previews[i].setVisibility(View.GONE);
}
}
public void setupButtons(final StatusActionListener listener, final int position) { public void setupButtons(final StatusActionListener listener, final int position) {
reblogButton.setOnClickListener(new View.OnClickListener() { reblogButton.setOnClickListener(new View.OnClickListener() {
@Override @Override

View file

@ -1,11 +1,13 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.DividerItemDecoration;
@ -184,7 +186,8 @@ public class TimelineFragment extends Fragment implements
} }
public void onFetchTimelineFailure(Exception exception) { public void onFetchTimelineFailure(Exception exception) {
Toast.makeText(getContext(), R.string.error_fetching_timeline, Toast.LENGTH_SHORT).show(); Toast.makeText(getContext(), R.string.error_fetching_timeline, Toast.LENGTH_SHORT)
.show();
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
} }
@ -312,4 +315,24 @@ public class TimelineFragment extends Fragment implements
}); });
popup.show(); popup.show();
} }
public void onViewMedia(String url, Status.MediaAttachment.Type type) {
switch (type) {
case IMAGE: {
Fragment newFragment = ViewMediaFragment.newInstance(url);
FragmentManager manager = getFragmentManager();
manager.beginTransaction()
.add(R.id.overlay_fragment_container, newFragment)
.addToBackStack(null)
.commit();
break;
}
case VIDEO: {
Intent intent = new Intent(getContext(), ViewVideoActivity.class);
intent.putExtra("url", url);
startActivity(intent);
break;
}
}
}
} }

View file

@ -0,0 +1,45 @@
package com.keylesspalace.tusky;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.android.volley.toolbox.ImageLoader;
import com.android.volley.toolbox.NetworkImageView;
public class ViewMediaFragment extends Fragment {
public static ViewMediaFragment newInstance(String url) {
Bundle arguments = new Bundle();
ViewMediaFragment fragment = new ViewMediaFragment();
arguments.putString("url", url);
fragment.setArguments(arguments);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, final ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_view_media, container, false);
Bundle arguments = getArguments();
String url = arguments.getString("url");
NetworkImageView image = (NetworkImageView) rootView.findViewById(R.id.view_media_image);
ImageLoader imageLoader = VolleySingleton.getInstance(getContext()).getImageLoader();
image.setImageUrl(url, imageLoader);
rootView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
return rootView;
}
private void dismiss() {
getFragmentManager().popBackStack();
}
}

View file

@ -0,0 +1,21 @@
package com.keylesspalace.tusky;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.MediaController;
import android.widget.VideoView;
public class ViewVideoActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_video);
String url = getIntent().getStringExtra("url");
VideoView videoView = (VideoView) findViewById(R.id.video_player);
videoView.setVideoPath(url);
MediaController controller = new MediaController(this);
videoView.setMediaController(controller);
controller.show();
videoView.start();
}
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main" android:id="@+id/activity_main"
@ -9,44 +9,57 @@
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.keylesspalace.tusky.MainActivity" tools:context="com.keylesspalace.tusky.MainActivity">
android:orientation="vertical">
<android.support.v7.widget.Toolbar <LinearLayout
android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="match_parent"
android:background="?attr/colorPrimary" android:orientation="vertical">
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<android.support.v4.view.ViewPager <android.support.v7.widget.Toolbar
android:id="@+id/pager" android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:elevation="4dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<android.support.v4.view.ViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_home" />
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_notifications" />
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_public" />
</android.support.design.widget.TabLayout>
</android.support.v4.view.ViewPager>
</LinearLayout>
<FrameLayout
android:id="@+id/overlay_fragment_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<android.support.design.widget.TabLayout </FrameLayout>
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TabItem </RelativeLayout>
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_home" />
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_notifications" />
<android.support.design.widget.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_public" />
</android.support.design.widget.TabLayout>
</android.support.v4.view.ViewPager>
</LinearLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/view_video_background">
<VideoView
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true" />
</RelativeLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#60000000">
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/view_media_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerInParent="true"
android:scaleType="fitCenter" />
</RelativeLayout>

View file

@ -76,11 +76,62 @@
android:layout_toEndOf="@+id/status_avatar" android:layout_toEndOf="@+id/status_avatar"
android:layout_below="@+id/status_name_bar" /> android:layout_below="@+id/status_name_bar" />
<LinearLayout
android:id="@+id/status_media_preview_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_below="@+id/status_content"
android:layout_toRightOf="@+id/status_avatar">
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/status_media_preview_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:scaleType="centerCrop"
android:layout_marginTop="@dimen/status_media_preview_top_margin" />
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/status_media_preview_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:scaleType="centerCrop"
android:layout_marginTop="@dimen/status_media_preview_top_margin" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/status_media_preview_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1" />
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/status_media_preview_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:scaleType="centerCrop" />
</LinearLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:orientation="horizontal" android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/status_content" android:layout_below="@id/status_media_preview_container"
android:layout_toRightOf="@+id/status_avatar" android:layout_toRightOf="@+id/status_avatar"
android:paddingBottom="8dp" android:paddingBottom="8dp"
android:paddingTop="8dp"> android:paddingTop="8dp">

View file

@ -4,4 +4,5 @@
<color name="colorPrimaryDark">#303F9F</color> <color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color> <color name="colorAccent">#FF4081</color>
<color name="gray">#4F4F4F</color> <color name="gray">#4F4F4F</color>
<color name="view_video_background">#000000</color>
</resources> </resources>

View file

@ -5,4 +5,5 @@
<dimen name="status_since_created_left_margin">4dp</dimen> <dimen name="status_since_created_left_margin">4dp</dimen>
<dimen name="status_avatar_padding">8dp</dimen> <dimen name="status_avatar_padding">8dp</dimen>
<dimen name="status_boost_icon_vertical_padding">5dp</dimen> <dimen name="status_boost_icon_vertical_padding">5dp</dimen>
<dimen name="status_media_preview_top_margin">4dp</dimen>
</resources> </resources>