diff --git a/AndroidManifest.xml b/AndroidManifest.xml index c208098d7..00b3406dd 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,7 +2,7 @@ + android:versionName="0.3.0" > + + + + + + + - - + android:versionName="0.3.0" > + + + + + + + - - - - + android:layout_height="fill_parent" /> - + + + + + + + + \ No newline at end of file diff --git a/res/layout/pull_list_view_footer.xml b/res/layout/pull_list_view_footer.xml new file mode 100644 index 000000000..b6f48860e --- /dev/null +++ b/res/layout/pull_list_view_footer.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/pull_list_view_header.xml b/res/layout/pull_list_view_header.xml new file mode 100644 index 000000000..9041d0ea7 --- /dev/null +++ b/res/layout/pull_list_view_header.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/pull_to_refresh_head.xml b/res/layout/pull_to_refresh_head.xml new file mode 100644 index 000000000..c51129b88 --- /dev/null +++ b/res/layout/pull_to_refresh_head.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/start.xml b/res/layout/start.xml new file mode 100644 index 000000000..097e8d304 --- /dev/null +++ b/res/layout/start.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 27fcbebfa..3ecf31420 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -3,7 +3,6 @@ 16dp 16dp 15dp - 120dp 150dp diff --git a/res/values/themes.xml b/res/values/themes.xml index 32dc3518f..6cb9aab93 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -15,4 +15,13 @@ @drawable/scrollbar_blue + + + \ No newline at end of file diff --git a/src/com/hippo/ehviewer/StartActivity.java b/src/com/hippo/ehviewer/StartActivity.java new file mode 100644 index 000000000..1dcd99d08 --- /dev/null +++ b/src/com/hippo/ehviewer/StartActivity.java @@ -0,0 +1,174 @@ +package com.hippo.ehviewer; + +import com.hippo.ehviewer.activity.MangaListActivity; +import com.hippo.ehviewer.dialog.DialogBuilder; +import com.hippo.ehviewer.util.Config; +import com.hippo.ehviewer.util.Crash; +import com.hippo.ehviewer.view.AlertButton; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.widget.LinearLayout; + +public class StartActivity extends Activity { + + private static final int CHECK_WARING = 0; + private static final int CHECK_CRESH = 1; + private static final int CHECK_NETWORK = 2; + + private String lastCrash; + private boolean isAnimationOver = false; + private boolean isCheckOver = false; + + private AlertDialog createWarningDialog() { + return new DialogBuilder(this).setCancelable(false) + .setTitle(R.string.dailog_waring_title) + .setMessage(R.string.dailog_waring_plain) + .setPositiveButton(R.string.dailog_waring_yes, + new View.OnClickListener() { + @Override + public void onClick(View v) { + ((AlertButton)v).dialog.dismiss(); + Config.allowed(); + + check(CHECK_CRESH); + } + }).setNegativeButton(R.string.dailog_waring_no, + new View.OnClickListener() { + @Override + public void onClick(View v) { + ((AlertButton)v).dialog.dismiss(); + finish(); + } + }).create(); + } + + private AlertDialog createSendCrashDialog() { + return new DialogBuilder(this).setCancelable(false) + .setTitle(R.string.dialog_send_crash_title) + .setMessage(R.string.dialog_send_crash_plain) + .setPositiveButton(R.string.dialog_send_crash_yes, new View.OnClickListener() { + @Override + public void onClick(View v) { + ((AlertButton)v).dialog.dismiss(); + Intent i = new Intent(Intent.ACTION_SENDTO); + i.setData(Uri.parse("mailto:ehviewersu@gmail.com")); + i.putExtra(Intent.EXTRA_SUBJECT, "I found a bug in EhViewer !"); + i.putExtra(Intent.EXTRA_TEXT, lastCrash); + startActivity(i); + lastCrash = null; + + check(CHECK_NETWORK); + } + }).setNegativeButton(R.string.dialog_send_crash_no, new View.OnClickListener() { + + @Override + public void onClick(View v) { + ((AlertButton)v).dialog.dismiss(); + lastCrash = null; + + check(CHECK_NETWORK); + } + }).create(); + } + + private AlertDialog createNetworkErrorDialog() { + return new DialogBuilder(this).setCancelable(false) + .setTitle(R.string.error).setMessage(R.string.em_no_network) + .setPositiveButton(R.string.dailog_network_error_yes, + new View.OnClickListener() { + @Override + public void onClick(View v) { + ((AlertButton)v).dialog.dismiss(); + checkOver(); + } + }) + .setNegativeButton(R.string.dailog_network_error_no, + new View.OnClickListener() { + @Override + public void onClick(View v) { + ((AlertButton)v).dialog.dismiss(); + finish(); + } + }).create(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.start); + + LinearLayout wellcome = (LinearLayout)findViewById(R.id.app_start_view); + // Show welcome in progress + AlphaAnimation aa = new AlphaAnimation(0.3f,1.0f); + aa.setDuration(3000); + aa.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationEnd(Animation arg0) { + synchronized (StartActivity.this) { + if (isCheckOver) + redirectTo(); + isAnimationOver = true; + } + } + @Override + public void onAnimationRepeat(Animation animation) {} + @Override + public void onAnimationStart(Animation animation) {} + }); + wellcome.startAnimation(aa); + check(CHECK_WARING); + } + + private void check(int order) { + switch (order) { + case CHECK_WARING: + if (!Config.isAllowed()) { + createWarningDialog().show(); + return; + } + case CHECK_CRESH: + if ((lastCrash = Crash.getLastCrash()) != null) { + createSendCrashDialog().show(); + return; + } + case CHECK_NETWORK: + if (!isNetworkAvailable()) { + createNetworkErrorDialog().show(); + return; + } + } + checkOver(); + } + + private synchronized void checkOver() { + if (isAnimationOver) + redirectTo(); + isCheckOver = true; + } + + private boolean isNetworkAvailable() { + ConnectivityManager cm = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mNetworkInfo = cm.getActiveNetworkInfo(); + if (mNetworkInfo == null || !mNetworkInfo.isAvailable()) + return false; + else + return true; + } + + private void redirectTo(){ + Intent intent = new Intent(this, MangaListActivity.class); + startActivity(intent); + finish(); + } +} diff --git a/src/com/hippo/ehviewer/activity/MangaListActivity.java b/src/com/hippo/ehviewer/activity/MangaListActivity.java index 7d7736319..032e8f774 100644 --- a/src/com/hippo/ehviewer/activity/MangaListActivity.java +++ b/src/com/hippo/ehviewer/activity/MangaListActivity.java @@ -1,19 +1,15 @@ package com.hippo.ehviewer.activity; -import java.io.IOException; +import java.net.MalformedURLException; import java.util.ArrayList; -import com.handmark.pulltorefresh.library.LoadingLayoutProxy; -import com.handmark.pulltorefresh.library.PullToRefreshBase; -import com.handmark.pulltorefresh.library.PullToRefreshBase.Mode; -import com.handmark.pulltorefresh.library.PullToRefreshListView; -import com.handmark.pulltorefresh.library.PullToRefreshBase.OnRefreshListener2; import com.hippo.ehviewer.BeautifyScreen; import com.hippo.ehviewer.ListMangaDetail; import com.hippo.ehviewer.ListUrls; import com.hippo.ehviewer.R; import com.hippo.ehviewer.dialog.DialogBuilder; import com.hippo.ehviewer.dialog.SuperDialogUtil; +import com.hippo.ehviewer.network.Downloader; import com.hippo.ehviewer.service.DownloadService; import com.hippo.ehviewer.service.DownloadServiceConnection; import com.hippo.ehviewer.util.Cache; @@ -28,36 +24,34 @@ import com.hippo.ehviewer.view.CheckImage; import com.hippo.ehviewer.view.OlImageView; import com.hippo.ehviewer.view.TagListView; import com.hippo.ehviewer.view.TagsAdapter; +import com.hippo.ehviewer.widget.PullListView; +import com.hippo.ehviewer.widget.PullListView.OnFooterRefreshListener; +import com.hippo.ehviewer.widget.PullListView.OnHeaderRefreshListener; import com.jeremyfeinstein.slidingmenu.lib.SlidingMenu; import com.jeremyfeinstein.slidingmenu.lib.app.SlidingActivity; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.IBinder; -import android.R.color; -import android.annotation.SuppressLint; import android.app.AlertDialog; +import android.app.NotificationManager; import android.app.SearchManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.ServiceConnection; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; +import android.support.v4.app.NotificationCompat; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; -import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.BaseAdapter; import android.widget.Button; @@ -70,7 +64,6 @@ import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.SearchView; import android.widget.Spinner; -import android.widget.TableLayout; import android.widget.TextView; import android.widget.Toast; @@ -82,12 +75,16 @@ import android.widget.Toast; public class MangaListActivity extends SlidingActivity { private static String TAG = "MangaListActivity"; - + + // How to get manga list + private static final int OTHER = 0; + private static final int PULL_HEADER = 1; + private static final int ACTION_FOOTER = 2; + private RelativeLayout mainView; private TagListView listMenu; private ListView isExhentaiList; - //private LinearLayout loginMenu; - private PullToRefreshListView pullListView; + private PullListView pullListView; private ListView listView; private View waitView; private Button freshButton; @@ -250,7 +247,7 @@ public class MangaListActivity extends SlidingActivity { if (isChecked) advance.setVisibility(View.VISIBLE); else - advance.setVisibility(View.GONE); + advance.setVisibility(View.INVISIBLE); } }); @@ -262,7 +259,7 @@ public class MangaListActivity extends SlidingActivity { ((AlertButton)v).dialog.dismiss(); MangaListActivity.this.showContent(); lus = getLus(filterDialog); - refresh(); + refresh(OTHER); } }).setNegativeButton(android.R.string.cancel, new View.OnClickListener() { @Override @@ -561,13 +558,12 @@ public class MangaListActivity extends SlidingActivity { mLoadListOver = false; waitView.setVisibility(View.VISIBLE); - pullListView.setMode(Mode.DISABLED); - freshButton.setVisibility(View.GONE); - noFoundView.setVisibility(View.GONE); - sadpanda.setVisibility(View.GONE); + freshButton.setVisibility(View.INVISIBLE); + noFoundView.setVisibility(View.INVISIBLE); + sadpanda.setVisibility(View.INVISIBLE); EhClient.getManagaList(lus.getUrl(), - new MangaListGetPackage(lus.clone(), new Integer[]{lus.getPage(), 1}), + new MangaListGetPackage(lus.clone(), true, OTHER), new MangaListGetListener()); } else { Toast.makeText(MangaListActivity.this, @@ -651,7 +647,7 @@ public class MangaListActivity extends SlidingActivity { if (isChecked) advance.setVisibility(View.VISIBLE); else - advance.setVisibility(View.GONE); + advance.setVisibility(View.INVISIBLE); } }); if (cb.isChecked()) @@ -863,44 +859,41 @@ public class MangaListActivity extends SlidingActivity { } private class MangaListGetPackage { - public ListUrls listUrls; - public Integer[] flag; - public MangaListGetPackage(ListUrls listUrls, Integer[] flag) { - this.listUrls = listUrls; - this.flag = flag; + public ListUrls targetListUrls; + public boolean isSetPosition; + public int getType; + public MangaListGetPackage(ListUrls targetListUrls, boolean isSetPosition, int getType) { + this.targetListUrls = targetListUrls; + this.isSetPosition = isSetPosition; + this.getType = getType; } } private class MangaListGetListener implements EhClient.OnGetManagaListListener { - @Override public void onSuccess(Object checkFlag, ArrayList newLmdArray, int indexPerPage, int maxPage) { MangaListGetPackage getPackage = (MangaListGetPackage)checkFlag; - if (!getPackage.listUrls.equals(lus)) + if (!getPackage.targetListUrls.equals(lus)) return; - pullListView.onRefreshComplete(); - // Check no Found view later - waitView.setVisibility(View.GONE); - freshButton.setVisibility(View.GONE); + waitView.setVisibility(View.INVISIBLE); + freshButton.setVisibility(View.INVISIBLE); if (maxPage == 0) { // If No hits found mLoadListOver = false; - pullListView.setMode(Mode.DISABLED); noFoundView.setVisibility(View.VISIBLE); - sadpanda.setVisibility(View.GONE); + sadpanda.setVisibility(View.INVISIBLE); setTitle(R.string.no_found); lmdArray.clear(); gmlAdapter.notifyDataSetChanged(); } else if (maxPage == -1) { //panda mLoadListOver = false; - pullListView.setMode(Mode.DISABLED); - noFoundView.setVisibility(View.GONE); + noFoundView.setVisibility(View.INVISIBLE); sadpanda.setVisibility(View.VISIBLE); setTitle(R.string.sadpanda); lmdArray.clear(); @@ -908,17 +901,16 @@ public class MangaListActivity extends SlidingActivity { } else { mLoadListOver = true; - pullListView.setMode(Mode.BOTH); - noFoundView.setVisibility(View.GONE); - sadpanda.setVisibility(View.GONE); + pullListView.setVisibility(View.VISIBLE); + noFoundView.setVisibility(View.INVISIBLE); + sadpanda.setVisibility(View.INVISIBLE); // Set indexPerPage and maxPage lus.setNumPerPage(indexPerPage); lus.setMax(maxPage); // Check refresh or get more - Integer[] flag = getPackage.flag; - int getPageIndex = flag[0]; - boolean setPositon = flag[1] != 0; + int getPageIndex = getPackage.targetListUrls.getPage(); + boolean setPositon = getPackage.isSetPosition; if (getPageIndex == 0 && firstPage == 0) { // Refresh firstPage = 0; lastPage = 0; @@ -989,6 +981,11 @@ public class MangaListActivity extends SlidingActivity { } } } + //handlePullList + if (getPackage.getType == PULL_HEADER) + pullListView.onHeaderRefreshComplete(); + else if (getPackage.getType == ACTION_FOOTER) + pullListView.onFooterRefreshComplete(true); // Reset pull string setHeaderPullString(); } @@ -996,25 +993,21 @@ public class MangaListActivity extends SlidingActivity { @Override public void onFailure(Object checkFlag, int errorMessageId) { MangaListGetPackage getPackage = (MangaListGetPackage)checkFlag; - if (!getPackage.listUrls.equals(lus)) + if (!getPackage.targetListUrls.equals(lus)) return; - pullListView.onRefreshComplete(); - // Check pull list view later // Check fresh view later - waitView.setVisibility(View.GONE); - noFoundView.setVisibility(View.GONE); - sadpanda.setVisibility(View.GONE); + waitView.setVisibility(View.INVISIBLE); + noFoundView.setVisibility(View.INVISIBLE); + sadpanda.setVisibility(View.INVISIBLE); - Integer[] flag = getPackage.flag; - int getPageIndex = flag[0]; + int getPageIndex = getPackage.targetListUrls.getPage(); if (getPageIndex == 0 && firstPage == 0) { // Refresh mLoadListOver = false; // Only show freshButton - pullListView.setMode(Mode.DISABLED); freshButton.setVisibility(View.VISIBLE); Toast.makeText(MangaListActivity.this, getString(errorMessageId), Toast.LENGTH_SHORT) @@ -1023,13 +1016,18 @@ public class MangaListActivity extends SlidingActivity { gmlAdapter.notifyDataSetChanged(); } else {// List is not empty // Only show freshButton - pullListView.setMode(Mode.BOTH); - freshButton.setVisibility(View.GONE); + freshButton.setVisibility(View.INVISIBLE); Toast.makeText( MangaListActivity.this, getString(errorMessageId) + " " + getString(R.string.em_retry), Toast.LENGTH_SHORT).show(); } + + //handlePullList + if (getPackage.getType == PULL_HEADER) + pullListView.onHeaderRefreshComplete(); + else if (getPackage.getType == ACTION_FOOTER) + pullListView.onFooterRefreshComplete(false); } } @@ -1038,7 +1036,7 @@ public class MangaListActivity extends SlidingActivity { @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - + pullListView.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); // First time when list is created if (mListFirst && mLoadListOver) { // load image @@ -1073,7 +1071,6 @@ public class MangaListActivity extends SlidingActivity { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { - // When srcoll over load image in view int getChildCount; if (scrollState == SCROLL_STATE_IDLE @@ -1140,18 +1137,24 @@ public class MangaListActivity extends SlidingActivity { } private void setHeaderPullString() { - LoadingLayoutProxy ill = (LoadingLayoutProxy)pullListView.getLoadingLayoutProxy(); if (firstPage == 0) - ill.setHeaderLabels(getResources().getTextArray(R.array.pull_refresh)); + pullListView.setHeaderString("下拉刷新...", + "释放刷新...", + "正在刷新...", + "刷新完成", + "取消刷新"); // TODO else - ill.setHeaderLabels(getResources().getTextArray(R.array.pull_pre)); + pullListView.setHeaderString("下拉读取上一页...", + "释放读取上一页...", + "正在读取上一页...", + "读取上一页完成", + "读取上一页刷新"); // TODO } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); - int menuWidth = (int)getResources().getDimension(R.dimen.menu_offset); - mSlidingMenu.setBehindOffset(menuWidth); + mSlidingMenu.setBehindOffsetRes(R.dimen.menu_offset); if (Build.VERSION.SDK_INT >= 19) { BeautifyScreen.fixColour(this); } @@ -1185,7 +1188,7 @@ public class MangaListActivity extends SlidingActivity { int page = intent.getIntExtra("page", 0); lus = new ListUrls(type, search, page); visiblePage = lus.getPage(); - + // Init dialog checkLoginDialog = createCheckLoginDialog(); loginDialog = createLoginDialog(); @@ -1196,22 +1199,19 @@ public class MangaListActivity extends SlidingActivity { mSlidingMenu = getSlidingMenu(); mSlidingMenu.setMode(SlidingMenu.LEFT_RIGHT); mSlidingMenu.setTouchModeAbove(SlidingMenu.TOUCHMODE_FULLSCREEN); - mSlidingMenu.setBehindOffsetRes(R.dimen.slidingmenu_offset); - mSlidingMenu.setFadeDegree(0.35f); - int menuWidth = (int)getResources().getDimension(R.dimen.menu_offset); - mSlidingMenu.setBehindOffset(menuWidth); + setSlidingActionBarEnabled(false); + mSlidingMenu.setBehindOffsetRes(R.dimen.menu_offset); setBehindContentView(R.layout.list_menu_list); mSlidingMenu.setSecondaryMenu(R.layout.list_menu_login); - + getActionBar().setDisplayHomeAsUpEnabled(true); - + // Get View mainView = (RelativeLayout) findViewById(R.id.list_main); listMenu = (TagListView) findViewById(R.id.list_menu_list); isExhentaiList = (ListView)findViewById(R.id.is_exhentai); - //loginMenu = (LinearLayout) findViewById(R.id.list_menu_login); - pullListView = (PullToRefreshListView) findViewById(R.id.list_list); - listView = pullListView.getRefreshableView(); + pullListView = ((PullListView)findViewById(R.id.list_list)); + listView = pullListView.getListView(); waitView = (View) findViewById(R.id.list_wait_first); freshButton = (Button) findViewById(R.id.list_refresh); noFoundView = (View) findViewById(R.id.list_no_found); @@ -1246,7 +1246,7 @@ public class MangaListActivity extends SlidingActivity { int position, long arg3) { if (position == 0) { // Home page lus = new ListUrls(ListUrls.ALL_TYPE, null, 0); - refresh(); + refresh(OTHER); showContent(); } else if (position == 1) { // Favourite Intent intent = new Intent(MangaListActivity.this, @@ -1264,7 +1264,7 @@ public class MangaListActivity extends SlidingActivity { ListUrls listUrls = Tag.get(listMenuTitle.get(position)); if (listUrls != null) { lus = listUrls; - refresh(); + refresh(OTHER); } showContent(); } @@ -1333,7 +1333,7 @@ public class MangaListActivity extends SlidingActivity { if (isChanged) { isExhentaiListAdapter.notifyDataSetChanged(); lus = new ListUrls(ListUrls.ALL_TYPE, null, 0); - refresh(); + refresh(OTHER); MangaListActivity.this.showContent(); } } @@ -1341,44 +1341,35 @@ public class MangaListActivity extends SlidingActivity { // Pull list view - pullListView.setOnRefreshListener(new OnRefreshListener2() { + pullListView.setOnRefreshListener(new OnHeaderRefreshListener() { @Override - public void onPullDownToRefresh( - PullToRefreshBase refreshView) { + public void onHeaderRefresh() { if (firstPage == 0) - refresh(); + refresh(PULL_HEADER); else { mListFirst = true; mLoadListOver = false; lus.setPage(firstPage - 1); EhClient.getManagaList(lus.getUrl(), - new MangaListGetPackage(lus.clone(), new Integer[]{lus.getPage(), 0}), + new MangaListGetPackage(lus.clone(), false, PULL_HEADER), new MangaListGetListener()); } } - @Override - public void onPullUpToRefresh( - PullToRefreshBase refreshView) { - if (lastPage >= lus.getMax() - 1) { - Toast.makeText(MangaListActivity.this, getString(R.string.last_page), Toast.LENGTH_SHORT).show(); - pullListView.onRefreshComplete(); - } else { - mListFirst = true; - mLoadListOver = false; - lus.setPage(lastPage + 1); - EhClient.getManagaList(lus.getUrl(), - new MangaListGetPackage(lus.clone(), new Integer[]{lus.getPage(), 0}), - new MangaListGetListener()); - } - - } }); - - LoadingLayoutProxy ill = (LoadingLayoutProxy)pullListView.getLoadingLayoutProxy(); - ill.setHeaderLabels(getResources().getTextArray(R.array.pull_refresh)); - ill.setFooterLabels(getResources().getTextArray(R.array.pull_next)); - pullListView.setMode(Mode.DISABLED); - + pullListView.setOnFooterRefreshListener(new OnFooterRefreshListener() { + @Override + public void onFooterRefresh() { + mListFirst = true; + mLoadListOver = false; + if (lus.setPage(lastPage + 1)) + EhClient.getManagaList(lus.getUrl(), + new MangaListGetPackage(lus.clone(), false, ACTION_FOOTER), + new MangaListGetListener()); + else + pullListView.onFooterRefreshComplete(true, false); + } + }); + pullListView.setFooterString("正在加载更多", "已全部加载完成,点击尝试加载更多", "加载失败,点击继续加载"); // TODO // Listview gmlAdapter = new GmlAdapter(); listView.setAdapter(gmlAdapter); @@ -1387,10 +1378,6 @@ public class MangaListActivity extends SlidingActivity { @Override public void onItemClick(AdapterView arg0, View arg1, int position, long arg3) { - if (position > 0) - position = position-1; - else - position = 0; Intent intent = new Intent(MangaListActivity.this, MangaDetailActivity.class); ListMangaDetail lmd = lmdArray.get(position); @@ -1418,10 +1405,7 @@ public class MangaListActivity extends SlidingActivity { @Override public boolean onItemLongClick(AdapterView arg0, View arg1, int position, long arg3) { - if (position > 0) - longClickItemIndex = position-1; - else - longClickItemIndex = 0; + longClickItemIndex = position; longClickDialog.show(); return true; } @@ -1457,7 +1441,7 @@ public class MangaListActivity extends SlidingActivity { public boolean onQueryTextSubmit(String query) { searchView.clearFocus(); lus = new ListUrls(ListUrls.ALL_TYPE, query); - refresh(); + refresh(OTHER); return true; } }); @@ -1509,7 +1493,7 @@ public class MangaListActivity extends SlidingActivity { return true; case R.id.action_refresh: lus.setPage(0); - refresh(); + refresh(OTHER); return true; case R.id.action_jump: if (mLoadListOver) @@ -1539,33 +1523,103 @@ public class MangaListActivity extends SlidingActivity { setTitle(String.format(getString(R.string.some_page), visiblePage + 1)); layoutDrawRight(); - + + pullListView.setVisibility(View.INVISIBLE); waitView.setVisibility(View.VISIBLE); - pullListView.setMode(Mode.DISABLED); - freshButton.setVisibility(View.GONE); - noFoundView.setVisibility(View.GONE); - sadpanda.setVisibility(View.GONE); - + freshButton.setVisibility(View.INVISIBLE); + noFoundView.setVisibility(View.INVISIBLE); + sadpanda.setVisibility(View.INVISIBLE); + + // Check update + //checkupdate(); + // get MangeList lus.setPage(0); EhClient.getManagaList(lus.getUrl(), - new MangaListGetPackage(lus.clone(), new Integer[]{lus.getPage(), 1}), + new MangaListGetPackage(lus.clone(), true, OTHER), new MangaListGetListener()); } + + private class UpdateListener implements Downloader.OnDownloadListener { + + private NotificationManager mNotifyManager; + private NotificationCompat.Builder mBuilder; + + public UpdateListener() { + mNotifyManager = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + mBuilder = new NotificationCompat.Builder(MangaListActivity.this.getApplication()); + mBuilder.setSmallIcon(R.drawable.ic_launcher); + } + + @Override + public void onDownloadStart(int totalSize) { + mBuilder.setContentTitle("正在下载更新") + .setContentText(null) + .setProgress(0, 0, true).setOngoing(true).setAutoCancel(false); + mNotifyManager.notify(233, mBuilder.build()); + } + @Override + public void onDownloadStatusUpdate( + int downloadSize, int totalSize) { + mBuilder.setContentTitle("正在下载更新") + .setContentText(String.format("%.2f / %.2f KB", downloadSize/1024.0f, totalSize/1024.0f)) + .setProgress(totalSize, downloadSize, false).setOngoing(true).setAutoCancel(false); + mNotifyManager.notify(233, mBuilder.build()); + + } + @Override + public void onDownloadOver(boolean ok, int eMesgId) { + mBuilder.setContentTitle("更新下载完成") + .setContentText("点击更新") + .setProgress(0, 0, false).setOngoing(false).setAutoCancel(true); + mNotifyManager.notify(233, mBuilder.build()); + + } + } + + private void checkupdate() { + EhClient.checkUpdate(new EhClient.OnCheckUpdateListener() { + @Override + public void onSuccess(String pageContext) { + String[] items = pageContext.split("\n"); + if (items.length == 2) { + String newVer = items[0]; + String url = EhClient.UPDATE_URL + items[1]; + + String name = url.substring(url.lastIndexOf('/')+1); + + Downloader d = new Downloader(); + try { + Downloader.Controlor controlor = d.resetData(Config.getDownloadPath(), name, url); + d.setOnDownloadListener(new UpdateListener()); + new Thread(d).start(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + } + @Override + public void onFailure(int errorMessageId) { + Log.d(TAG, getString(errorMessageId)); + } + }); + } + private void layoutDrawRight() { if (EhClient.isLogin()) { // If have login - loginView.setVisibility(View.GONE); + loginView.setVisibility(View.INVISIBLE); loginOverView.setVisibility(View.VISIBLE); usernameText.setText(EhClient.getUsername()); } else { loginView.setVisibility(View.VISIBLE); - loginOverView.setVisibility(View.GONE); + loginOverView.setVisibility(View.INVISIBLE); } } - private void refresh() { + private void refresh(int getType) { mListFirst = true; mLoadListOver = false; @@ -1573,21 +1627,15 @@ public class MangaListActivity extends SlidingActivity { lastPage = 0; // Only ProgressBar in center - if (pullListView.isRefreshing()) { - waitView.setVisibility(View.GONE); - pullListView.setMode(Mode.BOTH); - } else { - waitView.setVisibility(View.VISIBLE); - pullListView.setMode(Mode.DISABLED); - } - freshButton.setVisibility(View.GONE); - noFoundView.setVisibility(View.GONE); - sadpanda.setVisibility(View.GONE); + waitView.setVisibility(View.VISIBLE); + freshButton.setVisibility(View.INVISIBLE); + noFoundView.setVisibility(View.INVISIBLE); + sadpanda.setVisibility(View.INVISIBLE); // Get MangeList lus.setPage(0); EhClient.getManagaList(lus.getUrl(), - new MangaListGetPackage(lus.clone(), new Integer[]{lus.getPage(), 1}), + new MangaListGetPackage(lus.clone(), true, getType), new MangaListGetListener()); } @@ -1622,11 +1670,11 @@ public class MangaListActivity extends SlidingActivity { // *** Button onclick ***// public void buttonRefresh(View arg0) { - refresh(); + refresh(OTHER); } public void buttonLogout(View paramView) { - logoutButton.setVisibility(View.GONE); + logoutButton.setVisibility(View.INVISIBLE); waitlogoutView.setVisibility(View.VISIBLE); EhClient.logout(new EhClient.OnLogoutListener() { @Override @@ -1635,7 +1683,7 @@ public class MangaListActivity extends SlidingActivity { getString(R.string.toast_logout_succeeded), Toast.LENGTH_SHORT).show(); logoutButton.setVisibility(View.VISIBLE); - waitlogoutView.setVisibility(View.GONE); + waitlogoutView.setVisibility(View.INVISIBLE); Config.logoutNow(); layoutDrawRight(); } @@ -1646,7 +1694,7 @@ public class MangaListActivity extends SlidingActivity { getString(errorMessageId), Toast.LENGTH_SHORT).show(); logoutButton.setVisibility(View.VISIBLE); - waitlogoutView.setVisibility(View.GONE); + waitlogoutView.setVisibility(View.INVISIBLE); } }); } diff --git a/src/com/hippo/ehviewer/network/Downloader.java b/src/com/hippo/ehviewer/network/Downloader.java index 35a4be4ae..500e698b7 100644 --- a/src/com/hippo/ehviewer/network/Downloader.java +++ b/src/com/hippo/ehviewer/network/Downloader.java @@ -8,6 +8,8 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; +import java.util.List; +import java.util.Map; import android.util.Log; @@ -36,7 +38,7 @@ public class Downloader implements Runnable { private String mPath; private String mFileName; private URL mUrl; - private DownloadInfo mDi; + private Controlor mContorlor; private int mTotalSize; private int mDownloadSize = 0; @@ -48,46 +50,60 @@ public class Downloader implements Runnable { public interface OnDownloadListener { public void onDownloadStart(int totalSize); public void onDownloadStatusUpdate(int downloadSize, int totalSize); + public void onDownloadOver(boolean ok, int eMesgId); } - public void resetData(String path, String fileName, String urlStr, DownloadInfo di) throws MalformedURLException { + public class Controlor { + private boolean mStop = false; + + public synchronized void stop() { + mStop = true; + } + + public synchronized void reset() { + mStop = false; + } + + public synchronized boolean isStop() { + return mStop; + } + } + + public Controlor resetData(String path, String fileName, String urlStr) throws MalformedURLException { mPath = path; mFileName = fileName; mUrl = new URL(urlStr); - mDi = di; mDownloadSize = 0; mRedirectionCount = 0; status = COMPLETED; + + mContorlor.reset(); + return mContorlor; } public void setOnDownloadListener(OnDownloadListener listener) { mListener = listener; } - public Downloader(){} - - public Downloader(String path, String fileName, String urlStr, DownloadInfo di) throws MalformedURLException { - mPath = path; - mFileName = fileName; - mUrl = new URL(urlStr); - mDi = di; + public Downloader() { + mContorlor = new Controlor(); } @Override public void run() { + boolean ok = false; int retryTimes = 0; if (Cache.hasSdCard()) while(true) { try { - if (mDi.status == DownloadInfo.STOP) - throw new StopRequestException(STOP, - "Download is stopped"); - + if (mContorlor.isStop()) + throw new StopRequestException(STOP, "Download is stopped"); mDownloadSize = 0; mRedirectionCount = 0; status = COMPLETED; executeDownload(); + ok = true; break; } catch (StopRequestException e) { e.printStackTrace(); @@ -99,6 +115,9 @@ public class Downloader implements Runnable { } else status = NO_SDCARD; + + if (mListener != null) + mListener.onDownloadOver(ok, status); } public int getStatus() { @@ -112,22 +131,20 @@ public class Downloader implements Runnable { Log.d(TAG, "Get file " + mUrl.toString()); conn = (HttpURLConnection)mUrl.openConnection(); conn.setInstanceFollowRedirects(false); + conn.addRequestProperty("Range", "bytes=0-"); // TODO conn.setConnectTimeout(DEFAULT_TIMEOUT); conn.setReadTimeout(DEFAULT_TIMEOUT); - + conn.connect(); final int responseCode = conn.getResponseCode(); switch (responseCode) { case HttpURLConnection.HTTP_OK: + case HttpURLConnection.HTTP_PARTIAL: processResponseHeaders(conn); if (mListener != null) mListener.onDownloadStart(mTotalSize); transferData(conn); return; - case HttpURLConnection.HTTP_PARTIAL: - transferData(conn); - return; - case HttpURLConnection.HTTP_MOVED_PERM: case HttpURLConnection.HTTP_MOVED_TEMP: case HttpURLConnection.HTTP_SEE_OTHER: @@ -159,6 +176,17 @@ public class Downloader implements Runnable { */ private void processResponseHeaders(HttpURLConnection conn) { mTotalSize = conn.getContentLength(); + String range; + int index; + if (mTotalSize == -1 + && (range = conn.getHeaderField("Content-Range")) != null + && (index = range.lastIndexOf('/')) != -1) { + try { + mTotalSize = Integer.parseInt(range.substring(index + 1)); + } catch (Exception e) { + e.printStackTrace(); + } + } } /** @@ -169,7 +197,7 @@ public class Downloader implements Runnable { OutputStream out = null; File file = null; - if (mDi.status == DownloadInfo.STOP) + if (mContorlor.isStop()) throw new StopRequestException(STOP, "Download is stopped"); @@ -211,7 +239,6 @@ public class Downloader implements Runnable { for (;;) { int bytesRead = readFromResponse(data, in); if (bytesRead == -1) { // success, end of stream already reached - handleEndOfStream(); return; } @@ -220,7 +247,7 @@ public class Downloader implements Runnable { if (mListener != null) mListener.onDownloadStatusUpdate(mDownloadSize, mTotalSize); - if (mDi.status == DownloadInfo.STOP) + if (mContorlor.isStop()) throw new StopRequestException(STOP, "Download is stopped"); } @@ -238,9 +265,6 @@ public class Downloader implements Runnable { return entityStream.read(data); } catch (IOException ex) { // TODO: handle stream errors the same as other retries - if ("unexpected end of stream".equals(ex.getMessage())) { - return -1; - } throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Failed reading response: " + ex, ex); } @@ -263,17 +287,4 @@ public class Downloader implements Runnable { "Failed to write data: " + ex); } } - - /** - * Called when we've reached the end of the HTTP response stream, to update the database and - * check for consistency. - */ - private void handleEndOfStream() throws StopRequestException { - final boolean lengthMismatched = (mTotalSize != -1) - && (mDownloadSize != mTotalSize); - if (lengthMismatched) { - throw new StopRequestException(STATUS_HTTP_DATA_ERROR, - "mismatched content length; unable to resume"); - } - } } diff --git a/src/com/hippo/ehviewer/service/DownloadService.java b/src/com/hippo/ehviewer/service/DownloadService.java index 194a407ea..9460a93db 100644 --- a/src/com/hippo/ehviewer/service/DownloadService.java +++ b/src/com/hippo/ehviewer/service/DownloadService.java @@ -88,12 +88,16 @@ public class DownloadService extends Service { @Override public void onDownloadMangaOver(String id, boolean ok) { + String mesg = null; if (ok) - mBuilder.setContentTitle(getString(R.string.download_successfully) + " " + Download.get(id).title); + mesg = getString(R.string.download_successfully) + " "; else - mBuilder.setContentTitle(getString(R.string.download_unsuccessfully) + " " + Download.get(id).title); - mBuilder.setContentText(null).setProgress(0, 0, false).setOngoing(false) - .setAutoCancel(true); + mesg = getString(R.string.download_unsuccessfully) + " "; + DownloadInfo di = Download.get(id); + if (di != null) + mesg += Download.get(id).title; + mBuilder.setContentTitle(mesg); + mBuilder.setContentText(null).setProgress(0, 0, false).setOngoing(false); mNotifyManager.notify(Integer.parseInt(id), mBuilder.build()); } diff --git a/src/com/hippo/ehviewer/util/EhClient.java b/src/com/hippo/ehviewer/util/EhClient.java index 4360e5c1a..9cdedacc7 100644 --- a/src/com/hippo/ehviewer/util/EhClient.java +++ b/src/com/hippo/ehviewer/util/EhClient.java @@ -48,6 +48,9 @@ import com.hippo.ehviewer.service.DownloadService; import android.app.NotificationManager; import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Movie; @@ -60,6 +63,8 @@ import android.support.v4.app.NotificationCompat; import android.support.v4.util.LruCache; import android.util.Log; +// TODO Stringbuffer to StringBuild + public class EhClient { private static final String TAG = "EhClient"; @@ -73,6 +78,9 @@ public class EhClient { public static final String E_HENTAI_DETAIL_HEADER = "http://g.e-hentai.org/g/"; public static final String EXHENTAI_DETAIL_HEADER = "http://exhentai.org/g/"; + public static final String UPDATE_URL = "http://ehviewersu.appsp0t.com/"; + public static final String UPDATE_API = "http://ehviewersu.appsp0t.com/API"; + public static String listHeader; public static String detailHeader; @@ -93,6 +101,11 @@ public class EhClient { public void onFailure(int errorMessageId); } + public interface OnCheckUpdateListener { + public void onSuccess(String pageContext); + public void onFailure(int errorMessageId); + } + public interface OnLogoutListener { public void onSuccess(); public void onFailure(int errorMessageId); @@ -173,17 +186,6 @@ public class EhClient { return mInit; } - private static Handler checkNetworkHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - OnCheckNetworkListener listener = (OnCheckNetworkListener) msg.obj; - if (msg.arg1 == 0) - listener.onFailure(msg.what); - else - listener.onSuccess(); - }; - }; - public static void setHeader(boolean isExhentai) { Config.exhentai(isExhentai); if (isExhentai) { @@ -195,6 +197,102 @@ public class EhClient { } } + private static Handler checkUpdateHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + String pageContext = ((CheckUpdatePackage)msg.obj).pageContext; + OnCheckUpdateListener listener = ((CheckUpdatePackage)msg.obj).listener; + if (msg.arg1 == 0) + listener.onFailure(msg.what); + else + listener.onSuccess(pageContext); + }; + }; + + // Get Manga List + private static class CheckUpdatePackage { + public String pageContext; + public OnCheckUpdateListener listener; + + public CheckUpdatePackage(String pageContext, OnCheckUpdateListener listener) { + this.pageContext = pageContext; + this.listener = listener; + } + } + + public static void checkUpdate(final OnCheckUpdateListener listener) { + new Thread(new Runnable() { + @Override + public void run() { + int mOK = 0; + int emsgId = R.string.em_unknown_error; + StringBuffer sb = new StringBuffer(); + HttpURLConnection conn = null; + try { + URL url = new URL(UPDATE_API); + conn = (HttpURLConnection) url.openConnection(); + conn.addRequestProperty("Accept-Encoding", "gzip"); + conn.setRequestProperty("User-Agent", userAgent); + conn.setConnectTimeout(TIMEOUT); + conn.setReadTimeout(TIMEOUT); + conn.setDoOutput(true); + conn.setDoInput(true); + conn.setRequestMethod("POST"); + conn.setUseCaches(false); + conn.setInstanceFollowRedirects(true); + + DataOutputStream out = new DataOutputStream(conn.getOutputStream()); + StringBuffer StrBuf = new StringBuffer(); + PackageManager pm = mContext.getPackageManager(); + PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES); + StrBuf.append("last\n").append(pi.versionName); + out.writeBytes(StrBuf.toString()); + out.flush(); + out.close(); + + conn.connect(); + + getStringHUC(conn, sb); + mOK = 1; + } catch (MalformedURLException e) { + emsgId = R.string.em_url_format_error; + e.printStackTrace(); + } catch (ConnectTimeoutException e) { + emsgId = R.string.em_timeout; + e.printStackTrace(); + } catch (UnknownHostException e) { + emsgId = R.string.em_no_network_2; + e.printStackTrace(); + } catch (IOException e) { + emsgId = R.string.em_network_error; + e.printStackTrace(); + } catch (NameNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + if (conn != null) + conn.disconnect(); + } + Message msg = new Message(); + msg.what = emsgId; + msg.arg1 = mOK; + msg.obj = new CheckUpdatePackage(sb.toString(), listener); + checkUpdateHandler.sendMessage(msg); + } + }).start(); + } + + private static Handler checkNetworkHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + OnCheckNetworkListener listener = (OnCheckNetworkListener) msg.obj; + if (msg.arg1 == 0) + listener.onFailure(msg.what); + else + listener.onSuccess(); + }; + }; + /** * Is network available and can reach host * @return Error message id or 0 @@ -1190,6 +1288,7 @@ public class EhClient { /****** DownloadMangaManager ******/ public static class DownloadMangaManager { private static DownloadInfo curDownloadInfo = null; + private static Downloader.Controlor curControlor = null; private static ArrayList mDownloadQueue = new ArrayList(); private static Object taskLock = new Object(); @@ -1232,6 +1331,8 @@ public class EhClient { di.status = DownloadInfo.STOP; mDownloadQueue.remove(di); if (!id.equals(getCurDownloadId())) { + if (curControlor != null) + curControlor.stop(); mService.notifyUpdate(); Download.notify(id); } @@ -1287,6 +1388,9 @@ public class EhClient { curDownloadInfo.totalSize = totalSize/1024.0f; mService.notifyUpdate(); } + + @Override + public void onDownloadOver(boolean ok, int eMesgId) {} }); while(mDownloadQueue.size() > 0){ synchronized (taskLock) { @@ -1354,9 +1458,10 @@ public class EhClient { } try { - imageDownloader.resetData(folder.toString(), + // TODO + curControlor = imageDownloader.resetData(folder.toString(), String.format("%05d", curDownloadInfo.lastStartIndex) + "." + Util.getExtension(imageUrlStr), - imageUrlStr, curDownloadInfo); + imageUrlStr); imageDownloader.run(); curDownloadInfo.downloadSize = 0; diff --git a/src/com/hippo/ehviewer/util/Ui.java b/src/com/hippo/ehviewer/util/Ui.java index cc4d31b44..a721ef1de 100644 --- a/src/com/hippo/ehviewer/util/Ui.java +++ b/src/com/hippo/ehviewer/util/Ui.java @@ -74,8 +74,8 @@ public class Ui { BITMAP_START_HALF = BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.star_half), null, opt); BITMAP_START_EMPTY = BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.star_empty), null, opt); - OlImageView.setDefaultImage(Movie.decodeStream(mContext.getResources().openRawResource(R.drawable.wait)) - , BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.refresh), null, opt)); + OlImageView.setDefaultImage(BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.ic_launcher)) + , BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.ic_touch), null, opt)); } /** diff --git a/src/com/hippo/ehviewer/view/OlImageView.java b/src/com/hippo/ehviewer/view/OlImageView.java index 926cdc7df..e949334f1 100644 --- a/src/com/hippo/ehviewer/view/OlImageView.java +++ b/src/com/hippo/ehviewer/view/OlImageView.java @@ -30,7 +30,7 @@ public class OlImageView extends SuperImageView { private LruCache memoryCache = null; private DiskCache diskCache = null; - private static Movie waitMovie; + private static Bitmap waitBitmap; private static Bitmap refreshBitmap; private OnLoadListener onLoadListener = null; @@ -56,7 +56,7 @@ public class OlImageView extends SuperImageView { } public void setWaitMovie() { - setImageMovie(waitMovie); + setImageBitmap(waitBitmap); } public void setUrl(String s) { @@ -115,8 +115,7 @@ public class OlImageView extends SuperImageView { OlImageView.this.setImageMovie((Movie) res); return true; } - if(!verifyMovie(waitMovie)) - setImageMovie(waitMovie); + setImageBitmap(waitBitmap); return false; } } @@ -154,8 +153,7 @@ public class OlImageView extends SuperImageView { private void load() { status = STATUS_LOADING; - if(!verifyMovie(waitMovie)) - setImageMovie(waitMovie); + setImageBitmap(waitBitmap); int type = Util.getResourcesType(url); synchronized (this) { @@ -179,9 +177,9 @@ public class OlImageView extends SuperImageView { OlImageView.this.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { - OlImageView.this.setImageMovie(waitMovie); - OlImageView.this.loadImage(true); - OlImageView.this.setClickable(false); + setImageBitmap(waitBitmap); + loadImage(true); + setClickable(false); } }); OlImageView.this.status = STATUS_LOAD_FAIL; @@ -192,8 +190,8 @@ public class OlImageView extends SuperImageView { } } - public static void setDefaultImage(Movie waitMovie, Bitmap refreshBitmap) { - OlImageView.waitMovie = waitMovie; + public static void setDefaultImage(Bitmap waitBitmap, Bitmap refreshBitmap) { + OlImageView.waitBitmap = waitBitmap; OlImageView.refreshBitmap = refreshBitmap; } diff --git a/src/com/hippo/ehviewer/widget/PullListView.java b/src/com/hippo/ehviewer/widget/PullListView.java new file mode 100644 index 000000000..47d24f24e --- /dev/null +++ b/src/com/hippo/ehviewer/widget/PullListView.java @@ -0,0 +1,564 @@ +package com.hippo.ehviewer.widget; + +import com.hippo.ehviewer.R; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.LinearInterpolator; +import android.view.animation.RotateAnimation; +import android.view.animation.Transformation; +import android.widget.AbsListView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; + +public class PullListView extends LinearLayout implements AbsListView.OnScrollListener{ + @SuppressWarnings("unused") + private static final String TAG = "PullListView"; + + private final static int HEADER_PULL_TO_REFRESH = 0; + private final static int HEADER_RELEASE_TO_REFRESH = 1; + private final static int HEADER_REFRESHING = 2; + private final static int HEADER_DONE = 3; + private final static int HEADER_CANCEL = 4; + + private final static int FOOTER_REFRESHING = 0; + private final static int FOOTER_SUCCESS = 1; + private final static int FOOTER_FAIL = 2; + + // arrow animation + private RotateAnimation mAnimation; + private RotateAnimation mReverseAnimation; + + // Header animation + private Animation mRefreshAnimation; + private Animation mOverAnimation; + + private ListView mListView; + private LinearLayout mHeader; + private LinearLayout mFooter; + private TextView mTipTextView; + private ImageView mArrowImageView; + private ProgressBar mProgressBar; + + private TextView mFooterTipTextView; + private ProgressBar mFooterProgressBar; + + // Make sure only one pull event + private boolean isHeaderRecored = false; + private boolean isFooterRecored = false; + + // Header String to show + private String mHeaderPullStr; + private String mHeaderReleaseStr; + private String mHeaderRefreshStr; + private String mHeaderDoneStr; + private String mHeaderCancelStr; + + // Footer String to show + private String mFooterRefreshStr; + private String mFooterSuccessStr; + private String mFooterFailStr; + + // Header + private int headerHeight; + private int headerOriginalLeftPadding; + private int headerOriginalTopPadding; + private int headerOriginalRightPadding; + private int headerOriginalBottomPadding; + private int releaseTopPadding; + private int doneTopPadding; + private int pullThreshold; + private int pullOffestMax; + + // Footer + private boolean isActionWhenShow = true; + + private int startY; + private int headerState = HEADER_DONE; + private boolean isBack; + + private int footerState = FOOTER_SUCCESS; + + private OnHeaderRefreshListener headerRefreshListener; + private OnFooterRefreshListener footerRefreshListener; + + // To Check scroll to top + // Store y of last point + private int lastY; + + private boolean mHeaderEnabled = true; + private boolean mFooterEnabled = true; + + public interface OnHeaderRefreshListener { + public void onHeaderRefresh(); + } + + public interface OnFooterRefreshListener { + public void onFooterRefresh(); + } + + public PullListView(Context context) { + super(context); + init(context); + } + + public PullListView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + public PullListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + // Set arrow animation + mAnimation = new RotateAnimation(0, -180, + RotateAnimation.RELATIVE_TO_SELF, 0.5f, + RotateAnimation.RELATIVE_TO_SELF, 0.5f); + mAnimation.setInterpolator(new LinearInterpolator()); + mAnimation.setDuration(100); + mAnimation.setFillAfter(true); + + mReverseAnimation = new RotateAnimation(-180, 0, + RotateAnimation.RELATIVE_TO_SELF, 0.5f, + RotateAnimation.RELATIVE_TO_SELF, 0.5f); + mReverseAnimation.setInterpolator(new LinearInterpolator()); + mReverseAnimation.setDuration(100); + mReverseAnimation.setFillAfter(true); + + // Set header animation + mRefreshAnimation = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + mHeader.setPadding(headerOriginalLeftPadding, + (int)((1 - interpolatedTime) * releaseTopPadding + (interpolatedTime * headerOriginalTopPadding)), + headerOriginalRightPadding, headerOriginalBottomPadding); + mHeader.invalidate(); + } + }; + mRefreshAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + setEnabled(false); + } + + @Override + public void onAnimationEnd(Animation animation) { + setEnabled(true); + } + @Override + public void onAnimationRepeat(Animation animation) {} + }); + mRefreshAnimation.setDuration(100); + + mOverAnimation = new Animation() { + @Override + protected void applyTransformation(float interpolatedTime, Transformation t) { + mHeader.setPadding(headerOriginalLeftPadding, + (int)((1 - interpolatedTime) * doneTopPadding - (interpolatedTime * headerHeight)), + headerOriginalRightPadding, headerOriginalBottomPadding); + mHeader.invalidate(); + } + }; + mOverAnimation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + setEnabled(false); + } + + @Override + public void onAnimationEnd(Animation animation) { + setEnabled(true); + if (headerState == HEADER_CANCEL) { + headerState = HEADER_DONE; + isHeaderRecored = false; + isBack = false; + } + } + @Override + public void onAnimationRepeat(Animation animation) {} + }); + mOverAnimation.setDuration(200); + + View view = LayoutInflater.from(context).inflate(R.layout.pull_list_view, this); + mFooter = (LinearLayout)LayoutInflater.from(context).inflate(R.layout.pull_list_view_footer, null); + mFooter.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + footerRefresh(); + } + }); + mListView = (ListView)view.findViewById(R.id.list_view); + mListView.setOnScrollListener(this); + mListView.addFooterView(mFooter); + mFooterTipTextView = (TextView)mFooter.findViewById(R.id.footer_tip_text); + mFooterProgressBar = (ProgressBar)mFooter.findViewById(R.id.footer_progressBar); + + mHeader = (LinearLayout)view.findViewById(R.id.header); + mArrowImageView = (ImageView) mHeader.findViewById(R.id.header_arrow_image); + mArrowImageView.setMinimumWidth(50); + mArrowImageView.setMinimumHeight(50); + mProgressBar = (ProgressBar)mHeader.findViewById(R.id.header_progressBar); + mTipTextView = (TextView)mHeader.findViewById(R.id.header_tip_text); + + headerOriginalLeftPadding = mHeader.getPaddingLeft(); + headerOriginalTopPadding = mHeader.getPaddingTop(); + headerOriginalRightPadding = mHeader.getPaddingRight(); + headerOriginalBottomPadding = mHeader.getPaddingBottom(); + + measureView(mHeader); + headerHeight = mHeader.getMeasuredHeight(); + pullThreshold = headerHeight; + pullOffestMax = (int)(headerHeight * 2.3f); + + mHeader.setPadding(headerOriginalLeftPadding, -headerHeight, headerOriginalRightPadding, headerOriginalBottomPadding); + mHeader.invalidate(); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + // TODO Auto-generated method stub + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, + int visibleItemCount, int totalItemCount) { + if (firstVisibleItem + visibleItemCount == totalItemCount + && isActionWhenShow && totalItemCount > 1) + footerRefresh(); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + if (!mHeaderEnabled || isHeaderRecored || isFooterRecored) + return false; + + final int action = event.getAction() & MotionEvent.ACTION_MASK; + + switch (action) { + case MotionEvent.ACTION_DOWN: + lastY = (int)event.getY(); + return false; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + return false; + + case MotionEvent.ACTION_MOVE: + int y = (int)event.getY(); + int offsetY = y - lastY; + lastY = y; + + if (offsetY > 0 && isListAtTop()) + return true; + else + return false; + + default: + return false; + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final int action = event.getAction() & MotionEvent.ACTION_MASK; + + switch (action) { + case MotionEvent.ACTION_DOWN: + if (mHeaderEnabled && !isHeaderRecored && !isFooterRecored) { + isHeaderRecored = true; + startY = (int) event.getY(); + } + break; + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + if (headerState == HEADER_PULL_TO_REFRESH) { + headerState = HEADER_CANCEL; + changeHeaderViewByState(); + } + else if (headerState == HEADER_RELEASE_TO_REFRESH) { + headerState = HEADER_REFRESHING; + changeHeaderViewByState(); + onHeaderRefresh(); + } else if (headerState == HEADER_DONE) { + isHeaderRecored = false; + isBack = false; + changeHeaderViewByState(); + } + break; + + case MotionEvent.ACTION_MOVE: + int tempY = (int)event.getY(); + if (mHeaderEnabled && !isHeaderRecored && !isFooterRecored) { + isHeaderRecored = true; + startY = tempY; + } + if (headerState != HEADER_REFRESHING && isHeaderRecored) { + int offsetY = getShowOffset(tempY - startY, pullOffestMax); + if (headerState == HEADER_RELEASE_TO_REFRESH) { + // 往上推,推到屏幕足够掩盖head的程度,但还没有全部掩盖 + if (offsetY < pullThreshold && offsetY > 0) { + headerState = HEADER_PULL_TO_REFRESH; + changeHeaderViewByState(); + } + // 一下子推到顶 + else if (offsetY <= 0) { + headerState = HEADER_DONE; + startY = tempY; + } + } else if (headerState == HEADER_PULL_TO_REFRESH) { + // 下拉到可以进入RELEASE_TO_REFRESH的状态 + if (offsetY >= pullThreshold) { + headerState = HEADER_RELEASE_TO_REFRESH; + isBack = true; + changeHeaderViewByState(); + } + // 上推到顶了 + else if (offsetY <= 0) { + headerState = HEADER_DONE; + startY = tempY; + } + } else if (headerState == HEADER_DONE) { + if (offsetY > 0) { + headerState = HEADER_PULL_TO_REFRESH; + changeHeaderViewByState(); + } else { + startY = tempY; + } + } + + // 更新headView的size + if (headerState == HEADER_PULL_TO_REFRESH) { + int topPadding = (int)(offsetY - headerHeight); + mHeader.setPadding(headerOriginalLeftPadding, topPadding, headerOriginalRightPadding, headerOriginalBottomPadding); + mHeader.invalidate(); + } + + // 更新headView的paddingTop + if (headerState == HEADER_RELEASE_TO_REFRESH) { + int topPadding = (int)(offsetY - headerHeight); + mHeader.setPadding(headerOriginalLeftPadding, topPadding, headerOriginalRightPadding, headerOriginalBottomPadding); + mHeader.invalidate(); + } + } + break; + } + return true; + } + + private int getShowOffset(int eventOffset, int target) { + if (eventOffset <= 0) // avoid divide zero + return 0; + else + return (target * eventOffset) / (eventOffset + target); + } + + // Refresh header UI + private void changeHeaderViewByState() { + switch (headerState) { + case HEADER_RELEASE_TO_REFRESH: + mArrowImageView.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.GONE); + mTipTextView.setVisibility(View.VISIBLE); + mArrowImageView.clearAnimation(); + mArrowImageView.startAnimation(mAnimation); + mTipTextView.setText(mHeaderReleaseStr); + break; + case HEADER_PULL_TO_REFRESH: + mProgressBar.setVisibility(View.GONE); + mTipTextView.setVisibility(View.VISIBLE); + mArrowImageView.clearAnimation(); + mArrowImageView.setVisibility(View.VISIBLE); + if (isBack) { + isBack = false; + mArrowImageView.clearAnimation(); + mArrowImageView.startAnimation(mReverseAnimation); + } + mTipTextView.setText(mHeaderPullStr); + break; + case HEADER_REFRESHING: + releaseTopPadding = mHeader.getPaddingTop(); + mHeader.startAnimation(mRefreshAnimation); + mProgressBar.setVisibility(View.VISIBLE); + mArrowImageView.clearAnimation(); + mArrowImageView.setVisibility(View.GONE); + mTipTextView.setText(mHeaderRefreshStr); + break; + case HEADER_DONE: + case HEADER_CANCEL: + doneTopPadding = mHeader.getPaddingTop(); + mHeader.startAnimation(mOverAnimation); + mProgressBar.setVisibility(View.GONE); + mArrowImageView.setVisibility(View.GONE); + mArrowImageView.clearAnimation(); + // 此处更换图标 + mArrowImageView.setImageResource(R.drawable.ic_pulltorefresh_arrow); + if (headerState == HEADER_DONE) + mTipTextView.setText(mHeaderDoneStr); + else + mTipTextView.setText(mHeaderCancelStr); + break; + } + } + + // Refresh footer UI + private void changeFooterViewByState() { + switch (footerState) { + case FOOTER_REFRESHING: + mFooterTipTextView.setText(mFooterRefreshStr); + mFooterProgressBar.setVisibility(View.VISIBLE); + break; + case FOOTER_SUCCESS: + mFooterTipTextView.setText(mFooterSuccessStr); + mFooterProgressBar.setVisibility(View.GONE); + break; + case FOOTER_FAIL: + mFooterTipTextView.setText(mFooterFailStr); + mFooterProgressBar.setVisibility(View.GONE); + break; + } + } + + public ListView getListView() { + return mListView; + } + + // Header + public void setHeaderEnabled(boolean enabled) { + mHeaderEnabled = enabled; + } + + public void setHeaderString(String pullStr, String releaseStr, String refreshStr, String doneStr, String cancelStr) { + mHeaderPullStr = pullStr; + mHeaderReleaseStr = releaseStr; + mHeaderRefreshStr = refreshStr; + mHeaderDoneStr = doneStr; + mHeaderCancelStr = cancelStr; + } + + public void clickHeaderRefresh() { + if (!mHeaderEnabled || isHeaderRecored || isFooterRecored) + return; + + isHeaderRecored = true; + headerState = HEADER_REFRESHING; + //mListView.setSelection(0); + changeHeaderViewByState(); + onHeaderRefresh(); + } + + public void setOnRefreshListener(OnHeaderRefreshListener refreshListener) { + this.headerRefreshListener = refreshListener; + } + + private void onHeaderRefresh() { + if (headerRefreshListener != null) { + headerRefreshListener.onHeaderRefresh(); + } + } + + public boolean isHeaderRefreshing() { + return headerState == HEADER_REFRESHING; + } + + public void onHeaderRefreshComplete() { + headerState = HEADER_DONE; + isHeaderRecored = false; + isBack = false; + changeHeaderViewByState(); + } + + // Footer + public void setFooterEnabled(boolean enabled) { + // TODO Remove footer if false + mFooterEnabled = enabled; + } + + public void setFooterString(String refreshStr, String successStr, String failStr) { + mFooterRefreshStr = refreshStr; + mFooterSuccessStr = successStr; + mFooterFailStr = failStr; + changeFooterViewByState(); + } + + public void footerRefresh() { + if (!mHeaderEnabled || isHeaderRecored || isFooterRecored) + return; + + isFooterRecored = true; + footerState = FOOTER_REFRESHING; + changeFooterViewByState(); + + if (footerRefreshListener != null) { + footerRefreshListener.onFooterRefresh(); + } + } + + public void setOnFooterRefreshListener(OnFooterRefreshListener footerRefreshListener) { + this.footerRefreshListener = footerRefreshListener; + } + + public void onFooterRefreshComplete(boolean isSuccess) { + if (isSuccess) { + footerState = FOOTER_SUCCESS; + isActionWhenShow = true; + } + else { + footerState = FOOTER_FAIL; + isActionWhenShow = false; + } + isFooterRecored = false; + changeFooterViewByState(); + } + + public void onFooterRefreshComplete(boolean isSuccess, boolean actionWhenShow) { + if (isSuccess) + footerState = FOOTER_SUCCESS; + else + footerState = FOOTER_FAIL; + isActionWhenShow = actionWhenShow; + isFooterRecored = false; + changeFooterViewByState(); + } + + public void setActionWhenShow(boolean actionWhenShow) { + isActionWhenShow = actionWhenShow; + } + + private boolean isListAtTop() { + if(mListView.getChildCount() == 0) + return true; + return mListView.getChildAt(0).getTop() == 0; + } + + // Get header width and height + private void measureView(View child) { + ViewGroup.LayoutParams p = child.getLayoutParams(); + if (p == null) { + p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + } + int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width); + int lpHeight = p.height; + int childHeightSpec; + if (lpHeight > 0) { + childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, + MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(0, + MeasureSpec.UNSPECIFIED); + } + child.measure(childWidthSpec, childHeightSpec); + } +}