Audio player
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
package org.joinmastodon.android.ui.displayitems;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.SystemClock;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.AudioPlayerService;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.drawables.SeekBarThumbDrawable;
|
||||
|
||||
public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final Attachment attachment;
|
||||
|
||||
public AudioStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, Attachment attachment){
|
||||
super(parentID, parentFragment);
|
||||
this.status=status;
|
||||
this.attachment=attachment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getType(){
|
||||
return Type.AUDIO;
|
||||
}
|
||||
|
||||
public static class Holder extends StatusDisplayItem.Holder<AudioStatusDisplayItem> implements AudioPlayerService.Callback{
|
||||
private final ImageButton playPauseBtn;
|
||||
private final TextView time;
|
||||
private final SeekBar seekBar;
|
||||
|
||||
private int lastKnownPosition;
|
||||
private long lastKnownPositionTime;
|
||||
private boolean playing;
|
||||
private int lastRemainingSeconds=-1;
|
||||
private boolean seekbarBeingDragged;
|
||||
|
||||
private Runnable positionUpdater=this::updatePosition;
|
||||
|
||||
public Holder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.display_item_audio, parent);
|
||||
playPauseBtn=findViewById(R.id.play_pause_btn);
|
||||
time=findViewById(R.id.time);
|
||||
seekBar=findViewById(R.id.seekbar);
|
||||
seekBar.setThumb(new SeekBarThumbDrawable(context));
|
||||
playPauseBtn.setOnClickListener(this::onPlayPauseClick);
|
||||
itemView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener(){
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v){
|
||||
AudioPlayerService.registerCallback(Holder.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v){
|
||||
AudioPlayerService.unregisterCallback(Holder.this);
|
||||
}
|
||||
});
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener(){
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser){
|
||||
if(fromUser){
|
||||
int seconds=(int)(seekBar.getProgress()/10000.0*item.attachment.getDuration());
|
||||
time.setText(formatDuration(seconds));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar){
|
||||
seekbarBeingDragged=true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar){
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
service.seekTo((int)(seekBar.getProgress()/10000.0*item.attachment.getDuration()*1000.0));
|
||||
}
|
||||
seekbarBeingDragged=false;
|
||||
if(playing)
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(AudioStatusDisplayItem item){
|
||||
int seconds=(int)item.attachment.getDuration();
|
||||
String duration=formatDuration(seconds);
|
||||
time.getLayoutParams().width=(int)Math.ceil(time.getPaint().measureText("-"+duration));
|
||||
time.setText(duration);
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
seekBar.setEnabled(true);
|
||||
onPlayStateChanged(item.attachment.id, service.isPlaying(), service.getPosition());
|
||||
}else{
|
||||
seekBar.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void onPlayPauseClick(View v){
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
if(playing)
|
||||
service.pause(true);
|
||||
else
|
||||
service.play();
|
||||
}else{
|
||||
AudioPlayerService.start(v.getContext(), item.status, item.attachment);
|
||||
onPlayStateChanged(item.attachment.id, true, 0);
|
||||
seekBar.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayStateChanged(String attachmentID, boolean playing, int position){
|
||||
if(attachmentID.equals(item.attachment.id)){
|
||||
this.lastKnownPosition=position;
|
||||
lastKnownPositionTime=SystemClock.uptimeMillis();
|
||||
this.playing=playing;
|
||||
playPauseBtn.setImageResource(playing ? R.drawable.ic_fluent_pause_circle_24_filled : R.drawable.ic_fluent_play_circle_24_filled);
|
||||
if(!playing){
|
||||
lastRemainingSeconds=-1;
|
||||
time.setText(formatDuration((int) item.attachment.getDuration()));
|
||||
}else{
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaybackStopped(String attachmentID){
|
||||
if(attachmentID.equals(item.attachment.id)){
|
||||
playing=false;
|
||||
playPauseBtn.setImageResource(R.drawable.ic_fluent_play_circle_24_filled);
|
||||
seekBar.setProgress(0);
|
||||
seekBar.setEnabled(false);
|
||||
time.setText(formatDuration((int)item.attachment.getDuration()));
|
||||
}
|
||||
}
|
||||
|
||||
private String formatDuration(int seconds){
|
||||
if(seconds>=3600)
|
||||
return String.format("%d:%02d:%02d", seconds/3600, seconds%3600/60, seconds%60);
|
||||
else
|
||||
return String.format("%d:%02d", seconds/60, seconds%60);
|
||||
}
|
||||
|
||||
private void updatePosition(){
|
||||
if(!playing || seekbarBeingDragged)
|
||||
return;
|
||||
double pos=lastKnownPosition/1000.0+(SystemClock.uptimeMillis()-lastKnownPositionTime)/1000.0;
|
||||
seekBar.setProgress((int)Math.round(pos/item.attachment.getDuration()*10000.0));
|
||||
itemView.postOnAnimation(positionUpdater);
|
||||
int remainingSeconds=(int)(item.attachment.getDuration()-pos);
|
||||
if(remainingSeconds!=lastRemainingSeconds){
|
||||
lastRemainingSeconds=remainingSeconds;
|
||||
time.setText("-"+formatDuration(remainingSeconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,12 +52,12 @@ public abstract class StatusDisplayItem{
|
||||
case TEXT -> new TextStatusDisplayItem.Holder(activity, parent);
|
||||
case PHOTO -> new PhotoStatusDisplayItem.Holder(activity, parent);
|
||||
case GIFV -> new GifVStatusDisplayItem.Holder(activity, parent);
|
||||
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
|
||||
case VIDEO -> new VideoStatusDisplayItem.Holder(activity, parent);
|
||||
case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent);
|
||||
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
|
||||
case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent);
|
||||
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
|
||||
default -> throw new UnsupportedOperationException();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,6 +94,11 @@ public abstract class StatusDisplayItem{
|
||||
photoIndex++;
|
||||
}
|
||||
}
|
||||
for(Attachment att:statusForContent.mediaAttachments){
|
||||
if(att.type==Attachment.Type.AUDIO){
|
||||
items.add(new AudioStatusDisplayItem(parentID, fragment, statusForContent, att));
|
||||
}
|
||||
}
|
||||
if(statusForContent.poll!=null){
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, items);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.joinmastodon.android.ui.drawables;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SeekBarThumbDrawable extends Drawable{
|
||||
private Bitmap shadow1, shadow2;
|
||||
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private Context context;
|
||||
|
||||
public SeekBarThumbDrawable(Context context){
|
||||
this.context=context;
|
||||
shadow1=Bitmap.createBitmap(V.dp(24), V.dp(24), Bitmap.Config.ALPHA_8);
|
||||
shadow2=Bitmap.createBitmap(V.dp(24), V.dp(24), Bitmap.Config.ALPHA_8);
|
||||
Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.setColor(0xFF000000);
|
||||
paint.setShadowLayer(V.dp(2), 0, V.dp(1), 0xFF000000);
|
||||
new Canvas(shadow1).drawCircle(V.dp(12), V.dp(12), V.dp(9), paint);
|
||||
paint.setShadowLayer(V.dp(3), 0, V.dp(1), 0xFF000000);
|
||||
new Canvas(shadow2).drawCircle(V.dp(12), V.dp(12), V.dp(9), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas){
|
||||
float centerX=getBounds().centerX();
|
||||
float centerY=getBounds().centerY();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(0x4d000000);
|
||||
canvas.drawBitmap(shadow1, centerX-shadow1.getWidth()/2f, centerY-shadow1.getHeight()/2f, paint);
|
||||
paint.setColor(0x26000000);
|
||||
canvas.drawBitmap(shadow2, centerX-shadow2.getWidth()/2f, centerY-shadow2.getHeight()/2f, paint);
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorButtonText));
|
||||
canvas.drawCircle(centerX, centerY, V.dp(7), paint);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorAccentLight));
|
||||
paint.setStrokeWidth(V.dp(4));
|
||||
canvas.drawCircle(centerX, centerY, V.dp(7), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity(){
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicWidth(){
|
||||
return V.dp(24);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntrinsicHeight(){
|
||||
return V.dp(24);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Element;
|
||||
import org.jsoup.nodes.Node;
|
||||
import org.jsoup.nodes.TextNode;
|
||||
import org.jsoup.safety.Cleaner;
|
||||
import org.jsoup.safety.Safelist;
|
||||
import org.jsoup.select.NodeVisitor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -144,4 +146,8 @@ public class HtmlParser{
|
||||
view.setText(parseCustomEmoji(text, emojis));
|
||||
UiUtils.loadCustomEmojiInTextView(view);
|
||||
}
|
||||
|
||||
public static String strip(String html){
|
||||
return Jsoup.clean(html, Safelist.none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -13,7 +12,6 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.Spanned;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -40,7 +38,6 @@ import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import me.grishka.appkit.Nav;
|
||||
@@ -58,6 +55,7 @@ public class UiUtils{
|
||||
public static void launchWebBrowser(Context context, String url){
|
||||
// TODO setting for custom tabs
|
||||
new CustomTabsIntent.Builder()
|
||||
.setShowTitle(true)
|
||||
.build()
|
||||
.launchUrl(context, Uri.parse(url));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user