Android - Display a screen recording on a SurfaceView - java

I am developing an application that can record the contents of an Android device's screen and display it on a SurfaceView. As of now, the SurfaceView is a box in the middle of the screen and is currently showing the contents of the entire screen, along with itself, creating a repeating image. Is there a way to repeatedly hide the SurfaceView, create a virtual display, then show the SurfaceView with the contents of the virtual display?
RecordingSession.java
class RecordingSession
implements MediaScannerConnection.OnScanCompletedListener {
static final int VIRT_DISPLAY_FLAGS=
DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY |
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC;
private RecordingConfig config;
private final File output;
private final Context ctxt;
private final ToneGenerator beeper;
private MediaRecorder recorder;
private MediaProjection projection;
private VirtualDisplay vdisplay;
private Window window;
RecordingSession(Context ctxt, RecordingConfig config,
MediaProjection projection, Window window) {
this.ctxt=ctxt.getApplicationContext();
this.window = window;
this.config=config;
this.projection=projection;
this.beeper=new ToneGenerator(
AudioManager.STREAM_NOTIFICATION, 100);
output=new File(ctxt.getExternalFilesDir(null), "andcorder.mp4");
output.getParentFile().mkdirs();
}
void start() {
//this.window.close();
vdisplay=projection.createVirtualDisplay("andcorder",
config.width, config.height, config.density,
VIRT_DISPLAY_FLAGS, this.window.getScreenShot().getHolder().getSurface(), null, null);
//this.window.open();
beeper.startTone(ToneGenerator.TONE_PROP_ACK);
}
void stop() {
projection.stop();
vdisplay.release();
}
#Override
public void onScanCompleted(String path, Uri uri) {
beeper.startTone(ToneGenerator.TONE_PROP_NACK);
}
}
Window.java
public class Window {
// declaring required variables
private Context context;
private View mView;
private WindowManager.LayoutParams mParams;
private WindowManager mWindowManager;
private LayoutInflater layoutInflater;
public SurfaceView getScreenShot() {
return screenShot;
}
private SurfaceView screenShot;
private LinearLayout toDisplay;
public Window(Context context){
this.context=context;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// set the layout parameters of the window
mParams = new WindowManager.LayoutParams(
// Shrink the window to wrap the content rather
// than filling the screen
WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT,
// Display it on top of other application windows
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
// Don't let it grab the input focus
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
// Make the underlying application window visible
// through any transparent parts
PixelFormat.TRANSLUCENT);
}
// getting a LayoutInflater
layoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// inflating the view with the custom layout we created
mView = layoutInflater.inflate(R.layout.popup_window, null);
screenShot= mView.findViewById(R.id.screenShot);
// Define the position of the
// window within the screen
mParams.gravity = Gravity.CENTER;
mWindowManager = (WindowManager)context.getSystemService(WINDOW_SERVICE);
}
public void open() {
try {
// check if the view is already
// inflated or present in the window
if(mView.getWindowToken()==null) {
if(mView.getParent()==null) {
mWindowManager.addView(mView, mParams);
}
}
} catch (Exception e) {
Log.d("Error1",e.toString());
}
}
public void hide() {
try {
mView.setVisibility(View.INVISIBLE);
} catch (Exception e) {
Log.d("Error3",e.toString());
}
}
public void show() {
try {
mView.setVisibility(View.VISIBLE);
} catch (Exception e) {
Log.d("Error4",e.toString());
}
}
public void close() {
try {
// remove the view from the window
((WindowManager)context.getSystemService(WINDOW_SERVICE)).removeView(mView);
// invalidate the view
mView.invalidate();
// remove all views
((ViewGroup)mView.getParent()).removeAllViews();
// the above steps are necessary when you are adding and removing
// the view simultaneously, it might give some exceptions
} catch (Exception e) {
Log.d("Error2",e.toString());
}
}
}
popup_window.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="4dp"
android:background="#null">
<SurfaceView
android:id="#+id/screenShot"
android:layout_width="match_parent"
android:layout_height="250dp" />
</RelativeLayout>

I have played around with my code and discovered that doubling the virtualdisplay's height and width did the trick. I am a beginner in android development and therefore do not know the reason as to why this worked. If someone can shed light onto why this tweak did the trick, that would be great.
vdisplay=projection.createVirtualDisplay("andcorder",
config.width*2, config.height*2, config.density,
VIRT_DISPLAY_FLAGS, this.window.getScreenShot().getHolder().getSurface(), null, null);

Related

How to hide the navigation bar when entering fullview in webview?

I have found this code to enter in full screen when playing a video in webview, but there is a problem in it.
For example if I am in YouTube all works properly at the first time (when I play the video), but when I click to change the settings of the video like the quality or the speed, the navigation bar appears and still there until I exit the video.
}
public class CustomWebClient extends WebChromeClient {
private View mCustomView;
private WebChromeClient.CustomViewCallback mCustomViewCallback;
protected FrameLayout frame;
// Initially mOriginalOrientation is set to Landscape
private int mOriginalOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
private int mOriginalSystemUiVisibility;
// Constructor for CustomWebClient
public CustomWebClient() {}
public Bitmap getDefaultVideoPoster() {
if (MainActivity.this == null) {
return null; }
return BitmapFactory.decodeResource(MainActivity.this.getApplicationContext().getResources(), 2130837573); }
public void onShowCustomView(View paramView, WebChromeClient.CustomViewCallback viewCallback) {
if (this.mCustomView != null) {
onHideCustomView();
return; }
this.mCustomView = paramView;
this.mOriginalSystemUiVisibility = MainActivity.this.getWindow().getDecorView().getSystemUiVisibility();
// When CustomView is shown screen orientation changes to mOriginalOrientation (Landscape).
MainActivity.this.setRequestedOrientation(this.mOriginalOrientation);
//
//here is the code I'm using to hide status bar
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
// After that mOriginalOrientation is set to portrait.
this.mOriginalOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
this.mCustomViewCallback = viewCallback; ((FrameLayout)MainActivity.this.getWindow().getDecorView()).addView(this.mCustomView, new FrameLayout.LayoutParams(-1, -1)); MainActivity.this.getWindow().getDecorView().setSystemUiVisibility(3846);
//
//here is the code I'm using to hide navigation bar
getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
//
}
public void onHideCustomView() {
((FrameLayout)MainActivity.this.getWindow().getDecorView()).removeView(this.mCustomView);
this.mCustomView = null;
MainActivity.this.getWindow().getDecorView().setSystemUiVisibility(this.mOriginalSystemUiVisibility);
// When CustomView is hidden, screen orientation is set to mOriginalOrientation (portrait).
MainActivity.this.setRequestedOrientation(this.mOriginalOrientation);
//////////////
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().getDecorView().setSystemUiVisibility( View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
//////////////
// After that mOriginalOrientation is set to landscape.
this.mOriginalOrientation = android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; this.mCustomViewCallback.onCustomViewHidden();
this.mCustomViewCallback = null;
}
}
{
Here is some pictures of the problem:

Why is my Android ImageView empty (loading up through Uri, Bitmap or using Picasso)

I have a fragment to display a queue of either videos of images. The video I display in the VideoView works fine, it replays, it's golden. But the images I put in the ImageView just appear invisible. I tried loading them through Uri, by reading a Bitmap, now they're set up with Picasso, and none of it fixed it. The AssetObtainer you'll see in MultimediaPlayer works with both sound files and videos so far, so I highly doubt it has an issue with images. Here's the code:
MultimediaPlayer.java :
public class MultimediaPlayer extends Fragment
{
VideoView mVideoView;
ImageView mImageView;
MultimediaViewModel mMultimediaViewModel;
Play mPlayThread;
Activity mActivity;
AssetObtainer assetObtainer = new AssetObtainer();
public Long mTutorialId;
public List<Multimedia> multimedias = new LinkedList<>();
#Override
public View onCreateView(#NotNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
return inflater.inflate(R.layout.fragment_multimedia_player, container, false);
}
#Override
public void onViewCreated(View view, Bundle savedInstanceState)
{
mActivity = requireActivity();
mMultimediaViewModel = new ViewModelProvider(this).get(MultimediaViewModel.class);
mVideoView = view.findViewById(R.id.video_embed);
mImageView = view.findViewById(R.id.image_embed);
getPlayer(0);
}
private void getPlayer(int position)
{
if(mPlayThread != null) {
mPlayThread.interrupt();
position++;
}
if(!multimedias.isEmpty()) {
mPlayThread = new Play(multimedias.get(position));
mPlayThread.start();
}
}
private class Play extends Thread
{
private final Multimedia currentMedia;
Play(Multimedia media){
currentMedia = media;
}
#Override
public void run()
{
int position = currentMedia.getPosition();
int displayTime = currentMedia.getDisplayTime();
boolean loopBool = currentMedia.getLoop();
if(currentMedia.getType()) {
mActivity.runOnUiThread(() -> {
mImageView.setVisibility(View.VISIBLE);
mVideoView.setVisibility(View.GONE);
try {
Picasso.get().load(assetObtainer.getFileFromAssets(requireContext(), currentMedia.getFullFileName())).into(mImageView);
} catch (IOException ignored) {}
});
if(displayTime>0) {
try {
sleep(displayTime);
if(!loopBool) multimedias.remove(currentMedia);
if(position<multimedias.size()-1) {
getPlayer(position);
} else getPlayer(0);
} catch (InterruptedException e) {
mActivity.runOnUiThread(() -> mImageView.setVisibility(View.GONE));
interrupt();
}
}
} else {
mActivity.runOnUiThread(() -> {
mVideoView.setVisibility(View.VISIBLE);
mImageView.setVisibility(View.GONE);
try {
mVideoView.setVideoURI(Uri.fromFile(assetObtainer.getFileFromAssets(requireContext(), currentMedia.getFullFileName())));
} catch (IOException ignored) {}
if(loopBool && multimedias.size()==1) mVideoView.setOnCompletionListener(v->getPlayer(position-1));
mVideoView.start();
});
}
}
}
#Override
public void onPause()
{
if(mPlayThread!=null){
mPlayThread.interrupt();
}
super.onPause();
}
}
bed for the fragment in the activity .xml file :
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/layout_multimedia"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="#id/active_instructions" />
and the .xml file of the fragment :
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".tutorial.mediaplayer.MultimediaPlayer">
<VideoView
android:id="#+id/video_embed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="#+id/image_embed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="#string/image_default_no_description" />
</androidx.constraintlayout.widget.ConstraintLayout>
As mentioned, video works super fine, but images won't show. I'd appreciate even monkey wrench suggestions before I have to rework this entirely.
As mentioned in my other answer, it's not related to the ImageView, it's a problem with SQLite and loading a boolean from the database. The default false which I wanted to correspond with video types is default, so it worked, but since it doesn't default to true the image condition wasn't fulfilled. Anyway the ImageView will work once it is actually ran.

how load fragment in ViewPager only when its selected

I'm using 3 Fragments inside a Viewpager, the problem is that I am loading big data in an Asynctask and loaders. On devices like HTC one, it works fine, however, on low-end devices, it takes a lot of time. This is mainly because when I implement the pagerAdapter, I put the Fragments inside an ArrayList, this force the fragments instantiate when the main activity is loaded. What I need is that it just "load" the first fragment (main) and when the user Swype, load the other fragment. its any way to achieve this? this is my pageAdapater
public class PagerAdapter extends FragmentPagerAdapter {
private final ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
// private final ArrayList<String> titulos = new ArrayList<String>();
// private int NUM_PAGES =0;
public PagerAdapter(FragmentManager manager,int num_pages) {
super(manager);
// this.NUM_PAGES = num_pages;
}
public void addFragment(Fragment fragment,String title) {
mFragments.add(fragment);
notifyDataSetChanged();
}
#Override
public int getCount() {
//return NUM_PAGES;
return mFragments.size();
}
#Override
public Fragment getItem(int position) {
return mFragments.get(position);
}
}
The above method from Sun did not work for me (maybe it does for you), but I thought I would share my edit of his method also. Very good method by the way Sun!
private boolean _hasLoadedOnce= false; // your boolean field
#Override
public void setUserVisibleHint(boolean isFragmentVisible_) {
super.setUserVisibleHint(true);
if (this.isVisible()) {
// we check that the fragment is becoming visible
if (isFragmentVisible_ && !_hasLoadedOnce) {
new NetCheck().execute();
_hasLoadedOnce = true;
}
}
}
I'm gonna add my solution here since I faced a similar issue. My asynchronous task wasn't loading huge amounts of data, but it prevents unnecessary network calls. Here's what I added in my Fragment:
private boolean _hasLoadedOnce= false; // your boolean field
#Override
public void setUserVisibleHint(boolean isFragmentVisible_) {
super.setUserVisibleHint(isVisibleToUser);
if (this.isVisible()) {
// we check that the fragment is becoming visible
if (!isFragmentVisible_ && !_hasLoadedOnce) {
//run your async task here since the user has just focused on your fragment
_hasLoadedOnce = true;
}
}
}
With the above code, your Fragment will be loaded, but your async task will not run until the user actually scrolls to the Fragment for the first time. Once displayed, your async task will run for the first time automatically. Then you can provide a way to load more data via a button or pull to refresh. The above Fragment was in my ViewPager and seemed to work fine.
Slightly modified version to fix potential NPE caused by some views not fully initialised
private boolean _hasLoadedOnce= false; // your boolean field
#Override
public void setUserVisibleHint(boolean isFragmentVisible_) {
super.setUserVisibleHint(isVisibleToUser);
if (this.isVisible()) {
// we check that the fragment is becoming visible
if (!isFragmentVisible_ && !_hasLoadedOnce) {
new Handler().post(() -> {
makeAsyncRequest();//do your asyn stuffs
_hasLoadedOnce = true;
});
}
}
}
Use fragmentStatePageAdapter if you have a lot of pages and you want to destroy them when not visible.
It has implemented a setMenuVisibility(boolean menuVisible) when fragment becomes visible, so use that.
I might be late for the party but here's my solution and it works as expected. In all of your child fragments create a boolean variable:
private boolean loadFragmentExecuted = false;
in the child fragments create a generic method called loadFragment and move all of the logic you added in onCreateView to that method:
public void loadFragment()
{
if(!loadFragmentExecuted)
{
//Add your logic to manipulate the UI or load data etc...
loadFragmentExecuted = true;
}
}
in your pageview logic create the fragments dynamically like:
//add the fragment
String fragmentName = "com.something." + fragmentId;
//check if the class exists
try
{
Class myFragmentClass = Class.forName(fragmentName);
Fragment myFragment = (Fragment) myFragmentClass.newInstance();
mFragments.add(myFragment);
}
catch (ClassNotFoundException e)
{
e.printStackTrace();
}
catch (IllegalAccessException e)
{
e.printStackTrace();
}
catch (InstantiationException e)
{
e.printStackTrace();
}
then set your pager adapter and attach a tablayout with it:
//set our pager adapter that contains different fragments
mPagerAdapter = new BasePagerAdapter(mFragmentManager, mFragments);
//link the adapter to the viewpager
mViewPager.setAdapter(mPagerAdapter);
//cache fragments
int limit = (mPagerAdapter.getCount() > 0 ? mPagerAdapter.getCount() : 1);
mViewPager.setOffscreenPageLimit(limit);
//add the page listner to the viewPager and link it to the tabLayout
mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(mTabLayout));
//on tab selected select current viewpager item
mTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener()
{
#Override
public void onTabSelected(TabLayout.Tab tab)
{
mViewPager.setCurrentItem(tab.getPosition());
//get fragment for the selected tab
Fragment f = mPagerAdapter.getItem(tab.getPosition());
//load the content of the fragment
try
{
Class c = f.getClass();
Method loadFragment = c.getMethod("loadFragment");
loadFragment.invoke(f);
}
catch (IllegalAccessException e){}
catch (InvocationTargetException e){}
catch (NoSuchMethodException e){}
}
#Override
public void onTabUnselected(TabLayout.Tab tab)
{
}
#Override
public void onTabReselected(TabLayout.Tab tab)
{
}
});

Android: how to get default camera settings of builtin camera application

I have seen lots of tutorial and information but i could not find any single place how to use the default settings of the existing camera application into any other customized camera application. I have seen the sharpness of the image and its focus is very fine in the built-in camera application. Now i am creating my own application with my customized features but i am still unable to make it sharp and non-blurry... I dont want to use Intent technique of the camera because i have to do some image processing afterward.
I have used zooming but strangely zoom is not properly working ...like it works in built-in camera application
here is my surface change code
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h)
{
Log.e(TAG, "surfaceChanged");
// XXX stopPreview() will crash if preview is not running
if (mPreviewRunning) {
mCamera.stopPreview();
}
Camera.Parameters params = mCamera.getParameters();
List<Camera.Size> sizes = params.getSupportedPreviewSizes();
mFrameWidth = w;
mFrameHeight = h;
// selecting optimal camera preview size
{
double minDiff = Double.MAX_VALUE;
for (Camera.Size size : sizes)
{
if (Math.abs(size.height - h) < minDiff)
{
mFrameWidth = size.width;
mFrameHeight = size.height;
minDiff = Math.abs(size.height - h);
}
}
}
try
{
//params.set("rotation", 180);
//params.set("orientation", "landscape");
//params.set("auto", "WHITE_BALANCE_AUTO");//WHITE_BALANCE_AUTO
Display display = ((WindowManager)getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
if(display.getRotation() == Surface.ROTATION_0)
{
params.setPreviewSize(mFrameHeight, mFrameWidth);
mCamera.setDisplayOrientation(90);
}
if(display.getRotation() == Surface.ROTATION_90)
{
params.setPreviewSize(mFrameWidth, mFrameHeight);
}
if(display.getRotation() == Surface.ROTATION_180)
{
params.setPreviewSize(mFrameHeight, mFrameWidth);
}
if(display.getRotation() == Surface.ROTATION_270)
{
params.setPreviewSize(mFrameWidth, mFrameHeight);
mCamera.setDisplayOrientation(180);
}
if(params.isZoomSupported())
{
Log.e(TAG, params.getZoom()+"surfaceChanged camer zoom"+params.getMinExposureCompensation());
params.setZoom(params.getMaxZoom());
params.setExposureCompensation(1);
// params.setColorEffect("none");
params.setWhiteBalance(params.WHITE_BALANCE_AUTO);
params.setFocusMode(params.FOCUS_MODE_AUTO);
params.setSceneMode(params.SCENE_MODE_ACTION);
}
params.set("auto", "FOCUS_MODE_AUTO");
params.setPreviewSize(mFrameWidth,mFrameHeight);
mCamera.setParameters(params);
mCamera.setPreviewDisplay(holder);
}
catch (IOException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
mCamera.startPreview();
mPreviewRunning = true;
}
Kindly let me know how to make the camera preview exactly same as the built in application one.
You mean a fullscreen camera preview?
I use this code:
this.requestWindowFeature(Window.FEATURE_NO_TITLE); //no title
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); //no status bar, etc
and this:
setContentView(R.layout.main);
addContentView(overlay, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
((FrameLayout) findViewById(R.id.preview)).addView(preview);
the first snippet sets the app to fullscreen and hide title and status bar.
the second snipppet adds my overlay (extended View) to the main layout.
Here my xml and java code:
main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<FrameLayout
android:id="#+id/preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="#drawable/ic_launcher" />
</LinearLayout>
Overlay.java:
class Overlay extends View {
String text = "";
String textBearing = "Bearing: ";
public Overlay(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE);
paint.setTextSize(16);
canvas.drawText(text, 20, 20, paint);
canvas.drawText(textBearing, 20, 50, paint);
super.onDraw(canvas);
}
}
And my activity:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.requestWindowFeature(Window.FEATURE_NO_TITLE); //no title
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); //fullscreen
overlay = new Overlay(this);
setContentView(R.layout.main);
addContentView(overlay, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
camera = getCameraInstance(); //camera.open();
preview = new Preview(this, camera);
((FrameLayout) findViewById(R.id.preview)).addView(preview);
}
Hope it helps
I encountered the same problem with you. After reading the source code of the builtin camera app, and comparing the focus processing of builtin camera and my own camera, I realized the problem is on autofocus.
So try this:
mCamera.autoFocus(new Camera.AutoFocusCallback() {
#Override
public void onAutoFocus(boolean success, Camera camera) {
mCamera.takePicture(null, null, mPicture);
}
});
which makes the result image as sharp as builtin camera.
The documents is here.

StateListDrawable to switch colorfilters

I want to create custom buttons to use in a TabHost. I haven been trying to just use the same image resource (png), but have the colorfilter change depending on the state. So I made this bit to serve as the layout for the custom button:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="fill_parent">
<ImageView android:id="#+id/tab_icon"
android:layout_centerInParent="true" android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView android:id="#+id/tab_text" android:layout_below="#id/tab_icon"
android:layout_centerHorizontal="true" android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
In my activity, I add the tabs like this:
tabHost.addTab(tabHost.newTabSpec(TAB_NAME_NEWS).setIndicator(buildTab(R.drawable.tab_icon_news, R.string.news))
.setContent(newsIntent));
And this is the 'buildTab' method:
private final static int[] SELECTED = new int[] { android.R.attr.state_selected };
private final static int[] IDLE = new int[] { -android.R.attr.state_selected };
private View buildTab(int icon, int label) {
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.tab_button, null);
StateListDrawable drawable = new StateListDrawable();
Drawable selected = getResources().getDrawable(icon);
selected.mutate();
selected.setBounds(0, 0, selected.getIntrinsicWidth(), selected.getIntrinsicHeight());
selected.setColorFilter(new LightingColorFilter(0xFFFFFFFF, 0x0000FF00));
drawable.addState(SELECTED, selected);
Drawable idle = getResources().getDrawable(icon);
idle.mutate();
idle.setColorFilter(new LightingColorFilter(0xFFFFFFFF, 0x000000FF));
drawable.addState(IDLE, idle);
((ImageView) view.findViewById(R.id.tab_icon)).setImageDrawable(drawable);
((TextView) view.findViewById(R.id.tab_text)).setText(getString(label));
return view;
}
In the selected state, the image should be completely green (0x0000FF00), and in the non-selected state, it should be blue (0x000000FF).
The problem is that the colorfilters appear to be be completely ignored. I can not see the colors change under any circumstances.
I've also tried to get the same result by setting the android:tint property on the <ImageView/>, but apparently you cannot use a reference to a <selector> there, since it throws a NumberFormatException.
I don't see what I'm doing wrong so any help would be appreciated.
OK, I never got the above code to work, so here's what I ended up doing.
First, I subclassed LayerDrawable:
public class StateDrawable extends LayerDrawable {
public StateDrawable(Drawable[] layers) {
super(layers);
}
#Override
protected boolean onStateChange(int[] states) {
for (int state : states) {
if (state == android.R.attr.state_selected) {
super.setColorFilter(Color.argb(255, 255, 195, 0), PorterDuff.Mode.SRC_ATOP);
} else {
super.setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_ATOP);
}
}
return super.onStateChange(states);
}
#Override
public boolean isStateful() {
return true;
}
}
I changed the buildTab() method to the following:
private View buildTab(int icon, int label) {
LayoutInflater inflater = LayoutInflater.from(this);
View view = inflater.inflate(R.layout.tab_button, null);
((ImageView) view.findViewById(R.id.tab_icon)).setImageDrawable(new StateDrawable(new Drawable[] { getResources()
.getDrawable(icon) }));
((TextView) view.findViewById(R.id.tab_text)).setText(getString(label));
return view;
}
I still add the tabs like this:
Intent fooIntent = new Intent().setClass(this, FooActivity.class);
tabHost.addTab(tabHost.newTabSpec(TAB_NAME_INFO).setIndicator(buildTab(R.drawable.tab_icon_info, R.string.info)).setContent(infoIntent));
This works for me, compatible with android 1.6.
Couldn't solve it with applying a colorfilter directly to the drawable either. What worked for me was getting the image as a Bitmap, create an empty second one with same measures, define a canvas for the second one, apply that colorfilter to a paint object and draw the first bitmap on the second one. Finally create a BitmapDrawable from the new Bitmap and you're done. Here is the code
ImageButton imageButton = (ImageButton)findViewById(R.id.aga);
Bitmap one = BitmapFactory.decodeResource(getResources(), R.drawable.pen_circle);
Bitmap oneCopy = Bitmap.createBitmap(one.getWidth(), one.getHeight(), Config.ARGB_8888);
Canvas c = new Canvas(oneCopy);
Paint p = new Paint();
p.setColorFilter(new LightingColorFilter(Color.CYAN, 1));
c.drawBitmap(one, 0, 0, p);
StateListDrawable states = new StateListDrawable();
states.addState(new int[] {android.R.attr.state_pressed}, new BitmapDrawable(oneCopy));
states.addState(new int[] { }, imageButton.getDrawable());
imageButton.setImageDrawable(states);
This is my class, hacked to support ColorFilter:
Usage:
final Drawable icon = getResources().getDrawable(iconResId);
final Drawable filteredIcon = // this is important
icon.getConstantState().newDrawable();
final FilterableStateListDrawable selectorDrawable =
new FilterableStateListDrawable();
selectorDrawable.addState(ICON_STATE_SELECTED, filteredIcon,
new PorterDuffColorFilter(mIconOverlayColor, PorterDuff.Mode.SRC_ATOP));
selectorDrawable.addState(ICON_STATE_DEFAULT, icon);
As you see the ColorFilter is not applied directly to the drawable, it is associated to it while adding a state to the selector Drawable.
What's important here is that
you need to create a new drawable from the constant state or you'll modify the constant state and thus any instance of that drawable around your activity.
you need to use my custom addState method, it has the same name of the framework method addState but I've added an additional argument (ColorFilter). This method does NOT exist in the framework superclass!
The code (dirty, but work for me):
/**
* This is an extension to {#link android.graphics.drawable.StateListDrawable} that workaround a bug not allowing
* to set a {#link android.graphics.ColorFilter} to the drawable in one of the states., it add a method
* {#link #addState(int[], android.graphics.drawable.Drawable, android.graphics.ColorFilter)} for that purpose.
*/
public class FilterableStateListDrawable extends StateListDrawable {
private int currIdx = -1;
private int childrenCount = 0;
private SparseArray<ColorFilter> filterMap;
public FilterableStateListDrawable() {
super();
filterMap = new SparseArray<ColorFilter>();
}
#Override
public void addState(int[] stateSet, Drawable drawable) {
super.addState(stateSet, drawable);
childrenCount++;
}
/**
* Same as {#link #addState(int[], android.graphics.drawable.Drawable)}, but allow to set a colorFilter associated to this Drawable.
*
* #param stateSet - An array of resource Ids to associate with the image.
* Switch to this image by calling setState().
* #param drawable -The image to show.
* #param colorFilter - The {#link android.graphics.ColorFilter} to apply to this state
*/
public void addState(int[] stateSet, Drawable drawable, ColorFilter colorFilter) {
// this is a new custom method, does not exist in parent class
int currChild = childrenCount;
addState(stateSet, drawable);
filterMap.put(currChild, colorFilter);
}
#Override
public boolean selectDrawable(int idx) {
if (currIdx != idx) {
setColorFilter(getColorFilterForIdx(idx));
}
boolean result = super.selectDrawable(idx);
// check if the drawable has been actually changed to the one I expect
if (getCurrent() != null) {
currIdx = result ? idx : currIdx;
if (!result) {
// it has not been changed, meaning, back to previous filter
setColorFilter(getColorFilterForIdx(currIdx));
}
} else if (getCurrent() == null) {
currIdx = -1;
setColorFilter(null);
}
return result;
}
private ColorFilter getColorFilterForIdx(int idx) {
return filterMap != null ? filterMap.get(idx) : null;
}
}
I've opened a bug about this: https://code.google.com/p/android/issues/detail?id=60183
UPDATE: the bug has been fixed in the framework, since Lollipop I think.
I think the fix commit is this: https://android.googlesource.com/platform/frameworks/base/+/729427d%5E!/
or on Github: https://github.com/android/platform_frameworks_base/commit/729427d451bc4d4d268335b8dc1ff6404bc1c91e
My workaround should still work after Lollipop, it just don't use the fix by Google.
Here is my variation of Mopper's code. The idea is that ImageView gets color filter when user touches it, and color filter is removed when user stops touching it.
class PressedEffectStateListDrawable extends StateListDrawable {
private int selectionColor;
public PressedEffectStateListDrawable(Drawable drawable, int selectionColor) {
super();
this.selectionColor = selectionColor;
addState(new int[] { android.R.attr.state_pressed }, drawable);
addState(new int[] {}, drawable);
}
#Override
protected boolean onStateChange(int[] states) {
boolean isStatePressedInArray = false;
for (int state : states) {
if (state == android.R.attr.state_pressed) {
isStatePressedInArray = true;
}
}
if (isStatePressedInArray) {
super.setColorFilter(selectionColor, PorterDuff.Mode.MULTIPLY);
} else {
super.clearColorFilter();
}
return super.onStateChange(states);
}
#Override
public boolean isStateful() {
return true;
}
}
usage:
Drawable drawable = new FastBitmapDrawable(bm);
imageView.setImageDrawable(new PressedEffectStateListDrawable(drawable, 0xFF33b5e5));
Here is my variation of #Malachiasz code, this lets you pick whatever combination of states and colors to apply to the base drawable.
public class ColorFilteredStateDrawable extends StateListDrawable {
private final int[][] states;
private final int[] colors;
public ColorFilteredStateDrawable(Drawable drawable, int[][] states, int[] colors) {
super();
drawable.mutate();
this.states = states;
this.colors = colors;
for (int i = 0; i < states.length; i++) {
addState(states[i], drawable);
}
}
#Override
protected boolean onStateChange(int[] states) {
if (this.states != null) {
for (int i = 0; i < this.states.length; i++) {
if (StateSet.stateSetMatches(this.states[i], states)) {
super.setColorFilter(this.colors[i], PorterDuff.Mode.MULTIPLY);
return super.onStateChange(states);
}
}
super.clearColorFilter();
}
return super.onStateChange(states);
}
#Override
public boolean isStateful() {
return true;
}
}

Categories