replace last usages of layout animations

fix threadlistlayout recyclerview padding mess
animate search like reply with a translation animation
dont show comment counter when we dont know the board max comment count
remove some todo's
multisite
Floens 8 years ago
parent 077d3167a0
commit 759744984e
  1. 1
      Clover/app/src/main/java/org/floens/chan/core/model/orm/Filter.java
  2. 3
      Clover/app/src/main/java/org/floens/chan/core/presenter/ReplyPresenter.java
  3. 157
      Clover/app/src/main/java/org/floens/chan/ui/animation/AnimationUtils.java
  4. 5
      Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
  5. 23
      Clover/app/src/main/java/org/floens/chan/ui/controller/PassSettingsController.java
  6. 7
      Clover/app/src/main/java/org/floens/chan/ui/layout/ReplyLayout.java
  7. 86
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadListLayout.java
  8. 22
      Clover/app/src/main/java/org/floens/chan/ui/settings/SettingsController.java
  9. 4
      Clover/app/src/main/res/layout/controller_pass.xml
  10. 2
      Clover/app/src/main/res/layout/layout_thread_list.xml

@ -40,7 +40,6 @@ public class Filter {
@DatabaseField(canBeNull = false) @DatabaseField(canBeNull = false)
public boolean allBoards = true; public boolean allBoards = true;
// TODO(multi-site)
@DatabaseField(canBeNull = false) @DatabaseField(canBeNull = false)
public String boards; public String boards;

@ -111,6 +111,7 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
callback.loadDraftIntoViews(draft); callback.loadDraftIntoViews(draft);
callback.updateCommentCount(0, board.maxCommentChars, false); callback.updateCommentCount(0, board.maxCommentChars, false);
callback.setCommentHint(getString(loadable.isThreadMode() ? R.string.reply_comment_thread : R.string.reply_comment_board)); callback.setCommentHint(getString(loadable.isThreadMode() ? R.string.reply_comment_thread : R.string.reply_comment_board));
callback.showCommentCounter(board.maxCommentChars > 0);
if (draft.file != null) { if (draft.file != null) {
showPreview(draft.fileName, draft.file); showPreview(draft.fileName, draft.file);
@ -453,6 +454,8 @@ public class ReplyPresenter implements CaptchaCallback, ImagePickDelegate.ImageP
void setCommentHint(String hint); void setCommentHint(String hint);
void showCommentCounter(boolean show);
void setExpanded(boolean expanded); void setExpanded(boolean expanded);
void openNameOptions(boolean open); void openNameOptions(boolean open);

@ -17,180 +17,27 @@
*/ */
package org.floens.chan.ui.animation; package org.floens.chan.ui.animation;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ArgbEvaluator; import android.animation.ArgbEvaluator;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.TextView; import android.widget.TextView;
import java.util.HashMap;
import java.util.Map;
public class AnimationUtils { public class AnimationUtils {
public static int interpolate(int a, int b, float x) { public static int interpolate(int a, int b, float x) {
return (int) (a + (b - a) * x); return (int) (a + (b - a) * x);
} }
// a lot of these are deprecated, they animate the height with the layout params themselves,
// causing a measure loop for each frame. android just isn't designed for this, and it always
// lags. there are better ways to easily animate layouts, such as enabling the layoutAnimations
// flag.
@Deprecated
public static void setHeight(View view, boolean expand, boolean animated) {
setHeight(view, expand, animated, -1);
}
@Deprecated
public static void setHeight(View view, boolean expand, boolean animated, int knownWidth) {
if (animated) {
animateHeight(view, expand, knownWidth);
} else {
view.setVisibility(expand ? View.VISIBLE : View.GONE);
}
}
private static Map<View, ValueAnimator> layoutAnimations = new HashMap<>();
@Deprecated
public static int animateHeight(final View view, boolean expand) {
return animateHeight(view, expand, -1);
}
@Deprecated
public static int animateHeight(final View view, final boolean expand, int knownWidth) {
return animateHeight(view, expand, knownWidth, 300);
}
@Deprecated
public static int animateHeight(final View view, final boolean expand, int knownWidth, int duration) {
return animateHeight(view, expand, knownWidth, duration, null);
}
/**
* Animate the height of a view by changing the layoutParams.height value.<br>
* view.measure is used to figure out the height.
* Use knownWidth when the width of the view has not been measured yet.<br>
* You can call this even when a height animation is currently running, it will resolve any issues.<br>
* <b>This does cause some lag on complex views because requestLayout is called on each frame.</b>
*/
@Deprecated
public static int animateHeight(final View view, final boolean expand, int knownWidth, int duration, final LayoutAnimationProgress progressCallback) {
final int fromHeight;
int toHeight;
if (expand) {
int width = knownWidth < 0 ? view.getWidth() : knownWidth;
view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.UNSPECIFIED);
fromHeight = view.getHeight();
toHeight = view.getMeasuredHeight();
} else {
fromHeight = view.getHeight();
toHeight = 0;
}
animateLayout(true, view, fromHeight, toHeight, duration, true, progressCallback);
return toHeight;
}
@Deprecated
public static void animateLayout(final boolean vertical, final View view, final int from, final int to, int duration, final boolean wrapAfterwards, final LayoutAnimationProgress callback) {
ValueAnimator running = layoutAnimations.remove(view);
if (running != null) {
running.cancel();
}
ValueAnimator valueAnimator = ValueAnimator.ofInt(from, to);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
// Looks better
if (value == 1) {
value = 0;
}
if (vertical) {
view.getLayoutParams().height = value;
} else {
view.getLayoutParams().width = value;
}
view.requestLayout();
if (callback != null) {
callback.onLayoutAnimationProgress(view, vertical, from, to, value, animation.getAnimatedFraction());
}
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
view.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(Animator animation) {
if (to > 0) {
if (wrapAfterwards) {
if (vertical) {
view.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
} else {
view.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
}
}
} else {
if (vertical) {
view.getLayoutParams().height = 0;
} else {
view.getLayoutParams().width = 0;
}
view.setVisibility(View.GONE);
}
view.requestLayout();
layoutAnimations.remove(view);
}
});
valueAnimator.setInterpolator(new DecelerateInterpolator(2f));
valueAnimator.setDuration(duration);
valueAnimator.start();
layoutAnimations.put(view, valueAnimator);
}
@Deprecated
public interface LayoutAnimationProgress {
@Deprecated
void onLayoutAnimationProgress(View view, boolean vertical, int from, int to, int value, float progress);
}
public static void animateTextColor(final TextView text, int to) { public static void animateTextColor(final TextView text, int to) {
ValueAnimator animation = ValueAnimator.ofObject(new ArgbEvaluator(), text.getCurrentTextColor(), to); ValueAnimator animation = ValueAnimator.ofObject(new ArgbEvaluator(), text.getCurrentTextColor(), to);
animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { animation.addUpdateListener(a -> text.setTextColor((int) a.getAnimatedValue()));
@Override
public void onAnimationUpdate(ValueAnimator animation) {
text.setTextColor((int) animation.getAnimatedValue());
}
});
animation.start(); animation.start();
} }
public static void animateBackgroundColorDrawable(final View view, int newColor) { public static void animateBackgroundColorDrawable(final View view, int newColor) {
int currentBackground = ((ColorDrawable) view.getBackground()).getColor(); int currentBackground = ((ColorDrawable) view.getBackground()).getColor();
ValueAnimator animation = ValueAnimator.ofObject(new ArgbEvaluator(), currentBackground, newColor); ValueAnimator animation = ValueAnimator.ofObject(new ArgbEvaluator(), currentBackground, newColor);
animation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { animation.addUpdateListener(a -> view.setBackgroundColor((int) a.getAnimatedValue()));
@Override
public void onAnimationUpdate(ValueAnimator animation) {
view.setBackgroundColor((int) animation.getAnimatedValue());
}
});
animation.start(); animation.start();
} }
} }

@ -34,7 +34,6 @@ import org.floens.chan.core.manager.BoardManager;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.core.site.Sites; import org.floens.chan.core.site.Sites;
import org.floens.chan.ui.activity.StartActivity; import org.floens.chan.ui.activity.StartActivity;
import org.floens.chan.ui.animation.AnimationUtils;
import org.floens.chan.ui.helper.HintPopup; import org.floens.chan.ui.helper.HintPopup;
import org.floens.chan.ui.helper.RefreshUIMessage; import org.floens.chan.ui.helper.RefreshUIMessage;
import org.floens.chan.ui.settings.BooleanSettingView; import org.floens.chan.ui.settings.BooleanSettingView;
@ -121,7 +120,7 @@ public class MainSettingsController extends SettingsController implements Toolba
onPreferenceChange(imageAutoLoadView); onPreferenceChange(imageAutoLoadView);
if (!ChanSettings.developer.get()) { if (!ChanSettings.developer.get()) {
developerView.view.getLayoutParams().height = 0; developerView.view.setVisibility(View.GONE);
} }
if (ChanSettings.settingsOpenCounter.increase() == 3) { if (ChanSettings.settingsOpenCounter.increase() == 3) {
@ -353,7 +352,7 @@ public class MainSettingsController extends SettingsController implements Toolba
Toast.makeText(context, (developer ? "Enabled" : "Disabled") + " developer options", Toast.LENGTH_LONG).show(); Toast.makeText(context, (developer ? "Enabled" : "Disabled") + " developer options", Toast.LENGTH_LONG).show();
AnimationUtils.animateHeight(developerView.view, developer); developerView.view.setVisibility(developer ? View.VISIBLE : View.GONE);
} }
} }
})); }));

@ -37,7 +37,6 @@ import org.floens.chan.core.site.http.LoginRequest;
import org.floens.chan.core.site.http.LoginResponse; import org.floens.chan.core.site.http.LoginResponse;
import org.floens.chan.ui.view.CrossfadeView; import org.floens.chan.ui.view.CrossfadeView;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.ui.animation.AnimationUtils;
import javax.inject.Inject; import javax.inject.Inject;
@ -77,16 +76,16 @@ public class PassSettingsController extends Controller implements View.OnClickLi
navigationItem.setTitle(R.string.settings_screen_pass); navigationItem.setTitle(R.string.settings_screen_pass);
view = inflateRes(R.layout.controller_pass); view = inflateRes(R.layout.controller_pass);
container = (LinearLayout) view.findViewById(R.id.container); container = view.findViewById(R.id.container);
crossfadeView = (CrossfadeView) view.findViewById(R.id.crossfade); crossfadeView = view.findViewById(R.id.crossfade);
errors = (TextView) view.findViewById(R.id.errors); errors = view.findViewById(R.id.errors);
button = (Button) view.findViewById(R.id.button); button = view.findViewById(R.id.button);
bottomDescription = (TextView) view.findViewById(R.id.bottom_description); bottomDescription = view.findViewById(R.id.bottom_description);
inputToken = (EditText) view.findViewById(R.id.input_token); inputToken = view.findViewById(R.id.input_token);
inputPin = (EditText) view.findViewById(R.id.input_pin); inputPin = view.findViewById(R.id.input_pin);
authenticated = (TextView) view.findViewById(R.id.authenticated); authenticated = view.findViewById(R.id.authenticated);
AnimationUtils.setHeight(errors, false, false); errors.setVisibility(View.GONE);
final boolean loggedIn = loggedIn(); final boolean loggedIn = loggedIn();
button.setText(loggedIn ? R.string.setting_pass_logout : R.string.setting_pass_login); button.setText(loggedIn ? R.string.setting_pass_logout : R.string.setting_pass_login);
@ -191,12 +190,12 @@ public class PassSettingsController extends Controller implements View.OnClickLi
private void showError(String error) { private void showError(String error) {
errors.setText(error); errors.setText(error);
AnimationUtils.setHeight(errors, true, true, container.getWidth()); errors.setVisibility(View.VISIBLE);
} }
private void hideError() { private void hideError() {
errors.setText(null); errors.setText(null);
AnimationUtils.setHeight(errors, false, true, container.getHeight()); errors.setVisibility(View.GONE);
} }
private boolean loggedIn() { private boolean loggedIn() {

@ -345,11 +345,16 @@ public class ReplyLayout extends LoadView implements View.OnClickListener, Reply
comment.setHint(hint); comment.setHint(hint);
} }
@Override
public void showCommentCounter(boolean show) {
commentCounter.setVisibility(show ? View.VISIBLE : View.GONE);
}
@Override @Override
public void setExpanded(boolean expanded) { public void setExpanded(boolean expanded) {
setWrap(!expanded); setWrap(!expanded);
comment.setMaxLines(expanded ? 15 : 6); comment.setMaxLines(expanded ? 500 : 6);
preview.setLayoutParams(new LinearLayout.LayoutParams( preview.setLayoutParams(new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,

@ -45,7 +45,6 @@ import org.floens.chan.core.presenter.ReplyPresenter;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.adapter.PostAdapter; import org.floens.chan.ui.adapter.PostAdapter;
import org.floens.chan.ui.adapter.PostsFilter; import org.floens.chan.ui.adapter.PostsFilter;
import org.floens.chan.ui.animation.AnimationUtils;
import org.floens.chan.ui.cell.PostCell; import org.floens.chan.ui.cell.PostCell;
import org.floens.chan.ui.cell.PostCellInterface; import org.floens.chan.ui.cell.PostCellInterface;
import org.floens.chan.ui.cell.ThreadStatusCell; import org.floens.chan.ui.cell.ThreadStatusCell;
@ -81,7 +80,6 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
private int background; private int background;
private boolean searchOpen; private boolean searchOpen;
private int lastPostCount; private int lastPostCount;
private int recyclerViewTopPadding;
private Handler mainHandler = new Handler(Looper.getMainLooper()); private Handler mainHandler = new Handler(Looper.getMainLooper());
@ -174,8 +172,7 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
switch (postViewMode) { switch (postViewMode) {
case LIST: case LIST:
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext()); LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
recyclerViewTopPadding = 0; setRecyclerViewPadding();
recyclerView.setPadding(0, recyclerViewTopPadding + toolbarHeight(), 0, 0);
recyclerView.setLayoutManager(linearLayoutManager); recyclerView.setLayoutManager(linearLayoutManager);
layoutManager = linearLayoutManager; layoutManager = linearLayoutManager;
@ -187,8 +184,7 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
break; break;
case CARD: case CARD:
GridLayoutManager gridLayoutManager = new GridLayoutManager(null, spanCount, GridLayoutManager.VERTICAL, false); GridLayoutManager gridLayoutManager = new GridLayoutManager(null, spanCount, GridLayoutManager.VERTICAL, false);
recyclerViewTopPadding = dp(1); setRecyclerViewPadding();
recyclerView.setPadding(dp(1), recyclerViewTopPadding + toolbarHeight(), dp(1), dp(1));
recyclerView.setLayoutManager(gridLayoutManager); recyclerView.setLayoutManager(gridLayoutManager);
layoutManager = gridLayoutManager; layoutManager = gridLayoutManager;
@ -292,11 +288,9 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
} }
reply.onOpen(open); reply.onOpen(open);
if (open) { setRecyclerViewPadding();
recyclerView.setPadding(recyclerView.getPaddingLeft(), recyclerViewTopPadding + height, recyclerView.getPaddingRight(), recyclerView.getPaddingBottom()); if (!open) {
} else {
AndroidUtils.hideKeyboard(reply); AndroidUtils.hideKeyboard(reply);
recyclerView.setPadding(recyclerView.getPaddingLeft(), recyclerViewTopPadding + toolbarHeight(), recyclerView.getPaddingRight(), recyclerView.getPaddingBottom());
} }
threadListLayoutCallback.replyLayoutOpen(open); threadListLayoutCallback.replyLayoutOpen(open);
@ -312,19 +306,43 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
postAdapter.showError(error); postAdapter.showError(error);
} }
public void openSearch(boolean show) { public void openSearch(boolean open) {
if (showingThread != null && searchOpen != show) { if (showingThread != null && searchOpen != open) {
searchOpen = show; searchOpen = open;
int height = AnimationUtils.animateHeight(searchStatus, show);
if (show) { searchStatus.measure(
searchStatus.setText(R.string.search_empty); MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
recyclerView.setPadding(recyclerView.getPaddingLeft(), recyclerViewTopPadding + height, recyclerView.getPaddingRight(), recyclerView.getPaddingBottom()); MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
);
int height = searchStatus.getMeasuredHeight();
final ViewPropertyAnimator viewPropertyAnimator = searchStatus.animate();
viewPropertyAnimator.setListener(null);
viewPropertyAnimator.setInterpolator(new DecelerateInterpolator(2f));
viewPropertyAnimator.setDuration(600);
if (open) {
searchStatus.setVisibility(View.VISIBLE);
searchStatus.setTranslationY(-height);
viewPropertyAnimator.translationY(0f);
} else { } else {
recyclerView.setPadding(recyclerView.getPaddingLeft(), recyclerViewTopPadding + toolbarHeight(), recyclerView.getPaddingRight(), recyclerView.getPaddingBottom()); searchStatus.setTranslationY(0f);
viewPropertyAnimator.translationY(-height);
viewPropertyAnimator.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
viewPropertyAnimator.setListener(null);
searchStatus.setVisibility(View.GONE);
}
});
} }
attachToolbarScroll(!(show || replyOpen)); setRecyclerViewPadding();
if (open) {
searchStatus.setText(R.string.search_empty);
}
attachToolbarScroll(!(open || replyOpen));
} }
} }
@ -512,6 +530,36 @@ public class ThreadListLayout extends FrameLayout implements ReplyLayout.ReplyLa
} }
} }
private void setRecyclerViewPadding() {
int defaultPadding = 0;
if (postViewMode == ChanSettings.PostViewMode.CARD) {
defaultPadding = dp(1);
}
int left = defaultPadding;
int top = defaultPadding;
int right = defaultPadding;
int bottom = defaultPadding;
if (replyOpen) {
reply.measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
);
top += reply.getMeasuredHeight();
} else if (searchOpen) {
searchStatus.measure(
MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
);
top += searchStatus.getMeasuredHeight();
} else {
top += toolbarHeight();
}
recyclerView.setPadding(left, top, right, bottom);
}
private int toolbarHeight() { private int toolbarHeight() {
Toolbar toolbar = threadListLayoutCallback.getToolbar(); Toolbar toolbar = threadListLayoutCallback.getToolbar();
return toolbar.getToolbarHeight(); return toolbar.getToolbarHeight();

@ -28,7 +28,6 @@ import android.widget.TextView;
import org.floens.chan.R; import org.floens.chan.R;
import org.floens.chan.controller.Controller; import org.floens.chan.controller.Controller;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.ui.animation.AnimationUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -101,11 +100,7 @@ public class SettingsController extends Controller implements AndroidUtils.OnMea
} }
protected void setSettingViewVisibility(SettingView settingView, boolean visible, boolean animated) { protected void setSettingViewVisibility(SettingView settingView, boolean visible, boolean animated) {
if (animated) { settingView.view.setVisibility(visible ? View.VISIBLE : View.GONE);
AnimationUtils.animateHeight(settingView.view, visible, ((View) settingView.view.getParent()).getWidth());
} else {
settingView.view.setVisibility(visible ? View.VISIBLE : View.GONE);
}
if (settingView.divider != null) { if (settingView.divider != null) {
settingView.divider.setVisibility(visible ? View.VISIBLE : View.GONE); settingView.divider.setVisibility(visible ? View.VISIBLE : View.GONE);
@ -163,19 +158,8 @@ public class SettingsController extends Controller implements AndroidUtils.OnMea
final TextView bottom = ((TextView) view.findViewById(R.id.bottom)); final TextView bottom = ((TextView) view.findViewById(R.id.bottom));
if (bottom != null) { if (bottom != null) {
if (built) { bottom.setText(bottomText);
if (bottomText != null) { bottom.setVisibility(bottomText == null ? View.GONE : View.VISIBLE);
bottom.setText(bottomText);
}
// This way of animating never works on textviews if they're just added.
if (bottom.getHeight() != 0) {
AnimationUtils.animateHeight(bottom, bottomText != null, ((View) view.getParent()).getWidth());
}
} else {
bottom.setText(bottomText);
bottom.setVisibility(bottomText == null ? View.GONE : View.VISIBLE);
}
} }
} }
} }

@ -24,6 +24,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:id="@+id/container" android:id="@+id/container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="32dp" android:paddingBottom="32dp"
android:paddingLeft="32dp" android:paddingLeft="32dp"
@ -39,7 +40,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:paddingRight="4dp" android:paddingRight="4dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:textColor="#fff44336" android:textColor="#fff44336"
android:textSize="16sp" /> android:textSize="16sp"
android:visibility="gone" />
<org.floens.chan.ui.view.CrossfadeView <org.floens.chan.ui.view.CrossfadeView
android:id="@+id/crossfade" android:id="@+id/crossfade"

@ -40,7 +40,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<TextView <TextView
android:id="@+id/search_status" android:id="@+id/search_status"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="wrap_content"
android:background="?backcolor" android:background="?backcolor"
android:elevation="4dp" android:elevation="4dp"
android:gravity="center" android:gravity="center"

Loading…
Cancel
Save