0
0
mirror of https://github.com/mediathekview/zapp.git synced 2024-09-20 20:23:04 +02:00

Make channel order adjustable #6

This commit is contained in:
Christine Emrich 2016-10-11 21:21:17 +02:00
parent 68b31c59ab
commit a130d5057d
21 changed files with 584 additions and 11 deletions

View File

@ -28,21 +28,30 @@ android {
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile project(':programguide')
// tests
testCompile 'junit:junit:4.12'
androidTestCompile 'com.android.support:support-annotations:24.2.1'
androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5'
// support
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:support-v4:24.2.1'
compile 'com.android.support:design:24.2.1'
// helper
compile 'com.google.code.gson:gson:2.7'
compile 'commons-io:commons-io:2.5'
// changelog
compile 'com.github.porokoro.paperboy:paperboy:3.0.0'
// for butterknive:
// sortable list
compile 'com.github.woxthebox:draglistview:1.3'
// butterknive:
compile 'com.jakewharton:butterknife:8.4.0'
apt 'com.jakewharton:butterknife-compiler:8.4.0'
compile project(':programguide')
}

View File

@ -35,6 +35,11 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".ChannelSelectionActivity"
android:label="@string/activity_channel_selection_title"
android:parentActivityName=".ChannelListActivity">
</activity>
</application>
</manifest>

View File

@ -27,7 +27,7 @@ import butterknife.OnTouch;
import de.christinecoenen.code.zapp.adapters.ChannelDetailAdapter;
import de.christinecoenen.code.zapp.model.ChannelModel;
import de.christinecoenen.code.zapp.model.IChannelList;
import de.christinecoenen.code.zapp.model.json.JsonChannelList;
import de.christinecoenen.code.zapp.model.json.SortableJsonChannelList;
import de.christinecoenen.code.zapp.utils.ColorHelper;
import de.christinecoenen.code.zapp.utils.VideoErrorHandler;
import de.christinecoenen.code.zapp.utils.view.ClickableViewPager;
@ -133,7 +133,7 @@ public class ChannelDetailActivity extends FullscreenActivity implements
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
final IChannelList channelList = new JsonChannelList(this);
final IChannelList channelList = new SortableJsonChannelList(this);
// set to channel
Bundle extras = getIntent().getExtras();

View File

@ -15,14 +15,17 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnItemClick;
import de.christinecoenen.code.zapp.adapters.ChannelListAdapter;
import de.christinecoenen.code.zapp.model.IChannelList;
import de.christinecoenen.code.zapp.model.json.JsonChannelList;
import de.christinecoenen.code.zapp.model.ISortableChannelList;
import de.christinecoenen.code.zapp.model.json.SortableJsonChannelList;
public class ChannelListActivity extends AppCompatActivity {
protected @BindView(R.id.toolbar) Toolbar toolbar;
protected @BindView(R.id.gridview_channels) GridView channelGridView;
private ISortableChannelList channelList;
private BaseAdapter gridAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -33,11 +36,18 @@ public class ChannelListActivity extends AppCompatActivity {
setSupportActionBar(toolbar);
ViewCompat.setNestedScrollingEnabled(channelGridView, true);
IChannelList channelList = new JsonChannelList(this);
BaseAdapter gridAdapter = new ChannelListAdapter(this, channelList);
channelList = new SortableJsonChannelList(this);
gridAdapter = new ChannelListAdapter(this, channelList);
channelGridView.setAdapter(gridAdapter);
}
@Override
protected void onResume() {
super.onResume();
channelList.reloadChannelOrder();
gridAdapter.notifyDataSetChanged();
}
@OnItemClick(R.id.gridview_channels)
void onGridItemClick(int position) {
Intent intent = ChannelDetailActivity.getStartIntent(this, position);
@ -53,9 +63,15 @@ public class ChannelListActivity extends AppCompatActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.menu_channel_selection:
intent = ChannelSelectionActivity.getStartIntent(this);
startActivity(intent);
return true;
case R.id.menu_changelog:
Intent intent = ChangelogActivity.getStartIntent(this);
intent = ChangelogActivity.getStartIntent(this);
startActivity(intent);
return true;
default:

View File

@ -0,0 +1,59 @@
package de.christinecoenen.code.zapp;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import com.woxthebox.draglistview.DragListView;
import butterknife.BindView;
import butterknife.ButterKnife;
import de.christinecoenen.code.zapp.adapters.ChannelSelectionAdapter;
import de.christinecoenen.code.zapp.model.ISortableChannelList;
import de.christinecoenen.code.zapp.model.json.SortableJsonChannelList;
import de.christinecoenen.code.zapp.utils.view.GridAutofitLayoutManager;
import de.christinecoenen.code.zapp.utils.view.SimpleDragListListener;
public class ChannelSelectionActivity extends AppCompatActivity {
public static Intent getStartIntent(Context context) {
return new Intent(context, ChannelSelectionActivity.class);
}
protected @BindView(R.id.draglist_channel_selection) DragListView channelListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_channel_selection);
ButterKnife.bind(this);
ActionBar toolbar = getSupportActionBar();
if (toolbar != null) {
toolbar.setSubtitle(R.string.activity_channel_selection_subtitle);
}
// adapter
final ISortableChannelList channelList = new SortableJsonChannelList(this);
final ChannelSelectionAdapter listAdapter = new ChannelSelectionAdapter(this);
listAdapter.setItemList(channelList.getList());
// view
RecyclerView.LayoutManager layoutManager = new GridAutofitLayoutManager(this, 400);
channelListView.setLayoutManager(layoutManager);
channelListView.setAdapter(listAdapter, true);
channelListView.setDragListListener(new SimpleDragListListener() {
@Override
public void onItemDragEnded(int fromPosition, int toPosition) {
if (fromPosition != toPosition) {
channelList.persistChannelOrder();
}
}
});
}
}

View File

@ -0,0 +1,68 @@
package de.christinecoenen.code.zapp.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.woxthebox.draglistview.DragItemAdapter;
import butterknife.BindView;
import butterknife.ButterKnife;
import de.christinecoenen.code.zapp.R;
import de.christinecoenen.code.zapp.model.ChannelModel;
public class ChannelSelectionAdapter extends DragItemAdapter<ChannelModel, ChannelSelectionAdapter.ViewHolder> {
private final LayoutInflater inflater;
public ChannelSelectionAdapter(Context context) {
inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
setHasStableIds(true);
}
@Override
public ChannelSelectionAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = inflater.inflate(R.layout.item_channel_selection_list, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ChannelSelectionAdapter.ViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
ChannelModel channel = mItemList.get(position);
holder.setChannel(channel);
}
@Override
public long getItemId(int position) {
return mItemList.get(position).getId().hashCode();
}
class ViewHolder extends DragItemAdapter.ViewHolder {
@BindView(R.id.image_handle) ImageView handleView;
@BindView(R.id.image_channel_logo) ImageView logoView;
@BindView(R.id.text_channel_subtitle) TextView subtitle;
ViewHolder(final View itemView) {
super(itemView, R.id.image_handle, false);
ButterKnife.bind(this, itemView);
}
void setChannel(ChannelModel channel) {
logoView.setImageResource(channel.getDrawableId());
handleView.setContentDescription(channel.getName());
if (channel.getSubtitle() == null) {
subtitle.setVisibility(View.GONE);
} else {
subtitle.setText(channel.getSubtitle());
subtitle.setVisibility(View.VISIBLE);
}
}
}
}

View File

@ -1,7 +1,10 @@
package de.christinecoenen.code.zapp.model;
public interface IChannelList {
import java.util.List;
public interface IChannelList extends Iterable<ChannelModel> {
ChannelModel get(int index);
int size();
List<ChannelModel> getList();
}

View File

@ -0,0 +1,21 @@
package de.christinecoenen.code.zapp.model;
/**
* A sorted channel list. Channel order will be
* persisted across application starts.
*/
public interface ISortableChannelList extends IChannelList {
/**
* Reloads the current channel order from disk.
* Us this eg. in onResume when another activity
* might have modified the channel order.
*/
void reloadChannelOrder();
/**
* Writes the current channel order to disk.
*/
void persistChannelOrder();
}

View File

@ -0,0 +1,37 @@
package de.christinecoenen.code.zapp.model;
import java.util.Iterator;
import java.util.List;
/**
* Simple IChannelList wrapper around a List of ChannelModels.
*/
public class SimpleChannelList implements IChannelList {
private final List<ChannelModel> channels;
public SimpleChannelList(List<ChannelModel> channels) {
this.channels = channels;
}
@Override
public ChannelModel get(int index) {
return channels.get(index);
}
@Override
public int size() {
return channels.size();
}
@Override
public List<ChannelModel> getList() {
return channels;
}
@Override
public Iterator<ChannelModel> iterator() {
return channels.iterator();
}
}

View File

@ -7,6 +7,7 @@ import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.List;
import de.christinecoenen.code.zapp.R;
@ -40,6 +41,16 @@ public class JsonChannelList implements IChannelList {
return channels.size();
}
@Override
public List<ChannelModel> getList() {
return channels;
}
@Override
public Iterator<ChannelModel> iterator() {
return channels.iterator();
}
/**
* @return content of R.raw.channels json file
*/

View File

@ -0,0 +1,60 @@
package de.christinecoenen.code.zapp.model.json;
import android.content.Context;
import java.util.Iterator;
import java.util.List;
import de.christinecoenen.code.zapp.model.ChannelModel;
import de.christinecoenen.code.zapp.model.IChannelList;
import de.christinecoenen.code.zapp.model.ISortableChannelList;
import de.christinecoenen.code.zapp.model.SimpleChannelList;
import de.christinecoenen.code.zapp.preferences.PreferenceChannelOrderHelper;
public class SortableJsonChannelList implements ISortableChannelList {
private IChannelList channelList;
private final PreferenceChannelOrderHelper channelOrderHelper;
public SortableJsonChannelList(Context context) {
channelOrderHelper = new PreferenceChannelOrderHelper(context);
channelList = new JsonChannelList(context);
loadSortingFromDisk();
}
@Override
public ChannelModel get(int index) {
return channelList.get(index);
}
@Override
public int size() {
return channelList.size();
}
@Override
public List<ChannelModel> getList() {
return channelList.getList();
}
@Override
public Iterator<ChannelModel> iterator() {
return channelList.iterator();
}
@Override
public void reloadChannelOrder() {
loadSortingFromDisk();
}
@Override
public void persistChannelOrder() {
channelOrderHelper.saveChannelOrder(channelList.getList());
}
private void loadSortingFromDisk() {
List<ChannelModel> sortedChannels = channelOrderHelper.sortChannelList(channelList.getList());
channelList = new SimpleChannelList(sortedChannels);
}
}

View File

@ -0,0 +1,69 @@
package de.christinecoenen.code.zapp.preferences;
import android.content.Context;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import de.christinecoenen.code.zapp.model.ChannelModel;
import de.christinecoenen.code.zapp.utils.PreferenceHelper;
/**
* Helps persisting and reloading the channel order.
* SharedPreferences are used to store the values on disk.
*/
public class PreferenceChannelOrderHelper {
private static final String PREF_KEY_CHANNEL_ORDER = "PREF_KEY_CHANNEL_ORDER";
private final PreferenceHelper preferenceHelper;
public PreferenceChannelOrderHelper (Context context) {
preferenceHelper = new PreferenceHelper(context);
}
public void saveChannelOrder(List<ChannelModel> channels) {
List<String> sortedChannelIds = new ArrayList<>(channels.size());
for (ChannelModel channel : channels) {
sortedChannelIds.add(channel.getId());
}
preferenceHelper.saveList(PREF_KEY_CHANNEL_ORDER, sortedChannelIds);
}
public List<ChannelModel> sortChannelList(List<ChannelModel> channels) {
List<String> sortedChannelIds = preferenceHelper.loadList(PREF_KEY_CHANNEL_ORDER);
if (sortedChannelIds == null) {
// have never been saved before
return channels;
}
int size = Math.max(sortedChannelIds.size(), channels.size());
int unsavedIndex = sortedChannelIds.size();
ChannelModel[] sortedChannelArray = new ChannelModel[size];
for (ChannelModel channel : channels) {
int index = sortedChannelIds.indexOf(channel.getId());
if (index == -1) {
// order for this channel has never been saved - move to end
index = unsavedIndex++;
}
sortedChannelArray[index] = channel;
}
// save as editable list without null values in case channels have been deleted
List<ChannelModel> sortedChannelList = new ArrayList<>(Arrays.asList(sortedChannelArray));
sortedChannelList.removeAll(Collections.singleton((ChannelModel) null));
return sortedChannelList;
}
}

View File

@ -0,0 +1,41 @@
package de.christinecoenen.code.zapp.utils;
import android.content.Context;
import android.content.SharedPreferences;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
public class PreferenceHelper {
private static final String SHARED_PEFERENCES_NAME = "ZAPP_SHARED_PREFERENCES";
private final Gson gson = new Gson();
private final SharedPreferences preferences;
public PreferenceHelper(Context context) {
preferences = context.getSharedPreferences(SHARED_PEFERENCES_NAME, Context.MODE_PRIVATE);
}
public void saveList(String key, List list) {
SharedPreferences.Editor editor = preferences.edit();
String jsonList = gson.toJson(list);
editor.putString(key, jsonList);
editor.apply();
}
public <T> List<T> loadList(String key) {
if (preferences.contains(key)) {
String jsonList = preferences.getString(key, null);
Type listType = new TypeToken<ArrayList<T>>(){}.getType();
return gson.fromJson(jsonList, listType);
} else {
return null;
}
}
}

View File

@ -0,0 +1,67 @@
package de.christinecoenen.code.zapp.utils.view;
import android.content.Context;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
/**
* A GridLayoutManager with a flexible column count
* based on view size and item size.
*
* @see "http://stackoverflow.com/a/30256880/3012757"
*/
@SuppressWarnings("ALL")
public class GridAutofitLayoutManager extends GridLayoutManager {
private int mColumnWidth;
private boolean mColumnWidthChanged = true;
public GridAutofitLayoutManager(Context context, int columnWidth) {
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1);
setColumnWidth(checkedColumnWidth(context, columnWidth));
}
public GridAutofitLayoutManager(Context context, int columnWidth, int orientation, boolean reverseLayout) {
/* Initially set spanCount to 1, will be changed automatically later. */
super(context, 1, orientation, reverseLayout);
setColumnWidth(checkedColumnWidth(context, columnWidth));
}
private int checkedColumnWidth(Context context, int columnWidth) {
if (columnWidth <= 0) {
/* Set default columnWidth value (48dp here). It is better to move this constant
to static constant on top, but we need context to convert it to dp, so can't really
do so. */
columnWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 48,
context.getResources().getDisplayMetrics());
}
return columnWidth;
}
public void setColumnWidth(int newColumnWidth) {
if (newColumnWidth > 0 && newColumnWidth != mColumnWidth) {
mColumnWidth = newColumnWidth;
mColumnWidthChanged = true;
}
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
int width = getWidth();
int height = getHeight();
if (mColumnWidthChanged && mColumnWidth > 0 && width > 0 && height > 0) {
int totalSpace;
if (getOrientation() == VERTICAL) {
totalSpace = width - getPaddingRight() - getPaddingLeft();
} else {
totalSpace = height - getPaddingTop() - getPaddingBottom();
}
int spanCount = Math.max(1, totalSpace / mColumnWidth);
setSpanCount(spanCount);
mColumnWidthChanged = false;
}
super.onLayoutChildren(recycler, state);
}
}

View File

@ -0,0 +1,15 @@
package de.christinecoenen.code.zapp.utils.view;
import com.woxthebox.draglistview.DragListView;
public class SimpleDragListListener implements DragListView.DragListListener {
@Override
public void onItemDragStarted(int position) {}
@Override
public void onItemDragging(int itemPosition, float x, float y) {}
@Override
public void onItemDragEnded(int fromPosition, int toPosition) {}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M3,21h18v-2L3,19v2zM3,17h18v-2L3,15v2zM3,13h18v-2L3,11v2zM3,9h18L21,7L3,7v2zM3,3v2h18L21,3L3,3z"/>
</vector>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_channel_selection"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="de.christinecoenen.code.zapp.ChannelSelectionActivity">
<com.woxthebox.draglistview.DragListView
android:id="@+id/draglist_channel_selection"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="@dimen/activity_channel_selection_item_width"
android:layout_height="@dimen/activity_channel_selection_item_height"
android:layout_margin="@dimen/activity_channel_selection_item_margin"
android:background="@android:color/white"
android:orientation="horizontal">
<ImageView
android:id="@+id/image_handle"
android:layout_width="@dimen/activity_channel_selection_handleWidth"
android:layout_height="match_parent"
android:layout_gravity="center"
android:src="@drawable/ic_handle_white_24dp"
android:padding="@dimen/activity_channel_selection_item_margin"
tools:ignore="ContentDescription"
android:background="@color/black_overlay"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/image_channel_logo"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="4"
android:padding="@dimen/activity_vertical_margin"
tools:src="@drawable/channel_logo_das_erste"
tools:ignore="ContentDescription"/>
<TextView
android:id="@+id/text_channel_subtitle"
android:layout_width="match_parent"
android:layout_height="0dip"
android:layout_weight="1"
style="@style/Base.TextAppearance.AppCompat.Small"
android:textSize="12sp"
android:gravity="center|bottom"
android:lines="1"
android:ellipsize="end"
tools:text="Mecklenburg-Vorpommern" />
</LinearLayout>
</LinearLayout>

View File

@ -2,6 +2,12 @@
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_channel_selection"
android:title="@string/menu_channel_selection"
app:showAsAction="never"/>
<item
android:id="@+id/menu_changelog"
android:title="@string/menu_changelog"

View File

@ -5,4 +5,9 @@
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="view_program_info_subtitle_margin_bottom">4dp</dimen>
<dimen name="activity_channel_selection_item_width">120dip</dimen>
<dimen name="activity_channel_selection_item_height">75dip</dimen>
<dimen name="activity_channel_selection_item_margin">5dip</dimen>
<dimen name="activity_channel_selection_handleWidth">25dip</dimen>
</resources>

View File

@ -5,6 +5,9 @@
<string name="activity_channel_detail_info_error">Keine Programminfo</string>
<string name="activity_channel_selection_title">Sender anordnen</string>
<string name="activity_channel_selection_subtitle">Ziehe an den grauen Balken</string>
<string name="view_program_info_show_time">%1$s %2$s Uhr</string>
<!-- error messages -->
@ -19,6 +22,7 @@
<!-- menus -->
<string name="action_share">Teilen</string>
<string name="menu_channel_selection">Sender anordnen</string>
<string name="menu_changelog">Changelog</string>
<!-- paperboy changelog -->