Java tutorial
/* * Copyright (c) 2015 Jonas Kalderstam. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.nononsenseapps.feeder.ui; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.Cursor; import android.graphics.Point; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.ActionBar; import android.support.v7.util.SortedList; import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.style.ForegroundColorSpan; import android.util.Log; import android.util.TypedValue; import android.view.ActionMode; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.CheckedTextView; import android.widget.ImageView; import android.widget.TextView; import com.bumptech.glide.Glide; import com.nononsenseapps.feeder.R; import com.nononsenseapps.feeder.db.FeedItemSQL; import com.nononsenseapps.feeder.db.FeedSQL; import com.nononsenseapps.feeder.db.RssContentProvider; import com.nononsenseapps.feeder.db.RssDatabaseService; import com.nononsenseapps.feeder.db.Util; import com.nononsenseapps.feeder.model.RssSyncAdapter; import com.nononsenseapps.feeder.util.FeedItemDeltaCursorLoader; import com.nononsenseapps.feeder.util.PrefUtils; import com.nononsenseapps.feeder.util.TabletUtils; import org.joda.time.DateTimeZone; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.Locale; public class FeedFragment extends Fragment implements LoaderManager.LoaderCallbacks { // TODO change format possibly static final DateTimeFormatter shortDateTimeFormat = DateTimeFormat.mediumDate() .withLocale(Locale.getDefault()); private static final int FEEDITEMS_LOADER = 1; private static final int FEED_LOADER = 2; private static final int FEED_SETTINGS_LOADER = 3; private static final String ARG_FEED_ID = "feed_id"; private static final String ARG_FEED_TITLE = "feed_title"; private static final String ARG_FEED_URL = "feed_url"; private static final String ARG_FEED_TAG = "feed_tag"; // Filter for database loader private static final String ONLY_UNREAD = FeedItemSQL.COL_UNREAD + " IS 1 "; private static final String AND_UNREAD = " AND " + ONLY_UNREAD; private static final String TAG = "FeedFragment"; private FeedAdapter mAdapter; private RecyclerView mRecyclerView; private SwipeRefreshLayout mSwipeRefreshLayout; private View mEmptyView; private View mEmptyAddFeed; private View mEmptyOpenFeeds; private final BroadcastReceiver mSyncReceiver; private long id = -1; private String title = ""; private String url = ""; private String tag = ""; private String customTitle = ""; private LinearLayoutManager mLayoutManager; private View mCheckAllButton; private int notify = 0; private CheckedTextView mNotifyCheck; private ActionMode mActionMode = null; private FeedItemSQL mSelectedItem = null; private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { // Called when the action mode is created; startActionMode() was called @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { // Inflate a menu resource providing context menu items MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.contextmenu_feedfragment, menu); // Show/Hide enclosure menu.findItem(R.id.action_open_enclosure).setVisible(mSelectedItem.enclosurelink != null); // Add filename to tooltip if (mSelectedItem.enclosurelink != null) { String filename = mSelectedItem.getEnclosureFilename(); if (filename != null) { menu.findItem(R.id.action_open_enclosure).setTitle(filename); } } return true; } // Called each time the action mode is shown. Always called after onCreateActionMode, but // may be called multiple times if the mode is invalidated. @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { return false; // Return false if nothing is done } // Called when the user selects a contextual menu item @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case R.id.action_open_in_browser: // Open in browser startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(mSelectedItem.link))); mode.finish(); // Action picked, so close the CAB return true; case R.id.action_open_enclosure: // Open enclosure link startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(mSelectedItem.enclosurelink))); mode.finish(); // Action picked, so close the CAB return true; case R.id.action_toggle_unread: // if (mSelectedItem.isUnread()) { RssDatabaseService.markItemAsRead(getActivity(), mSelectedItem.id); } else { RssDatabaseService.markItemAsUnread(getActivity(), mSelectedItem.id); } mode.finish(); // Action picked, so close the CAB return true; default: return false; } } // Called when the user exits the action mode @Override public void onDestroyActionMode(ActionMode mode) { mActionMode = null; mSelectedItem = null; } }; public FeedFragment() { // Listens on sync broadcasts mSyncReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (RssSyncAdapter.SYNC_BROADCAST.equals(intent.getAction())) { onSyncBroadcast(intent.getBooleanExtra(RssSyncAdapter.SYNC_BROADCAST_IS_ACTIVE, false)); } } }; } /** * Returns a new instance of this fragment */ public static FeedFragment newInstance(long id, String title, String url, String tag) { FeedFragment fragment = new FeedFragment(); Bundle args = new Bundle(); args.putLong(ARG_FEED_ID, id); args.putString(ARG_FEED_TITLE, title); args.putString(ARG_FEED_URL, url); args.putString(ARG_FEED_TAG, tag); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { id = getArguments().getLong(ARG_FEED_ID, -1); title = getArguments().getString(ARG_FEED_TITLE); url = getArguments().getString(ARG_FEED_URL); tag = getArguments().getString(ARG_FEED_TAG); // It's a tag, use as title if (id < 1) { title = tag; } // Special tag if (id < 1 && (title == null || title.isEmpty())) { title = getString(R.string.no_tag); } } setHasOptionsMenu(true); // Load some RSS getLoaderManager().restartLoader(FEEDITEMS_LOADER, Bundle.EMPTY, this); // Load feed itself if missing info if (id > 0 && (title == null || title.isEmpty())) { getLoaderManager().restartLoader(FEED_LOADER, Bundle.EMPTY, this); } else { // Get notification settings at least getLoaderManager().restartLoader(FEED_SETTINGS_LOADER, Bundle.EMPTY, this); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_feed, container, false); mRecyclerView = (RecyclerView) rootView.findViewById(android.R.id.list); // improve performance if you know that changes in content // do not change the size of the RecyclerView mRecyclerView.setHasFixedSize(true); if (TabletUtils.isTablet(getActivity())) { final int cols = TabletUtils.numberOfFeedColumns(getActivity()); // use a grid layout mLayoutManager = new GridLayoutManager(getActivity(), cols); // I want the padding header to span the entire width ((GridLayoutManager) mLayoutManager).setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { if (FeedAdapter.HEADERTYPE == mAdapter.getItemViewType(position)) { return cols; } else { return 1; } } }); // TODO, use better dividers such as simple padding // I want some dividers mRecyclerView.addItemDecoration(new DividerColor(getActivity(), DividerColor.VERTICAL_LIST, 0, cols)); // I want some dividers mRecyclerView.addItemDecoration(new DividerColor(getActivity(), DividerColor.HORIZONTAL_LIST)); } else { // use a linear layout manager mLayoutManager = new LinearLayoutManager(getActivity()); // I want some dividers // mRecyclerView.addItemDecoration(new DividerColor // (getActivity(), DividerColor.VERTICAL_LIST, 0, 1)); } mRecyclerView.setLayoutManager(mLayoutManager); // Setup swipe refresh mSwipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swiperefresh); // Set the offset so it comes out of the correct place final int toolbarHeight = getResources().getDimensionPixelOffset(R.dimen.toolbar_height); final int totalToolbarHeight = getResources().getDimensionPixelOffset(R.dimen.total_toolbar_height); mSwipeRefreshLayout.setProgressViewOffset(false, toolbarHeight, Math.round(1.5f * totalToolbarHeight)); // The arrow will cycle between these colors (in order) mSwipeRefreshLayout.setColorSchemeResources(R.color.refresh_progress_1, R.color.refresh_progress_2, R.color.refresh_progress_3); mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { // Sync this specific feed(s) if (id > 0) { RssContentProvider.RequestSync(id); } else if (tag != null) { RssContentProvider.RequestSync(tag); } else { RssContentProvider.RequestSync(); } } }); // Set up the empty view mEmptyView = rootView.findViewById(android.R.id.empty); mEmptyAddFeed = mEmptyView.findViewById(R.id.empty_add_feed); ((TextView) mEmptyAddFeed).setText(android.text.Html.fromHtml(getString(R.string.empty_feed_add))); mEmptyOpenFeeds = mEmptyView.findViewById(R.id.empty_open_feeds); ((TextView) mEmptyOpenFeeds).setText(android.text.Html.fromHtml(getString(R.string.empty_feed_open))); mEmptyAddFeed.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { startActivity(new Intent(getActivity(), EditFeedActivity.class)); } }); mEmptyOpenFeeds.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { ((BaseActivity) getActivity()).openNavDrawer(); } }); // specify an adapter mAdapter = new FeedAdapter(getActivity()); mRecyclerView.setAdapter(mAdapter); // check all button mCheckAllButton = rootView.findViewById(R.id.checkall_button); mCheckAllButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { markAsRead(); } }); // So is toolbar buttons mNotifyCheck = (CheckedTextView) getActivity().findViewById(R.id.notifycheck); mNotifyCheck.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // Remember that we are switching to opposite notify = mNotifyCheck.isChecked() ? 0 : 1; mNotifyCheck.setChecked(notify == 1); setNotifications(notify == 1); } }); return rootView; } private void onSyncBroadcast(boolean syncing) { // Background syncs trigger the sync layout if (mSwipeRefreshLayout.isRefreshing() != syncing) { mSwipeRefreshLayout.setRefreshing(syncing); } } @Override public void onActivityCreated(Bundle bundle) { super.onActivityCreated(bundle); ActionBar ab = ((BaseActivity) getActivity()).getSupportActionBar(); if (ab != null) { ab.setTitle(title); } ((BaseActivity) getActivity()).enableActionBarAutoHide(mRecyclerView); } @Override public void onResume() { super.onResume(); // List might be shorter than screen once item has been read ((BaseActivity) getActivity()).showActionBar(); // Listen on broadcasts LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mSyncReceiver, new IntentFilter(RssSyncAdapter.SYNC_BROADCAST)); } @Override public void onPause() { // Unregister receiver LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mSyncReceiver); mSwipeRefreshLayout.setRefreshing(false); super.onPause(); } @Override public void onDetach() { super.onDetach(); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.feed_fragment, menu); if (id < 1) { menu.findItem(R.id.action_edit_feed).setVisible(false); menu.findItem(R.id.action_delete_feed).setVisible(false); menu.findItem(R.id.action_add_templated).setVisible(false); } // Set toggleable state MenuItem menuItem = menu.findItem(R.id.action_only_unread); final boolean onlyUnread = PrefUtils.isShowOnlyUnread(getActivity()); menuItem.setChecked(onlyUnread); menuItem.setTitle(onlyUnread ? R.string.show_unread_items : R.string.show_all_items); if (onlyUnread) { menuItem.setIcon(R.drawable.ic_action_visibility_off); } else { menuItem.setIcon(R.drawable.ic_action_visibility); } // Don't forget super call here super.onCreateOptionsMenu(menu, inflater); } private void setNotifications(boolean on) { RssDatabaseService.setNotify(getActivity(), on, this.id, this.tag); } private void markAsRead() { RssDatabaseService.markFeedAsRead(getActivity(), this.id, this.tag); } @Override public boolean onOptionsItemSelected(MenuItem menuItem) { final long id = menuItem.getItemId(); if (id == R.id.action_sync) { // Sync all feeds when menu button pressed RssContentProvider.RequestSync(); return true; } else if (id == R.id.action_edit_feed && this.id > 0) { Intent i = new Intent(getActivity(), EditFeedActivity.class); // TODO do not animate the back movement here i.putExtra(EditFeedActivity.SHOULD_FINISH_BACK, true); i.putExtra(EditFeedActivity._ID, this.id); i.putExtra(EditFeedActivity.TITLE, customTitle); i.putExtra(EditFeedActivity.TAG, tag); i.setData(Uri.parse(url)); startActivity(i); return true; } else if (id == R.id.action_add_templated && this.id > 0) { Intent i = new Intent(getActivity(), EditFeedActivity.class); // TODO do not animate the back movement here i.putExtra(EditFeedActivity.SHOULD_FINISH_BACK, true); i.putExtra(EditFeedActivity.TEMPLATE, true); //i.putExtra(EditFeedActivity.TITLE, title); i.putExtra(EditFeedActivity.TAG, tag); i.setData(Uri.parse(url)); startActivity(i); return true; } else if (id == R.id.action_delete_feed && this.id > 0) { getActivity().getContentResolver().delete(FeedSQL.URI_FEEDS, Util.WHEREIDIS, Util.LongsToStringArray(this.id)); RssContentProvider.notifyAllUris(getActivity()); // Tell activity to open another fragment ((FeedActivity) getActivity()).loadFirstFeedInDB(true); return true; } // else if (id == R.id.action_mark_as_read) { // markAsRead(); // return true; // } else if (id == R.id.action_only_unread) { final boolean onlyUnread = !menuItem.isChecked(); PrefUtils.setPrefShowOnlyUnread(getActivity(), onlyUnread); menuItem.setChecked(onlyUnread); if (onlyUnread) { menuItem.setIcon(R.drawable.ic_action_visibility_off); } else { menuItem.setIcon(R.drawable.ic_action_visibility); } menuItem.setTitle(onlyUnread ? R.string.show_unread_items : R.string.show_all_items); //getActivity().invalidateOptionsMenu(); // Restart loader getLoaderManager().restartLoader(FEEDITEMS_LOADER, new Bundle(), this); return true; } else { return super.onOptionsItemSelected(menuItem); } } /** * @return SQL selection */ protected String getLoaderSelection() { String filter = null; if (id > 0) { filter = FeedItemSQL.COL_FEED + " IS ? "; } else if (tag != null) { filter = FeedItemSQL.COL_TAG + " IS ? "; } final boolean onlyUnread = PrefUtils.isShowOnlyUnread(getActivity()); if (onlyUnread && filter != null) { filter += AND_UNREAD; } else if (onlyUnread) { filter = ONLY_UNREAD; } return filter; } /** * @return args that match getLoaderSelection */ protected String[] getLoaderSelectionArgs() { String[] args = null; if (id > 0) { args = Util.LongsToStringArray(this.id); } else if (tag != null) { args = Util.ToStringArray(this.tag); } return args; } @Override public Loader onCreateLoader(final int ID, final Bundle bundle) { if (ID == FEEDITEMS_LOADER) { return new FeedItemDeltaCursorLoader(getActivity(), FeedItemSQL.URI_FEED_ITEMS.buildUpon() .appendQueryParameter(RssContentProvider.QUERY_PARAM_LIMIT, "50").build(), FeedItemSQL.FIELDS, getLoaderSelection(), getLoaderSelectionArgs(), FeedItemSQL.COL_PUBDATE + " DESC"); } else if (ID == FEED_LOADER) { return new CursorLoader(getActivity(), Uri.withAppendedPath(FeedSQL.URI_FEEDS, Long.toString(id)), FeedSQL.FIELDS, null, null, null); } else if (ID == FEED_SETTINGS_LOADER) { String where; String[] whereArgs; if (id > 0) { where = Util.WHEREIDIS; whereArgs = Util.LongsToStringArray(id); } else { where = FeedSQL.COL_TAG + " IS ?"; whereArgs = Util.ToStringArray(tag); } return new CursorLoader(getActivity(), FeedSQL.URI_FEEDS, Util.ToStringArray("DISTINCT " + FeedSQL.COL_NOTIFY), where, whereArgs, null); } return null; } @Override public void onLoadFinished(final Loader cursorLoader, final Object result) { if (FEEDITEMS_LOADER == cursorLoader.getId()) { HashMap<FeedItemSQL, Integer> map = (HashMap<FeedItemSQL, Integer>) result; mAdapter.updateData(map); boolean empty = mAdapter.getItemCount() <= 2; mEmptyView.setVisibility(empty ? View.VISIBLE : View.GONE); mSwipeRefreshLayout.setVisibility(empty ? View.GONE : View.VISIBLE); } else if (FEED_LOADER == cursorLoader.getId()) { Cursor cursor = (Cursor) result; if (cursor.moveToFirst()) { FeedSQL feed = new FeedSQL(cursor); this.title = feed.title; this.customTitle = feed.customTitle; this.url = feed.url; this.notify = feed.notify; ((BaseActivity) getActivity()).getSupportActionBar().setTitle(title); mNotifyCheck.setChecked(this.notify == 1); } // Reset loader getLoaderManager().destroyLoader(cursorLoader.getId()); } else if (FEED_SETTINGS_LOADER == cursorLoader.getId()) { Cursor cursor = (Cursor) result; if (cursor.getCount() == 1 && cursor.moveToFirst()) { // Conclusive results this.notify = cursor.getInt(0); } else { this.notify = 0; } mNotifyCheck.setChecked(this.notify == 1); // Reset loader getLoaderManager().destroyLoader(cursorLoader.getId()); } } @Override public void onLoaderReset(final Loader cursorLoader) { if (FEEDITEMS_LOADER == cursorLoader.getId()) { Log.d(TAG, "onLoaderReset FeedItem"); //mAdapter.swapCursor(null); } } class FeedAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { public static final int HEADERTYPE = 0; public static final int ITEMTYPE = 1; // 64dp at xhdpi is 128 pixels private final int defImgWidth; private final int defImgHeight; private final boolean isGrid; private final int unreadTextColor; private final int readTextColor; private final int linkColor; private final Drawable bgProtection; private final SortedList<FeedItemSQL> mItems; private HashMap<Long, FeedItemSQL> mItemMap; String temps; public FeedAdapter(final Context context) { super(); setHasStableIds(true); mItemMap = new HashMap<>(); mItems = new SortedList<>(FeedItemSQL.class, new SortedList.Callback<FeedItemSQL>() { @Override public int compare(FeedItemSQL a, FeedItemSQL b) { final int retval; // Compare pubdates if (a.getPubDate() == null && b.getPubDate() == null) { return 0; } else if (a.getPubDate() != null && b.getPubDate() == null) { retval = -1; } else if (a.getPubDate() == null) { retval = 1; } else { retval = b.getPubDate().compareTo(a.getPubDate()); } return retval; } @Override public void onInserted(int position, int count) { FeedAdapter.this.notifyItemRangeInserted(1 + position, count); } @Override public void onRemoved(int position, int count) { FeedAdapter.this.notifyItemRangeRemoved(1 + position, count); } @Override public void onMoved(int fromPosition, int toPosition) { FeedAdapter.this.notifyItemMoved(1 + fromPosition, 1 + toPosition); } @Override public void onChanged(int position, int count) { FeedAdapter.this.notifyItemRangeChanged(1 + position, count); } @Override public boolean areContentsTheSame(FeedItemSQL a, FeedItemSQL b) { return a.isUnread() == b.isUnread() && a.feedtitle.compareToIgnoreCase(b.feedtitle) == 0 && a.getDomain().compareToIgnoreCase(b.getDomain()) == 0 && a.plainsnippet.compareToIgnoreCase(b.plainsnippet) == 0 && a.plaintitle.compareToIgnoreCase(b.plaintitle) == 0; } @Override public boolean areItemsTheSame(FeedItemSQL item1, FeedItemSQL item2) { return item1.id == item2.id; } }); isGrid = TabletUtils.isTablet(context); unreadTextColor = context.getResources().getColor(R.color.primary_text_default_material_dark); readTextColor = context.getResources().getColor(R.color.secondary_text_material_dark); linkColor = context.getResources().getColor(R.color.accent); bgProtection = context.getResources().getDrawable(R.drawable.bg_protect); if (isGrid) { defImgHeight = Math.round(context.getResources().getDimension(R.dimen.grid_item_size)); Point size = new Point(); getActivity().getWindowManager().getDefaultDisplay().getSize(size); defImgWidth = size.x / TabletUtils.numberOfFeedColumns(context); } else { defImgWidth = Math.round(context.getResources().getDimension(R.dimen.item_img_def_width)); defImgHeight = Math.round(context.getResources().getDimension(R.dimen.item_img_def_height)); } } @Override public long getItemId(final int hposition) { if (hposition == 0) { // header return -2; } else { int position = hposition - 1; if (position >= mItems.size()) { return -3; } else { return mItems.get(position).id; } } } public void updateData(HashMap<FeedItemSQL, Integer> map) { HashMap<Long, FeedItemSQL> oldItemMap = mItemMap; mItemMap = new HashMap<>(); mItems.beginBatchedUpdates(); for (FeedItemSQL item : map.keySet()) { if (map.get(item) >= 0) { // Sorted list handles inserting of existing elements mItems.add(item); // Add to new map as well mItemMap.put(item.id, item); // And remove from old oldItemMap.remove(item.id); } else { mItems.remove(item); // And remove from old oldItemMap.remove(item.id); } } // If any items remain in old set, they are not present in current result set, // remove them. This is pretty much what is done in the delta loader, but if // the loader is restarted, then it has no old data to go on. for (FeedItemSQL item : oldItemMap.values()) { mItems.remove(item); } mItems.endBatchedUpdates(); } @Override public int getItemCount() { // header + rest return 2 + mItems.size(); } @Override public int getItemViewType(int position) { if (position == 0 || (position - 1) >= mItems.size()) { return HEADERTYPE; } else { return ITEMTYPE; } } @Override public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) { if (viewType == HEADERTYPE) { // Header final View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.padding_header_item, parent, false); return new HeaderHolder(v); } else { // normal item final int item_layout = R.layout.list_story_item; if (TabletUtils.isTablet(parent.getContext())) { // TODO } else { //item_layout = R.layout.view_story; } return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(item_layout, parent, false)); } } @Override public void onBindViewHolder(final RecyclerView.ViewHolder vHolder, final int hposition) { if (getItemViewType(hposition) == HEADERTYPE) { // Nothing to bind for padding return; } final ViewHolder holder = (ViewHolder) vHolder; // Make sure view is reset if it was dismissed holder.resetView(); // Compensate for header final int position = hposition - 1; // Get item final FeedItemSQL item = mItems.get(position); holder.rssItem = item; // Set the title first SpannableStringBuilder titleText = new SpannableStringBuilder(item.feedtitle); // If no body, display domain of link to be opened if (holder.rssItem.description == null || holder.rssItem.description.isEmpty()) { titleText.append(" \u2014 "); if (holder.rssItem.enclosurelink != null) { titleText.append(holder.rssItem.getEnclosureFilename()); } else { titleText.append(holder.rssItem.getDomain()); } titleText.setSpan(new ForegroundColorSpan(linkColor), item.feedtitle.length() + 3, titleText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } holder.authorTextView.setText(titleText); if (item.getPubDate() == null) { holder.dateTextView.setVisibility(View.GONE); } else { holder.dateTextView.setVisibility(View.VISIBLE); holder.dateTextView.setText( item.getPubDate().withZone(DateTimeZone.getDefault()).toString(shortDateTimeFormat)); } holder.fillTitle(); if (item.imageurl == null || item.imageurl.isEmpty()) { holder.imageView.setVisibility(View.GONE); //holder.textGroup.setBackground(null); } else { // Take up width holder.imageView.setVisibility(View.INVISIBLE); // Load image when item has been measured holder.itemView.getViewTreeObserver().addOnPreDrawListener(holder); } } // Provide a reference to the views for each data item // Complex data items may need more than one view per item, and // you provide access to all the views for a data item in a view holder public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener, ViewTreeObserver.OnPreDrawListener { public final TextView titleTextView; public final TextView bodyTextView; public final TextView dateTextView; public final TextView authorTextView; //public final View textGroup; public final ImageView imageView; public final View view; private final View bgFrame; private final View checkLeft; private final View checkRight; private final View checkBg; public FeedItemSQL rssItem; public ViewHolder(View v) { super(v); //textGroup = v.findViewById(R.id.story_text); titleTextView = (TextView) v.findViewById(R.id.story_snippet); bodyTextView = (TextView) v.findViewById(R.id.story_body); dateTextView = (TextView) v.findViewById(R.id.story_date); authorTextView = (TextView) v.findViewById(R.id.story_author); imageView = (ImageView) v.findViewById(R.id.story_image); checkBg = v.findViewById(R.id.check_bg); checkLeft = v.findViewById(R.id.check_left); checkRight = v.findViewById(R.id.check_right); bgFrame = v.findViewById(R.id.swiping_item); this.view = v; v.setOnClickListener(this); v.setOnLongClickListener(this); // Swipe handler v.setOnTouchListener( new SwipeDismissTouchListener(v, null, new SwipeDismissTouchListener.DismissCallbacks() { @Override public boolean canDismiss(Object token) { //return rssItem != null && rssItem.isUnread(); return rssItem != null; } @Override public void onDismiss(View view, Object token) { rssItem.setUnread(!rssItem.isUnread()); // Update the item directly before updating database if (!PrefUtils.isShowOnlyUnread(getActivity())) { // Just update the view state fillTitle(); resetView(); } else { // Remove it from the dataset directly mItems.remove(rssItem); } // Make database consistent with content if (rssItem.isUnread()) { RssDatabaseService.markItemAsUnread(getActivity(), rssItem.id); } else { RssDatabaseService.markItemAsRead(getActivity(), rssItem.id); } } /** * Called when a swipe is started. * * @param goingRight true if swiping to the right, false if left */ @Override public void onSwipeStarted(boolean goingRight) { // SwipeRefreshLayout does not honor requestDisallowInterceptTouchEvent mSwipeRefreshLayout.setEnabled(false); TypedValue typedValue = new TypedValue(); if (PrefUtils.isNightMode(getActivity())) { getActivity().getTheme().resolveAttribute(R.attr.nightBGColor, typedValue, true); } else { getActivity().getTheme().resolveAttribute(android.R.attr.windowBackground, typedValue, true); } bgFrame.setBackgroundColor(typedValue.data); checkBg.setVisibility(View.VISIBLE); if (goingRight) { checkLeft.setVisibility(View.VISIBLE); } else { checkRight.setVisibility(View.VISIBLE); } } /** * Called when user doesn't swipe all the way. */ @Override public void onSwipeCancelled() { // SwipeRefreshLayout does not honor requestDisallowInterceptTouchEvent mSwipeRefreshLayout.setEnabled(true); checkBg.setVisibility(View.INVISIBLE); checkLeft.setVisibility(View.INVISIBLE); checkRight.setVisibility(View.INVISIBLE); bgFrame.setBackground(null); } /** * @return the subview which should move */ @Override public View getSwipingView() { return bgFrame; } })); } public void resetView() { checkBg.setVisibility(View.INVISIBLE); checkLeft.setVisibility(View.INVISIBLE); checkRight.setVisibility(View.INVISIBLE); bgFrame.clearAnimation(); bgFrame.setAlpha(1.0f); bgFrame.setTranslationX(0.0f); bgFrame.setBackground(null); } public void fillTitle() { if (rssItem.plaintitle == null) { titleTextView.setVisibility(View.GONE); } else { titleTextView.setVisibility(View.VISIBLE); // \u2014 is a EM-dash, basically a long version of '-' temps = (rssItem.plainsnippet == null || rssItem.plainsnippet.isEmpty()) ? rssItem.plaintitle : rssItem.plaintitle + " \u2014 " + rssItem.plainsnippet + "\u2026"; Spannable textSpan = new SpannableString(temps); // Body is always grey textSpan.setSpan(new ForegroundColorSpan(readTextColor), rssItem.plaintitle.length(), temps.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // Title depends on status textSpan.setSpan(new ForegroundColorSpan(rssItem.isUnread() ? unreadTextColor : readTextColor), 0, rssItem.plaintitle.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); titleTextView.setText(textSpan); } } /** * OnItemClickListener replacement. * <p/> * If a feeditem does not have any content, * then it opens the link in the browser directly. * * @param view */ @Override public void onClick(final View view) { if (mActionMode != null) { mActionMode.finish(); } // Open item if not empty if (rssItem.description != null && !rssItem.description.isEmpty()) { Intent i = new Intent(getActivity(), ReaderActivity.class); //i.setData(Uri.parse(link)); i.putExtra(BaseActivity.SHOULD_FINISH_BACK, true); ReaderActivity.setRssExtras(i, rssItem); // ActivityOptionsCompat options = ActivityOptionsCompat // .makeScaleUpAnimation(view, 0, 0, view.getWidth(), // view.getHeight()); // ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), // new Pair<View, String>(titleTextView, // "title")); //new Pair<View, String>(imageView, "image")); //ActivityCompat.startActivity(getActivity(), i, null); startActivity(i); } else { // Mark as read RssDatabaseService.markItemAsRead(getActivity(), rssItem.id); // Open in browser since no content was posted // Use enclosure or link startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(rssItem.enclosurelink != null ? rssItem.enclosurelink : rssItem.link))); } } // Called when the user long-clicks on someView @Override public boolean onLongClick(View view) { // Remember which item mSelectedItem = this.rssItem; if (mActionMode == null) { // Start the CAB using the ActionMode.Callback defined above mActionMode = getActivity().startActionMode(mActionModeCallback); //view.setSelected(true); view.setActivated(true); } mActionMode.setSubtitle(mSelectedItem.title); mActionMode.setTitle("Selected"); return true; } /** * Called when item has been measured, it is now the time to insert the image. * * @return Return true to proceed with the current drawing pass, or false to cancel. */ @Override public boolean onPreDraw() { imageView.setVisibility(View.VISIBLE); // Width is fixed int w = defImgWidth; // Use the parent's height int h = itemView.getHeight(); //Log.d("JONAS3", "iv:" + imageView.getHeight() + ", item:" + h); if (!isDetached() && getActivity() != null) { try { Glide.with(FeedFragment.this).load(rssItem.imageurl).centerCrop().into(imageView); } catch (IllegalArgumentException e) { // Could still happen if we have a race-condition? Log.d(TAG, e.getLocalizedMessage()); } } //Picasso.with(getActivity()).load(rssItem.imageurl).resize(w, h).centerCrop().noFade() // .tag(FeedFragment.this) // .into(imageView); // if (isGrid) { // textGroup.setBackground(bgProtection); // } // Remove as listener itemView.getViewTreeObserver().removeOnPreDrawListener(this); return true; } /* Intent story = new Intent(getActivity(), StoryActivity.class); story.putExtra("title", titleTextView.getText()); story.putExtra("body", bodyTextView.getText()); Bundle activityOptions = null; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.L) { ActivityOptions options = ActivityOptions .makeSceneTransitionAnimation(getActivity(), new Pair<View, String>(titleTextView, "title"), new Pair<View, String>(bodyTextView, "body"), new Pair<View, String>(imageView, "image")); getActivity().setExitSharedElementListener(new SharedElementListener() { @Override public void remapSharedElements(List<String> names, Map<String, View> sharedElements) { super.remapSharedElements(names, sharedElements); sharedElements.put("title", titleTextView); sharedElements.put("body", bodyTextView); sharedElements.put("image", imageView); } }); activityOptions = options.toBundle(); } startActivity(story, activityOptions);*/ // } } } public class HeaderHolder extends RecyclerView.ViewHolder { public HeaderHolder(final View itemView) { super(itemView); } } }