-
Notifications
You must be signed in to change notification settings - Fork 243
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented a scalable control pad (remote) (#428)
* Refactored RemoteFragment and created a compound view for the actual remote. I called it ControlPad to make it more clear what its main function is. * Implemented a custom grid layout (SquareGridLayout) that will always be square. When its width and height are both set to match_parent, it will take the smallest of the two as the actual size. * For devices with a smallest width smaller then 360dp the ControlPad is sized to the maximum available space. For larger devices we still use the old fixed sizes.
- Loading branch information
1 parent
2761c86
commit 3106a5f
Showing
13 changed files
with
1,287 additions
and
591 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
328 changes: 114 additions & 214 deletions
328
app/src/main/java/org/xbmc/kore/ui/sections/remote/RemoteFragment.java
Large diffs are not rendered by default.
Oops, something went wrong.
81 changes: 81 additions & 0 deletions
81
app/src/main/java/org/xbmc/kore/ui/viewgroups/SquareGridLayout.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
/* | ||
* Copyright 2017 Martijn Brekhof. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.xbmc.kore.ui.viewgroups; | ||
|
||
|
||
import android.content.Context; | ||
import android.support.v7.widget.GridLayout; | ||
import android.util.AttributeSet; | ||
import android.view.ViewGroup; | ||
import android.view.ViewParent; | ||
import android.view.ViewTreeObserver; | ||
import android.widget.RelativeLayout; | ||
|
||
import org.xbmc.kore.utils.LogUtils; | ||
|
||
/** | ||
* The square grid layout creates a square layout that will fit inside | ||
* the boundaries provided by the parent layout. | ||
*/ | ||
public class SquareGridLayout extends GridLayout { | ||
|
||
public SquareGridLayout(Context context) { | ||
super(context); | ||
fixForRelativeLayout(); | ||
} | ||
|
||
public SquareGridLayout(Context context, AttributeSet attrs) { | ||
super(context, attrs); | ||
fixForRelativeLayout(); | ||
} | ||
|
||
public SquareGridLayout(Context context, AttributeSet attrs, int defStyle) { | ||
super(context, attrs, defStyle); | ||
fixForRelativeLayout(); | ||
} | ||
|
||
@Override | ||
protected void onMeasure(int widthSpec, int heightSpec) { | ||
int width = MeasureSpec.getSize(widthSpec); | ||
int height = MeasureSpec.getSize(heightSpec); | ||
int size = Math.min(width, height); | ||
|
||
super.onMeasure(MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY), | ||
MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY)); | ||
} | ||
|
||
/** | ||
* When used in a relative layout we need to set the layout parameters to | ||
* the correct size manually. Otherwise the grid layout will be stretched. | ||
*/ | ||
private void fixForRelativeLayout() { | ||
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { | ||
@Override | ||
public void onGlobalLayout() { | ||
ViewGroup.LayoutParams pParams = getLayoutParams(); | ||
|
||
if (pParams instanceof RelativeLayout.LayoutParams) { | ||
int size = Math.min(getWidth(), getHeight()); | ||
pParams.width = size; | ||
pParams.height = size; | ||
setLayoutParams(pParams); | ||
} | ||
|
||
getViewTreeObserver().removeGlobalOnLayoutListener(this); | ||
} | ||
}); | ||
} | ||
} |
240 changes: 240 additions & 0 deletions
240
app/src/main/java/org/xbmc/kore/ui/widgets/ControlPad.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
/* | ||
* Copyright 2017 Martijn Brekhof. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.xbmc.kore.ui.widgets; | ||
|
||
import android.annotation.TargetApi; | ||
import android.content.Context; | ||
import android.content.res.ColorStateList; | ||
import android.content.res.Resources; | ||
import android.content.res.TypedArray; | ||
import android.graphics.BitmapFactory; | ||
import android.graphics.PorterDuff; | ||
import android.graphics.PorterDuffColorFilter; | ||
import android.graphics.drawable.BitmapDrawable; | ||
import android.support.annotation.Nullable; | ||
import android.util.AttributeSet; | ||
import android.view.LayoutInflater; | ||
import android.view.MotionEvent; | ||
import android.view.View; | ||
import android.view.animation.Animation; | ||
import android.view.animation.AnimationUtils; | ||
import android.widget.ImageView; | ||
|
||
import org.xbmc.kore.R; | ||
import org.xbmc.kore.ui.viewgroups.SquareGridLayout; | ||
import org.xbmc.kore.utils.LogUtils; | ||
import org.xbmc.kore.utils.RepeatListener; | ||
import org.xbmc.kore.utils.Utils; | ||
|
||
import butterknife.ButterKnife; | ||
import butterknife.InjectView; | ||
|
||
public class ControlPad extends SquareGridLayout | ||
implements View.OnClickListener, View.OnLongClickListener { | ||
private static final String TAG = LogUtils.makeLogTag(ControlPad.class); | ||
|
||
private static final int initialButtonRepeatInterval = 400; // ms | ||
private static final int buttonRepeatInterval = 80; // ms | ||
|
||
public interface OnPadButtonsListener { | ||
void leftButtonClicked(); | ||
void rightButtonClicked(); | ||
void upButtonClicked(); | ||
void downButtonClicked(); | ||
void selectButtonClicked(); | ||
void backButtonClicked(); | ||
void infoButtonClicked(); | ||
boolean infoButtonLongClicked(); | ||
void contextButtonClicked(); | ||
void osdButtonClicked(); | ||
} | ||
|
||
private OnPadButtonsListener onPadButtonsListener; | ||
|
||
@InjectView(R.id.select) ImageView selectButton; | ||
@InjectView(R.id.left) ImageView leftButton; | ||
@InjectView(R.id.right) ImageView rightButton; | ||
@InjectView(R.id.up) ImageView upButton; | ||
@InjectView(R.id.down) ImageView downButton; | ||
@InjectView(R.id.back) ImageView backButton; | ||
@InjectView(R.id.info) ImageView infoButton; | ||
@InjectView(R.id.context) ImageView contextButton; | ||
@InjectView(R.id.osd) ImageView osdButton; | ||
|
||
public ControlPad(Context context) { | ||
super(context); | ||
initializeView(context); | ||
} | ||
|
||
public ControlPad(Context context, AttributeSet attrs) { | ||
super(context, attrs); | ||
initializeView(context); | ||
} | ||
|
||
public ControlPad(Context context, AttributeSet attrs, int defStyle) { | ||
super(context, attrs, defStyle); | ||
initializeView(context); | ||
} | ||
|
||
@Override | ||
public void setOnClickListener(@Nullable View.OnClickListener l) { | ||
throw new Error("Use setOnPadButtonsListener(listener)"); | ||
} | ||
|
||
@Override | ||
public void setOnLongClickListener(@Nullable OnLongClickListener l) { | ||
throw new Error("Use setOnPadButtonsListener(listener)"); | ||
} | ||
|
||
public void setOnPadButtonsListener(OnPadButtonsListener onPadButtonsListener) { | ||
this.onPadButtonsListener = onPadButtonsListener; | ||
} | ||
|
||
private void initializeView(Context context) { | ||
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); | ||
inflater.inflate(R.layout.remote_control_pad, this); | ||
ButterKnife.inject(this, this); | ||
|
||
applyTheme(); | ||
setupListeners(context); | ||
} | ||
|
||
@Override | ||
public void onClick(View v) { | ||
if (onPadButtonsListener == null) | ||
return; | ||
|
||
switch (v.getId()) { | ||
case R.id.select: | ||
onPadButtonsListener.selectButtonClicked(); | ||
break; | ||
case R.id.left: | ||
onPadButtonsListener.leftButtonClicked(); | ||
break; | ||
case R.id.right: | ||
onPadButtonsListener.rightButtonClicked(); | ||
break; | ||
case R.id.up: | ||
onPadButtonsListener.upButtonClicked(); | ||
break; | ||
case R.id.down: | ||
onPadButtonsListener.downButtonClicked(); | ||
break; | ||
case R.id.back: | ||
onPadButtonsListener.backButtonClicked(); | ||
break; | ||
case R.id.info: | ||
onPadButtonsListener.infoButtonClicked(); | ||
break; | ||
case R.id.context: | ||
onPadButtonsListener.contextButtonClicked(); | ||
break; | ||
case R.id.osd: | ||
onPadButtonsListener.osdButtonClicked(); | ||
break; | ||
default: | ||
LogUtils.LOGD(TAG, "Unknown button "+v.getId()+" clicked"); | ||
} | ||
} | ||
|
||
@Override | ||
public boolean onLongClick(View v) { | ||
if ((onPadButtonsListener != null) && (v.getId() == R.id.info)) { | ||
return onPadButtonsListener.infoButtonLongClicked(); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
@TargetApi(21) | ||
private void applyTheme() { | ||
Resources.Theme theme = getContext().getTheme(); | ||
TypedArray styledAttributes = theme.obtainStyledAttributes(new int[] { | ||
R.attr.remoteButtonColorFilter, | ||
R.attr.contentBackgroundColor}); | ||
Resources resources = getResources(); | ||
int remoteButtonsColor = styledAttributes.getColor(styledAttributes.getIndex(0), resources.getColor(R.color.white)), | ||
remoteBackgroundColor = styledAttributes.getColor(styledAttributes.getIndex(1), resources.getColor(R.color.dark_content_background_dim_70pct)); | ||
styledAttributes.recycle(); | ||
|
||
leftButton.setColorFilter(remoteButtonsColor); | ||
rightButton.setColorFilter(remoteButtonsColor); | ||
upButton.setColorFilter(remoteButtonsColor); | ||
downButton.setColorFilter(remoteButtonsColor); | ||
|
||
selectButton.setColorFilter(remoteButtonsColor); | ||
backButton.setColorFilter(remoteButtonsColor); | ||
infoButton.setColorFilter(remoteButtonsColor); | ||
osdButton.setColorFilter(remoteButtonsColor); | ||
contextButton.setColorFilter(remoteButtonsColor); | ||
|
||
|
||
// On ICS the remote background isn't shown as the tinting isn't supported | ||
//int backgroundResourceId = R.drawable.remote_background_square_black_alpha; | ||
int backgroundResourceId = R.drawable.remote_background_square_black; | ||
if (Utils.isLollipopOrLater()) { | ||
setBackgroundTintList(ColorStateList.valueOf(remoteBackgroundColor)); | ||
setBackgroundResource(backgroundResourceId); | ||
} else if (Utils.isJellybeanOrLater()) { | ||
BitmapDrawable background = new BitmapDrawable(getResources(), | ||
BitmapFactory.decodeResource(getResources(), backgroundResourceId)); | ||
background.setColorFilter(new PorterDuffColorFilter(remoteBackgroundColor, PorterDuff.Mode.SRC_IN)); | ||
setBackground(background); | ||
} | ||
} | ||
|
||
private void setupListeners(Context context) { | ||
final Animation buttonInAnim = AnimationUtils.loadAnimation(context, R.anim.button_in); | ||
final Animation buttonOutAnim = AnimationUtils.loadAnimation(context, R.anim.button_out); | ||
|
||
RepeatListener repeatListener = new RepeatListener(initialButtonRepeatInterval, | ||
buttonRepeatInterval, this, | ||
buttonInAnim, buttonOutAnim, getContext()); | ||
|
||
OnTouchListener feedbackTouchListener = new View.OnTouchListener() { | ||
@Override | ||
public boolean onTouch(View v, MotionEvent event) { | ||
switch (event.getAction()) { | ||
case MotionEvent.ACTION_DOWN: | ||
buttonInAnim.setFillAfter(true); | ||
v.startAnimation(buttonInAnim); | ||
break; | ||
case MotionEvent.ACTION_UP: | ||
case MotionEvent.ACTION_CANCEL: | ||
v.startAnimation(buttonOutAnim); | ||
break; | ||
} | ||
return false; | ||
} | ||
}; | ||
|
||
leftButton.setOnTouchListener(repeatListener); | ||
rightButton.setOnTouchListener(repeatListener); | ||
upButton.setOnTouchListener(repeatListener); | ||
downButton.setOnTouchListener(repeatListener); | ||
setupButton(selectButton, feedbackTouchListener); | ||
setupButton(backButton, feedbackTouchListener); | ||
setupButton(infoButton, feedbackTouchListener); | ||
setupButton(contextButton, feedbackTouchListener); | ||
setupButton(osdButton, feedbackTouchListener); | ||
} | ||
|
||
private void setupButton(View button, OnTouchListener feedbackTouchListener) { | ||
button.setOnTouchListener(feedbackTouchListener); | ||
button.setOnClickListener(this); | ||
button.setOnLongClickListener(this); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.