WIP: headlines list switched to recycler view

known issues: context menu not working
This commit is contained in:
Andrew Dolgov 2017-06-01 17:03:11 +03:00
parent 1ca3a7d681
commit 583578fe8a
6 changed files with 515 additions and 360 deletions

View File

@ -32,6 +32,7 @@ dependencies {
compile 'com.bogdwellers:pinchtozoom:0.1' compile 'com.bogdwellers:pinchtozoom:0.1'
compile 'com.github.bumptech.glide:glide:3.8.0' compile 'com.github.bumptech.glide:glide:3.8.0'
compile 'jp.wasabeef:glide-transformations:2.0.2' compile 'jp.wasabeef:glide-transformations:2.0.2'
compile 'com.android.support:recyclerview-v7:25.3.1'
compile 'com.android.support:cardview-v7:25.3.1' compile 'com.android.support:cardview-v7:25.3.1'
compile 'com.android.support:support-v4:25.3.1' compile 'com.android.support:support-v4:25.3.1'
compile 'com.android.support:appcompat-v7:25.3.1' compile 'com.android.support:appcompat-v7:25.3.1'

View File

@ -9,7 +9,6 @@ import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.res.Resources.Theme; import android.content.res.Resources.Theme;
import android.graphics.Bitmap;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
@ -18,13 +17,15 @@ import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat; import android.support.v4.app.ActivityCompat;
import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Html; import android.text.Html;
import android.transition.Fade; import android.transition.Fade;
import android.transition.Transition; import android.transition.Transition;
@ -43,12 +44,7 @@ import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.ListView; import android.widget.ListView;
@ -63,7 +59,6 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.GlideDrawable; import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.GlideDrawableImageViewTarget;
import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.target.Target;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.shamanland.fab.FloatingActionButton; import com.shamanland.fab.FloatingActionButton;
@ -72,6 +67,7 @@ import com.shamanland.fab.ShowHideOnScroll;
import org.fox.ttrss.types.Article; import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.ArticleList; import org.fox.ttrss.types.ArticleList;
import org.fox.ttrss.types.Feed; import org.fox.ttrss.types.Feed;
import org.fox.ttrss.util.HeaderViewRecyclerAdapter;
import org.fox.ttrss.util.HeadlinesRequest; import org.fox.ttrss.util.HeadlinesRequest;
import java.io.IOException; import java.io.IOException;
@ -85,7 +81,8 @@ import java.util.TimeZone;
import jp.wasabeef.glide.transformations.CropCircleTransformation; import jp.wasabeef.glide.transformations.CropCircleTransformation;
public class HeadlinesFragment extends Fragment implements OnItemClickListener, OnScrollListener { public class HeadlinesFragment extends Fragment {
public enum ArticlesSelection { ALL, NONE, UNREAD } public enum ArticlesSelection { ALL, NONE, UNREAD }
public static final int FLAVOR_IMG_MIN_SIZE = 128; public static final int FLAVOR_IMG_MIN_SIZE = 128;
@ -100,15 +97,13 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
private Article m_activeArticle; private Article m_activeArticle;
private String m_searchQuery = ""; private String m_searchQuery = "";
private boolean m_refreshInProgress = false; private boolean m_refreshInProgress = false;
private boolean m_autoCatchupDisabled = false;
private int m_firstId = 0; private int m_firstId = 0;
private boolean m_lazyLoadDisabled = false; private boolean m_lazyLoadDisabled = false;
private SharedPreferences m_prefs; private SharedPreferences m_prefs;
private ArticleListAdapter m_adapter; private HeaderViewRecyclerAdapter m_adapter;
private ArticleList m_articles = new ArticleList(); //Application.getInstance().m_loadedArticles; private ArticleList m_articles = new ArticleList();
//private ArticleList m_selectedArticles = new ArticleList();
private ArticleList m_readArticles = new ArticleList(); private ArticleList m_readArticles = new ArticleList();
private HeadlinesEventListener m_listener; private HeadlinesEventListener m_listener;
private OnlineActivity m_activity; private OnlineActivity m_activity;
@ -116,8 +111,9 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
private int m_maxImageSize = 0; private int m_maxImageSize = 0;
private boolean m_compactLayoutMode = false; private boolean m_compactLayoutMode = false;
private int m_listPreviousVisibleItem; private int m_listPreviousVisibleItem;
private ListView m_list; private RecyclerView m_list;
//private ImageLoader m_imageLoader = ImageLoader.getInstance(); private LinearLayoutManager m_layoutManager;
private View m_listLoadingView; private View m_listLoadingView;
private View m_topChangedView; private View m_topChangedView;
private View m_amrFooterView; private View m_amrFooterView;
@ -327,7 +323,18 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
} }
}); });
m_list = (ListView) view.findViewById(R.id.headlines_list); m_list = (RecyclerView) view.findViewById(R.id.headlines_list);
m_layoutManager = new LinearLayoutManager(m_activity.getApplicationContext());
m_list.setLayoutManager(m_layoutManager);
m_list.setItemAnimator(new DefaultItemAnimator());
m_list.addItemDecoration(new DividerItemDecoration(m_list.getContext(), m_layoutManager.getOrientation()));
ArticleListAdapter adapter = new ArticleListAdapter(getActivity(), R.layout.headlines_row, m_articles);
m_adapter = new HeaderViewRecyclerAdapter(adapter);
m_list.setAdapter(m_adapter);
FloatingActionButton fab = (FloatingActionButton) view.findViewById(R.id.headlines_fab); FloatingActionButton fab = (FloatingActionButton) view.findViewById(R.id.headlines_fab);
@ -358,23 +365,91 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
m_amrFooterView = inflater.inflate(R.layout.headlines_footer, container, false); m_amrFooterView = inflater.inflate(R.layout.headlines_footer, container, false);
m_amrFooterView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT, screenHeight)); m_amrFooterView.setLayoutParams(new ListView.LayoutParams(ListView.LayoutParams.MATCH_PARENT, screenHeight));
m_list.addFooterView(m_amrFooterView, null, false); m_adapter.addFooterView(m_amrFooterView);
} }
if (m_activity.isSmallScreen()) { if (m_activity.isSmallScreen()) {
View layout = inflater.inflate(R.layout.headlines_heading_spacer, m_list, false); View layout = inflater.inflate(R.layout.headlines_heading_spacer, m_list, false);
m_list.addHeaderView(layout); m_adapter.addHeaderView(layout);
m_swipeLayout.setProgressViewOffset(false, 0, m_swipeLayout.setProgressViewOffset(false, 0,
m_activity.getResources().getDimensionPixelSize(R.dimen.abc_action_bar_default_height_material) + m_activity.getResources().getDimensionPixelSize(R.dimen.abc_action_bar_default_height_material) +
m_activity.getResources().getDimensionPixelSize(R.dimen.abc_action_bar_default_padding_end_material)); m_activity.getResources().getDimensionPixelSize(R.dimen.abc_action_bar_default_padding_end_material) + 5);
} }
m_adapter = new ArticleListAdapter(getActivity(), R.layout.headlines_row, m_articles); m_list.setOnScrollListener(new RecyclerView.OnScrollListener() {
m_list.setAdapter(m_adapter); @Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
m_list.setOnItemClickListener(this); if (newState != RecyclerView.SCROLL_STATE_IDLE) {
m_list.setOnScrollListener(this);
try {
if (m_mediaPlayer != null && m_mediaPlayer.isPlaying()) {
m_mediaPlayer.pause();
}
} catch (IllegalStateException e) {
// i guess it was already released, oh well
}
}
if (newState == RecyclerView.SCROLL_STATE_IDLE && m_prefs.getBoolean("headlines_mark_read_scroll", false)) {
if (!m_readArticles.isEmpty()) {
m_activity.toggleArticlesUnread(m_readArticles);
m_activity.refresh(false);
m_readArticles.clear();
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstVisibleItem = m_layoutManager.findFirstVisibleItemPosition();
int lastVisibleItem = m_layoutManager.findLastVisibleItemPosition();
//Log.d(TAG, "onScrolled: FVI=" + firstVisibleItem + " LVI=" + lastVisibleItem);
if (m_prefs.getBoolean("headlines_mark_read_scroll", false) && firstVisibleItem > m_adapter.getHeaderCount()) {
if (firstVisibleItem <= m_articles.size()) {
Article a = (Article) m_articles.get(firstVisibleItem - m_adapter.getHeaderCount() - 1);
if (a != null && a.unread) {
Log.d(TAG, "title=" + a.title);
a.unread = false;
m_readArticles.add(a);
m_feed.unread--;
}
}
}
if (!m_activity.isTablet()) {
if (m_adapter.getItemCount() > 0) {
if (firstVisibleItem > m_listPreviousVisibleItem) {
m_activity.getSupportActionBar().hide();
} else if (firstVisibleItem < m_listPreviousVisibleItem) {
m_activity.getSupportActionBar().show();
}
} else {
m_activity.getSupportActionBar().show();
}
m_listPreviousVisibleItem = firstVisibleItem;
}
if (!m_refreshInProgress && !m_lazyLoadDisabled && lastVisibleItem >= m_articles.size() - 5) {
refresh(true);
}
}
});
if (!enableSwipeToDismiss) registerForContextMenu(m_list); if (!enableSwipeToDismiss) registerForContextMenu(m_list);
@ -412,38 +487,15 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
m_listener = (HeadlinesEventListener) activity; m_listener = (HeadlinesEventListener) activity;
} }
@Override
public void onItemClick(AdapterView<?> av, View view, int position, long id) {
ListView list = (ListView)av;
Log.d(TAG, "onItemClick=" + position);
if (list != null) {
Article article = (Article)list.getItemAtPosition(position);
// could be footer or w/e
if (article != null && article.id >= 0) {
m_listener.onArticleSelected(article);
// only set active article when it makes sense (in DetailActivity)
if (getActivity() instanceof DetailActivity) {
m_activeArticle = article;
}
m_adapter.notifyDataSetChanged();
}
}
}
public void refresh(boolean append) { public void refresh(boolean append) {
refresh(append, false); refresh(append, false);
} }
@SuppressWarnings({ "serial" }) @SuppressWarnings({ "serial" })
public void refresh(boolean append, boolean userInitiated) { public void refresh(final boolean append, boolean userInitiated) {
m_list.removeFooterView(m_listLoadingView); m_adapter.removeFooterView(m_listLoadingView);
m_list.removeFooterView(m_topChangedView); m_adapter.removeFooterView(m_topChangedView);
m_list.removeFooterView(m_amrFooterView); m_adapter.removeFooterView(m_amrFooterView);
if (!append) m_lazyLoadDisabled = false; if (!append) m_lazyLoadDisabled = false;
@ -452,19 +504,6 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
if (m_swipeLayout != null) m_swipeLayout.setRefreshing(true); if (m_swipeLayout != null) m_swipeLayout.setRefreshing(true);
// new stuff may appear on top, scroll back to show it
if (!append) {
if (getView() != null) {
Log.d(TAG, "scroll hack");
m_autoCatchupDisabled = true;
m_list.setSelection(0);
m_autoCatchupDisabled = false;
m_articles.clear();
m_adapter.notifyDataSetChanged();
}
}
final boolean fappend = append;
final String sessionId = m_activity.getSessionId(); final String sessionId = m_activity.getSessionId();
final boolean isCat = m_feed.is_cat; final boolean isCat = m_feed.is_cat;
@ -482,10 +521,9 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
if (m_swipeLayout != null) m_swipeLayout.setRefreshing(false); if (m_swipeLayout != null) m_swipeLayout.setRefreshing(false);
//m_listLoadingView.setVisibility(View.GONE); m_adapter.removeFooterView(m_listLoadingView);
m_list.removeFooterView(m_listLoadingView); m_adapter.removeFooterView(m_topChangedView);
m_list.removeFooterView(m_topChangedView); m_adapter.removeFooterView(m_amrFooterView);
m_list.removeFooterView(m_amrFooterView);
if (result != null) { if (result != null) {
m_refreshInProgress = false; m_refreshInProgress = false;
@ -497,7 +535,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
if (m_firstIdChanged) { if (m_firstIdChanged) {
m_lazyLoadDisabled = true; m_lazyLoadDisabled = true;
m_list.addFooterView(m_topChangedView, null, false); m_adapter.addFooterView(m_topChangedView);
} }
if (m_amountLoaded < HEADLINES_REQUEST_SIZE) { if (m_amountLoaded < HEADLINES_REQUEST_SIZE) {
@ -507,16 +545,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
HeadlinesFragment.this.m_firstId = m_firstId; HeadlinesFragment.this.m_firstId = m_firstId;
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();
m_listener.onHeadlinesLoaded(fappend); m_listener.onHeadlinesLoaded(append);
// not sure why but listview sometimes gets positioned while ignoring the header so
// top headline content becomes partially obscured by the toolbar on phones
// (not reproducible on avd)
if (!fappend) {
m_list.smoothScrollToPosition(0);
}
//m_listLoadingView.setVisibility(m_amountLoaded == HEADLINES_REQUEST_SIZE ? View.VISIBLE : View.GONE);
} else { } else {
if (m_lastError == ApiCommon.ApiError.LOGIN_FAILED) { if (m_lastError == ApiCommon.ApiError.LOGIN_FAILED) {
@ -531,7 +560,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
} }
} }
if (m_amrFooterView != null) m_list.addFooterView(m_amrFooterView, null, false); if (m_amrFooterView != null) m_adapter.addFooterView(m_amrFooterView);
} }
}; };
@ -562,12 +591,8 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
} }
if (skip > 0) { if (skip > 0) {
m_list.addFooterView(m_listLoadingView, null, false); m_adapter.addFooterView(m_listLoadingView);
//m_listLoadingView.setVisibility(View.VISIBLE);
} }
} else {
//m_activity.setLoadingStatus(R.string.blank, true);
} }
final int fskip = skip; final int fskip = skip;
@ -638,7 +663,10 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
out.putBoolean("lazyLoadDisabled", m_lazyLoadDisabled); out.putBoolean("lazyLoadDisabled", m_lazyLoadDisabled);
} }
static class HeadlineViewHolder { static class HeadlineViewHolder extends RecyclerView.ViewHolder {
public View view;
public Article article;
public TextView titleView; public TextView titleView;
public TextView feedTitleView; public TextView feedTitleView;
public ImageView markedView; public ImageView markedView;
@ -659,11 +687,39 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
public View topChangedMessage; public View topChangedMessage;
public View flavorImageOverflow; public View flavorImageOverflow;
public SurfaceView flavorVideoView; public SurfaceView flavorVideoView;
public int position; //public int position;
public boolean flavorImageEmbedded; public boolean flavorImageEmbedded;
public HeadlineViewHolder(View v) {
super(v);
view = v;
titleView = (TextView)v.findViewById(R.id.title);
feedTitleView = (TextView)v.findViewById(R.id.feed_title);
markedView = (ImageView)v.findViewById(R.id.marked);
publishedView = (ImageView)v.findViewById(R.id.published);
excerptView = (TextView)v.findViewById(R.id.excerpt);
flavorImageView = (ImageView) v.findViewById(R.id.flavor_image);
flavorVideoKindView = (ImageView) v.findViewById(R.id.flavor_video_kind);
authorView = (TextView)v.findViewById(R.id.author);
dateView = (TextView) v.findViewById(R.id.date);
selectionBoxView = (CheckBox) v.findViewById(R.id.selected);
menuButtonView = (ImageView) v.findViewById(R.id.article_menu_button);
flavorImageHolder = (ViewGroup) v.findViewById(R.id.flavorImageHolder);
flavorImageLoadingBar = (ProgressBar) v.findViewById(R.id.flavorImageLoadingBar);
headlineFooter = v.findViewById(R.id.headline_footer);
textImage = (ImageView) v.findViewById(R.id.text_image);
textChecked = (ImageView) v.findViewById(R.id.text_checked);
headlineHeader = v.findViewById(R.id.headline_header);
topChangedMessage = v.findViewById(R.id.headlines_row_top_changed);
flavorImageOverflow = v.findViewById(R.id.flavor_image_overflow);
flavorVideoView = (SurfaceView) v.findViewById(R.id.flavor_video);
}
} }
private class ArticleListAdapter extends ArrayAdapter<Article> { private class ArticleListAdapter extends RecyclerView.Adapter<HeadlineViewHolder> {
private ArrayList<Article> items; private ArrayList<Article> items;
public static final int VIEW_NORMAL = 0; public static final int VIEW_NORMAL = 0;
@ -678,14 +734,14 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
private ColorGenerator m_colorGenerator = ColorGenerator.DEFAULT; private ColorGenerator m_colorGenerator = ColorGenerator.DEFAULT;
private TextDrawable.IBuilder m_drawableBuilder = TextDrawable.builder().round(); private TextDrawable.IBuilder m_drawableBuilder = TextDrawable.builder().round();
//private final DisplayImageOptions displayImageOptions;
boolean showFlavorImage; boolean showFlavorImage;
private int m_minimumHeightToEmbed; private int m_minimumHeightToEmbed;
boolean m_youtubeInstalled; boolean m_youtubeInstalled;
private int m_screenHeight; private int m_screenHeight;
public ArticleListAdapter(Context context, int textViewResourceId, ArrayList<Article> items) { public ArticleListAdapter(Context context, int textViewResourceId, ArrayList<Article> items) {
super(context, textViewResourceId, items); super();
this.items = items; this.items = items;
Display display = m_activity.getWindowManager().getDefaultDisplay(); Display display = m_activity.getWindowManager().getDefaultDisplay();
@ -702,13 +758,6 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
theme.resolveAttribute(R.attr.headlineTitleHighScoreUnreadTextColor, tv, true); theme.resolveAttribute(R.attr.headlineTitleHighScoreUnreadTextColor, tv, true);
titleHighScoreUnreadColor = tv.data; titleHighScoreUnreadColor = tv.data;
/*displayImageOptions = new DisplayImageOptions.Builder()
.cacheInMemory(true)
.resetViewBeforeLoading(true)
.cacheOnDisk(true)
.displayer(new FadeInBitmapDisplayer(500))
.build();*/
List<ApplicationInfo> packages = m_activity.getPackageManager().getInstalledApplications(0); List<ApplicationInfo> packages = m_activity.getPackageManager().getInstalledApplications(0);
for (ApplicationInfo pi : packages) { for (ApplicationInfo pi : packages) {
if (pi.packageName.equals("com.google.android.youtube")) { if (pi.packageName.equals("com.google.android.youtube")) {
@ -718,93 +767,12 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
} }
} }
public int getViewTypeCount() {
return VIEW_COUNT;
}
@Override @Override
public int getItemViewType(int position) { public HeadlineViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Article a = items.get(position);
if (m_activeArticle != null && a.id == m_activeArticle.id && a.unread) {
return VIEW_SELECTED_UNREAD;
} else if (m_activeArticle != null && a.id == m_activeArticle.id) {
return VIEW_SELECTED;
} else if (a.unread) {
return VIEW_UNREAD;
} else {
return VIEW_NORMAL;
}
}
private void updateTextCheckedState(final HeadlineViewHolder holder, final Article article, final int position) {
String tmp = article.title.length() > 0 ? article.title.substring(0, 1).toUpperCase() : "?";
if (article.selected) {
holder.textImage.setImageDrawable(m_drawableBuilder.build(" ", 0xff616161));
//holder.textImage.setTag(null);
holder.textChecked.setVisibility(View.VISIBLE);
} else {
final Drawable textDrawable = m_drawableBuilder.build(tmp, m_colorGenerator.getColor(article.title));
holder.textImage.setImageDrawable(textDrawable);
//holder.textImage.setTag(null);
//holder.textChecked.setVisibility(View.GONE);
if (!showFlavorImage || article.flavorImage == null) {
holder.textImage.setImageDrawable(textDrawable);
//holder.textImage.setTag(null);
} else {
//final GlideDrawableImageViewTarget glideImage = new GlideDrawableImageViewTarget(holder.textImage);
Glide.with(HeadlinesFragment.this)
.load(article.flavorImageUri)
.placeholder(textDrawable)
.bitmapTransform(new CropCircleTransformation(getActivity()))
.dontAnimate()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.skipMemoryCache(false)
.listener(new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
if (resource.getIntrinsicWidth() < THUMB_IMG_MIN_SIZE || resource.getIntrinsicHeight() < THUMB_IMG_MIN_SIZE) {
return true;
} else {
return false;
}
}
})
.into(holder.textImage);
}
holder.textChecked.setVisibility(View.GONE);
}
}
@Override
public View getView(final int position, final View convertView, ViewGroup parent) {
View v = convertView;
final Article article = items.get(position);
final HeadlineViewHolder holder;
int headlineFontSize = Integer.parseInt(m_prefs.getString("headlines_font_size_sp", "13"));
int headlineSmallFontSize = Math.max(10, Math.min(18, headlineFontSize - 2));
if (v == null) {
int layoutId = m_compactLayoutMode ? R.layout.headlines_row_compact : R.layout.headlines_row; int layoutId = m_compactLayoutMode ? R.layout.headlines_row_compact : R.layout.headlines_row;
switch (getItemViewType(position)) { switch (viewType) {
case VIEW_UNREAD: case VIEW_UNREAD:
layoutId = m_compactLayoutMode ? R.layout.headlines_row_unread_compact : R.layout.headlines_row_unread; layoutId = m_compactLayoutMode ? R.layout.headlines_row_unread_compact : R.layout.headlines_row_unread;
break; break;
@ -816,43 +784,43 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
break; break;
} }
LayoutInflater vi = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); View v = LayoutInflater.from(parent.getContext()).inflate(layoutId, parent, false);
v = vi.inflate(layoutId, null);
holder = new HeadlineViewHolder(); return new HeadlineViewHolder(v);
holder.titleView = (TextView)v.findViewById(R.id.title);
holder.feedTitleView = (TextView)v.findViewById(R.id.feed_title);
holder.markedView = (ImageView)v.findViewById(R.id.marked);
holder.publishedView = (ImageView)v.findViewById(R.id.published);
holder.excerptView = (TextView)v.findViewById(R.id.excerpt);
holder.flavorImageView = (ImageView) v.findViewById(R.id.flavor_image);
holder.flavorVideoKindView = (ImageView) v.findViewById(R.id.flavor_video_kind);
holder.authorView = (TextView)v.findViewById(R.id.author);
holder.dateView = (TextView) v.findViewById(R.id.date);
holder.selectionBoxView = (CheckBox) v.findViewById(R.id.selected);
holder.menuButtonView = (ImageView) v.findViewById(R.id.article_menu_button);
holder.flavorImageHolder = (ViewGroup) v.findViewById(R.id.flavorImageHolder);
holder.flavorImageLoadingBar = (ProgressBar) v.findViewById(R.id.flavorImageLoadingBar);
holder.headlineFooter = v.findViewById(R.id.headline_footer);
holder.textImage = (ImageView) v.findViewById(R.id.text_image);
holder.textChecked = (ImageView) v.findViewById(R.id.text_checked);
holder.headlineHeader = v.findViewById(R.id.headline_header);
holder.topChangedMessage = v.findViewById(R.id.headlines_row_top_changed);
holder.flavorImageOverflow = v.findViewById(R.id.flavor_image_overflow);
holder.flavorVideoView = (SurfaceView) v.findViewById(R.id.flavor_video);
v.setTag(holder);
// http://code.google.com/p/android/issues/detail?id=3414
((ViewGroup)v).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
} else {
holder = (HeadlineViewHolder) v.getTag();
} }
//Log.d(TAG, "getView: " + position + ":" + article.title); @Override
public void onBindViewHolder(final HeadlineViewHolder holder, final int position) {
holder.article = items.get(position);
holder.position = position; int headlineFontSize = Integer.parseInt(m_prefs.getString("headlines_font_size_sp", "13"));
int headlineSmallFontSize = Math.max(10, Math.min(18, headlineFontSize - 2));
final Article article = holder.article;
//holder.position = position;
holder.view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
m_activity.openContextMenu(v);
return false;
}
});
holder.view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
m_listener.onArticleSelected(article);
// only set active article when it makes sense (in DetailActivity)
if (getActivity() instanceof DetailActivity) {
m_activeArticle = article;
}
m_adapter.notifyDataSetChanged();
}
});
// block footer clicks to make button/selection clicking easier // block footer clicks to make button/selection clicking easier
if (holder.headlineFooter != null) { if (holder.headlineFooter != null) {
@ -1319,8 +1287,6 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
} }
if (holder.menuButtonView != null) { if (holder.menuButtonView != null) {
//if (m_activity.isDarkTheme())
// ib.setImageResource(R.drawable.ic_mailbox_collapsed_holo_dark);
holder.menuButtonView.setOnClickListener(new OnClickListener() { holder.menuButtonView.setOnClickListener(new OnClickListener() {
@Override @Override
@ -1345,7 +1311,73 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
}); });
} }
return v; }
@Override
public int getItemViewType(int position) {
Article a = items.get(position);
if (m_activeArticle != null && a.id == m_activeArticle.id && a.unread) {
return VIEW_SELECTED_UNREAD;
} else if (m_activeArticle != null && a.id == m_activeArticle.id) {
return VIEW_SELECTED;
} else if (a.unread) {
return VIEW_UNREAD;
} else {
return VIEW_NORMAL;
}
}
@Override
public int getItemCount() {
return items.size();
}
private void updateTextCheckedState(final HeadlineViewHolder holder, final Article article, final int position) {
String tmp = article.title.length() > 0 ? article.title.substring(0, 1).toUpperCase() : "?";
if (article.selected) {
holder.textImage.setImageDrawable(m_drawableBuilder.build(" ", 0xff616161));
//holder.textImage.setTag(null);
holder.textChecked.setVisibility(View.VISIBLE);
} else {
final Drawable textDrawable = m_drawableBuilder.build(tmp, m_colorGenerator.getColor(article.title));
holder.textImage.setImageDrawable(textDrawable);
if (!showFlavorImage || article.flavorImage == null) {
holder.textImage.setImageDrawable(textDrawable);
} else {
Glide.with(HeadlinesFragment.this)
.load(article.flavorImageUri)
.placeholder(textDrawable)
.bitmapTransform(new CropCircleTransformation(getActivity()))
.dontAnimate()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.skipMemoryCache(false)
.listener(new RequestListener<String, GlideDrawable>() {
@Override
public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
if (resource.getIntrinsicWidth() < THUMB_IMG_MIN_SIZE || resource.getIntrinsicHeight() < THUMB_IMG_MIN_SIZE) {
return true;
} else {
return false;
}
}
})
.into(holder.textImage);
}
holder.textChecked.setVisibility(View.GONE);
}
} }
private void openGalleryForType(Article article, HeadlineViewHolder holder, View transitionView) { private void openGalleryForType(Article article, HeadlineViewHolder holder, View transitionView) {
@ -1430,14 +1462,6 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
return px; return px;
} }
private void repositionFlavorVideo(View view, HeadlineViewHolder holder) {
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) view.getLayoutParams();
lp.addRule(RelativeLayout.BELOW, R.id.headline_header);
view.setLayoutParams(lp);
}
private void maybeRepositionFlavorImage(View view, GlideDrawable resource, HeadlineViewHolder holder, boolean forceDown) { private void maybeRepositionFlavorImage(View view, GlideDrawable resource, HeadlineViewHolder holder, boolean forceDown) {
RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) view.getLayoutParams(); RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) view.getLayoutParams();
@ -1523,14 +1547,9 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();
ListView list = (ListView)getView().findViewById(R.id.headlines_list); if (m_list != null) {
if (list != null) {
int position = getArticlePositionById(article.id); int position = getArticlePositionById(article.id);
m_list.smoothScrollToPosition(position);
if (position != -1) {
list.smoothScrollToPosition(position);
}
} }
} }
} }
@ -1553,15 +1572,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
} }
public Article getArticleAtPosition(int position) { public Article getArticleAtPosition(int position) {
try { return m_articles.get(position);
return (Article) m_list.getItemAtPosition(position);
} catch (ClassCastException e) {
return null;
} catch (IndexOutOfBoundsException e) {
return null;
} catch (NullPointerException e) {
return null;
}
} }
public Article getArticleById(int id) { public Article getArticleById(int id) {
@ -1580,74 +1591,14 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
return tmp; return tmp;
} }
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (m_prefs.getBoolean("headlines_mark_read_scroll", false) && firstVisibleItem > (m_activity.isSmallScreen() ? 1 : 0) && !m_autoCatchupDisabled) {
Article a = (Article) view.getItemAtPosition(firstVisibleItem - 1);
if (a != null && a.unread) {
Log.d(TAG, "title=" + a.title);
a.unread = false;
m_readArticles.add(a);
m_feed.unread--;
}
}
if (!m_activity.isTablet()) {
if (m_adapter.getCount() > 0) {
if (firstVisibleItem > m_listPreviousVisibleItem) {
m_activity.getSupportActionBar().hide();
} else if (firstVisibleItem < m_listPreviousVisibleItem) {
m_activity.getSupportActionBar().show();
}
} else {
m_activity.getSupportActionBar().show();
}
m_listPreviousVisibleItem = firstVisibleItem;
}
if (!m_refreshInProgress && !m_lazyLoadDisabled && firstVisibleItem + visibleItemCount == m_articles.size()) {
refresh(true);
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
/*if (m_activeSurface != null) {
m_activeSurface.setVisibility(View.GONE);
}*/
try {
if (m_mediaPlayer != null && m_mediaPlayer.isPlaying()) {
m_mediaPlayer.pause();
}
} catch (IllegalStateException e) {
// i guess it was already released, oh well
}
}
if (scrollState == SCROLL_STATE_IDLE && m_prefs.getBoolean("headlines_mark_read_scroll", false)) {
if (!m_readArticles.isEmpty()) {
m_activity.toggleArticlesUnread(m_readArticles);
m_activity.refresh(false);
m_readArticles.clear();
}
}
}
public Article getActiveArticle() { public Article getActiveArticle() {
return m_activeArticle; return m_activeArticle;
} }
public int getArticlePositionById(int id) { public int getArticlePositionById(int id) {
for (Article a : m_adapter.items) { for (int i = 0; i < m_articles.size(); i++) {
if (a.id == id) { if (m_articles.get(i).id == id) {
return m_adapter.getPosition(a) + m_list.getHeaderViewsCount(); return i + m_adapter.getHeaderCount();
} }
} }

View File

@ -0,0 +1,214 @@
package org.fox.ttrss.util;
/*
* Copyright (C) 2014 darnmason
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* <p>
* RecyclerView adapter designed to wrap an existing adapter allowing the addition of
* header views and footer views.
* </p>
* <p>
* I implemented it to aid with the transition from ListView to RecyclerView where the ListView's
* addHeaderView and addFooterView methods were used. Using this class you may initialize your
* header views in the Fragment/Activity and add them to the adapter in the same way you used to
* add them to a ListView.
* </p>
* <p>
* I also required to be able to swap out multiple adapters with different content, therefore
* setAdapter may be called multiple times.
* </p>
* Created by darnmason on 07/11/2014.
*/
public class HeaderViewRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int HEADERS_START = Integer.MIN_VALUE;
private static final int FOOTERS_START = Integer.MIN_VALUE + 10;
private static final int ITEMS_START = Integer.MIN_VALUE + 20;
private static final int ADAPTER_MAX_TYPES = 100;
private RecyclerView.Adapter mWrappedAdapter;
private List<View> mHeaderViews, mFooterViews;
private Map<Class, Integer> mItemTypesOffset;
/**
* Construct a new header view recycler adapter
* @param adapter The underlying adapter to wrap
*/
public HeaderViewRecyclerAdapter(RecyclerView.Adapter adapter) {
mHeaderViews = new ArrayList<View>();
mFooterViews = new ArrayList<View>();
mItemTypesOffset = new HashMap<Class, Integer>();
setWrappedAdapter(adapter);
}
/**
* Replaces the underlying adapter, notifying RecyclerView of changes
* @param adapter The new adapter to wrap
*/
public void setAdapter(RecyclerView.Adapter adapter) {
if(mWrappedAdapter != null && mWrappedAdapter.getItemCount() > 0) {
notifyItemRangeRemoved(getHeaderCount(), mWrappedAdapter.getItemCount());
}
setWrappedAdapter(adapter);
notifyItemRangeInserted(getHeaderCount(), mWrappedAdapter.getItemCount());
}
@Override
public int getItemViewType(int position) {
int hCount = getHeaderCount();
if (position < hCount) return HEADERS_START + position;
else {
int itemCount = mWrappedAdapter.getItemCount();
if (position < hCount + itemCount) {
return getAdapterTypeOffset() + mWrappedAdapter.getItemViewType(position - hCount);
}
else return FOOTERS_START + position - hCount - itemCount;
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
if (viewType < HEADERS_START + getHeaderCount())
return new StaticViewHolder(mHeaderViews.get(viewType - HEADERS_START));
else if (viewType < FOOTERS_START + getFooterCount())
return new StaticViewHolder(mFooterViews.get(viewType - FOOTERS_START));
else {
return mWrappedAdapter.onCreateViewHolder(viewGroup, viewType - getAdapterTypeOffset());
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
int hCount = getHeaderCount();
if (position >= hCount && position < hCount + mWrappedAdapter.getItemCount())
mWrappedAdapter.onBindViewHolder(viewHolder, position - hCount);
}
/**
* Add a static view to appear at the start of the RecyclerView. Headers are displayed in the
* order they were added.
* @param view The header view to add
*/
public void addHeaderView(View view) {
mHeaderViews.add(view);
}
/**
* Add a static view to appear at the end of the RecyclerView. Footers are displayed in the
* order they were added.
* @param view The footer view to add
*/
public void addFooterView(View view) {
mFooterViews.add(view);
}
public void removeFooterView(View view) {
mFooterViews.remove(view);
}
@Override
public int getItemCount() {
return getHeaderCount() + getFooterCount() + getWrappedItemCount();
}
/**
* @return The item count in the underlying adapter
*/
public int getWrappedItemCount() {
return mWrappedAdapter.getItemCount();
}
/**
* @return The number of header views added
*/
public int getHeaderCount() {
return mHeaderViews.size();
}
/**
* @return The number of footer views added
*/
public int getFooterCount() {
return mFooterViews.size();
}
private void setWrappedAdapter(RecyclerView.Adapter adapter) {
if (mWrappedAdapter != null) mWrappedAdapter.unregisterAdapterDataObserver(mDataObserver);
mWrappedAdapter = adapter;
Class adapterClass = mWrappedAdapter.getClass();
if(!mItemTypesOffset.containsKey(adapterClass)) putAdapterTypeOffset(adapterClass);
mWrappedAdapter.registerAdapterDataObserver(mDataObserver);
}
private void putAdapterTypeOffset(Class adapterClass) {
mItemTypesOffset.put(adapterClass, ITEMS_START + mItemTypesOffset.size() * ADAPTER_MAX_TYPES);
}
private int getAdapterTypeOffset() {
return mItemTypesOffset.get(mWrappedAdapter.getClass());
}
private RecyclerView.AdapterDataObserver mDataObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
super.onChanged();
notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
super.onItemRangeChanged(positionStart, itemCount);
notifyItemRangeChanged(positionStart + getHeaderCount(), itemCount);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
notifyItemRangeInserted(positionStart + getHeaderCount(), itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
super.onItemRangeRemoved(positionStart, itemCount);
notifyItemRangeRemoved(positionStart + getHeaderCount(), itemCount);
}
@Override
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
super.onItemRangeMoved(fromPosition, toPosition, itemCount);
int hCount = getHeaderCount();
// TODO: No notifyItemRangeMoved method?
notifyItemRangeChanged(fromPosition + hCount, toPosition + hCount + itemCount);
}
};
private static class StaticViewHolder extends RecyclerView.ViewHolder {
public StaticViewHolder(View itemView) {
super(itemView);
}
}
}

View File

@ -11,24 +11,13 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" > android:layout_height="match_parent" >
<ListView <android.support.v7.widget.RecyclerView
android:id="@+id/headlines_list" android:id="@+id/headlines_list"
android:drawSelectorOnTop="true" android:drawSelectorOnTop="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
</android.support.v4.widget.SwipeRefreshLayout> </android.support.v4.widget.SwipeRefreshLayout>
<!-- <TextView
android:id="@+id/no_headlines"
android:clickable="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/no_headlines"
android:textAppearance="?android:attr/textAppearanceLarge"
android:visibility="invisible" >
</TextView> -->
<com.shamanland.fab.FloatingActionButton <com.shamanland.fab.FloatingActionButton
android:id="@+id/headlines_fab" android:id="@+id/headlines_fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/headlines_row" android:id="@+id/headlines_row"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
tools:ignore="HardcodedText"> tools:ignore="HardcodedText">
<TableLayout <TableLayout

View File

@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/headlines_row" android:id="@+id/headlines_row"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:background="?headlineUnreadBackground" android:background="?headlineUnreadBackground"
tools:ignore="HardcodedText"> tools:ignore="HardcodedText">