mirror of https://github.com/kurisufriend/Clover
parent
3d70e4edec
commit
bfef68a78b
Binary file not shown.
@ -0,0 +1,44 @@ |
||||
package org.floens.chan.controller; |
||||
|
||||
import android.content.Context; |
||||
import android.content.res.Configuration; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
|
||||
import org.floens.chan.ui.toolbar.NavigationItem; |
||||
|
||||
public abstract class Controller { |
||||
public Context context; |
||||
public View view; |
||||
|
||||
public Controller stackSiblingController; |
||||
public NavigationController navigationController; |
||||
public NavigationItem navigationItem = new NavigationItem(); |
||||
|
||||
public Controller(Context context) { |
||||
this.context = context; |
||||
} |
||||
|
||||
public void onCreate() { |
||||
} |
||||
|
||||
public void onShow() { |
||||
} |
||||
|
||||
public void onHide() { |
||||
} |
||||
|
||||
public void onDestroy() { |
||||
} |
||||
|
||||
public View inflateRes(int resId) { |
||||
return LayoutInflater.from(context).inflate(resId, null); |
||||
} |
||||
|
||||
public void onConfigurationChanged(Configuration newConfig) { |
||||
} |
||||
|
||||
public boolean onBack() { |
||||
return false; |
||||
} |
||||
} |
@ -0,0 +1,22 @@ |
||||
package org.floens.chan.controller; |
||||
|
||||
public abstract class ControllerTransition { |
||||
private Callback callback; |
||||
|
||||
public Controller from; |
||||
public Controller to; |
||||
|
||||
public abstract void perform(); |
||||
|
||||
public void onCompleted() { |
||||
this.callback.onControllerTransitionCompleted(); |
||||
} |
||||
|
||||
public void setCallback(Callback callback) { |
||||
this.callback = callback; |
||||
} |
||||
|
||||
public interface Callback { |
||||
public void onControllerTransitionCompleted(); |
||||
} |
||||
} |
@ -0,0 +1,173 @@ |
||||
package org.floens.chan.controller; |
||||
|
||||
import android.content.Context; |
||||
import android.content.res.Configuration; |
||||
import android.support.v4.widget.DrawerLayout; |
||||
import android.view.View; |
||||
import android.widget.FrameLayout; |
||||
|
||||
import org.floens.chan.ui.toolbar.Toolbar; |
||||
import org.floens.chan.utils.AndroidUtils; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
public abstract class NavigationController extends Controller implements ControllerTransition.Callback, Toolbar.ToolbarCallback { |
||||
public Toolbar toolbar; |
||||
public FrameLayout container; |
||||
public DrawerLayout drawerLayout; |
||||
public FrameLayout drawer; |
||||
|
||||
private List<Controller> controllerList = new ArrayList<>(); |
||||
private ControllerTransition controllerTransition; |
||||
private boolean blockingInput = true; |
||||
|
||||
public NavigationController(Context context, final Controller startController) { |
||||
super(context); |
||||
} |
||||
|
||||
public boolean pushController(final Controller to) { |
||||
if (blockingInput) return false; |
||||
|
||||
if (this.controllerTransition != null) { |
||||
throw new IllegalArgumentException("Cannot push controller while a transition is in progress."); |
||||
} |
||||
|
||||
blockingInput = true; |
||||
|
||||
final Controller from = controllerList.get(controllerList.size() - 1); |
||||
|
||||
to.stackSiblingController = from; |
||||
to.navigationController = this; |
||||
to.onCreate(); |
||||
|
||||
controllerList.add(to); |
||||
|
||||
this.controllerTransition = new PushControllerTransition(); |
||||
container.addView(to.view, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); |
||||
AndroidUtils.waitForMeasure(to.view, new AndroidUtils.OnMeasuredCallback() { |
||||
@Override |
||||
public void onMeasured(View view, int width, int height) { |
||||
to.onShow(); |
||||
|
||||
doTransition(true, from, to, controllerTransition); |
||||
} |
||||
}); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
public boolean popController() { |
||||
if (blockingInput) return false; |
||||
|
||||
if (this.controllerTransition != null) { |
||||
throw new IllegalArgumentException("Cannot pop controller while a transition is in progress."); |
||||
} |
||||
|
||||
if (controllerList.size() == 1) { |
||||
throw new IllegalArgumentException("Cannot pop with 1 controller left"); |
||||
} |
||||
|
||||
blockingInput = true; |
||||
|
||||
final Controller from = controllerList.get(controllerList.size() - 1); |
||||
final Controller to = controllerList.get(controllerList.size() - 2); |
||||
|
||||
this.controllerTransition = new PopControllerTransition(); |
||||
container.addView(to.view, 0, new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); |
||||
AndroidUtils.waitForMeasure(to.view, new AndroidUtils.OnMeasuredCallback() { |
||||
@Override |
||||
public void onMeasured(View view, int width, int height) { |
||||
to.onShow(); |
||||
|
||||
doTransition(false, from, to, controllerTransition); |
||||
} |
||||
}); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public void onControllerTransitionCompleted() { |
||||
if (controllerTransition instanceof PushControllerTransition) { |
||||
controllerTransition.from.onHide(); |
||||
container.removeView(controllerTransition.from.view); |
||||
} else if (controllerTransition instanceof PopControllerTransition) { |
||||
controllerList.remove(controllerTransition.from); |
||||
|
||||
controllerTransition.from.onHide(); |
||||
container.removeView(controllerTransition.from.view); |
||||
controllerTransition.from.onDestroy(); |
||||
} |
||||
this.controllerTransition = null; |
||||
blockingInput = false; |
||||
} |
||||
|
||||
public boolean onBack() { |
||||
if (blockingInput) return true; |
||||
|
||||
if (controllerList.size() > 0) { |
||||
Controller top = controllerList.get(controllerList.size() - 1); |
||||
if (top.onBack()) { |
||||
return true; |
||||
} else { |
||||
if (controllerList.size() > 1) { |
||||
popController(); |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
} else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onConfigurationChanged(Configuration newConfig) { |
||||
for (Controller controller : controllerList) { |
||||
controller.onConfigurationChanged(newConfig); |
||||
} |
||||
} |
||||
|
||||
public void initWithController(final Controller controller) { |
||||
controllerList.add(controller); |
||||
controller.navigationController = this; |
||||
controller.onCreate(); |
||||
toolbar.setNavigationItem(false, true, controller.navigationItem); |
||||
container.addView(controller.view, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); |
||||
|
||||
AndroidUtils.waitForMeasure(controller.view, new AndroidUtils.OnMeasuredCallback() { |
||||
@Override |
||||
public void onMeasured(View view, int width, int height) { |
||||
onCreate(); |
||||
onShow(); |
||||
|
||||
controller.onShow(); |
||||
blockingInput = false; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public void onMenuClicked() { |
||||
|
||||
} |
||||
|
||||
private void doTransition(boolean pushing, Controller from, Controller to, ControllerTransition transition) { |
||||
transition.setCallback(this); |
||||
transition.from = from; |
||||
transition.to = to; |
||||
transition.perform(); |
||||
|
||||
toolbar.setNavigationItem(true, pushing, to.navigationItem); |
||||
} |
||||
|
||||
@Override |
||||
public void onMenuBackClicked(boolean isArrow) { |
||||
if (isArrow) { |
||||
onBack(); |
||||
} else { |
||||
onMenuClicked(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,38 @@ |
||||
package org.floens.chan.controller; |
||||
|
||||
import android.animation.Animator; |
||||
import android.animation.AnimatorListenerAdapter; |
||||
import android.animation.AnimatorSet; |
||||
import android.animation.ObjectAnimator; |
||||
import android.view.View; |
||||
import android.view.animation.AccelerateInterpolator; |
||||
import android.view.animation.DecelerateInterpolator; |
||||
|
||||
public class PopControllerTransition extends ControllerTransition { |
||||
@Override |
||||
public void perform() { |
||||
Animator toAlpha = ObjectAnimator.ofFloat(to.view, View.ALPHA, to.view.getAlpha(), 1f); |
||||
toAlpha.setInterpolator(new DecelerateInterpolator()); // new PathInterpolator(0f, 0f, 0.2f, 1f)
|
||||
toAlpha.setDuration(250); |
||||
|
||||
Animator fromY = ObjectAnimator.ofFloat(from.view, View.Y, 0f, from.view.getHeight() * 0.05f); |
||||
fromY.setInterpolator(new AccelerateInterpolator(2.5f)); |
||||
fromY.setDuration(250); |
||||
|
||||
fromY.addListener(new AnimatorListenerAdapter() { |
||||
@Override |
||||
public void onAnimationEnd(Animator animation) { |
||||
onCompleted(); |
||||
} |
||||
}); |
||||
|
||||
Animator fromAlpha = ObjectAnimator.ofFloat(from.view, View.ALPHA, from.view.getAlpha(), 0f); |
||||
fromAlpha.setInterpolator(new AccelerateInterpolator(2f)); |
||||
fromAlpha.setStartDelay(100); |
||||
fromAlpha.setDuration(150); |
||||
|
||||
AnimatorSet set = new AnimatorSet(); |
||||
set.playTogether(toAlpha, fromY, fromAlpha); |
||||
set.start(); |
||||
} |
||||
} |
@ -0,0 +1,37 @@ |
||||
package org.floens.chan.controller; |
||||
|
||||
import android.animation.Animator; |
||||
import android.animation.AnimatorListenerAdapter; |
||||
import android.animation.AnimatorSet; |
||||
import android.animation.ObjectAnimator; |
||||
import android.view.View; |
||||
import android.view.animation.AccelerateDecelerateInterpolator; |
||||
import android.view.animation.DecelerateInterpolator; |
||||
|
||||
public class PushControllerTransition extends ControllerTransition { |
||||
@Override |
||||
public void perform() { |
||||
Animator fromAlpha = ObjectAnimator.ofFloat(from.view, View.ALPHA, 1f, 0.7f); |
||||
fromAlpha.setDuration(217); |
||||
fromAlpha.setInterpolator(new AccelerateDecelerateInterpolator()); // new PathInterpolator(0.4f, 0f, 0.2f, 1f)
|
||||
|
||||
Animator toAlpha = ObjectAnimator.ofFloat(to.view, View.ALPHA, 0f, 1f); |
||||
toAlpha.setDuration(200); |
||||
toAlpha.setInterpolator(new DecelerateInterpolator(2f)); |
||||
|
||||
Animator toY = ObjectAnimator.ofFloat(to.view, View.Y, to.view.getHeight() * 0.08f, 0f); |
||||
toY.setDuration(350); |
||||
toY.setInterpolator(new DecelerateInterpolator(2.5f)); |
||||
|
||||
toY.addListener(new AnimatorListenerAdapter() { |
||||
@Override |
||||
public void onAnimationEnd(Animator animation) { |
||||
onCompleted(); |
||||
} |
||||
}); |
||||
|
||||
AnimatorSet set = new AnimatorSet(); |
||||
set.playTogether(fromAlpha, toAlpha, toY); |
||||
set.start(); |
||||
} |
||||
} |
@ -0,0 +1,291 @@ |
||||
package org.floens.chan.core.presenter; |
||||
|
||||
import android.text.TextUtils; |
||||
import android.view.Menu; |
||||
|
||||
import com.android.volley.VolleyError; |
||||
|
||||
import org.floens.chan.ChanApplication; |
||||
import org.floens.chan.R; |
||||
import org.floens.chan.chan.ChanUrls; |
||||
import org.floens.chan.core.ChanPreferences; |
||||
import org.floens.chan.core.loader.ChanLoader; |
||||
import org.floens.chan.core.loader.LoaderPool; |
||||
import org.floens.chan.core.model.ChanThread; |
||||
import org.floens.chan.core.model.Loadable; |
||||
import org.floens.chan.core.model.Post; |
||||
import org.floens.chan.core.model.PostLinkable; |
||||
import org.floens.chan.core.model.SavedReply; |
||||
import org.floens.chan.ui.adapter.PostAdapter; |
||||
import org.floens.chan.ui.view.PostView; |
||||
import org.floens.chan.utils.AndroidUtils; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
public class ThreadPresenter implements ChanLoader.ChanLoaderCallback, PostAdapter.PostAdapterCallback, PostView.PostViewCallback { |
||||
private ThreadPresenterCallback threadPresenterCallback; |
||||
|
||||
private Loadable loadable; |
||||
private ChanLoader chanLoader; |
||||
|
||||
public ThreadPresenter(ThreadPresenterCallback threadPresenterCallback) { |
||||
this.threadPresenterCallback = threadPresenterCallback; |
||||
} |
||||
|
||||
public void bindLoadable(Loadable loadable) { |
||||
if (!loadable.equals(this.loadable)) { |
||||
if (this.loadable != null) { |
||||
unbindLoadable(); |
||||
} |
||||
|
||||
this.loadable = loadable; |
||||
|
||||
chanLoader = LoaderPool.getInstance().obtain(loadable, this); |
||||
} |
||||
} |
||||
|
||||
public void unbindLoadable() { |
||||
threadPresenterCallback.showLoading(); |
||||
} |
||||
|
||||
public void requestData() { |
||||
threadPresenterCallback.showLoading(); |
||||
chanLoader.requestData(); |
||||
} |
||||
|
||||
@Override |
||||
public Loadable getLoadable() { |
||||
return loadable; |
||||
} |
||||
|
||||
/* |
||||
* ChanLoader callbacks |
||||
*/ |
||||
@Override |
||||
public void onChanLoaderData(ChanThread result) { |
||||
threadPresenterCallback.showPosts(result); |
||||
} |
||||
|
||||
@Override |
||||
public void onChanLoaderError(VolleyError error) { |
||||
threadPresenterCallback.showError(error); |
||||
} |
||||
|
||||
/* |
||||
* PostAdapter callbacks |
||||
*/ |
||||
@Override |
||||
public void onFilteredResults(String filter, int count, boolean all) { |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void onListScrolledToBottom() { |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void onListStatusClicked() { |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void scrollTo(int position) { |
||||
|
||||
} |
||||
|
||||
/* |
||||
* PostView callbacks |
||||
*/ |
||||
@Override |
||||
public void onPostClicked(Post post) { |
||||
if (loadable.mode == Loadable.Mode.CATALOG) { |
||||
Loadable threadLoadable = new Loadable(post.board, post.no); |
||||
threadLoadable.generateTitle(post); |
||||
threadPresenterCallback.showThread(threadLoadable); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onThumbnailClicked(Post post) { |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void onPopulatePostOptions(Post post, Menu menu) { |
||||
if (chanLoader.getLoadable().isBoardMode() || chanLoader.getLoadable().isCatalogMode()) { |
||||
menu.add(Menu.NONE, 9, Menu.NONE, AndroidUtils.getRes().getString(R.string.action_pin)); |
||||
} |
||||
|
||||
if (chanLoader.getLoadable().isThreadMode()) { |
||||
menu.add(Menu.NONE, 10, Menu.NONE, AndroidUtils.getRes().getString(R.string.post_quick_reply)); |
||||
} |
||||
|
||||
String[] baseOptions = AndroidUtils.getRes().getStringArray(R.array.post_options); |
||||
for (int i = 0; i < baseOptions.length; i++) { |
||||
menu.add(Menu.NONE, i, Menu.NONE, baseOptions[i]); |
||||
} |
||||
|
||||
if (!TextUtils.isEmpty(post.id)) { |
||||
menu.add(Menu.NONE, 6, Menu.NONE, AndroidUtils.getRes().getString(R.string.post_highlight_id)); |
||||
} |
||||
|
||||
// Only add the delete option when the post is a saved reply
|
||||
if (ChanApplication.getDatabaseManager().isSavedReply(post.board, post.no)) { |
||||
menu.add(Menu.NONE, 7, Menu.NONE, AndroidUtils.getRes().getString(R.string.delete)); |
||||
} |
||||
|
||||
if (ChanPreferences.getDeveloper()) { |
||||
menu.add(Menu.NONE, 8, Menu.NONE, "Make this a saved reply"); |
||||
} |
||||
} |
||||
|
||||
public void onPostOptionClicked(Post post, int id) { |
||||
switch (id) { |
||||
case 10: // Quick reply
|
||||
// openReply(false); TODO
|
||||
// Pass through
|
||||
case 0: // Quote
|
||||
ChanApplication.getReplyManager().quote(post.no); |
||||
break; |
||||
case 1: // Quote inline
|
||||
ChanApplication.getReplyManager().quoteInline(post.no, post.comment.toString()); |
||||
break; |
||||
case 2: // Info
|
||||
showPostInfo(post); |
||||
break; |
||||
case 3: // Show clickables
|
||||
if (post.linkables.size() > 0) { |
||||
threadPresenterCallback.showPostLinkables(post.linkables); |
||||
} |
||||
break; |
||||
case 4: // Copy text
|
||||
threadPresenterCallback.clipboardPost(post); |
||||
break; |
||||
case 5: // Report
|
||||
AndroidUtils.openLink(ChanUrls.getReportUrl(post.board, post.no)); |
||||
break; |
||||
case 6: // Id
|
||||
//TODO
|
||||
// highlightedId = post.id;
|
||||
// threadManagerListener.onRefreshView();
|
||||
break; |
||||
case 7: // Delete
|
||||
// deletePost(post); TODO
|
||||
break; |
||||
case 8: // Save reply (debug)
|
||||
ChanApplication.getDatabaseManager().saveReply(new SavedReply(post.board, post.no, "foo")); |
||||
break; |
||||
case 9: // Pin
|
||||
ChanApplication.getWatchManager().addPin(post); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onPostLinkableClicked(PostLinkable linkable) { |
||||
if (linkable.type == PostLinkable.Type.QUOTE) { |
||||
Post post = findPostById((Integer) linkable.value); |
||||
|
||||
List<Post> list = new ArrayList<>(1); |
||||
list.add(post); |
||||
threadPresenterCallback.showPostsPopup(linkable.post, list); |
||||
} else if (linkable.type == PostLinkable.Type.LINK) { |
||||
threadPresenterCallback.openLink((String) linkable.value); |
||||
} else if (linkable.type == PostLinkable.Type.THREAD) { |
||||
PostLinkable.ThreadLink link = (PostLinkable.ThreadLink) linkable.value; |
||||
Loadable thread = new Loadable(link.board, link.threadId); |
||||
|
||||
threadPresenterCallback.showThread(thread); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onShowPostReplies(Post post) { |
||||
List<Post> posts = new ArrayList<>(); |
||||
for (int no : post.repliesFrom) { |
||||
Post replyPost = findPostById(no); |
||||
if (replyPost != null) { |
||||
posts.add(replyPost); |
||||
} |
||||
} |
||||
if (posts.size() > 0) { |
||||
threadPresenterCallback.showPostsPopup(post, posts); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public boolean isPostHightlighted(Post post) { |
||||
return false; |
||||
} |
||||
|
||||
public void highlightPost(int no) { |
||||
} |
||||
|
||||
public void scrollToPost(int no) { |
||||
} |
||||
|
||||
@Override |
||||
public boolean isPostLastSeen(Post post) { |
||||
return false; |
||||
} |
||||
|
||||
private void showPostInfo(Post post) { |
||||
String text = ""; |
||||
|
||||
if (post.hasImage) { |
||||
text += "File: " + post.filename + "." + post.ext + " \nDimensions: " + post.imageWidth + "x" |
||||
+ post.imageHeight + "\nSize: " + AndroidUtils.getReadableFileSize(post.fileSize, false) + "\n\n"; |
||||
} |
||||
|
||||
text += "Time: " + post.date; |
||||
|
||||
if (!TextUtils.isEmpty(post.id)) { |
||||
text += "\nId: " + post.id; |
||||
} |
||||
|
||||
if (!TextUtils.isEmpty(post.tripcode)) { |
||||
text += "\nTripcode: " + post.tripcode; |
||||
} |
||||
|
||||
if (!TextUtils.isEmpty(post.countryName)) { |
||||
text += "\nCountry: " + post.countryName; |
||||
} |
||||
|
||||
if (!TextUtils.isEmpty(post.capcode)) { |
||||
text += "\nCapcode: " + post.capcode; |
||||
} |
||||
|
||||
threadPresenterCallback.showPostInfo(text); |
||||
} |
||||
|
||||
private Post findPostById(int id) { |
||||
for (Post post : chanLoader.getThread().posts) { |
||||
if (post.no == id) { |
||||
return post; |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
public interface ThreadPresenterCallback { |
||||
public void showPosts(ChanThread thread); |
||||
|
||||
public void showError(VolleyError error); |
||||
|
||||
public void showLoading(); |
||||
|
||||
public void showPostInfo(String info); |
||||
|
||||
public void showPostLinkables(List<PostLinkable> linkables); |
||||
|
||||
public void clipboardPost(Post post); |
||||
|
||||
public void showThread(Loadable threadLoadable); |
||||
|
||||
public void openLink(String link); |
||||
|
||||
public void showPostsPopup(Post forPost, List<Post> posts); |
||||
} |
||||
} |
@ -0,0 +1,102 @@ |
||||
package org.floens.chan.ui.controller; |
||||
|
||||
import android.content.Context; |
||||
|
||||
import org.floens.chan.R; |
||||
import org.floens.chan.chan.ChanUrls; |
||||
import org.floens.chan.controller.Controller; |
||||
import org.floens.chan.core.model.Loadable; |
||||
import org.floens.chan.ui.layout.ThreadLayout; |
||||
import org.floens.chan.ui.toolbar.ToolbarMenu; |
||||
import org.floens.chan.ui.toolbar.ToolbarMenuItem; |
||||
import org.floens.chan.ui.toolbar.ToolbarMenuSubItem; |
||||
import org.floens.chan.ui.toolbar.ToolbarMenuSubMenu; |
||||
import org.floens.chan.utils.AndroidUtils; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
public class BrowseController extends Controller implements ToolbarMenuItem.ToolbarMenuItemCallback, ThreadLayout.ThreadLayoutCallback { |
||||
private static final int REFRESH_ID = 1; |
||||
private static final int POST_ID = 2; |
||||
private static final int SEARCH_ID = 101; |
||||
private static final int SHARE_ID = 102; |
||||
private static final int SETTINGS_ID = 103; |
||||
|
||||
private ThreadLayout threadLayout; |
||||
|
||||
public BrowseController(Context context) { |
||||
super(context); |
||||
} |
||||
|
||||
@Override |
||||
public void onCreate() { |
||||
super.onCreate(); |
||||
|
||||
navigationItem.title = "Hello world"; |
||||
ToolbarMenu menu = new ToolbarMenu(context); |
||||
navigationItem.menu = menu; |
||||
navigationItem.hasBack = false; |
||||
|
||||
menu.addItem(new ToolbarMenuItem(context, this, REFRESH_ID, R.drawable.ic_action_refresh)); |
||||
menu.addItem(new ToolbarMenuItem(context, this, POST_ID, R.drawable.ic_action_write)); |
||||
|
||||
ToolbarMenuItem overflow = menu.createOverflow(this); |
||||
|
||||
List<ToolbarMenuSubItem> items = new ArrayList<>(); |
||||
items.add(new ToolbarMenuSubItem(SEARCH_ID, context.getString(R.string.action_search))); |
||||
items.add(new ToolbarMenuSubItem(SHARE_ID, context.getString(R.string.action_share))); |
||||
items.add(new ToolbarMenuSubItem(SETTINGS_ID, context.getString(R.string.action_settings))); |
||||
|
||||
overflow.setSubMenu(new ToolbarMenuSubMenu(context, overflow.getView(), items)); |
||||
|
||||
threadLayout = new ThreadLayout(context); |
||||
threadLayout.setCallback(this); |
||||
|
||||
view = threadLayout; |
||||
|
||||
Loadable loadable = new Loadable("g"); |
||||
loadable.mode = Loadable.Mode.CATALOG; |
||||
loadable.generateTitle(); |
||||
navigationItem.title = loadable.title; |
||||
|
||||
threadLayout.getPresenter().bindLoadable(loadable); |
||||
threadLayout.getPresenter().requestData(); |
||||
} |
||||
|
||||
@Override |
||||
public void onMenuItemClicked(ToolbarMenuItem item) { |
||||
switch (item.getId()) { |
||||
case REFRESH_ID: |
||||
threadLayout.getPresenter().requestData(); |
||||
break; |
||||
case POST_ID: |
||||
// TODO
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onSubMenuItemClicked(ToolbarMenuItem parent, ToolbarMenuSubItem item) { |
||||
switch (item.getId()) { |
||||
case SEARCH_ID: |
||||
// TODO
|
||||
break; |
||||
case SHARE_ID: |
||||
String link = ChanUrls.getCatalogUrlDesktop(threadLayout.getPresenter().getLoadable().board); |
||||
AndroidUtils.shareLink(link); |
||||
break; |
||||
case SETTINGS_ID: |
||||
SettingsController settingsController = new SettingsController(context); |
||||
navigationController.pushController(settingsController); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void openThread(Loadable threadLoadable) { |
||||
ViewThreadController viewThreadController = new ViewThreadController(context); |
||||
viewThreadController.setLoadable(threadLoadable); |
||||
navigationController.pushController(viewThreadController); |
||||
} |
||||
} |
@ -0,0 +1,65 @@ |
||||
package org.floens.chan.ui.controller; |
||||
|
||||
import android.content.Context; |
||||
import android.content.res.Configuration; |
||||
import android.support.v4.widget.DrawerLayout; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import android.widget.FrameLayout; |
||||
|
||||
import org.floens.chan.R; |
||||
import org.floens.chan.controller.Controller; |
||||
import org.floens.chan.controller.NavigationController; |
||||
import org.floens.chan.ui.toolbar.Toolbar; |
||||
import org.floens.chan.utils.AndroidUtils; |
||||
|
||||
import static org.floens.chan.utils.AndroidUtils.dp; |
||||
|
||||
public class RootNavigationController extends NavigationController { |
||||
public RootNavigationController(Context context, Controller startController) { |
||||
super(context, startController); |
||||
|
||||
view = inflateRes(R.layout.root_layout); |
||||
toolbar = (Toolbar) view.findViewById(R.id.toolbar); |
||||
container = (FrameLayout) view.findViewById(R.id.container); |
||||
drawerLayout = (DrawerLayout) view.findViewById(R.id.drawer_layout); |
||||
drawer = (FrameLayout) view.findViewById(R.id.drawer); |
||||
|
||||
toolbar.setCallback(this); |
||||
|
||||
initWithController(startController); |
||||
} |
||||
|
||||
@Override |
||||
public void onConfigurationChanged(Configuration newConfig) { |
||||
super.onConfigurationChanged(newConfig); |
||||
|
||||
AndroidUtils.waitForLayout(drawer, new AndroidUtils.OnMeasuredCallback() { |
||||
@Override |
||||
public void onMeasured(View view, int width, int height) { |
||||
setDrawerWidth(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
@Override |
||||
public void onCreate() { |
||||
setDrawerWidth(); |
||||
} |
||||
|
||||
@Override |
||||
public void onMenuClicked() { |
||||
super.onMenuClicked(); |
||||
|
||||
drawerLayout.openDrawer(drawer); |
||||
} |
||||
|
||||
private void setDrawerWidth() { |
||||
int width = Math.min(view.getWidth() - dp(56), dp(56) * 6); |
||||
if (drawer.getWidth() != width) { |
||||
ViewGroup.LayoutParams params = drawer.getLayoutParams(); |
||||
params.width = width; |
||||
drawer.setLayoutParams(params); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
package org.floens.chan.ui.controller; |
||||
|
||||
import android.content.Context; |
||||
|
||||
import org.floens.chan.R; |
||||
import org.floens.chan.controller.Controller; |
||||
|
||||
public class SettingsController extends Controller { |
||||
public SettingsController(Context context) { |
||||
super(context); |
||||
} |
||||
|
||||
@Override |
||||
public void onCreate() { |
||||
super.onCreate(); |
||||
|
||||
navigationItem.title = context.getString(R.string.action_settings); |
||||
|
||||
view = inflateRes(R.layout.settings_layout); |
||||
} |
||||
} |
@ -0,0 +1,54 @@ |
||||
package org.floens.chan.ui.controller; |
||||
|
||||
import android.app.AlertDialog; |
||||
import android.content.Context; |
||||
import android.content.DialogInterface; |
||||
|
||||
import org.floens.chan.R; |
||||
import org.floens.chan.controller.Controller; |
||||
import org.floens.chan.core.model.Loadable; |
||||
import org.floens.chan.ui.layout.ThreadLayout; |
||||
|
||||
public class ViewThreadController extends Controller implements ThreadLayout.ThreadLayoutCallback { |
||||
private ThreadLayout threadLayout; |
||||
private Loadable loadable; |
||||
|
||||
public ViewThreadController(Context context) { |
||||
super(context); |
||||
} |
||||
|
||||
public void setLoadable(Loadable loadable) { |
||||
this.loadable = loadable; |
||||
} |
||||
|
||||
@Override |
||||
public void onCreate() { |
||||
super.onCreate(); |
||||
|
||||
threadLayout = new ThreadLayout(context); |
||||
threadLayout.setCallback(this); |
||||
view = threadLayout; |
||||
view.setBackgroundColor(0xffffffff); |
||||
|
||||
threadLayout.getPresenter().bindLoadable(loadable); |
||||
threadLayout.getPresenter().requestData(); |
||||
|
||||
navigationItem.title = loadable.title; |
||||
} |
||||
|
||||
@Override |
||||
public void openThread(Loadable threadLoadable) { |
||||
// TODO implement, scroll to post and fix title
|
||||
new AlertDialog.Builder(context) |
||||
.setNegativeButton(R.string.cancel, null) |
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |
||||
@Override |
||||
public void onClick(final DialogInterface dialog, final int which) { |
||||
// threadManagerListener.onOpenThread(thread, link.postId);
|
||||
} |
||||
}) |
||||
.setTitle(R.string.open_thread_confirmation) |
||||
.setMessage("/" + threadLoadable.board + "/" + threadLoadable.no) |
||||
.show(); |
||||
} |
||||
} |
@ -0,0 +1,153 @@ |
||||
package org.floens.chan.ui.drawable; |
||||
|
||||
import android.graphics.Canvas; |
||||
import android.graphics.ColorFilter; |
||||
import android.graphics.Paint; |
||||
import android.graphics.Path; |
||||
import android.graphics.PixelFormat; |
||||
import android.graphics.Rect; |
||||
import android.graphics.drawable.Drawable; |
||||
|
||||
import static org.floens.chan.utils.AndroidUtils.dp; |
||||
|
||||
public class ArrowMenuDrawable extends Drawable { |
||||
private final Paint mPaint = new Paint(); |
||||
|
||||
// The angle in degress that the arrow head is inclined at.
|
||||
private static final float ARROW_HEAD_ANGLE = (float) Math.toRadians(45); |
||||
private final float mBarThickness; |
||||
// The length of top and bottom bars when they merge into an arrow
|
||||
private final float mTopBottomArrowSize; |
||||
// The length of middle bar
|
||||
private final float mBarSize; |
||||
// The length of the middle bar when arrow is shaped
|
||||
private final float mMiddleArrowSize; |
||||
// The space between bars when they are parallel
|
||||
private final float mBarGap; |
||||
// Use Path instead of canvas operations so that if color has transparency, overlapping sections
|
||||
// wont look different
|
||||
private final Path mPath = new Path(); |
||||
// The reported intrinsic size of the drawable.
|
||||
private final int mSize; |
||||
// Whether we should mirror animation when animation is reversed.
|
||||
private boolean mVerticalMirror = false; |
||||
// The interpolated version of the original progress
|
||||
private float mProgress; |
||||
|
||||
public ArrowMenuDrawable() { |
||||
mPaint.setColor(0xffffffff); |
||||
mPaint.setAntiAlias(true); |
||||
mSize = dp(24f); |
||||
mBarSize = dp(18f); |
||||
mTopBottomArrowSize = dp(11.31f); |
||||
mBarThickness = dp(2f); |
||||
mBarGap = dp(3f); |
||||
mMiddleArrowSize = dp(16f); |
||||
|
||||
mPaint.setStyle(Paint.Style.STROKE); |
||||
mPaint.setStrokeJoin(Paint.Join.ROUND); |
||||
mPaint.setStrokeCap(Paint.Cap.SQUARE); |
||||
mPaint.setStrokeWidth(mBarThickness); |
||||
|
||||
setProgress(0f); |
||||
} |
||||
|
||||
boolean isLayoutRtl() { |
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public void draw(Canvas canvas) { |
||||
Rect bounds = getBounds(); |
||||
// Interpolated widths of arrow bars
|
||||
final float arrowSize = lerp(mBarSize, mTopBottomArrowSize, mProgress); |
||||
final float middleBarSize = lerp(mBarSize, mMiddleArrowSize, mProgress); |
||||
// Interpolated size of middle bar
|
||||
final float middleBarCut = lerp(0, mBarThickness / 2, mProgress); |
||||
// The rotation of the top and bottom bars (that make the arrow head)
|
||||
final float rotation = lerp(0, ARROW_HEAD_ANGLE, mProgress); |
||||
|
||||
// The whole canvas rotates as the transition happens
|
||||
final float canvasRotate = lerp(-180, 0, mProgress); |
||||
final float topBottomBarOffset = lerp(mBarGap + mBarThickness, 0, mProgress); |
||||
mPath.rewind(); |
||||
|
||||
final float arrowEdge = -middleBarSize / 2; |
||||
// draw middle bar
|
||||
mPath.moveTo(arrowEdge + middleBarCut, 0); |
||||
mPath.rLineTo(middleBarSize - middleBarCut, 0); |
||||
|
||||
float arrowWidth = arrowSize * (float) Math.cos(rotation); |
||||
float arrowHeight = arrowSize * (float) Math.sin(rotation); |
||||
|
||||
if (mProgress == 0f || mProgress == 1f) { |
||||
arrowWidth = Math.round(arrowWidth); |
||||
arrowHeight = Math.round(arrowHeight); |
||||
} |
||||
|
||||
// top bar
|
||||
mPath.moveTo(arrowEdge, topBottomBarOffset); |
||||
mPath.rLineTo(arrowWidth, arrowHeight); |
||||
|
||||
// bottom bar
|
||||
mPath.moveTo(arrowEdge, -topBottomBarOffset); |
||||
mPath.rLineTo(arrowWidth, -arrowHeight); |
||||
mPath.moveTo(0, 0); |
||||
mPath.close(); |
||||
|
||||
canvas.save(); |
||||
// Rotate the whole canvas if spinning.
|
||||
canvas.rotate(canvasRotate * ((mVerticalMirror) ? -1 : 1), |
||||
bounds.centerX(), bounds.centerY()); |
||||
canvas.translate(bounds.centerX(), bounds.centerY()); |
||||
canvas.drawPath(mPath, mPaint); |
||||
|
||||
canvas.restore(); |
||||
} |
||||
|
||||
@Override |
||||
public void setAlpha(int i) { |
||||
mPaint.setAlpha(i); |
||||
} |
||||
|
||||
@Override |
||||
public void setColorFilter(ColorFilter colorFilter) { |
||||
mPaint.setColorFilter(colorFilter); |
||||
} |
||||
|
||||
@Override |
||||
public int getIntrinsicHeight() { |
||||
return mSize; |
||||
} |
||||
|
||||
@Override |
||||
public int getIntrinsicWidth() { |
||||
return mSize; |
||||
} |
||||
|
||||
@Override |
||||
public int getOpacity() { |
||||
return PixelFormat.TRANSLUCENT; |
||||
} |
||||
|
||||
public float getProgress() { |
||||
return mProgress; |
||||
} |
||||
|
||||
public void setProgress(float progress) { |
||||
if (progress == 1f) { |
||||
mVerticalMirror = true; |
||||
} else if (progress == 0f) { |
||||
mVerticalMirror = false; |
||||
} |
||||
mProgress = progress; |
||||
invalidateSelf(); |
||||
} |
||||
|
||||
/** |
||||
* Linear interpolate between a and b with parameter t. |
||||
*/ |
||||
private static float lerp(float a, float b, float t) { |
||||
return a + (b - a) * t; |
||||
} |
||||
} |
@ -0,0 +1,85 @@ |
||||
package org.floens.chan.ui.helper; |
||||
|
||||
import android.app.Activity; |
||||
import android.app.FragmentTransaction; |
||||
import android.content.Context; |
||||
|
||||
import org.floens.chan.core.model.Post; |
||||
import org.floens.chan.core.presenter.ThreadPresenter; |
||||
import org.floens.chan.ui.fragment.PostRepliesFragment; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
public class PostPopupHelper { |
||||
private Context context; |
||||
private ThreadPresenter presenter; |
||||
|
||||
private final List<RepliesData> dataQueue = new ArrayList<>(); |
||||
private PostRepliesFragment currentPopupFragment; |
||||
|
||||
public PostPopupHelper(Context context, ThreadPresenter presenter) { |
||||
this.context = context; |
||||
this.presenter = presenter; |
||||
} |
||||
|
||||
public void showPosts(Post forPost, List<Post> posts) { |
||||
RepliesData data = new RepliesData(forPost, posts); |
||||
|
||||
dataQueue.add(data); |
||||
|
||||
if (currentPopupFragment != null) { |
||||
currentPopupFragment.dismissNoCallback(); |
||||
} |
||||
|
||||
presentFragment(data); |
||||
} |
||||
|
||||
public void onPostRepliesPop() { |
||||
if (dataQueue.size() == 0) |
||||
return; |
||||
|
||||
dataQueue.remove(dataQueue.size() - 1); |
||||
|
||||
if (dataQueue.size() > 0) { |
||||
presentFragment(dataQueue.get(dataQueue.size() - 1)); |
||||
} else { |
||||
currentPopupFragment = null; |
||||
} |
||||
} |
||||
|
||||
public void closeAllPostFragments() { |
||||
dataQueue.clear(); |
||||
if (currentPopupFragment != null) { |
||||
currentPopupFragment.dismissNoCallback(); |
||||
currentPopupFragment = null; |
||||
} |
||||
} |
||||
|
||||
public void postClicked(Post p) { |
||||
closeAllPostFragments(); |
||||
presenter.highlightPost(p.no); |
||||
presenter.scrollToPost(p.no); |
||||
} |
||||
|
||||
private void presentFragment(RepliesData data) { |
||||
PostRepliesFragment fragment = PostRepliesFragment.newInstance(data, this, presenter); |
||||
// TODO fade animations on all platforms
|
||||
FragmentTransaction ft = ((Activity) context).getFragmentManager().beginTransaction(); |
||||
ft.add(fragment, "postPopup"); |
||||
ft.commitAllowingStateLoss(); |
||||
currentPopupFragment = fragment; |
||||
} |
||||
|
||||
public static class RepliesData { |
||||
public List<Post> posts; |
||||
public Post forPost; |
||||
public int listViewIndex; |
||||
public int listViewTop; |
||||
|
||||
public RepliesData(Post forPost, List<Post> posts) { |
||||
this.forPost = forPost; |
||||
this.posts = posts; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,234 @@ |
||||
package org.floens.chan.ui.layout; |
||||
|
||||
import android.animation.Animator; |
||||
import android.animation.AnimatorListenerAdapter; |
||||
import android.animation.AnimatorSet; |
||||
import android.animation.ObjectAnimator; |
||||
import android.animation.ValueAnimator; |
||||
import android.app.Activity; |
||||
import android.content.Context; |
||||
import android.graphics.Color; |
||||
import android.graphics.Point; |
||||
import android.graphics.Rect; |
||||
import android.graphics.drawable.Drawable; |
||||
import android.os.Build; |
||||
import android.util.AttributeSet; |
||||
import android.view.LayoutInflater; |
||||
import android.view.MotionEvent; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import android.view.Window; |
||||
import android.view.animation.DecelerateInterpolator; |
||||
import android.widget.FrameLayout; |
||||
import android.widget.ImageView; |
||||
|
||||
import org.floens.chan.R; |
||||
|
||||
import static org.floens.chan.utils.AndroidUtils.dp; |
||||
import static org.floens.chan.utils.AnimationUtils.calculateBoundsAnimation; |
||||
|
||||
|
||||
public class ImageViewLayout extends FrameLayout implements View.OnClickListener { |
||||
private ImageView imageView; |
||||
|
||||
private Callback callback; |
||||
private Drawable drawable; |
||||
|
||||
private int statusBarColorPrevious; |
||||
private AnimatorSet startAnimation; |
||||
private AnimatorSet endAnimation; |
||||
|
||||
public static ImageViewLayout attach(Window window) { |
||||
ImageViewLayout imageViewLayout = (ImageViewLayout) LayoutInflater.from(window.getContext()).inflate(R.layout.image_view_layout, null); |
||||
window.addContentView(imageViewLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); |
||||
return imageViewLayout; |
||||
} |
||||
|
||||
public ImageViewLayout(Context context, AttributeSet attrs) { |
||||
super(context, attrs); |
||||
} |
||||
|
||||
@Override |
||||
protected void onFinishInflate() { |
||||
super.onFinishInflate(); |
||||
this.imageView = (ImageView) findViewById(R.id.image); |
||||
setOnClickListener(this); |
||||
} |
||||
|
||||
@Override |
||||
public boolean onTouchEvent(MotionEvent event) { |
||||
super.onTouchEvent(event); |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public void onClick(View v) { |
||||
removeImage(); |
||||
} |
||||
|
||||
public void setImage(Callback callback, final Drawable drawable) { |
||||
this.callback = callback; |
||||
this.drawable = drawable; |
||||
|
||||
this.imageView.setImageDrawable(drawable); |
||||
|
||||
Rect startBounds = callback.getImageViewLayoutStartBounds(); |
||||
final Rect endBounds = new Rect(); |
||||
final Point globalOffset = new Point(); |
||||
getGlobalVisibleRect(endBounds, globalOffset); |
||||
float startScale = calculateBoundsAnimation(startBounds, endBounds, globalOffset); |
||||
|
||||
imageView.setPivotX(0f); |
||||
imageView.setPivotY(0f); |
||||
imageView.setX(startBounds.left); |
||||
imageView.setY(startBounds.top); |
||||
imageView.setScaleX(startScale); |
||||
imageView.setScaleY(startScale); |
||||
|
||||
Window window = ((Activity) getContext()).getWindow(); |
||||
if (Build.VERSION.SDK_INT >= 21) { |
||||
statusBarColorPrevious = window.getStatusBarColor(); |
||||
} |
||||
|
||||
startAnimation(startBounds, endBounds, startScale); |
||||
} |
||||
|
||||
public void removeImage() { |
||||
if (startAnimation != null || endAnimation != null) { |
||||
return; |
||||
} |
||||
|
||||
endAnimation(); |
||||
// endAnimationEmpty();
|
||||
} |
||||
|
||||
private void startAnimation(Rect startBounds, Rect finalBounds, float startScale) { |
||||
startAnimation = new AnimatorSet(); |
||||
|
||||
ValueAnimator backgroundAlpha = ValueAnimator.ofFloat(0f, 1f); |
||||
backgroundAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
||||
@Override |
||||
public void onAnimationUpdate(ValueAnimator animation) { |
||||
setBackgroundAlpha((float) animation.getAnimatedValue()); |
||||
} |
||||
}); |
||||
|
||||
startAnimation |
||||
.play(ObjectAnimator.ofFloat(imageView, View.X, startBounds.left, finalBounds.left)) |
||||
.with(ObjectAnimator.ofFloat(imageView, View.Y, startBounds.top, finalBounds.top)) |
||||
.with(ObjectAnimator.ofFloat(imageView, View.SCALE_X, startScale, 1f)) |
||||
.with(ObjectAnimator.ofFloat(imageView, View.SCALE_Y, startScale, 1f)) |
||||
.with(backgroundAlpha); |
||||
|
||||
startAnimation.setDuration(200); |
||||
startAnimation.setInterpolator(new DecelerateInterpolator()); |
||||
startAnimation.addListener(new AnimatorListenerAdapter() { |
||||
@Override |
||||
public void onAnimationEnd(Animator animation) { |
||||
startAnimationEnd(); |
||||
startAnimation = null; |
||||
} |
||||
}); |
||||
startAnimation.start(); |
||||
} |
||||
|
||||
private void startAnimationEnd() { |
||||
imageView.setX(0f); |
||||
imageView.setY(0f); |
||||
imageView.setScaleX(1f); |
||||
imageView.setScaleY(1f); |
||||
// controller.setVisibility(false);
|
||||
} |
||||
|
||||
private void endAnimation() { |
||||
// controller.setVisibility(true);
|
||||
|
||||
Rect startBounds = callback.getImageViewLayoutStartBounds(); |
||||
final Rect endBounds = new Rect(); |
||||
final Point globalOffset = new Point(); |
||||
getGlobalVisibleRect(endBounds, globalOffset); |
||||
float startScale = calculateBoundsAnimation(startBounds, endBounds, globalOffset); |
||||
|
||||
endAnimation = new AnimatorSet(); |
||||
|
||||
ValueAnimator backgroundAlpha = ValueAnimator.ofFloat(1f, 0f); |
||||
backgroundAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
||||
@Override |
||||
public void onAnimationUpdate(ValueAnimator animation) { |
||||
setBackgroundAlpha((float) animation.getAnimatedValue()); |
||||
} |
||||
}); |
||||
|
||||
endAnimation |
||||
.play(ObjectAnimator.ofFloat(imageView, View.X, startBounds.left)) |
||||
.with(ObjectAnimator.ofFloat(imageView, View.Y, startBounds.top)) |
||||
.with(ObjectAnimator.ofFloat(imageView, View.SCALE_X, 1f, startScale)) |
||||
.with(ObjectAnimator.ofFloat(imageView, View.SCALE_Y, 1f, startScale)) |
||||
.with(backgroundAlpha); |
||||
|
||||
endAnimation.setDuration(200); |
||||
endAnimation.setInterpolator(new DecelerateInterpolator()); |
||||
endAnimation.addListener(new AnimatorListenerAdapter() { |
||||
@Override |
||||
public void onAnimationEnd(Animator animation) { |
||||
endAnimationEnd(); |
||||
} |
||||
}); |
||||
endAnimation.start(); |
||||
} |
||||
|
||||
private void endAnimationEmpty() { |
||||
endAnimation = new AnimatorSet(); |
||||
|
||||
ValueAnimator backgroundAlpha = ValueAnimator.ofFloat(1f, 0f); |
||||
backgroundAlpha.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
||||
@Override |
||||
public void onAnimationUpdate(ValueAnimator animation) { |
||||
setBackgroundAlpha((float) animation.getAnimatedValue()); |
||||
} |
||||
}); |
||||
endAnimation |
||||
.play(ObjectAnimator.ofFloat(imageView, View.Y, imageView.getTop(), imageView.getTop() + dp(20))) |
||||
.with(ObjectAnimator.ofFloat(imageView, View.ALPHA, 1f, 0f)) |
||||
.with(backgroundAlpha); |
||||
|
||||
endAnimation.setDuration(200); |
||||
endAnimation.setInterpolator(new DecelerateInterpolator()); |
||||
endAnimation.addListener(new AnimatorListenerAdapter() { |
||||
@Override |
||||
public void onAnimationEnd(Animator animation) { |
||||
endAnimationEnd(); |
||||
} |
||||
}); |
||||
endAnimation.start(); |
||||
} |
||||
|
||||
private void endAnimationEnd() { |
||||
Window window = ((Activity) getContext()).getWindow(); |
||||
if (Build.VERSION.SDK_INT >= 21) { |
||||
window.setStatusBarColor(statusBarColorPrevious); |
||||
} |
||||
|
||||
callback.onImageViewLayoutDestroy(); |
||||
} |
||||
|
||||
private void setBackgroundAlpha(float alpha) { |
||||
setBackgroundColor(Color.argb((int) (alpha * 255f), 0, 0, 0)); |
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) { |
||||
Window window = ((Activity) getContext()).getWindow(); |
||||
|
||||
int r = (int) ((1f - alpha) * Color.red(statusBarColorPrevious)); |
||||
int g = (int) ((1f - alpha) * Color.green(statusBarColorPrevious)); |
||||
int b = (int) ((1f - alpha) * Color.blue(statusBarColorPrevious)); |
||||
|
||||
window.setStatusBarColor(Color.argb(255, r, g, b)); |
||||
} |
||||
} |
||||
|
||||
public interface Callback { |
||||
public Rect getImageViewLayoutStartBounds(); |
||||
|
||||
public void onImageViewLayoutDestroy(); |
||||
} |
||||
} |
@ -0,0 +1,157 @@ |
||||
package org.floens.chan.ui.layout; |
||||
|
||||
import android.app.AlertDialog; |
||||
import android.content.ClipData; |
||||
import android.content.ClipboardManager; |
||||
import android.content.Context; |
||||
import android.content.DialogInterface; |
||||
import android.util.AttributeSet; |
||||
import android.widget.Toast; |
||||
|
||||
import com.android.volley.VolleyError; |
||||
|
||||
import org.floens.chan.R; |
||||
import org.floens.chan.core.ChanPreferences; |
||||
import org.floens.chan.core.model.ChanThread; |
||||
import org.floens.chan.core.model.Loadable; |
||||
import org.floens.chan.core.model.Post; |
||||
import org.floens.chan.core.model.PostLinkable; |
||||
import org.floens.chan.core.presenter.ThreadPresenter; |
||||
import org.floens.chan.ui.helper.PostPopupHelper; |
||||
import org.floens.chan.ui.view.LoadView; |
||||
import org.floens.chan.utils.AndroidUtils; |
||||
|
||||
import java.util.List; |
||||
|
||||
/** |
||||
* Wrapper around ThreadListLayout, so that it cleanly manages between loadbar and listview. |
||||
*/ |
||||
public class ThreadLayout extends LoadView implements ThreadPresenter.ThreadPresenterCallback { |
||||
private ThreadLayoutCallback callback; |
||||
private ThreadPresenter presenter; |
||||
|
||||
private ThreadListLayout threadListLayout; |
||||
private PostPopupHelper postPopupHelper; |
||||
private boolean visible; |
||||
|
||||
public ThreadLayout(Context context) { |
||||
super(context); |
||||
init(); |
||||
} |
||||
|
||||
public ThreadLayout(Context context, AttributeSet attrs) { |
||||
super(context, attrs); |
||||
init(); |
||||
} |
||||
|
||||
public ThreadLayout(Context context, AttributeSet attrs, int defStyleAttr) { |
||||
super(context, attrs, defStyleAttr); |
||||
init(); |
||||
} |
||||
|
||||
private void init() { |
||||
presenter = new ThreadPresenter(this); |
||||
|
||||
threadListLayout = new ThreadListLayout(getContext()); |
||||
threadListLayout.setCallbacks(presenter, presenter); |
||||
|
||||
postPopupHelper = new PostPopupHelper(getContext(), presenter); |
||||
|
||||
switchVisible(false); |
||||
} |
||||
|
||||
public void setCallback(ThreadLayoutCallback callback) { |
||||
this.callback = callback; |
||||
} |
||||
|
||||
public ThreadPresenter getPresenter() { |
||||
return presenter; |
||||
} |
||||
|
||||
@Override |
||||
public void showPosts(ChanThread thread) { |
||||
threadListLayout.showPosts(thread, !visible); |
||||
switchVisible(true); |
||||
} |
||||
|
||||
@Override |
||||
public void showError(VolleyError error) { |
||||
switchVisible(true); |
||||
threadListLayout.showError(error); |
||||
} |
||||
|
||||
@Override |
||||
public void showLoading() { |
||||
switchVisible(false); |
||||
} |
||||
|
||||
public void showPostInfo(String info) { |
||||
new AlertDialog.Builder(getContext()) |
||||
.setTitle(R.string.post_info) |
||||
.setMessage(info) |
||||
.setPositiveButton(R.string.ok, null) |
||||
.show(); |
||||
} |
||||
|
||||
public void showPostLinkables(final List<PostLinkable> linkables) { |
||||
String[] keys = new String[linkables.size()]; |
||||
for (int i = 0; i < linkables.size(); i++) { |
||||
keys[i] = linkables.get(i).key; |
||||
} |
||||
|
||||
new AlertDialog.Builder(getContext()) |
||||
.setItems(keys, new DialogInterface.OnClickListener() { |
||||
@Override |
||||
public void onClick(DialogInterface dialog, int which) { |
||||
presenter.onPostLinkableClicked(linkables.get(which)); |
||||
} |
||||
}) |
||||
.show(); |
||||
} |
||||
|
||||
public void clipboardPost(Post post) { |
||||
ClipboardManager clipboard = (ClipboardManager) AndroidUtils.getAppRes().getSystemService(Context.CLIPBOARD_SERVICE); |
||||
ClipData clip = ClipData.newPlainText("Post text", post.comment.toString()); |
||||
clipboard.setPrimaryClip(clip); |
||||
Toast.makeText(getContext(), R.string.post_text_copied_to_clipboard, Toast.LENGTH_SHORT).show(); |
||||
} |
||||
|
||||
@Override |
||||
public void openLink(final String link) { |
||||
if (ChanPreferences.getOpenLinkConfirmation()) { |
||||
new AlertDialog.Builder(getContext()) |
||||
.setNegativeButton(R.string.cancel, null) |
||||
.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |
||||
@Override |
||||
public void onClick(DialogInterface dialog, int which) { |
||||
AndroidUtils.openLink(link); |
||||
} |
||||
}) |
||||
.setTitle(R.string.open_link_confirmation) |
||||
.setMessage(link) |
||||
.show(); |
||||
} else { |
||||
AndroidUtils.openLink(link); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void showThread(Loadable threadLoadable) { |
||||
callback.openThread(threadLoadable); |
||||
} |
||||
|
||||
public void showPostsPopup(Post forPost, List<Post> posts) { |
||||
postPopupHelper.showPosts(forPost, posts); |
||||
} |
||||
|
||||
private void switchVisible(boolean visible) { |
||||
if (this.visible != visible) { |
||||
this.visible = visible; |
||||
setView(visible ? threadListLayout : null); |
||||
} |
||||
} |
||||
|
||||
public interface ThreadLayoutCallback { |
||||
public void openThread(Loadable threadLoadable); |
||||
} |
||||
} |
@ -0,0 +1,79 @@ |
||||
package org.floens.chan.ui.layout; |
||||
|
||||
import android.content.Context; |
||||
import android.util.AttributeSet; |
||||
import android.widget.ListView; |
||||
import android.widget.RelativeLayout; |
||||
|
||||
import com.android.volley.VolleyError; |
||||
|
||||
import org.floens.chan.core.model.ChanThread; |
||||
import org.floens.chan.ui.adapter.PostAdapter; |
||||
import org.floens.chan.ui.view.PostView; |
||||
|
||||
/** |
||||
* A layout that wraps around a listview to manage showing posts. |
||||
*/ |
||||
public class ThreadListLayout extends RelativeLayout { |
||||
private ListView listView; |
||||
private PostAdapter postAdapter; |
||||
private PostAdapter.PostAdapterCallback postAdapterCallback; |
||||
private PostView.PostViewCallback postViewCallback; |
||||
|
||||
private int restoreListViewIndex; |
||||
private int restoreListViewTop; |
||||
|
||||
public ThreadListLayout(Context context) { |
||||
super(context); |
||||
init(); |
||||
} |
||||
|
||||
public ThreadListLayout(Context context, AttributeSet attrs) { |
||||
super(context, attrs); |
||||
init(); |
||||
} |
||||
|
||||
public ThreadListLayout(Context context, AttributeSet attrs, int defStyleAttr) { |
||||
super(context, attrs, defStyleAttr); |
||||
init(); |
||||
} |
||||
|
||||
@Override |
||||
protected void onDetachedFromWindow() { |
||||
super.onDetachedFromWindow(); |
||||
restoreListViewIndex = listView.getFirstVisiblePosition(); |
||||
restoreListViewTop = listView.getChildAt(0) == null ? 0 : listView.getChildAt(0).getTop(); |
||||
} |
||||
|
||||
@Override |
||||
protected void onAttachedToWindow() { |
||||
super.onAttachedToWindow(); |
||||
listView.setSelectionFromTop(restoreListViewIndex, restoreListViewTop); |
||||
} |
||||
|
||||
private void init() { |
||||
listView = new ListView(getContext()); |
||||
addView(listView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); |
||||
} |
||||
|
||||
public void setCallbacks(PostAdapter.PostAdapterCallback postAdapterCallback, PostView.PostViewCallback postViewCallback) { |
||||
this.postAdapterCallback = postAdapterCallback; |
||||
this.postViewCallback = postViewCallback; |
||||
|
||||
postAdapter = new PostAdapter(getContext(), postAdapterCallback, postViewCallback); |
||||
listView.setAdapter(postAdapter); |
||||
} |
||||
|
||||
public void showPosts(ChanThread thread, boolean initial) { |
||||
if (initial) { |
||||
listView.setSelectionFromTop(0, 0); |
||||
restoreListViewIndex = 0; |
||||
restoreListViewTop = 0; |
||||
} |
||||
postAdapter.setThread(thread); |
||||
} |
||||
|
||||
public void showError(VolleyError error) { |
||||
|
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
package org.floens.chan.ui.toolbar; |
||||
|
||||
import android.widget.LinearLayout; |
||||
|
||||
public class NavigationItem { |
||||
public String title = ""; |
||||
public ToolbarMenu menu; |
||||
public boolean hasBack = true; |
||||
public LinearLayout view; |
||||
} |
@ -0,0 +1,220 @@ |
||||
package org.floens.chan.ui.toolbar; |
||||
|
||||
import android.animation.Animator; |
||||
import android.animation.AnimatorListenerAdapter; |
||||
import android.animation.AnimatorSet; |
||||
import android.animation.ObjectAnimator; |
||||
import android.animation.ValueAnimator; |
||||
import android.content.Context; |
||||
import android.graphics.Color; |
||||
import android.os.Build; |
||||
import android.text.TextUtils; |
||||
import android.util.AttributeSet; |
||||
import android.util.TypedValue; |
||||
import android.view.Gravity; |
||||
import android.view.View; |
||||
import android.view.animation.DecelerateInterpolator; |
||||
import android.widget.FrameLayout; |
||||
import android.widget.ImageView; |
||||
import android.widget.LinearLayout; |
||||
import android.widget.TextView; |
||||
|
||||
import org.floens.chan.R; |
||||
import org.floens.chan.ui.drawable.ArrowMenuDrawable; |
||||
import org.floens.chan.utils.AndroidUtils; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import static org.floens.chan.utils.AndroidUtils.dp; |
||||
import static org.floens.chan.utils.AndroidUtils.getAttrDrawable; |
||||
|
||||
public class Toolbar extends LinearLayout implements View.OnClickListener { |
||||
private ImageView arrowMenuView; |
||||
private ArrowMenuDrawable arrowMenuDrawable; |
||||
|
||||
private FrameLayout navigationItemContainer; |
||||
|
||||
private ToolbarCallback callback; |
||||
private NavigationItem navigationItem; |
||||
|
||||
public Toolbar(Context context) { |
||||
super(context); |
||||
init(); |
||||
} |
||||
|
||||
public Toolbar(Context context, AttributeSet attrs) { |
||||
super(context, attrs); |
||||
init(); |
||||
} |
||||
|
||||
public Toolbar(Context context, AttributeSet attrs, int defStyleAttr) { |
||||
super(context, attrs, defStyleAttr); |
||||
init(); |
||||
} |
||||
|
||||
public void setNavigationItem(final boolean animate, final boolean pushing, final NavigationItem item) { |
||||
if (item.menu != null) { |
||||
AndroidUtils.waitForMeasure(this, new AndroidUtils.OnMeasuredCallback() { |
||||
@Override |
||||
public void onMeasured(View view, int width, int height) { |
||||
setNavigationItemView(animate, pushing, item); |
||||
} |
||||
}); |
||||
} else { |
||||
setNavigationItemView(animate, pushing, item); |
||||
} |
||||
} |
||||
|
||||
public void setCallback(ToolbarCallback callback) { |
||||
this.callback = callback; |
||||
} |
||||
|
||||
@Override |
||||
public void onClick(View v) { |
||||
if (v == arrowMenuView) { |
||||
if (callback != null) { |
||||
callback.onMenuBackClicked(arrowMenuDrawable.getProgress() == 1f); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void setArrowMenuProgress(float progress) { |
||||
arrowMenuDrawable.setProgress(progress); |
||||
} |
||||
|
||||
private void init() { |
||||
setOrientation(HORIZONTAL); |
||||
|
||||
FrameLayout leftButtonContainer = new FrameLayout(getContext()); |
||||
addView(leftButtonContainer, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); |
||||
|
||||
arrowMenuView = new ImageView(getContext()); |
||||
arrowMenuView.setOnClickListener(this); |
||||
arrowMenuView.setFocusable(true); |
||||
arrowMenuView.setScaleType(ImageView.ScaleType.CENTER); |
||||
arrowMenuDrawable = new ArrowMenuDrawable(); |
||||
arrowMenuView.setImageDrawable(arrowMenuDrawable); |
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
||||
//noinspection deprecation
|
||||
arrowMenuView.setBackgroundDrawable(getAttrDrawable(android.R.attr.selectableItemBackgroundBorderless)); |
||||
} else { |
||||
//noinspection deprecation
|
||||
arrowMenuView.setBackgroundResource(R.drawable.gray_background_selector); |
||||
} |
||||
|
||||
leftButtonContainer.addView(arrowMenuView, new FrameLayout.LayoutParams(dp(56), FrameLayout.LayoutParams.MATCH_PARENT, Gravity.CENTER_VERTICAL)); |
||||
|
||||
navigationItemContainer = new FrameLayout(getContext()); |
||||
addView(navigationItemContainer, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)); |
||||
} |
||||
|
||||
private void setNavigationItemView(boolean animate, boolean pushing, NavigationItem toItem) { |
||||
toItem.view = createNavigationItemView(toItem); |
||||
|
||||
navigationItemContainer.addView(toItem.view, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); |
||||
|
||||
final NavigationItem fromItem = navigationItem; |
||||
|
||||
final int duration = 300; |
||||
final int offset = dp(16); |
||||
|
||||
if (animate) { |
||||
toItem.view.setAlpha(0f); |
||||
|
||||
List<Animator> animations = new ArrayList<>(5); |
||||
|
||||
if (fromItem != null && fromItem.hasBack != toItem.hasBack) { |
||||
ValueAnimator arrowAnimation = ValueAnimator.ofFloat(fromItem.hasBack ? 1f : 0f, toItem.hasBack ? 1f : 0f); |
||||
arrowAnimation.setDuration(duration); |
||||
arrowAnimation.setInterpolator(new DecelerateInterpolator(2f)); |
||||
arrowAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { |
||||
@Override |
||||
public void onAnimationUpdate(ValueAnimator animation) { |
||||
setArrowMenuProgress((float) animation.getAnimatedValue()); |
||||
} |
||||
}); |
||||
animations.add(arrowAnimation); |
||||
} else { |
||||
setArrowMenuProgress(toItem.hasBack ? 1f : 0f); |
||||
} |
||||
|
||||
Animator toYAnimation = ObjectAnimator.ofFloat(toItem.view, View.TRANSLATION_Y, pushing ? offset : -offset, 0f); |
||||
toYAnimation.setDuration(duration); |
||||
toYAnimation.setInterpolator(new DecelerateInterpolator(2f)); |
||||
toYAnimation.addListener(new AnimatorListenerAdapter() { |
||||
@Override |
||||
public void onAnimationEnd(Animator animation) { |
||||
if (fromItem != null) { |
||||
removeNavigationItem(fromItem); |
||||
} |
||||
} |
||||
}); |
||||
animations.add(toYAnimation); |
||||
|
||||
Animator toAlphaAnimation = ObjectAnimator.ofFloat(toItem.view, View.ALPHA, 0f, 1f); |
||||
toAlphaAnimation.setDuration(duration); |
||||
toAlphaAnimation.setInterpolator(new DecelerateInterpolator(2f)); |
||||
animations.add(toAlphaAnimation); |
||||
|
||||
if (fromItem != null) { |
||||
Animator fromYAnimation = ObjectAnimator.ofFloat(fromItem.view, View.TRANSLATION_Y, 0f, pushing ? -offset : offset); |
||||
fromYAnimation.setDuration(duration); |
||||
fromYAnimation.setInterpolator(new DecelerateInterpolator(2f)); |
||||
animations.add(fromYAnimation); |
||||
|
||||
Animator fromAlphaAnimation = ObjectAnimator.ofFloat(fromItem.view, View.ALPHA, 1f, 0f); |
||||
fromAlphaAnimation.setDuration(duration); |
||||
fromAlphaAnimation.setInterpolator(new DecelerateInterpolator(2f)); |
||||
animations.add(fromAlphaAnimation); |
||||
} |
||||
|
||||
AnimatorSet set = new AnimatorSet(); |
||||
set.setStartDelay(pushing ? 0 : 100); |
||||
set.playTogether(animations); |
||||
set.start(); |
||||
} else { |
||||
// No animation
|
||||
if (fromItem != null) { |
||||
removeNavigationItem(fromItem); |
||||
} |
||||
setArrowMenuProgress(toItem.hasBack ? 1f : 0f); |
||||
} |
||||
|
||||
navigationItem = toItem; |
||||
} |
||||
|
||||
private void removeNavigationItem(NavigationItem item) { |
||||
item.view.removeAllViews(); |
||||
navigationItemContainer.removeView(item.view); |
||||
item.view = null; |
||||
} |
||||
|
||||
private LinearLayout createNavigationItemView(NavigationItem item) { |
||||
LinearLayout wrapper = new LinearLayout(getContext()); |
||||
|
||||
TextView titleView = new TextView(getContext()); |
||||
titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20); |
||||
// titleView.setTextColor(Color.argb((int)(0.87 * 255.0), 0, 0, 0));
|
||||
titleView.setTextColor(Color.argb(255, 255, 255, 255)); |
||||
titleView.setGravity(Gravity.CENTER_VERTICAL); |
||||
titleView.setSingleLine(true); |
||||
titleView.setLines(1); |
||||
titleView.setEllipsize(TextUtils.TruncateAt.END); |
||||
titleView.setPadding(dp(16), 0, 0, 0); |
||||
titleView.setTypeface(AndroidUtils.ROBOTO_MEDIUM); |
||||
titleView.setText(item.title); |
||||
wrapper.addView(titleView, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)); |
||||
|
||||
if (item.menu != null) { |
||||
wrapper.addView(item.menu, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT)); |
||||
} |
||||
|
||||
return wrapper; |
||||
} |
||||
|
||||
public interface ToolbarCallback { |
||||
public void onMenuBackClicked(boolean isArrow); |
||||
} |
||||
} |
@ -0,0 +1,71 @@ |
||||
package org.floens.chan.ui.toolbar; |
||||
|
||||
import android.content.Context; |
||||
import android.util.AttributeSet; |
||||
import android.view.Gravity; |
||||
import android.widget.ImageView; |
||||
import android.widget.LinearLayout; |
||||
|
||||
import org.floens.chan.R; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import static org.floens.chan.utils.AndroidUtils.dp; |
||||
|
||||
public class ToolbarMenu extends LinearLayout { |
||||
private List<ToolbarMenuItem> items = new ArrayList<>(); |
||||
|
||||
public ToolbarMenu(Context context) { |
||||
super(context); |
||||
init(); |
||||
} |
||||
|
||||
public ToolbarMenu(Context context, AttributeSet attrs) { |
||||
super(context, attrs); |
||||
init(); |
||||
} |
||||
|
||||
public ToolbarMenu(Context context, AttributeSet attrs, int defStyleAttr) { |
||||
super(context, attrs, defStyleAttr); |
||||
init(); |
||||
} |
||||
|
||||
private void init() { |
||||
setOrientation(HORIZONTAL); |
||||
setGravity(Gravity.CENTER_VERTICAL); |
||||
|
||||
// overflowItem = new ToolbarMenuItem(getContext(), this, 100, R.drawable.ic_more_vert_white_24dp, 10 + 32);
|
||||
//
|
||||
// List<ToolbarMenuItemSubMenu.SubItem> subItems = new ArrayList<>();
|
||||
// subItems.add(new ToolbarMenuItemSubMenu.SubItem(1, "Sub 1"));
|
||||
// subItems.add(new ToolbarMenuItemSubMenu.SubItem(2, "Sub 2"));
|
||||
// subItems.add(new ToolbarMenuItemSubMenu.SubItem(3, "Sub 3"));
|
||||
//
|
||||
// ToolbarMenuItemSubMenu sub = new ToolbarMenuItemSubMenu(getContext(), overflowItem.getView(), subItems);
|
||||
// overflowItem.setSubMenu(sub);
|
||||
//
|
||||
// addItem(overflowItem);
|
||||
} |
||||
|
||||
public ToolbarMenuItem addItem(ToolbarMenuItem item) { |
||||
items.add(item); |
||||
ImageView icon = item.getView(); |
||||
if (icon != null) { |
||||
int viewIndex = Math.min(getChildCount(), item.getId()); |
||||
addView(icon, viewIndex); |
||||
} |
||||
return item; |
||||
} |
||||
|
||||
public ToolbarMenuItem createOverflow(ToolbarMenuItem.ToolbarMenuItemCallback callback) { |
||||
ToolbarMenuItem overflow = addItem(new ToolbarMenuItem(getContext(), callback, 100, R.drawable.ic_more)); |
||||
ImageView overflowImage = overflow.getView(); |
||||
// 36dp
|
||||
overflowImage.setLayoutParams(new LinearLayout.LayoutParams(dp(36), dp(54))); |
||||
int p = dp(16); |
||||
overflowImage.setPadding(0, 0, p, 0); |
||||
|
||||
return overflow; |
||||
} |
||||
} |
@ -0,0 +1,86 @@ |
||||
package org.floens.chan.ui.toolbar; |
||||
|
||||
import android.content.Context; |
||||
import android.graphics.drawable.Drawable; |
||||
import android.os.Build; |
||||
import android.view.View; |
||||
import android.widget.ImageView; |
||||
import android.widget.LinearLayout; |
||||
|
||||
import org.floens.chan.R; |
||||
|
||||
import static org.floens.chan.utils.AndroidUtils.dp; |
||||
import static org.floens.chan.utils.AndroidUtils.getAttrDrawable; |
||||
|
||||
public class ToolbarMenuItem implements View.OnClickListener, ToolbarMenuSubMenu.ToolbarMenuItemSubMenuCallback { |
||||
private ToolbarMenuItemCallback callback; |
||||
private int id; |
||||
private ToolbarMenuSubMenu subMenu; |
||||
|
||||
private ImageView imageView; |
||||
|
||||
public ToolbarMenuItem(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, int id, int drawable) { |
||||
this(context, callback, id, context.getResources().getDrawable(drawable)); |
||||
} |
||||
|
||||
public ToolbarMenuItem(Context context, ToolbarMenuItem.ToolbarMenuItemCallback callback, int id, Drawable drawable) { |
||||
this.id = id; |
||||
this.callback = callback; |
||||
|
||||
if (drawable != null) { |
||||
imageView = new ImageView(context); |
||||
imageView.setOnClickListener(this); |
||||
imageView.setFocusable(true); |
||||
imageView.setScaleType(ImageView.ScaleType.CENTER); |
||||
imageView.setLayoutParams(new LinearLayout.LayoutParams(dp(56), dp(56))); |
||||
int p = dp(16); |
||||
imageView.setPadding(p, p, p, p); |
||||
|
||||
imageView.setImageDrawable(drawable); |
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
||||
//noinspection deprecation
|
||||
imageView.setBackgroundDrawable(getAttrDrawable(android.R.attr.selectableItemBackgroundBorderless)); |
||||
} else { |
||||
//noinspection deprecation
|
||||
imageView.setBackgroundResource(R.drawable.gray_background_selector); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public void setSubMenu(ToolbarMenuSubMenu subMenu) { |
||||
this.subMenu = subMenu; |
||||
subMenu.setCallback(this); |
||||
} |
||||
|
||||
public void showSubMenu() { |
||||
subMenu.show(); |
||||
} |
||||
|
||||
@Override |
||||
public void onClick(View v) { |
||||
if (subMenu != null) { |
||||
subMenu.show(); |
||||
} |
||||
callback.onMenuItemClicked(this); |
||||
} |
||||
|
||||
public int getId() { |
||||
return id; |
||||
} |
||||
|
||||
public ImageView getView() { |
||||
return imageView; |
||||
} |
||||
|
||||
@Override |
||||
public void onSubMenuItemClicked(ToolbarMenuSubItem item) { |
||||
callback.onSubMenuItemClicked(this, item); |
||||
} |
||||
|
||||
public interface ToolbarMenuItemCallback { |
||||
public void onMenuItemClicked(ToolbarMenuItem item); |
||||
|
||||
public void onSubMenuItemClicked(ToolbarMenuItem parent, ToolbarMenuSubItem item); |
||||
} |
||||
} |
@ -0,0 +1,19 @@ |
||||
package org.floens.chan.ui.toolbar; |
||||
|
||||
public class ToolbarMenuSubItem { |
||||
private int id; |
||||
private String text; |
||||
|
||||
public ToolbarMenuSubItem(int id, String text) { |
||||
this.id = id; |
||||
this.text = text; |
||||
} |
||||
|
||||
public int getId() { |
||||
return id; |
||||
} |
||||
|
||||
public String getText() { |
||||
return text; |
||||
} |
||||
} |
@ -0,0 +1,112 @@ |
||||
package org.floens.chan.ui.toolbar; |
||||
|
||||
import android.content.Context; |
||||
import android.support.v7.widget.ListPopupWindow; |
||||
import android.view.Gravity; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import android.view.ViewTreeObserver; |
||||
import android.widget.AdapterView; |
||||
import android.widget.ArrayAdapter; |
||||
import android.widget.PopupWindow; |
||||
import android.widget.TextView; |
||||
|
||||
import org.floens.chan.R; |
||||
import org.floens.chan.utils.AndroidUtils; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
|
||||
import static org.floens.chan.utils.AndroidUtils.dp; |
||||
|
||||
public class ToolbarMenuSubMenu { |
||||
private final Context context; |
||||
private final View anchor; |
||||
private List<ToolbarMenuSubItem> items; |
||||
private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener; |
||||
|
||||
private ToolbarMenuItemSubMenuCallback callback; |
||||
|
||||
public ToolbarMenuSubMenu(Context context, View anchor, List<ToolbarMenuSubItem> items) { |
||||
this.context = context; |
||||
this.anchor = anchor; |
||||
this.items = items; |
||||
} |
||||
|
||||
public void setCallback(ToolbarMenuItemSubMenuCallback callback) { |
||||
this.callback = callback; |
||||
} |
||||
|
||||
public void show() { |
||||
final ListPopupWindow popupWindow = new ListPopupWindow(context); |
||||
popupWindow.setAnchorView(anchor); |
||||
popupWindow.setModal(true); |
||||
popupWindow.setDropDownGravity(Gravity.RIGHT | Gravity.TOP); |
||||
popupWindow.setVerticalOffset(-anchor.getHeight() + dp(5)); |
||||
popupWindow.setHorizontalOffset(-dp(5)); |
||||
popupWindow.setContentWidth(dp(3 * 56)); |
||||
|
||||
List<String> stringItems = new ArrayList<>(items.size()); |
||||
for (ToolbarMenuSubItem item : items) { |
||||
stringItems.add(item.getText()); |
||||
} |
||||
|
||||
popupWindow.setAdapter(new SubMenuArrayAdapter(context, R.layout.toolbar_menu_item, stringItems)); |
||||
popupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { |
||||
@Override |
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { |
||||
if (position >= 0 && position < items.size()) { |
||||
callback.onSubMenuItemClicked(items.get(position)); |
||||
popupWindow.dismiss(); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { |
||||
@Override |
||||
public void onGlobalLayout() { |
||||
if (popupWindow.isShowing()) { |
||||
// Recalculate anchor position
|
||||
popupWindow.show(); |
||||
} |
||||
} |
||||
}; |
||||
anchor.getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener); |
||||
|
||||
popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() { |
||||
@Override |
||||
public void onDismiss() { |
||||
if (anchor.getViewTreeObserver().isAlive()) { |
||||
anchor.getViewTreeObserver().removeGlobalOnLayoutListener(globalLayoutListener); |
||||
} |
||||
globalLayoutListener = null; |
||||
} |
||||
}); |
||||
|
||||
popupWindow.show(); |
||||
} |
||||
|
||||
public interface ToolbarMenuItemSubMenuCallback { |
||||
public void onSubMenuItemClicked(ToolbarMenuSubItem item); |
||||
} |
||||
|
||||
private static class SubMenuArrayAdapter extends ArrayAdapter<String> { |
||||
public SubMenuArrayAdapter(Context context, int resource, List<String> objects) { |
||||
super(context, resource, objects); |
||||
} |
||||
|
||||
@Override |
||||
public View getDropDownView(int position, View convertView, ViewGroup parent) { |
||||
if (convertView == null) { |
||||
convertView = LayoutInflater.from(getContext()).inflate(R.layout.toolbar_menu_item, parent, false); |
||||
} |
||||
|
||||
TextView textView = (TextView) convertView; |
||||
textView.setText(getItem(position)); |
||||
textView.setTypeface(AndroidUtils.ROBOTO_MEDIUM); |
||||
|
||||
return textView; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,193 @@ |
||||
package org.floens.chan.utils; |
||||
|
||||
import android.app.Dialog; |
||||
import android.content.Context; |
||||
import android.content.DialogInterface; |
||||
import android.content.Intent; |
||||
import android.content.res.Resources; |
||||
import android.content.res.TypedArray; |
||||
import android.graphics.Typeface; |
||||
import android.graphics.drawable.Drawable; |
||||
import android.net.Uri; |
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
import android.util.Log; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import android.view.ViewTreeObserver; |
||||
import android.view.inputmethod.InputMethodManager; |
||||
import android.widget.Toast; |
||||
|
||||
import org.floens.chan.ChanApplication; |
||||
import org.floens.chan.R; |
||||
|
||||
import java.util.HashMap; |
||||
|
||||
public class AndroidUtils { |
||||
public final static ViewGroup.LayoutParams MATCH_PARAMS = new ViewGroup.LayoutParams( |
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); |
||||
public final static ViewGroup.LayoutParams WRAP_PARAMS = new ViewGroup.LayoutParams( |
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); |
||||
public final static ViewGroup.LayoutParams MATCH_WRAP_PARAMS = new ViewGroup.LayoutParams( |
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); |
||||
public final static ViewGroup.LayoutParams WRAP_MATCH_PARAMS = new ViewGroup.LayoutParams( |
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); |
||||
private static HashMap<String, Typeface> typefaceCache = new HashMap<>(); |
||||
|
||||
public static Typeface ROBOTO_MEDIUM; |
||||
|
||||
public static void init() { |
||||
ROBOTO_MEDIUM = getTypeface("Roboto-Medium.ttf"); |
||||
} |
||||
|
||||
public static Resources getRes() { |
||||
return ChanApplication.con.getResources(); |
||||
} |
||||
|
||||
public static Context getAppRes() { |
||||
return ChanApplication.con; |
||||
} |
||||
|
||||
public static void openLink(String link) { |
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link)); |
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
||||
|
||||
if (intent.resolveActivity(getAppRes().getPackageManager()) != null) { |
||||
getAppRes().startActivity(intent); |
||||
} else { |
||||
Toast.makeText(getAppRes(), R.string.open_link_failed, Toast.LENGTH_LONG).show(); |
||||
} |
||||
} |
||||
|
||||
public static void shareLink(String link) { |
||||
Intent intent = new Intent(Intent.ACTION_SEND); |
||||
intent.setType("text/plain"); |
||||
intent.putExtra(Intent.EXTRA_TEXT, link); |
||||
Intent chooser = Intent.createChooser(intent, getRes().getString(R.string.action_share)); |
||||
chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
||||
|
||||
if (chooser.resolveActivity(getAppRes().getPackageManager()) != null) { |
||||
getAppRes().startActivity(chooser); |
||||
} else { |
||||
Toast.makeText(getAppRes(), R.string.open_link_failed, Toast.LENGTH_LONG).show(); |
||||
} |
||||
} |
||||
|
||||
public static int getAttrPixel(int attr) { |
||||
TypedArray typedArray = ChanApplication.con.getTheme().obtainStyledAttributes(new int[]{attr}); |
||||
int pixels = typedArray.getDimensionPixelSize(0, 0); |
||||
typedArray.recycle(); |
||||
return pixels; |
||||
} |
||||
|
||||
public static Drawable getAttrDrawable(int attr) { |
||||
TypedArray typedArray = ChanApplication.con.getTheme().obtainStyledAttributes(new int[]{attr}); |
||||
Drawable drawable = typedArray.getDrawable(0); |
||||
typedArray.recycle(); |
||||
return drawable; |
||||
} |
||||
|
||||
public static int dp(float dp) { |
||||
return (int) (dp * getRes().getDisplayMetrics().density); |
||||
} |
||||
|
||||
public static Typeface getTypeface(String name) { |
||||
if (!typefaceCache.containsKey(name)) { |
||||
Typeface typeface = Typeface.createFromAsset(getRes().getAssets(), "font/" + name); |
||||
typefaceCache.put(name, typeface); |
||||
} |
||||
return typefaceCache.get(name); |
||||
} |
||||
|
||||
/** |
||||
* Causes the runnable to be added to the message queue. The runnable will |
||||
* be run on the ui thread. |
||||
* |
||||
* @param runnable |
||||
*/ |
||||
public static void runOnUiThread(Runnable runnable) { |
||||
new Handler(Looper.getMainLooper()).post(runnable); |
||||
} |
||||
|
||||
public static void requestKeyboardFocus(Dialog dialog, final View view) { |
||||
view.requestFocus(); |
||||
dialog.setOnShowListener(new DialogInterface.OnShowListener() { |
||||
@Override |
||||
public void onShow(DialogInterface dialog) { |
||||
InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService( |
||||
Context.INPUT_METHOD_SERVICE); |
||||
imm.showSoftInput(view, 0); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public static String getReadableFileSize(int bytes, boolean si) { |
||||
int unit = si ? 1000 : 1024; |
||||
if (bytes < unit) |
||||
return bytes + " B"; |
||||
int exp = (int) (Math.log(bytes) / Math.log(unit)); |
||||
String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); |
||||
return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); |
||||
} |
||||
|
||||
public static CharSequence ellipsize(CharSequence text, int max) { |
||||
if (text.length() <= max) { |
||||
return text; |
||||
} else { |
||||
return text.subSequence(0, max) + "\u2026"; |
||||
} |
||||
} |
||||
|
||||
public interface OnMeasuredCallback { |
||||
void onMeasured(View view, int width, int height); |
||||
} |
||||
|
||||
/** |
||||
* Waits for a measure. Calls callback immediately if the view width and height are more than 0. |
||||
* Otherwise it registers an onpredrawlistener and rechedules a layout. |
||||
* Warning: the view you give must be attached to the view root!!! |
||||
*/ |
||||
public static void waitForMeasure(final View view, final OnMeasuredCallback callback) { |
||||
waitForMeasure(true, view, callback); |
||||
} |
||||
|
||||
public static void waitForLayout(final View view, final OnMeasuredCallback callback) { |
||||
waitForMeasure(false, view, callback); |
||||
} |
||||
|
||||
private static void waitForMeasure(boolean returnIfZero, final View view, final OnMeasuredCallback callback) { |
||||
int width = view.getWidth(); |
||||
int height = view.getHeight(); |
||||
|
||||
if (returnIfZero && width > 0 && height > 0) { |
||||
callback.onMeasured(view, width, height); |
||||
} else { |
||||
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { |
||||
@Override |
||||
public boolean onPreDraw() { |
||||
final ViewTreeObserver observer = view.getViewTreeObserver(); |
||||
if (observer.isAlive()) { |
||||
observer.removeOnPreDrawListener(this); |
||||
} |
||||
|
||||
try { |
||||
callback.onMeasured(view, view.getWidth(), view.getHeight()); |
||||
} catch (Exception e) { |
||||
Log.i("AndroidUtils", "Exception in onMeasured", e); |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
public static void setPressedDrawable(View view) { |
||||
TypedArray arr = view.getContext().obtainStyledAttributes(new int[]{android.R.attr.selectableItemBackground}); |
||||
|
||||
Drawable drawable = arr.getDrawable(0); |
||||
|
||||
arr.recycle(); |
||||
view.setBackgroundDrawable(drawable); |
||||
} |
||||
} |
@ -0,0 +1,51 @@ |
||||
package org.floens.chan.utils; |
||||
|
||||
import android.graphics.Matrix; |
||||
import android.graphics.Point; |
||||
import android.graphics.Rect; |
||||
import android.widget.ImageView; |
||||
|
||||
public class AnimationUtils { |
||||
/** |
||||
* On your start view call startView.getGlobalVisibleRect(startBounds) |
||||
* and on your end container call endContainer.getGlobalVisibleRect(endBounds, globalOffset);<br> |
||||
* startBounds and endBounds will be adjusted appropriately and the starting scale will be returned. |
||||
* |
||||
* @param startBounds your startBounds |
||||
* @param endBounds your endBounds |
||||
* @param globalOffset your globalOffset |
||||
* @return the starting scale |
||||
*/ |
||||
public static float calculateBoundsAnimation(Rect startBounds, Rect endBounds, Point globalOffset) { |
||||
startBounds.offset(-globalOffset.x, -globalOffset.y); |
||||
endBounds.offset(-globalOffset.x, -globalOffset.y); |
||||
|
||||
float startScale; |
||||
if ((float) endBounds.width() / endBounds.height() > (float) startBounds.width() / startBounds.height()) { |
||||
// Extend start bounds horizontally
|
||||
startScale = (float) startBounds.height() / endBounds.height(); |
||||
float startWidth = startScale * endBounds.width(); |
||||
float deltaWidth = (startWidth - startBounds.width()) / 2; |
||||
startBounds.left -= deltaWidth; |
||||
startBounds.right += deltaWidth; |
||||
} else { |
||||
// Extend start bounds vertically
|
||||
startScale = (float) startBounds.width() / endBounds.width(); |
||||
float startHeight = startScale * endBounds.height(); |
||||
float deltaHeight = (startHeight - startBounds.height()) / 2; |
||||
startBounds.top -= deltaHeight; |
||||
startBounds.bottom += deltaHeight; |
||||
} |
||||
|
||||
return startScale; |
||||
} |
||||
|
||||
public static void adjustImageViewBoundsToDrawableBounds(ImageView imageView, Rect bounds) { |
||||
float[] f = new float[9]; |
||||
imageView.getImageMatrix().getValues(f); |
||||
bounds.left += f[Matrix.MTRANS_X]; |
||||
bounds.top += f[Matrix.MTRANS_Y]; |
||||
bounds.right = (bounds.left + (int) (imageView.getDrawable().getIntrinsicWidth() * f[Matrix.MSCALE_X])); |
||||
bounds.bottom = (bounds.top + (int) (imageView.getDrawable().getIntrinsicHeight() * f[Matrix.MSCALE_Y])); |
||||
} |
||||
} |
@ -1,42 +0,0 @@ |
||||
/* |
||||
* Clover - 4chan browser https://github.com/Floens/Clover/
|
||||
* Copyright (C) 2014 Floens |
||||
* |
||||
* 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 org.floens.chan.utils; |
||||
|
||||
import android.animation.Animator; |
||||
import android.animation.Animator.AnimatorListener; |
||||
|
||||
/** |
||||
* Extends AnimatorListener with no-op methods. |
||||
*/ |
||||
public class SimpleAnimatorListener implements AnimatorListener { |
||||
@Override |
||||
public void onAnimationCancel(Animator animation) { |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationEnd(Animator animation) { |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationRepeat(Animator animation) { |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationStart(Animator animation) { |
||||
} |
||||
} |
@ -1,130 +0,0 @@ |
||||
/* |
||||
* Clover - 4chan browser https://github.com/Floens/Clover/
|
||||
* Copyright (C) 2014 Floens |
||||
* |
||||
* 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 org.floens.chan.utils; |
||||
|
||||
import android.app.Dialog; |
||||
import android.content.ActivityNotFoundException; |
||||
import android.content.Context; |
||||
import android.content.DialogInterface; |
||||
import android.content.DialogInterface.OnShowListener; |
||||
import android.content.Intent; |
||||
import android.content.res.TypedArray; |
||||
import android.graphics.drawable.Drawable; |
||||
import android.net.Uri; |
||||
import android.os.Handler; |
||||
import android.os.Looper; |
||||
import android.util.DisplayMetrics; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import android.view.inputmethod.InputMethodManager; |
||||
import android.widget.Toast; |
||||
|
||||
import org.floens.chan.ChanApplication; |
||||
import org.floens.chan.R; |
||||
|
||||
public class Utils { |
||||
private static DisplayMetrics displayMetrics; |
||||
|
||||
public final static ViewGroup.LayoutParams MATCH_PARAMS = new ViewGroup.LayoutParams( |
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); |
||||
public final static ViewGroup.LayoutParams WRAP_PARAMS = new ViewGroup.LayoutParams( |
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); |
||||
public final static ViewGroup.LayoutParams MATCH_WRAP_PARAMS = new ViewGroup.LayoutParams( |
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); |
||||
public final static ViewGroup.LayoutParams WRAP_MATCH_PARAMS = new ViewGroup.LayoutParams( |
||||
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT); |
||||
|
||||
public static int dp(float dp) { |
||||
// return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
|
||||
if (displayMetrics == null) { |
||||
displayMetrics = ChanApplication.getInstance().getResources().getDisplayMetrics(); |
||||
} |
||||
|
||||
return (int) (dp * displayMetrics.density); |
||||
} |
||||
|
||||
/** |
||||
* Sets the android.R.attr.selectableItemBackground as background drawable |
||||
* on the view. |
||||
* |
||||
* @param view |
||||
*/ |
||||
@SuppressWarnings("deprecation") |
||||
public static void setPressedDrawable(View view) { |
||||
Drawable drawable = Utils.getSelectableBackgroundDrawable(view.getContext()); |
||||
view.setBackgroundDrawable(drawable); |
||||
} |
||||
|
||||
public static Drawable getSelectableBackgroundDrawable(Context context) { |
||||
TypedArray arr = context.obtainStyledAttributes(new int[]{android.R.attr.selectableItemBackground}); |
||||
|
||||
Drawable drawable = arr.getDrawable(0); |
||||
|
||||
arr.recycle(); |
||||
|
||||
return drawable; |
||||
} |
||||
|
||||
/** |
||||
* Causes the runnable to be added to the message queue. The runnable will |
||||
* be run on the ui thread. |
||||
* |
||||
* @param runnable |
||||
*/ |
||||
public static void runOnUiThread(Runnable runnable) { |
||||
new Handler(Looper.getMainLooper()).post(runnable); |
||||
} |
||||
|
||||
public static void requestKeyboardFocus(Dialog dialog, final View view) { |
||||
view.requestFocus(); |
||||
dialog.setOnShowListener(new OnShowListener() { |
||||
@Override |
||||
public void onShow(DialogInterface dialog) { |
||||
InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService( |
||||
Context.INPUT_METHOD_SERVICE); |
||||
imm.showSoftInput(view, 0); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public static void openLink(Context context, String link) { |
||||
try { |
||||
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(link))); |
||||
} catch (ActivityNotFoundException e) { |
||||
e.printStackTrace(); |
||||
Toast.makeText(context, R.string.open_link_failed, Toast.LENGTH_LONG).show(); |
||||
} |
||||
} |
||||
|
||||
public static String getReadableFileSize(int bytes, boolean si) { |
||||
int unit = si ? 1000 : 1024; |
||||
if (bytes < unit) |
||||
return bytes + " B"; |
||||
int exp = (int) (Math.log(bytes) / Math.log(unit)); |
||||
String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); |
||||
return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); |
||||
} |
||||
|
||||
public static CharSequence ellipsize(CharSequence text, int max) { |
||||
if (text.length() <= max) { |
||||
return text; |
||||
} else { |
||||
return text.subSequence(0, max) + "\u2026"; |
||||
} |
||||
} |
||||
} |
After Width: | Height: | Size: 219 B |
After Width: | Height: | Size: 202 B |
After Width: | Height: | Size: 269 B |
After Width: | Height: | Size: 313 B |
After Width: | Height: | Size: 393 B |
@ -0,0 +1,14 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android"> |
||||
<item android:state_pressed="true"> |
||||
<shape> |
||||
<solid android:color="#20000000" /> |
||||
</shape> |
||||
</item> |
||||
<item android:state_focused="true"> |
||||
<shape> |
||||
<solid android:color="#20000000" /> |
||||
</shape> |
||||
</item> |
||||
<item android:drawable="@android:color/transparent" /> |
||||
</selector> |
@ -0,0 +1,12 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<org.floens.multipanetest.layout.ImageViewLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:background="#ff000000"> |
||||
|
||||
<ImageView |
||||
android:id="@+id/image" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" /> |
||||
|
||||
</org.floens.multipanetest.layout.ImageViewLayout> |
@ -0,0 +1,42 @@ |
||||
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:id="@+id/drawer_layout" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent"> |
||||
|
||||
<LinearLayout |
||||
android:id="@+id/root_layout" |
||||
android:orientation="vertical" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:background="#ffffffff"> |
||||
|
||||
<org.floens.chan.ui.toolbar.Toolbar |
||||
android:elevation="8dp" |
||||
android:id="@+id/toolbar" |
||||
android:background="@color/primary" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="56dp" /> |
||||
|
||||
<FrameLayout |
||||
android:id="@+id/container" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="0dp" |
||||
android:layout_weight="1" /> |
||||
|
||||
</LinearLayout> |
||||
|
||||
<FrameLayout |
||||
android:background="#ffffff00" |
||||
android:id="@+id/drawer" |
||||
android:layout_gravity="left" |
||||
android:layout_width="200dp" |
||||
android:layout_height="match_parent"> |
||||
|
||||
<TextView |
||||
android:text="Hello, world!" |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" /> |
||||
|
||||
</FrameLayout> |
||||
|
||||
</android.support.v4.widget.DrawerLayout> |
@ -0,0 +1,13 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" |
||||
android:background="#ffffffff" |
||||
android:orientation="vertical"> |
||||
|
||||
<TextView |
||||
android:layout_width="wrap_content" |
||||
android:layout_height="wrap_content" |
||||
android:text="Settings here" /> |
||||
|
||||
</LinearLayout> |
@ -0,0 +1,9 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="48dp" |
||||
android:textSize="16sp" |
||||
android:textColor="#ff000000" |
||||
android:gravity="center_vertical" |
||||
android:paddingLeft="16dp" |
||||
android:paddingRight="16dp" /> |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue