I have a screen filled with buttons, but want the onTouch-method to use the entire screen's coordinates. I first tried using a RelativeLayout with an onTouchListener, but never managed to make it "connect" with the listener (i.e. nothing happened when screen was touched), I also tried putting an ImageView on top of the screen, and then making this view invisible.
This last method gave correct responses to onClicks, but I never managed to make it invisible.
If this is the best solution, which I highly doubt, how do I make the ImageView totally invisible, without losing its onTouchListener (I've experimented with white backgroundColor and setAlpha(0)).
Can I somehow make the onTouchListener react to the whole screen, using global coordinates, while the screen is showing (and altering) several buttons (preferably without the invisible imageview)?
If you don't understand what I'm asking for, feel free to complain about that. I'll try to fill the gaps as needed.
Edit:
I've now managed to resolve the issue by using the regular onTouch-method. I ran into several problems making both ACTION_DOWN and ACTION_MOVE activate the buttons, but I finally got it working. For other people reading this: onInterceptTouchEvent could possibly be used (but I never figured out how to get the screen coordinates instead of the view-coordinates).
Sorry if I'm wrong, but I believe I've just had a similar problem. I wanted a title screen that displayed a picture and on the picture words that say "Click to go on" or something similar. I messed around for a bit and found that you can make a layout clickable.
android:focusable="true"
android:id="#+id/titlescreenframe">
is in my xml file for my layout. The background image is simply in the background attribute (I realize you aren't using images)
Anyway, back in my activity
private FrameLayout fl;
...
fl = (FrameLayout)findViewById(R.id.titlescreenframe);
fl.setOnClickListener(this);
And then I use a switch statement to handle that and the buttons that are on the next layout. Here if you need it: Using Switch Statement to Handle Button Clicks
Seems this should work with other layouts as well, and I don't have literally any views on my main layout. (unless the layout itself counts as one?)
Ha! Just realized you said you found the solution. Silly timing. I'll post on the off-chance this helps someone, happy coding everyone. :)
Have you tried onInterceptTouchEvent on the layout?
I used dispatchTouchEvent for a similar problem. You could consider it a drop-in replacement for onTouchEvent, except that it always sends you an event, even if it's over an existing view.
Return true if you're handling the event, otherwise, be sure to call super.dispatchTouchEvent() and return its result.
As for getting screen coordinates - simply call the MotionEvent's getRawX() and getRawY() rather than getX() and getY(). Those are the absolute screen coordinates, including the action bar and all. If you want to cross-reference those with views, getLocationOnScreen is probably the easiest solution.
touch event return's to child views first. and if you define onClick or onTouch listener for them, parnt view (for example fragment) will not receive any touch listener. So if you want define swipe listener for fragment in this situation, you must implement it in a new class:
package com.neganet.QRelations.fragments;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.FrameLayout;
public class SwipeListenerFragment extends FrameLayout {
private float x1,x2;
static final int MIN_DISTANCE=150;
private onSwipeEventDetected mSwipeDetectedListener;
public SwipeListenerFragment(Context context) {
super(context);
}
public SwipeListenerFragment(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SwipeListenerFragment(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean result=false;
switch(ev.getAction())
{
case MotionEvent.ACTION_DOWN:
x1 = ev.getX();
break;
case MotionEvent.ACTION_UP:
x2 = ev.getX();
float deltaX = x2 - x1;
if (Math.abs(deltaX) > MIN_DISTANCE)
{
if(deltaX<0)
{
result=true;
if(mSwipeDetectedListener!=null)
mSwipeDetectedListener.swipeLeftDetected();
}else if(deltaX>0){
result=true;
if(mSwipeDetectedListener!=null)
mSwipeDetectedListener.swipeRightDetected();
}
}
break;
}
return result;
}
public interface onSwipeEventDetected
{
public void swipeLeftDetected();
public void swipeRightDetected();
}
public void registerToSwipeEvents(onSwipeEventDetected listener)
{
this.mSwipeDetectedListener=listener;
}
}
you can make implements for other types of Layouts completely like this. this class can detect both right and left swipe and specially it returns onInterceptTouchEvent true after detect. its important because if we don't do it some times child views maybe receive event and both of Swipe for fragment and onClick for child view (for example) runs and cause some issues.
after making this class, you must change your fragment xml file:
<com.neganet.QRelations.fragments.SwipeListenerFragment xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:id="#+id/main_list_layout"
android:clickable="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:layout_height="match_parent" tools:context="com.neganet.QRelations.fragments.mainList"
android:background="#color/main_frag_back">
<!-- TODO: Update blank fragment layout -->
<android.support.v7.widget.RecyclerView
android:id="#+id/farazList"
android:scrollbars="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="left|center_vertical" />
</com.neganet.QRelations.fragments.SwipeListenerFragment>
you see that begin tag is the class that we made. now in fragment class:
View view=inflater.inflate(R.layout.fragment_main_list, container, false);
SwipeListenerFragment tdView=(SwipeListenerFragment) view;
tdView.registerToSwipeEvents(this);
and then Implement SwipeListenerFragment.onSwipeEventDetected in it:
#Override
public void swipeLeftDetected() {
Toast.makeText(getActivity(), "left", Toast.LENGTH_SHORT).show();
}
#Override
public void swipeRightDetected() {
Toast.makeText(getActivity(), "right", Toast.LENGTH_SHORT).show();
}
It's a little complicated but works perfect :)
Related
I am trying to replicate a behavior that the current Google Maps has which allows the bottom sheet to be revealed when sliding up from the bottom bar.
Notice in the recording below that I first tap on one of the buttons at the bottom bar and then slide up, which in turn reveals the sheet behind it.
I cannot find anywhere explained how something like this can be achieved. I tried exploring the BottomSheetBehavior and customizing it, but nowhere I can find a way to track the initial tap and then let the sheet take over the movement once the touch slop threshold is reached.
How can I achieve this behavior without resorting to libraries? Or are there any official Google/Android views that allow this behavior between two sections (the navigation bar and bottom sheet)?
Took some time but I found a solution based on examples and discussion provided by two authors, their contributions can be found here:
https://gist.github.com/davidliu/c246a717f00494a6ad237a592a3cea4f
https://github.com/gavingt/BottomSheetTest
The basic logic is to handle touch events in onInterceptTouchEvent in a custom BottomSheetBehavior and check in a CoordinatorLayout if the given view (from now on named proxy view) is of interest for the rest of the touch delegation in isPointInChildBounds.
This can be adapted to use more than one proxy view if needed, the only change necessary for this is to make a proxy view list and iterate the list instead of using a single proxy view reference.
Below follows the code example of this implementation. Do note that this is only configured to handle vertical movements, if horizontal movements are necessary then adapt the code to your need.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<com.example.tabsheet.CustomCoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/customCoordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#android:color/darker_gray">
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 1" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 2" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 3" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 4" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="#drawable/ic_launcher_background"
android:text="Tab 5" />
</com.google.android.material.tabs.TabLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/bottomSheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#3F51B5"
android:clipToPadding="false"
app:behavior_peekHeight="0dp"
app:layout_behavior=".CustomBottomSheetBehavior" />
</com.example.tabsheet.CustomCoordinatorLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
final CustomCoordinatorLayout customCoordinatorLayout;
final CoordinatorLayout bottomSheet;
final TabLayout tabLayout;
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
customCoordinatorLayout = findViewById(R.id.customCoordinatorLayout);
bottomSheet = findViewById(R.id.bottomSheet);
tabLayout = findViewById(R.id.tabLayout);
iniList(bottomSheet);
customCoordinatorLayout.setProxyView(tabLayout);
}
private void iniList(final ViewGroup parent) {
#ColorInt int backgroundColor;
final int padding;
final int maxItems;
final float density;
final NestedScrollView nestedScrollView;
final LinearLayout linearLayout;
final ColorDrawable dividerDrawable;
int i;
TextView textView;
ViewGroup.LayoutParams layoutParams;
density = Resources.getSystem().getDisplayMetrics().density;
padding = (int) (20 * density);
maxItems = 50;
backgroundColor = ContextCompat.getColor(this, android.R.color.holo_blue_bright);
dividerDrawable = new ColorDrawable(Color.WHITE);
layoutParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
nestedScrollView = new NestedScrollView(this);
nestedScrollView.setLayoutParams(layoutParams);
nestedScrollView.setClipToPadding(false);
nestedScrollView.setBackgroundColor(backgroundColor);
linearLayout = new LinearLayout(this);
linearLayout.setLayoutParams(layoutParams);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
linearLayout.setDividerDrawable(dividerDrawable);
for (i = 0; i < maxItems; i++) {
textView = new TextView(this);
textView.setText("Item " + (1 + i));
textView.setPadding(padding, padding, padding, padding);
linearLayout.addView(textView, layoutParams);
}
nestedScrollView.addView(linearLayout);
parent.addView(nestedScrollView);
}
}
CustomCoordinatorLayout.java
public class CustomCoordinatorLayout extends CoordinatorLayout {
private View proxyView;
public CustomCoordinatorLayout(#NonNull Context context) {
super(context);
}
public CustomCoordinatorLayout(
#NonNull Context context,
#Nullable AttributeSet attrs
) {
super(context, attrs);
}
public CustomCoordinatorLayout(
#NonNull Context context,
#Nullable AttributeSet attrs,
int defStyleAttr
) {
super(context, attrs, defStyleAttr);
}
#Override
public boolean isPointInChildBounds(
#NonNull View child,
int x,
int y
) {
if (super.isPointInChildBounds(child, x, y)) {
return true;
}
// we want to intercept touch events if they are
// within the proxy view bounds, for this reason
// we instruct the coordinator layout to check
// if this is true and let the touch delegation
// respond to that result
if (proxyView != null) {
return super.isPointInChildBounds(proxyView, x, y);
}
return false;
}
// for this example we are only interested in intercepting
// touch events for a single view, if more are needed use
// a List<View> viewList instead and iterate in
// isPointInChildBounds
public void setProxyView(View proxyView) {
this.proxyView = proxyView;
}
}
CustomBottomSheetBehavior.java
public class CustomBottomSheetBehavior<V extends View> extends BottomSheetBehavior<V> {
// we'll use the device's touch slop value to find out when a tap
// becomes a scroll by checking how far the finger moved to be
// considered a scroll. if the finger moves more than the touch
// slop then it's a scroll, otherwise it is just a tap and we
// ignore the touch events
private int touchSlop;
private float initialY;
private boolean ignoreUntilClose;
public CustomBottomSheetBehavior(
#NonNull Context context,
#Nullable AttributeSet attrs
) {
super(context, attrs);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
#Override
public boolean onInterceptTouchEvent(
#NonNull CoordinatorLayout parent,
#NonNull V child,
#NonNull MotionEvent event
) {
// touch events are ignored if the bottom sheet is already
// open and we save that state for further processing
if (getState() == STATE_EXPANDED) {
ignoreUntilClose = true;
return super.onInterceptTouchEvent(parent, child, event);
}
switch (event.getAction()) {
// this is the first event we want to begin observing
// so we set the initial value for further processing
// as a positive value to make things easier
case MotionEvent.ACTION_DOWN:
initialY = Math.abs(event.getRawY());
return super.onInterceptTouchEvent(parent, child, event);
// if the last bottom sheet state was not open then
// we check if the current finger movement has exceed
// the touch slop in which case we return true to tell
// the system we are consuming the touch event
// otherwise we let the default handling behavior
// since we don't care about the direction of the
// movement we ensure its difference is a positive
// integer to simplify the condition check
case MotionEvent.ACTION_MOVE:
return !ignoreUntilClose
&& Math.abs(initialY - Math.abs(event.getRawY())) > touchSlop
|| super.onInterceptTouchEvent(parent, child, event);
// once the tap or movement is completed we reset
// the initial values to restore normal behavior
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
initialY = 0;
ignoreUntilClose = false;
return super.onInterceptTouchEvent(parent, child, event);
}
return super.onInterceptTouchEvent(parent, child, event);
}
}
Result with transparent status bar and navigation bar to help visualize the bottom sheet sliding up, but excluded from the code above since it was not relevant for this question.
Note: It is possible you might not even need a custom bottom sheet behavior if your bottom sheet layout contains a certain scrollable view type (NestedScrollView for example) that can be used as is by the CoordinatorLayout, so try without the custom bottom sheet behavior once your layout is ready since it will make this simpler.
You could try something like this (It's Pseudocode, hopefully you understand what I'm getting at):
<FrameLayout id="+id/bottomSheet">
<View id="exploreNearby bottomMargin="buttonContainerHeight/>
<LinearLayout>
<Button id="explore"/>
<Button id="explore"/>
<Button id="explore"/>
</LinearLayout>
<View width="match" height="match" id="+id/touchCatcher"
</FrameLayout>
Add a gesture detector on the bottomSheet view on override onTouch(). which uses SimpleOnGestureListener to wait for a "scroll" events - everything but a scroll event you can replicate down through to the view as normal.
On a scroll event you can grow your exploreNearby as a delta (make sure it doesn't recurse or go to high or too low).
The Bottom sheet class will already do this for you. Just set it's peek height to 0 and it should already listen for the slide up gesture.
However, I'm not positive it will work with a peek height of 0. So if that doesn't work, simply put a peek height of 20dp and make the top portion of the bottom sheet layout transparent so it is not visible.
That should do the trick for ya, unless I'm misunderstanding your question. If your goal is to simply be able to tap at the bottom and slide upwards bringing up the bottom sheet that should be pretty straight forward.
The one possible issue that you "could" encounter is if the bottom sheet doesn't receive the touch events due to the button already consuming it. If this happens you will need to create a touch handler for the whole screen and return "true" that you are handling it each time, then simply forward the touch events to the underlying view, so when you get above the threshold of your bottom tab bar you start sending the touch events to the bottom sheet layout instead of the tab bar.
It sounds harder than it is. Most classes have an onTouch and you just forward it on. However, only go that route, if it doesn't work for you out of the box the way I described in the first two scenarios.
Lastly, one other option that might work is to create your tab buttons as part of the bottomSheetLayout and make the peek height equivalent of the tab bar. Then make sure the tab bar is constrained to bottomsheet parent bottom, so that when you swipe up it simply stays at the bottom. This would enable you to click the buttons or get the free bottom sheet behavior.
Happy Coding!
I have a layout consists of a Parent RecyclerView with a sub Recyclerview in it
i know that it is not good to put a list inside another list but i have to so that i can use the sub list features like swiping and drag and drop
My issue is that the child Recyclerview gain focus and stops the parent from scrolling if the touch point was on it
simply i want if the touch was vertically on the child Recyclerview
the parent scrolls up and down and if the touch was horizontal or a click then the child Recyclerview list item swipes left and right.
Any help to achieve this?
I finally found a solution.
Create Custom LinearLayoutManager
public class CustomLinearLayoutManager extends LinearLayoutManager {
public CustomLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
// it will always pass false to RecyclerView when calling "canScrollVertically()" method.
#Override
public boolean canScrollVertically() {
return false;
}
}
Then instantiate it like this for vertical scrolling
CustomLinearLayoutManager customLayoutManager = new CustomLinearLayoutManager(getActivity(),LinearLayoutManager.VERTICAL,false);
Finally set the custom layout as layout manager of recycler view
recyclerView.setLayoutManager(customLayoutManager);
android:nestedScrollingEnabled="false" in the child RecyclerView
You can add
android:nestedScrollingEnabled="false"
to your RecyclerView in XML or
childRecyclerView.setNestedScrollingEnabled(false);
to your RecyclerView in Java.
EDIT:-
childRecyclerView.setNestedScrollingEnabled(false); will work only in android_version>21 devices. to work in all devices use the following
ViewCompat.setNestedScrollingEnabled(childRecyclerView, false);
On your ActivityName.java, inside the onCreate() method write:
RecyclerView v = (RecyclerView) findViewById(R.id.your_recycler_view_id);
v.setNestedScrollingEnabled(false);
By any means, if you are using Coordinator Layout, In case you want to simplify things, and you want to disable nested scrolling.
<android.support.v4.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<android.support.v7.widget.RecyclerView
android:id="#+id/activitiesListRV"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</android.support.v4.widget.NestedScrollView>
And again you apply the same principle:
On your ActivityName.java, inside the onCreate() method write:
RecyclerView v = (RecyclerView) findViewById(R.id.your_recycler_view_id);
v.setNestedScrollingEnabled(false);
So basically in XML, you have to specify the app: layout_behavior
app:layout_behavior="#string/appbar_scrolling_view_behavior">
While it might not be good practice to have embedded recycler views, sometimes you cannot avoid it. Something like this might work:
public class NoScrollRecycler extends RecyclerView {
public NoScrollRecycler(Context context){
super(context);
}
public NoScrollRecycler(Context context, AttributeSet attrs){
super(context, attrs);
}
public NoScrollRecycler(Context context, AttributeSet attrs, int style){
super(context, attrs, style);
}
#Override
public boolean dispatchTouchEvent(MotionEvent ev){
//Ignore scroll events.
if(ev.getAction() == MotionEvent.ACTION_MOVE)
return true;
//Dispatch event for non-scroll actions, namely clicks!
return super.dispatchTouchEvent(ev);
}
}
This will disable the scroll event, but not the click events. Use this class for the "child" RecyclerView. You want the PARENT recyclerview to scroll, but not the child. Well this should do that, since the parent will just be the standard RecyclerView, but the child will be this custom one with no scrolling, but handles clicks. Might need to disable clicking for the parent RecyclerView.. Not sure as I have not tested this, so consider it just an example...
Also, to use this in XML (incase you didn't know) do the following:
<com.yourpackage.location.NoScrollRecycler
...
... >
...
...
</com.yourpackage.location.NoScrollRecycler>
you can use setNestedScrollingEnabled(false); on sub RecyclerView which stops scrolling inside sub RecyclerView.
In my case code was
mInnerRecyclerView.setNestedScrollingEnabled(false); where mInnerRecyclerView being inner RecyclerView.
Ithink I'm too late but here i found the solution if it's still annoying someone:
RecyclerView v = (RecyclerView);
findViewById(R.id.your_recycler_view_id);
v.setNestedScrollingEnabled(false);
sensorsRecyclerView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
I've tried many suggested solutions and couldn't find one that worked in my case. I have more than 1 RecyclerView inside a ScrollView using a GridLayoutManager. The result from the suggestion above resulted in the ScrollView stopping to scroll whenever I lifted my finger (it didn't glide to the top or bottom of the view when my finger was lifted over a RecyclerView)
Looking through the RecyclerView source, inside the onTouchEvent there is a call to the layout manager:
final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
final boolean canScrollVertically = mLayout.canScrollVertically();
If you override these in a custom layout manager, return false and it will stop scrolling. It also fixes the problem where the ScrollView would stop scrolling abruptly.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.RecyclerView
android:id="#+id/rv"
android:layout_marginTop="2dp"
android:layout_marginLeft="2dp"
android:layout_marginBottom="10dp"
android:layout_marginRight="2dp"
android:nestedScrollingEnabled="false"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
put the code inside LinearLayout...no need to do anything pragmatically
If you don't want make a custom view, another option is to create a same sized layout in front of the RecyclerView, and make it clickable.
EDIT:
But unfortunately it blocks events for list item too.
The Kotlin way:
recyclerView.layoutManager = object: LinearLayoutManager(recyclerView.context) {
override fun canScrollVertically(): Boolean {
return false
}
}
How do you make a horizontally scrolling ListView for vertical Mongolian script in Android apps?
Background
Android has fairly good support for many of the world's languages, even RTL languages like Arabic and Hebrew. However, there is no built in support for top-to-bottom languages like traditional Mongolian (which is still very much alive in Inner Mongolia and not to be confused with Cyrillic Mongolian). The following graphic shows the text direction with English added for clarity.
Since this functionality is not built into Android, it makes almost every single aspect of app development extremely difficult. This is expecially true with horizontal ListViews, which are not supported out of the box in Android. There is also very, very little information available online. There are a number of app developers for traditional Mongolian, but whether it is for commercial reasons or otherwise, they do not seem to make their code open source.
Because of these difficulties I would like to make a series of StackOverflow questions that can serve as a central place to collect answers for some of the more difficult programming problems related to traditional Mongolian app development. Even if you are not literate in Mongolian, your help reviewing the code, making comments and questions, giving answers or even up-voting the question would be appreciated.
Mongolian Horizontally Scrolling ListView with Vertical Script
A Mongolian ListView needs to have the following requirements:
Scrolls horizontally from left to right
Touch events work the same as with a normal ListView
Custom layouts are supported the same as in a normal ListView
Also needs to support everything that a Mongolian TextView would support:
Supports a traditional Mongolian font
Displays text vertically from top to bottom
Line wrapping goes from left to right.
Line breaks occur at a space (same as English)
The image below shows the basic functionality a Mongolian ListView should have:
My answer is below, but I welcome other ways of solving this problem.
Other related questions in this series:
How to make a traditional Mongolian script TextView in Android
How to make a traditional Mongolian script EditText in Android
More to come... (Toast, Dialog, Menu)
iOS:
How do you make a vertical text UILabel and UITextView for iOS in Swift?
Update
RecyclerViews have a horizontal layout. So it is relatively easy to put a Vertical Mongolian TextView inside one of these. Here is an example from mongol-library.
See this answer for a general solution to using a RecyclerView to make a horizontally scrolling list.
Old answer
It is quite unfortunate that horizontal ListViews are not provided by the Android API. There are a number of StackOverflow Q&As that talk about how to do them, though. Here are a couple samples:
How can I make a horizontal ListView in Android?
Horizontal ListView in Android?
But when I actually tried to implement these suggestions as well as incorporate Mongolian vertical text, I was having a terrible time. Somewhere in my search I found a slightly different answer. It was a class that rotated an entire layout. It did so by extending ViewGroup. In this way anything (including a ListView) can be put in the ViewGroup and it gets rotated. All the touch events work, too.
As I explained in my answer about Mongolian TextViews, it is not enough to simply rotate Mongolian text. That would be enough if every ListView item (or other text element in the ViewGroup) was only a single line, but rotating multiple lines make the line wrap go the wrong direction. However, mirroring the layout horizontally and also using a vertically mirrored font can overcome this, as is shown in the following image.
I adapted the rotated ViewGroup code to also do the horizontal mirroring.
public class MongolViewGroup extends ViewGroup {
private int angle = 90;
private final Matrix rotateMatrix = new Matrix();
private final Rect viewRectRotated = new Rect();
private final RectF tempRectF1 = new RectF();
private final RectF tempRectF2 = new RectF();
private final float[] viewTouchPoint = new float[2];
private final float[] childTouchPoint = new float[2];
private boolean angleChanged = true;
public MongolViewGroup(Context context) {
this(context, null);
}
public MongolViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
setWillNotDraw(false);
}
public View getView() {
return getChildAt(0);
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final View view = getView();
if (view != null) {
measureChild(view, heightMeasureSpec, widthMeasureSpec);
setMeasuredDimension(resolveSize(view.getMeasuredHeight(), widthMeasureSpec),
resolveSize(view.getMeasuredWidth(), heightMeasureSpec));
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
if (angleChanged) {
final RectF layoutRect = tempRectF1;
final RectF layoutRectRotated = tempRectF2;
layoutRect.set(0, 0, right - left, bottom - top);
rotateMatrix.setRotate(angle, layoutRect.centerX(), layoutRect.centerY());
rotateMatrix.postScale(-1, 1);
rotateMatrix.mapRect(layoutRectRotated, layoutRect);
layoutRectRotated.round(viewRectRotated);
angleChanged = false;
}
final View view = getView();
if (view != null) {
view.layout(viewRectRotated.left, viewRectRotated.top, viewRectRotated.right,
viewRectRotated.bottom);
}
}
#Override
protected void dispatchDraw(Canvas canvas) {
canvas.save();
canvas.rotate(-angle, getWidth() / 2f, getHeight() / 2f);
canvas.scale(-1, 1);
super.dispatchDraw(canvas);
canvas.restore();
}
#Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
invalidate();
return super.invalidateChildInParent(location, dirty);
}
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
viewTouchPoint[0] = event.getX();
viewTouchPoint[1] = event.getY();
rotateMatrix.mapPoints(childTouchPoint, viewTouchPoint);
event.setLocation(childTouchPoint[0], childTouchPoint[1]);
boolean result = super.dispatchTouchEvent(event);
event.setLocation(viewTouchPoint[0], viewTouchPoint[1]);
return result;
}
}
The Mongolian vertically mirrored font still needs to be set somewhere else, though. I find it easiest to make a custom TextView to do it:
public class MongolNonRotatedTextView extends TextView {
// This class does not rotate the textview. It only displays the Mongol font.
// For use with MongolLayout, which does all the rotation and mirroring.
// Constructors
public MongolNonRotatedTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public MongolNonRotatedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MongolNonRotatedTextView(Context context) {
super(context);
init();
}
// This class requires the mirrored Mongolian font to be in the assets/fonts folder
private void init() {
Typeface tf = Typeface.createFromAsset(getContext().getAssets(),
"fonts/MongolMirroredFont.ttf");
setTypeface(tf);
}
}
Then the custom ListView item xml layout can look something like this:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/rlListItem"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.example.MongolNonRotatedTextView
android:id="#+id/tvListViewText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"/>
</RelativeLayout>
Known issues:
If you look carefully at the image below you can see faint horizontal and vertical lines around the text. Although this image comes from another developer's app, I am getting the same artifacts in my app when I use the rotated ViewGroup (but not when I use the rotated TextView). If anyone knows where these are coming from, please leave me a comment!
This solution does not deal with rendering the Unicode text. Either you need to use non-Unicode text (discouraged) or you need to include a rendering engine in your app. (Android does not support OpenType smartfont rendering at this time. Hopefully this will change in the future. iOS, by comparison does support complex text rendering fonts.) See this link for a Unicode Mongolian rendering engine example.
I have a form within a ScrollView. When I tap into an EditText the soft keyboard appears and the ScrollView scrolls the now focused EditText so that it just comes into view.
However, I have hint information just below the EditText that I also would like to show, so the scrolling should go just a bit further up, like this:
The EditText is embedded in a form element and actually I'd like to scroll to the bottom of that. I've checked the source code of ScrollView and it will just scroll to the bottom of the currently focused view. Maybe there's a way to tell the ScrollView that the form element is the currently focused element?
Of course I could write my own ScrollView sub class and override the scroll behavior, but I wonder if there's a more elegant way of doing this.
Any other suggestions (with adjust scrolling with a fixed offset or so) are also appreciated.
I have not really found any way to configure the scrolling behavior of the ScrollView from the outside. So I ended up to define my own sub class of ScrollView:`
/**
* {#link ScrollView} extension that allows to configure scroll offset.
*/
public class ConfigurableScrollView extends ScrollView {
private int scrollOffset = 0;
public ConfigurableScrollView (final Context context, final AttributeSet attrs) {
super(context, attrs);
}
public void setScrollOffset (final int scrollOffset) {
this.scrollOffset = scrollOffset;
}
#Override
protected int computeScrollDeltaToGetChildRectOnScreen (final Rect rect) {
// adjust by scroll offset
int scrollDelta = super.computeScrollDeltaToGetChildRectOnScreen(rect);
int newScrollDelta = (int) Math.signum(scrollDelta) * (scrollDelta + this.scrollOffset);
return newScrollDelta;
}
}
computeScrollDelta(...) is the only protected method that can be targeted for overriding, apart from onSizeChanged(...).
The signum function in the example above ensures that scrolling is only increased, if the ScrollView really thinks that scrolling is necessary (e.g. when keyboard pops up).
I can now set the extra scroll offset once from the outside, as calculated from the height of the hint.
It's not hard to use the extended ConfigurableScrollView instead of the standard ScrollView, I only had to replace the ScrollView XML tag with the FQN of the new class.
Considering that you are using ScrollView you have the possibility to use the method ScrollTo as follow:
scrollView.post(new Runnable() {
#Override
public void run() {
sv.scrollTo(x-value, y-value);
}
});
where the first argument is the scroll value for X, while the second argument is the scroll value for Y. So you just have to set your scrollView offset when the keyboard is displayed.
Hope it helps;)
TL;DR: I am looking for a complete working sample of what I'll refer to as "the Gmail three-fragment animation" scenario. Specifically, we want to start with two fragments, like this:
Upon some UI event (e.g., tapping on something in Fragment B), we want:
Fragment A to slide off the screen to the left
Fragment B to slide to the left edge of the screen and shrink to take up the spot vacated by Fragment A
Fragment C to slide in from the right side of the screen and to take up the spot vacated by Fragment B
And, on a BACK button press, we want that set of operations to be reversed.
Now, I have seen lots of partial implementations; I'll review four of them below. Beyond being incomplete, they all have their issues.
#Reto Meier contributed this popular answer to the same basic question, indicating that you would use setCustomAnimations() with a FragmentTransaction. For a two-fragment scenario (e.g., you only see Fragment A initially, and want to replace it with a new Fragment B using animated effects), I am in complete agreement. However:
Since you can only specify one "in" and one "out" animation, I can't see how you would handle all the different animations required for the three-fragment scenario
The <objectAnimator> in his sample code uses hard-wired positions in pixels, and that would seem to be impractical given varying screen sizes, yet setCustomAnimations() requires animation resources, precluding the possibility of defining these things in Java
I am at a loss as to how the object animators for scale tie in with things like android:layout_weight in a LinearLayout for allocating space on a percentage basis
I am at a loss as to how Fragment C is handled at the outset (GONE? android:layout_weight of 0? pre-animated to a scale of 0? something else?)
#Roman Nurik points out that you can animate any property, including ones that you define yourself. That can help solve the issue of the hard-wired positions, at the cost of inventing your own custom layout manager subclass. That helps some, but I'm still baffled by the rest of Reto's solution.
The author of this pastebin entry shows some tantalizing pseudocode, basically saying that all three fragments would reside in the container initially, with Fragment C hidden at the outset via a hide() transaction operation. We then show() C and hide() A when the UI event occurs. However, I don't see how that handles the fact that B changes size. It also relies on the fact that you apparently can add multiple fragments to the same container, and I am not sure whether or not that is reliable behavior over the long term (not to mention it should break findFragmentById(), though I can live with that).
The author of this blog post indicates that Gmail is not using setCustomAnimations() at all, but instead directly uses object animators ("you just change left margin of the root view + change width of the right view"). However, this is still a two-fragment solution AFAICT, and the implementation shown once again hard-wires dimensions in pixels.
I will continue plugging away at this, so I may wind up answering this myself someday, but I am really hoping that somebody has worked out the three-fragment solution for this animation scenario and can post the code (or a link thereto). Animations in Android make me want to pull my hair out, and those of you who have seen me know that this is a largely fruitless endeavor.
Uploaded my proposal at github
(Is working with all android versions though view hardware acceleration is strongly recommended for this kind of animations. For non hardware accelerated devices a bitmap caching implementation should fit better)
Demo video with the animation is Here (Slow frame rate cause of the screen cast. Actual performance is very fast)
Usage:
layout = new ThreeLayout(this, 3);
layout.setAnimationDuration(1000);
setContentView(layout);
layout.getLeftView(); //<---inflate FragmentA here
layout.getMiddleView(); //<---inflate FragmentB here
layout.getRightView(); //<---inflate FragmentC here
//Left Animation set
layout.startLeftAnimation();
//Right Animation set
layout.startRightAnimation();
//You can even set interpolators
Explaination:
Created a new custom RelativeLayout(ThreeLayout) and 2 custom Animations(MyScalAnimation, MyTranslateAnimation)
ThreeLayout gets the weight of the left pane as param ,assuming the other visible view has weight=1.
So new ThreeLayout(context,3) creates a new view with 3 children and the left pane with have 1/3 of the total screen. The other view occupies the all available space.
It calculates width at runtime,a safer implementation is that the dimentions are be calculated first time in draw(). instead of in post()
Scale and Translate animations actually resize and move the view and not pseudo-[scale,move]. Notice that fillAfter(true) is not used anywhere.
View2 is right_of View1
and
View3 is right_of View2
Having set these rules RelativeLayout takes care of everything else. Animations alter the margins (on move) and [width,height] on scale
To access each child (so that you can inflate it with your Fragment you can call
public FrameLayout getLeftLayout() {}
public FrameLayout getMiddleLayout() {}
public FrameLayout getRightLayout() {}
Below are demonstrated the 2 animations
Stage1
---IN Screen----------!-----OUT----
[View1][_____View2_____][_____View3_____]
Stage2
--OUT-!--------IN Screen------
[View1][View2][_____View3_____]
OK, here is my own solution, derived from the Email AOSP app, per #Christopher's suggestion in the question's comments.
https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePane
#weakwire's solution is reminiscent of mine, though he uses classic Animation rather than animators, and he uses RelativeLayout rules to enforce positioning. From the bounty standpoint, he will probably get the bounty, unless somebody else with a slicker solution yet posts an answer.
In a nutshell, the ThreePaneLayout in that project is a LinearLayout subclass, designed to work in landscape with three children. Those childrens' widths can be set in the layout XML, via whatever desired means -- I show using weights, but you could have specific widths set by dimension resources or whatever. The third child -- Fragment C in the question -- should have a width of zero.
package com.commonsware.android.anim.threepane;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
public class ThreePaneLayout extends LinearLayout {
private static final int ANIM_DURATION=500;
private View left=null;
private View middle=null;
private View right=null;
private int leftWidth=-1;
private int middleWidthNormal=-1;
public ThreePaneLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initSelf();
}
void initSelf() {
setOrientation(HORIZONTAL);
}
#Override
public void onFinishInflate() {
super.onFinishInflate();
left=getChildAt(0);
middle=getChildAt(1);
right=getChildAt(2);
}
public View getLeftView() {
return(left);
}
public View getMiddleView() {
return(middle);
}
public View getRightView() {
return(right);
}
public void hideLeft() {
if (leftWidth == -1) {
leftWidth=left.getWidth();
middleWidthNormal=middle.getWidth();
resetWidget(left, leftWidth);
resetWidget(middle, middleWidthNormal);
resetWidget(right, middleWidthNormal);
requestLayout();
}
translateWidgets(-1 * leftWidth, left, middle, right);
ObjectAnimator.ofInt(this, "middleWidth", middleWidthNormal,
leftWidth).setDuration(ANIM_DURATION).start();
}
public void showLeft() {
translateWidgets(leftWidth, left, middle, right);
ObjectAnimator.ofInt(this, "middleWidth", leftWidth,
middleWidthNormal).setDuration(ANIM_DURATION)
.start();
}
public void setMiddleWidth(int value) {
middle.getLayoutParams().width=value;
requestLayout();
}
private void translateWidgets(int deltaX, View... views) {
for (final View v : views) {
v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
v.animate().translationXBy(deltaX).setDuration(ANIM_DURATION)
.setListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
v.setLayerType(View.LAYER_TYPE_NONE, null);
}
});
}
}
private void resetWidget(View v, int width) {
LinearLayout.LayoutParams p=
(LinearLayout.LayoutParams)v.getLayoutParams();
p.width=width;
p.weight=0;
}
}
However, at runtime, no matter how you originally set up the widths, width management is taken over by ThreePaneLayout the first time you use hideLeft() to switch from showing what the question referred to as Fragments A and B to Fragments B and C. In the terminology of ThreePaneLayout -- which has no specific ties to fragments -- the three pieces are left, middle, and right. At the time you call hideLeft(), we record the sizes of left and middle and zero out any weights that were used on any of the three, so we can completely control the sizes. At the point in time of hideLeft(), we set the size of right to be the original size of middle.
The animations are two-fold:
Use a ViewPropertyAnimator to perform a translation of the three widgets to the left by the width of left, using a hardware layer
Use an ObjectAnimator on a custom pseudo-property of middleWidth to change the middle width from whatever it started with to the original width of left
(it is possible that it is a better idea to use an AnimatorSet and ObjectAnimators for all of these, though this works for now)
(it is also possible that the middleWidth ObjectAnimator negates the value of the hardware layer, since that requires fairly continuous invalidation)
(it is definitely possible that I still have gaps in my animation comprehension, and that I like parenthetical statements)
The net effect is that left slides off the screen, middle slides to the original position and size of left, and right translates in right behind middle.
showLeft() simply reverses the process, with the same mix of animators, just with the directions reversed.
The activity uses a ThreePaneLayout to hold a pair of ListFragment widgets and a Button. Selecting something in the left fragment adds (or updates the contents of) the middle fragment. Selecting something in the middle fragment sets the caption of the Button, plus executes hideLeft() on the ThreePaneLayout. Pressing BACK, if we hid the left side, will execute showLeft(); otherwise, BACK exits the activity. Since this does not use FragmentTransactions for affecting the animations, we are stuck managing that "back stack" ourselves.
The project linked-to above uses native fragments and the native animator framework. I have another version of the same project that uses the Android Support fragments backport and NineOldAndroids for the animation:
https://github.com/commonsguy/cw-omnibus/tree/master/Animation/ThreePaneBC
The backport works fine on a 1st generation Kindle Fire, though the animation is a bit jerky given the lower hardware specs and lack of hardware acceleration support. Both implementations seem smooth on a Nexus 7 and other current-generation tablets.
I am certainly open for ideas of how to improve this solution, or other solutions that offer clear advantages over what I did here (or what #weakwire used).
Thanks again to everyone who has contributed!
We built a library called PanesLibrary which solves this problem. It's even more flexible than what's been previously offered because:
Each pane can be dynamically sized
It allows for any number of panes (not just 2 or 3)
Fragments inside of panes are correctly retained on orientation changes.
You can check it out here: https://github.com/Mapsaurus/Android-PanesLibrary
Here's a demo: http://www.youtube.com/watch?v=UA-lAGVXoLU&feature=youtu.be
It basically allows you to easily add any number of dynamically sized panes and attach fragments to those panes. Hope you find it useful! :)
Building off one of the examples you linked to (http://android.amberfog.com/?p=758), how about animating the layout_weight property? This way, you can animate the change in weight of the 3 fragments together, AND you get the bonus that they all slide nicely together:
Start with a simple layout. Since we're going to be animating layout_weight, we need a LinearLayout as the root view for the 3 panels.:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/container"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="#+id/panel1"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="match_parent"/>
<LinearLayout
android:id="#+id/panel2"
android:layout_width="0dip"
android:layout_weight="2"
android:layout_height="match_parent"/>
<LinearLayout
android:id="#+id/panel3"
android:layout_width="0dip"
android:layout_weight="0"
android:layout_height="match_parent"/>
</LinearLayout>
Then the demo class:
public class DemoActivity extends Activity implements View.OnClickListener {
public static final int ANIM_DURATION = 500;
private static final Interpolator interpolator = new DecelerateInterpolator();
boolean isCollapsed = false;
private Fragment frag1, frag2, frag3;
private ViewGroup panel1, panel2, panel3;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
panel1 = (ViewGroup) findViewById(R.id.panel1);
panel2 = (ViewGroup) findViewById(R.id.panel2);
panel3 = (ViewGroup) findViewById(R.id.panel3);
frag1 = new ColorFrag(Color.BLUE);
frag2 = new InfoFrag();
frag3 = new ColorFrag(Color.RED);
final FragmentManager fm = getFragmentManager();
final FragmentTransaction trans = fm.beginTransaction();
trans.replace(R.id.panel1, frag1);
trans.replace(R.id.panel2, frag2);
trans.replace(R.id.panel3, frag3);
trans.commit();
}
#Override
public void onClick(View view) {
toggleCollapseState();
}
private void toggleCollapseState() {
//Most of the magic here can be attributed to: http://android.amberfog.com/?p=758
if (isCollapsed) {
PropertyValuesHolder[] arrayOfPropertyValuesHolder = new PropertyValuesHolder[3];
arrayOfPropertyValuesHolder[0] = PropertyValuesHolder.ofFloat("Panel1Weight", 0.0f, 1.0f);
arrayOfPropertyValuesHolder[1] = PropertyValuesHolder.ofFloat("Panel2Weight", 1.0f, 2.0f);
arrayOfPropertyValuesHolder[2] = PropertyValuesHolder.ofFloat("Panel3Weight", 2.0f, 0.0f);
ObjectAnimator localObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(this, arrayOfPropertyValuesHolder).setDuration(ANIM_DURATION);
localObjectAnimator.setInterpolator(interpolator);
localObjectAnimator.start();
} else {
PropertyValuesHolder[] arrayOfPropertyValuesHolder = new PropertyValuesHolder[3];
arrayOfPropertyValuesHolder[0] = PropertyValuesHolder.ofFloat("Panel1Weight", 1.0f, 0.0f);
arrayOfPropertyValuesHolder[1] = PropertyValuesHolder.ofFloat("Panel2Weight", 2.0f, 1.0f);
arrayOfPropertyValuesHolder[2] = PropertyValuesHolder.ofFloat("Panel3Weight", 0.0f, 2.0f);
ObjectAnimator localObjectAnimator = ObjectAnimator.ofPropertyValuesHolder(this, arrayOfPropertyValuesHolder).setDuration(ANIM_DURATION);
localObjectAnimator.setInterpolator(interpolator);
localObjectAnimator.start();
}
isCollapsed = !isCollapsed;
}
#Override
public void onBackPressed() {
//TODO: Very basic stack handling. Would probably want to do something relating to fragments here..
if(isCollapsed) {
toggleCollapseState();
} else {
super.onBackPressed();
}
}
/*
* Our magic getters/setters below!
*/
public float getPanel1Weight() {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel1.getLayoutParams();
return params.weight;
}
public void setPanel1Weight(float newWeight) {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel1.getLayoutParams();
params.weight = newWeight;
panel1.setLayoutParams(params);
}
public float getPanel2Weight() {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel2.getLayoutParams();
return params.weight;
}
public void setPanel2Weight(float newWeight) {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel2.getLayoutParams();
params.weight = newWeight;
panel2.setLayoutParams(params);
}
public float getPanel3Weight() {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel3.getLayoutParams();
return params.weight;
}
public void setPanel3Weight(float newWeight) {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) panel3.getLayoutParams();
params.weight = newWeight;
panel3.setLayoutParams(params);
}
/**
* Crappy fragment which displays a toggle button
*/
public static class InfoFrag extends Fragment {
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
LinearLayout layout = new LinearLayout(getActivity());
layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
layout.setBackgroundColor(Color.DKGRAY);
Button b = new Button(getActivity());
b.setOnClickListener((DemoActivity) getActivity());
b.setText("Toggle Me!");
layout.addView(b);
return layout;
}
}
/**
* Crappy fragment which just fills the screen with a color
*/
public static class ColorFrag extends Fragment {
private int mColor;
public ColorFrag() {
mColor = Color.BLUE; //Default
}
public ColorFrag(int color) {
mColor = color;
}
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
FrameLayout layout = new FrameLayout(getActivity());
layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
layout.setBackgroundColor(mColor);
return layout;
}
}
}
Also this example doesn't use FragmentTransactions to achieve the animations (rather, it animates the views the fragments are attached to), so you would need to do all the backstack/fragment transactions yourself, but compared to the effort of getting the animations working nicely, this doesnt seem like a bad trade-off :)
Horrible low-res video of it in action: http://youtu.be/Zm517j3bFCo
This isn't using fragments.... It's a custom layout with 3 children. When you click on a message, you offset the 3 childrens using offsetLeftAndRight() and a animator.
In JellyBean you can enable "Show layout bounds" in the "Developper Options" settings. When the slide animation is complete, you can still see that the left menu is still there, but underneath the middle panel.
It's similar to Cyril Mottier's Fly-in app menu, but with 3 elements instead of 2.
Additionnally, the ViewPager of the third children is another indication of this behavior: ViewPager usually uses Fragments (I know they don't have to, but I have never seen an implementation other that Fragment), and since you can't uses Fragments inside another Fragment the 3 children are probably not fragments....
I am currently trying to do something like that, except that Fragment B scale to take the available space and that the 3 pane can be open at the same time if there is enough room. Here is my solution so far, but i'm not sure if i'm going to stick with it. I hope someone will provide an answer showing The Right Way.
Instead of using a LinearLayout and animating the weight, I use a RelativeLayout and animate the margins. I'm not sure it's the best way because it require a call to requestLayout() at each update. It's smooth on all my devices though.
So, I animate the layout, i am not using fragments transaction. I handle the back button manually to close fragment C if it is open.
FragmentB use layout_toLeftOf/ToRightOf to keep it aligned to fragment A and C.
When my app trigger an event to display fragment C, I slide-in fragment C, and i slide-out fragment A at the same time. (2 separate animation). Inversely, when Fragment A open, i close C at the same time.
In portrait mode or on smaller screen, i use a slightly different layout and slide Fragment C over the screen.
To use percentage for the width of Fragment A and C, i think you would have to compute it at run time... (?)
Here is the activity's layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/rootpane"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<!-- FRAGMENT A -->
<fragment
android:id="#+id/fragment_A"
android:layout_width="300dp"
android:layout_height="fill_parent"
class="com.xyz.fragA" />
<!-- FRAGMENT C -->
<fragment
android:id="#+id/fragment_C"
android:layout_width="600dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
class="com.xyz.fragC"/>
<!-- FRAGMENT B -->
<fragment
android:id="#+id/fragment_B"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:layout_marginLeft="0dip"
android:layout_marginRight="0dip"
android:layout_toLeftOf="#id/fragment_C"
android:layout_toRightOf="#id/fragment_A"
class="com.xyz.fragB" />
</RelativeLayout>
The animation to slide FragmentC in or out:
private ValueAnimator createFragmentCAnimation(final View fragmentCRootView, boolean slideIn) {
ValueAnimator anim = null;
final RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) fragmentCRootView.getLayoutParams();
if (slideIn) {
// set the rightMargin so the view is just outside the right edge of the screen.
lp.rightMargin = -(lp.width);
// the view's visibility was GONE, make it VISIBLE
fragmentCRootView.setVisibility(View.VISIBLE);
anim = ValueAnimator.ofInt(lp.rightMargin, 0);
} else
// slide out: animate rightMargin until the view is outside the screen
anim = ValueAnimator.ofInt(0, -(lp.width));
anim.setInterpolator(new DecelerateInterpolator(5));
anim.setDuration(300);
anim.addUpdateListener(new AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
Integer rightMargin = (Integer) animation.getAnimatedValue();
lp.rightMargin = rightMargin;
fragmentCRootView.requestLayout();
}
});
if (!slideIn) {
// if the view was sliding out, set visibility to GONE once the animation is done
anim.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
fragmentCRootView.setVisibility(View.GONE);
}
});
}
return anim;
}