Draggable drawer with a handle (instead of action bar) on top of other apps
Asked Answered
I

5

13

Background

We all know we can use a navigation drawer as a new way to navigate in an app (even with a library, like this one) .

We also know that some apps can float above others (as shown on AirCalc, and done like so) ,using a SYSTEM_ALERT_WINDOW permission.

I've noticed that some apps combine expanding & collapsing of views that are on top , like the next ones:

and many more...

The problem

We need to merge the 2 concepts of being on top of other apps and allow dragging a handle to show the content on its left side (like a navigation drawer)

Maybe this could show what I mean:

enter image description here

As far as I know, putting anything on top using a system-alert permission requires knowing the size of the view.

However, this is different, since i can't set it to be the entire screen because i don't want to block the rest of the screen in case the user sees only the handle of the navigation drawer.

The question

Is it possible to merge the 2 concepts ?

How would I allow all states to behave nicely while being on top?

for avoiding blocking of touches , I would also like to allow the user to drag the handle up and down, or maybe customize its position in some way.

Imaginary answered 12/9, 2013 at 15:26 Comment(9)
Sounds a bit tricky. But, it shouldn't be that hard to implement. However, perfecting it would require a lot of work. Are you looking for ideas? Or, for someone to implement it.Gorgon
@user2558882 i'm looking for a way to overcome the issues i've written, and since i'm not familiar with ideas of how to slide things nicely and smoothly, i would be very happy for implementation of this part. of course, the hard part is to make the sliding look to the user as all states are actually one single state.Imaginary
It's an interesting task. I'll give it a try.Gorgon
@user2558882 i think it's better to first try to do it without having the view/s set on top, and then handle the problems that emerge when they are on top. the surroundings should be touchable when it's state 0 .Imaginary
And how about when the drawer is half open? Should the remaining area to the right be touchable?Gorgon
@user2558882 when it's half open, it means the user has either clicked on it and it's animating to be closed, or it means the user is still touching it (dragging) . in both cases, the remaining area isn't touchable since it's a "semi"-phase. i think it's ok to assume it since users that touch the remaining area won't really expect it to do anything in this "semi"-phase.Imaginary
I haven't had much time to work on this. I've just modified the NavigationDrawer example a bit. You'll still need to put in a lot of work into this. Give it a try: Link.Gorgon
Launch the app to bring the drawer on screen. To close/finish it, launch the app again. You can access the screen when the drawer is closed. The drawer will only respond when ACTION_DOWN starts from ImageView. Orientation change isn't handled right now. So, you'll have to test portrait and landscape independently. It's quite rusty right now. Hopefully, it'll at least send you off on the right path.Gorgon
@user2558882 i can't download it for some reason , and i think you forgot to post the code (it's just an apk).Imaginary
R
8

Based on a few ideas from https://github.com/NikolaDespotoski/DrawerLayoutEdgeToggle I have implemented a much simpler version of a handle for the NavigationDrawer.

Use like this:

View drawer = findViewById(R.id.drawer);
float verticalOffset = 0.2f;
DrawerHandle.attach(drawer, R.layout.handle, verticalOffset);

DrawerHandle:

import android.content.Context;
import android.graphics.Point;
import android.os.Build;
import android.support.v4.view.GravityCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.DrawerLayout;
import android.support.v4.widget.ViewDragHelper;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;

public class DrawerHandle implements DrawerLayout.DrawerListener {
    public static final String TAG = "DrawerHandle";

    private ViewGroup mRootView;
    private DrawerLayout mDrawerLayout;
    private View mHandle;
    private View mDrawer;

    private float mVerticalOffset;
    private int mGravity;
    private WindowManager mWM;
    private Display mDisplay;
    private Point mScreenDimensions = new Point();

    private OnClickListener mHandleClickListener = new OnClickListener(){

        @Override
        public void onClick(View v) {
            if(!mDrawerLayout.isDrawerOpen(mGravity)) mDrawerLayout.openDrawer(mGravity);
            else mDrawerLayout.closeDrawer(mGravity);
        }

    };

    private OnTouchListener mHandleTouchListener = new OnTouchListener() {
        private static final int MAX_CLICK_DURATION = 200;
        private long startClickTime;
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN: {
                    startClickTime = System.currentTimeMillis();
                    break;
                }
                case MotionEvent.ACTION_UP: {
                    if(System.currentTimeMillis() - startClickTime < MAX_CLICK_DURATION) {
                        v.performClick();
                        return true;
                    }
                }
            }
            MotionEvent copy = MotionEvent.obtain(event);
            copy.setEdgeFlags(ViewDragHelper.EDGE_ALL);
            copy.setLocation(event.getRawX() + (mGravity == Gravity.LEFT || mGravity == GravityCompat.START ? -mHandle.getWidth()/2 : mHandle.getWidth() / 2), event.getRawY());
            mDrawerLayout.onTouchEvent(copy);
            copy.recycle();
            return true;
        }
    };

    private int getDrawerViewGravity(View drawerView) {
        final int gravity = ((DrawerLayout.LayoutParams) drawerView.getLayoutParams()).gravity;
        return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(drawerView));
    }

    private float getTranslation(float slideOffset){
        return (mGravity == GravityCompat.START || mGravity == Gravity.LEFT) ? slideOffset*mDrawer.getWidth() : -slideOffset*mDrawer.getWidth();
    }

    private void updateScreenDimensions() {

        if (Build.VERSION.SDK_INT >= 13) {
            mDisplay.getSize(mScreenDimensions);
        } else {
            mScreenDimensions.x = mDisplay.getWidth();
            mScreenDimensions.y = mDisplay.getHeight();
        }
    }

    private DrawerHandle(DrawerLayout drawerLayout, View drawer, int handleLayout, float handleVerticalOffset) {
        mDrawer = drawer;
        mGravity = getDrawerViewGravity(mDrawer);
        mDrawerLayout = drawerLayout;
        mRootView = (ViewGroup)mDrawerLayout.getRootView();
        LayoutInflater inflater = (LayoutInflater) mDrawerLayout.getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE );
        mHandle = inflater.inflate(handleLayout, mRootView, false);
        mWM = (WindowManager) mDrawerLayout.getContext().getSystemService(Context.WINDOW_SERVICE);
        mDisplay = mWM.getDefaultDisplay();

        mHandle.setOnClickListener(mHandleClickListener);   
        mHandle.setOnTouchListener(mHandleTouchListener);
        mRootView.addView(mHandle, new FrameLayout.LayoutParams(mHandle.getLayoutParams().width, mHandle.getLayoutParams().height, mGravity));
        setVerticalOffset(handleVerticalOffset);
        mDrawerLayout.setDrawerListener(this);
    }

    public static DrawerHandle attach(View drawer, int handleLayout, float verticalOffset) {
        if (!(drawer.getParent() instanceof DrawerLayout)) throw new IllegalArgumentException("Argument drawer must be direct child of a DrawerLayout");
        return new DrawerHandle((DrawerLayout)drawer.getParent(), drawer, handleLayout, verticalOffset);
    }

    public static DrawerHandle attach(View drawer, int handleLayout) {
        return attach(drawer, handleLayout, 0);
    }

    @Override
    public void onDrawerClosed(View arg0) {
    }

    @Override
    public void onDrawerOpened(View arg0) {

    }

    @Override
    public void onDrawerSlide(View arg0, float slideOffset) {
        float translationX = getTranslation(slideOffset);
        mHandle.setTranslationX(translationX);
    }

    @Override
    public void onDrawerStateChanged(int arg0) {

    }

    public View getView(){
        return mHandle;
    }

    public View getDrawer() {
        return mDrawer;
    }

    public void setVerticalOffset(float offset) {
        updateScreenDimensions();
        mVerticalOffset = offset;
        mHandle.setY(mVerticalOffset*mScreenDimensions.y);
    }
}

Layout:

<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" >        

        <RelativeLayout 
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            >

            <com.fscz.views.BounceViewPager
                android:id="@+id/content_pager"
                android:layout_width="match_parent"
                android:layout_height="match_parent" 
                android:layout_centerInParent="true"
                />

            <com.fscz.views.CirclePageIndicator
                android:id="@+id/content_indicator"
                android:layout_height="wrap_content"
                android:layout_width="wrap_content" 
                android:padding="10dp"
                android:layout_centerHorizontal="true"
                android:layout_alignParentBottom="true"
                android:layout_marginTop="100dp"
                style="@style/link"
                />

        </RelativeLayout>

    <LinearLayout
            android:id="@+id/drawer"
            android:layout_width="240dp"
            android:layout_height="match_parent"
            android:layout_gravity="right"
            android:orientation="vertical"
            android:padding="20dp"
            android:background="@color/black_transparent"
            >
            <TextView
                android:layout_width="240dp"
                android:layout_height="wrap_content"
                style="@style/text"
                android:text="@string/collections"
                android:paddingBottom="20dp"
                />
            <ListView 
                android:id="@+id/drawer_list"
                android:layout_width="240dp"
                android:layout_height="0dip"
                android:choiceMode="singleChoice"
                android:divider="@android:color/transparent"
                android:dividerHeight="0dp"
                android:layout_weight="1"
                />
    </LinearLayout>

</android.support.v4.widget.DrawerLayout>

Activity:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_browse);

    mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
    mDrawer = findViewById(R.id.drawer);
    mDrawerList = (ListView) findViewById(R.id.drawer_list);

    mDrawerList.setAdapter(new ArrayAdapter<String>(this,
            R.layout.drawer_list_item, Preferences.getKnownCollections()));
    mDrawerList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapter, View view, int pos, long id) {
                Preferences.setActiveCollection(Preferences.getKnownCollections()[pos]);
                loader.loadAll(Preferences.getKnownCollections()[pos], BrowseActivity.this);
                mDrawerLayout.closeDrawers();
            }
    });

    DrawerHandle.attach(mDrawer, R.layout.handle, 0.2f);

}
Rodriquez answered 8/9, 2014 at 10:21 Comment(6)
looks promising. can you please put it in a project so I could test it out? it's just that it was a very long time that I've asked this question...Imaginary
never mind the rep. i just wanted to show a simple solution, as this question as been asked quite a few times. i am using the exact code in an application right now and it is tested with both left and right hand side drawer.Rodriquez
But it is interesting. I've tried doing it myself, and in the end we dropped it because I didn't make it. The most problematic part for me was the dragging that should first start making the layout larger, and then continue to the next phase as you still do the dragging. I don't know why I didn't succeed in it. Can you please share a tiny sample that does it? I don't even care if it's a drawer.Imaginary
@Rodriquez i have tried your solution as u suggested above.but when i click on the handle image my app got crashed...any suggestion will be helpful from your side.Please look here :: #28186904Loathe
i answered your question. you are trying to add the handle to the layout and not the drawer.Rodriquez
I guess what lacks in this solution is the explanation that contains simple XML layout (Linear, Constraint or similar) containing View (ImageView, ImageButton or similar) that represents handle.Yakut
A
1

Check this library out! Very simple implementation.

https://github.com/kedzie/DraggableDrawers

Astigmia answered 6/8, 2014 at 20:20 Comment(1)
This question was written a long time ago. I don't think that what you've offered works well with on top views. Have you checked it out? The sample doesn't seem to use this permission...Imaginary
S
0

Its quite an interesting idea you got there. From what I've understood so far; you want to create a navigation drawer type view and implement it like a floating overlay.

It is possible. I have found possible resources which may help you. You could either use a Window manager,wrap the navigation drawer view within the Window Manager, call it with a service. And Track the user touch motion with the WinodowManager. LaoutParams.(with the x & y position of the touch; using onTouchListner)

Here is an open source project where they are trying to create a facebook chat head like feature using WindowManager

There is also an Android library, which you can creating floating windows.I presume the app you pointed out earlier AirCalc etc are using a customized version of this project.

Here is a simple Demo of this project. and here is the link to the project StandOut lib project

Regards -Sathya

Sinking answered 19/9, 2013 at 10:33 Comment(1)
no, i already know how to make anything float (as i've shown in the links inside the question) , and all you need to keep it float is just a foreground service . the hard part isn't that. the hard part is to make the view which will have 2 states AND on top, since views that are on top must have their sizes predefined, and if you have 2 states while having a dragging event, it's a problem because you need to change the size on the touch-down event.Imaginary
H
0

This can work from API level 8 because there is no View.setX before API level 11 I guess.

You can put this inside your OnCreate:

YOU_DRAWER.setDrawerListener(this);

and override this method and implement this DrawerListener on your activity:

@Override
public void onDrawerSlide(View arg0, float arg1) {

    /// then arg0 is your drawer View
    /// the arg1 is your offset of the drawer in the screen

    params.leftMargin = (int) (arg0.getWidth()*arg1); 
    YOUR_VIEW.setLayoutParams(params);
}
Heterodyne answered 6/6, 2014 at 19:41 Comment(1)
This isn't about normal views being moved. It's about floating views, on top of other apps. Look at Facebook's chat-head and of "AirCalc" , to see what I mean...Imaginary
Y
0

Basing on answer from Fabian I have even simplified this solution slightly.

So what you need is:

  1. MainActivity.xml like this:

     <androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/lout_drawer"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
    
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/lout_main_top"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         tools:context=".MainActivity">
    
      ...  Your Views on Main screen here ...
     </androidx.constraintlayout.widget.ConstraintLayout>
    
     <com.google.android.material.navigation.NavigationView
         android:id="@+id/nv_drawer"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:layout_gravity="start"
         />
     </androidx.drawerlayout.widget.DrawerLayout>
    
  2. Content of NavigationView - menu_drawer_layout.xml - which you inject in the code (so you do not define it directly in main_activity.xml, in DrawerLayout, with app:headerLayout. Otherwise it gets duplicated):

     <androidx.constraintlayout.widget.ConstraintLayout
         xmlns:android="http://schemas.android.com/apk/res/android"
         android:id="@+id/lout_menu_main"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content">
    
     ...  any Views here ...
     </androidx.constraintlayout.widget.ConstraintLayout>
    

in the NavigationView you can optionally add Menu (xml) which you can create in res/menu (as xml, you will find instructions how to do it easily).

  1. Layout containing a handler - menu_drawer_handler.xml. The View inside might be a graphic stored in drawable or xml from drawable. Here is example:

     <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         xmlns:tools="http://schemas.android.com/tools"
         xmlns:app="http://schemas.android.com/apk/res-auto"
         tools:ignore="contentDescription">
    
         <ImageView
             android:id="@+id/iv_drawer_handler"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:src="@drawable/your_drawable_here"
             android:scaleType="fitXY"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
             />
    
     </androidx.constraintlayout.widget.ConstraintLayout>
    
  2. And the simplified code from #Fabian:

     public class DrawerHandle implements DrawerLayout.DrawerListener {
    
     private final View mHandle;
     private final View mDrawer;
    
     private final int mGravity;
     private final Display mDisplay;
     private final Point mScreenDimensions = new Point();
    
     private int getDrawerViewGravity(View drawerView) {
         final int gravity = ((DrawerLayout.LayoutParams) drawerView.getLayoutParams()).gravity;
         return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(drawerView));
     }
    
     @SuppressLint("RtlHardcoded")
     private float getTranslation(float slideOffset){
         return (mGravity == GravityCompat.START || mGravity == Gravity.LEFT) ? slideOffset * mDrawer.getWidth() : -slideOffset * mDrawer.getWidth();
     }
    
     private void updateScreenDimensions() {
         mDisplay.getSize(mScreenDimensions);
     }
    
     public DrawerHandle(DrawerLayout drawerLayout, View drawer, int handleLayout, float handleVerticalOffset) {
         mDrawer = drawer;
         mGravity = getDrawerViewGravity(mDrawer);
         ViewGroup mRootView = (ViewGroup) drawerLayout.getRootView();
         LayoutInflater inflater = (LayoutInflater) drawerLayout.getContext().getSystemService( Context.LAYOUT_INFLATER_SERVICE );
         mHandle = inflater.inflate(handleLayout, mRootView, false);
         WindowManager mWM = (WindowManager) drawerLayout.getContext().getSystemService(Context.WINDOW_SERVICE);
         mDisplay = mWM.getDefaultDisplay();
    
         mRootView.addView(mHandle, new FrameLayout.LayoutParams(mHandle.getLayoutParams().width, mHandle.getLayoutParams().height, mGravity));
         setVerticalOffset(handleVerticalOffset);
         drawerLayout.addDrawerListener(this);
     }
    
     public static DrawerHandle attach(View drawer, int handleLayout, float verticalOffset) {
         if (!(drawer.getParent() instanceof DrawerLayout)) throw new IllegalArgumentException("Argument drawer must be direct child of a DrawerLayout");
         return new DrawerHandle((DrawerLayout)drawer.getParent(), drawer, handleLayout, verticalOffset);
     }
    
     public static DrawerHandle attach(View drawer, int handleLayout) {
         return attach(drawer, handleLayout, 0);
     }
    
     @Override
     public void onDrawerClosed(@NonNull View arg0) {
     }
    
     @Override
     public void onDrawerOpened(@NonNull View arg0) {
    
     }
    
     @Override
     public void onDrawerSlide(@NonNull View arg0, float slideOffset) {
         float translationX = getTranslation(slideOffset);
         mHandle.setTranslationX(translationX);
     }
    
     @Override
     public void onDrawerStateChanged(int arg0) {
    
     }
    
     public View getView(){
         return mHandle;
     }
    
     public View getDrawer() {
          return mDrawer;
     }
    
     public void setVerticalOffset(float offset) {
         updateScreenDimensions();
         mHandle.setY(offset *mScreenDimensions.y);
     }
     }
    
  3. To attach handle to the DrawerLayout (and inflate HeaderLayout) use this in MainActivity:

         NavigationView drawer = findViewById(R.id.nv_drawer);
         View headerLayout = drawer.inflateHeaderView(R.layout.menu_drawer_layout);
         DrawerHandle.attach(drawer, R.layout.menu_drawer_handler);
    

I hope this is complete set of what you need to do, without using external libraries.

Yakut answered 2/5, 2022 at 5:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.